From a450cb69b5e4549f5515cdb057a68771f56cefd7 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 5 Nov 2021 17:52:08 -0700 Subject: Use file scoped namespaces (#38076) * Use file scoped namespaces --- .../src/AuthenticateResult.cs | 219 +- .../src/AuthenticationHttpContextExtensions.cs | 423 +- .../src/AuthenticationOptions.cs | 157 +- .../src/AuthenticationProperties.cs | 381 +- .../src/AuthenticationScheme.cs | 81 +- .../src/AuthenticationSchemeBuilder.cs | 75 +- .../src/AuthenticationTicket.cs | 103 +- .../src/AuthenticationToken.cs | 25 +- .../src/IAuthenticateResultFeature.cs | 19 +- .../src/IAuthenticationFeature.cs | 25 +- .../src/IAuthenticationHandler.cs | 51 +- .../src/IAuthenticationHandlerProvider.cs | 23 +- .../src/IAuthenticationRequestHandler.cs | 30 +- .../src/IAuthenticationSchemeProvider.cs | 160 +- .../src/IAuthenticationService.cs | 93 +- .../src/IAuthenticationSignInHandler.cs | 23 +- .../src/IAuthenticationSignOutHandler.cs | 22 +- .../src/IClaimsTransformation.cs | 27 +- .../src/TokenExtensions.cs | 265 +- ...uthenticationCoreServiceCollectionExtensions.cs | 74 +- .../src/AuthenticationFeature.cs | 25 +- .../src/AuthenticationHandlerProvider.cs | 83 +- .../src/AuthenticationSchemeProvider.cs | 329 +- .../src/AuthenticationService.cs | 489 ++- .../src/NoopClaimsTransformation.cs | 23 +- .../test/AuthenticationPropertiesTests.cs | 661 ++- .../test/AuthenticationSchemeProviderTests.cs | 343 +- .../test/AuthenticationServiceTests.cs | 699 ++- .../test/AuthenticationTicketTests.cs | 67 +- .../test/TokenExtensionTests.cs | 303 +- src/Http/Headers/src/BaseHeaderParser.cs | 97 +- src/Http/Headers/src/CacheControlHeaderValue.cs | 1377 +++--- .../Headers/src/ContentDispositionHeaderValue.cs | 1245 +++--- ...tentDispositionHeaderValueIdentityExtensions.cs | 57 +- src/Http/Headers/src/ContentRangeHeaderValue.cs | 679 ++- src/Http/Headers/src/CookieHeaderParser.cs | 39 +- src/Http/Headers/src/CookieHeaderValue.cs | 325 +- src/Http/Headers/src/EntityTagHeaderValue.cs | 439 +- src/Http/Headers/src/GenericHeaderParser.cs | 33 +- src/Http/Headers/src/HeaderNames.cs | 403 +- src/Http/Headers/src/HeaderQuality.cs | 25 +- src/Http/Headers/src/HeaderUtilities.cs | 1093 +++-- src/Http/Headers/src/HttpHeaderParser.cs | 239 +- src/Http/Headers/src/MediaTypeHeaderValue.cs | 1289 +++--- .../Headers/src/MediaTypeHeaderValueComparer.cs | 211 +- src/Http/Headers/src/NameValueHeaderValue.cs | 739 ++-- src/Http/Headers/src/ObjectCollection.cs | 109 +- src/Http/Headers/src/RangeConditionHeaderValue.cs | 293 +- src/Http/Headers/src/RangeHeaderValue.cs | 287 +- src/Http/Headers/src/RangeItemHeaderValue.cs | 345 +- src/Http/Headers/src/SameSiteMode.cs | 33 +- src/Http/Headers/src/SetCookieHeaderValue.cs | 1135 +++-- .../Headers/src/StringWithQualityHeaderValue.cs | 407 +- .../src/StringWithQualityHeaderValueComparer.cs | 107 +- .../Headers/test/CacheControlHeaderValueTest.cs | 1013 +++-- .../test/ContentDispositionHeaderValueTest.cs | 1041 +++-- .../Headers/test/ContentRangeHeaderValueTest.cs | 517 ++- src/Http/Headers/test/CookieHeaderValueTest.cs | 467 +- src/Http/Headers/test/DateParserTest.cs | 89 +- src/Http/Headers/test/EntityTagHeaderValueTest.cs | 591 ++- src/Http/Headers/test/HeaderUtilitiesTest.cs | 483 +- .../test/MediaTypeHeaderValueComparerTests.cs | 31 +- src/Http/Headers/test/MediaTypeHeaderValueTest.cs | 1561 ++++--- src/Http/Headers/test/NameValueHeaderValueTest.cs | 921 ++-- .../Headers/test/RangeConditionHeaderValueTest.cs | 323 +- src/Http/Headers/test/RangeHeaderValueTest.cs | 339 +- src/Http/Headers/test/RangeItemHeaderValueTest.cs | 255 +- src/Http/Headers/test/SetCookieHeaderValueTest.cs | 735 ++-- .../StringWithQualityHeaderValueComparerTest.cs | 31 +- .../test/StringWithQualityHeaderValueTest.cs | 535 ++- .../Microbenchmarks/GetHeaderSplitBenchmark.cs | 83 +- .../perf/Microbenchmarks/PathStringBenchmark.cs | 43 +- .../src/BadHttpRequestException.cs | 95 +- src/Http/Http.Abstractions/src/ConnectionInfo.cs | 91 +- src/Http/Http.Abstractions/src/CookieBuilder.cs | 179 +- .../Http.Abstractions/src/CookieSecurePolicy.cs | 49 +- .../src/Extensions/EndpointBuilder.cs | 43 +- .../src/Extensions/HeaderDictionaryExtensions.cs | 89 +- .../Extensions/HttpResponseWritingExtensions.cs | 213 +- .../src/Extensions/IEndpointConventionBuilder.cs | 25 +- .../src/Extensions/MapExtensions.cs | 119 +- .../src/Extensions/MapMiddleware.cs | 117 +- .../Http.Abstractions/src/Extensions/MapOptions.cs | 35 +- .../src/Extensions/MapWhenExtensions.cs | 83 +- .../src/Extensions/MapWhenMiddleware.cs | 89 +- .../src/Extensions/MapWhenOptions.cs | 47 +- .../src/Extensions/RequestTrailerExtensions.cs | 91 +- .../src/Extensions/ResponseTrailerExtensions.cs | 75 +- .../src/Extensions/RunExtensions.cs | 37 +- .../src/Extensions/UseExtensions.cs | 77 +- .../src/Extensions/UseMiddlewareExtensions.cs | 337 +- .../src/Extensions/UsePathBaseExtensions.cs | 47 +- .../src/Extensions/UsePathBaseMiddleware.cs | 103 +- .../src/Extensions/UseWhenExtensions.cs | 91 +- src/Http/Http.Abstractions/src/FragmentString.cs | 265 +- src/Http/Http.Abstractions/src/HostString.cs | 599 ++- src/Http/Http.Abstractions/src/HttpContext.cs | 107 +- src/Http/Http.Abstractions/src/HttpMethods.cs | 353 +- src/Http/Http.Abstractions/src/HttpProtocol.cs | 221 +- src/Http/Http.Abstractions/src/HttpRequest.cs | 245 +- src/Http/Http.Abstractions/src/HttpResponse.cs | 283 +- .../Http.Abstractions/src/IApplicationBuilder.cs | 77 +- src/Http/Http.Abstractions/src/ICorsMetadata.cs | 13 +- .../Http.Abstractions/src/IHttpContextAccessor.cs | 25 +- .../Http.Abstractions/src/IHttpContextFactory.cs | 31 +- src/Http/Http.Abstractions/src/IMiddleware.cs | 23 +- .../Http.Abstractions/src/IMiddlewareFactory.cs | 31 +- src/Http/Http.Abstractions/src/IResult.cs | 21 +- .../src/Internal/HeaderSegment.cs | 87 +- .../src/Internal/HeaderSegmentCollection.cs | 486 +- .../src/Internal/HostStringHelper.cs | 23 +- .../src/Internal/ParsingHelpers.cs | 233 +- .../src/Internal/PathStringHelper.cs | 87 +- .../src/Metadata/IAcceptsMetadata.cs | 35 +- .../src/Metadata/IFromBodyMetadata.cs | 17 +- .../src/Metadata/IFromHeaderMetadata.cs | 17 +- .../src/Metadata/IFromQueryMetadata.cs | 17 +- .../src/Metadata/IFromRouteMetadata.cs | 17 +- .../src/Metadata/IFromServiceMetadata.cs | 13 +- .../src/Metadata/IProducesResponseTypeMetadata.cs | 35 +- .../src/Metadata/ITagsMetadata.cs | 17 +- src/Http/Http.Abstractions/src/PathString.cs | 785 ++-- src/Http/Http.Abstractions/src/QueryString.cs | 459 +- src/Http/Http.Abstractions/src/RequestDelegate.cs | 17 +- .../Http.Abstractions/src/RequestDelegateResult.cs | 40 +- src/Http/Http.Abstractions/src/Routing/Endpoint.cs | 79 +- .../src/Routing/EndpointHttpContextExtensions.cs | 89 +- .../src/Routing/EndpointMetadataCollection.cs | 311 +- .../src/Routing/IEndpointFeature.cs | 21 +- .../src/Routing/IRouteValuesFeature.cs | 21 +- .../src/Routing/RouteValueDictionary.cs | 1127 +++-- src/Http/Http.Abstractions/src/StatusCodes.cs | 667 ++- src/Http/Http.Abstractions/src/WebSocketManager.cs | 65 +- .../Http.Abstractions/test/CookieBuilderTests.cs | 83 +- .../test/EndpointHttpContextExtensionsTests.cs | 249 +- .../test/EndpointMetadataCollectionTests.cs | 91 +- .../Http.Abstractions/test/FragmentStringTests.cs | 55 +- src/Http/Http.Abstractions/test/HostStringTest.cs | 325 +- .../Http.Abstractions/test/HttpMethodslTests.cs | 43 +- .../Http.Abstractions/test/HttpProtocolTests.cs | 213 +- .../test/HttpResponseWritingExtensionsTests.cs | 117 +- .../test/MapPathMiddlewareTests.cs | 463 +- .../test/MapPredicateMiddlewareTests.cs | 193 +- src/Http/Http.Abstractions/test/PathStringTests.cs | 579 ++- .../Http.Abstractions/test/QueryStringTests.cs | 251 +- .../test/RouteValueDictionaryTests.cs | 3249 +++++++------- .../Http.Abstractions/test/UseExtensionsTests.cs | 119 +- .../Http.Abstractions/test/UseMiddlewareTest.cs | 601 ++- .../test/UsePathBaseExtensionsTests.cs | 271 +- .../test/UseWhenExtensionsTests.cs | 235 +- .../src/HeaderDictionaryTypeExtensions.cs | 435 +- .../src/HttpContextServerVariableExtensions.cs | 41 +- .../src/HttpRequestJsonExtensions.cs | 343 +- .../src/HttpRequestMultipartExtensions.cs | 35 +- .../src/HttpResponseJsonExtensions.cs | 337 +- .../src/HttpValidationProblemDetails.cs | 57 +- src/Http/Http.Extensions/src/JsonConstants.cs | 11 +- src/Http/Http.Extensions/src/JsonOptions.cs | 39 +- src/Http/Http.Extensions/src/ProblemDetails.cs | 97 +- src/Http/Http.Extensions/src/QueryBuilder.cs | 179 +- .../Http.Extensions/src/RequestDelegateFactory.cs | 2179 +++++---- .../src/RequestDelegateFactoryOptions.cs | 43 +- src/Http/Http.Extensions/src/RequestHeaders.cs | 683 ++- src/Http/Http.Extensions/src/ResponseExtensions.cs | 79 +- src/Http/Http.Extensions/src/ResponseHeaders.cs | 443 +- .../src/SendFileResponseExtensions.cs | 239 +- src/Http/Http.Extensions/src/SessionExtensions.cs | 117 +- .../Http.Extensions/src/StreamCopyOperation.cs | 49 +- src/Http/Http.Extensions/src/TagsAttribute.cs | 43 +- src/Http/Http.Extensions/src/UriHelper.cs | 453 +- .../test/HeaderDictionaryTypeExtensionsTest.cs | 283 +- .../test/HttpRequestExtensionsTests.cs | 39 +- .../test/HttpRequestJsonExtensionsTests.cs | 391 +- .../test/HttpResponseJsonExtensionsTests.cs | 779 ++-- ...ttpValidationProblemDetailsJsonConverterTest.cs | 237 +- .../test/ParameterBindingMethodCacheTests.cs | 1403 +++--- .../test/ProblemDetailsJsonConverterTest.cs | 295 +- src/Http/Http.Extensions/test/QueryBuilderTests.cs | 139 +- .../test/RequestDelegateFactoryTests.cs | 4619 ++++++++++---------- .../Http.Extensions/test/ResponseExtensionTests.cs | 99 +- .../test/SendFileResponseExtensionsTests.cs | 213 +- src/Http/Http.Extensions/test/TestStream.cs | 91 +- src/Http/Http.Extensions/test/UriHelperTests.cs | 377 +- .../Authentication/IHttpAuthenticationFeature.cs | 17 +- src/Http/Http.Features/src/CookieOptions.cs | 101 +- src/Http/Http.Features/src/HttpsCompressionMode.cs | 37 +- .../src/IBadRequestExceptionFeature.cs | 17 +- src/Http/Http.Features/src/IFormCollection.cs | 157 +- src/Http/Http.Features/src/IFormFeature.cs | 67 +- src/Http/Http.Features/src/IFormFile.cs | 105 +- src/Http/Http.Features/src/IFormFileCollection.cs | 61 +- .../Http.Features/src/IHeaderDictionary.Keyed.cs | 363 +- src/Http/Http.Features/src/IHeaderDictionary.cs | 29 +- .../Http.Features/src/IHttpBodyControlFeature.cs | 17 +- .../Http.Features/src/IHttpConnectionFeature.cs | 49 +- .../src/IHttpMaxRequestBodySizeFeature.cs | 43 +- .../src/IHttpRequestBodyDetectionFeature.cs | 43 +- src/Http/Http.Features/src/IHttpRequestFeature.cs | 149 +- .../src/IHttpRequestIdentifierFeature.cs | 19 +- .../src/IHttpRequestLifetimeFeature.cs | 31 +- .../src/IHttpRequestTrailersFeature.cs | 31 +- src/Http/Http.Features/src/IHttpResetFeature.cs | 19 +- .../Http.Features/src/IHttpResponseBodyFeature.cs | 81 +- src/Http/Http.Features/src/IHttpResponseFeature.cs | 97 +- .../src/IHttpResponseTrailersFeature.cs | 25 +- src/Http/Http.Features/src/IHttpUpgradeFeature.cs | 31 +- .../Http.Features/src/IHttpWebSocketFeature.cs | 31 +- .../Http.Features/src/IHttpsCompressionFeature.cs | 17 +- src/Http/Http.Features/src/IItemsFeature.cs | 17 +- src/Http/Http.Features/src/IQueryCollection.cs | 147 +- src/Http/Http.Features/src/IQueryFeature.cs | 17 +- .../Http.Features/src/IRequestBodyPipeFeature.cs | 17 +- .../Http.Features/src/IRequestCookieCollection.cs | 147 +- .../Http.Features/src/IRequestCookiesFeature.cs | 17 +- src/Http/Http.Features/src/IResponseCookies.cs | 83 +- .../Http.Features/src/IResponseCookiesFeature.cs | 19 +- .../Http.Features/src/IServerVariablesFeature.cs | 21 +- .../Http.Features/src/IServiceProvidersFeature.cs | 17 +- src/Http/Http.Features/src/ISession.cs | 105 +- src/Http/Http.Features/src/ISessionFeature.cs | 19 +- .../Http.Features/src/ITlsConnectionFeature.cs | 25 +- .../Http.Features/src/ITlsTokenBindingFeature.cs | 53 +- .../Http.Features/src/ITrackingConsentFeature.cs | 63 +- src/Http/Http.Features/src/SameSiteMode.cs | 33 +- .../Http.Features/src/WebSocketAcceptContext.cs | 107 +- src/Http/Http.Results/src/AcceptedAtRouteResult.cs | 101 +- src/Http/Http.Results/src/AcceptedResult.cs | 97 +- .../Http.Results/src/BadRequestObjectResult.cs | 11 +- src/Http/Http.Results/src/ChallengeResult.cs | 169 +- src/Http/Http.Results/src/ConflictObjectResult.cs | 11 +- src/Http/Http.Results/src/ContentResult.cs | 101 +- src/Http/Http.Results/src/CreatedAtRouteResult.cs | 101 +- src/Http/Http.Results/src/CreatedResult.cs | 81 +- src/Http/Http.Results/src/FileContentResult.cs | 105 +- src/Http/Http.Results/src/FileResult.cs | 123 +- src/Http/Http.Results/src/FileStreamResult.cs | 123 +- src/Http/Http.Results/src/ForbidResult.cs | 181 +- src/Http/Http.Results/src/IResultExtensions.cs | 15 +- src/Http/Http.Results/src/JsonResult.cs | 107 +- src/Http/Http.Results/src/LocalRedirectResult.cs | 163 +- src/Http/Http.Results/src/NoContentResult.cs | 9 +- src/Http/Http.Results/src/NotFoundObjectResult.cs | 11 +- src/Http/Http.Results/src/ObjectResult.cs | 179 +- src/Http/Http.Results/src/OkObjectResult.cs | 11 +- src/Http/Http.Results/src/PhysicalFileResult.cs | 187 +- src/Http/Http.Results/src/RedirectResult.cs | 117 +- src/Http/Http.Results/src/RedirectToRouteResult.cs | 331 +- src/Http/Http.Results/src/ResultExtensions.cs | 15 +- src/Http/Http.Results/src/Results.cs | 1123 +++-- src/Http/Http.Results/src/SignInResult.cs | 141 +- src/Http/Http.Results/src/SignOutResult.cs | 183 +- src/Http/Http.Results/src/StatusCodeResult.cs | 79 +- src/Http/Http.Results/src/UnauthorizedResult.cs | 9 +- .../src/UnprocessableEntityObjectResult.cs | 11 +- src/Http/Http.Results/src/VirtualFileResult.cs | 171 +- .../test/AcceptedAtRouteResultTests.cs | 141 +- src/Http/Http.Results/test/AcceptedResultTests.cs | 93 +- .../test/BadRequestObjectResultTests.cs | 23 +- src/Http/Http.Results/test/ChallengeResultTest.cs | 93 +- .../Http.Results/test/ConflictObjectResultTest.cs | 23 +- src/Http/Http.Results/test/ContentResultTest.cs | 125 +- .../Http.Results/test/CreatedAtRouteResultTests.cs | 115 +- src/Http/Http.Results/test/CreatedResultTest.cs | 105 +- .../Http.Results/test/FileContentResultTest.cs | 39 +- src/Http/Http.Results/test/FileStreamResultTest.cs | 121 +- src/Http/Http.Results/test/ForbidResultTest.cs | 215 +- .../Http.Results/test/LocalRedirectResultTest.cs | 247 +- .../Http.Results/test/NotFoundObjectResultTest.cs | 87 +- src/Http/Http.Results/test/ObjectResultTests.cs | 295 +- src/Http/Http.Results/test/OkObjectResultTest.cs | 53 +- .../Http.Results/test/PhysicalFileResultTest.cs | 49 +- src/Http/Http.Results/test/RedirectResultTest.cs | 37 +- .../Http.Results/test/RedirectToRouteResultTest.cs | 185 +- src/Http/Http.Results/test/SignInResultTest.cs | 135 +- src/Http/Http.Results/test/SignOutResultTest.cs | 137 +- .../Http.Results/test/StatusCodeResultTests.cs | 51 +- src/Http/Http.Results/test/TestLinkGenerator.cs | 33 +- .../Http.Results/test/UnauthorizedResultTests.cs | 19 +- .../test/UnprocessableEntityObjectResultTests.cs | 23 +- .../Http.Results/test/VirtualFileResultTest.cs | 21 +- .../AdaptiveCapacityDictionaryBenchmark.cs | 489 ++- .../Microbenchmarks/HeaderUtilitiesBenchmark.cs | 23 +- .../Microbenchmarks/QueryCollectionBenchmarks.cs | 145 +- .../RequestCookieCollectionBenchmarks.cs | 27 +- .../RouteValueDictionaryBenchmark.cs | 459 +- src/Http/Http/src/BindingAddress.cs | 353 +- src/Http/Http/src/Builder/ApplicationBuilder.cs | 213 +- src/Http/Http/src/DefaultHttpContext.cs | 393 +- .../src/Extensions/HttpRequestRewindExtensions.cs | 153 +- .../Authentication/HttpAuthenticationFeature.cs | 17 +- ...DefaultConnectionLifetimeNotificationFeature.cs | 43 +- .../Http/src/Features/DefaultSessionFeature.cs | 19 +- src/Http/Http/src/Features/FormFeature.cs | 481 +- src/Http/Http/src/Features/FormOptions.cs | 201 +- .../Http/src/Features/HttpConnectionFeature.cs | 33 +- src/Http/Http/src/Features/HttpRequestFeature.cs | 89 +- .../src/Features/HttpRequestIdentifierFeature.cs | 89 +- .../src/Features/HttpRequestLifetimeFeature.cs | 23 +- src/Http/Http/src/Features/HttpResponseFeature.cs | 73 +- src/Http/Http/src/Features/IHttpActivityFeature.cs | 17 +- src/Http/Http/src/Features/ItemsFeature.cs | 25 +- src/Http/Http/src/Features/QueryFeature.cs | 323 +- .../Http/src/Features/RequestBodyPipeFeature.cs | 65 +- .../Http/src/Features/RequestCookiesFeature.cs | 135 +- .../Http/src/Features/RequestServicesFeature.cs | 131 +- .../Http/src/Features/ResponseCookiesFeature.cs | 77 +- src/Http/Http/src/Features/RouteValuesFeature.cs | 37 +- .../Http/src/Features/ServiceProvidersFeature.cs | 17 +- src/Http/Http/src/Features/TlsConnectionFeature.cs | 25 +- src/Http/Http/src/FormCollection.cs | 327 +- src/Http/Http/src/FormFile.cs | 177 +- src/Http/Http/src/FormFileCollection.cs | 55 +- src/Http/Http/src/HeaderDictionary.cs | 637 ++- src/Http/Http/src/HttpContextAccessor.cs | 57 +- .../Http/src/HttpServiceCollectionExtensions.cs | 31 +- src/Http/Http/src/Internal/BufferingHelper.cs | 69 +- .../Http/src/Internal/DefaultConnectionInfo.cs | 183 +- src/Http/Http/src/Internal/DefaultHttpRequest.cs | 353 +- src/Http/Http/src/Internal/DefaultHttpResponse.cs | 245 +- .../Http/src/Internal/DefaultWebSocketManager.cs | 109 +- src/Http/Http/src/Internal/ItemsDictionary.cs | 213 +- src/Http/Http/src/Internal/ReferenceReadStream.cs | 233 +- .../Http/src/Internal/RequestCookieCollection.cs | 307 +- src/Http/Http/src/Internal/ResponseCookies.cs | 325 +- src/Http/Http/src/MiddlewareFactory.cs | 51 +- src/Http/Http/src/QueryCollection.cs | 361 +- src/Http/Http/src/QueryCollectionInternal.cs | 185 +- src/Http/Http/src/RequestFormReaderExtensions.cs | 65 +- src/Http/Http/src/SendFileFallback.cs | 79 +- src/Http/Http/src/StreamResponseBodyFeature.cs | 223 +- src/Http/Http/test/ApplicationBuilderTests.cs | 139 +- src/Http/Http/test/BindingAddressTests.cs | 181 +- src/Http/Http/test/DefaultHttpContextTests.cs | 805 ++-- src/Http/Http/test/Features/FakeResponseFeature.cs | 29 +- src/Http/Http/test/Features/FormFeatureTests.cs | 995 +++-- .../Features/HttpRequestIdentifierFeatureTests.cs | 53 +- .../Http/test/Features/NonSeekableReadStream.cs | 119 +- src/Http/Http/test/Features/QueryFeatureTests.cs | 367 +- .../test/Features/RequestBodyPipeFeatureTests.cs | 67 +- .../Features/StreamResponseBodyFeatureTests.cs | 133 +- src/Http/Http/test/HeaderDictionaryTests.cs | 161 +- src/Http/Http/test/HttpContextAccessorTests.cs | 243 +- .../test/HttpServiceCollectionExtensionsTests.cs | 37 +- .../Http/test/Internal/DefaultHttpRequestTests.cs | 443 +- .../Http/test/Internal/DefaultHttpResponseTests.cs | 401 +- .../Http/test/Internal/ItemsDictionaryTests.cs | 40 +- .../Http/test/Internal/ReferenceReadStreamTests.cs | 111 +- .../Http/test/RequestCookiesCollectionTests.cs | 77 +- src/Http/Http/test/ResponseCookiesTest.cs | 403 +- src/Http/Metadata/src/IAllowAnonymous.cs | 13 +- src/Http/Metadata/src/IAuthorizeData.cs | 33 +- src/Http/Owin/src/DictionaryStringArrayWrapper.cs | 85 +- src/Http/Owin/src/DictionaryStringValuesWrapper.cs | 149 +- src/Http/Owin/src/IOwinEnvironmentFeature.cs | 17 +- src/Http/Owin/src/OwinConstants.cs | 235 +- src/Http/Owin/src/OwinEnvironment.cs | 615 ++- src/Http/Owin/src/OwinEnvironmentFeature.cs | 17 +- src/Http/Owin/src/OwinExtensions.cs | 321 +- src/Http/Owin/src/OwinFeatureCollection.cs | 675 ++- src/Http/Owin/src/Utilities.cs | 77 +- .../src/WebSockets/OwinWebSocketAcceptAdapter.cs | 225 +- .../src/WebSockets/OwinWebSocketAcceptContext.cs | 81 +- .../Owin/src/WebSockets/OwinWebSocketAdapter.cs | 313 +- .../Owin/src/WebSockets/WebSocketAcceptAdapter.cs | 153 +- src/Http/Owin/src/WebSockets/WebSocketAdapter.cs | 265 +- src/Http/Owin/test/OwinEnvironmentTests.cs | 263 +- src/Http/Owin/test/OwinExtensionTests.cs | 263 +- src/Http/Owin/test/OwinFeatureCollectionTests.cs | 73 +- .../src/IOutboundParameterTransformer.cs | 23 +- .../Routing.Abstractions/src/IParameterPolicy.cs | 13 +- .../Routing.Abstractions/src/IRouteConstraint.cs | 47 +- src/Http/Routing.Abstractions/src/IRouteHandler.cs | 29 +- src/Http/Routing.Abstractions/src/IRouter.cs | 31 +- .../Routing.Abstractions/src/IRoutingFeature.cs | 17 +- src/Http/Routing.Abstractions/src/LinkGenerator.cs | 279 +- src/Http/Routing.Abstractions/src/LinkOptions.cs | 37 +- .../src/Properties/AssemblyInfo.cs | 2 +- src/Http/Routing.Abstractions/src/RouteContext.cs | 75 +- src/Http/Routing.Abstractions/src/RouteData.cs | 455 +- .../Routing.Abstractions/src/RouteDirection.cs | 25 +- .../src/RoutingHttpContextExtensions.cs | 67 +- .../Routing.Abstractions/src/VirtualPathContext.cs | 101 +- .../Routing.Abstractions/src/VirtualPathData.cs | 141 +- .../Routing.Abstractions/test/RouteDataTest.cs | 293 +- .../test/VirtualPathDataTests.cs | 93 +- .../EndpointMetadataCollectionBenchmark.cs | 185 +- .../EndpointRoutingBenchmarkBase.cs | 209 +- .../LinkGenerationGithubBenchmark.cs | 107 +- ...SingleRouteRouteValuesAddressSchemeBenchmark.cs | 97 +- .../SingleRouteWithConstraintsBenchmark.cs | 101 +- .../SingleRouteWithNoParametersBenchmark.cs | 95 +- .../SingleRouteWithParametersBenchmark.cs | 101 +- .../Matching/FastPathTokenizerBenchmarkBase.cs | 43 +- .../Matching/FastPathTokenizerEmptyBenchmark.cs | 39 +- .../Matching/FastPathTokenizerLargeBenchmark.cs | 47 +- .../FastPathTokenizerPlaintextBenchmark.cs | 39 +- .../Matching/FastPathTokenizerSmallBenchmark.cs | 39 +- .../Matching/HttpMethodPolicyJumpTableBenchmark.cs | 79 +- .../Matching/JumpTableMultipleEntryBenchmark.cs | 255 +- .../Matching/JumpTableSingleEntryBenchmark.cs | 191 +- .../Matching/JumpTableZeroEntryBenchmark.cs | 77 +- .../Matching/MatcherAzureBenchmark.cs | 73 +- .../Matching/MatcherBuilderAzureBenchmark.cs | 37 +- .../Matching/MatcherBuilderGithubBenchmark.cs | 37 +- .../MatcherBuilderMultipleEntryBenchmark.cs | 263 +- .../Matching/MatcherGithubBenchmark.cs | 61 +- .../Matching/MatcherSingleEntryBenchmark.cs | 109 +- .../Matching/RouteEndpointAzureBenchmark.cs | 15 +- .../Microbenchmarks/Matching/TrivialMatcher.cs | 65 +- .../Matching/TrivialMatcherBuilder.cs | 23 +- src/Http/Routing/src/ArrayBuilder.cs | 247 +- .../src/Builder/EndpointRouteBuilderExtensions.cs | 899 ++-- .../EndpointRoutingApplicationBuilderExtensions.cs | 247 +- .../FallbackEndpointRouteBuilderExtensions.cs | 157 +- .../OpenApiRouteHandlerBuilderExtensions.cs | 359 +- .../Routing/src/Builder/RouteHandlerBuilder.cs | 71 +- .../src/Builder/RoutingBuilderExtensions.cs | 107 +- .../RoutingEndpointConventionBuilderExtensions.cs | 233 +- .../Routing/src/CompositeEndpointDataSource.cs | 321 +- .../Routing/src/ConfigureRouteHandlerOptions.cs | 25 +- src/Http/Routing/src/ConfigureRouteOptions.cs | 37 +- .../src/Constraints/AlphaRouteConstraint.cs | 19 +- .../Routing/src/Constraints/BoolRouteConstraint.cs | 73 +- .../src/Constraints/CompositeRouteConstraint.cs | 95 +- .../src/Constraints/DateTimeRouteConstraint.cs | 85 +- .../src/Constraints/DecimalRouteConstraint.cs | 73 +- .../src/Constraints/DoubleRouteConstraint.cs | 81 +- .../src/Constraints/FileNameRouteConstraint.cs | 255 +- .../src/Constraints/FloatRouteConstraint.cs | 81 +- .../Routing/src/Constraints/GuidRouteConstraint.cs | 77 +- .../src/Constraints/HttpMethodRouteConstraint.cs | 129 +- .../Routing/src/Constraints/IntRouteConstraint.cs | 73 +- .../src/Constraints/LengthRouteConstraint.cs | 163 +- .../Routing/src/Constraints/LongRouteConstraint.cs | 73 +- .../src/Constraints/MaxLengthRouteConstraint.cs | 93 +- .../Routing/src/Constraints/MaxRouteConstraint.cs | 91 +- .../src/Constraints/MinLengthRouteConstraint.cs | 93 +- .../Routing/src/Constraints/MinRouteConstraint.cs | 91 +- .../src/Constraints/NonFileNameRouteConstraint.cs | 203 +- .../Routing/src/Constraints/NullRouteConstraint.cs | 21 +- .../src/Constraints/OptionalRouteConstraint.cs | 89 +- .../src/Constraints/RangeRouteConstraint.cs | 113 +- .../src/Constraints/RegexInlineRouteConstraint.cs | 21 +- .../src/Constraints/RegexRouteConstraint.cs | 121 +- .../src/Constraints/RequiredRouteConstraint.cs | 63 +- .../src/Constraints/StringRouteConstraint.cs | 79 +- src/Http/Routing/src/DataSourceDependentCache.cs | 125 +- src/Http/Routing/src/DataTokensMetadata.cs | 35 +- .../Routing/src/DecisionTree/DecisionCriterion.cs | 11 +- .../src/DecisionTree/DecisionCriterionValue.cs | 23 +- .../DecisionCriterionValueEqualityComparer.cs | 31 +- .../src/DecisionTree/DecisionTreeBuilder.cs | 353 +- .../Routing/src/DecisionTree/DecisionTreeNode.cs | 23 +- src/Http/Routing/src/DecisionTree/IClassifier.cs | 13 +- .../Routing/src/DecisionTree/ItemDescriptor.cs | 13 +- .../src/DefaultEndpointConventionBuilder.cs | 55 +- src/Http/Routing/src/DefaultEndpointDataSource.cs | 75 +- .../Routing/src/DefaultEndpointRouteBuilder.cs | 23 +- .../Routing/src/DefaultInlineConstraintResolver.cs | 89 +- src/Http/Routing/src/DefaultLinkGenerator.cs | 733 ++-- src/Http/Routing/src/DefaultLinkParser.cs | 285 +- .../Routing/src/DefaultParameterPolicyFactory.cs | 105 +- .../RoutingServiceCollectionExtensions.cs | 203 +- src/Http/Routing/src/EndpointDataSource.cs | 29 +- src/Http/Routing/src/EndpointGroupNameAttribute.cs | 37 +- src/Http/Routing/src/EndpointMiddleware.cs | 149 +- src/Http/Routing/src/EndpointNameAddressScheme.cs | 143 +- src/Http/Routing/src/EndpointNameAttribute.cs | 47 +- src/Http/Routing/src/EndpointNameMetadata.cs | 43 +- src/Http/Routing/src/EndpointRoutingMiddleware.cs | 263 +- .../Routing/src/ExcludeFromDescriptionAttribute.cs | 19 +- src/Http/Routing/src/HostAttribute.cs | 89 +- src/Http/Routing/src/HttpMethodMetadata.cs | 87 +- src/Http/Routing/src/IDataTokenMetadata.cs | 21 +- src/Http/Routing/src/IDynamicEndpointMetadata.cs | 47 +- src/Http/Routing/src/IEndpointAddressScheme.cs | 23 +- src/Http/Routing/src/IEndpointGroupNameMetadata.cs | 17 +- src/Http/Routing/src/IEndpointNameMetadata.cs | 25 +- src/Http/Routing/src/IEndpointRouteBuilder.cs | 37 +- .../Routing/src/IExcludeFromDescriptionMetadata.cs | 21 +- src/Http/Routing/src/IHostMetadata.cs | 21 +- src/Http/Routing/src/IHttpMethodMetadata.cs | 27 +- src/Http/Routing/src/IInlineConstraintResolver.cs | 21 +- src/Http/Routing/src/INamedRouter.cs | 17 +- src/Http/Routing/src/IRouteBuilder.cs | 53 +- src/Http/Routing/src/IRouteCollection.cs | 19 +- src/Http/Routing/src/IRouteNameMetadata.cs | 19 +- .../Routing/src/ISuppressLinkGenerationMetadata.cs | 21 +- src/Http/Routing/src/ISuppressMatchingMetadata.cs | 21 +- src/Http/Routing/src/InlineRouteParameterParser.cs | 415 +- src/Http/Routing/src/Internal/DfaGraphWriter.cs | 139 +- .../LinkGeneratorEndpointNameAddressExtensions.cs | 409 +- .../LinkGeneratorRouteValuesAddressExtensions.cs | 371 +- src/Http/Routing/src/LinkParser.cs | 55 +- .../src/LinkParserEndpointNameAddressExtensions.cs | 77 +- .../Routing/src/MapRouteRouteBuilderExtensions.cs | 265 +- .../src/Matching/AmbiguousMatchException.cs | 29 +- src/Http/Routing/src/Matching/Ascii.cs | 103 +- src/Http/Routing/src/Matching/Candidate.cs | 199 +- src/Http/Routing/src/Matching/CandidateSet.cs | 591 ++- src/Http/Routing/src/Matching/CandidateState.cs | 85 +- .../src/Matching/DataSourceDependentMatcher.cs | 143 +- .../src/Matching/DefaultEndpointSelector.cs | 195 +- src/Http/Routing/src/Matching/DfaMatcher.cs | 615 ++- src/Http/Routing/src/Matching/DfaMatcherBuilder.cs | 1421 +++--- src/Http/Routing/src/Matching/DfaMatcherFactory.cs | 53 +- src/Http/Routing/src/Matching/DfaNode.cs | 175 +- src/Http/Routing/src/Matching/DfaState.cs | 51 +- .../Routing/src/Matching/DictionaryJumpTable.cs | 83 +- src/Http/Routing/src/Matching/EndpointComparer.cs | 219 +- .../src/Matching/EndpointMetadataComparer.cs | 253 +- src/Http/Routing/src/Matching/EndpointSelector.cs | 37 +- src/Http/Routing/src/Matching/FastPathTokenizer.cs | 65 +- src/Http/Routing/src/Matching/HostMatcherPolicy.cs | 685 ++- .../HttpMethodDictionaryPolicyJumpTable.cs | 67 +- .../src/Matching/HttpMethodMatcherPolicy.cs | 779 ++-- .../HttpMethodSingleEntryPolicyJumpTable.cs | 63 +- .../src/Matching/IEndpointComparerPolicy.cs | 49 +- .../src/Matching/IEndpointSelectorPolicy.cs | 77 +- src/Http/Routing/src/Matching/ILEmitTrieFactory.cs | 991 +++-- .../Routing/src/Matching/ILEmitTrieJumpTable.cs | 153 +- .../Routing/src/Matching/INodeBuilderPolicy.cs | 47 +- .../IParameterLiteralNodeMatchingPolicy.cs | 29 +- src/Http/Routing/src/Matching/JumpTable.cs | 17 +- src/Http/Routing/src/Matching/JumpTableBuilder.cs | 151 +- .../Routing/src/Matching/LinearSearchJumpTable.cs | 93 +- src/Http/Routing/src/Matching/Matcher.cs | 23 +- src/Http/Routing/src/Matching/MatcherBuilder.cs | 11 +- src/Http/Routing/src/Matching/MatcherFactory.cs | 9 +- src/Http/Routing/src/Matching/MatcherPolicy.cs | 93 +- src/Http/Routing/src/Matching/PathSegment.cs | 59 +- src/Http/Routing/src/Matching/PolicyJumpTable.cs | 25 +- .../Routing/src/Matching/PolicyJumpTableEdge.cs | 45 +- src/Http/Routing/src/Matching/PolicyNodeEdge.cs | 45 +- .../src/Matching/SingleEntryAsciiJumpTable.cs | 77 +- .../Routing/src/Matching/SingleEntryJumpTable.cs | 77 +- .../Routing/src/Matching/ZeroEntryJumpTable.cs | 35 +- src/Http/Routing/src/ModelEndpointDataSource.cs | 43 +- src/Http/Routing/src/NullRouter.cs | 29 +- src/Http/Routing/src/ParameterPolicyActivator.cs | 273 +- src/Http/Routing/src/ParameterPolicyFactory.cs | 83 +- src/Http/Routing/src/PathTokenizer.cs | 277 +- .../src/Patterns/DefaultRoutePatternTransformer.cs | 367 +- .../Routing/src/Patterns/RouteParameterParser.cs | 427 +- src/Http/Routing/src/Patterns/RoutePattern.cs | 283 +- .../Routing/src/Patterns/RoutePatternException.cs | 77 +- .../Routing/src/Patterns/RoutePatternFactory.cs | 1491 ++++--- .../src/Patterns/RoutePatternLiteralPart.cs | 41 +- .../Routing/src/Patterns/RoutePatternMatcher.cs | 737 ++-- .../src/Patterns/RoutePatternParameterKind.cs | 35 +- .../src/Patterns/RoutePatternParameterPart.cs | 185 +- .../RoutePatternParameterPolicyReference.cs | 53 +- .../Routing/src/Patterns/RoutePatternParser.cs | 809 ++-- src/Http/Routing/src/Patterns/RoutePatternPart.cs | 61 +- .../Routing/src/Patterns/RoutePatternPartKind.cs | 33 +- .../src/Patterns/RoutePatternPathSegment.cs | 67 +- .../src/Patterns/RoutePatternSeparatorPart.cs | 75 +- .../src/Patterns/RoutePatternTransformer.cs | 55 +- .../src/RequestDelegateRouteBuilderExtensions.cs | 521 ++- src/Http/Routing/src/Route.cs | 175 +- src/Http/Routing/src/RouteBase.cs | 539 ++- src/Http/Routing/src/RouteBuilder.cs | 99 +- src/Http/Routing/src/RouteCollection.cs | 269 +- src/Http/Routing/src/RouteConstraintBuilder.cs | 295 +- src/Http/Routing/src/RouteConstraintMatcher.cs | 131 +- src/Http/Routing/src/RouteCreationException.cs | 41 +- src/Http/Routing/src/RouteEndpoint.cs | 87 +- src/Http/Routing/src/RouteEndpointBuilder.cs | 85 +- src/Http/Routing/src/RouteHandler.cs | 65 +- src/Http/Routing/src/RouteHandlerOptions.cs | 27 +- src/Http/Routing/src/RouteNameMetadata.cs | 39 +- src/Http/Routing/src/RouteOptions.cs | 255 +- src/Http/Routing/src/RouteValueEqualityComparer.cs | 77 +- src/Http/Routing/src/RouteValuesAddress.cs | 33 +- src/Http/Routing/src/RouteValuesAddressScheme.cs | 291 +- src/Http/Routing/src/RouterMiddleware.cs | 103 +- src/Http/Routing/src/RoutingFeature.cs | 17 +- src/Http/Routing/src/RoutingMarkerService.cs | 15 +- src/Http/Routing/src/SegmentState.cs | 23 +- .../Routing/src/SuppressLinkGenerationMetadata.cs | 21 +- src/Http/Routing/src/SuppressMatchingMetadata.cs | 19 +- .../src/Template/DefaultTemplateBinderFactory.cs | 109 +- src/Http/Routing/src/Template/InlineConstraint.cs | 57 +- src/Http/Routing/src/Template/RoutePrecedence.cs | 397 +- src/Http/Routing/src/Template/RouteTemplate.cs | 205 +- src/Http/Routing/src/Template/TemplateBinder.cs | 1197 +++-- .../Routing/src/Template/TemplateBinderFactory.cs | 37 +- src/Http/Routing/src/Template/TemplateMatcher.cs | 139 +- src/Http/Routing/src/Template/TemplateParser.cs | 45 +- src/Http/Routing/src/Template/TemplatePart.cs | 295 +- src/Http/Routing/src/Template/TemplateSegment.cs | 91 +- .../Routing/src/Template/TemplateValuesResult.cs | 41 +- src/Http/Routing/src/Tree/InboundMatch.cs | 33 +- src/Http/Routing/src/Tree/InboundRouteEntry.cs | 91 +- .../Routing/src/Tree/LinkGenerationDecisionTree.cs | 397 +- src/Http/Routing/src/Tree/OutboundMatch.cs | 25 +- src/Http/Routing/src/Tree/OutboundMatchResult.cs | 19 +- src/Http/Routing/src/Tree/OutboundRouteEntry.cs | 97 +- src/Http/Routing/src/Tree/TreeEnumerator.cs | 141 +- src/Http/Routing/src/Tree/TreeRouteBuilder.cs | 403 +- src/Http/Routing/src/Tree/TreeRouter.cs | 489 ++- src/Http/Routing/src/Tree/UrlMatchingNode.cs | 113 +- src/Http/Routing/src/Tree/UrlMatchingTree.cs | 293 +- .../src/UriBuilderContextPooledObjectPolicy.cs | 21 +- src/Http/Routing/src/UriBuildingContext.cs | 473 +- .../Benchmarks/EndpointRoutingBenchmarkTest.cs | 85 +- .../Benchmarks/RouterBenchmarkTest.cs | 87 +- .../EndpointRoutingIntegrationTest.cs | 587 ++- .../FunctionalTests/EndpointRoutingSampleTest.cs | 445 +- .../test/FunctionalTests/HostMatchingTests.cs | 209 +- .../test/FunctionalTests/MapFallbackTest.cs | 185 +- .../test/FunctionalTests/RouteHandlerTest.cs | 85 +- .../test/FunctionalTests/RouterSampleTest.cs | 179 +- .../test/FunctionalTests/RoutingTestFixture.cs | 75 +- .../WebHostBuilderExtensionsTest.cs | 71 +- ...pointRoutingApplicationBuilderExtensionsTest.cs | 567 ++- ...stDelegateEndpointRouteBuilderExtensionsTest.cs | 307 +- ...uteHandlerEndpointRouteBuilderExtensionsTest.cs | 1385 +++--- .../Builder/RoutingBuilderExtensionsTest.cs | 209 +- ...utingEndpointConventionBuilderExtensionsTest.cs | 265 +- .../UnitTests/CompositeEndpointDataSourceTest.cs | 297 +- .../test/UnitTests/ConstraintMatcherTest.cs | 347 +- .../Constraints/AlphaRouteConstraintTests.cs | 39 +- .../Constraints/BoolRouteConstraintTests.cs | 45 +- .../Constraints/CompositeRouteConstraintTests.cs | 71 +- .../UnitTests/Constraints/ConstraintsTestHelper.cs | 17 +- .../Constraints/DateTimeRouteConstraintTests.cs | 65 +- .../Constraints/DecimalRouteConstraintTests.cs | 47 +- .../Constraints/DoubleRouteConstraintTests.cs | 35 +- .../Constraints/FIleNameRouteConstraintTest.cs | 97 +- .../Constraints/FloatRouteConstraintTests.cs | 33 +- .../Constraints/GuidRouteConstraintTests.cs | 41 +- .../Constraints/HttpMethodRouteConstraintTests.cs | 165 +- .../Constraints/IntRouteConstraintsTests.cs | 33 +- .../Constraints/LengthRouteConstraintTests.cs | 183 +- .../Constraints/LongRouteConstraintTests.cs | 37 +- .../Constraints/MaxLengthRouteConstraintTests.cs | 55 +- .../Constraints/MaxRouteConstraintTests.cs | 29 +- .../Constraints/MinLengthRouteConstraintTests.cs | 55 +- .../Constraints/MinRouteConstraintTests.cs | 29 +- .../Constraints/NonFIleNameRouteConstraintTest.cs | 99 +- .../Constraints/RangeRouteConstraintTests.cs | 65 +- .../Constraints/RegexInlineRouteConstraintTests.cs | 125 +- .../Constraints/RegexRouteConstraintTests.cs | 203 +- .../Constraints/RequiredRouteConstraintTests.cs | 139 +- .../Constraints/StringRouteConstraintTest.cs | 291 +- .../test/UnitTests/DataSourceDependentCacheTest.cs | 203 +- .../test/UnitTests/DecisionTreeBuilderTest.cs | 315 +- .../UnitTests/DefaultEndpointDataSourceTests.cs | 167 +- .../DefaultInlineConstraintResolverTest.cs | 651 ++- .../DefaultLinkGeneratorProcessTemplateTest.cs | 2869 ++++++------ .../test/UnitTests/DefaultLinkGeneratorTest.cs | 1229 +++--- .../test/UnitTests/DefaultLinkParserTest.cs | 263 +- .../UnitTests/DefaultParameterPolicyFactoryTest.cs | 797 ++-- src/Http/Routing/test/UnitTests/EndpointFactory.cs | 53 +- .../test/UnitTests/EndpointMiddlewareTest.cs | 437 +- .../UnitTests/EndpointNameAddressSchemeTest.cs | 285 +- .../UnitTests/EndpointRoutingMiddlewareTest.cs | 305 +- .../UnitTests/InlineRouteParameterParserTests.cs | 1555 ++++--- .../test/UnitTests/Internal/DfaGraphWriterTest.cs | 115 +- .../LinkGeneratorEndpointNameExtensionsTest.cs | 273 +- .../test/UnitTests/LinkGeneratorIntegrationTest.cs | 1003 +++-- ...inkGeneratorRouteValuesAddressExtensionsTest.cs | 377 +- .../test/UnitTests/LinkGeneratorTestBase.cs | 113 +- .../LinkParserEndpointNameExtensionsTest.cs | 71 +- .../Routing/test/UnitTests/LinkParserTestBase.cs | 97 +- .../Routing/test/UnitTests/Logging/WriteContext.cs | 23 +- .../Routing/test/UnitTests/MatcherPolicyTest.cs | 111 +- .../UnitTests/Matching/AcceptsMatcherPolicyTest.cs | 813 ++-- .../Routing/test/UnitTests/Matching/AsciiTest.cs | 207 +- .../test/UnitTests/Matching/BarebonesMatcher.cs | 167 +- .../UnitTests/Matching/BarebonesMatcherBuilder.cs | 37 +- .../Matching/BarebonesMatcherConformanceTest.cs | 81 +- .../test/UnitTests/Matching/CandidateSetTest.cs | 609 ++- .../Matching/DataSourceDependentMatcherTest.cs | 457 +- .../Matching/DefaultEndpointSelectorTest.cs | 281 +- .../UnitTests/Matching/DfaMatcherBuilderTest.cs | 3815 ++++++++-------- .../Matching/DfaMatcherConformanceTest.cs | 97 +- .../test/UnitTests/Matching/DfaMatcherTest.cs | 1535 ++++--- .../UnitTests/Matching/DictionaryJumpTableTest.cs | 17 +- .../UnitTests/Matching/EndpointComparerTest.cs | 499 ++- .../Matching/EndpointMetadataComparerTest.cs | 159 +- .../UnitTests/Matching/FastPathTokenizerTest.cs | 245 +- .../Matching/FullFeaturedMatcherConformanceTest.cs | 867 ++-- ...PolicyIEndpointSelectorPolicyIntegrationTest.cs | 11 +- ...tcherPolicyINodeBuilderPolicyIntegrationTest.cs | 11 +- .../HostMatcherPolicyIntegrationTestBase.cs | 653 ++- .../UnitTests/Matching/HostMatcherPolicyTest.cs | 397 +- ...PolicyIEndpointSelectorPolicyIntegrationTest.cs | 11 +- ...tcherPolicyINodeBuilderPolicyIntegrationTest.cs | 11 +- .../HttpMethodMatcherPolicyIntegrationTestBase.cs | 593 ++- .../Matching/HttpMethodMatcherPolicyTest.cs | 565 ++- .../UnitTests/Matching/ILEmitTrieFactoryTest.cs | 51 +- .../UnitTests/Matching/ILEmitTrieJumpTableTest.cs | 431 +- .../Matching/LinearSearchJumpTableTest.cs | 17 +- .../test/UnitTests/Matching/MatcherAssert.cs | 147 +- .../MatcherConformanceTest.MultipleEndpoint.cs | 7 +- .../MatcherConformanceTest.SingleEndpoint.cs | 625 ++- .../UnitTests/Matching/MatcherConformanceTest.cs | 73 +- .../Matching/MultipleEntryJumpTableTest.cs | 141 +- .../NonVectorizedILEmitTrieJumpTableTest.cs | 9 +- .../test/UnitTests/Matching/RouteMatcher.cs | 39 +- .../test/UnitTests/Matching/RouteMatcherBuilder.cs | 145 +- .../Matching/RouteMatcherConformanceTest.cs | 55 +- .../Matching/SingleEntryAsciiJumpTableTest.cs | 11 +- .../UnitTests/Matching/SingleEntryJumpTableTest.cs | 11 +- .../Matching/SingleEntryJumpTableTestBase.cs | 95 +- .../test/UnitTests/Matching/TreeRouterMatcher.cs | 39 +- .../UnitTests/Matching/TreeRouterMatcherBuilder.cs | 139 +- .../Matching/TreeRouterMatcherConformanceTest.cs | 79 +- .../Matching/VectorizedILEmitTrieJumpTableTest.cs | 13 +- .../UnitTests/Matching/ZeroEntryJumpTableTest.cs | 53 +- .../Routing/test/UnitTests/PathTokenizerTest.cs | 75 +- .../Patterns/DefaultRoutePatternTransformerTest.cs | 781 ++-- .../Patterns/InlineRouteParameterParserTest.cs | 2129 +++++---- .../UnitTests/Patterns/RoutePatternFactoryTest.cs | 1291 +++--- .../UnitTests/Patterns/RoutePatternMatcherTest.cs | 1851 ++++---- .../UnitTests/Patterns/RoutePatternParserTest.cs | 1253 +++--- .../RequestDelegateRouteBuilderExtensionsTest.cs | 157 +- .../Routing/test/UnitTests/RouteBuilderTest.cs | 81 +- .../Routing/test/UnitTests/RouteCollectionTest.cs | 1099 +++-- .../test/UnitTests/RouteConstraintBuilderTest.cs | 347 +- .../test/UnitTests/RouteEndpointBuilderTest.cs | 39 +- .../test/UnitTests/RouteHandlerOptionsTests.cs | 105 +- .../Routing/test/UnitTests/RouteOptionsTests.cs | 107 +- src/Http/Routing/test/UnitTests/RouteTest.cs | 3261 +++++++------- .../UnitTests/RouteValueEqualityComparerTest.cs | 61 +- .../test/UnitTests/RouteValuesAddressSchemeTest.cs | 853 ++-- .../Routing/test/UnitTests/RouterMiddlewareTest.cs | 207 +- ...tingEndpointConventionBuilderExtensionsTests.cs | 45 +- .../Template/RoutePatternPrecedenceTests.cs | 63 +- .../UnitTests/Template/RoutePrecedenceTestsBase.cs | 217 +- .../Template/RouteTemplatePrecedenceTests.cs | 29 +- .../test/UnitTests/Template/TemplateBinderTests.cs | 1597 ++++--- .../UnitTests/Template/TemplateMatcherTests.cs | 1849 ++++---- .../test/UnitTests/Template/TemplateParserTests.cs | 1623 ++++--- .../test/UnitTests/Template/TemplateSegmentTest.cs | 67 +- .../UnitTests/TemplateParserDefaultValuesTests.cs | 251 +- src/Http/Routing/test/UnitTests/TestConstants.cs | 9 +- .../UnitTests/TestObjects/CapturingConstraint.cs | 27 +- .../TestObjects/DynamicEndpointDataSource.cs | 85 +- .../TestObjects/SlugifyParameterTransformer.cs | 13 +- .../test/UnitTests/TestObjects/TestMatcher.cs | 29 +- .../UnitTests/TestObjects/TestMatcherFactory.cs | 25 +- .../UnitTests/TestObjects/TestServiceProvider.cs | 13 +- .../TestObjects/UpperCaseParameterTransform.cs | 11 +- .../Tree/LinkGenerationDecisionTreeTest.cs | 1121 +++-- .../test/UnitTests/Tree/TreeRouteBuilderTest.cs | 413 +- .../Routing/test/UnitTests/Tree/TreeRouterTest.cs | 3231 +++++++------- .../test/UnitTests/UriBuildingContextTest.cs | 157 +- .../Routing/test/testassets/Benchmarks/Program.cs | 91 +- .../Benchmarks/StartupUsingEndpointRouting.cs | 35 +- .../testassets/Benchmarks/StartupUsingRouter.cs | 43 +- .../Framework/FrameworkConfigurationBuilder.cs | 47 +- .../Framework/FrameworkEndpointDataSource.cs | 121 +- .../FrameworkEndpointRouteBuilderExtensions.cs | 33 +- .../EndpointRouteBuilderExtensions.cs | 23 +- .../HelloExtension/HelloAppBuilderExtensions.cs | 23 +- .../HelloExtension/HelloMiddleware.cs | 51 +- .../RoutingSandbox/HelloExtension/HelloOptions.cs | 9 +- .../test/testassets/RoutingSandbox/Program.cs | 109 +- .../RoutingSandbox/SlugifyParameterTransformer.cs | 13 +- .../RoutingSandbox/UseEndpointRoutingStartup.cs | 171 +- .../testassets/RoutingSandbox/UseRouterStartup.cs | 47 +- .../EndsWithStringRouteConstraint.cs | 33 +- .../EndpointRouteBuilderExtensions.cs | 23 +- .../HelloExtension/HelloAppBuilderExtensions.cs | 23 +- .../HelloExtension/HelloMiddleware.cs | 51 +- .../RoutingWebSite/HelloExtension/HelloOptions.cs | 9 +- .../RoutingWebSite/MapFallbackStartup.cs | 39 +- .../test/testassets/RoutingWebSite/Program.cs | 111 +- .../RoutingWebSite/UseEndpointRoutingStartup.cs | 267 +- .../testassets/RoutingWebSite/UseRouterStartup.cs | 63 +- src/Http/Routing/tools/Swaggatherer/Program.cs | 13 +- src/Http/Routing/tools/Swaggatherer/RouteEntry.cs | 15 +- .../tools/Swaggatherer/SwaggathererApplication.cs | 355 +- src/Http/Routing/tools/Swaggatherer/Template.cs | 103 +- src/Http/Shared/CookieHeaderParserShared.cs | 341 +- src/Http/Shared/HttpParseResult.cs | 13 +- src/Http/Shared/HttpRuleParser.cs | 495 ++- src/Http/Shared/StreamCopyOperationInternal.cs | 117 +- .../FormPipeReaderInternalsBenchmark.cs | 65 +- .../perf/Microbenchmarks/FormReaderBenchmark.cs | 53 +- .../HttpRequestStreamReaderReadLineBenchmark.cs | 83 +- .../WebUtilities/src/AspNetCoreTempDirectory.cs | 39 +- src/Http/WebUtilities/src/Base64UrlTextEncoder.cs | 45 +- src/Http/WebUtilities/src/BufferedReadStream.cs | 657 ++- .../WebUtilities/src/FileBufferingReadStream.cs | 761 ++-- .../WebUtilities/src/FileBufferingWriteStream.cs | 479 +- src/Http/WebUtilities/src/FileMultipartSection.cs | 95 +- src/Http/WebUtilities/src/FormMultipartSection.cs | 91 +- src/Http/WebUtilities/src/FormPipeReader.cs | 637 ++- src/Http/WebUtilities/src/FormReader.cs | 527 ++- .../WebUtilities/src/HttpRequestStreamReader.cs | 831 ++-- .../WebUtilities/src/HttpResponseStreamWriter.cs | 889 ++-- src/Http/WebUtilities/src/KeyValueAccumulator.cs | 149 +- src/Http/WebUtilities/src/MultipartBoundary.cs | 95 +- src/Http/WebUtilities/src/MultipartReader.cs | 227 +- src/Http/WebUtilities/src/MultipartReaderStream.cs | 481 +- src/Http/WebUtilities/src/MultipartSection.cs | 73 +- .../src/MultipartSectionConverterExtensions.cs | 101 +- .../src/MultipartSectionStreamExtensions.cs | 65 +- src/Http/WebUtilities/src/PagedByteBuffer.cs | 191 +- src/Http/WebUtilities/src/QueryHelpers.cs | 281 +- src/Http/WebUtilities/src/ReasonPhrases.cs | 161 +- .../WebUtilities/src/StreamHelperExtensions.cs | 123 +- .../test/AspNetCoreTempDirectoryTests.cs | 17 +- .../test/FileBufferingReadStreamTests.cs | 873 ++-- .../test/FileBufferingWriteStreamTests.cs | 611 ++- src/Http/WebUtilities/test/FormPipeReaderTests.cs | 925 ++-- src/Http/WebUtilities/test/FormReaderAsyncTest.cs | 21 +- src/Http/WebUtilities/test/FormReaderTests.cs | 341 +- .../test/HttpRequestStreamReaderTest.cs | 815 ++-- .../test/HttpResponseStreamWriterTest.cs | 1155 +++-- src/Http/WebUtilities/test/MultipartReaderTests.cs | 549 ++- .../WebUtilities/test/NonSeekableReadStream.cs | 123 +- src/Http/WebUtilities/test/PagedByteBufferTest.cs | 389 +- src/Http/WebUtilities/test/QueryHelpersTests.cs | 359 +- src/Http/WebUtilities/test/WebEncodersTests.cs | 91 +- src/Http/samples/SampleApp/Program.cs | 15 +- 820 files changed, 96136 insertions(+), 96956 deletions(-) (limited to 'src/Http') diff --git a/src/Http/Authentication.Abstractions/src/AuthenticateResult.cs b/src/Http/Authentication.Abstractions/src/AuthenticateResult.cs index e55e557309..9e3448c0c5 100644 --- a/src/Http/Authentication.Abstractions/src/AuthenticateResult.cs +++ b/src/Http/Authentication.Abstractions/src/AuthenticateResult.cs @@ -5,130 +5,129 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Security.Claims; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Contains the result of an Authenticate call +/// +public class AuthenticateResult { /// - /// Contains the result of an Authenticate call + /// Creates a new instance. + /// + protected AuthenticateResult() { } + + /// + /// If a ticket was produced, authenticate was successful. + /// + [MemberNotNullWhen(true, nameof(Ticket), nameof(Principal), nameof(Properties))] + public bool Succeeded => Ticket != null; + + /// + /// The authentication ticket. + /// + public AuthenticationTicket? Ticket { get; protected set; } + + /// + /// Gets the claims-principal with authenticated user identities. + /// + public ClaimsPrincipal? Principal => Ticket?.Principal; + + /// + /// Additional state values for the authentication session. /// - public class AuthenticateResult + public AuthenticationProperties? Properties { get; protected set; } + + /// + /// Holds failure information from the authentication. + /// + public Exception? Failure { get; protected set; } + + /// + /// Indicates that there was no information returned for this authentication scheme. + /// + public bool None { get; protected set; } + + /// + /// Create a new deep copy of the result + /// + /// A copy of the result + public AuthenticateResult Clone() { - /// - /// Creates a new instance. - /// - protected AuthenticateResult() { } - - /// - /// If a ticket was produced, authenticate was successful. - /// - [MemberNotNullWhen(true, nameof(Ticket), nameof(Principal), nameof(Properties))] - public bool Succeeded => Ticket != null; - - /// - /// The authentication ticket. - /// - public AuthenticationTicket? Ticket { get; protected set; } - - /// - /// Gets the claims-principal with authenticated user identities. - /// - public ClaimsPrincipal? Principal => Ticket?.Principal; - - /// - /// Additional state values for the authentication session. - /// - public AuthenticationProperties? Properties { get; protected set; } - - /// - /// Holds failure information from the authentication. - /// - public Exception? Failure { get; protected set; } - - /// - /// Indicates that there was no information returned for this authentication scheme. - /// - public bool None { get; protected set; } - - /// - /// Create a new deep copy of the result - /// - /// A copy of the result - public AuthenticateResult Clone() + if (None) { - if (None) - { - return NoResult(); - } - if (Failure != null) - { - return Fail(Failure, Properties?.Clone()); - } - if (Succeeded) - { - return Success(Ticket!.Clone()); - } - // This shouldn't happen - throw new NotImplementedException(); + return NoResult(); } - - /// - /// Indicates that authentication was successful. - /// - /// The ticket representing the authentication result. - /// The result. - public static AuthenticateResult Success(AuthenticationTicket ticket) + if (Failure != null) { - if (ticket == null) - { - throw new ArgumentNullException(nameof(ticket)); - } - return new AuthenticateResult() { Ticket = ticket, Properties = ticket.Properties }; + return Fail(Failure, Properties?.Clone()); } - - /// - /// Indicates that there was no information returned for this authentication scheme. - /// - /// The result. - public static AuthenticateResult NoResult() + if (Succeeded) { - return new AuthenticateResult() { None = true }; + return Success(Ticket!.Clone()); } + // This shouldn't happen + throw new NotImplementedException(); + } - /// - /// Indicates that there was a failure during authentication. - /// - /// The failure exception. - /// The result. - public static AuthenticateResult Fail(Exception failure) + /// + /// Indicates that authentication was successful. + /// + /// The ticket representing the authentication result. + /// The result. + public static AuthenticateResult Success(AuthenticationTicket ticket) + { + if (ticket == null) { - return new AuthenticateResult() { Failure = failure }; + throw new ArgumentNullException(nameof(ticket)); } + return new AuthenticateResult() { Ticket = ticket, Properties = ticket.Properties }; + } - /// - /// Indicates that there was a failure during authentication. - /// - /// The failure exception. - /// Additional state values for the authentication session. - /// The result. - public static AuthenticateResult Fail(Exception failure, AuthenticationProperties? properties) - { - return new AuthenticateResult() { Failure = failure, Properties = properties }; - } + /// + /// Indicates that there was no information returned for this authentication scheme. + /// + /// The result. + public static AuthenticateResult NoResult() + { + return new AuthenticateResult() { None = true }; + } + + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure exception. + /// The result. + public static AuthenticateResult Fail(Exception failure) + { + return new AuthenticateResult() { Failure = failure }; + } - /// - /// Indicates that there was a failure during authentication. - /// - /// The failure message. - /// The result. - public static AuthenticateResult Fail(string failureMessage) - => Fail(new Exception(failureMessage)); - - /// - /// Indicates that there was a failure during authentication. - /// - /// The failure message. - /// Additional state values for the authentication session. - /// The result. - public static AuthenticateResult Fail(string failureMessage, AuthenticationProperties? properties) - => Fail(new Exception(failureMessage), properties); + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure exception. + /// Additional state values for the authentication session. + /// The result. + public static AuthenticateResult Fail(Exception failure, AuthenticationProperties? properties) + { + return new AuthenticateResult() { Failure = failure, Properties = properties }; } + + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure message. + /// The result. + public static AuthenticateResult Fail(string failureMessage) + => Fail(new Exception(failureMessage)); + + /// + /// Indicates that there was a failure during authentication. + /// + /// The failure message. + /// Additional state values for the authentication session. + /// The result. + public static AuthenticateResult Fail(string failureMessage, AuthenticationProperties? properties) + => Fail(new Exception(failureMessage), properties); } diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationHttpContextExtensions.cs b/src/Http/Authentication.Abstractions/src/AuthenticationHttpContextExtensions.cs index 5e6f4c8519..e6feefba0b 100644 --- a/src/Http/Authentication.Abstractions/src/AuthenticationHttpContextExtensions.cs +++ b/src/Http/Authentication.Abstractions/src/AuthenticationHttpContextExtensions.cs @@ -6,218 +6,217 @@ using Microsoft.AspNetCore.Authentication.Abstractions; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Extension methods to expose Authentication on HttpContext. +/// +public static class AuthenticationHttpContextExtensions { /// - /// Extension methods to expose Authentication on HttpContext. - /// - public static class AuthenticationHttpContextExtensions - { - /// - /// Authenticate the current request using the default authentication scheme. - /// The default authentication scheme can be configured using . - /// - /// The context. - /// The . - public static Task AuthenticateAsync(this HttpContext context) => - context.AuthenticateAsync(scheme: null); - - /// - /// Authenticate the current request using the specified scheme. - /// - /// The context. - /// The name of the authentication scheme. - /// The . - public static Task AuthenticateAsync(this HttpContext context, string? scheme) => - GetAuthenticationService(context).AuthenticateAsync(context, scheme); - - /// - /// Challenge the current request using the specified scheme. - /// An authentication challenge can be issued when an unauthenticated user requests an endpoint that requires authentication. - /// - /// The context. - /// The name of the authentication scheme. - /// The result. - public static Task ChallengeAsync(this HttpContext context, string? scheme) => - context.ChallengeAsync(scheme, properties: null); - - /// - /// Challenge the current request using the default challenge scheme. - /// An authentication challenge can be issued when an unauthenticated user requests an endpoint that requires authentication. - /// The default challenge scheme can be configured using . - /// - /// The context. - /// The task. - public static Task ChallengeAsync(this HttpContext context) => - context.ChallengeAsync(scheme: null, properties: null); - - /// - /// Challenge the current request using the default challenge scheme. - /// An authentication challenge can be issued when an unauthenticated user requests an endpoint that requires authentication. - /// The default challenge scheme can be configured using . - /// - /// The context. - /// The properties. - /// The task. - public static Task ChallengeAsync(this HttpContext context, AuthenticationProperties? properties) => - context.ChallengeAsync(scheme: null, properties: properties); - - /// - /// Challenge the current request using the specified scheme. - /// An authentication challenge can be issued when an unauthenticated user requests an endpoint that requires authentication. - /// - /// The context. - /// The name of the authentication scheme. - /// The properties. - /// The task. - public static Task ChallengeAsync(this HttpContext context, string? scheme, AuthenticationProperties? properties) => - GetAuthenticationService(context).ChallengeAsync(context, scheme, properties); - - /// - /// Forbid the current request using the specified scheme. - /// Forbid is used when an authenticated user attempts to access a resource they are not permitted to access. - /// - /// The context. - /// The name of the authentication scheme. - /// The task. - public static Task ForbidAsync(this HttpContext context, string? scheme) => - context.ForbidAsync(scheme, properties: null); - - /// - /// Forbid the current request using the default forbid scheme. - /// Forbid is used when an authenticated user attempts to access a resource they are not permitted to access. - /// The default forbid scheme can be configured using . - /// - /// The context. - /// The task. - public static Task ForbidAsync(this HttpContext context) => - context.ForbidAsync(scheme: null, properties: null); - - /// - /// Forbid the current request using the default forbid scheme. - /// Forbid is used when an authenticated user attempts to access a resource they are not permitted to access. - /// The default forbid scheme can be configured using . - /// - /// The context. - /// The properties. - /// The task. - public static Task ForbidAsync(this HttpContext context, AuthenticationProperties? properties) => - context.ForbidAsync(scheme: null, properties: properties); - - /// - /// Forbid the current request using the specified scheme. - /// Forbid is used when an authenticated user attempts to access a resource they are not permitted to access. - /// - /// The context. - /// The name of the authentication scheme. - /// The properties. - /// The task. - public static Task ForbidAsync(this HttpContext context, string? scheme, AuthenticationProperties? properties) => - GetAuthenticationService(context).ForbidAsync(context, scheme, properties); - - /// - /// Sign in a principal for the specified scheme. - /// - /// The context. - /// The name of the authentication scheme. - /// The user. - /// The task. - public static Task SignInAsync(this HttpContext context, string? scheme, ClaimsPrincipal principal) => - context.SignInAsync(scheme, principal, properties: null); - - /// - /// Sign in a principal for the default authentication scheme. - /// The default scheme for signing in can be configured using . - /// - /// The context. - /// The user. - /// The task. - public static Task SignInAsync(this HttpContext context, ClaimsPrincipal principal) => - context.SignInAsync(scheme: null, principal: principal, properties: null); - - /// - /// Sign in a principal for the default authentication scheme. - /// The default scheme for signing in can be configured using . - /// - /// The context. - /// The user. - /// The properties. - /// The task. - public static Task SignInAsync(this HttpContext context, ClaimsPrincipal principal, AuthenticationProperties? properties) => - context.SignInAsync(scheme: null, principal: principal, properties: properties); - - /// - /// Sign in a principal for the specified scheme. - /// - /// The context. - /// The name of the authentication scheme. - /// The user. - /// The properties. - /// The task. - public static Task SignInAsync(this HttpContext context, string? scheme, ClaimsPrincipal principal, AuthenticationProperties? properties) => - GetAuthenticationService(context).SignInAsync(context, scheme, principal, properties); - - /// - /// Sign out a principal for the default authentication scheme. - /// The default scheme for signing out can be configured using . - /// - /// The context. - /// The task. - public static Task SignOutAsync(this HttpContext context) => context.SignOutAsync(scheme: null, properties: null); - - /// - /// Sign out a principal for the default authentication scheme. - /// The default scheme for signing out can be configured using . - /// - /// The context. - /// The properties. - /// The task. - public static Task SignOutAsync(this HttpContext context, AuthenticationProperties? properties) => context.SignOutAsync(scheme: null, properties: properties); - - /// - /// Sign out a principal for the specified scheme. - /// - /// The context. - /// The name of the authentication scheme. - /// The task. - public static Task SignOutAsync(this HttpContext context, string? scheme) => context.SignOutAsync(scheme, properties: null); - - /// - /// Sign out a principal for the specified scheme. - /// - /// The context. - /// The name of the authentication scheme. - /// The properties. - /// The task. - public static Task SignOutAsync(this HttpContext context, string? scheme, AuthenticationProperties? properties) => - GetAuthenticationService(context).SignOutAsync(context, scheme, properties); - - /// - /// Authenticates the request using the specified scheme and returns the value for the token. - /// - /// The context. - /// The name of the authentication scheme. - /// The name of the token. - /// The value of the token if present. - public static Task GetTokenAsync(this HttpContext context, string? scheme, string tokenName) => - GetAuthenticationService(context).GetTokenAsync(context, scheme, tokenName); - - /// - /// Authenticates the request using the default authentication scheme and returns the value for the token. - /// The default authentication scheme can be configured using . - /// - /// The context. - /// The name of the token. - /// The value of the token if present. - public static Task GetTokenAsync(this HttpContext context, string tokenName) => - GetAuthenticationService(context).GetTokenAsync(context, tokenName); - - // This project doesn't reference AuthenticationServiceCollectionExtensions.AddAuthentication so we use a string. - private static IAuthenticationService GetAuthenticationService(HttpContext context) => - context.RequestServices.GetService() ?? - throw new InvalidOperationException(Resources.FormatException_UnableToFindServices( - nameof(IAuthenticationService), - nameof(IServiceCollection), - "AddAuthentication")); - } + /// Authenticate the current request using the default authentication scheme. + /// The default authentication scheme can be configured using . + /// + /// The context. + /// The . + public static Task AuthenticateAsync(this HttpContext context) => + context.AuthenticateAsync(scheme: null); + + /// + /// Authenticate the current request using the specified scheme. + /// + /// The context. + /// The name of the authentication scheme. + /// The . + public static Task AuthenticateAsync(this HttpContext context, string? scheme) => + GetAuthenticationService(context).AuthenticateAsync(context, scheme); + + /// + /// Challenge the current request using the specified scheme. + /// An authentication challenge can be issued when an unauthenticated user requests an endpoint that requires authentication. + /// + /// The context. + /// The name of the authentication scheme. + /// The result. + public static Task ChallengeAsync(this HttpContext context, string? scheme) => + context.ChallengeAsync(scheme, properties: null); + + /// + /// Challenge the current request using the default challenge scheme. + /// An authentication challenge can be issued when an unauthenticated user requests an endpoint that requires authentication. + /// The default challenge scheme can be configured using . + /// + /// The context. + /// The task. + public static Task ChallengeAsync(this HttpContext context) => + context.ChallengeAsync(scheme: null, properties: null); + + /// + /// Challenge the current request using the default challenge scheme. + /// An authentication challenge can be issued when an unauthenticated user requests an endpoint that requires authentication. + /// The default challenge scheme can be configured using . + /// + /// The context. + /// The properties. + /// The task. + public static Task ChallengeAsync(this HttpContext context, AuthenticationProperties? properties) => + context.ChallengeAsync(scheme: null, properties: properties); + + /// + /// Challenge the current request using the specified scheme. + /// An authentication challenge can be issued when an unauthenticated user requests an endpoint that requires authentication. + /// + /// The context. + /// The name of the authentication scheme. + /// The properties. + /// The task. + public static Task ChallengeAsync(this HttpContext context, string? scheme, AuthenticationProperties? properties) => + GetAuthenticationService(context).ChallengeAsync(context, scheme, properties); + + /// + /// Forbid the current request using the specified scheme. + /// Forbid is used when an authenticated user attempts to access a resource they are not permitted to access. + /// + /// The context. + /// The name of the authentication scheme. + /// The task. + public static Task ForbidAsync(this HttpContext context, string? scheme) => + context.ForbidAsync(scheme, properties: null); + + /// + /// Forbid the current request using the default forbid scheme. + /// Forbid is used when an authenticated user attempts to access a resource they are not permitted to access. + /// The default forbid scheme can be configured using . + /// + /// The context. + /// The task. + public static Task ForbidAsync(this HttpContext context) => + context.ForbidAsync(scheme: null, properties: null); + + /// + /// Forbid the current request using the default forbid scheme. + /// Forbid is used when an authenticated user attempts to access a resource they are not permitted to access. + /// The default forbid scheme can be configured using . + /// + /// The context. + /// The properties. + /// The task. + public static Task ForbidAsync(this HttpContext context, AuthenticationProperties? properties) => + context.ForbidAsync(scheme: null, properties: properties); + + /// + /// Forbid the current request using the specified scheme. + /// Forbid is used when an authenticated user attempts to access a resource they are not permitted to access. + /// + /// The context. + /// The name of the authentication scheme. + /// The properties. + /// The task. + public static Task ForbidAsync(this HttpContext context, string? scheme, AuthenticationProperties? properties) => + GetAuthenticationService(context).ForbidAsync(context, scheme, properties); + + /// + /// Sign in a principal for the specified scheme. + /// + /// The context. + /// The name of the authentication scheme. + /// The user. + /// The task. + public static Task SignInAsync(this HttpContext context, string? scheme, ClaimsPrincipal principal) => + context.SignInAsync(scheme, principal, properties: null); + + /// + /// Sign in a principal for the default authentication scheme. + /// The default scheme for signing in can be configured using . + /// + /// The context. + /// The user. + /// The task. + public static Task SignInAsync(this HttpContext context, ClaimsPrincipal principal) => + context.SignInAsync(scheme: null, principal: principal, properties: null); + + /// + /// Sign in a principal for the default authentication scheme. + /// The default scheme for signing in can be configured using . + /// + /// The context. + /// The user. + /// The properties. + /// The task. + public static Task SignInAsync(this HttpContext context, ClaimsPrincipal principal, AuthenticationProperties? properties) => + context.SignInAsync(scheme: null, principal: principal, properties: properties); + + /// + /// Sign in a principal for the specified scheme. + /// + /// The context. + /// The name of the authentication scheme. + /// The user. + /// The properties. + /// The task. + public static Task SignInAsync(this HttpContext context, string? scheme, ClaimsPrincipal principal, AuthenticationProperties? properties) => + GetAuthenticationService(context).SignInAsync(context, scheme, principal, properties); + + /// + /// Sign out a principal for the default authentication scheme. + /// The default scheme for signing out can be configured using . + /// + /// The context. + /// The task. + public static Task SignOutAsync(this HttpContext context) => context.SignOutAsync(scheme: null, properties: null); + + /// + /// Sign out a principal for the default authentication scheme. + /// The default scheme for signing out can be configured using . + /// + /// The context. + /// The properties. + /// The task. + public static Task SignOutAsync(this HttpContext context, AuthenticationProperties? properties) => context.SignOutAsync(scheme: null, properties: properties); + + /// + /// Sign out a principal for the specified scheme. + /// + /// The context. + /// The name of the authentication scheme. + /// The task. + public static Task SignOutAsync(this HttpContext context, string? scheme) => context.SignOutAsync(scheme, properties: null); + + /// + /// Sign out a principal for the specified scheme. + /// + /// The context. + /// The name of the authentication scheme. + /// The properties. + /// The task. + public static Task SignOutAsync(this HttpContext context, string? scheme, AuthenticationProperties? properties) => + GetAuthenticationService(context).SignOutAsync(context, scheme, properties); + + /// + /// Authenticates the request using the specified scheme and returns the value for the token. + /// + /// The context. + /// The name of the authentication scheme. + /// The name of the token. + /// The value of the token if present. + public static Task GetTokenAsync(this HttpContext context, string? scheme, string tokenName) => + GetAuthenticationService(context).GetTokenAsync(context, scheme, tokenName); + + /// + /// Authenticates the request using the default authentication scheme and returns the value for the token. + /// The default authentication scheme can be configured using . + /// + /// The context. + /// The name of the token. + /// The value of the token if present. + public static Task GetTokenAsync(this HttpContext context, string tokenName) => + GetAuthenticationService(context).GetTokenAsync(context, tokenName); + + // This project doesn't reference AuthenticationServiceCollectionExtensions.AddAuthentication so we use a string. + private static IAuthenticationService GetAuthenticationService(HttpContext context) => + context.RequestServices.GetService() ?? + throw new InvalidOperationException(Resources.FormatException_UnableToFindServices( + nameof(IAuthenticationService), + nameof(IServiceCollection), + "AddAuthentication")); } diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationOptions.cs b/src/Http/Authentication.Abstractions/src/AuthenticationOptions.cs index a1c5d08e55..a56c2528c5 100644 --- a/src/Http/Authentication.Abstractions/src/AuthenticationOptions.cs +++ b/src/Http/Authentication.Abstractions/src/AuthenticationOptions.cs @@ -7,98 +7,97 @@ using System.Diagnostics.CodeAnalysis; using System.Security.Claims; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Options to configure authentication. +/// +public class AuthenticationOptions { + private readonly IList _schemes = new List(); + /// - /// Options to configure authentication. + /// Returns the schemes in the order they were added (important for request handling priority) /// - public class AuthenticationOptions - { - private readonly IList _schemes = new List(); + public IEnumerable Schemes => _schemes; - /// - /// Returns the schemes in the order they were added (important for request handling priority) - /// - public IEnumerable Schemes => _schemes; - - /// - /// Maps schemes by name. - /// - public IDictionary SchemeMap { get; } = new Dictionary(StringComparer.Ordinal); + /// + /// Maps schemes by name. + /// + public IDictionary SchemeMap { get; } = new Dictionary(StringComparer.Ordinal); - /// - /// Adds an . - /// - /// The name of the scheme being added. - /// Configures the scheme. - public void AddScheme(string name, Action configureBuilder) + /// + /// Adds an . + /// + /// The name of the scheme being added. + /// Configures the scheme. + public void AddScheme(string name, Action configureBuilder) + { + if (name == null) { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - if (configureBuilder == null) - { - throw new ArgumentNullException(nameof(configureBuilder)); - } - if (SchemeMap.ContainsKey(name)) - { - throw new InvalidOperationException("Scheme already exists: " + name); - } - - var builder = new AuthenticationSchemeBuilder(name); - configureBuilder(builder); - _schemes.Add(builder); - SchemeMap[name] = builder; + throw new ArgumentNullException(nameof(name)); + } + if (configureBuilder == null) + { + throw new ArgumentNullException(nameof(configureBuilder)); + } + if (SchemeMap.ContainsKey(name)) + { + throw new InvalidOperationException("Scheme already exists: " + name); } - /// - /// Adds an . - /// - /// The responsible for the scheme. - /// The name of the scheme being added. - /// The display name for the scheme. - public void AddScheme<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]THandler>(string name, string? displayName) where THandler : IAuthenticationHandler - => AddScheme(name, b => - { - b.DisplayName = displayName; - b.HandlerType = typeof(THandler); - }); + var builder = new AuthenticationSchemeBuilder(name); + configureBuilder(builder); + _schemes.Add(builder); + SchemeMap[name] = builder; + } + + /// + /// Adds an . + /// + /// The responsible for the scheme. + /// The name of the scheme being added. + /// The display name for the scheme. + public void AddScheme<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>(string name, string? displayName) where THandler : IAuthenticationHandler + => AddScheme(name, b => + { + b.DisplayName = displayName; + b.HandlerType = typeof(THandler); + }); - /// - /// Used as the fallback default scheme for all the other defaults. - /// - public string? DefaultScheme { get; set; } + /// + /// Used as the fallback default scheme for all the other defaults. + /// + public string? DefaultScheme { get; set; } - /// - /// Used as the default scheme by . - /// - public string? DefaultAuthenticateScheme { get; set; } + /// + /// Used as the default scheme by . + /// + public string? DefaultAuthenticateScheme { get; set; } - /// - /// Used as the default scheme by . - /// - public string? DefaultSignInScheme { get; set; } + /// + /// Used as the default scheme by . + /// + public string? DefaultSignInScheme { get; set; } - /// - /// Used as the default scheme by . - /// - public string? DefaultSignOutScheme { get; set; } + /// + /// Used as the default scheme by . + /// + public string? DefaultSignOutScheme { get; set; } - /// - /// Used as the default scheme by . - /// - public string? DefaultChallengeScheme { get; set; } + /// + /// Used as the default scheme by . + /// + public string? DefaultChallengeScheme { get; set; } - /// - /// Used as the default scheme by . - /// - public string? DefaultForbidScheme { get; set; } + /// + /// Used as the default scheme by . + /// + public string? DefaultForbidScheme { get; set; } - /// - /// If true, SignIn should throw if attempted with a user is not authenticated. - /// A user is considered authenticated if returns for the associated with the HTTP request. - /// - public bool RequireAuthenticatedSignIn { get; set; } = true; - } + /// + /// If true, SignIn should throw if attempted with a user is not authenticated. + /// A user is considered authenticated if returns for the associated with the HTTP request. + /// + public bool RequireAuthenticatedSignIn { get; set; } = true; } diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs b/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs index 976adb1474..e56ce335f9 100644 --- a/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs +++ b/src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs @@ -6,224 +6,223 @@ using System.Collections.Generic; using System.Globalization; using System.Text.Json.Serialization; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Dictionary used to store state values about the authentication session. +/// +public class AuthenticationProperties { + internal const string IssuedUtcKey = ".issued"; + internal const string ExpiresUtcKey = ".expires"; + internal const string IsPersistentKey = ".persistent"; + internal const string RedirectUriKey = ".redirect"; + internal const string RefreshKey = ".refresh"; + internal const string UtcDateTimeFormat = "r"; + + /// + /// Initializes a new instance of the class. + /// + public AuthenticationProperties() + : this(items: null, parameters: null) + { } + /// - /// Dictionary used to store state values about the authentication session. + /// Initializes a new instance of the class. /// - public class AuthenticationProperties + /// State values dictionary to use. + [JsonConstructor] + public AuthenticationProperties(IDictionary items) + : this(items, parameters: null) + { } + + /// + /// Initializes a new instance of the class. + /// + /// State values dictionary to use. + /// Parameters dictionary to use. + public AuthenticationProperties(IDictionary? items, IDictionary? parameters) { - internal const string IssuedUtcKey = ".issued"; - internal const string ExpiresUtcKey = ".expires"; - internal const string IsPersistentKey = ".persistent"; - internal const string RedirectUriKey = ".redirect"; - internal const string RefreshKey = ".refresh"; - internal const string UtcDateTimeFormat = "r"; - - /// - /// Initializes a new instance of the class. - /// - public AuthenticationProperties() - : this(items: null, parameters: null) - { } - - /// - /// Initializes a new instance of the class. - /// - /// State values dictionary to use. - [JsonConstructor] - public AuthenticationProperties(IDictionary items) - : this(items, parameters: null) - { } - - /// - /// Initializes a new instance of the class. - /// - /// State values dictionary to use. - /// Parameters dictionary to use. - public AuthenticationProperties(IDictionary? items, IDictionary? parameters) - { - Items = items ?? new Dictionary(StringComparer.Ordinal); - Parameters = parameters ?? new Dictionary(StringComparer.Ordinal); - } + Items = items ?? new Dictionary(StringComparer.Ordinal); + Parameters = parameters ?? new Dictionary(StringComparer.Ordinal); + } - /// - /// Return a copy. - /// - /// A copy. - public AuthenticationProperties Clone() - => new AuthenticationProperties( - new Dictionary(Items, StringComparer.Ordinal), - new Dictionary(Parameters, StringComparer.Ordinal)); - - /// - /// State values about the authentication session. - /// - public IDictionary Items { get; } - - /// - /// Collection of parameters that are passed to the authentication handler. These are not intended for - /// serialization or persistence, only for flowing data between call sites. - /// - [JsonIgnore] - public IDictionary Parameters { get; } - - /// - /// Gets or sets whether the authentication session is persisted across multiple requests. - /// - [JsonIgnore] - public bool IsPersistent - { - get => GetString(IsPersistentKey) != null; - set => SetString(IsPersistentKey, value ? string.Empty : null); - } + /// + /// Return a copy. + /// + /// A copy. + public AuthenticationProperties Clone() + => new AuthenticationProperties( + new Dictionary(Items, StringComparer.Ordinal), + new Dictionary(Parameters, StringComparer.Ordinal)); - /// - /// Gets or sets the full path or absolute URI to be used as an http redirect response value. - /// - [JsonIgnore] - public string? RedirectUri - { - get => GetString(RedirectUriKey); - set => SetString(RedirectUriKey, value); - } + /// + /// State values about the authentication session. + /// + public IDictionary Items { get; } - /// - /// Gets or sets the time at which the authentication ticket was issued. - /// - [JsonIgnore] - public DateTimeOffset? IssuedUtc - { - get => GetDateTimeOffset(IssuedUtcKey); - set => SetDateTimeOffset(IssuedUtcKey, value); - } + /// + /// Collection of parameters that are passed to the authentication handler. These are not intended for + /// serialization or persistence, only for flowing data between call sites. + /// + [JsonIgnore] + public IDictionary Parameters { get; } + + /// + /// Gets or sets whether the authentication session is persisted across multiple requests. + /// + [JsonIgnore] + public bool IsPersistent + { + get => GetString(IsPersistentKey) != null; + set => SetString(IsPersistentKey, value ? string.Empty : null); + } + + /// + /// Gets or sets the full path or absolute URI to be used as an http redirect response value. + /// + [JsonIgnore] + public string? RedirectUri + { + get => GetString(RedirectUriKey); + set => SetString(RedirectUriKey, value); + } + + /// + /// Gets or sets the time at which the authentication ticket was issued. + /// + [JsonIgnore] + public DateTimeOffset? IssuedUtc + { + get => GetDateTimeOffset(IssuedUtcKey); + set => SetDateTimeOffset(IssuedUtcKey, value); + } - /// - /// Gets or sets the time at which the authentication ticket expires. - /// - [JsonIgnore] - public DateTimeOffset? ExpiresUtc + /// + /// Gets or sets the time at which the authentication ticket expires. + /// + [JsonIgnore] + public DateTimeOffset? ExpiresUtc + { + get => GetDateTimeOffset(ExpiresUtcKey); + set => SetDateTimeOffset(ExpiresUtcKey, value); + } + + /// + /// Gets or sets if refreshing the authentication session should be allowed. + /// + [JsonIgnore] + public bool? AllowRefresh + { + get => GetBool(RefreshKey); + set => SetBool(RefreshKey, value); + } + + /// + /// Get a string value from the collection. + /// + /// Property key. + /// Retrieved value or null if the property is not set. + public string? GetString(string key) + { + return Items.TryGetValue(key, out var value) ? value : null; + } + + /// + /// Set or remove a string value from the collection. + /// + /// Property key. + /// Value to set or to remove the property. + public void SetString(string key, string? value) + { + if (value != null) { - get => GetDateTimeOffset(ExpiresUtcKey); - set => SetDateTimeOffset(ExpiresUtcKey, value); + Items[key] = value; } - - /// - /// Gets or sets if refreshing the authentication session should be allowed. - /// - [JsonIgnore] - public bool? AllowRefresh + else { - get => GetBool(RefreshKey); - set => SetBool(RefreshKey, value); + Items.Remove(key); } + } + + /// + /// Get a parameter from the collection. + /// + /// Parameter type. + /// Parameter key. + /// Retrieved value or the default value if the property is not set. + public T? GetParameter(string key) + => Parameters.TryGetValue(key, out var obj) && obj is T value ? value : default; - /// - /// Get a string value from the collection. - /// - /// Property key. - /// Retrieved value or null if the property is not set. - public string? GetString(string key) + /// + /// Set a parameter value in the collection. + /// + /// Parameter type. + /// Parameter key. + /// Value to set. + public void SetParameter(string key, T value) + => Parameters[key] = value; + + /// + /// Get a nullable from the collection. + /// + /// Property key. + /// Retrieved value or if the property is not set. + protected bool? GetBool(string key) + { + if (Items.TryGetValue(key, out var value) && bool.TryParse(value, out var boolValue)) { - return Items.TryGetValue(key, out var value) ? value : null; + return boolValue; } + return null; + } - /// - /// Set or remove a string value from the collection. - /// - /// Property key. - /// Value to set or to remove the property. - public void SetString(string key, string? value) + /// + /// Set or remove a value in the collection. + /// + /// Property key. + /// Value to set or to remove the property. + protected void SetBool(string key, bool? value) + { + if (value.HasValue) { - if (value != null) - { - Items[key] = value; - } - else - { - Items.Remove(key); - } + Items[key] = value.GetValueOrDefault().ToString(); } - - /// - /// Get a parameter from the collection. - /// - /// Parameter type. - /// Parameter key. - /// Retrieved value or the default value if the property is not set. - public T? GetParameter(string key) - => Parameters.TryGetValue(key, out var obj) && obj is T value ? value : default; - - /// - /// Set a parameter value in the collection. - /// - /// Parameter type. - /// Parameter key. - /// Value to set. - public void SetParameter(string key, T value) - => Parameters[key] = value; - - /// - /// Get a nullable from the collection. - /// - /// Property key. - /// Retrieved value or if the property is not set. - protected bool? GetBool(string key) + else { - if (Items.TryGetValue(key, out var value) && bool.TryParse(value, out var boolValue)) - { - return boolValue; - } - return null; + Items.Remove(key); } + } - /// - /// Set or remove a value in the collection. - /// - /// Property key. - /// Value to set or to remove the property. - protected void SetBool(string key, bool? value) + /// + /// Get a nullable value from the collection. + /// + /// Property key. + /// Retrieved value or if the property is not set. + protected DateTimeOffset? GetDateTimeOffset(string key) + { + if (Items.TryGetValue(key, out var value) + && DateTimeOffset.TryParseExact(value, UtcDateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dateTimeOffset)) { - if (value.HasValue) - { - Items[key] = value.GetValueOrDefault().ToString(); - } - else - { - Items.Remove(key); - } + return dateTimeOffset; } + return null; + } - /// - /// Get a nullable value from the collection. - /// - /// Property key. - /// Retrieved value or if the property is not set. - protected DateTimeOffset? GetDateTimeOffset(string key) + /// + /// Sets or removes a value in the collection. + /// + /// Property key. + /// Value to set or to remove the property. + protected void SetDateTimeOffset(string key, DateTimeOffset? value) + { + if (value.HasValue) { - if (Items.TryGetValue(key, out var value) - && DateTimeOffset.TryParseExact(value, UtcDateTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dateTimeOffset)) - { - return dateTimeOffset; - } - return null; + Items[key] = value.GetValueOrDefault().ToString(UtcDateTimeFormat, CultureInfo.InvariantCulture); } - - /// - /// Sets or removes a value in the collection. - /// - /// Property key. - /// Value to set or to remove the property. - protected void SetDateTimeOffset(string key, DateTimeOffset? value) + else { - if (value.HasValue) - { - Items[key] = value.GetValueOrDefault().ToString(UtcDateTimeFormat, CultureInfo.InvariantCulture); - } - else - { - Items.Remove(key); - } + Items.Remove(key); } } } diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationScheme.cs b/src/Http/Authentication.Abstractions/src/AuthenticationScheme.cs index 6a067d8949..605b4783b7 100644 --- a/src/Http/Authentication.Abstractions/src/AuthenticationScheme.cs +++ b/src/Http/Authentication.Abstractions/src/AuthenticationScheme.cs @@ -4,54 +4,53 @@ using System; using System.Diagnostics.CodeAnalysis; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// AuthenticationSchemes assign a name to a specific +/// handlerType. +/// +public class AuthenticationScheme { /// - /// AuthenticationSchemes assign a name to a specific - /// handlerType. + /// Initializes a new instance of . /// - public class AuthenticationScheme + /// The name for the authentication scheme. + /// The display name for the authentication scheme. + /// The type that handles this scheme. + public AuthenticationScheme(string name, string? displayName, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type handlerType) { - /// - /// Initializes a new instance of . - /// - /// The name for the authentication scheme. - /// The display name for the authentication scheme. - /// The type that handles this scheme. - public AuthenticationScheme(string name, string? displayName, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type handlerType) + if (name == null) { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - if (handlerType == null) - { - throw new ArgumentNullException(nameof(handlerType)); - } - if (!typeof(IAuthenticationHandler).IsAssignableFrom(handlerType)) - { - throw new ArgumentException("handlerType must implement IAuthenticationHandler."); - } - - Name = name; - HandlerType = handlerType; - DisplayName = displayName; + throw new ArgumentNullException(nameof(name)); + } + if (handlerType == null) + { + throw new ArgumentNullException(nameof(handlerType)); + } + if (!typeof(IAuthenticationHandler).IsAssignableFrom(handlerType)) + { + throw new ArgumentException("handlerType must implement IAuthenticationHandler."); } - /// - /// The name of the authentication scheme. - /// - public string Name { get; } + Name = name; + HandlerType = handlerType; + DisplayName = displayName; + } - /// - /// The display name for the scheme. Null is valid and used for non user facing schemes. - /// - public string? DisplayName { get; } + /// + /// The name of the authentication scheme. + /// + public string Name { get; } - /// - /// The type that handles this scheme. - /// - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - public Type HandlerType { get; } - } + /// + /// The display name for the scheme. Null is valid and used for non user facing schemes. + /// + public string? DisplayName { get; } + + /// + /// The type that handles this scheme. + /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] + public Type HandlerType { get; } } diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationSchemeBuilder.cs b/src/Http/Authentication.Abstractions/src/AuthenticationSchemeBuilder.cs index ad35625f5e..d4c2e94791 100644 --- a/src/Http/Authentication.Abstractions/src/AuthenticationSchemeBuilder.cs +++ b/src/Http/Authentication.Abstractions/src/AuthenticationSchemeBuilder.cs @@ -4,50 +4,49 @@ using System; using System.Diagnostics.CodeAnalysis; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Used to build s. +/// +public class AuthenticationSchemeBuilder { /// - /// Used to build s. + /// Constructor. /// - public class AuthenticationSchemeBuilder + /// The name of the scheme being built. + public AuthenticationSchemeBuilder(string name) { - /// - /// Constructor. - /// - /// The name of the scheme being built. - public AuthenticationSchemeBuilder(string name) - { - Name = name; - } + Name = name; + } - /// - /// Gets the name of the scheme being built. - /// - public string Name { get; } - - /// - /// Gets or sets the display name for the scheme being built. - /// - public string? DisplayName { get; set; } - - /// - /// Gets or sets the type responsible for this scheme. - /// - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - public Type? HandlerType { get; set; } - - /// - /// Builds the instance. - /// - /// The . - public AuthenticationScheme Build() - { - if (HandlerType is null) - { - throw new InvalidOperationException($"{nameof(HandlerType)} must be configured to build an {nameof(AuthenticationScheme)}."); - } + /// + /// Gets the name of the scheme being built. + /// + public string Name { get; } + + /// + /// Gets or sets the display name for the scheme being built. + /// + public string? DisplayName { get; set; } - return new AuthenticationScheme(Name, DisplayName, HandlerType); + /// + /// Gets or sets the type responsible for this scheme. + /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] + public Type? HandlerType { get; set; } + + /// + /// Builds the instance. + /// + /// The . + public AuthenticationScheme Build() + { + if (HandlerType is null) + { + throw new InvalidOperationException($"{nameof(HandlerType)} must be configured to build an {nameof(AuthenticationScheme)}."); } + + return new AuthenticationScheme(Name, DisplayName, HandlerType); } } diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationTicket.cs b/src/Http/Authentication.Abstractions/src/AuthenticationTicket.cs index 79c4913a30..87e28c7b9d 100644 --- a/src/Http/Authentication.Abstractions/src/AuthenticationTicket.cs +++ b/src/Http/Authentication.Abstractions/src/AuthenticationTicket.cs @@ -4,70 +4,69 @@ using System; using System.Security.Claims; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Contains user identity information as well as additional authentication state. +/// +public class AuthenticationTicket { /// - /// Contains user identity information as well as additional authentication state. + /// Initializes a new instance of the class /// - public class AuthenticationTicket + /// the that represents the authenticated user. + /// additional properties that can be consumed by the user or runtime. + /// the authentication scheme that was responsible for this ticket. + public AuthenticationTicket(ClaimsPrincipal principal, AuthenticationProperties? properties, string authenticationScheme) { - /// - /// Initializes a new instance of the class - /// - /// the that represents the authenticated user. - /// additional properties that can be consumed by the user or runtime. - /// the authentication scheme that was responsible for this ticket. - public AuthenticationTicket(ClaimsPrincipal principal, AuthenticationProperties? properties, string authenticationScheme) + if (principal == null) { - if (principal == null) - { - throw new ArgumentNullException(nameof(principal)); - } - - AuthenticationScheme = authenticationScheme; - Principal = principal; - Properties = properties ?? new AuthenticationProperties(); + throw new ArgumentNullException(nameof(principal)); } - /// - /// Initializes a new instance of the class - /// - /// the that represents the authenticated user. - /// the authentication scheme that was responsible for this ticket. - public AuthenticationTicket(ClaimsPrincipal principal, string authenticationScheme) - : this(principal, properties: null, authenticationScheme: authenticationScheme) - { } + AuthenticationScheme = authenticationScheme; + Principal = principal; + Properties = properties ?? new AuthenticationProperties(); + } - /// - /// Gets the authentication scheme that was responsible for this ticket. - /// - public string AuthenticationScheme { get; } + /// + /// Initializes a new instance of the class + /// + /// the that represents the authenticated user. + /// the authentication scheme that was responsible for this ticket. + public AuthenticationTicket(ClaimsPrincipal principal, string authenticationScheme) + : this(principal, properties: null, authenticationScheme: authenticationScheme) + { } - /// - /// Gets the claims-principal with authenticated user identities. - /// - public ClaimsPrincipal Principal { get; } + /// + /// Gets the authentication scheme that was responsible for this ticket. + /// + public string AuthenticationScheme { get; } - /// - /// Additional state values for the authentication session. - /// - public AuthenticationProperties Properties { get; } + /// + /// Gets the claims-principal with authenticated user identities. + /// + public ClaimsPrincipal Principal { get; } + + /// + /// Additional state values for the authentication session. + /// + public AuthenticationProperties Properties { get; } - /// - /// Returns a copy of the ticket. - /// - /// - /// The method clones the by calling on each of the . - /// - /// A copy of the ticket - public AuthenticationTicket Clone() + /// + /// Returns a copy of the ticket. + /// + /// + /// The method clones the by calling on each of the . + /// + /// A copy of the ticket + public AuthenticationTicket Clone() + { + var principal = new ClaimsPrincipal(); + foreach (var identity in Principal.Identities) { - var principal = new ClaimsPrincipal(); - foreach (var identity in Principal.Identities) - { - principal.AddIdentity(identity.Clone()); - } - return new AuthenticationTicket(principal, Properties.Clone(), AuthenticationScheme); + principal.AddIdentity(identity.Clone()); } + return new AuthenticationTicket(principal, Properties.Clone(), AuthenticationScheme); } } diff --git a/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs b/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs index 2ba7a5bd35..51f7ee9e55 100644 --- a/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs +++ b/src/Http/Authentication.Abstractions/src/AuthenticationToken.cs @@ -2,21 +2,20 @@ // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Name/Value representing a token. +/// +public class AuthenticationToken { /// - /// Name/Value representing a token. + /// Name. /// - public class AuthenticationToken - { - /// - /// Name. - /// - public string Name { get; set; } = default!; + public string Name { get; set; } = default!; - /// - /// Value. - /// - public string Value { get; set; } = default!; - } + /// + /// Value. + /// + public string Value { get; set; } = default!; } diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticateResultFeature.cs b/src/Http/Authentication.Abstractions/src/IAuthenticateResultFeature.cs index 6f5e264ac0..88fdf3786d 100644 --- a/src/Http/Authentication.Abstractions/src/IAuthenticateResultFeature.cs +++ b/src/Http/Authentication.Abstractions/src/IAuthenticateResultFeature.cs @@ -3,17 +3,16 @@ using Microsoft.AspNetCore.Http.Features.Authentication; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Used to capture the from the authorization middleware. +/// +public interface IAuthenticateResultFeature { /// - /// Used to capture the from the authorization middleware. + /// The from the authorization middleware. + /// Set to null if the property is set after the authorization middleware. /// - public interface IAuthenticateResultFeature - { - /// - /// The from the authorization middleware. - /// Set to null if the property is set after the authorization middleware. - /// - AuthenticateResult? AuthenticateResult { get; set; } - } + AuthenticateResult? AuthenticateResult { get; set; } } diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationFeature.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationFeature.cs index 9b71da47ce..391b3fe43b 100644 --- a/src/Http/Authentication.Abstractions/src/IAuthenticationFeature.cs +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationFeature.cs @@ -3,21 +3,20 @@ using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Used to capture path info so redirects can be computed properly within an app.Map(). +/// +public interface IAuthenticationFeature { /// - /// Used to capture path info so redirects can be computed properly within an app.Map(). + /// The original path base. /// - public interface IAuthenticationFeature - { - /// - /// The original path base. - /// - PathString OriginalPathBase { get; set; } + PathString OriginalPathBase { get; set; } - /// - /// The original path. - /// - PathString OriginalPath { get; set; } - } + /// + /// The original path. + /// + PathString OriginalPath { get; set; } } diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationHandler.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationHandler.cs index ad2816be0f..dc1056edc6 100644 --- a/src/Http/Authentication.Abstractions/src/IAuthenticationHandler.cs +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationHandler.cs @@ -4,36 +4,35 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Created per request to handle authentication for a particular scheme. +/// +public interface IAuthenticationHandler { /// - /// Created per request to handle authentication for a particular scheme. + /// Initialize the authentication handler. The handler should initialize anything it needs from the request and scheme as part of this method. /// - public interface IAuthenticationHandler - { - /// - /// Initialize the authentication handler. The handler should initialize anything it needs from the request and scheme as part of this method. - /// - /// The scheme. - /// The context. - Task InitializeAsync(AuthenticationScheme scheme, HttpContext context); + /// The scheme. + /// The context. + Task InitializeAsync(AuthenticationScheme scheme, HttpContext context); - /// - /// Authenticate the current request. - /// - /// The result. - Task AuthenticateAsync(); + /// + /// Authenticate the current request. + /// + /// The result. + Task AuthenticateAsync(); - /// - /// Challenge the current request. - /// - /// The that contains the extra meta-data arriving with the authentication. - Task ChallengeAsync(AuthenticationProperties? properties); + /// + /// Challenge the current request. + /// + /// The that contains the extra meta-data arriving with the authentication. + Task ChallengeAsync(AuthenticationProperties? properties); - /// - /// Forbid the current request. - /// - /// The that contains the extra meta-data arriving with the authentication. - Task ForbidAsync(AuthenticationProperties? properties); - } + /// + /// Forbid the current request. + /// + /// The that contains the extra meta-data arriving with the authentication. + Task ForbidAsync(AuthenticationProperties? properties); } diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationHandlerProvider.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationHandlerProvider.cs index 127216180b..4626bbd422 100644 --- a/src/Http/Authentication.Abstractions/src/IAuthenticationHandlerProvider.cs +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationHandlerProvider.cs @@ -4,19 +4,18 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Provides the appropriate IAuthenticationHandler instance for the authenticationScheme and request. +/// +public interface IAuthenticationHandlerProvider { /// - /// Provides the appropriate IAuthenticationHandler instance for the authenticationScheme and request. + /// Returns the handler instance that will be used. /// - public interface IAuthenticationHandlerProvider - { - /// - /// Returns the handler instance that will be used. - /// - /// The . - /// The name of the authentication scheme being handled. - /// The handler instance. - Task GetHandlerAsync(HttpContext context, string authenticationScheme); - } + /// The . + /// The name of the authentication scheme being handled. + /// The handler instance. + Task GetHandlerAsync(HttpContext context, string authenticationScheme); } diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationRequestHandler.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationRequestHandler.cs index de0ef1a755..1f1583f6ab 100644 --- a/src/Http/Authentication.Abstractions/src/IAuthenticationRequestHandler.cs +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationRequestHandler.cs @@ -3,23 +3,21 @@ using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Used to determine if a handler wants to participate in request processing. +/// +public interface IAuthenticationRequestHandler : IAuthenticationHandler { /// - /// Used to determine if a handler wants to participate in request processing. + /// Gets a value that determines if the request should stop being processed. + /// + /// This feature is supported by the Authentication middleware + /// which does not invoke any subsequent or middleware configured in the request pipeline + /// if the handler returns . + /// /// - public interface IAuthenticationRequestHandler : IAuthenticationHandler - { - /// - /// Gets a value that determines if the request should stop being processed. - /// - /// This feature is supported by the Authentication middleware - /// which does not invoke any subsequent or middleware configured in the request pipeline - /// if the handler returns . - /// - /// - /// if request processing should stop. - Task HandleRequestAsync(); - } - + /// if request processing should stop. + Task HandleRequestAsync(); } diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationSchemeProvider.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationSchemeProvider.cs index cac9dc9736..f74146a8a0 100644 --- a/src/Http/Authentication.Abstractions/src/IAuthenticationSchemeProvider.cs +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationSchemeProvider.cs @@ -5,99 +5,99 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Responsible for managing what authenticationSchemes are supported. +/// +public interface IAuthenticationSchemeProvider { /// - /// Responsible for managing what authenticationSchemes are supported. + /// Returns all currently registered s. /// - public interface IAuthenticationSchemeProvider - { - /// - /// Returns all currently registered s. - /// - /// All currently registered s. - Task> GetAllSchemesAsync(); + /// All currently registered s. + Task> GetAllSchemesAsync(); - /// - /// Returns the matching the name, or null. - /// - /// The name of the authenticationScheme. - /// The scheme or null if not found. - Task GetSchemeAsync(string name); + /// + /// Returns the matching the name, or null. + /// + /// The name of the authenticationScheme. + /// The scheme or null if not found. + Task GetSchemeAsync(string name); - /// - /// Returns the scheme that will be used by default for . - /// This is typically specified via . - /// Otherwise, this will fallback to . - /// - /// The scheme that will be used by default for . - Task GetDefaultAuthenticateSchemeAsync(); + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + Task GetDefaultAuthenticateSchemeAsync(); - /// - /// Returns the scheme that will be used by default for . - /// This is typically specified via . - /// Otherwise, this will fallback to . - /// - /// The scheme that will be used by default for . - Task GetDefaultChallengeSchemeAsync(); + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + Task GetDefaultChallengeSchemeAsync(); - /// - /// Returns the scheme that will be used by default for . - /// This is typically specified via . - /// Otherwise, this will fallback to . - /// - /// The scheme that will be used by default for . - Task GetDefaultForbidSchemeAsync(); + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + Task GetDefaultForbidSchemeAsync(); - /// - /// Returns the scheme that will be used by default for . - /// This is typically specified via . - /// Otherwise, this will fallback to . - /// - /// The scheme that will be used by default for . - Task GetDefaultSignInSchemeAsync(); + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + Task GetDefaultSignInSchemeAsync(); - /// - /// Returns the scheme that will be used by default for . - /// This is typically specified via . - /// Otherwise, this will fallback to . - /// - /// The scheme that will be used by default for . - Task GetDefaultSignOutSchemeAsync(); + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + Task GetDefaultSignOutSchemeAsync(); - /// - /// Registers a scheme for use by . - /// - /// The scheme. - void AddScheme(AuthenticationScheme scheme); + /// + /// Registers a scheme for use by . + /// + /// The scheme. + void AddScheme(AuthenticationScheme scheme); - /// - /// Registers a scheme for use by . - /// - /// The scheme. - /// true if the scheme was added successfully. - bool TryAddScheme(AuthenticationScheme scheme) + /// + /// Registers a scheme for use by . + /// + /// The scheme. + /// true if the scheme was added successfully. + bool TryAddScheme(AuthenticationScheme scheme) + { + try + { + AddScheme(scheme); + return true; + } + catch { - try - { - AddScheme(scheme); - return true; - } - catch { - return false; - } + return false; } + } - /// - /// Removes a scheme, preventing it from being used by . - /// - /// The name of the authenticationScheme being removed. - void RemoveScheme(string name); + /// + /// Removes a scheme, preventing it from being used by . + /// + /// The name of the authenticationScheme being removed. + void RemoveScheme(string name); - /// - /// Returns the schemes in priority order for request handling. - /// - /// The schemes in priority order for request handling - Task> GetRequestHandlerSchemesAsync(); - } + /// + /// Returns the schemes in priority order for request handling. + /// + /// The schemes in priority order for request handling + Task> GetRequestHandlerSchemesAsync(); } diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationService.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationService.cs index b83e44efd1..523b0409f1 100644 --- a/src/Http/Authentication.Abstractions/src/IAuthenticationService.cs +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationService.cs @@ -5,58 +5,57 @@ using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Used to provide authentication. +/// +public interface IAuthenticationService { /// - /// Used to provide authentication. + /// Authenticate for the specified authentication scheme. /// - public interface IAuthenticationService - { - /// - /// Authenticate for the specified authentication scheme. - /// - /// The . - /// The name of the authentication scheme. - /// The result. - Task AuthenticateAsync(HttpContext context, string? scheme); + /// The . + /// The name of the authentication scheme. + /// The result. + Task AuthenticateAsync(HttpContext context, string? scheme); - /// - /// Challenge the specified authentication scheme. - /// An authentication challenge can be issued when an unauthenticated user requests an endpoint that requires authentication. - /// - /// The . - /// The name of the authentication scheme. - /// The . - /// A task. - Task ChallengeAsync(HttpContext context, string? scheme, AuthenticationProperties? properties); + /// + /// Challenge the specified authentication scheme. + /// An authentication challenge can be issued when an unauthenticated user requests an endpoint that requires authentication. + /// + /// The . + /// The name of the authentication scheme. + /// The . + /// A task. + Task ChallengeAsync(HttpContext context, string? scheme, AuthenticationProperties? properties); - /// - /// Forbids the specified authentication scheme. - /// Forbid is used when an authenticated user attempts to access a resource they are not permitted to access. - /// - /// The . - /// The name of the authentication scheme. - /// The . - /// A task. - Task ForbidAsync(HttpContext context, string? scheme, AuthenticationProperties? properties); + /// + /// Forbids the specified authentication scheme. + /// Forbid is used when an authenticated user attempts to access a resource they are not permitted to access. + /// + /// The . + /// The name of the authentication scheme. + /// The . + /// A task. + Task ForbidAsync(HttpContext context, string? scheme, AuthenticationProperties? properties); - /// - /// Sign a principal in for the specified authentication scheme. - /// - /// The . - /// The name of the authentication scheme. - /// The to sign in. - /// The . - /// A task. - Task SignInAsync(HttpContext context, string? scheme, ClaimsPrincipal principal, AuthenticationProperties? properties); + /// + /// Sign a principal in for the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The to sign in. + /// The . + /// A task. + Task SignInAsync(HttpContext context, string? scheme, ClaimsPrincipal principal, AuthenticationProperties? properties); - /// - /// Sign out the specified authentication scheme. - /// - /// The . - /// The name of the authentication scheme. - /// The . - /// A task. - Task SignOutAsync(HttpContext context, string? scheme, AuthenticationProperties? properties); - } + /// + /// Sign out the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The . + /// A task. + Task SignOutAsync(HttpContext context, string? scheme, AuthenticationProperties? properties); } diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationSignInHandler.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationSignInHandler.cs index da767ef7c4..3053b58127 100644 --- a/src/Http/Authentication.Abstractions/src/IAuthenticationSignInHandler.cs +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationSignInHandler.cs @@ -4,19 +4,18 @@ using System.Security.Claims; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Used to determine if a handler supports SignIn. +/// +public interface IAuthenticationSignInHandler : IAuthenticationSignOutHandler { /// - /// Used to determine if a handler supports SignIn. + /// Handle sign in. /// - public interface IAuthenticationSignInHandler : IAuthenticationSignOutHandler - { - /// - /// Handle sign in. - /// - /// The user. - /// The that contains the extra meta-data arriving with the authentication. - /// A task. - Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties); - } + /// The user. + /// The that contains the extra meta-data arriving with the authentication. + /// A task. + Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties); } diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationSignOutHandler.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationSignOutHandler.cs index 68f8dc36f2..34a5ef379c 100644 --- a/src/Http/Authentication.Abstractions/src/IAuthenticationSignOutHandler.cs +++ b/src/Http/Authentication.Abstractions/src/IAuthenticationSignOutHandler.cs @@ -3,19 +3,17 @@ using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Used to determine if a handler supports SignOut. +/// +public interface IAuthenticationSignOutHandler : IAuthenticationHandler { /// - /// Used to determine if a handler supports SignOut. + /// Signout behavior. /// - public interface IAuthenticationSignOutHandler : IAuthenticationHandler - { - /// - /// Signout behavior. - /// - /// The that contains the extra meta-data arriving with the authentication. - /// A task. - Task SignOutAsync(AuthenticationProperties? properties); - } - + /// The that contains the extra meta-data arriving with the authentication. + /// A task. + Task SignOutAsync(AuthenticationProperties? properties); } diff --git a/src/Http/Authentication.Abstractions/src/IClaimsTransformation.cs b/src/Http/Authentication.Abstractions/src/IClaimsTransformation.cs index 664d5ec006..339d7feb86 100644 --- a/src/Http/Authentication.Abstractions/src/IClaimsTransformation.cs +++ b/src/Http/Authentication.Abstractions/src/IClaimsTransformation.cs @@ -4,20 +4,19 @@ using System.Security.Claims; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Used by the for claims transformation. +/// +public interface IClaimsTransformation { /// - /// Used by the for claims transformation. + /// Provides a central transformation point to change the specified principal. + /// Note: this will be run on each AuthenticateAsync call, so its safer to + /// return a new ClaimsPrincipal if your transformation is not idempotent. /// - public interface IClaimsTransformation - { - /// - /// Provides a central transformation point to change the specified principal. - /// Note: this will be run on each AuthenticateAsync call, so its safer to - /// return a new ClaimsPrincipal if your transformation is not idempotent. - /// - /// The to transform. - /// The transformed principal. - Task TransformAsync(ClaimsPrincipal principal); - } -} \ No newline at end of file + /// The to transform. + /// The transformed principal. + Task TransformAsync(ClaimsPrincipal principal); +} diff --git a/src/Http/Authentication.Abstractions/src/TokenExtensions.cs b/src/Http/Authentication.Abstractions/src/TokenExtensions.cs index ec8504bfaf..8bdc3d644d 100644 --- a/src/Http/Authentication.Abstractions/src/TokenExtensions.cs +++ b/src/Http/Authentication.Abstractions/src/TokenExtensions.cs @@ -6,167 +6,166 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Extension methods for storing authentication tokens in . +/// +public static class AuthenticationTokenExtensions { + private const string TokenNamesKey = ".TokenNames"; + private const string TokenKeyPrefix = ".Token."; + /// - /// Extension methods for storing authentication tokens in . + /// Stores a set of authentication tokens, after removing any old tokens. /// - public static class AuthenticationTokenExtensions + /// The properties. + /// The tokens to store. + public static void StoreTokens(this AuthenticationProperties properties, IEnumerable tokens) { - private const string TokenNamesKey = ".TokenNames"; - private const string TokenKeyPrefix = ".Token."; - - /// - /// Stores a set of authentication tokens, after removing any old tokens. - /// - /// The properties. - /// The tokens to store. - public static void StoreTokens(this AuthenticationProperties properties, IEnumerable tokens) + if (properties == null) { - if (properties == null) - { - throw new ArgumentNullException(nameof(properties)); - } - if (tokens == null) - { - throw new ArgumentNullException(nameof(tokens)); - } - - // Clear old tokens first - var oldTokens = properties.GetTokens(); - foreach (var t in oldTokens) - { - properties.Items.Remove(TokenKeyPrefix + t.Name); - } - properties.Items.Remove(TokenNamesKey); - - var tokenNames = new List(); - foreach (var token in tokens) - { - if (token.Name is null) - { - throw new ArgumentNullException(nameof(tokens), "Token name cannot be null."); - } + throw new ArgumentNullException(nameof(properties)); + } + if (tokens == null) + { + throw new ArgumentNullException(nameof(tokens)); + } - // REVIEW: should probably check that there are no ; in the token name and throw or encode - tokenNames.Add(token.Name); - properties.Items[TokenKeyPrefix + token.Name] = token.Value; - } - if (tokenNames.Count > 0) - { - properties.Items[TokenNamesKey] = string.Join(";", tokenNames.ToArray()); - } + // Clear old tokens first + var oldTokens = properties.GetTokens(); + foreach (var t in oldTokens) + { + properties.Items.Remove(TokenKeyPrefix + t.Name); } + properties.Items.Remove(TokenNamesKey); - /// - /// Returns the value of a token. - /// - /// The properties. - /// The token name. - /// The token value. - public static string? GetTokenValue(this AuthenticationProperties properties, string tokenName) + var tokenNames = new List(); + foreach (var token in tokens) { - if (properties == null) - { - throw new ArgumentNullException(nameof(properties)); - } - if (tokenName == null) + if (token.Name is null) { - throw new ArgumentNullException(nameof(tokenName)); + throw new ArgumentNullException(nameof(tokens), "Token name cannot be null."); } - var tokenKey = TokenKeyPrefix + tokenName; + // REVIEW: should probably check that there are no ; in the token name and throw or encode + tokenNames.Add(token.Name); + properties.Items[TokenKeyPrefix + token.Name] = token.Value; + } + if (tokenNames.Count > 0) + { + properties.Items[TokenNamesKey] = string.Join(";", tokenNames.ToArray()); + } + } - return properties.Items.TryGetValue(tokenKey, out var value) ? value : null; + /// + /// Returns the value of a token. + /// + /// The properties. + /// The token name. + /// The token value. + public static string? GetTokenValue(this AuthenticationProperties properties, string tokenName) + { + if (properties == null) + { + throw new ArgumentNullException(nameof(properties)); } + if (tokenName == null) + { + throw new ArgumentNullException(nameof(tokenName)); + } + + var tokenKey = TokenKeyPrefix + tokenName; - /// - /// Updates the value of a token if already present. - /// - /// The to update. - /// The token name. - /// The token value. - /// if the token was updated, otherwise . - public static bool UpdateTokenValue(this AuthenticationProperties properties, string tokenName, string tokenValue) + return properties.Items.TryGetValue(tokenKey, out var value) ? value : null; + } + + /// + /// Updates the value of a token if already present. + /// + /// The to update. + /// The token name. + /// The token value. + /// if the token was updated, otherwise . + public static bool UpdateTokenValue(this AuthenticationProperties properties, string tokenName, string tokenValue) + { + if (properties == null) { - if (properties == null) - { - throw new ArgumentNullException(nameof(properties)); - } - if (tokenName == null) - { - throw new ArgumentNullException(nameof(tokenName)); - } + throw new ArgumentNullException(nameof(properties)); + } + if (tokenName == null) + { + throw new ArgumentNullException(nameof(tokenName)); + } - var tokenKey = TokenKeyPrefix + tokenName; - if (!properties.Items.ContainsKey(tokenKey)) - { - return false; - } - properties.Items[tokenKey] = tokenValue; - return true; + var tokenKey = TokenKeyPrefix + tokenName; + if (!properties.Items.ContainsKey(tokenKey)) + { + return false; } + properties.Items[tokenKey] = tokenValue; + return true; + } - /// - /// Returns all of the instances contained in the properties. - /// - /// The properties. - /// The authentication tokens. - public static IEnumerable GetTokens(this AuthenticationProperties properties) + /// + /// Returns all of the instances contained in the properties. + /// + /// The properties. + /// The authentication tokens. + public static IEnumerable GetTokens(this AuthenticationProperties properties) + { + if (properties == null) { - if (properties == null) - { - throw new ArgumentNullException(nameof(properties)); - } + throw new ArgumentNullException(nameof(properties)); + } - var tokens = new List(); - if (properties.Items.TryGetValue(TokenNamesKey, out var value) && !string.IsNullOrEmpty(value)) + var tokens = new List(); + if (properties.Items.TryGetValue(TokenNamesKey, out var value) && !string.IsNullOrEmpty(value)) + { + var tokenNames = value.Split(';'); + foreach (var name in tokenNames) { - var tokenNames = value.Split(';'); - foreach (var name in tokenNames) + var token = properties.GetTokenValue(name); + if (token != null) { - var token = properties.GetTokenValue(name); - if (token != null) - { - tokens.Add(new AuthenticationToken { Name = name, Value = token }); - } + tokens.Add(new AuthenticationToken { Name = name, Value = token }); } } - - return tokens; } - /// - /// Authenticates the request using the specified authentication scheme and returns the value for the token. - /// - /// The . - /// The context. - /// The name of the token. - /// The value of the token if present. - public static Task GetTokenAsync(this IAuthenticationService auth, HttpContext context, string tokenName) - => auth.GetTokenAsync(context, scheme: null, tokenName: tokenName); - - /// - /// Authenticates the request using the specified authentication scheme and returns the value for the token. - /// - /// The . - /// The context. - /// The name of the authentication scheme. - /// The name of the token. - /// The value of the token if present. - public static async Task GetTokenAsync(this IAuthenticationService auth, HttpContext context, string? scheme, string tokenName) - { - if (auth == null) - { - throw new ArgumentNullException(nameof(auth)); - } - if (tokenName == null) - { - throw new ArgumentNullException(nameof(tokenName)); - } + return tokens; + } + + /// + /// Authenticates the request using the specified authentication scheme and returns the value for the token. + /// + /// The . + /// The context. + /// The name of the token. + /// The value of the token if present. + public static Task GetTokenAsync(this IAuthenticationService auth, HttpContext context, string tokenName) + => auth.GetTokenAsync(context, scheme: null, tokenName: tokenName); - var result = await auth.AuthenticateAsync(context, scheme); - return result?.Properties?.GetTokenValue(tokenName); + /// + /// Authenticates the request using the specified authentication scheme and returns the value for the token. + /// + /// The . + /// The context. + /// The name of the authentication scheme. + /// The name of the token. + /// The value of the token if present. + public static async Task GetTokenAsync(this IAuthenticationService auth, HttpContext context, string? scheme, string tokenName) + { + if (auth == null) + { + throw new ArgumentNullException(nameof(auth)); + } + if (tokenName == null) + { + throw new ArgumentNullException(nameof(tokenName)); } + + var result = await auth.AuthenticateAsync(context, scheme); + return result?.Properties?.GetTokenValue(tokenName); } } diff --git a/src/Http/Authentication.Core/src/AuthenticationCoreServiceCollectionExtensions.cs b/src/Http/Authentication.Core/src/AuthenticationCoreServiceCollectionExtensions.cs index 4af2730a1b..9384e6ce63 100644 --- a/src/Http/Authentication.Core/src/AuthenticationCoreServiceCollectionExtensions.cs +++ b/src/Http/Authentication.Core/src/AuthenticationCoreServiceCollectionExtensions.cs @@ -5,52 +5,52 @@ using System; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection.Extensions; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for setting up authentication services in an . +/// +public static class AuthenticationCoreServiceCollectionExtensions { /// - /// Extension methods for setting up authentication services in an . + /// Add core authentication services needed for . /// - public static class AuthenticationCoreServiceCollectionExtensions + /// The . + /// The service collection. + public static IServiceCollection AddAuthenticationCore(this IServiceCollection services) { - /// - /// Add core authentication services needed for . - /// - /// The . - /// The service collection. - public static IServiceCollection AddAuthenticationCore(this IServiceCollection services) + if (services == null) { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - services.TryAddScoped(); - services.TryAddSingleton(); // Can be replaced with scoped ones that use DbContext - services.TryAddScoped(); - services.TryAddSingleton(); - return services; + throw new ArgumentNullException(nameof(services)); } - /// - /// Add core authentication services needed for . - /// - /// The . - /// Used to configure the . - /// The service collection. - public static IServiceCollection AddAuthenticationCore(this IServiceCollection services, Action configureOptions) { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } + services.TryAddScoped(); + services.TryAddSingleton(); // Can be replaced with scoped ones that use DbContext + services.TryAddScoped(); + services.TryAddSingleton(); + return services; + } - if (configureOptions == null) - { - throw new ArgumentNullException(nameof(configureOptions)); - } + /// + /// Add core authentication services needed for . + /// + /// The . + /// Used to configure the . + /// The service collection. + public static IServiceCollection AddAuthenticationCore(this IServiceCollection services, Action configureOptions) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } - services.AddAuthenticationCore(); - services.Configure(configureOptions); - return services; + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); } + + services.AddAuthenticationCore(); + services.Configure(configureOptions); + return services; } } diff --git a/src/Http/Authentication.Core/src/AuthenticationFeature.cs b/src/Http/Authentication.Core/src/AuthenticationFeature.cs index 1dc87f9da1..21bb7f9206 100644 --- a/src/Http/Authentication.Core/src/AuthenticationFeature.cs +++ b/src/Http/Authentication.Core/src/AuthenticationFeature.cs @@ -3,21 +3,20 @@ using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Used to capture path info so redirects can be computed properly within an app.Map(). +/// +public class AuthenticationFeature : IAuthenticationFeature { /// - /// Used to capture path info so redirects can be computed properly within an app.Map(). + /// The original path base. /// - public class AuthenticationFeature : IAuthenticationFeature - { - /// - /// The original path base. - /// - public PathString OriginalPathBase { get; set; } + public PathString OriginalPathBase { get; set; } - /// - /// The original path. - /// - public PathString OriginalPath { get; set; } - } + /// + /// The original path. + /// + public PathString OriginalPath { get; set; } } diff --git a/src/Http/Authentication.Core/src/AuthenticationHandlerProvider.cs b/src/Http/Authentication.Core/src/AuthenticationHandlerProvider.cs index b2720b03c7..7eaa229506 100644 --- a/src/Http/Authentication.Core/src/AuthenticationHandlerProvider.cs +++ b/src/Http/Authentication.Core/src/AuthenticationHandlerProvider.cs @@ -7,57 +7,56 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Implementation of . +/// +public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider { /// - /// Implementation of . + /// Constructor. /// - public class AuthenticationHandlerProvider : IAuthenticationHandlerProvider + /// The . + public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes) { - /// - /// Constructor. - /// - /// The . - public AuthenticationHandlerProvider(IAuthenticationSchemeProvider schemes) - { - Schemes = schemes; - } + Schemes = schemes; + } - /// - /// The . - /// - public IAuthenticationSchemeProvider Schemes { get; } + /// + /// The . + /// + public IAuthenticationSchemeProvider Schemes { get; } - // handler instance cache, need to initialize once per request - private readonly Dictionary _handlerMap = new Dictionary(StringComparer.Ordinal); + // handler instance cache, need to initialize once per request + private readonly Dictionary _handlerMap = new Dictionary(StringComparer.Ordinal); - /// - /// Returns the handler instance that will be used. - /// - /// The context. - /// The name of the authentication scheme being handled. - /// The handler instance. - public async Task GetHandlerAsync(HttpContext context, string authenticationScheme) + /// + /// Returns the handler instance that will be used. + /// + /// The context. + /// The name of the authentication scheme being handled. + /// The handler instance. + public async Task GetHandlerAsync(HttpContext context, string authenticationScheme) + { + if (_handlerMap.TryGetValue(authenticationScheme, out var value)) { - if (_handlerMap.TryGetValue(authenticationScheme, out var value)) - { - return value; - } + return value; + } - var scheme = await Schemes.GetSchemeAsync(authenticationScheme); - if (scheme == null) - { - return null; - } - var handler = (context.RequestServices.GetService(scheme.HandlerType) ?? - ActivatorUtilities.CreateInstance(context.RequestServices, scheme.HandlerType)) - as IAuthenticationHandler; - if (handler != null) - { - await handler.InitializeAsync(scheme, context); - _handlerMap[authenticationScheme] = handler; - } - return handler; + var scheme = await Schemes.GetSchemeAsync(authenticationScheme); + if (scheme == null) + { + return null; + } + var handler = (context.RequestServices.GetService(scheme.HandlerType) ?? + ActivatorUtilities.CreateInstance(context.RequestServices, scheme.HandlerType)) + as IAuthenticationHandler; + if (handler != null) + { + await handler.InitializeAsync(scheme, context); + _handlerMap[authenticationScheme] = handler; } + return handler; } } diff --git a/src/Http/Authentication.Core/src/AuthenticationSchemeProvider.cs b/src/Http/Authentication.Core/src/AuthenticationSchemeProvider.cs index cefbd900d7..4025983651 100644 --- a/src/Http/Authentication.Core/src/AuthenticationSchemeProvider.cs +++ b/src/Http/Authentication.Core/src/AuthenticationSchemeProvider.cs @@ -8,201 +8,200 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Implements . +/// +public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider { /// - /// Implements . + /// Creates an instance of + /// using the specified , /// - public class AuthenticationSchemeProvider : IAuthenticationSchemeProvider + /// The options. + public AuthenticationSchemeProvider(IOptions options) + : this(options, new Dictionary(StringComparer.Ordinal)) { - /// - /// Creates an instance of - /// using the specified , - /// - /// The options. - public AuthenticationSchemeProvider(IOptions options) - : this(options, new Dictionary(StringComparer.Ordinal)) + } + + /// + /// Creates an instance of + /// using the specified and . + /// + /// The options. + /// The dictionary used to store authentication schemes. + protected AuthenticationSchemeProvider(IOptions options, IDictionary schemes) + { + _options = options.Value; + + _schemes = schemes ?? throw new ArgumentNullException(nameof(schemes)); + _requestHandlers = new List(); + + foreach (var builder in _options.Schemes) { + var scheme = builder.Build(); + AddScheme(scheme); } + } - /// - /// Creates an instance of - /// using the specified and . - /// - /// The options. - /// The dictionary used to store authentication schemes. - protected AuthenticationSchemeProvider(IOptions options, IDictionary schemes) - { - _options = options.Value; + private readonly AuthenticationOptions _options; + private readonly object _lock = new object(); - _schemes = schemes ?? throw new ArgumentNullException(nameof(schemes)); - _requestHandlers = new List(); + private readonly IDictionary _schemes; + private readonly List _requestHandlers; + // Used as a safe return value for enumeration apis + private IEnumerable _schemesCopy = Array.Empty(); + private IEnumerable _requestHandlersCopy = Array.Empty(); - foreach (var builder in _options.Schemes) - { - var scheme = builder.Build(); - AddScheme(scheme); - } - } + private Task GetDefaultSchemeAsync() + => _options.DefaultScheme != null + ? GetSchemeAsync(_options.DefaultScheme) + : Task.FromResult(null); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + public virtual Task GetDefaultAuthenticateSchemeAsync() + => _options.DefaultAuthenticateScheme != null + ? GetSchemeAsync(_options.DefaultAuthenticateScheme) + : GetDefaultSchemeAsync(); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + public virtual Task GetDefaultChallengeSchemeAsync() + => _options.DefaultChallengeScheme != null + ? GetSchemeAsync(_options.DefaultChallengeScheme) + : GetDefaultSchemeAsync(); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + public virtual Task GetDefaultForbidSchemeAsync() + => _options.DefaultForbidScheme != null + ? GetSchemeAsync(_options.DefaultForbidScheme) + : GetDefaultChallengeSchemeAsync(); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise, this will fallback to . + /// + /// The scheme that will be used by default for . + public virtual Task GetDefaultSignInSchemeAsync() + => _options.DefaultSignInScheme != null + ? GetSchemeAsync(_options.DefaultSignInScheme) + : GetDefaultSchemeAsync(); + + /// + /// Returns the scheme that will be used by default for . + /// This is typically specified via . + /// Otherwise this will fallback to if that supports sign out. + /// + /// The scheme that will be used by default for . + public virtual Task GetDefaultSignOutSchemeAsync() + => _options.DefaultSignOutScheme != null + ? GetSchemeAsync(_options.DefaultSignOutScheme) + : GetDefaultSignInSchemeAsync(); + + /// + /// Returns the matching the name, or null. + /// + /// The name of the authenticationScheme. + /// The scheme or null if not found. + public virtual Task GetSchemeAsync(string name) + => Task.FromResult(_schemes.ContainsKey(name) ? _schemes[name] : null); + + /// + /// Returns the schemes in priority order for request handling. + /// + /// The schemes in priority order for request handling + public virtual Task> GetRequestHandlerSchemesAsync() + => Task.FromResult(_requestHandlersCopy); - private readonly AuthenticationOptions _options; - private readonly object _lock = new object(); - - private readonly IDictionary _schemes; - private readonly List _requestHandlers; - // Used as a safe return value for enumeration apis - private IEnumerable _schemesCopy = Array.Empty(); - private IEnumerable _requestHandlersCopy = Array.Empty(); - - private Task GetDefaultSchemeAsync() - => _options.DefaultScheme != null - ? GetSchemeAsync(_options.DefaultScheme) - : Task.FromResult(null); - - /// - /// Returns the scheme that will be used by default for . - /// This is typically specified via . - /// Otherwise, this will fallback to . - /// - /// The scheme that will be used by default for . - public virtual Task GetDefaultAuthenticateSchemeAsync() - => _options.DefaultAuthenticateScheme != null - ? GetSchemeAsync(_options.DefaultAuthenticateScheme) - : GetDefaultSchemeAsync(); - - /// - /// Returns the scheme that will be used by default for . - /// This is typically specified via . - /// Otherwise, this will fallback to . - /// - /// The scheme that will be used by default for . - public virtual Task GetDefaultChallengeSchemeAsync() - => _options.DefaultChallengeScheme != null - ? GetSchemeAsync(_options.DefaultChallengeScheme) - : GetDefaultSchemeAsync(); - - /// - /// Returns the scheme that will be used by default for . - /// This is typically specified via . - /// Otherwise, this will fallback to . - /// - /// The scheme that will be used by default for . - public virtual Task GetDefaultForbidSchemeAsync() - => _options.DefaultForbidScheme != null - ? GetSchemeAsync(_options.DefaultForbidScheme) - : GetDefaultChallengeSchemeAsync(); - - /// - /// Returns the scheme that will be used by default for . - /// This is typically specified via . - /// Otherwise, this will fallback to . - /// - /// The scheme that will be used by default for . - public virtual Task GetDefaultSignInSchemeAsync() - => _options.DefaultSignInScheme != null - ? GetSchemeAsync(_options.DefaultSignInScheme) - : GetDefaultSchemeAsync(); - - /// - /// Returns the scheme that will be used by default for . - /// This is typically specified via . - /// Otherwise this will fallback to if that supports sign out. - /// - /// The scheme that will be used by default for . - public virtual Task GetDefaultSignOutSchemeAsync() - => _options.DefaultSignOutScheme != null - ? GetSchemeAsync(_options.DefaultSignOutScheme) - : GetDefaultSignInSchemeAsync(); - - /// - /// Returns the matching the name, or null. - /// - /// The name of the authenticationScheme. - /// The scheme or null if not found. - public virtual Task GetSchemeAsync(string name) - => Task.FromResult(_schemes.ContainsKey(name) ? _schemes[name] : null); - - /// - /// Returns the schemes in priority order for request handling. - /// - /// The schemes in priority order for request handling - public virtual Task> GetRequestHandlerSchemesAsync() - => Task.FromResult(_requestHandlersCopy); - - /// - /// Registers a scheme for use by . - /// - /// The scheme. - /// true if the scheme was added successfully. - public virtual bool TryAddScheme(AuthenticationScheme scheme) + /// + /// Registers a scheme for use by . + /// + /// The scheme. + /// true if the scheme was added successfully. + public virtual bool TryAddScheme(AuthenticationScheme scheme) + { + if (_schemes.ContainsKey(scheme.Name)) + { + return false; + } + lock (_lock) { if (_schemes.ContainsKey(scheme.Name)) { return false; } - lock (_lock) + if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType)) { - if (_schemes.ContainsKey(scheme.Name)) - { - return false; - } - if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType)) - { - _requestHandlers.Add(scheme); - _requestHandlersCopy = _requestHandlers.ToArray(); - } - _schemes[scheme.Name] = scheme; - _schemesCopy = _schemes.Values.ToArray(); - return true; + _requestHandlers.Add(scheme); + _requestHandlersCopy = _requestHandlers.ToArray(); } + _schemes[scheme.Name] = scheme; + _schemesCopy = _schemes.Values.ToArray(); + return true; } + } - /// - /// Registers a scheme for use by . - /// - /// The scheme. - public virtual void AddScheme(AuthenticationScheme scheme) + /// + /// Registers a scheme for use by . + /// + /// The scheme. + public virtual void AddScheme(AuthenticationScheme scheme) + { + if (_schemes.ContainsKey(scheme.Name)) { - if (_schemes.ContainsKey(scheme.Name)) + throw new InvalidOperationException("Scheme already exists: " + scheme.Name); + } + lock (_lock) + { + if (!TryAddScheme(scheme)) { throw new InvalidOperationException("Scheme already exists: " + scheme.Name); } - lock (_lock) - { - if (!TryAddScheme(scheme)) - { - throw new InvalidOperationException("Scheme already exists: " + scheme.Name); - } - } } + } - /// - /// Removes a scheme, preventing it from being used by . - /// - /// The name of the authenticationScheme being removed. - public virtual void RemoveScheme(string name) + /// + /// Removes a scheme, preventing it from being used by . + /// + /// The name of the authenticationScheme being removed. + public virtual void RemoveScheme(string name) + { + if (!_schemes.ContainsKey(name)) { - if (!_schemes.ContainsKey(name)) - { - return; - } - lock (_lock) + return; + } + lock (_lock) + { + if (_schemes.ContainsKey(name)) { - if (_schemes.ContainsKey(name)) + var scheme = _schemes[name]; + if (_requestHandlers.Remove(scheme)) { - var scheme = _schemes[name]; - if (_requestHandlers.Remove(scheme)) - { - _requestHandlersCopy = _requestHandlers.ToArray(); - } - _schemes.Remove(name); - _schemesCopy = _schemes.Values.ToArray(); + _requestHandlersCopy = _requestHandlers.ToArray(); } + _schemes.Remove(name); + _schemesCopy = _schemes.Values.ToArray(); } } - - /// - public virtual Task> GetAllSchemesAsync() - => Task.FromResult(_schemesCopy); } + + /// + public virtual Task> GetAllSchemesAsync() + => Task.FromResult(_schemesCopy); } diff --git a/src/Http/Authentication.Core/src/AuthenticationService.cs b/src/Http/Authentication.Core/src/AuthenticationService.cs index 9e691585e5..8444ffcf51 100644 --- a/src/Http/Authentication.Core/src/AuthenticationService.cs +++ b/src/Http/Authentication.Core/src/AuthenticationService.cs @@ -9,332 +9,331 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Implements . +/// +public class AuthenticationService : IAuthenticationService { + private HashSet? _transformCache; + /// - /// Implements . + /// Constructor. /// - public class AuthenticationService : IAuthenticationService + /// The . + /// The . + /// The . + /// The . + public AuthenticationService(IAuthenticationSchemeProvider schemes, IAuthenticationHandlerProvider handlers, IClaimsTransformation transform, IOptions options) { - private HashSet? _transformCache; - - /// - /// Constructor. - /// - /// The . - /// The . - /// The . - /// The . - public AuthenticationService(IAuthenticationSchemeProvider schemes, IAuthenticationHandlerProvider handlers, IClaimsTransformation transform, IOptions options) - { - Schemes = schemes; - Handlers = handlers; - Transform = transform; - Options = options.Value; - } + Schemes = schemes; + Handlers = handlers; + Transform = transform; + Options = options.Value; + } - /// - /// Used to lookup AuthenticationSchemes. - /// - public IAuthenticationSchemeProvider Schemes { get; } - - /// - /// Used to resolve IAuthenticationHandler instances. - /// - public IAuthenticationHandlerProvider Handlers { get; } - - /// - /// Used for claims transformation. - /// - public IClaimsTransformation Transform { get; } - - /// - /// The . - /// - public AuthenticationOptions Options { get; } - - /// - /// Authenticate for the specified authentication scheme. - /// - /// The . - /// The name of the authentication scheme. - /// The result. - public virtual async Task AuthenticateAsync(HttpContext context, string? scheme) - { - if (scheme == null) - { - var defaultScheme = await Schemes.GetDefaultAuthenticateSchemeAsync(); - scheme = defaultScheme?.Name; - if (scheme == null) - { - throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action configureOptions)."); - } - } + /// + /// Used to lookup AuthenticationSchemes. + /// + public IAuthenticationSchemeProvider Schemes { get; } - var handler = await Handlers.GetHandlerAsync(context, scheme); - if (handler == null) - { - throw await CreateMissingHandlerException(scheme); - } + /// + /// Used to resolve IAuthenticationHandler instances. + /// + public IAuthenticationHandlerProvider Handlers { get; } - // Handlers should not return null, but we'll be tolerant of null values for legacy reasons. - var result = (await handler.AuthenticateAsync()) ?? AuthenticateResult.NoResult(); + /// + /// Used for claims transformation. + /// + public IClaimsTransformation Transform { get; } - if (result.Succeeded) - { - var principal = result.Principal!; - var doTransform = true; - _transformCache ??= new HashSet(); - if (_transformCache.Contains(principal)) - { - doTransform = false; - } - - if (doTransform) - { - principal = await Transform.TransformAsync(principal); - _transformCache.Add(principal); - } - return AuthenticateResult.Success(new AuthenticationTicket(principal, result.Properties, result.Ticket!.AuthenticationScheme)); - } - return result; - } + /// + /// The . + /// + public AuthenticationOptions Options { get; } - /// - /// Challenge the specified authentication scheme. - /// - /// The . - /// The name of the authentication scheme. - /// The . - /// A task. - public virtual async Task ChallengeAsync(HttpContext context, string? scheme, AuthenticationProperties? properties) + /// + /// Authenticate for the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The result. + public virtual async Task AuthenticateAsync(HttpContext context, string? scheme) + { + if (scheme == null) { + var defaultScheme = await Schemes.GetDefaultAuthenticateSchemeAsync(); + scheme = defaultScheme?.Name; if (scheme == null) { - var defaultChallengeScheme = await Schemes.GetDefaultChallengeSchemeAsync(); - scheme = defaultChallengeScheme?.Name; - if (scheme == null) - { - throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action configureOptions)."); - } - } - - var handler = await Handlers.GetHandlerAsync(context, scheme); - if (handler == null) - { - throw await CreateMissingHandlerException(scheme); + throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultAuthenticateScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action configureOptions)."); } + } - await handler.ChallengeAsync(properties); + var handler = await Handlers.GetHandlerAsync(context, scheme); + if (handler == null) + { + throw await CreateMissingHandlerException(scheme); } - /// - /// Forbid the specified authentication scheme. - /// - /// The . - /// The name of the authentication scheme. - /// The . - /// A task. - public virtual async Task ForbidAsync(HttpContext context, string? scheme, AuthenticationProperties? properties) + // Handlers should not return null, but we'll be tolerant of null values for legacy reasons. + var result = (await handler.AuthenticateAsync()) ?? AuthenticateResult.NoResult(); + + if (result.Succeeded) { - if (scheme == null) + var principal = result.Principal!; + var doTransform = true; + _transformCache ??= new HashSet(); + if (_transformCache.Contains(principal)) { - var defaultForbidScheme = await Schemes.GetDefaultForbidSchemeAsync(); - scheme = defaultForbidScheme?.Name; - if (scheme == null) - { - throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultForbidScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action configureOptions)."); - } + doTransform = false; } - var handler = await Handlers.GetHandlerAsync(context, scheme); - if (handler == null) + if (doTransform) { - throw await CreateMissingHandlerException(scheme); + principal = await Transform.TransformAsync(principal); + _transformCache.Add(principal); } - - await handler.ForbidAsync(properties); + return AuthenticateResult.Success(new AuthenticationTicket(principal, result.Properties, result.Ticket!.AuthenticationScheme)); } + return result; + } - /// - /// Sign a principal in for the specified authentication scheme. - /// - /// The . - /// The name of the authentication scheme. - /// The to sign in. - /// The . - /// A task. - public virtual async Task SignInAsync(HttpContext context, string? scheme, ClaimsPrincipal principal, AuthenticationProperties? properties) + /// + /// Challenge the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The . + /// A task. + public virtual async Task ChallengeAsync(HttpContext context, string? scheme, AuthenticationProperties? properties) + { + if (scheme == null) { - if (principal == null) + var defaultChallengeScheme = await Schemes.GetDefaultChallengeSchemeAsync(); + scheme = defaultChallengeScheme?.Name; + if (scheme == null) { - throw new ArgumentNullException(nameof(principal)); + throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action configureOptions)."); } + } - if (Options.RequireAuthenticatedSignIn) - { - if (principal.Identity == null) - { - throw new InvalidOperationException("SignInAsync when principal.Identity == null is not allowed when AuthenticationOptions.RequireAuthenticatedSignIn is true."); - } - if (!principal.Identity.IsAuthenticated) - { - throw new InvalidOperationException("SignInAsync when principal.Identity.IsAuthenticated is false is not allowed when AuthenticationOptions.RequireAuthenticatedSignIn is true."); - } - } + var handler = await Handlers.GetHandlerAsync(context, scheme); + if (handler == null) + { + throw await CreateMissingHandlerException(scheme); + } + + await handler.ChallengeAsync(properties); + } + /// + /// Forbid the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The . + /// A task. + public virtual async Task ForbidAsync(HttpContext context, string? scheme, AuthenticationProperties? properties) + { + if (scheme == null) + { + var defaultForbidScheme = await Schemes.GetDefaultForbidSchemeAsync(); + scheme = defaultForbidScheme?.Name; if (scheme == null) { - var defaultScheme = await Schemes.GetDefaultSignInSchemeAsync(); - scheme = defaultScheme?.Name; - if (scheme == null) - { - throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignInScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action configureOptions)."); - } + throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultForbidScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action configureOptions)."); } + } - var handler = await Handlers.GetHandlerAsync(context, scheme); - if (handler == null) - { - throw await CreateMissingSignInHandlerException(scheme); - } + var handler = await Handlers.GetHandlerAsync(context, scheme); + if (handler == null) + { + throw await CreateMissingHandlerException(scheme); + } - var signInHandler = handler as IAuthenticationSignInHandler; - if (signInHandler == null) - { - throw await CreateMismatchedSignInHandlerException(scheme, handler); - } + await handler.ForbidAsync(properties); + } - await signInHandler.SignInAsync(principal, properties); + /// + /// Sign a principal in for the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The to sign in. + /// The . + /// A task. + public virtual async Task SignInAsync(HttpContext context, string? scheme, ClaimsPrincipal principal, AuthenticationProperties? properties) + { + if (principal == null) + { + throw new ArgumentNullException(nameof(principal)); } - /// - /// Sign out the specified authentication scheme. - /// - /// The . - /// The name of the authentication scheme. - /// The . - /// A task. - public virtual async Task SignOutAsync(HttpContext context, string? scheme, AuthenticationProperties? properties) + if (Options.RequireAuthenticatedSignIn) { - if (scheme == null) + if (principal.Identity == null) { - var defaultScheme = await Schemes.GetDefaultSignOutSchemeAsync(); - scheme = defaultScheme?.Name; - if (scheme == null) - { - throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignOutScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action configureOptions)."); - } + throw new InvalidOperationException("SignInAsync when principal.Identity == null is not allowed when AuthenticationOptions.RequireAuthenticatedSignIn is true."); } - - var handler = await Handlers.GetHandlerAsync(context, scheme); - if (handler == null) + if (!principal.Identity.IsAuthenticated) { - throw await CreateMissingSignOutHandlerException(scheme); + throw new InvalidOperationException("SignInAsync when principal.Identity.IsAuthenticated is false is not allowed when AuthenticationOptions.RequireAuthenticatedSignIn is true."); } + } - var signOutHandler = handler as IAuthenticationSignOutHandler; - if (signOutHandler == null) + if (scheme == null) + { + var defaultScheme = await Schemes.GetDefaultSignInSchemeAsync(); + scheme = defaultScheme?.Name; + if (scheme == null) { - throw await CreateMismatchedSignOutHandlerException(scheme, handler); + throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignInScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action configureOptions)."); } + } - await signOutHandler.SignOutAsync(properties); + var handler = await Handlers.GetHandlerAsync(context, scheme); + if (handler == null) + { + throw await CreateMissingSignInHandlerException(scheme); } - private async Task CreateMissingHandlerException(string scheme) + var signInHandler = handler as IAuthenticationSignInHandler; + if (signInHandler == null) { - var schemes = string.Join(", ", (await Schemes.GetAllSchemesAsync()).Select(sch => sch.Name)); + throw await CreateMismatchedSignInHandlerException(scheme, handler); + } - var footer = $" Did you forget to call AddAuthentication().Add[SomeAuthHandler](\"{scheme}\",...)?"; + await signInHandler.SignInAsync(principal, properties); + } - if (string.IsNullOrEmpty(schemes)) + /// + /// Sign out the specified authentication scheme. + /// + /// The . + /// The name of the authentication scheme. + /// The . + /// A task. + public virtual async Task SignOutAsync(HttpContext context, string? scheme, AuthenticationProperties? properties) + { + if (scheme == null) + { + var defaultScheme = await Schemes.GetDefaultSignOutSchemeAsync(); + scheme = defaultScheme?.Name; + if (scheme == null) { - return new InvalidOperationException( - $"No authentication handlers are registered." + footer); + throw new InvalidOperationException($"No authenticationScheme was specified, and there was no DefaultSignOutScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action configureOptions)."); } - - return new InvalidOperationException( - $"No authentication handler is registered for the scheme '{scheme}'. The registered schemes are: {schemes}." + footer); } - private async Task GetAllSignInSchemeNames() + var handler = await Handlers.GetHandlerAsync(context, scheme); + if (handler == null) { - return string.Join(", ", (await Schemes.GetAllSchemesAsync()) - .Where(sch => typeof(IAuthenticationSignInHandler).IsAssignableFrom(sch.HandlerType)) - .Select(sch => sch.Name)); + throw await CreateMissingSignOutHandlerException(scheme); } - private async Task CreateMissingSignInHandlerException(string scheme) + var signOutHandler = handler as IAuthenticationSignOutHandler; + if (signOutHandler == null) { - var schemes = await GetAllSignInSchemeNames(); + throw await CreateMismatchedSignOutHandlerException(scheme, handler); + } - // CookieAuth is the only implementation of sign-in. - var footer = $" Did you forget to call AddAuthentication().AddCookie(\"{scheme}\",...)?"; + await signOutHandler.SignOutAsync(properties); + } - if (string.IsNullOrEmpty(schemes)) - { - return new InvalidOperationException( - $"No sign-in authentication handlers are registered." + footer); - } + private async Task CreateMissingHandlerException(string scheme) + { + var schemes = string.Join(", ", (await Schemes.GetAllSchemesAsync()).Select(sch => sch.Name)); + + var footer = $" Did you forget to call AddAuthentication().Add[SomeAuthHandler](\"{scheme}\",...)?"; + if (string.IsNullOrEmpty(schemes)) + { return new InvalidOperationException( - $"No sign-in authentication handler is registered for the scheme '{scheme}'. The registered sign-in schemes are: {schemes}." + footer); + $"No authentication handlers are registered." + footer); } - private async Task CreateMismatchedSignInHandlerException(string scheme, IAuthenticationHandler handler) - { - var schemes = await GetAllSignInSchemeNames(); + return new InvalidOperationException( + $"No authentication handler is registered for the scheme '{scheme}'. The registered schemes are: {schemes}." + footer); + } - var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for SignInAsync. "; + private async Task GetAllSignInSchemeNames() + { + return string.Join(", ", (await Schemes.GetAllSchemesAsync()) + .Where(sch => typeof(IAuthenticationSignInHandler).IsAssignableFrom(sch.HandlerType)) + .Select(sch => sch.Name)); + } - if (string.IsNullOrEmpty(schemes)) - { - // CookieAuth is the only implementation of sign-in. - return new InvalidOperationException(mismatchError - + $"Did you forget to call AddAuthentication().AddCookie(\"Cookies\") and SignInAsync(\"Cookies\",...)?"); - } + private async Task CreateMissingSignInHandlerException(string scheme) + { + var schemes = await GetAllSignInSchemeNames(); - return new InvalidOperationException(mismatchError + $"The registered sign-in schemes are: {schemes}."); - } + // CookieAuth is the only implementation of sign-in. + var footer = $" Did you forget to call AddAuthentication().AddCookie(\"{scheme}\",...)?"; - private async Task GetAllSignOutSchemeNames() + if (string.IsNullOrEmpty(schemes)) { - return string.Join(", ", (await Schemes.GetAllSchemesAsync()) - .Where(sch => typeof(IAuthenticationSignOutHandler).IsAssignableFrom(sch.HandlerType)) - .Select(sch => sch.Name)); + return new InvalidOperationException( + $"No sign-in authentication handlers are registered." + footer); } - private async Task CreateMissingSignOutHandlerException(string scheme) - { - var schemes = await GetAllSignOutSchemeNames(); + return new InvalidOperationException( + $"No sign-in authentication handler is registered for the scheme '{scheme}'. The registered sign-in schemes are: {schemes}." + footer); + } - var footer = $" Did you forget to call AddAuthentication().AddCookie(\"{scheme}\",...)?"; + private async Task CreateMismatchedSignInHandlerException(string scheme, IAuthenticationHandler handler) + { + var schemes = await GetAllSignInSchemeNames(); - if (string.IsNullOrEmpty(schemes)) - { - // CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it. - return new InvalidOperationException($"No sign-out authentication handlers are registered." + footer); - } + var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for SignInAsync. "; - return new InvalidOperationException( - $"No sign-out authentication handler is registered for the scheme '{scheme}'. The registered sign-out schemes are: {schemes}." + footer); + if (string.IsNullOrEmpty(schemes)) + { + // CookieAuth is the only implementation of sign-in. + return new InvalidOperationException(mismatchError + + $"Did you forget to call AddAuthentication().AddCookie(\"Cookies\") and SignInAsync(\"Cookies\",...)?"); } - private async Task CreateMismatchedSignOutHandlerException(string scheme, IAuthenticationHandler handler) + return new InvalidOperationException(mismatchError + $"The registered sign-in schemes are: {schemes}."); + } + + private async Task GetAllSignOutSchemeNames() + { + return string.Join(", ", (await Schemes.GetAllSchemesAsync()) + .Where(sch => typeof(IAuthenticationSignOutHandler).IsAssignableFrom(sch.HandlerType)) + .Select(sch => sch.Name)); + } + + private async Task CreateMissingSignOutHandlerException(string scheme) + { + var schemes = await GetAllSignOutSchemeNames(); + + var footer = $" Did you forget to call AddAuthentication().AddCookie(\"{scheme}\",...)?"; + + if (string.IsNullOrEmpty(schemes)) { - var schemes = await GetAllSignOutSchemeNames(); + // CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it. + return new InvalidOperationException($"No sign-out authentication handlers are registered." + footer); + } - var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for {nameof(SignOutAsync)}. "; + return new InvalidOperationException( + $"No sign-out authentication handler is registered for the scheme '{scheme}'. The registered sign-out schemes are: {schemes}." + footer); + } - if (string.IsNullOrEmpty(schemes)) - { - // CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it. - return new InvalidOperationException(mismatchError - + $"Did you forget to call AddAuthentication().AddCookie(\"Cookies\") and {nameof(SignOutAsync)}(\"Cookies\",...)?"); - } + private async Task CreateMismatchedSignOutHandlerException(string scheme, IAuthenticationHandler handler) + { + var schemes = await GetAllSignOutSchemeNames(); - return new InvalidOperationException(mismatchError + $"The registered sign-out schemes are: {schemes}."); + var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for {nameof(SignOutAsync)}. "; + + if (string.IsNullOrEmpty(schemes)) + { + // CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it. + return new InvalidOperationException(mismatchError + + $"Did you forget to call AddAuthentication().AddCookie(\"Cookies\") and {nameof(SignOutAsync)}(\"Cookies\",...)?"); } + + return new InvalidOperationException(mismatchError + $"The registered sign-out schemes are: {schemes}."); } } diff --git a/src/Http/Authentication.Core/src/NoopClaimsTransformation.cs b/src/Http/Authentication.Core/src/NoopClaimsTransformation.cs index 68510f8a69..dda2690a30 100644 --- a/src/Http/Authentication.Core/src/NoopClaimsTransformation.cs +++ b/src/Http/Authentication.Core/src/NoopClaimsTransformation.cs @@ -4,21 +4,20 @@ using System.Security.Claims; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Authentication +namespace Microsoft.AspNetCore.Authentication; + +/// +/// Default claims transformation is a no-op. +/// +public class NoopClaimsTransformation : IClaimsTransformation { /// - /// Default claims transformation is a no-op. + /// Returns the principal unchanged. /// - public class NoopClaimsTransformation : IClaimsTransformation + /// The user. + /// The principal unchanged. + public virtual Task TransformAsync(ClaimsPrincipal principal) { - /// - /// Returns the principal unchanged. - /// - /// The user. - /// The principal unchanged. - public virtual Task TransformAsync(ClaimsPrincipal principal) - { - return Task.FromResult(principal); - } + return Task.FromResult(principal); } } diff --git a/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs b/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs index 725803873c..bbe00de0bd 100644 --- a/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs +++ b/src/Http/Authentication.Core/test/AuthenticationPropertiesTests.cs @@ -8,379 +8,379 @@ using System.Linq; using System.Text.Json; using Xunit; -namespace Microsoft.AspNetCore.Authentication.Core.Test +namespace Microsoft.AspNetCore.Authentication.Core.Test; + +public class AuthenticationPropertiesTests { - public class AuthenticationPropertiesTests + [Fact] + public void Clone_Copies() { - [Fact] - public void Clone_Copies() + var items = new Dictionary { - var items = new Dictionary - { - ["foo"] = "bar", - }; - var value = "value"; - var parameters = new Dictionary - { - ["foo2"] = value, - }; - var props = new AuthenticationProperties(items, parameters); - Assert.Same(items, props.Items); - Assert.Same(parameters, props.Parameters); - var copy = props.Clone(); - Assert.NotSame(props.Items, copy.Items); - Assert.NotSame(props.Parameters, copy.Parameters); - // Objects in the dictionaries will still be the same - Assert.Equal(props.Items, copy.Items); - Assert.Equal(props.Parameters, copy.Parameters); - props.Items["change"] = "good"; - props.Parameters["something"] = "bad"; - Assert.NotEqual(props.Items, copy.Items); - Assert.NotEqual(props.Parameters, copy.Parameters); - } - - [Fact] - public void DefaultConstructor_EmptyCollections() + ["foo"] = "bar", + }; + var value = "value"; + var parameters = new Dictionary { - var props = new AuthenticationProperties(); - Assert.Empty(props.Items); - Assert.Empty(props.Parameters); - } + ["foo2"] = value, + }; + var props = new AuthenticationProperties(items, parameters); + Assert.Same(items, props.Items); + Assert.Same(parameters, props.Parameters); + var copy = props.Clone(); + Assert.NotSame(props.Items, copy.Items); + Assert.NotSame(props.Parameters, copy.Parameters); + // Objects in the dictionaries will still be the same + Assert.Equal(props.Items, copy.Items); + Assert.Equal(props.Parameters, copy.Parameters); + props.Items["change"] = "good"; + props.Parameters["something"] = "bad"; + Assert.NotEqual(props.Items, copy.Items); + Assert.NotEqual(props.Parameters, copy.Parameters); + } - [Fact] - public void ItemsConstructor_ReusesItemsDictionary() - { - var items = new Dictionary - { - ["foo"] = "bar", - }; - var props = new AuthenticationProperties(items); - Assert.Same(items, props.Items); - Assert.Empty(props.Parameters); - } + [Fact] + public void DefaultConstructor_EmptyCollections() + { + var props = new AuthenticationProperties(); + Assert.Empty(props.Items); + Assert.Empty(props.Parameters); + } - [Fact] - public void FullConstructor_ReusesDictionaries() + [Fact] + public void ItemsConstructor_ReusesItemsDictionary() + { + var items = new Dictionary { - var items = new Dictionary - { - ["foo"] = "bar", - }; - var parameters = new Dictionary - { - ["number"] = 1234, - ["list"] = new List { "a", "b", "c" }, - }; - var props = new AuthenticationProperties(items, parameters); - Assert.Same(items, props.Items); - Assert.Same(parameters, props.Parameters); - } + ["foo"] = "bar", + }; + var props = new AuthenticationProperties(items); + Assert.Same(items, props.Items); + Assert.Empty(props.Parameters); + } - [Fact] - public void GetSetString() + [Fact] + public void FullConstructor_ReusesDictionaries() + { + var items = new Dictionary { - var props = new AuthenticationProperties(); - Assert.Null(props.GetString("foo")); - Assert.Equal(0, props.Items.Count); - - props.SetString("foo", "foo bar"); - Assert.Equal("foo bar", props.GetString("foo")); - Assert.Equal("foo bar", props.Items["foo"]); - Assert.Equal(1, props.Items.Count); - - props.SetString("foo", "foo baz"); - Assert.Equal("foo baz", props.GetString("foo")); - Assert.Equal("foo baz", props.Items["foo"]); - Assert.Equal(1, props.Items.Count); - - props.SetString("bar", "xy"); - Assert.Equal("xy", props.GetString("bar")); - Assert.Equal("xy", props.Items["bar"]); - Assert.Equal(2, props.Items.Count); - - props.SetString("bar", string.Empty); - Assert.Equal(string.Empty, props.GetString("bar")); - Assert.Equal(string.Empty, props.Items["bar"]); - - props.SetString("foo", null); - Assert.Null(props.GetString("foo")); - Assert.Equal(1, props.Items.Count); - - props.SetString("doesntexist", null); - Assert.False(props.Items.ContainsKey("doesntexist")); - Assert.Equal(1, props.Items.Count); - } - - [Fact] - public void GetSetParameter_String() + ["foo"] = "bar", + }; + var parameters = new Dictionary { - var props = new AuthenticationProperties(); - Assert.Null(props.GetParameter("foo")); - Assert.Equal(0, props.Parameters.Count); - - props.SetParameter("foo", "foo bar"); - Assert.Equal("foo bar", props.GetParameter("foo")); - Assert.Equal("foo bar", props.Parameters["foo"]); - Assert.Equal(1, props.Parameters.Count); - - props.SetParameter("foo", null); - Assert.Null(props.GetParameter("foo")); - Assert.Null(props.Parameters["foo"]); - Assert.Equal(1, props.Parameters.Count); - } + ["number"] = 1234, + ["list"] = new List { "a", "b", "c" }, + }; + var props = new AuthenticationProperties(items, parameters); + Assert.Same(items, props.Items); + Assert.Same(parameters, props.Parameters); + } - [Fact] - public void GetSetParameter_Int() - { - var props = new AuthenticationProperties(); - Assert.Null(props.GetParameter("foo")); - Assert.Equal(0, props.Parameters.Count); - - props.SetParameter("foo", 123); - Assert.Equal(123, props.GetParameter("foo")); - Assert.Equal(123, props.Parameters["foo"]); - Assert.Equal(1, props.Parameters.Count); - - props.SetParameter("foo", null); - Assert.Null(props.GetParameter("foo")); - Assert.Null(props.Parameters["foo"]); - Assert.Equal(1, props.Parameters.Count); - } + [Fact] + public void GetSetString() + { + var props = new AuthenticationProperties(); + Assert.Null(props.GetString("foo")); + Assert.Equal(0, props.Items.Count); + + props.SetString("foo", "foo bar"); + Assert.Equal("foo bar", props.GetString("foo")); + Assert.Equal("foo bar", props.Items["foo"]); + Assert.Equal(1, props.Items.Count); + + props.SetString("foo", "foo baz"); + Assert.Equal("foo baz", props.GetString("foo")); + Assert.Equal("foo baz", props.Items["foo"]); + Assert.Equal(1, props.Items.Count); + + props.SetString("bar", "xy"); + Assert.Equal("xy", props.GetString("bar")); + Assert.Equal("xy", props.Items["bar"]); + Assert.Equal(2, props.Items.Count); + + props.SetString("bar", string.Empty); + Assert.Equal(string.Empty, props.GetString("bar")); + Assert.Equal(string.Empty, props.Items["bar"]); + + props.SetString("foo", null); + Assert.Null(props.GetString("foo")); + Assert.Equal(1, props.Items.Count); + + props.SetString("doesntexist", null); + Assert.False(props.Items.ContainsKey("doesntexist")); + Assert.Equal(1, props.Items.Count); + } - [Fact] - public void GetSetParameter_Collection() - { - var props = new AuthenticationProperties(); - Assert.Null(props.GetParameter("foo")); - Assert.Equal(0, props.Parameters.Count); - - var list = new string[] { "a", "b", "c" }; - props.SetParameter>("foo", list); - Assert.Equal(new string[] { "a", "b", "c" }, props.GetParameter>("foo")); - Assert.Same(list, props.Parameters["foo"]); - Assert.Equal(1, props.Parameters.Count); - - props.SetParameter?>("foo", null); - Assert.Null(props.GetParameter>("foo")); - Assert.Null(props.Parameters["foo"]); - Assert.Equal(1, props.Parameters.Count); - } + [Fact] + public void GetSetParameter_String() + { + var props = new AuthenticationProperties(); + Assert.Null(props.GetParameter("foo")); + Assert.Equal(0, props.Parameters.Count); + + props.SetParameter("foo", "foo bar"); + Assert.Equal("foo bar", props.GetParameter("foo")); + Assert.Equal("foo bar", props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + + props.SetParameter("foo", null); + Assert.Null(props.GetParameter("foo")); + Assert.Null(props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + } - [Fact] - public void IsPersistent_Test() - { - var props = new AuthenticationProperties(); - Assert.False(props.IsPersistent); + [Fact] + public void GetSetParameter_Int() + { + var props = new AuthenticationProperties(); + Assert.Null(props.GetParameter("foo")); + Assert.Equal(0, props.Parameters.Count); + + props.SetParameter("foo", 123); + Assert.Equal(123, props.GetParameter("foo")); + Assert.Equal(123, props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + + props.SetParameter("foo", null); + Assert.Null(props.GetParameter("foo")); + Assert.Null(props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + } - props.IsPersistent = true; - Assert.True(props.IsPersistent); - Assert.Equal(string.Empty, props.Items.First().Value); + [Fact] + public void GetSetParameter_Collection() + { + var props = new AuthenticationProperties(); + Assert.Null(props.GetParameter("foo")); + Assert.Equal(0, props.Parameters.Count); + + var list = new string[] { "a", "b", "c" }; + props.SetParameter>("foo", list); + Assert.Equal(new string[] { "a", "b", "c" }, props.GetParameter>("foo")); + Assert.Same(list, props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + + props.SetParameter?>("foo", null); + Assert.Null(props.GetParameter>("foo")); + Assert.Null(props.Parameters["foo"]); + Assert.Equal(1, props.Parameters.Count); + } - props.Items.Clear(); - Assert.False(props.IsPersistent); - } + [Fact] + public void IsPersistent_Test() + { + var props = new AuthenticationProperties(); + Assert.False(props.IsPersistent); - [Fact] - public void RedirectUri_Test() - { - var props = new AuthenticationProperties(); - Assert.Null(props.RedirectUri); + props.IsPersistent = true; + Assert.True(props.IsPersistent); + Assert.Equal(string.Empty, props.Items.First().Value); - props.RedirectUri = "http://example.com"; - Assert.Equal("http://example.com", props.RedirectUri); - Assert.Equal("http://example.com", props.Items.First().Value); + props.Items.Clear(); + Assert.False(props.IsPersistent); + } - props.Items.Clear(); - Assert.Null(props.RedirectUri); - } + [Fact] + public void RedirectUri_Test() + { + var props = new AuthenticationProperties(); + Assert.Null(props.RedirectUri); - [Fact] - public void IssuedUtc_Test() - { - var props = new AuthenticationProperties(); - Assert.Null(props.IssuedUtc); + props.RedirectUri = "http://example.com"; + Assert.Equal("http://example.com", props.RedirectUri); + Assert.Equal("http://example.com", props.Items.First().Value); - props.IssuedUtc = new DateTimeOffset(new DateTime(2018, 03, 21, 0, 0, 0, DateTimeKind.Utc)); - Assert.Equal(new DateTimeOffset(new DateTime(2018, 03, 21, 0, 0, 0, DateTimeKind.Utc)), props.IssuedUtc); - Assert.Equal("Wed, 21 Mar 2018 00:00:00 GMT", props.Items.First().Value); + props.Items.Clear(); + Assert.Null(props.RedirectUri); + } - props.Items.Clear(); - Assert.Null(props.IssuedUtc); - } + [Fact] + public void IssuedUtc_Test() + { + var props = new AuthenticationProperties(); + Assert.Null(props.IssuedUtc); - [Fact] - public void ExpiresUtc_Test() - { - var props = new AuthenticationProperties(); - Assert.Null(props.ExpiresUtc); + props.IssuedUtc = new DateTimeOffset(new DateTime(2018, 03, 21, 0, 0, 0, DateTimeKind.Utc)); + Assert.Equal(new DateTimeOffset(new DateTime(2018, 03, 21, 0, 0, 0, DateTimeKind.Utc)), props.IssuedUtc); + Assert.Equal("Wed, 21 Mar 2018 00:00:00 GMT", props.Items.First().Value); - props.ExpiresUtc = new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc)); - Assert.Equal(new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc)), props.ExpiresUtc); - Assert.Equal("Mon, 19 Mar 2018 12:34:56 GMT", props.Items.First().Value); + props.Items.Clear(); + Assert.Null(props.IssuedUtc); + } - props.Items.Clear(); - Assert.Null(props.ExpiresUtc); - } + [Fact] + public void ExpiresUtc_Test() + { + var props = new AuthenticationProperties(); + Assert.Null(props.ExpiresUtc); - [Fact] - public void AllowRefresh_Test() - { - var props = new AuthenticationProperties(); - Assert.Null(props.AllowRefresh); + props.ExpiresUtc = new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc)); + Assert.Equal(new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc)), props.ExpiresUtc); + Assert.Equal("Mon, 19 Mar 2018 12:34:56 GMT", props.Items.First().Value); - props.AllowRefresh = true; - Assert.True(props.AllowRefresh); - Assert.Equal("True", props.Items.First().Value); + props.Items.Clear(); + Assert.Null(props.ExpiresUtc); + } - props.AllowRefresh = false; - Assert.False(props.AllowRefresh); - Assert.Equal("False", props.Items.First().Value); + [Fact] + public void AllowRefresh_Test() + { + var props = new AuthenticationProperties(); + Assert.Null(props.AllowRefresh); - props.Items.Clear(); - Assert.Null(props.AllowRefresh); - } + props.AllowRefresh = true; + Assert.True(props.AllowRefresh); + Assert.Equal("True", props.Items.First().Value); - [Fact] - public void SetDateTimeOffset() - { - var props = new MyAuthenticationProperties(); + props.AllowRefresh = false; + Assert.False(props.AllowRefresh); + Assert.Equal("False", props.Items.First().Value); - props.SetDateTimeOffset("foo", new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc))); - Assert.Equal("Mon, 19 Mar 2018 12:34:56 GMT", props.Items["foo"]); + props.Items.Clear(); + Assert.Null(props.AllowRefresh); + } - props.SetDateTimeOffset("foo", null); - Assert.False(props.Items.ContainsKey("foo")); + [Fact] + public void SetDateTimeOffset() + { + var props = new MyAuthenticationProperties(); - props.SetDateTimeOffset("doesnotexist", null); - Assert.False(props.Items.ContainsKey("doesnotexist")); - } + props.SetDateTimeOffset("foo", new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc))); + Assert.Equal("Mon, 19 Mar 2018 12:34:56 GMT", props.Items["foo"]); - [Fact] - public void GetDateTimeOffset() - { - var props = new MyAuthenticationProperties(); - var dateTimeOffset = new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc)); + props.SetDateTimeOffset("foo", null); + Assert.False(props.Items.ContainsKey("foo")); + + props.SetDateTimeOffset("doesnotexist", null); + Assert.False(props.Items.ContainsKey("doesnotexist")); + } - props.Items["foo"] = dateTimeOffset.ToString("r", CultureInfo.InvariantCulture); - Assert.Equal(dateTimeOffset, props.GetDateTimeOffset("foo")); + [Fact] + public void GetDateTimeOffset() + { + var props = new MyAuthenticationProperties(); + var dateTimeOffset = new DateTimeOffset(new DateTime(2018, 03, 19, 12, 34, 56, DateTimeKind.Utc)); - props.Items.Remove("foo"); - Assert.Null(props.GetDateTimeOffset("foo")); + props.Items["foo"] = dateTimeOffset.ToString("r", CultureInfo.InvariantCulture); + Assert.Equal(dateTimeOffset, props.GetDateTimeOffset("foo")); - props.Items["foo"] = "BAR"; - Assert.Null(props.GetDateTimeOffset("foo")); - Assert.Equal("BAR", props.Items["foo"]); - } + props.Items.Remove("foo"); + Assert.Null(props.GetDateTimeOffset("foo")); - [Fact] - public void SetBool() - { - var props = new MyAuthenticationProperties(); + props.Items["foo"] = "BAR"; + Assert.Null(props.GetDateTimeOffset("foo")); + Assert.Equal("BAR", props.Items["foo"]); + } - props.SetBool("foo", true); - Assert.Equal(true.ToString(), props.Items["foo"]); + [Fact] + public void SetBool() + { + var props = new MyAuthenticationProperties(); - props.SetBool("foo", false); - Assert.Equal(false.ToString(), props.Items["foo"]); + props.SetBool("foo", true); + Assert.Equal(true.ToString(), props.Items["foo"]); - props.SetBool("foo", null); - Assert.False(props.Items.ContainsKey("foo")); - } + props.SetBool("foo", false); + Assert.Equal(false.ToString(), props.Items["foo"]); - [Fact] - public void GetBool() - { - var props = new MyAuthenticationProperties(); + props.SetBool("foo", null); + Assert.False(props.Items.ContainsKey("foo")); + } - props.Items["foo"] = true.ToString(); - Assert.True(props.GetBool("foo")); + [Fact] + public void GetBool() + { + var props = new MyAuthenticationProperties(); - props.Items["foo"] = false.ToString(); - Assert.False(props.GetBool("foo")); + props.Items["foo"] = true.ToString(); + Assert.True(props.GetBool("foo")); - props.Items["foo"] = null; - Assert.Null(props.GetBool("foo")); + props.Items["foo"] = false.ToString(); + Assert.False(props.GetBool("foo")); - props.Items["foo"] = "BAR"; - Assert.Null(props.GetBool("foo")); - Assert.Equal("BAR", props.Items["foo"]); - } + props.Items["foo"] = null; + Assert.Null(props.GetBool("foo")); + + props.Items["foo"] = "BAR"; + Assert.Null(props.GetBool("foo")); + Assert.Equal("BAR", props.Items["foo"]); + } - [Fact] - public void Roundtrip_Serializes_With_SystemTextJson() + [Fact] + public void Roundtrip_Serializes_With_SystemTextJson() + { + var props = new AuthenticationProperties() { - var props = new AuthenticationProperties() - { - AllowRefresh = true, - ExpiresUtc = new DateTimeOffset(2021, 03, 28, 13, 47, 00, TimeSpan.Zero), - IssuedUtc = new DateTimeOffset(2021, 03, 28, 12, 47, 00, TimeSpan.Zero), - IsPersistent = true, - RedirectUri = "/foo/bar" - }; + AllowRefresh = true, + ExpiresUtc = new DateTimeOffset(2021, 03, 28, 13, 47, 00, TimeSpan.Zero), + IssuedUtc = new DateTimeOffset(2021, 03, 28, 12, 47, 00, TimeSpan.Zero), + IsPersistent = true, + RedirectUri = "/foo/bar" + }; - props.Items.Add("foo", "bar"); + props.Items.Add("foo", "bar"); - props.Parameters.Add("baz", "quux"); + props.Parameters.Add("baz", "quux"); - var json = JsonSerializer.Serialize(props); + var json = JsonSerializer.Serialize(props); - // Verify that Parameters was not serialized - Assert.NotNull(json); - Assert.DoesNotContain("baz", json); - Assert.DoesNotContain("quux", json); + // Verify that Parameters was not serialized + Assert.NotNull(json); + Assert.DoesNotContain("baz", json); + Assert.DoesNotContain("quux", json); - var deserialized = JsonSerializer.Deserialize(json); + var deserialized = JsonSerializer.Deserialize(json); - Assert.NotNull(deserialized); + Assert.NotNull(deserialized); - Assert.Equal(props.AllowRefresh, deserialized!.AllowRefresh); - Assert.Equal(props.ExpiresUtc, deserialized.ExpiresUtc); - Assert.Equal(props.IssuedUtc, deserialized.IssuedUtc); - Assert.Equal(props.IsPersistent, deserialized.IsPersistent); - Assert.Equal(props.RedirectUri, deserialized.RedirectUri); + Assert.Equal(props.AllowRefresh, deserialized!.AllowRefresh); + Assert.Equal(props.ExpiresUtc, deserialized.ExpiresUtc); + Assert.Equal(props.IssuedUtc, deserialized.IssuedUtc); + Assert.Equal(props.IsPersistent, deserialized.IsPersistent); + Assert.Equal(props.RedirectUri, deserialized.RedirectUri); - Assert.NotNull(deserialized.Items); - Assert.True(deserialized.Items.ContainsKey("foo")); - Assert.Equal(props.Items["foo"], deserialized.Items["foo"]); + Assert.NotNull(deserialized.Items); + Assert.True(deserialized.Items.ContainsKey("foo")); + Assert.Equal(props.Items["foo"], deserialized.Items["foo"]); - // Ensure that parameters are not round-tripped - Assert.NotNull(deserialized.Parameters); - Assert.Equal(0, deserialized.Parameters.Count); - } + // Ensure that parameters are not round-tripped + Assert.NotNull(deserialized.Parameters); + Assert.Equal(0, deserialized.Parameters.Count); + } - [Fact] - public void Parameters_Is_Not_Deserialized_With_SystemTextJson() - { - var json = @"{""Parameters"":{""baz"":""quux""}}"; + [Fact] + public void Parameters_Is_Not_Deserialized_With_SystemTextJson() + { + var json = @"{""Parameters"":{""baz"":""quux""}}"; - var deserialized = JsonSerializer.Deserialize(json); + var deserialized = JsonSerializer.Deserialize(json); - Assert.NotNull(deserialized); + Assert.NotNull(deserialized); - // Ensure that parameters is not deserialized from a raw payload - Assert.NotNull(deserialized!.Parameters); - Assert.Equal(0, deserialized.Parameters.Count); - } + // Ensure that parameters is not deserialized from a raw payload + Assert.NotNull(deserialized!.Parameters); + Assert.Equal(0, deserialized.Parameters.Count); + } - [Fact] - public void Serialization_Is_Minimised_With_SystemTextJson() + [Fact] + public void Serialization_Is_Minimised_With_SystemTextJson() + { + var props = new AuthenticationProperties() { - var props = new AuthenticationProperties() - { - AllowRefresh = true, - ExpiresUtc = new DateTimeOffset(2021, 03, 28, 13, 47, 00, TimeSpan.Zero), - IssuedUtc = new DateTimeOffset(2021, 03, 28, 12, 47, 00, TimeSpan.Zero), - IsPersistent = true, - RedirectUri = "/foo/bar" - }; - - props.Items.Add("foo", "bar"); - - var options = new JsonSerializerOptions() { WriteIndented = true }; // Indented for readability if test fails - var json = JsonSerializer.Serialize(props, options); - - // Verify that the payload doesn't duplicate the properties backed by Items - Assert.Equal(@"{ + AllowRefresh = true, + ExpiresUtc = new DateTimeOffset(2021, 03, 28, 13, 47, 00, TimeSpan.Zero), + IssuedUtc = new DateTimeOffset(2021, 03, 28, 12, 47, 00, TimeSpan.Zero), + IsPersistent = true, + RedirectUri = "/foo/bar" + }; + + props.Items.Add("foo", "bar"); + + var options = new JsonSerializerOptions() { WriteIndented = true }; // Indented for readability if test fails + var json = JsonSerializer.Serialize(props, options); + + // Verify that the payload doesn't duplicate the properties backed by Items + Assert.Equal(@"{ ""Items"": { "".refresh"": ""True"", "".expires"": ""Sun, 28 Mar 2021 13:47:00 GMT"", @@ -390,29 +390,28 @@ namespace Microsoft.AspNetCore.Authentication.Core.Test ""foo"": ""bar"" } }", json, ignoreLineEndingDifferences: true); + } + + public class MyAuthenticationProperties : AuthenticationProperties + { + public new DateTimeOffset? GetDateTimeOffset(string key) + { + return base.GetDateTimeOffset(key); + } + + public new void SetDateTimeOffset(string key, DateTimeOffset? value) + { + base.SetDateTimeOffset(key, value); + } + + public new void SetBool(string key, bool? value) + { + base.SetBool(key, value); } - public class MyAuthenticationProperties : AuthenticationProperties + public new bool? GetBool(string key) { - public new DateTimeOffset? GetDateTimeOffset(string key) - { - return base.GetDateTimeOffset(key); - } - - public new void SetDateTimeOffset(string key, DateTimeOffset? value) - { - base.SetDateTimeOffset(key, value); - } - - public new void SetBool(string key, bool? value) - { - base.SetBool(key, value); - } - - public new bool? GetBool(string key) - { - return base.GetBool(key); - } + return base.GetBool(key); } } } diff --git a/src/Http/Authentication.Core/test/AuthenticationSchemeProviderTests.cs b/src/Http/Authentication.Core/test/AuthenticationSchemeProviderTests.cs index dca13ea3b0..29be714577 100644 --- a/src/Http/Authentication.Core/test/AuthenticationSchemeProviderTests.cs +++ b/src/Http/Authentication.Core/test/AuthenticationSchemeProviderTests.cs @@ -11,214 +11,213 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Xunit; -namespace Microsoft.AspNetCore.Authentication.Core.Test +namespace Microsoft.AspNetCore.Authentication.Core.Test; + +public class AuthenticationSchemeProviderTests { - public class AuthenticationSchemeProviderTests + [Fact] + public async Task NoDefaultsByDefault() { - [Fact] - public async Task NoDefaultsByDefault() - { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("B", "whatever"); - }).BuildServiceProvider(); - - var provider = services.GetRequiredService(); - Assert.Null(await provider.GetDefaultForbidSchemeAsync()); - Assert.Null(await provider.GetDefaultAuthenticateSchemeAsync()); - Assert.Null(await provider.GetDefaultChallengeSchemeAsync()); - Assert.Null(await provider.GetDefaultSignInSchemeAsync()); - Assert.Null(await provider.GetDefaultSignOutSchemeAsync()); - } - - [Fact] - public async Task DefaultSchemesFallbackToDefaultScheme() - { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.DefaultScheme = "B"; - o.AddScheme("B", "whatever"); - }).BuildServiceProvider(); - - var provider = services.GetRequiredService(); - Assert.Equal("B", (await provider.GetDefaultForbidSchemeAsync())!.Name); - Assert.Equal("B", (await provider.GetDefaultAuthenticateSchemeAsync())!.Name); - Assert.Equal("B", (await provider.GetDefaultChallengeSchemeAsync())!.Name); - Assert.Equal("B", (await provider.GetDefaultSignInSchemeAsync())!.Name); - Assert.Equal("B", (await provider.GetDefaultSignOutSchemeAsync())!.Name); - } - + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("B", "whatever"); + }).BuildServiceProvider(); + + var provider = services.GetRequiredService(); + Assert.Null(await provider.GetDefaultForbidSchemeAsync()); + Assert.Null(await provider.GetDefaultAuthenticateSchemeAsync()); + Assert.Null(await provider.GetDefaultChallengeSchemeAsync()); + Assert.Null(await provider.GetDefaultSignInSchemeAsync()); + Assert.Null(await provider.GetDefaultSignOutSchemeAsync()); + } - [Fact] - public async Task DefaultSignOutFallsbackToSignIn() + [Fact] + public async Task DefaultSchemesFallbackToDefaultScheme() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("signin", "whatever"); - o.AddScheme("foobly", "whatever"); - o.DefaultSignInScheme = "signin"; - }).BuildServiceProvider(); + o.DefaultScheme = "B"; + o.AddScheme("B", "whatever"); + }).BuildServiceProvider(); + + var provider = services.GetRequiredService(); + Assert.Equal("B", (await provider.GetDefaultForbidSchemeAsync())!.Name); + Assert.Equal("B", (await provider.GetDefaultAuthenticateSchemeAsync())!.Name); + Assert.Equal("B", (await provider.GetDefaultChallengeSchemeAsync())!.Name); + Assert.Equal("B", (await provider.GetDefaultSignInSchemeAsync())!.Name); + Assert.Equal("B", (await provider.GetDefaultSignOutSchemeAsync())!.Name); + } - var provider = services.GetRequiredService(); - var scheme = await provider.GetDefaultSignOutSchemeAsync(); - Assert.NotNull(scheme); - Assert.Equal("signin", scheme!.Name); - } - [Fact] - public async Task DefaultForbidFallsbackToChallenge() + [Fact] + public async Task DefaultSignOutFallsbackToSignIn() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("challenge", "whatever"); - o.AddScheme("foobly", "whatever"); - o.DefaultChallengeScheme = "challenge"; - }).BuildServiceProvider(); + o.AddScheme("signin", "whatever"); + o.AddScheme("foobly", "whatever"); + o.DefaultSignInScheme = "signin"; + }).BuildServiceProvider(); + + var provider = services.GetRequiredService(); + var scheme = await provider.GetDefaultSignOutSchemeAsync(); + Assert.NotNull(scheme); + Assert.Equal("signin", scheme!.Name); + } - var provider = services.GetRequiredService(); - var scheme = await provider.GetDefaultForbidSchemeAsync(); - Assert.NotNull(scheme); - Assert.Equal("challenge", scheme!.Name); - } + [Fact] + public async Task DefaultForbidFallsbackToChallenge() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("challenge", "whatever"); + o.AddScheme("foobly", "whatever"); + o.DefaultChallengeScheme = "challenge"; + }).BuildServiceProvider(); + + var provider = services.GetRequiredService(); + var scheme = await provider.GetDefaultForbidSchemeAsync(); + Assert.NotNull(scheme); + Assert.Equal("challenge", scheme!.Name); + } - [Fact] - public async Task DefaultSchemesAreSet() - { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("A", "whatever"); - o.AddScheme("B", "whatever"); - o.AddScheme("C", "whatever"); - o.AddScheme("Def", "whatever"); - o.DefaultScheme = "Def"; - o.DefaultChallengeScheme = "A"; - o.DefaultForbidScheme = "B"; - o.DefaultSignInScheme = "C"; - o.DefaultSignOutScheme = "A"; - o.DefaultAuthenticateScheme = "C"; - }).BuildServiceProvider(); - - var provider = services.GetRequiredService(); - Assert.Equal("B", (await provider.GetDefaultForbidSchemeAsync())!.Name); - Assert.Equal("C", (await provider.GetDefaultAuthenticateSchemeAsync())!.Name); - Assert.Equal("A", (await provider.GetDefaultChallengeSchemeAsync())!.Name); - Assert.Equal("C", (await provider.GetDefaultSignInSchemeAsync())!.Name); - Assert.Equal("A", (await provider.GetDefaultSignOutSchemeAsync())!.Name); - } + [Fact] + public async Task DefaultSchemesAreSet() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("A", "whatever"); + o.AddScheme("B", "whatever"); + o.AddScheme("C", "whatever"); + o.AddScheme("Def", "whatever"); + o.DefaultScheme = "Def"; + o.DefaultChallengeScheme = "A"; + o.DefaultForbidScheme = "B"; + o.DefaultSignInScheme = "C"; + o.DefaultSignOutScheme = "A"; + o.DefaultAuthenticateScheme = "C"; + }).BuildServiceProvider(); + + var provider = services.GetRequiredService(); + Assert.Equal("B", (await provider.GetDefaultForbidSchemeAsync())!.Name); + Assert.Equal("C", (await provider.GetDefaultAuthenticateSchemeAsync())!.Name); + Assert.Equal("A", (await provider.GetDefaultChallengeSchemeAsync())!.Name); + Assert.Equal("C", (await provider.GetDefaultSignInSchemeAsync())!.Name); + Assert.Equal("A", (await provider.GetDefaultSignOutSchemeAsync())!.Name); + } - [Fact] - public async Task SignOutWillDefaultsToSignInThatDoesNotSignOut() + [Fact] + public async Task SignOutWillDefaultsToSignInThatDoesNotSignOut() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("signin", "whatever"); - o.DefaultSignInScheme = "signin"; - }).BuildServiceProvider(); + o.AddScheme("signin", "whatever"); + o.DefaultSignInScheme = "signin"; + }).BuildServiceProvider(); - var provider = services.GetRequiredService(); - Assert.NotNull(await provider.GetDefaultSignOutSchemeAsync()); - } + var provider = services.GetRequiredService(); + Assert.NotNull(await provider.GetDefaultSignOutSchemeAsync()); + } - [Fact] - public void SchemeRegistrationIsCaseSensitive() + [Fact] + public void SchemeRegistrationIsCaseSensitive() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("signin", "whatever"); - o.AddScheme("signin", "whatever"); - }).BuildServiceProvider(); + o.AddScheme("signin", "whatever"); + o.AddScheme("signin", "whatever"); + }).BuildServiceProvider(); - var error = Assert.Throws(() => services.GetRequiredService()); + var error = Assert.Throws(() => services.GetRequiredService()); - Assert.Contains("Scheme already exists: signin", error.Message); - } - - [Fact] - public void CanSafelyTryAddSchemes() - { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - }).BuildServiceProvider(); - - var o = services.GetRequiredService(); - Assert.True(o.TryAddScheme(new AuthenticationScheme("signin", "whatever", typeof(Handler)))); - Assert.True(o.TryAddScheme(new AuthenticationScheme("signin2", "whatever", typeof(Handler)))); - Assert.False(o.TryAddScheme(new AuthenticationScheme("signin", "whatever", typeof(Handler)))); - Assert.True(o.TryAddScheme(new AuthenticationScheme("signin3", "whatever", typeof(Handler)))); - Assert.False(o.TryAddScheme(new AuthenticationScheme("signin2", "whatever", typeof(Handler)))); - o.RemoveScheme("signin2"); - Assert.True(o.TryAddScheme(new AuthenticationScheme("signin2", "whatever", typeof(Handler)))); - } + Assert.Contains("Scheme already exists: signin", error.Message); + } - [Fact] - public async Task LookupUsesProvidedStringComparer() + [Fact] + public void CanSafelyTryAddSchemes() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => { - var services = new ServiceCollection().AddOptions() - .AddSingleton() - .AddAuthenticationCore(o => o.AddScheme("signin", "whatever")) - .BuildServiceProvider(); + }).BuildServiceProvider(); + + var o = services.GetRequiredService(); + Assert.True(o.TryAddScheme(new AuthenticationScheme("signin", "whatever", typeof(Handler)))); + Assert.True(o.TryAddScheme(new AuthenticationScheme("signin2", "whatever", typeof(Handler)))); + Assert.False(o.TryAddScheme(new AuthenticationScheme("signin", "whatever", typeof(Handler)))); + Assert.True(o.TryAddScheme(new AuthenticationScheme("signin3", "whatever", typeof(Handler)))); + Assert.False(o.TryAddScheme(new AuthenticationScheme("signin2", "whatever", typeof(Handler)))); + o.RemoveScheme("signin2"); + Assert.True(o.TryAddScheme(new AuthenticationScheme("signin2", "whatever", typeof(Handler)))); + } + + [Fact] + public async Task LookupUsesProvidedStringComparer() + { + var services = new ServiceCollection().AddOptions() + .AddSingleton() + .AddAuthenticationCore(o => o.AddScheme("signin", "whatever")) + .BuildServiceProvider(); - var provider = services.GetRequiredService(); + var provider = services.GetRequiredService(); - var a = await provider.GetSchemeAsync("signin"); - var b = await provider.GetSchemeAsync("SignIn"); - var c = await provider.GetSchemeAsync("SIGNIN"); + var a = await provider.GetSchemeAsync("signin"); + var b = await provider.GetSchemeAsync("SignIn"); + var c = await provider.GetSchemeAsync("SIGNIN"); - Assert.NotNull(a); - Assert.Same(a, b); - Assert.Same(b, c); - } + Assert.NotNull(a); + Assert.Same(a, b); + Assert.Same(b, c); + } - private class Handler : IAuthenticationHandler + private class Handler : IAuthenticationHandler + { + public Task AuthenticateAsync() { - public Task AuthenticateAsync() - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); + } - public Task ChallengeAsync(AuthenticationProperties? properties) - { - throw new NotImplementedException(); - } + public Task ChallengeAsync(AuthenticationProperties? properties) + { + throw new NotImplementedException(); + } - public Task ForbidAsync(AuthenticationProperties? properties) - { - throw new NotImplementedException(); - } + public Task ForbidAsync(AuthenticationProperties? properties) + { + throw new NotImplementedException(); + } - public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) - { - throw new NotImplementedException(); - } + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + throw new NotImplementedException(); } + } - private class SignInHandler : Handler, IAuthenticationSignInHandler + private class SignInHandler : Handler, IAuthenticationSignInHandler + { + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties) { - public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); + } - public Task SignOutAsync(AuthenticationProperties? properties) - { - throw new NotImplementedException(); - } + public Task SignOutAsync(AuthenticationProperties? properties) + { + throw new NotImplementedException(); } + } - private class SignOutHandler : Handler, IAuthenticationSignOutHandler + private class SignOutHandler : Handler, IAuthenticationSignOutHandler + { + public Task SignOutAsync(AuthenticationProperties? properties) { - public Task SignOutAsync(AuthenticationProperties? properties) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } + } - private class IgnoreCaseSchemeProvider : AuthenticationSchemeProvider + private class IgnoreCaseSchemeProvider : AuthenticationSchemeProvider + { + public IgnoreCaseSchemeProvider(IOptions options) + : base(options, new Dictionary(StringComparer.OrdinalIgnoreCase)) { - public IgnoreCaseSchemeProvider(IOptions options) - : base(options, new Dictionary(StringComparer.OrdinalIgnoreCase)) - { - } } } } diff --git a/src/Http/Authentication.Core/test/AuthenticationServiceTests.cs b/src/Http/Authentication.Core/test/AuthenticationServiceTests.cs index 72b4bf94e2..903823e8a6 100644 --- a/src/Http/Authentication.Core/test/AuthenticationServiceTests.cs +++ b/src/Http/Authentication.Core/test/AuthenticationServiceTests.cs @@ -8,414 +8,413 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Microsoft.AspNetCore.Authentication.Core.Test +namespace Microsoft.AspNetCore.Authentication.Core.Test; + +public class AuthenticationServiceTests { - public class AuthenticationServiceTests + [Fact] + public async Task AuthenticateThrowsForSchemeMismatch() { - [Fact] - public async Task AuthenticateThrowsForSchemeMismatch() - { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("base", "whatever"); - }).BuildServiceProvider(); - var context = new DefaultHttpContext(); - context.RequestServices = services; - - await context.AuthenticateAsync("base"); - var ex = await Assert.ThrowsAsync(() => context.AuthenticateAsync("missing")); - Assert.Contains("base", ex.Message); - } + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("base", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.AuthenticateAsync("base"); + var ex = await Assert.ThrowsAsync(() => context.AuthenticateAsync("missing")); + Assert.Contains("base", ex.Message); + } + + [Fact] + public async Task CustomHandlersAuthenticateRunsClaimsTransformationEveryTime() + { + var transform = new RunOnce(); + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("base", "whatever"); + }) + .AddSingleton(transform) + .BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + // Because base handler returns a different principal per call, its run multiple times + await context.AuthenticateAsync("base"); + Assert.Equal(1, transform.Ran); + + await context.AuthenticateAsync("base"); + Assert.Equal(2, transform.Ran); + + await context.AuthenticateAsync("base"); + Assert.Equal(3, transform.Ran); + } + + [Fact] + public async Task ChallengeThrowsForSchemeMismatch() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("base", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.ChallengeAsync("base"); + var ex = await Assert.ThrowsAsync(() => context.ChallengeAsync("missing")); + Assert.Contains("base", ex.Message); + } + + [Fact] + public async Task ForbidThrowsForSchemeMismatch() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("base", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.ForbidAsync("base"); + var ex = await Assert.ThrowsAsync(() => context.ForbidAsync("missing")); + Assert.Contains("base", ex.Message); + } + + [Fact] + public async Task CanOnlySignInWithIsAuthenticated() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("signin", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + var ex = await Assert.ThrowsAsync(() => context.SignInAsync("signin", new ClaimsPrincipal(), null)); + await context.SignInAsync("signin", new ClaimsPrincipal(new ClaimsIdentity("whatever")), null); + } + + [Fact] + public async Task CanSignInWithoutIsAuthenticated() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("signin", "whatever"); + o.RequireAuthenticatedSignIn = false; + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.SignInAsync("signin", new ClaimsPrincipal(), null); + await context.SignInAsync("signin", new ClaimsPrincipal(new ClaimsIdentity("whatever")), null); + } - [Fact] - public async Task CustomHandlersAuthenticateRunsClaimsTransformationEveryTime() + [Fact] + public async Task CanOnlySignInIfSupported() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => { - var transform = new RunOnce(); - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("base", "whatever"); - }) - .AddSingleton(transform) - .BuildServiceProvider(); - var context = new DefaultHttpContext(); - context.RequestServices = services; + o.AddScheme("uber", "whatever"); + o.AddScheme("base", "whatever"); + o.AddScheme("signin", "whatever"); + o.AddScheme("signout", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.SignInAsync("uber", new ClaimsPrincipal(new ClaimsIdentity("whatever")), null); + var ex = await Assert.ThrowsAsync(() => context.SignInAsync("base", new ClaimsPrincipal(new ClaimsIdentity("whatever")), null)); + Assert.Contains("uber", ex.Message); + Assert.Contains("signin", ex.Message); + await context.SignInAsync("signin", new ClaimsPrincipal(new ClaimsIdentity("whatever")), null); + ex = await Assert.ThrowsAsync(() => context.SignInAsync("signout", new ClaimsPrincipal(new ClaimsIdentity("whatever")), null)); + Assert.Contains("uber", ex.Message); + Assert.Contains("signin", ex.Message); + } - // Because base handler returns a different principal per call, its run multiple times - await context.AuthenticateAsync("base"); - Assert.Equal(1, transform.Ran); + [Fact] + public async Task CanOnlySignOutIfSupported() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("uber", "whatever"); + o.AddScheme("base", "whatever"); + o.AddScheme("signin", "whatever"); + o.AddScheme("signout", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.SignOutAsync("uber"); + var ex = await Assert.ThrowsAsync(() => context.SignOutAsync("base")); + Assert.Contains("uber", ex.Message); + Assert.Contains("signout", ex.Message); + await context.SignOutAsync("signout"); + await context.SignOutAsync("signin"); + } + + [Fact] + public async Task ServicesWithDefaultIAuthenticationHandlerMethodsTest() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("base", "whatever"); + o.DefaultScheme = "base"; + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.AuthenticateAsync(); + await context.ChallengeAsync(); + await context.ForbidAsync(); + var ex = await Assert.ThrowsAsync(() => context.SignOutAsync()); + Assert.Contains("cannot be used for SignOutAsync", ex.Message); + ex = await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity("whatever")))); + Assert.Contains("cannot be used for SignInAsync", ex.Message); + } + + [Fact] + public async Task ServicesWithDefaultUberMethodsTest() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("base", "whatever"); + o.DefaultScheme = "base"; + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.AuthenticateAsync(); + await context.ChallengeAsync(); + await context.ForbidAsync(); + await context.SignOutAsync(); + await context.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity("whatever"))); + } - await context.AuthenticateAsync("base"); - Assert.Equal(2, transform.Ran); + [Fact] + public async Task ServicesWithDefaultSignInMethodsTest() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("base", "whatever"); + o.DefaultScheme = "base"; + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.AuthenticateAsync(); + await context.ChallengeAsync(); + await context.ForbidAsync(); + await context.SignOutAsync(); + await context.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity("whatever"))); + } + + [Fact] + public async Task ServicesWithDefaultSignOutMethodsTest() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("base", "whatever"); + o.DefaultScheme = "base"; + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.AuthenticateAsync(); + await context.ChallengeAsync(); + await context.ForbidAsync(); + await context.SignOutAsync(); + var ex = await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity("whatever")))); + Assert.Contains("cannot be used for SignInAsync", ex.Message); + } + + [Fact] + public async Task ServicesWithDefaultForbidMethod_CallsForbidMethod() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("forbid", "whatever"); + o.DefaultForbidScheme = "forbid"; + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.ForbidAsync(); + } - await context.AuthenticateAsync("base"); - Assert.Equal(3, transform.Ran); + private class RunOnce : IClaimsTransformation + { + public int Ran = 0; + public Task TransformAsync(ClaimsPrincipal principal) + { + Ran++; + return Task.FromResult(new ClaimsPrincipal()); } + } - [Fact] - public async Task ChallengeThrowsForSchemeMismatch() + private class BaseHandler : IAuthenticationHandler + { + public Task AuthenticateAsync() { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("base", "whatever"); - }).BuildServiceProvider(); - var context = new DefaultHttpContext(); - context.RequestServices = services; + return Task.FromResult(AuthenticateResult.Success( + new AuthenticationTicket( + new ClaimsPrincipal(new ClaimsIdentity("whatever")), + new AuthenticationProperties(), + "whatever"))); + } - await context.ChallengeAsync("base"); - var ex = await Assert.ThrowsAsync(() => context.ChallengeAsync("missing")); - Assert.Contains("base", ex.Message); + public Task ChallengeAsync(AuthenticationProperties? properties) + { + return Task.FromResult(0); } - [Fact] - public async Task ForbidThrowsForSchemeMismatch() + public Task ForbidAsync(AuthenticationProperties? properties) { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("base", "whatever"); - }).BuildServiceProvider(); - var context = new DefaultHttpContext(); - context.RequestServices = services; + return Task.FromResult(0); + } - await context.ForbidAsync("base"); - var ex = await Assert.ThrowsAsync(() => context.ForbidAsync("missing")); - Assert.Contains("base", ex.Message); + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + return Task.FromResult(0); } + } - [Fact] - public async Task CanOnlySignInWithIsAuthenticated() + private class SignInHandler : IAuthenticationSignInHandler + { + public Task AuthenticateAsync() { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("signin", "whatever"); - }).BuildServiceProvider(); - var context = new DefaultHttpContext(); - context.RequestServices = services; + return Task.FromResult(AuthenticateResult.NoResult()); + } - var ex = await Assert.ThrowsAsync(() => context.SignInAsync("signin", new ClaimsPrincipal(), null)); - await context.SignInAsync("signin", new ClaimsPrincipal(new ClaimsIdentity("whatever")), null); + public Task ChallengeAsync(AuthenticationProperties? properties) + { + return Task.FromResult(0); } - [Fact] - public async Task CanSignInWithoutIsAuthenticated() + public Task ForbidAsync(AuthenticationProperties? properties) { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("signin", "whatever"); - o.RequireAuthenticatedSignIn = false; - }).BuildServiceProvider(); - var context = new DefaultHttpContext(); - context.RequestServices = services; + return Task.FromResult(0); + } - await context.SignInAsync("signin", new ClaimsPrincipal(), null); - await context.SignInAsync("signin", new ClaimsPrincipal(new ClaimsIdentity("whatever")), null); + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + return Task.FromResult(0); } - [Fact] - public async Task CanOnlySignInIfSupported() - { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("uber", "whatever"); - o.AddScheme("base", "whatever"); - o.AddScheme("signin", "whatever"); - o.AddScheme("signout", "whatever"); - }).BuildServiceProvider(); - var context = new DefaultHttpContext(); - context.RequestServices = services; - - await context.SignInAsync("uber", new ClaimsPrincipal(new ClaimsIdentity("whatever")), null); - var ex = await Assert.ThrowsAsync(() => context.SignInAsync("base", new ClaimsPrincipal(new ClaimsIdentity("whatever")), null)); - Assert.Contains("uber", ex.Message); - Assert.Contains("signin", ex.Message); - await context.SignInAsync("signin", new ClaimsPrincipal(new ClaimsIdentity("whatever")), null); - ex = await Assert.ThrowsAsync(() => context.SignInAsync("signout", new ClaimsPrincipal(new ClaimsIdentity("whatever")), null)); - Assert.Contains("uber", ex.Message); - Assert.Contains("signin", ex.Message); + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties) + { + return Task.FromResult(0); } - [Fact] - public async Task CanOnlySignOutIfSupported() - { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("uber", "whatever"); - o.AddScheme("base", "whatever"); - o.AddScheme("signin", "whatever"); - o.AddScheme("signout", "whatever"); - }).BuildServiceProvider(); - var context = new DefaultHttpContext(); - context.RequestServices = services; - - await context.SignOutAsync("uber"); - var ex = await Assert.ThrowsAsync(() => context.SignOutAsync("base")); - Assert.Contains("uber", ex.Message); - Assert.Contains("signout", ex.Message); - await context.SignOutAsync("signout"); - await context.SignOutAsync("signin"); + public Task SignOutAsync(AuthenticationProperties? properties) + { + return Task.FromResult(0); } + } - [Fact] - public async Task ServicesWithDefaultIAuthenticationHandlerMethodsTest() - { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("base", "whatever"); - o.DefaultScheme = "base"; - }).BuildServiceProvider(); - var context = new DefaultHttpContext(); - context.RequestServices = services; - - await context.AuthenticateAsync(); - await context.ChallengeAsync(); - await context.ForbidAsync(); - var ex = await Assert.ThrowsAsync(() => context.SignOutAsync()); - Assert.Contains("cannot be used for SignOutAsync", ex.Message); - ex = await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity("whatever")))); - Assert.Contains("cannot be used for SignInAsync", ex.Message); + public class SignOutHandler : IAuthenticationSignOutHandler + { + public Task AuthenticateAsync() + { + return Task.FromResult(AuthenticateResult.NoResult()); } - [Fact] - public async Task ServicesWithDefaultUberMethodsTest() - { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("base", "whatever"); - o.DefaultScheme = "base"; - }).BuildServiceProvider(); - var context = new DefaultHttpContext(); - context.RequestServices = services; - - await context.AuthenticateAsync(); - await context.ChallengeAsync(); - await context.ForbidAsync(); - await context.SignOutAsync(); - await context.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity("whatever"))); + public Task ChallengeAsync(AuthenticationProperties? properties) + { + return Task.FromResult(0); } - [Fact] - public async Task ServicesWithDefaultSignInMethodsTest() - { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("base", "whatever"); - o.DefaultScheme = "base"; - }).BuildServiceProvider(); - var context = new DefaultHttpContext(); - context.RequestServices = services; - - await context.AuthenticateAsync(); - await context.ChallengeAsync(); - await context.ForbidAsync(); - await context.SignOutAsync(); - await context.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity("whatever"))); + public Task ForbidAsync(AuthenticationProperties? properties) + { + return Task.FromResult(0); } - [Fact] - public async Task ServicesWithDefaultSignOutMethodsTest() - { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("base", "whatever"); - o.DefaultScheme = "base"; - }).BuildServiceProvider(); - var context = new DefaultHttpContext(); - context.RequestServices = services; - - await context.AuthenticateAsync(); - await context.ChallengeAsync(); - await context.ForbidAsync(); - await context.SignOutAsync(); - var ex = await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal(new ClaimsIdentity("whatever")))); - Assert.Contains("cannot be used for SignInAsync", ex.Message); + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + return Task.FromResult(0); } - [Fact] - public async Task ServicesWithDefaultForbidMethod_CallsForbidMethod() + public Task SignOutAsync(AuthenticationProperties? properties) { - var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => - { - o.AddScheme("forbid", "whatever"); - o.DefaultForbidScheme = "forbid"; - }).BuildServiceProvider(); - var context = new DefaultHttpContext(); - context.RequestServices = services; + return Task.FromResult(0); + } + } - await context.ForbidAsync(); + private class UberHandler : IAuthenticationHandler, IAuthenticationRequestHandler, IAuthenticationSignInHandler, IAuthenticationSignOutHandler + { + public Task AuthenticateAsync() + { + return Task.FromResult(AuthenticateResult.NoResult()); + } + + public Task ChallengeAsync(AuthenticationProperties? properties) + { + return Task.FromResult(0); + } + + public Task ForbidAsync(AuthenticationProperties? properties) + { + return Task.FromResult(0); } - private class RunOnce : IClaimsTransformation + public Task HandleRequestAsync() { - public int Ran = 0; - public Task TransformAsync(ClaimsPrincipal principal) - { - Ran++; - return Task.FromResult(new ClaimsPrincipal()); - } + return Task.FromResult(false); } - private class BaseHandler : IAuthenticationHandler - { - public Task AuthenticateAsync() - { - return Task.FromResult(AuthenticateResult.Success( - new AuthenticationTicket( - new ClaimsPrincipal(new ClaimsIdentity("whatever")), - new AuthenticationProperties(), - "whatever"))); - } - - public Task ChallengeAsync(AuthenticationProperties? properties) - { - return Task.FromResult(0); - } - - public Task ForbidAsync(AuthenticationProperties? properties) - { - return Task.FromResult(0); - } - - public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) - { - return Task.FromResult(0); - } + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + return Task.FromResult(0); } - private class SignInHandler : IAuthenticationSignInHandler - { - public Task AuthenticateAsync() - { - return Task.FromResult(AuthenticateResult.NoResult()); - } - - public Task ChallengeAsync(AuthenticationProperties? properties) - { - return Task.FromResult(0); - } - - public Task ForbidAsync(AuthenticationProperties? properties) - { - return Task.FromResult(0); - } - - public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) - { - return Task.FromResult(0); - } - - public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties) - { - return Task.FromResult(0); - } - - public Task SignOutAsync(AuthenticationProperties? properties) - { - return Task.FromResult(0); - } + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties) + { + return Task.FromResult(0); } - public class SignOutHandler : IAuthenticationSignOutHandler + public Task SignOutAsync(AuthenticationProperties? properties) { - public Task AuthenticateAsync() - { - return Task.FromResult(AuthenticateResult.NoResult()); - } + return Task.FromResult(0); + } + } - public Task ChallengeAsync(AuthenticationProperties? properties) - { - return Task.FromResult(0); - } + private class ForbidHandler : IAuthenticationHandler, IAuthenticationRequestHandler, IAuthenticationSignInHandler, IAuthenticationSignOutHandler + { + public Task AuthenticateAsync() + { + throw new NotImplementedException(); + } - public Task ForbidAsync(AuthenticationProperties? properties) - { - return Task.FromResult(0); - } + public Task ChallengeAsync(AuthenticationProperties? properties) + { + throw new NotImplementedException(); + } - public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) - { - return Task.FromResult(0); - } + public Task ForbidAsync(AuthenticationProperties? properties) + { + return Task.FromResult(0); + } - public Task SignOutAsync(AuthenticationProperties? properties) - { - return Task.FromResult(0); - } + public Task HandleRequestAsync() + { + throw new NotImplementedException(); } - private class UberHandler : IAuthenticationHandler, IAuthenticationRequestHandler, IAuthenticationSignInHandler, IAuthenticationSignOutHandler - { - public Task AuthenticateAsync() - { - return Task.FromResult(AuthenticateResult.NoResult()); - } - - public Task ChallengeAsync(AuthenticationProperties? properties) - { - return Task.FromResult(0); - } - - public Task ForbidAsync(AuthenticationProperties? properties) - { - return Task.FromResult(0); - } - - public Task HandleRequestAsync() - { - return Task.FromResult(false); - } - - public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) - { - return Task.FromResult(0); - } - - public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties) - { - return Task.FromResult(0); - } - - public Task SignOutAsync(AuthenticationProperties? properties) - { - return Task.FromResult(0); - } + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) + { + return Task.FromResult(0); } - private class ForbidHandler : IAuthenticationHandler, IAuthenticationRequestHandler, IAuthenticationSignInHandler, IAuthenticationSignOutHandler - { - public Task AuthenticateAsync() - { - throw new NotImplementedException(); - } - - public Task ChallengeAsync(AuthenticationProperties? properties) - { - throw new NotImplementedException(); - } - - public Task ForbidAsync(AuthenticationProperties? properties) - { - return Task.FromResult(0); - } - - public Task HandleRequestAsync() - { - throw new NotImplementedException(); - } - - public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) - { - return Task.FromResult(0); - } - - public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties) - { - throw new NotImplementedException(); - } - - public Task SignOutAsync(AuthenticationProperties? properties) - { - throw new NotImplementedException(); - } + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties) + { + throw new NotImplementedException(); } + public Task SignOutAsync(AuthenticationProperties? properties) + { + throw new NotImplementedException(); + } } + } diff --git a/src/Http/Authentication.Core/test/AuthenticationTicketTests.cs b/src/Http/Authentication.Core/test/AuthenticationTicketTests.cs index 0c6260c421..29519ff5ea 100644 --- a/src/Http/Authentication.Core/test/AuthenticationTicketTests.cs +++ b/src/Http/Authentication.Core/test/AuthenticationTicketTests.cs @@ -5,43 +5,42 @@ using System.Collections.Generic; using System.Security.Claims; using Xunit; -namespace Microsoft.AspNetCore.Authentication.Core.Test +namespace Microsoft.AspNetCore.Authentication.Core.Test; + +public class AuthenticationTicketTests { - public class AuthenticationTicketTests + [Fact] + public void Clone_Copies() { - [Fact] - public void Clone_Copies() + var items = new Dictionary + { + ["foo"] = "bar", + }; + var value = "value"; + var parameters = new Dictionary { - var items = new Dictionary - { - ["foo"] = "bar", - }; - var value = "value"; - var parameters = new Dictionary - { - ["foo2"] = value, - }; - var props = new AuthenticationProperties(items, parameters); - var identity = new ClaimsIdentity(); - var principal = new ClaimsPrincipal(identity); - var ticket = new AuthenticationTicket(principal, props, "scheme"); + ["foo2"] = value, + }; + var props = new AuthenticationProperties(items, parameters); + var identity = new ClaimsIdentity(); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, props, "scheme"); - Assert.Same(items, ticket.Properties.Items); - Assert.Same(parameters, ticket.Properties.Parameters); - var copy = ticket.Clone(); - Assert.NotSame(ticket.Principal, copy.Principal); - Assert.NotSame(ticket.Properties.Items, copy.Properties.Items); - Assert.NotSame(ticket.Properties.Parameters, copy.Properties.Parameters); - // Objects in the dictionaries will still be the same - Assert.Equal(ticket.Properties.Items, copy.Properties.Items); - Assert.Equal(ticket.Properties.Parameters, copy.Properties.Parameters); - props.Items["change"] = "good"; - props.Parameters["something"] = "bad"; - Assert.NotEqual(ticket.Properties.Items, copy.Properties.Items); - Assert.NotEqual(ticket.Properties.Parameters, copy.Properties.Parameters); - identity.AddClaim(new Claim("name", "value")); - Assert.True(ticket.Principal.HasClaim("name", "value")); - Assert.False(copy.Principal.HasClaim("name", "value")); - } + Assert.Same(items, ticket.Properties.Items); + Assert.Same(parameters, ticket.Properties.Parameters); + var copy = ticket.Clone(); + Assert.NotSame(ticket.Principal, copy.Principal); + Assert.NotSame(ticket.Properties.Items, copy.Properties.Items); + Assert.NotSame(ticket.Properties.Parameters, copy.Properties.Parameters); + // Objects in the dictionaries will still be the same + Assert.Equal(ticket.Properties.Items, copy.Properties.Items); + Assert.Equal(ticket.Properties.Parameters, copy.Properties.Parameters); + props.Items["change"] = "good"; + props.Parameters["something"] = "bad"; + Assert.NotEqual(ticket.Properties.Items, copy.Properties.Items); + Assert.NotEqual(ticket.Properties.Parameters, copy.Properties.Parameters); + identity.AddClaim(new Claim("name", "value")); + Assert.True(ticket.Principal.HasClaim("name", "value")); + Assert.False(copy.Principal.HasClaim("name", "value")); } } diff --git a/src/Http/Authentication.Core/test/TokenExtensionTests.cs b/src/Http/Authentication.Core/test/TokenExtensionTests.cs index aac27e22bb..bb2acc8645 100644 --- a/src/Http/Authentication.Core/test/TokenExtensionTests.cs +++ b/src/Http/Authentication.Core/test/TokenExtensionTests.cs @@ -10,54 +10,153 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Microsoft.AspNetCore.Authentication.Core.Test +namespace Microsoft.AspNetCore.Authentication.Core.Test; + +public class TokenExtensionTests { - public class TokenExtensionTests + [Fact] + public void CanStoreMultipleTokens() { - [Fact] - public void CanStoreMultipleTokens() - { - var props = new AuthenticationProperties(); - var tokens = new List(); - var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; - var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; - var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; - tokens.Add(tok1); - tokens.Add(tok2); - tokens.Add(tok3); - props.StoreTokens(tokens); + var props = new AuthenticationProperties(); + var tokens = new List(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + + Assert.Equal("1", props.GetTokenValue("One")); + Assert.Equal("2", props.GetTokenValue("Two")); + Assert.Equal("3", props.GetTokenValue("Three")); + Assert.Equal(3, props.GetTokens().Count()); + } - Assert.Equal("1", props.GetTokenValue("One")); - Assert.Equal("2", props.GetTokenValue("Two")); - Assert.Equal("3", props.GetTokenValue("Three")); - Assert.Equal(3, props.GetTokens().Count()); - } + [Fact] + public void SubsequentStoreTokenDeletesPreviousTokens() + { + var props = new AuthenticationProperties(); + var tokens = new List(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + + props.StoreTokens(tokens); + + props.StoreTokens(new[] { new AuthenticationToken { Name = "Zero", Value = "0" } }); + + Assert.Equal("0", props.GetTokenValue("Zero")); + Assert.Null(props.GetTokenValue("One")); + Assert.Null(props.GetTokenValue("Two")); + Assert.Null(props.GetTokenValue("Three")); + Assert.Single(props.GetTokens()); + } - [Fact] - public void SubsequentStoreTokenDeletesPreviousTokens() - { - var props = new AuthenticationProperties(); - var tokens = new List(); - var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; - var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; - var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; - tokens.Add(tok1); - tokens.Add(tok2); - tokens.Add(tok3); + [Fact] + public void CanUpdateTokens() + { + var props = new AuthenticationProperties(); + var tokens = new List(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + + tok1.Value = ".1"; + tok2.Value = ".2"; + tok3.Value = ".3"; + props.StoreTokens(tokens); + + Assert.Equal(".1", props.GetTokenValue("One")); + Assert.Equal(".2", props.GetTokenValue("Two")); + Assert.Equal(".3", props.GetTokenValue("Three")); + Assert.Equal(3, props.GetTokens().Count()); + } - props.StoreTokens(tokens); + [Fact] + public void CanUpdateTokenValues() + { + var props = new AuthenticationProperties(); + var tokens = new List(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + + Assert.True(props.UpdateTokenValue("One", ".11")); + Assert.True(props.UpdateTokenValue("Two", ".22")); + Assert.True(props.UpdateTokenValue("Three", ".33")); + + Assert.Equal(".11", props.GetTokenValue("One")); + Assert.Equal(".22", props.GetTokenValue("Two")); + Assert.Equal(".33", props.GetTokenValue("Three")); + Assert.Equal(3, props.GetTokens().Count()); + } - props.StoreTokens(new[] { new AuthenticationToken { Name = "Zero", Value = "0" } }); + [Fact] + public void UpdateTokenValueReturnsFalseForUnknownToken() + { + var props = new AuthenticationProperties(); + var tokens = new List(); + var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; + var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; + var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; + tokens.Add(tok1); + tokens.Add(tok2); + tokens.Add(tok3); + props.StoreTokens(tokens); + + Assert.False(props.UpdateTokenValue("ONE", ".11")); + Assert.False(props.UpdateTokenValue("Jigglypuff", ".11")); + + Assert.Null(props.GetTokenValue("ONE")); + Assert.Null(props.GetTokenValue("Jigglypuff")); + Assert.Equal(3, props.GetTokens().Count()); + } - Assert.Equal("0", props.GetTokenValue("Zero")); - Assert.Null(props.GetTokenValue("One")); - Assert.Null(props.GetTokenValue("Two")); - Assert.Null(props.GetTokenValue("Three")); - Assert.Single(props.GetTokens()); - } + [Fact] + public async Task GetTokenWorksWithDefaultAuthenticateScheme() + { + var context = new DefaultHttpContext(); + var services = new ServiceCollection().AddOptions() + .AddAuthenticationCore(o => + { + o.DefaultScheme = "simple"; + o.AddScheme("simple", s => s.HandlerType = typeof(SimpleAuth)); + }); + context.RequestServices = services.BuildServiceProvider(); + + Assert.Equal("1", await context.GetTokenAsync("One")); + Assert.Equal("2", await context.GetTokenAsync("Two")); + Assert.Equal("3", await context.GetTokenAsync("Three")); + } + + [Fact] + public async Task GetTokenWorksWithExplicitScheme() + { + var context = new DefaultHttpContext(); + var services = new ServiceCollection().AddOptions() + .AddAuthenticationCore(o => o.AddScheme("simple", s => s.HandlerType = typeof(SimpleAuth))); + context.RequestServices = services.BuildServiceProvider(); + + Assert.Equal("1", await context.GetTokenAsync("simple", "One")); + Assert.Equal("2", await context.GetTokenAsync("simple", "Two")); + Assert.Equal("3", await context.GetTokenAsync("simple", "Three")); + } - [Fact] - public void CanUpdateTokens() + private class SimpleAuth : IAuthenticationHandler + { + public Task AuthenticateAsync() { var props = new AuthenticationProperties(); var tokens = new List(); @@ -68,133 +167,33 @@ namespace Microsoft.AspNetCore.Authentication.Core.Test tokens.Add(tok2); tokens.Add(tok3); props.StoreTokens(tokens); - - tok1.Value = ".1"; - tok2.Value = ".2"; - tok3.Value = ".3"; - props.StoreTokens(tokens); - - Assert.Equal(".1", props.GetTokenValue("One")); - Assert.Equal(".2", props.GetTokenValue("Two")); - Assert.Equal(".3", props.GetTokenValue("Three")); - Assert.Equal(3, props.GetTokens().Count()); + return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), props, "simple"))); } - [Fact] - public void CanUpdateTokenValues() + public Task ChallengeAsync(AuthenticationProperties? properties) { - var props = new AuthenticationProperties(); - var tokens = new List(); - var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; - var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; - var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; - tokens.Add(tok1); - tokens.Add(tok2); - tokens.Add(tok3); - props.StoreTokens(tokens); - - Assert.True(props.UpdateTokenValue("One", ".11")); - Assert.True(props.UpdateTokenValue("Two", ".22")); - Assert.True(props.UpdateTokenValue("Three", ".33")); - - Assert.Equal(".11", props.GetTokenValue("One")); - Assert.Equal(".22", props.GetTokenValue("Two")); - Assert.Equal(".33", props.GetTokenValue("Three")); - Assert.Equal(3, props.GetTokens().Count()); + throw new NotImplementedException(); } - [Fact] - public void UpdateTokenValueReturnsFalseForUnknownToken() + public Task ForbidAsync(AuthenticationProperties? properties) { - var props = new AuthenticationProperties(); - var tokens = new List(); - var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; - var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; - var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; - tokens.Add(tok1); - tokens.Add(tok2); - tokens.Add(tok3); - props.StoreTokens(tokens); - - Assert.False(props.UpdateTokenValue("ONE", ".11")); - Assert.False(props.UpdateTokenValue("Jigglypuff", ".11")); - - Assert.Null(props.GetTokenValue("ONE")); - Assert.Null(props.GetTokenValue("Jigglypuff")); - Assert.Equal(3, props.GetTokens().Count()); + throw new NotImplementedException(); } - [Fact] - public async Task GetTokenWorksWithDefaultAuthenticateScheme() + public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) { - var context = new DefaultHttpContext(); - var services = new ServiceCollection().AddOptions() - .AddAuthenticationCore(o => - { - o.DefaultScheme = "simple"; - o.AddScheme("simple", s => s.HandlerType = typeof(SimpleAuth)); - }); - context.RequestServices = services.BuildServiceProvider(); - - Assert.Equal("1", await context.GetTokenAsync("One")); - Assert.Equal("2", await context.GetTokenAsync("Two")); - Assert.Equal("3", await context.GetTokenAsync("Three")); + return Task.FromResult(0); } - [Fact] - public async Task GetTokenWorksWithExplicitScheme() + public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) { - var context = new DefaultHttpContext(); - var services = new ServiceCollection().AddOptions() - .AddAuthenticationCore(o => o.AddScheme("simple", s => s.HandlerType = typeof(SimpleAuth))); - context.RequestServices = services.BuildServiceProvider(); - - Assert.Equal("1", await context.GetTokenAsync("simple", "One")); - Assert.Equal("2", await context.GetTokenAsync("simple", "Two")); - Assert.Equal("3", await context.GetTokenAsync("simple", "Three")); + throw new NotImplementedException(); } - private class SimpleAuth : IAuthenticationHandler + public Task SignOutAsync(AuthenticationProperties properties) { - public Task AuthenticateAsync() - { - var props = new AuthenticationProperties(); - var tokens = new List(); - var tok1 = new AuthenticationToken { Name = "One", Value = "1" }; - var tok2 = new AuthenticationToken { Name = "Two", Value = "2" }; - var tok3 = new AuthenticationToken { Name = "Three", Value = "3" }; - tokens.Add(tok1); - tokens.Add(tok2); - tokens.Add(tok3); - props.StoreTokens(tokens); - return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), props, "simple"))); - } - - public Task ChallengeAsync(AuthenticationProperties? properties) - { - throw new NotImplementedException(); - } - - public Task ForbidAsync(AuthenticationProperties? properties) - { - throw new NotImplementedException(); - } - - public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) - { - return Task.FromResult(0); - } - - public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties) - { - throw new NotImplementedException(); - } - - public Task SignOutAsync(AuthenticationProperties properties) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } - } + } diff --git a/src/Http/Headers/src/BaseHeaderParser.cs b/src/Http/Headers/src/BaseHeaderParser.cs index fd5ea9bd10..bf35270963 100644 --- a/src/Http/Headers/src/BaseHeaderParser.cs +++ b/src/Http/Headers/src/BaseHeaderParser.cs @@ -3,69 +3,68 @@ using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +internal abstract class BaseHeaderParser : HttpHeaderParser { - internal abstract class BaseHeaderParser : HttpHeaderParser + protected BaseHeaderParser(bool supportsMultipleValues) + : base(supportsMultipleValues) { - protected BaseHeaderParser(bool supportsMultipleValues) - : base(supportsMultipleValues) - { - } + } - protected abstract int GetParsedValueLength(StringSegment value, int startIndex, out T? parsedValue); + protected abstract int GetParsedValueLength(StringSegment value, int startIndex, out T? parsedValue); - public sealed override bool TryParseValue(StringSegment value, ref int index, out T? parsedValue) - { - parsedValue = default; + public sealed override bool TryParseValue(StringSegment value, ref int index, out T? parsedValue) + { + parsedValue = default; - // If multiple values are supported (i.e. list of values), then accept an empty string: The header may - // be added multiple times to the request/response message. E.g. - // Accept: text/xml; q=1 - // Accept: - // Accept: text/plain; q=0.2 - if (StringSegment.IsNullOrEmpty(value) || (index == value.Length)) - { - return SupportsMultipleValues; - } + // If multiple values are supported (i.e. list of values), then accept an empty string: The header may + // be added multiple times to the request/response message. E.g. + // Accept: text/xml; q=1 + // Accept: + // Accept: text/plain; q=0.2 + if (StringSegment.IsNullOrEmpty(value) || (index == value.Length)) + { + return SupportsMultipleValues; + } - var separatorFound = false; - var current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(value, index, SupportsMultipleValues, - out separatorFound); + var separatorFound = false; + var current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(value, index, SupportsMultipleValues, + out separatorFound); - if (separatorFound && !SupportsMultipleValues) - { - return false; // leading separators not allowed if we don't support multiple values. - } + if (separatorFound && !SupportsMultipleValues) + { + return false; // leading separators not allowed if we don't support multiple values. + } - if (current == value.Length) + if (current == value.Length) + { + if (SupportsMultipleValues) { - if (SupportsMultipleValues) - { - index = current; - } - return SupportsMultipleValues; + index = current; } + return SupportsMultipleValues; + } - var length = GetParsedValueLength(value, current, out var result); - - if (length == 0) - { - return false; - } + var length = GetParsedValueLength(value, current, out var result); - current = current + length; - current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(value, current, SupportsMultipleValues, - out separatorFound); + if (length == 0) + { + return false; + } - // If we support multiple values and we've not reached the end of the string, then we must have a separator. - if ((separatorFound && !SupportsMultipleValues) || (!separatorFound && (current < value.Length))) - { - return false; - } + current = current + length; + current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(value, current, SupportsMultipleValues, + out separatorFound); - index = current; - parsedValue = result; - return true; + // If we support multiple values and we've not reached the end of the string, then we must have a separator. + if ((separatorFound && !SupportsMultipleValues) || (!separatorFound && (current < value.Length))) + { + return false; } + + index = current; + parsedValue = result; + return true; } } diff --git a/src/Http/Headers/src/CacheControlHeaderValue.cs b/src/Http/Headers/src/CacheControlHeaderValue.cs index 3082fa5524..37ff5185dc 100644 --- a/src/Http/Headers/src/CacheControlHeaderValue.cs +++ b/src/Http/Headers/src/CacheControlHeaderValue.cs @@ -9,824 +9,823 @@ using System.Globalization; using System.Text; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +/// +/// Represents the Cache-Control HTTP header. +/// +public class CacheControlHeaderValue { /// - /// Represents the Cache-Control HTTP header. + /// A constant for the public cache-control directive. + /// + public static readonly string PublicString = "public"; + + /// + /// A constant for the private cache-control directive. + /// + public static readonly string PrivateString = "private"; + + /// + /// A constant for the max-age cache-control directive. + /// + public static readonly string MaxAgeString = "max-age"; + + /// + /// A constant for the s-maxage cache-control directive. + /// + public static readonly string SharedMaxAgeString = "s-maxage"; + + /// + /// A constant for the no-cache cache-control directive. + /// + public static readonly string NoCacheString = "no-cache"; + + /// + /// A constant for the no-store cache-control directive. + /// + public static readonly string NoStoreString = "no-store"; + + /// + /// A constant for the max-stale cache-control directive. + /// + public static readonly string MaxStaleString = "max-stale"; + + /// + /// A constant for the min-fresh cache-control directive. + /// + public static readonly string MinFreshString = "min-fresh"; + + /// + /// A constant for the no-transform cache-control directive. /// - public class CacheControlHeaderValue + public static readonly string NoTransformString = "no-transform"; + + /// + /// A constant for the only-if-cached cache-control directive. + /// + public static readonly string OnlyIfCachedString = "only-if-cached"; + + /// + /// A constant for the must-revalidate cache-control directive. + /// + public static readonly string MustRevalidateString = "must-revalidate"; + + /// + /// A constant for the proxy-revalidate cache-control directive. + /// + public static readonly string ProxyRevalidateString = "proxy-revalidate"; + + // The Cache-Control header is special: It is a header supporting a list of values, but we represent the list + // as _one_ instance of CacheControlHeaderValue. I.e we set 'SupportsMultipleValues' to 'true' since it is + // OK to have multiple Cache-Control headers in a request/response message. However, after parsing all + // Cache-Control headers, only one instance of CacheControlHeaderValue is created (if all headers contain valid + // values, otherwise we may have multiple strings containing the invalid values). + private static readonly HttpHeaderParser Parser + = new GenericHeaderParser(true, GetCacheControlLength); + + private static readonly Action CheckIsValidTokenAction = CheckIsValidToken; + + private bool _noCache; + private ICollection? _noCacheHeaders; + private bool _noStore; + private TimeSpan? _maxAge; + private TimeSpan? _sharedMaxAge; + private bool _maxStale; + private TimeSpan? _maxStaleLimit; + private TimeSpan? _minFresh; + private bool _noTransform; + private bool _onlyIfCached; + private bool _public; + private bool _private; + private ICollection? _privateHeaders; + private bool _mustRevalidate; + private bool _proxyRevalidate; + private IList? _extensions; + + /// + /// Initializes a new instance of . + /// + public CacheControlHeaderValue() { - /// - /// A constant for the public cache-control directive. - /// - public static readonly string PublicString = "public"; - - /// - /// A constant for the private cache-control directive. - /// - public static readonly string PrivateString = "private"; - - /// - /// A constant for the max-age cache-control directive. - /// - public static readonly string MaxAgeString = "max-age"; - - /// - /// A constant for the s-maxage cache-control directive. - /// - public static readonly string SharedMaxAgeString = "s-maxage"; - - /// - /// A constant for the no-cache cache-control directive. - /// - public static readonly string NoCacheString = "no-cache"; - - /// - /// A constant for the no-store cache-control directive. - /// - public static readonly string NoStoreString = "no-store"; - - /// - /// A constant for the max-stale cache-control directive. - /// - public static readonly string MaxStaleString = "max-stale"; - - /// - /// A constant for the min-fresh cache-control directive. - /// - public static readonly string MinFreshString = "min-fresh"; - - /// - /// A constant for the no-transform cache-control directive. - /// - public static readonly string NoTransformString = "no-transform"; - - /// - /// A constant for the only-if-cached cache-control directive. - /// - public static readonly string OnlyIfCachedString = "only-if-cached"; - - /// - /// A constant for the must-revalidate cache-control directive. - /// - public static readonly string MustRevalidateString = "must-revalidate"; - - /// - /// A constant for the proxy-revalidate cache-control directive. - /// - public static readonly string ProxyRevalidateString = "proxy-revalidate"; - - // The Cache-Control header is special: It is a header supporting a list of values, but we represent the list - // as _one_ instance of CacheControlHeaderValue. I.e we set 'SupportsMultipleValues' to 'true' since it is - // OK to have multiple Cache-Control headers in a request/response message. However, after parsing all - // Cache-Control headers, only one instance of CacheControlHeaderValue is created (if all headers contain valid - // values, otherwise we may have multiple strings containing the invalid values). - private static readonly HttpHeaderParser Parser - = new GenericHeaderParser(true, GetCacheControlLength); - - private static readonly Action CheckIsValidTokenAction = CheckIsValidToken; - - private bool _noCache; - private ICollection? _noCacheHeaders; - private bool _noStore; - private TimeSpan? _maxAge; - private TimeSpan? _sharedMaxAge; - private bool _maxStale; - private TimeSpan? _maxStaleLimit; - private TimeSpan? _minFresh; - private bool _noTransform; - private bool _onlyIfCached; - private bool _public; - private bool _private; - private ICollection? _privateHeaders; - private bool _mustRevalidate; - private bool _proxyRevalidate; - private IList? _extensions; - - /// - /// Initializes a new instance of . - /// - public CacheControlHeaderValue() - { - // This type is unique in that there is no single required parameter. - } - - /// - /// Gets or sets a value for the no-cache directive. - /// - /// Configuring no-cache indicates that the client must re-validate cached responses with the original server - /// before using it. - /// - /// - /// See https://tools.ietf.org/html/rfc7234#section-5.2.1.4 - public bool NoCache - { - get { return _noCache; } - set { _noCache = value; } - } - - /// - /// Gets a collection of field names in the "no-cache" directive in a cache-control header field on an HTTP response. - /// - public ICollection NoCacheHeaders - { - get - { - if (_noCacheHeaders == null) - { - _noCacheHeaders = new ObjectCollection(CheckIsValidTokenAction); - } - return _noCacheHeaders; - } - } + // This type is unique in that there is no single required parameter. + } + + /// + /// Gets or sets a value for the no-cache directive. + /// + /// Configuring no-cache indicates that the client must re-validate cached responses with the original server + /// before using it. + /// + /// + /// See https://tools.ietf.org/html/rfc7234#section-5.2.1.4 + public bool NoCache + { + get { return _noCache; } + set { _noCache = value; } + } - /// - /// Gets or sets a value for the no-store directive. - /// - /// Configuring no-store indicates that the response may not be stored in any cache. - /// - /// - /// See https://tools.ietf.org/html/rfc7234#section-5.2.1.5 - public bool NoStore - { - get { return _noStore; } - set { _noStore = value; } - } - - /// - /// Gets or sets a value for the max-age directive. - /// - /// max-age specifies the maximum amount of time the response is considered fresh. - /// - /// - /// See https://tools.ietf.org/html/rfc7234#section-5.2.1.1 - public TimeSpan? MaxAge - { - get { return _maxAge; } - set { _maxAge = value; } - } - - /// - /// Gets or sets a value for the s-maxage directive. - /// - /// Overrides max-age, but only for shared caches (such as proxies). - /// - /// - /// See https://tools.ietf.org/html/rfc7234#section-5.2.2.9 - public TimeSpan? SharedMaxAge - { - get { return _sharedMaxAge; } - set { _sharedMaxAge = value; } - } - - /// - /// Gets or sets a value that determines if the max-stale is included. - /// - /// max-stale that the client will accept stale responses. The maximum tolerance for staleness - /// is specified by . - /// - /// - /// See https://tools.ietf.org/html/rfc7234#section-5.2.1.2 - public bool MaxStale - { - get { return _maxStale; } - set { _maxStale = value; } - } - - /// - /// Gets or sets a value for the max-stale directive. - /// - /// Indicates the maximum duration an HTTP client is willing to accept a response that has exceeded its expiration time. - /// - /// - /// See https://tools.ietf.org/html/rfc7234#section-5.2.1.2 - public TimeSpan? MaxStaleLimit - { - get { return _maxStaleLimit; } - set { _maxStaleLimit = value; } - } - - /// - /// Gets or sets a value for the min-fresh directive. - /// - /// Indicates the freshness lifetime that an HTTP client is willing to accept a response. - /// - /// - /// See https://tools.ietf.org/html/rfc7234#section-5.2.1.3 - public TimeSpan? MinFresh - { - get { return _minFresh; } - set { _minFresh = value; } - } - - /// - /// Gets or sets a value for the no-transform request directive. - /// - /// Forbids intermediate caches or proxies from editing the response payload. - /// - /// - /// See https://tools.ietf.org/html/rfc7234#section-5.2.1.6 - public bool NoTransform - { - get { return _noTransform; } - set { _noTransform = value; } - } - - /// - /// Gets or sets a value for the only-if-cached request directive. - /// - /// Indicates that the client only wishes to obtain a stored response - /// - /// - /// See https://tools.ietf.org/html/rfc7234#section-5.2.1.7 - public bool OnlyIfCached - { - get { return _onlyIfCached; } - set { _onlyIfCached = value; } - } - - /// - /// Gets or sets a value that determines if the public response directive is included. - /// - /// Indicates that the response may be stored by any cache. - /// - /// - /// See https://tools.ietf.org/html/rfc7234#section-5.2.2.5 - public bool Public - { - get { return _public; } - set { _public = value; } - } - - /// - /// Gets or sets a value that determines if the private response directive is included. - /// - /// Indicates that the response may not be stored by a shared cache. - /// - /// - /// See https://tools.ietf.org/html/rfc7234#section-5.2.2.6 - public bool Private - { - get { return _private; } - set { _private = value; } - } - - /// - /// Gets a collection of field names in the "private" directive in a cache-control header field on an HTTP response. - /// - public ICollection PrivateHeaders - { - get + /// + /// Gets a collection of field names in the "no-cache" directive in a cache-control header field on an HTTP response. + /// + public ICollection NoCacheHeaders + { + get + { + if (_noCacheHeaders == null) { - if (_privateHeaders == null) - { - _privateHeaders = new ObjectCollection(CheckIsValidTokenAction); - } - return _privateHeaders; + _noCacheHeaders = new ObjectCollection(CheckIsValidTokenAction); } + return _noCacheHeaders; } + } - /// - /// Gets or sets a value that determines if the must-revalidate response directive is included. - /// - /// Indicates that caches must revalidate the use of stale caches with the origin server before their use. - /// - /// - /// See https://tools.ietf.org/html/rfc7234#section-5.2.2.1 - public bool MustRevalidate - { - get { return _mustRevalidate; } - set { _mustRevalidate = value; } - } + /// + /// Gets or sets a value for the no-store directive. + /// + /// Configuring no-store indicates that the response may not be stored in any cache. + /// + /// + /// See https://tools.ietf.org/html/rfc7234#section-5.2.1.5 + public bool NoStore + { + get { return _noStore; } + set { _noStore = value; } + } - /// - /// Gets or sets a value that determines if the proxy-validate response directive is included. - /// - /// Indicates that shared caches must revalidate the use of stale caches with the origin server before their use. - /// - /// - /// See https://tools.ietf.org/html/rfc7234#section-5.2.2.1 - public bool ProxyRevalidate - { - get { return _proxyRevalidate; } - set { _proxyRevalidate = value; } - } + /// + /// Gets or sets a value for the max-age directive. + /// + /// max-age specifies the maximum amount of time the response is considered fresh. + /// + /// + /// See https://tools.ietf.org/html/rfc7234#section-5.2.1.1 + public TimeSpan? MaxAge + { + get { return _maxAge; } + set { _maxAge = value; } + } - /// - /// Gets cache-extension tokens, each with an optional assigned value. - /// - public IList Extensions + /// + /// Gets or sets a value for the s-maxage directive. + /// + /// Overrides max-age, but only for shared caches (such as proxies). + /// + /// + /// See https://tools.ietf.org/html/rfc7234#section-5.2.2.9 + public TimeSpan? SharedMaxAge + { + get { return _sharedMaxAge; } + set { _sharedMaxAge = value; } + } + + /// + /// Gets or sets a value that determines if the max-stale is included. + /// + /// max-stale that the client will accept stale responses. The maximum tolerance for staleness + /// is specified by . + /// + /// + /// See https://tools.ietf.org/html/rfc7234#section-5.2.1.2 + public bool MaxStale + { + get { return _maxStale; } + set { _maxStale = value; } + } + + /// + /// Gets or sets a value for the max-stale directive. + /// + /// Indicates the maximum duration an HTTP client is willing to accept a response that has exceeded its expiration time. + /// + /// + /// See https://tools.ietf.org/html/rfc7234#section-5.2.1.2 + public TimeSpan? MaxStaleLimit + { + get { return _maxStaleLimit; } + set { _maxStaleLimit = value; } + } + + /// + /// Gets or sets a value for the min-fresh directive. + /// + /// Indicates the freshness lifetime that an HTTP client is willing to accept a response. + /// + /// + /// See https://tools.ietf.org/html/rfc7234#section-5.2.1.3 + public TimeSpan? MinFresh + { + get { return _minFresh; } + set { _minFresh = value; } + } + + /// + /// Gets or sets a value for the no-transform request directive. + /// + /// Forbids intermediate caches or proxies from editing the response payload. + /// + /// + /// See https://tools.ietf.org/html/rfc7234#section-5.2.1.6 + public bool NoTransform + { + get { return _noTransform; } + set { _noTransform = value; } + } + + /// + /// Gets or sets a value for the only-if-cached request directive. + /// + /// Indicates that the client only wishes to obtain a stored response + /// + /// + /// See https://tools.ietf.org/html/rfc7234#section-5.2.1.7 + public bool OnlyIfCached + { + get { return _onlyIfCached; } + set { _onlyIfCached = value; } + } + + /// + /// Gets or sets a value that determines if the public response directive is included. + /// + /// Indicates that the response may be stored by any cache. + /// + /// + /// See https://tools.ietf.org/html/rfc7234#section-5.2.2.5 + public bool Public + { + get { return _public; } + set { _public = value; } + } + + /// + /// Gets or sets a value that determines if the private response directive is included. + /// + /// Indicates that the response may not be stored by a shared cache. + /// + /// + /// See https://tools.ietf.org/html/rfc7234#section-5.2.2.6 + public bool Private + { + get { return _private; } + set { _private = value; } + } + + /// + /// Gets a collection of field names in the "private" directive in a cache-control header field on an HTTP response. + /// + public ICollection PrivateHeaders + { + get { - get + if (_privateHeaders == null) { - if (_extensions == null) - { - _extensions = new ObjectCollection(); - } - return _extensions; + _privateHeaders = new ObjectCollection(CheckIsValidTokenAction); } + return _privateHeaders; } + } - /// - public override string ToString() - { - var sb = new StringBuilder(); + /// + /// Gets or sets a value that determines if the must-revalidate response directive is included. + /// + /// Indicates that caches must revalidate the use of stale caches with the origin server before their use. + /// + /// + /// See https://tools.ietf.org/html/rfc7234#section-5.2.2.1 + public bool MustRevalidate + { + get { return _mustRevalidate; } + set { _mustRevalidate = value; } + } - AppendValueIfRequired(sb, _noStore, NoStoreString); - AppendValueIfRequired(sb, _noTransform, NoTransformString); - AppendValueIfRequired(sb, _onlyIfCached, OnlyIfCachedString); - AppendValueIfRequired(sb, _public, PublicString); - AppendValueIfRequired(sb, _mustRevalidate, MustRevalidateString); - AppendValueIfRequired(sb, _proxyRevalidate, ProxyRevalidateString); + /// + /// Gets or sets a value that determines if the proxy-validate response directive is included. + /// + /// Indicates that shared caches must revalidate the use of stale caches with the origin server before their use. + /// + /// + /// See https://tools.ietf.org/html/rfc7234#section-5.2.2.1 + public bool ProxyRevalidate + { + get { return _proxyRevalidate; } + set { _proxyRevalidate = value; } + } - if (_noCache) + /// + /// Gets cache-extension tokens, each with an optional assigned value. + /// + public IList Extensions + { + get + { + if (_extensions == null) { - AppendValueWithSeparatorIfRequired(sb, NoCacheString); - if ((_noCacheHeaders != null) && (_noCacheHeaders.Count > 0)) - { - sb.Append("=\""); - AppendValues(sb, _noCacheHeaders); - sb.Append('\"'); - } + _extensions = new ObjectCollection(); } + return _extensions; + } + } - if (_maxAge.HasValue) - { - AppendValueWithSeparatorIfRequired(sb, MaxAgeString); - sb.Append('='); - sb.Append(((int)_maxAge.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); - } + /// + public override string ToString() + { + var sb = new StringBuilder(); - if (_sharedMaxAge.HasValue) - { - AppendValueWithSeparatorIfRequired(sb, SharedMaxAgeString); - sb.Append('='); - sb.Append(((int)_sharedMaxAge.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); - } + AppendValueIfRequired(sb, _noStore, NoStoreString); + AppendValueIfRequired(sb, _noTransform, NoTransformString); + AppendValueIfRequired(sb, _onlyIfCached, OnlyIfCachedString); + AppendValueIfRequired(sb, _public, PublicString); + AppendValueIfRequired(sb, _mustRevalidate, MustRevalidateString); + AppendValueIfRequired(sb, _proxyRevalidate, ProxyRevalidateString); - if (_maxStale) + if (_noCache) + { + AppendValueWithSeparatorIfRequired(sb, NoCacheString); + if ((_noCacheHeaders != null) && (_noCacheHeaders.Count > 0)) { - AppendValueWithSeparatorIfRequired(sb, MaxStaleString); - if (_maxStaleLimit.HasValue) - { - sb.Append('='); - sb.Append(((int)_maxStaleLimit.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); - } + sb.Append("=\""); + AppendValues(sb, _noCacheHeaders); + sb.Append('\"'); } + } - if (_minFresh.HasValue) - { - AppendValueWithSeparatorIfRequired(sb, MinFreshString); - sb.Append('='); - sb.Append(((int)_minFresh.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); - } + if (_maxAge.HasValue) + { + AppendValueWithSeparatorIfRequired(sb, MaxAgeString); + sb.Append('='); + sb.Append(((int)_maxAge.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); + } - if (_private) + if (_sharedMaxAge.HasValue) + { + AppendValueWithSeparatorIfRequired(sb, SharedMaxAgeString); + sb.Append('='); + sb.Append(((int)_sharedMaxAge.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); + } + + if (_maxStale) + { + AppendValueWithSeparatorIfRequired(sb, MaxStaleString); + if (_maxStaleLimit.HasValue) { - AppendValueWithSeparatorIfRequired(sb, PrivateString); - if ((_privateHeaders != null) && (_privateHeaders.Count > 0)) - { - sb.Append("=\""); - AppendValues(sb, _privateHeaders); - sb.Append('\"'); - } + sb.Append('='); + sb.Append(((int)_maxStaleLimit.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); } - - NameValueHeaderValue.ToString(_extensions, ',', false, sb); - - return sb.ToString(); } - /// - public override bool Equals(object? obj) + if (_minFresh.HasValue) { - var other = obj as CacheControlHeaderValue; + AppendValueWithSeparatorIfRequired(sb, MinFreshString); + sb.Append('='); + sb.Append(((int)_minFresh.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo)); + } - if (other == null) + if (_private) + { + AppendValueWithSeparatorIfRequired(sb, PrivateString); + if ((_privateHeaders != null) && (_privateHeaders.Count > 0)) { - return false; + sb.Append("=\""); + AppendValues(sb, _privateHeaders); + sb.Append('\"'); } + } - if ((_noCache != other._noCache) || (_noStore != other._noStore) || (_maxAge != other._maxAge) || - (_sharedMaxAge != other._sharedMaxAge) || (_maxStale != other._maxStale) || - (_maxStaleLimit != other._maxStaleLimit) || (_minFresh != other._minFresh) || - (_noTransform != other._noTransform) || (_onlyIfCached != other._onlyIfCached) || - (_public != other._public) || (_private != other._private) || - (_mustRevalidate != other._mustRevalidate) || (_proxyRevalidate != other._proxyRevalidate)) - { - return false; - } + NameValueHeaderValue.ToString(_extensions, ',', false, sb); - if (!HeaderUtilities.AreEqualCollections(_noCacheHeaders, other._noCacheHeaders, - StringSegmentComparer.OrdinalIgnoreCase)) - { - return false; - } + return sb.ToString(); + } - if (!HeaderUtilities.AreEqualCollections(_privateHeaders, other._privateHeaders, - StringSegmentComparer.OrdinalIgnoreCase)) - { - return false; - } + /// + public override bool Equals(object? obj) + { + var other = obj as CacheControlHeaderValue; - if (!HeaderUtilities.AreEqualCollections(_extensions, other._extensions)) - { - return false; - } + if (other == null) + { + return false; + } - return true; + if ((_noCache != other._noCache) || (_noStore != other._noStore) || (_maxAge != other._maxAge) || + (_sharedMaxAge != other._sharedMaxAge) || (_maxStale != other._maxStale) || + (_maxStaleLimit != other._maxStaleLimit) || (_minFresh != other._minFresh) || + (_noTransform != other._noTransform) || (_onlyIfCached != other._onlyIfCached) || + (_public != other._public) || (_private != other._private) || + (_mustRevalidate != other._mustRevalidate) || (_proxyRevalidate != other._proxyRevalidate)) + { + return false; } - /// - public override int GetHashCode() + if (!HeaderUtilities.AreEqualCollections(_noCacheHeaders, other._noCacheHeaders, + StringSegmentComparer.OrdinalIgnoreCase)) { - // Use a different bit for bool fields: bool.GetHashCode() will return 0 (false) or 1 (true). So we would - // end up having the same hash code for e.g. two instances where one has only noCache set and the other - // only noStore. - int result = _noCache.GetHashCode() ^ (_noStore.GetHashCode() << 1) ^ (_maxStale.GetHashCode() << 2) ^ - (_noTransform.GetHashCode() << 3) ^ (_onlyIfCached.GetHashCode() << 4) ^ - (_public.GetHashCode() << 5) ^ (_private.GetHashCode() << 6) ^ - (_mustRevalidate.GetHashCode() << 7) ^ (_proxyRevalidate.GetHashCode() << 8); + return false; + } - // XOR the hashcode of timespan values with different numbers to make sure two instances with the same - // timespan set on different fields result in different hashcodes. - result = result ^ (_maxAge.HasValue ? _maxAge.GetValueOrDefault().GetHashCode() ^ 1 : 0) ^ - (_sharedMaxAge.HasValue ? _sharedMaxAge.GetValueOrDefault().GetHashCode() ^ 2 : 0) ^ - (_maxStaleLimit.HasValue ? _maxStaleLimit.GetValueOrDefault().GetHashCode() ^ 4 : 0) ^ - (_minFresh.HasValue ? _minFresh.GetValueOrDefault().GetHashCode() ^ 8 : 0); + if (!HeaderUtilities.AreEqualCollections(_privateHeaders, other._privateHeaders, + StringSegmentComparer.OrdinalIgnoreCase)) + { + return false; + } - if ((_noCacheHeaders != null) && (_noCacheHeaders.Count > 0)) - { - foreach (var noCacheHeader in _noCacheHeaders) - { - result = result ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(noCacheHeader); - } - } + if (!HeaderUtilities.AreEqualCollections(_extensions, other._extensions)) + { + return false; + } - if ((_privateHeaders != null) && (_privateHeaders.Count > 0)) - { - foreach (var privateHeader in _privateHeaders) - { - result = result ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(privateHeader); - } - } + return true; + } - if ((_extensions != null) && (_extensions.Count > 0)) + /// + public override int GetHashCode() + { + // Use a different bit for bool fields: bool.GetHashCode() will return 0 (false) or 1 (true). So we would + // end up having the same hash code for e.g. two instances where one has only noCache set and the other + // only noStore. + int result = _noCache.GetHashCode() ^ (_noStore.GetHashCode() << 1) ^ (_maxStale.GetHashCode() << 2) ^ + (_noTransform.GetHashCode() << 3) ^ (_onlyIfCached.GetHashCode() << 4) ^ + (_public.GetHashCode() << 5) ^ (_private.GetHashCode() << 6) ^ + (_mustRevalidate.GetHashCode() << 7) ^ (_proxyRevalidate.GetHashCode() << 8); + + // XOR the hashcode of timespan values with different numbers to make sure two instances with the same + // timespan set on different fields result in different hashcodes. + result = result ^ (_maxAge.HasValue ? _maxAge.GetValueOrDefault().GetHashCode() ^ 1 : 0) ^ + (_sharedMaxAge.HasValue ? _sharedMaxAge.GetValueOrDefault().GetHashCode() ^ 2 : 0) ^ + (_maxStaleLimit.HasValue ? _maxStaleLimit.GetValueOrDefault().GetHashCode() ^ 4 : 0) ^ + (_minFresh.HasValue ? _minFresh.GetValueOrDefault().GetHashCode() ^ 8 : 0); + + if ((_noCacheHeaders != null) && (_noCacheHeaders.Count > 0)) + { + foreach (var noCacheHeader in _noCacheHeaders) { - foreach (var extension in _extensions) - { - result = result ^ extension.GetHashCode(); - } + result = result ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(noCacheHeader); } - - return result; } - /// - /// Parses as a value. - /// - /// The values to parse. - /// The parsed values. - public static CacheControlHeaderValue Parse(StringSegment input) + if ((_privateHeaders != null) && (_privateHeaders.Count > 0)) { - var index = 0; - // Cache-Control is unusual because there are no required values so the parser will succeed for an empty string, but still return null. - var result = Parser.ParseValue(input, ref index); - if (result == null) + foreach (var privateHeader in _privateHeaders) { - throw new FormatException("No cache directives found."); + result = result ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(privateHeader); } - return result; } - /// - /// Attempts to parse the specified as a . - /// - /// The value to parse. - /// The parsed value. - /// if input is a valid , otherwise . - public static bool TryParse(StringSegment input, [NotNullWhen(true)] out CacheControlHeaderValue? parsedValue) + if ((_extensions != null) && (_extensions.Count > 0)) { - var index = 0; - // Cache-Control is unusual because there are no required values so the parser will succeed for an empty string, but still return null. - if (Parser.TryParseValue(input, ref index, out parsedValue) && parsedValue != null) + foreach (var extension in _extensions) { - return true; + result = result ^ extension.GetHashCode(); } - parsedValue = null; - return false; } - private static int GetCacheControlLength(StringSegment input, int startIndex, out CacheControlHeaderValue? parsedValue) + return result; + } + + /// + /// Parses as a value. + /// + /// The values to parse. + /// The parsed values. + public static CacheControlHeaderValue Parse(StringSegment input) + { + var index = 0; + // Cache-Control is unusual because there are no required values so the parser will succeed for an empty string, but still return null. + var result = Parser.ParseValue(input, ref index); + if (result == null) { - Contract.Requires(startIndex >= 0); + throw new FormatException("No cache directives found."); + } + return result; + } - parsedValue = null; + /// + /// Attempts to parse the specified as a . + /// + /// The value to parse. + /// The parsed value. + /// if input is a valid , otherwise . + public static bool TryParse(StringSegment input, [NotNullWhen(true)] out CacheControlHeaderValue? parsedValue) + { + var index = 0; + // Cache-Control is unusual because there are no required values so the parser will succeed for an empty string, but still return null. + if (Parser.TryParseValue(input, ref index, out parsedValue) && parsedValue != null) + { + return true; + } + parsedValue = null; + return false; + } + + private static int GetCacheControlLength(StringSegment input, int startIndex, out CacheControlHeaderValue? parsedValue) + { + Contract.Requires(startIndex >= 0); - if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) + parsedValue = null; + + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) + { + return 0; + } + + // Cache-Control header consists of a list of name/value pairs, where the value is optional. So use an + // instance of NameValueHeaderParser to parse the string. + var current = startIndex; + var nameValueList = new List(); + while (current < input.Length) + { + if (!NameValueHeaderValue.MultipleValueParser.TryParseValue(input, ref current, out var nameValue)) { return 0; } - // Cache-Control header consists of a list of name/value pairs, where the value is optional. So use an - // instance of NameValueHeaderParser to parse the string. - var current = startIndex; - var nameValueList = new List(); - while (current < input.Length) + if (nameValue != null) { - if (!NameValueHeaderValue.MultipleValueParser.TryParseValue(input, ref current, out var nameValue)) - { - return 0; - } - - if (nameValue != null) - { - nameValueList.Add(nameValue); - } + nameValueList.Add(nameValue); } + } - // If we get here, we were able to successfully parse the string as list of name/value pairs. Now analyze - // the name/value pairs. + // If we get here, we were able to successfully parse the string as list of name/value pairs. Now analyze + // the name/value pairs. - // Cache-Control is a header supporting lists of values. However, expose the header as an instance of - // CacheControlHeaderValue. - var result = new CacheControlHeaderValue(); + // Cache-Control is a header supporting lists of values. However, expose the header as an instance of + // CacheControlHeaderValue. + var result = new CacheControlHeaderValue(); - if (!TrySetCacheControlValues(result, nameValueList)) - { - return 0; - } + if (!TrySetCacheControlValues(result, nameValueList)) + { + return 0; + } - parsedValue = result; + parsedValue = result; - // If we get here we successfully parsed the whole string. - return input.Length - startIndex; - } + // If we get here we successfully parsed the whole string. + return input.Length - startIndex; + } - private static bool TrySetCacheControlValues( - CacheControlHeaderValue cc, - List nameValueList) + private static bool TrySetCacheControlValues( + CacheControlHeaderValue cc, + List nameValueList) + { + for (var i = 0; i < nameValueList.Count; i++) { - for (var i = 0; i < nameValueList.Count; i++) - { - var nameValue = nameValueList[i]; - var name = nameValue.Name; - var success = true; - - switch (name.Length) - { - case 6: - if (StringSegment.Equals(PublicString, name, StringComparison.OrdinalIgnoreCase)) - { - success = TrySetTokenOnlyValue(nameValue, ref cc._public); - } - else - { - goto default; - } - break; - - case 7: - if (StringSegment.Equals(MaxAgeString, name, StringComparison.OrdinalIgnoreCase)) - { - success = TrySetTimeSpan(nameValue, ref cc._maxAge); - } - else if(StringSegment.Equals(PrivateString, name, StringComparison.OrdinalIgnoreCase)) - { - success = TrySetOptionalTokenList(nameValue, ref cc._private, ref cc._privateHeaders); - } - else - { - goto default; - } - break; + var nameValue = nameValueList[i]; + var name = nameValue.Name; + var success = true; + + switch (name.Length) + { + case 6: + if (StringSegment.Equals(PublicString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTokenOnlyValue(nameValue, ref cc._public); + } + else + { + goto default; + } + break; - case 8: - if (StringSegment.Equals(NoCacheString, name, StringComparison.OrdinalIgnoreCase)) - { - success = TrySetOptionalTokenList(nameValue, ref cc._noCache, ref cc._noCacheHeaders); - } - else if (StringSegment.Equals(NoStoreString, name, StringComparison.OrdinalIgnoreCase)) - { - success = TrySetTokenOnlyValue(nameValue, ref cc._noStore); - } - else if (StringSegment.Equals(SharedMaxAgeString, name, StringComparison.OrdinalIgnoreCase)) - { - success = TrySetTimeSpan(nameValue, ref cc._sharedMaxAge); - } - else - { - goto default; - } - break; + case 7: + if (StringSegment.Equals(MaxAgeString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTimeSpan(nameValue, ref cc._maxAge); + } + else if (StringSegment.Equals(PrivateString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetOptionalTokenList(nameValue, ref cc._private, ref cc._privateHeaders); + } + else + { + goto default; + } + break; - case 9: - if (StringSegment.Equals(MaxStaleString, name, StringComparison.OrdinalIgnoreCase)) - { - success = ((nameValue.Value == null) || TrySetTimeSpan(nameValue, ref cc._maxStaleLimit)); - if (success) - { - cc._maxStale = true; - } - } - else if (StringSegment.Equals(MinFreshString, name, StringComparison.OrdinalIgnoreCase)) - { - success = TrySetTimeSpan(nameValue, ref cc._minFresh); - } - else - { - goto default; - } - break; + case 8: + if (StringSegment.Equals(NoCacheString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetOptionalTokenList(nameValue, ref cc._noCache, ref cc._noCacheHeaders); + } + else if (StringSegment.Equals(NoStoreString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTokenOnlyValue(nameValue, ref cc._noStore); + } + else if (StringSegment.Equals(SharedMaxAgeString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTimeSpan(nameValue, ref cc._sharedMaxAge); + } + else + { + goto default; + } + break; - case 12: - if (StringSegment.Equals(NoTransformString, name, StringComparison.OrdinalIgnoreCase)) + case 9: + if (StringSegment.Equals(MaxStaleString, name, StringComparison.OrdinalIgnoreCase)) + { + success = ((nameValue.Value == null) || TrySetTimeSpan(nameValue, ref cc._maxStaleLimit)); + if (success) { - success = TrySetTokenOnlyValue(nameValue, ref cc._noTransform); + cc._maxStale = true; } - else - { - goto default; - } - break; + } + else if (StringSegment.Equals(MinFreshString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTimeSpan(nameValue, ref cc._minFresh); + } + else + { + goto default; + } + break; - case 14: - if (StringSegment.Equals(OnlyIfCachedString, name, StringComparison.OrdinalIgnoreCase)) - { - success = TrySetTokenOnlyValue(nameValue, ref cc._onlyIfCached); - } - else - { - goto default; - } - break; + case 12: + if (StringSegment.Equals(NoTransformString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTokenOnlyValue(nameValue, ref cc._noTransform); + } + else + { + goto default; + } + break; - case 15: - if (StringSegment.Equals(MustRevalidateString, name, StringComparison.OrdinalIgnoreCase)) - { - success = TrySetTokenOnlyValue(nameValue, ref cc._mustRevalidate); - } - else - { - goto default; - } - break; + case 14: + if (StringSegment.Equals(OnlyIfCachedString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTokenOnlyValue(nameValue, ref cc._onlyIfCached); + } + else + { + goto default; + } + break; - case 16: - if (StringSegment.Equals(ProxyRevalidateString, name, StringComparison.OrdinalIgnoreCase)) - { - success = TrySetTokenOnlyValue(nameValue, ref cc._proxyRevalidate); - } - else - { - goto default; - } - break; + case 15: + if (StringSegment.Equals(MustRevalidateString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTokenOnlyValue(nameValue, ref cc._mustRevalidate); + } + else + { + goto default; + } + break; - default: - cc.Extensions.Add(nameValue); // success is always true - break; - } + case 16: + if (StringSegment.Equals(ProxyRevalidateString, name, StringComparison.OrdinalIgnoreCase)) + { + success = TrySetTokenOnlyValue(nameValue, ref cc._proxyRevalidate); + } + else + { + goto default; + } + break; - if (!success) - { - return false; - } + default: + cc.Extensions.Add(nameValue); // success is always true + break; } - return true; - } - - private static bool TrySetTokenOnlyValue(NameValueHeaderValue nameValue, ref bool boolField) - { - if (nameValue.Value != null) + if (!success) { return false; } + } + + return true; + } + private static bool TrySetTokenOnlyValue(NameValueHeaderValue nameValue, ref bool boolField) + { + if (nameValue.Value != null) + { + return false; + } + + boolField = true; + return true; + } + + private static bool TrySetOptionalTokenList( + NameValueHeaderValue nameValue, + ref bool boolField, + ref ICollection? destination) + { + if (nameValue.Value == null) + { boolField = true; return true; } - private static bool TrySetOptionalTokenList( - NameValueHeaderValue nameValue, - ref bool boolField, - ref ICollection? destination) + // We need the string to be at least 3 chars long: 2x quotes and at least 1 character. Also make sure we + // have a quoted string. Note that NameValueHeaderValue will never have leading/trailing whitespaces. + var valueString = nameValue.Value; + if ((valueString.Length < 3) || (valueString[0] != '\"') || (valueString[valueString.Length - 1] != '\"')) + { + return false; + } + + // We have a quoted string. Now verify that the string contains a list of valid tokens separated by ','. + var current = 1; // skip the initial '"' character. + var maxLength = valueString.Length - 1; // -1 because we don't want to parse the final '"'. + var separatorFound = false; + var originalValueCount = destination == null ? 0 : destination.Count; + while (current < maxLength) { - if (nameValue.Value == null) + current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(valueString, current, true, + out separatorFound); + + if (current == maxLength) { - boolField = true; - return true; + break; } - // We need the string to be at least 3 chars long: 2x quotes and at least 1 character. Also make sure we - // have a quoted string. Note that NameValueHeaderValue will never have leading/trailing whitespaces. - var valueString = nameValue.Value; - if ((valueString.Length < 3) || (valueString[0] != '\"') || (valueString[valueString.Length - 1] != '\"')) + var tokenLength = HttpRuleParser.GetTokenLength(valueString, current); + + if (tokenLength == 0) { + // We already skipped whitespaces and separators. If we don't have a token it must be an invalid + // character. return false; } - // We have a quoted string. Now verify that the string contains a list of valid tokens separated by ','. - var current = 1; // skip the initial '"' character. - var maxLength = valueString.Length - 1; // -1 because we don't want to parse the final '"'. - var separatorFound = false; - var originalValueCount = destination == null ? 0 : destination.Count; - while (current < maxLength) + if (destination == null) { - current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(valueString, current, true, - out separatorFound); - - if (current == maxLength) - { - break; - } - - var tokenLength = HttpRuleParser.GetTokenLength(valueString, current); - - if (tokenLength == 0) - { - // We already skipped whitespaces and separators. If we don't have a token it must be an invalid - // character. - return false; - } + destination = new ObjectCollection(CheckIsValidTokenAction); + } - if (destination == null) - { - destination = new ObjectCollection(CheckIsValidTokenAction); - } + destination.Add(valueString.Subsegment(current, tokenLength)); - destination.Add(valueString.Subsegment(current, tokenLength)); + current = current + tokenLength; + } - current = current + tokenLength; - } + // After parsing a valid token list, we expect to have at least one value + if ((destination != null) && (destination.Count > originalValueCount)) + { + boolField = true; + return true; + } - // After parsing a valid token list, we expect to have at least one value - if ((destination != null) && (destination.Count > originalValueCount)) - { - boolField = true; - return true; - } + return false; + } + private static bool TrySetTimeSpan(NameValueHeaderValue nameValue, ref TimeSpan? timeSpan) + { + if (nameValue.Value == null) + { return false; } - private static bool TrySetTimeSpan(NameValueHeaderValue nameValue, ref TimeSpan? timeSpan) + int seconds; + if (!HeaderUtilities.TryParseNonNegativeInt32(nameValue.Value, out seconds)) { - if (nameValue.Value == null) - { - return false; - } + return false; + } - int seconds; - if (!HeaderUtilities.TryParseNonNegativeInt32(nameValue.Value, out seconds)) - { - return false; - } + timeSpan = new TimeSpan(0, 0, seconds); - timeSpan = new TimeSpan(0, 0, seconds); + return true; + } - return true; + private static void AppendValueIfRequired(StringBuilder sb, bool appendValue, string value) + { + if (appendValue) + { + AppendValueWithSeparatorIfRequired(sb, value); } + } - private static void AppendValueIfRequired(StringBuilder sb, bool appendValue, string value) + private static void AppendValueWithSeparatorIfRequired(StringBuilder sb, string value) + { + if (sb.Length > 0) { - if (appendValue) - { - AppendValueWithSeparatorIfRequired(sb, value); - } + sb.Append(", "); } + sb.Append(value); + } - private static void AppendValueWithSeparatorIfRequired(StringBuilder sb, string value) + private static void AppendValues(StringBuilder sb, IEnumerable values) + { + var first = true; + foreach (var value in values) { - if (sb.Length > 0) + if (first) { - sb.Append(", "); + first = false; } - sb.Append(value); - } - - private static void AppendValues(StringBuilder sb, IEnumerable values) - { - var first = true; - foreach (var value in values) + else { - if (first) - { - first = false; - } - else - { - sb.Append(", "); - } - - sb.Append(value.AsSpan()); + sb.Append(", "); } - } - private static void CheckIsValidToken(StringSegment item) - { - HeaderUtilities.CheckValidToken(item, nameof(item)); + sb.Append(value.AsSpan()); } } + + private static void CheckIsValidToken(StringSegment item) + { + HeaderUtilities.CheckValidToken(item, nameof(item)); + } } diff --git a/src/Http/Headers/src/ContentDispositionHeaderValue.cs b/src/Http/Headers/src/ContentDispositionHeaderValue.cs index 1cf3b08242..0ce6151204 100644 --- a/src/Http/Headers/src/ContentDispositionHeaderValue.cs +++ b/src/Http/Headers/src/ContentDispositionHeaderValue.cs @@ -13,819 +13,818 @@ using System.Runtime.CompilerServices; using System.Text; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +/// +/// Represents the value of a Content-Disposition header. +/// +/// +/// Note this is for use both in HTTP (https://tools.ietf.org/html/rfc6266) and MIME (https://tools.ietf.org/html/rfc2183) +/// +public class ContentDispositionHeaderValue { + private const string FileNameString = "filename"; + private const string NameString = "name"; + private const string FileNameStarString = "filename*"; + private const string CreationDateString = "creation-date"; + private const string ModificationDateString = "modification-date"; + private const string ReadDateString = "read-date"; + private const string SizeString = "size"; + private const int MaxStackAllocSizeBytes = 256; + private static readonly char[] QuestionMark = new char[] { '?' }; + private static readonly char[] SingleQuote = new char[] { '\'' }; + private static readonly char[] EscapeChars = new char[] { '\\', '"' }; + private static ReadOnlySpan MimePrefix => new byte[] { (byte)'"', (byte)'=', (byte)'?', (byte)'u', (byte)'t', (byte)'f', (byte)'-', (byte)'8', (byte)'?', (byte)'B', (byte)'?' }; + private static ReadOnlySpan MimeSuffix => new byte[] { (byte)'?', (byte)'=', (byte)'"' }; + + private static readonly HttpHeaderParser Parser + = new GenericHeaderParser(false, GetDispositionTypeLength); + + // Use list instead of dictionary since we may have multiple parameters with the same name. + private ObjectCollection? _parameters; + private StringSegment _dispositionType; + + private ContentDispositionHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + /// - /// Represents the value of a Content-Disposition header. + /// Initializes a new instance of . /// - /// - /// Note this is for use both in HTTP (https://tools.ietf.org/html/rfc6266) and MIME (https://tools.ietf.org/html/rfc2183) - /// - public class ContentDispositionHeaderValue - { - private const string FileNameString = "filename"; - private const string NameString = "name"; - private const string FileNameStarString = "filename*"; - private const string CreationDateString = "creation-date"; - private const string ModificationDateString = "modification-date"; - private const string ReadDateString = "read-date"; - private const string SizeString = "size"; - private const int MaxStackAllocSizeBytes = 256; - private static readonly char[] QuestionMark = new char[] { '?' }; - private static readonly char[] SingleQuote = new char[] { '\'' }; - private static readonly char[] EscapeChars = new char[] { '\\', '"' }; - private static ReadOnlySpan MimePrefix => new byte[] { (byte)'"', (byte)'=', (byte)'?', (byte)'u', (byte)'t', (byte)'f', (byte)'-', (byte)'8', (byte)'?', (byte)'B', (byte)'?' }; - private static ReadOnlySpan MimeSuffix => new byte[] { (byte)'?', (byte)'=', (byte)'"' }; - - private static readonly HttpHeaderParser Parser - = new GenericHeaderParser(false, GetDispositionTypeLength); - - // Use list instead of dictionary since we may have multiple parameters with the same name. - private ObjectCollection? _parameters; - private StringSegment _dispositionType; - - private ContentDispositionHeaderValue() - { - // Used by the parser to create a new instance of this type. - } - - /// - /// Initializes a new instance of . - /// - /// A that represents a content disposition type. - public ContentDispositionHeaderValue(StringSegment dispositionType) - { - CheckDispositionTypeFormat(dispositionType, "dispositionType"); - _dispositionType = dispositionType; - } - - /// - /// Gets or sets a content disposition type. - /// - public StringSegment DispositionType - { - get { return _dispositionType; } - set - { - CheckDispositionTypeFormat(value, "value"); - _dispositionType = value; - } + /// A that represents a content disposition type. + public ContentDispositionHeaderValue(StringSegment dispositionType) + { + CheckDispositionTypeFormat(dispositionType, "dispositionType"); + _dispositionType = dispositionType; + } + + /// + /// Gets or sets a content disposition type. + /// + public StringSegment DispositionType + { + get { return _dispositionType; } + set + { + CheckDispositionTypeFormat(value, "value"); + _dispositionType = value; } + } - /// - /// Gets a collection of parameters included the Content-Disposition header. - /// - public IList Parameters + /// + /// Gets a collection of parameters included the Content-Disposition header. + /// + public IList Parameters + { + get { - get + if (_parameters == null) { - if (_parameters == null) - { - _parameters = new ObjectCollection(); - } - return _parameters; + _parameters = new ObjectCollection(); } + return _parameters; } + } - // Helpers to access specific parameters in the list + // Helpers to access specific parameters in the list - /// - /// Gets or sets the name of the content body part. - /// - public StringSegment Name - { - get { return GetName(NameString); } - set { SetName(NameString, value); } - } + /// + /// Gets or sets the name of the content body part. + /// + public StringSegment Name + { + get { return GetName(NameString); } + set { SetName(NameString, value); } + } - /// - /// Gets or sets a value that suggests how to construct a filename for storing the message payload - /// to be used if the entity is detached and stored in a separate file. - /// - public StringSegment FileName - { - get { return GetName(FileNameString); } - set { SetName(FileNameString, value); } - } + /// + /// Gets or sets a value that suggests how to construct a filename for storing the message payload + /// to be used if the entity is detached and stored in a separate file. + /// + public StringSegment FileName + { + get { return GetName(FileNameString); } + set { SetName(FileNameString, value); } + } - /// - /// Gets or sets a value that suggests how to construct filenames for storing message payloads - /// to be used if the entities are detached and stored in a separate files. - /// - public StringSegment FileNameStar - { - get { return GetName(FileNameStarString); } - set { SetName(FileNameStarString, value); } - } + /// + /// Gets or sets a value that suggests how to construct filenames for storing message payloads + /// to be used if the entities are detached and stored in a separate files. + /// + public StringSegment FileNameStar + { + get { return GetName(FileNameStarString); } + set { SetName(FileNameStarString, value); } + } - /// - /// Gets or sets the at which the file was created. - /// - public DateTimeOffset? CreationDate - { - get { return GetDate(CreationDateString); } - set { SetDate(CreationDateString, value); } - } + /// + /// Gets or sets the at which the file was created. + /// + public DateTimeOffset? CreationDate + { + get { return GetDate(CreationDateString); } + set { SetDate(CreationDateString, value); } + } - /// - /// Gets or sets the at which the file was last modified. - /// - public DateTimeOffset? ModificationDate - { - get { return GetDate(ModificationDateString); } - set { SetDate(ModificationDateString, value); } - } + /// + /// Gets or sets the at which the file was last modified. + /// + public DateTimeOffset? ModificationDate + { + get { return GetDate(ModificationDateString); } + set { SetDate(ModificationDateString, value); } + } - /// - /// Gets or sets the at which the file was last read. - /// - public DateTimeOffset? ReadDate - { - get { return GetDate(ReadDateString); } - set { SetDate(ReadDateString, value); } - } + /// + /// Gets or sets the at which the file was last read. + /// + public DateTimeOffset? ReadDate + { + get { return GetDate(ReadDateString); } + set { SetDate(ReadDateString, value); } + } - /// - /// Gets or sets the approximate size, in bytes, of the file. - /// - public long? Size + /// + /// Gets or sets the approximate size, in bytes, of the file. + /// + public long? Size + { + get { - get + var sizeParameter = NameValueHeaderValue.Find(_parameters, SizeString); + if (sizeParameter != null) { - var sizeParameter = NameValueHeaderValue.Find(_parameters, SizeString); - if (sizeParameter != null) + var sizeString = sizeParameter.Value; + if (HeaderUtilities.TryParseNonNegativeInt64(sizeString, out var value)) { - var sizeString = sizeParameter.Value; - if (HeaderUtilities.TryParseNonNegativeInt64(sizeString, out var value)) - { - return value; - } + return value; } - return null; } - set + return null; + } + set + { + var sizeParameter = NameValueHeaderValue.Find(_parameters, SizeString); + if (value == null) { - var sizeParameter = NameValueHeaderValue.Find(_parameters, SizeString); - if (value == null) - { - // Remove parameter - if (sizeParameter != null) - { - _parameters!.Remove(sizeParameter); - } - } - else if (value < 0) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } - else if (sizeParameter != null) - { - sizeParameter.Value = value.GetValueOrDefault().ToString(CultureInfo.InvariantCulture); - } - else + // Remove parameter + if (sizeParameter != null) { - var sizeString = value.GetValueOrDefault().ToString(CultureInfo.InvariantCulture); - Parameters.Add(new NameValueHeaderValue(SizeString, sizeString)); + _parameters!.Remove(sizeParameter); } } - } - - /// - /// Sets both FileName and FileNameStar using encodings appropriate for HTTP headers. - /// - /// - public void SetHttpFileName(StringSegment fileName) - { - if (!StringSegment.IsNullOrEmpty(fileName)) + else if (value < 0) { - FileName = Sanitize(fileName); + throw new ArgumentOutOfRangeException(nameof(value)); + } + else if (sizeParameter != null) + { + sizeParameter.Value = value.GetValueOrDefault().ToString(CultureInfo.InvariantCulture); } else { - FileName = fileName; + var sizeString = value.GetValueOrDefault().ToString(CultureInfo.InvariantCulture); + Parameters.Add(new NameValueHeaderValue(SizeString, sizeString)); } - FileNameStar = fileName; } + } - /// - /// Sets the FileName parameter using encodings appropriate for MIME headers. - /// The FileNameStar parameter is removed. - /// - /// - public void SetMimeFileName(StringSegment fileName) + /// + /// Sets both FileName and FileNameStar using encodings appropriate for HTTP headers. + /// + /// + public void SetHttpFileName(StringSegment fileName) + { + if (!StringSegment.IsNullOrEmpty(fileName)) { - FileNameStar = null; - FileName = fileName; + FileName = Sanitize(fileName); } - - /// - public override string ToString() + else { - return _dispositionType + NameValueHeaderValue.ToString(_parameters, ';', true); + FileName = fileName; } + FileNameStar = fileName; + } - /// - public override bool Equals(object? obj) - { - var other = obj as ContentDispositionHeaderValue; + /// + /// Sets the FileName parameter using encodings appropriate for MIME headers. + /// The FileNameStar parameter is removed. + /// + /// + public void SetMimeFileName(StringSegment fileName) + { + FileNameStar = null; + FileName = fileName; + } - if (other == null) - { - return false; - } + /// + public override string ToString() + { + return _dispositionType + NameValueHeaderValue.ToString(_parameters, ';', true); + } - return _dispositionType.Equals(other._dispositionType, StringComparison.OrdinalIgnoreCase) && - HeaderUtilities.AreEqualCollections(_parameters, other._parameters); - } + /// + public override bool Equals(object? obj) + { + var other = obj as ContentDispositionHeaderValue; - /// - public override int GetHashCode() + if (other == null) { - // The dispositionType string is case-insensitive. - return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_dispositionType) ^ NameValueHeaderValue.GetHashCode(_parameters); + return false; } - /// - /// Parses as a value. - /// - /// The values to parse. - /// The parsed values. - public static ContentDispositionHeaderValue Parse(StringSegment input) + return _dispositionType.Equals(other._dispositionType, StringComparison.OrdinalIgnoreCase) && + HeaderUtilities.AreEqualCollections(_parameters, other._parameters); + } + + /// + public override int GetHashCode() + { + // The dispositionType string is case-insensitive. + return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_dispositionType) ^ NameValueHeaderValue.GetHashCode(_parameters); + } + + /// + /// Parses as a value. + /// + /// The values to parse. + /// The parsed values. + public static ContentDispositionHeaderValue Parse(StringSegment input) + { + var index = 0; + return Parser.ParseValue(input, ref index)!; + } + + /// + /// Attempts to parse the specified as a . + /// + /// The value to parse. + /// The parsed value. + /// if input is a valid , otherwise . + public static bool TryParse(StringSegment input, [NotNullWhen(true)] out ContentDispositionHeaderValue? parsedValue) + { + var index = 0; + return Parser.TryParseValue(input, ref index, out parsedValue!); + } + + private static int GetDispositionTypeLength(StringSegment input, int startIndex, out ContentDispositionHeaderValue? parsedValue) + { + Contract.Requires(startIndex >= 0); + + parsedValue = null; + + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) { - var index = 0; - return Parser.ParseValue(input, ref index)!; + return 0; } - /// - /// Attempts to parse the specified as a . - /// - /// The value to parse. - /// The parsed value. - /// if input is a valid , otherwise . - public static bool TryParse(StringSegment input, [NotNullWhen(true)] out ContentDispositionHeaderValue? parsedValue) + // Caller must remove leading whitespaces. If not, we'll return 0. + var dispositionTypeLength = GetDispositionTypeExpressionLength(input, startIndex, out var dispositionType); + + if (dispositionTypeLength == 0) { - var index = 0; - return Parser.TryParseValue(input, ref index, out parsedValue!); + return 0; } - private static int GetDispositionTypeLength(StringSegment input, int startIndex, out ContentDispositionHeaderValue? parsedValue) + var current = startIndex + dispositionTypeLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + var contentDispositionHeader = new ContentDispositionHeaderValue(); + contentDispositionHeader._dispositionType = dispositionType; + + // If we're not done and we have a parameter delimiter, then we have a list of parameters. + if ((current < input.Length) && (input[current] == ';')) { - Contract.Requires(startIndex >= 0); + current++; // skip delimiter. + int parameterLength = NameValueHeaderValue.GetNameValueListLength(input, current, ';', + contentDispositionHeader.Parameters); - parsedValue = null; + parsedValue = contentDispositionHeader; + return current + parameterLength - startIndex; + } - if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) - { - return 0; - } + // We have a ContentDisposition header without parameters. + parsedValue = contentDispositionHeader; + return current - startIndex; + } - // Caller must remove leading whitespaces. If not, we'll return 0. - var dispositionTypeLength = GetDispositionTypeExpressionLength(input, startIndex, out var dispositionType); + private static int GetDispositionTypeExpressionLength(StringSegment input, int startIndex, out StringSegment dispositionType) + { + Contract.Requires((input.Length > 0) && (startIndex < input.Length)); - if (dispositionTypeLength == 0) - { - return 0; - } + // This method just parses the disposition type string, it does not parse parameters. + dispositionType = null; - var current = startIndex + dispositionTypeLength; - current = current + HttpRuleParser.GetWhitespaceLength(input, current); - var contentDispositionHeader = new ContentDispositionHeaderValue(); - contentDispositionHeader._dispositionType = dispositionType; + // Parse the disposition type, i.e. in content-disposition string + // "; param1=value1; param2=value2" + var typeLength = HttpRuleParser.GetTokenLength(input, startIndex); - // If we're not done and we have a parameter delimiter, then we have a list of parameters. - if ((current < input.Length) && (input[current] == ';')) - { - current++; // skip delimiter. - int parameterLength = NameValueHeaderValue.GetNameValueListLength(input, current, ';', - contentDispositionHeader.Parameters); + if (typeLength == 0) + { + return 0; + } - parsedValue = contentDispositionHeader; - return current + parameterLength - startIndex; - } + dispositionType = input.Subsegment(startIndex, typeLength); + return typeLength; + } - // We have a ContentDisposition header without parameters. - parsedValue = contentDispositionHeader; - return current - startIndex; + private static void CheckDispositionTypeFormat(StringSegment dispositionType, string parameterName) + { + if (StringSegment.IsNullOrEmpty(dispositionType)) + { + throw new ArgumentException("An empty string is not allowed.", parameterName); } - private static int GetDispositionTypeExpressionLength(StringSegment input, int startIndex, out StringSegment dispositionType) + // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. + var dispositionTypeLength = GetDispositionTypeExpressionLength(dispositionType, 0, out var tempDispositionType); + if ((dispositionTypeLength == 0) || (tempDispositionType.Length != dispositionType.Length)) { - Contract.Requires((input.Length > 0) && (startIndex < input.Length)); - - // This method just parses the disposition type string, it does not parse parameters. - dispositionType = null; - - // Parse the disposition type, i.e. in content-disposition string - // "; param1=value1; param2=value2" - var typeLength = HttpRuleParser.GetTokenLength(input, startIndex); - - if (typeLength == 0) - { - return 0; - } - - dispositionType = input.Subsegment(startIndex, typeLength); - return typeLength; + throw new FormatException(string.Format(CultureInfo.InvariantCulture, + "Invalid disposition type '{0}'.", dispositionType)); } + } - private static void CheckDispositionTypeFormat(StringSegment dispositionType, string parameterName) + // Gets a parameter of the given name and attempts to extract a date. + // Returns null if the parameter is not present or the format is incorrect. + private DateTimeOffset? GetDate(string parameter) + { + var dateParameter = NameValueHeaderValue.Find(_parameters, parameter); + if (dateParameter != null) { - if (StringSegment.IsNullOrEmpty(dispositionType)) + var dateString = dateParameter.Value; + // Should have quotes, remove them. + if (IsQuoted(dateString)) { - throw new ArgumentException("An empty string is not allowed.", parameterName); + dateString = dateString.Subsegment(1, dateString.Length - 2); } - - // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. - var dispositionTypeLength = GetDispositionTypeExpressionLength(dispositionType, 0, out var tempDispositionType); - if ((dispositionTypeLength == 0) || (tempDispositionType.Length != dispositionType.Length)) + DateTimeOffset date; + if (HttpRuleParser.TryStringToDate(dateString, out date)) { - throw new FormatException(string.Format(CultureInfo.InvariantCulture, - "Invalid disposition type '{0}'.", dispositionType)); + return date; } } + return null; + } - // Gets a parameter of the given name and attempts to extract a date. - // Returns null if the parameter is not present or the format is incorrect. - private DateTimeOffset? GetDate(string parameter) + // Add the given parameter to the list. Remove if date is null. + private void SetDate(string parameter, DateTimeOffset? date) + { + var dateParameter = NameValueHeaderValue.Find(_parameters, parameter); + if (date == null) { - var dateParameter = NameValueHeaderValue.Find(_parameters, parameter); + // Remove parameter if (dateParameter != null) { - var dateString = dateParameter.Value; - // Should have quotes, remove them. - if (IsQuoted(dateString)) - { - dateString = dateString.Subsegment(1, dateString.Length - 2); - } - DateTimeOffset date; - if (HttpRuleParser.TryStringToDate(dateString, out date)) - { - return date; - } + _parameters!.Remove(dateParameter); } - return null; } - - // Add the given parameter to the list. Remove if date is null. - private void SetDate(string parameter, DateTimeOffset? date) + else { - var dateParameter = NameValueHeaderValue.Find(_parameters, parameter); - if (date == null) + // Must always be quoted + var dateString = HeaderUtilities.FormatDate(date.GetValueOrDefault(), quoted: true); + if (dateParameter != null) { - // Remove parameter - if (dateParameter != null) - { - _parameters!.Remove(dateParameter); - } + dateParameter.Value = dateString; } else { - // Must always be quoted - var dateString = HeaderUtilities.FormatDate(date.GetValueOrDefault(), quoted: true); - if (dateParameter != null) - { - dateParameter.Value = dateString; - } - else - { - Parameters.Add(new NameValueHeaderValue(parameter, dateString)); - } + Parameters.Add(new NameValueHeaderValue(parameter, dateString)); } } + } - // Gets a parameter of the given name and attempts to decode it if necessary. - // Returns null if the parameter is not present or the raw value if the encoding is incorrect. - private StringSegment GetName(string parameter) + // Gets a parameter of the given name and attempts to decode it if necessary. + // Returns null if the parameter is not present or the raw value if the encoding is incorrect. + private StringSegment GetName(string parameter) + { + var nameParameter = NameValueHeaderValue.Find(_parameters, parameter); + if (nameParameter != null) { - var nameParameter = NameValueHeaderValue.Find(_parameters, parameter); - if (nameParameter != null) + string? result; + // filename*=utf-8'lang'%7FMyString + if (parameter.EndsWith("*", StringComparison.Ordinal)) { - string? result; - // filename*=utf-8'lang'%7FMyString - if (parameter.EndsWith("*", StringComparison.Ordinal)) - { - if (TryDecode5987(nameParameter.Value, out result)) - { - return result; - } - return null; // Unrecognized encoding - } - - // filename="=?utf-8?B?BDFSDFasdfasdc==?=" - if (TryDecodeMime(nameParameter.Value, out result)) + if (TryDecode5987(nameParameter.Value, out result)) { return result; } - // May not have been encoded - return HeaderUtilities.RemoveQuotes(nameParameter.Value); + return null; // Unrecognized encoding } - return null; - } - // Add/update the given parameter in the list, encoding if necessary. - // Remove if value is null/Empty - private void SetName(StringSegment parameter, StringSegment value) - { - var nameParameter = NameValueHeaderValue.Find(_parameters, parameter); - if (StringSegment.IsNullOrEmpty(value)) - { - // Remove parameter - if (nameParameter != null) - { - _parameters!.Remove(nameParameter); - } - } - else + // filename="=?utf-8?B?BDFSDFasdfasdc==?=" + if (TryDecodeMime(nameParameter.Value, out result)) { - var processedValue = StringSegment.Empty; - if (parameter.EndsWith("*", StringComparison.Ordinal)) - { - processedValue = Encode5987(value); - } - else - { - processedValue = EncodeAndQuoteMime(value); - } - - if (nameParameter != null) - { - nameParameter.Value = processedValue; - } - else - { - Parameters.Add(new NameValueHeaderValue(parameter, processedValue)); - } + return result; } + // May not have been encoded + return HeaderUtilities.RemoveQuotes(nameParameter.Value); } + return null; + } - // Returns input for decoding failures, as the content might not be encoded - private StringSegment EncodeAndQuoteMime(StringSegment input) + // Add/update the given parameter in the list, encoding if necessary. + // Remove if value is null/Empty + private void SetName(StringSegment parameter, StringSegment value) + { + var nameParameter = NameValueHeaderValue.Find(_parameters, parameter); + if (StringSegment.IsNullOrEmpty(value)) { - var result = input; - var needsQuotes = false; - // Remove bounding quotes, they'll get re-added later - if (IsQuoted(result)) + // Remove parameter + if (nameParameter != null) { - result = result.Subsegment(1, result.Length - 2); - needsQuotes = true; + _parameters!.Remove(nameParameter); } - - if (RequiresEncoding(result)) + } + else + { + var processedValue = StringSegment.Empty; + if (parameter.EndsWith("*", StringComparison.Ordinal)) { - // EncodeMimeWithQuotes will Base64 encode any quotes in the input, and surround the payload in quotes - // so there is no need to add quotes - needsQuotes = false; - result = EncodeMimeWithQuotes(result); // "=?utf-8?B?asdfasdfaesdf?=" + processedValue = Encode5987(value); } - else if (!needsQuotes && HttpRuleParser.GetTokenLength(result, 0) != result.Length) + else { - needsQuotes = true; + processedValue = EncodeAndQuoteMime(value); } - if (needsQuotes) + if (nameParameter != null) { - if (result.IndexOfAny(EscapeChars) != -1) - { - // '\' and '"' must be escaped in a quoted string - result = result.ToString().Replace(@"\", @"\\").Replace(@"""", @"\"""); - } - // Re-add quotes "value" - result = string.Format(CultureInfo.InvariantCulture, "\"{0}\"", result); + nameParameter.Value = processedValue; } - return result; - } - - // Replaces characters not suitable for HTTP headers with '_' rather than MIME encoding them. - private StringSegment Sanitize(StringSegment input) - { - var result = input; - - if (RequiresEncoding(result)) + else { - var builder = new StringBuilder(result.Length); - for (int i = 0; i < result.Length; i++) - { - var c = result[i]; - if ((int)c > 0x7f || (int)c < 0x20) - { - c = '_'; // Replace out-of-range characters - } - builder.Append(c); - } - result = builder.ToString(); + Parameters.Add(new NameValueHeaderValue(parameter, processedValue)); } - - return result; } + } - // Returns true if the value starts and ends with a quote - private static bool IsQuoted(StringSegment value) + // Returns input for decoding failures, as the content might not be encoded + private StringSegment EncodeAndQuoteMime(StringSegment input) + { + var result = input; + var needsQuotes = false; + // Remove bounding quotes, they'll get re-added later + if (IsQuoted(result)) { - Contract.Assert(value != null); + result = result.Subsegment(1, result.Length - 2); + needsQuotes = true; + } - return value.Length > 1 && value.StartsWith("\"", StringComparison.Ordinal) - && value.EndsWith("\"", StringComparison.Ordinal); + if (RequiresEncoding(result)) + { + // EncodeMimeWithQuotes will Base64 encode any quotes in the input, and surround the payload in quotes + // so there is no need to add quotes + needsQuotes = false; + result = EncodeMimeWithQuotes(result); // "=?utf-8?B?asdfasdfaesdf?=" + } + else if (!needsQuotes && HttpRuleParser.GetTokenLength(result, 0) != result.Length) + { + needsQuotes = true; } - // tspecials are required to be in a quoted string. Only non-ascii needs to be encoded. - private static bool RequiresEncoding(StringSegment input) + if (needsQuotes) { - Contract.Assert(input != null); + if (result.IndexOfAny(EscapeChars) != -1) + { + // '\' and '"' must be escaped in a quoted string + result = result.ToString().Replace(@"\", @"\\").Replace(@"""", @"\"""); + } + // Re-add quotes "value" + result = string.Format(CultureInfo.InvariantCulture, "\"{0}\"", result); + } + return result; + } + + // Replaces characters not suitable for HTTP headers with '_' rather than MIME encoding them. + private StringSegment Sanitize(StringSegment input) + { + var result = input; - for (int i = 0; i < input.Length; i++) + if (RequiresEncoding(result)) + { + var builder = new StringBuilder(result.Length); + for (int i = 0; i < result.Length; i++) { - if ((int)input[i] > 0x7f || (int)input[i] < 0x20) + var c = result[i]; + if ((int)c > 0x7f || (int)c < 0x20) { - return true; + c = '_'; // Replace out-of-range characters } + builder.Append(c); } - return false; + result = builder.ToString(); } - // Encode using MIME encoding - // And adds surrounding quotes, Encoded data must always be quoted, the equals signs are invalid in tokens - [SkipLocalsInit] - private string EncodeMimeWithQuotes(StringSegment input) - { - var requiredLength = MimePrefix.Length + - Base64.GetMaxEncodedToUtf8Length(Encoding.UTF8.GetByteCount(input.AsSpan())) + - MimeSuffix.Length; - byte[]? bufferFromPool = null; - Span buffer = requiredLength <= MaxStackAllocSizeBytes - ? stackalloc byte[MaxStackAllocSizeBytes] - : bufferFromPool = ArrayPool.Shared.Rent(requiredLength); - buffer = buffer[..requiredLength]; - - MimePrefix.CopyTo(buffer); - var bufferContent = buffer.Slice(MimePrefix.Length); - var contentLength = Encoding.UTF8.GetBytes(input.AsSpan(), bufferContent); + return result; + } - Base64.EncodeToUtf8InPlace(bufferContent, contentLength, out var base64ContentLength); + // Returns true if the value starts and ends with a quote + private static bool IsQuoted(StringSegment value) + { + Contract.Assert(value != null); - MimeSuffix.CopyTo(bufferContent.Slice(base64ContentLength)); + return value.Length > 1 && value.StartsWith("\"", StringComparison.Ordinal) + && value.EndsWith("\"", StringComparison.Ordinal); + } - var result = Encoding.UTF8.GetString(buffer.Slice(0, MimePrefix.Length + base64ContentLength + MimeSuffix.Length)); + // tspecials are required to be in a quoted string. Only non-ascii needs to be encoded. + private static bool RequiresEncoding(StringSegment input) + { + Contract.Assert(input != null); - if (bufferFromPool is not null) + for (int i = 0; i < input.Length; i++) + { + if ((int)input[i] > 0x7f || (int)input[i] < 0x20) { - ArrayPool.Shared.Return(bufferFromPool); + return true; } - - return result; } + return false; + } + + // Encode using MIME encoding + // And adds surrounding quotes, Encoded data must always be quoted, the equals signs are invalid in tokens + [SkipLocalsInit] + private string EncodeMimeWithQuotes(StringSegment input) + { + var requiredLength = MimePrefix.Length + + Base64.GetMaxEncodedToUtf8Length(Encoding.UTF8.GetByteCount(input.AsSpan())) + + MimeSuffix.Length; + byte[]? bufferFromPool = null; + Span buffer = requiredLength <= MaxStackAllocSizeBytes + ? stackalloc byte[MaxStackAllocSizeBytes] + : bufferFromPool = ArrayPool.Shared.Rent(requiredLength); + buffer = buffer[..requiredLength]; + + MimePrefix.CopyTo(buffer); + var bufferContent = buffer.Slice(MimePrefix.Length); + var contentLength = Encoding.UTF8.GetBytes(input.AsSpan(), bufferContent); + + Base64.EncodeToUtf8InPlace(bufferContent, contentLength, out var base64ContentLength); + + MimeSuffix.CopyTo(bufferContent.Slice(base64ContentLength)); - // Attempt to decode MIME encoded strings - private bool TryDecodeMime(StringSegment input, [NotNullWhen(true)] out string? output) + var result = Encoding.UTF8.GetString(buffer.Slice(0, MimePrefix.Length + base64ContentLength + MimeSuffix.Length)); + + if (bufferFromPool is not null) { - Contract.Assert(input != null); + ArrayPool.Shared.Return(bufferFromPool); + } - output = null; - var processedInput = input; - // Require quotes, min of "=?e?b??=" - if (!IsQuoted(processedInput) || processedInput.Length < 10) - { - return false; - } + return result; + } - var parts = processedInput.Split(QuestionMark).ToArray(); - // "=, encodingName, encodingType, encodedData, =" - if (parts.Length != 5 || parts[0] != "\"=" || parts[4] != "=\"" - || !parts[2].Equals("b", StringComparison.OrdinalIgnoreCase)) - { - // Not encoded. - // This does not support multi-line encoding. - // Only base64 encoding is supported, not quoted printable - return false; - } + // Attempt to decode MIME encoded strings + private bool TryDecodeMime(StringSegment input, [NotNullWhen(true)] out string? output) + { + Contract.Assert(input != null); - try - { - var encoding = Encoding.GetEncoding(parts[1].ToString()); - var bytes = Convert.FromBase64String(parts[3].ToString()); - output = encoding.GetString(bytes, 0, bytes.Length); - return true; - } - catch (ArgumentException) - { - // Unknown encoding or bad characters - } - catch (FormatException) - { - // Bad base64 decoding - } + output = null; + var processedInput = input; + // Require quotes, min of "=?e?b??=" + if (!IsQuoted(processedInput) || processedInput.Length < 10) + { return false; } - // Encode a string using RFC 5987 encoding - // encoding'lang'PercentEncodedSpecials - [SkipLocalsInit] - private static string Encode5987(StringSegment input) + var parts = processedInput.Split(QuestionMark).ToArray(); + // "=, encodingName, encodingType, encodedData, =" + if (parts.Length != 5 || parts[0] != "\"=" || parts[4] != "=\"" + || !parts[2].Equals("b", StringComparison.OrdinalIgnoreCase)) { - var builder = new StringBuilder("UTF-8\'\'"); + // Not encoded. + // This does not support multi-line encoding. + // Only base64 encoding is supported, not quoted printable + return false; + } + + try + { + var encoding = Encoding.GetEncoding(parts[1].ToString()); + var bytes = Convert.FromBase64String(parts[3].ToString()); + output = encoding.GetString(bytes, 0, bytes.Length); + return true; + } + catch (ArgumentException) + { + // Unknown encoding or bad characters + } + catch (FormatException) + { + // Bad base64 decoding + } + return false; + } - var maxInputBytes = Encoding.UTF8.GetMaxByteCount(input.Length); - byte[]? bufferFromPool = null; - Span inputBytes = maxInputBytes <= MaxStackAllocSizeBytes - ? stackalloc byte[MaxStackAllocSizeBytes] - : bufferFromPool = ArrayPool.Shared.Rent(maxInputBytes); + // Encode a string using RFC 5987 encoding + // encoding'lang'PercentEncodedSpecials + [SkipLocalsInit] + private static string Encode5987(StringSegment input) + { + var builder = new StringBuilder("UTF-8\'\'"); - var bytesWritten = Encoding.UTF8.GetBytes(input, inputBytes); - inputBytes = inputBytes[..bytesWritten]; + var maxInputBytes = Encoding.UTF8.GetMaxByteCount(input.Length); + byte[]? bufferFromPool = null; + Span inputBytes = maxInputBytes <= MaxStackAllocSizeBytes + ? stackalloc byte[MaxStackAllocSizeBytes] + : bufferFromPool = ArrayPool.Shared.Rent(maxInputBytes); - int totalBytesConsumed = 0; - while (totalBytesConsumed < inputBytes.Length) - { - if (inputBytes[totalBytesConsumed] <= 0x7F) - { - // This is an ASCII char. Let's handle it ourselves. + var bytesWritten = Encoding.UTF8.GetBytes(input, inputBytes); + inputBytes = inputBytes[..bytesWritten]; - char c = (char)inputBytes[totalBytesConsumed]; - if (!HttpRuleParser.IsTokenChar(c) || c == '*' || c == '\'' || c == '%') - { - HexEscape(builder, c); - } - else - { - builder.Append(c); - } + int totalBytesConsumed = 0; + while (totalBytesConsumed < inputBytes.Length) + { + if (inputBytes[totalBytesConsumed] <= 0x7F) + { + // This is an ASCII char. Let's handle it ourselves. - totalBytesConsumed++; + char c = (char)inputBytes[totalBytesConsumed]; + if (!HttpRuleParser.IsTokenChar(c) || c == '*' || c == '\'' || c == '%') + { + HexEscape(builder, c); } else { - // Non-ASCII, let's rely on Rune to decode it. + builder.Append(c); + } - Rune.DecodeFromUtf8(inputBytes.Slice(totalBytesConsumed), out Rune r, out int bytesConsumedForRune); - Contract.Assert(!r.IsAscii, "We shouldn't have gotten here if the Rune is ASCII."); + totalBytesConsumed++; + } + else + { + // Non-ASCII, let's rely on Rune to decode it. - for (int i = 0; i < bytesConsumedForRune; i++) - { - HexEscape(builder, (char)inputBytes[totalBytesConsumed + i]); - } + Rune.DecodeFromUtf8(inputBytes.Slice(totalBytesConsumed), out Rune r, out int bytesConsumedForRune); + Contract.Assert(!r.IsAscii, "We shouldn't have gotten here if the Rune is ASCII."); - totalBytesConsumed += bytesConsumedForRune; + for (int i = 0; i < bytesConsumedForRune; i++) + { + HexEscape(builder, (char)inputBytes[totalBytesConsumed + i]); } - } - if (bufferFromPool is not null) - { - ArrayPool.Shared.Return(bufferFromPool); + totalBytesConsumed += bytesConsumedForRune; } + } - return builder.ToString(); + if (bufferFromPool is not null) + { + ArrayPool.Shared.Return(bufferFromPool); } - private static readonly char[] HexUpperChars = { + return builder.ToString(); + } + + private static readonly char[] HexUpperChars = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; - private static void HexEscape(StringBuilder builder, char c) + private static void HexEscape(StringBuilder builder, char c) + { + builder.Append('%'); + builder.Append(HexUpperChars[(c & 0xf0) >> 4]); + builder.Append(HexUpperChars[c & 0xf]); + } + + // Attempt to decode using RFC 5987 encoding. + // encoding'language'my%20string + private static bool TryDecode5987(StringSegment input, [NotNullWhen(true)] out string? output) + { + output = null; + + var parts = input.Split(SingleQuote).ToArray(); + if (parts.Length != 3) { - builder.Append('%'); - builder.Append(HexUpperChars[(c & 0xf0) >> 4]); - builder.Append(HexUpperChars[c & 0xf]); + return false; } - // Attempt to decode using RFC 5987 encoding. - // encoding'language'my%20string - private static bool TryDecode5987(StringSegment input, [NotNullWhen(true)] out string? output) + var decoded = new StringBuilder(); + byte[]? unescapedBytes = null; + try { - output = null; + var encoding = Encoding.GetEncoding(parts[0].ToString()); - var parts = input.Split(SingleQuote).ToArray(); - if (parts.Length != 3) + var dataString = parts[2]; + unescapedBytes = ArrayPool.Shared.Rent(dataString.Length); + var unescapedBytesCount = 0; + for (var index = 0; index < dataString.Length; index++) { - return false; - } - - var decoded = new StringBuilder(); - byte[]? unescapedBytes = null; - try - { - var encoding = Encoding.GetEncoding(parts[0].ToString()); - - var dataString = parts[2]; - unescapedBytes = ArrayPool.Shared.Rent(dataString.Length); - var unescapedBytesCount = 0; - for (var index = 0; index < dataString.Length; index++) + if (IsHexEncoding(dataString, index)) // %FF { - if (IsHexEncoding(dataString, index)) // %FF - { - // Unescape and cache bytes, multi-byte characters must be decoded all at once - unescapedBytes[unescapedBytesCount++] = HexUnescape(dataString, ref index); - index--; // HexUnescape did +=3; Offset the for loop's ++ - } - else - { - if (unescapedBytesCount > 0) - { - // Decode any previously cached bytes - decoded.Append(encoding.GetString(unescapedBytes, 0, unescapedBytesCount)); - unescapedBytesCount = 0; - } - decoded.Append(dataString[index]); // Normal safe character - } + // Unescape and cache bytes, multi-byte characters must be decoded all at once + unescapedBytes[unescapedBytesCount++] = HexUnescape(dataString, ref index); + index--; // HexUnescape did +=3; Offset the for loop's ++ } - - if (unescapedBytesCount > 0) + else { - // Decode any previously cached bytes - decoded.Append(encoding.GetString(unescapedBytes, 0, unescapedBytesCount)); + if (unescapedBytesCount > 0) + { + // Decode any previously cached bytes + decoded.Append(encoding.GetString(unescapedBytes, 0, unescapedBytesCount)); + unescapedBytesCount = 0; + } + decoded.Append(dataString[index]); // Normal safe character } } - catch (ArgumentException) + + if (unescapedBytesCount > 0) { - return false; // Unknown encoding or bad characters + // Decode any previously cached bytes + decoded.Append(encoding.GetString(unescapedBytes, 0, unescapedBytesCount)); } - finally + } + catch (ArgumentException) + { + return false; // Unknown encoding or bad characters + } + finally + { + if (unescapedBytes != null) { - if (unescapedBytes != null) - { - ArrayPool.Shared.Return(unescapedBytes); - } + ArrayPool.Shared.Return(unescapedBytes); } + } - output = decoded.ToString(); + output = decoded.ToString(); + return true; + } + + private static bool IsHexEncoding(StringSegment pattern, int index) + { + if ((pattern.Length - index) < 3) + { + return false; + } + if ((pattern[index] == '%') && IsEscapedAscii(pattern[index + 1], pattern[index + 2])) + { return true; } + return false; + } - private static bool IsHexEncoding(StringSegment pattern, int index) + private static bool IsEscapedAscii(char digit, char next) + { + if (!(((digit >= '0') && (digit <= '9')) + || ((digit >= 'A') && (digit <= 'F')) + || ((digit >= 'a') && (digit <= 'f')))) { - if ((pattern.Length - index) < 3) - { - return false; - } - if ((pattern[index] == '%') && IsEscapedAscii(pattern[index + 1], pattern[index + 2])) - { - return true; - } return false; } - private static bool IsEscapedAscii(char digit, char next) + if (!(((next >= '0') && (next <= '9')) + || ((next >= 'A') && (next <= 'F')) + || ((next >= 'a') && (next <= 'f')))) { - if (!(((digit >= '0') && (digit <= '9')) - || ((digit >= 'A') && (digit <= 'F')) - || ((digit >= 'a') && (digit <= 'f')))) - { - return false; - } + return false; + } - if (!(((next >= '0') && (next <= '9')) - || ((next >= 'A') && (next <= 'F')) - || ((next >= 'a') && (next <= 'f')))) - { - return false; - } + return true; + } - return true; + private static byte HexUnescape(StringSegment pattern, ref int index) + { + if ((index < 0) || (index >= pattern.Length)) + { + throw new ArgumentOutOfRangeException(nameof(index)); } - - private static byte HexUnescape(StringSegment pattern, ref int index) + if ((pattern[index] == '%') + && (pattern.Length - index >= 3)) { - if ((index < 0) || (index >= pattern.Length)) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - if ((pattern[index] == '%') - && (pattern.Length - index >= 3)) - { - var ret = UnEscapeAscii(pattern[index + 1], pattern[index + 2]); - index += 3; - return ret; - } - return (byte)pattern[index++]; + var ret = UnEscapeAscii(pattern[index + 1], pattern[index + 2]); + index += 3; + return ret; } + return (byte)pattern[index++]; + } - internal static byte UnEscapeAscii(char digit, char next) + internal static byte UnEscapeAscii(char digit, char next) + { + if (!(((digit >= '0') && (digit <= '9')) + || ((digit >= 'A') && (digit <= 'F')) + || ((digit >= 'a') && (digit <= 'f')))) { - if (!(((digit >= '0') && (digit <= '9')) - || ((digit >= 'A') && (digit <= 'F')) - || ((digit >= 'a') && (digit <= 'f')))) - { - throw new ArgumentOutOfRangeException(nameof(digit)); - } - - var res = (digit <= '9') - ? ((int)digit - (int)'0') - : (((digit <= 'F') - ? ((int)digit - (int)'A') - : ((int)digit - (int)'a')) - + 10); + throw new ArgumentOutOfRangeException(nameof(digit)); + } - if (!(((next >= '0') && (next <= '9')) - || ((next >= 'A') && (next <= 'F')) - || ((next >= 'a') && (next <= 'f')))) - { - throw new ArgumentOutOfRangeException(nameof(next)); - } + var res = (digit <= '9') + ? ((int)digit - (int)'0') + : (((digit <= 'F') + ? ((int)digit - (int)'A') + : ((int)digit - (int)'a')) + + 10); - return (byte)((res << 4) + ((next <= '9') - ? ((int)next - (int)'0') - : (((next <= 'F') - ? ((int)next - (int)'A') - : ((int)next - (int)'a')) - + 10))); + if (!(((next >= '0') && (next <= '9')) + || ((next >= 'A') && (next <= 'F')) + || ((next >= 'a') && (next <= 'f')))) + { + throw new ArgumentOutOfRangeException(nameof(next)); } + + return (byte)((res << 4) + ((next <= '9') + ? ((int)next - (int)'0') + : (((next <= 'F') + ? ((int)next - (int)'A') + : ((int)next - (int)'a')) + + 10))); } } diff --git a/src/Http/Headers/src/ContentDispositionHeaderValueIdentityExtensions.cs b/src/Http/Headers/src/ContentDispositionHeaderValueIdentityExtensions.cs index c713b7b005..82afd6b8c7 100644 --- a/src/Http/Headers/src/ContentDispositionHeaderValueIdentityExtensions.cs +++ b/src/Http/Headers/src/ContentDispositionHeaderValueIdentityExtensions.cs @@ -4,43 +4,42 @@ using System; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +/// +/// Various extension methods for for identifying the type of the disposition header +/// +public static class ContentDispositionHeaderValueIdentityExtensions { /// - /// Various extension methods for for identifying the type of the disposition header + /// Checks if the content disposition header is a file disposition /// - public static class ContentDispositionHeaderValueIdentityExtensions + /// The header to check + /// True if the header is file disposition, false otherwise + public static bool IsFileDisposition(this ContentDispositionHeaderValue header) { - /// - /// Checks if the content disposition header is a file disposition - /// - /// The header to check - /// True if the header is file disposition, false otherwise - public static bool IsFileDisposition(this ContentDispositionHeaderValue header) + if (header == null) { - if (header == null) - { - throw new ArgumentNullException(nameof(header)); - } - - return header.DispositionType.Equals("form-data") - && (!StringSegment.IsNullOrEmpty(header.FileName) || !StringSegment.IsNullOrEmpty(header.FileNameStar)); + throw new ArgumentNullException(nameof(header)); } - /// - /// Checks if the content disposition header is a form disposition - /// - /// The header to check - /// True if the header is form disposition, false otherwise - public static bool IsFormDisposition(this ContentDispositionHeaderValue header) - { - if (header == null) - { - throw new ArgumentNullException(nameof(header)); - } + return header.DispositionType.Equals("form-data") + && (!StringSegment.IsNullOrEmpty(header.FileName) || !StringSegment.IsNullOrEmpty(header.FileNameStar)); + } - return header.DispositionType.Equals("form-data") - && StringSegment.IsNullOrEmpty(header.FileName) && StringSegment.IsNullOrEmpty(header.FileNameStar); + /// + /// Checks if the content disposition header is a form disposition + /// + /// The header to check + /// True if the header is form disposition, false otherwise + public static bool IsFormDisposition(this ContentDispositionHeaderValue header) + { + if (header == null) + { + throw new ArgumentNullException(nameof(header)); } + + return header.DispositionType.Equals("form-data") + && StringSegment.IsNullOrEmpty(header.FileName) && StringSegment.IsNullOrEmpty(header.FileNameStar); } } diff --git a/src/Http/Headers/src/ContentRangeHeaderValue.cs b/src/Http/Headers/src/ContentRangeHeaderValue.cs index 4a58c36d60..cada5ec343 100644 --- a/src/Http/Headers/src/ContentRangeHeaderValue.cs +++ b/src/Http/Headers/src/ContentRangeHeaderValue.cs @@ -8,443 +8,442 @@ using System.Globalization; using System.Text; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +/// +/// Represents a Content-Range response HTTP header. +/// +public class ContentRangeHeaderValue { + private static readonly HttpHeaderParser Parser + = new GenericHeaderParser(false, GetContentRangeLength); + + private StringSegment _unit; + + private ContentRangeHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + /// - /// Represents a Content-Range response HTTP header. + /// Initializes a new instance of . /// - public class ContentRangeHeaderValue + /// The start of the range. + /// The end of the range. + /// The total size of the document in bytes. + public ContentRangeHeaderValue(long from, long to, long length) { - private static readonly HttpHeaderParser Parser - = new GenericHeaderParser(false, GetContentRangeLength); - - private StringSegment _unit; + // Scenario: "Content-Range: bytes 12-34/5678" - private ContentRangeHeaderValue() + if (length < 0) { - // Used by the parser to create a new instance of this type. + throw new ArgumentOutOfRangeException(nameof(length)); } - - /// - /// Initializes a new instance of . - /// - /// The start of the range. - /// The end of the range. - /// The total size of the document in bytes. - public ContentRangeHeaderValue(long from, long to, long length) + if ((to < 0) || (to > length)) { - // Scenario: "Content-Range: bytes 12-34/5678" - - if (length < 0) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - if ((to < 0) || (to > length)) - { - throw new ArgumentOutOfRangeException(nameof(to)); - } - if ((from < 0) || (from > to)) - { - throw new ArgumentOutOfRangeException(nameof(from)); - } - - From = from; - To = to; - Length = length; - _unit = HeaderUtilities.BytesUnit; + throw new ArgumentOutOfRangeException(nameof(to)); } - - /// - /// Initializes a new instance of . - /// - /// The total size of the document in bytes. - public ContentRangeHeaderValue(long length) + if ((from < 0) || (from > to)) { - // Scenario: "Content-Range: bytes */1234" - - if (length < 0) - { - throw new ArgumentOutOfRangeException(nameof(length)); - } - - Length = length; - _unit = HeaderUtilities.BytesUnit; + throw new ArgumentOutOfRangeException(nameof(from)); } - /// - /// Initializes a new instance of . - /// - /// The start of the range. - /// The end of the range. - public ContentRangeHeaderValue(long from, long to) - { - // Scenario: "Content-Range: bytes 12-34/*" - - if (to < 0) - { - throw new ArgumentOutOfRangeException(nameof(to)); - } - if ((from < 0) || (from > to)) - { - throw new ArgumentOutOfRangeException(nameof(@from)); - } + From = from; + To = to; + Length = length; + _unit = HeaderUtilities.BytesUnit; + } - From = from; - To = to; - _unit = HeaderUtilities.BytesUnit; - } + /// + /// Initializes a new instance of . + /// + /// The total size of the document in bytes. + public ContentRangeHeaderValue(long length) + { + // Scenario: "Content-Range: bytes */1234" - /// - /// Gets or sets the unit in which ranges are specified. - /// - /// Defaults to bytes. - public StringSegment Unit + if (length < 0) { - get { return _unit; } - set - { - HeaderUtilities.CheckValidToken(value, nameof(value)); - _unit = value; - } + throw new ArgumentOutOfRangeException(nameof(length)); } - /// - /// Gets the start of the range. - /// - public long? From { get; private set; } - - /// - /// Gets the end of the range. - /// - public long? To { get; private set; } + Length = length; + _unit = HeaderUtilities.BytesUnit; + } - /// - /// Gets the total size of the document. - /// - [NotNullIfNotNull(nameof(Length))] - public long? Length { get; private set; } + /// + /// Initializes a new instance of . + /// + /// The start of the range. + /// The end of the range. + public ContentRangeHeaderValue(long from, long to) + { + // Scenario: "Content-Range: bytes 12-34/*" - /// - /// Gets a value that determines if has been specified. - /// - [MemberNotNullWhen(true, nameof(Length))] - public bool HasLength // e.g. "Content-Range: bytes 12-34/*" + if (to < 0) { - get { return Length != null; } + throw new ArgumentOutOfRangeException(nameof(to)); } - - /// - /// Gets a value that determines if and have been specified. - /// - [MemberNotNullWhen(true, nameof(From), nameof(To))] - public bool HasRange // e.g. "Content-Range: bytes */1234" + if ((from < 0) || (from > to)) { - get { return From != null && To != null; } + throw new ArgumentOutOfRangeException(nameof(@from)); } - /// - public override bool Equals(object? obj) + From = from; + To = to; + _unit = HeaderUtilities.BytesUnit; + } + + /// + /// Gets or sets the unit in which ranges are specified. + /// + /// Defaults to bytes. + public StringSegment Unit + { + get { return _unit; } + set { - var other = obj as ContentRangeHeaderValue; + HeaderUtilities.CheckValidToken(value, nameof(value)); + _unit = value; + } + } - if (other == null) - { - return false; - } + /// + /// Gets the start of the range. + /// + public long? From { get; private set; } - return ((From == other.From) && (To == other.To) && (Length == other.Length) && - StringSegment.Equals(Unit, other.Unit, StringComparison.OrdinalIgnoreCase)); - } + /// + /// Gets the end of the range. + /// + public long? To { get; private set; } - /// - public override int GetHashCode() - { - var result = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(Unit); + /// + /// Gets the total size of the document. + /// + [NotNullIfNotNull(nameof(Length))] + public long? Length { get; private set; } - if (HasRange) - { - result = result ^ From.GetHashCode() ^ To.GetHashCode(); - } + /// + /// Gets a value that determines if has been specified. + /// + [MemberNotNullWhen(true, nameof(Length))] + public bool HasLength // e.g. "Content-Range: bytes 12-34/*" + { + get { return Length != null; } + } - if (HasLength) - { - result = result ^ Length.GetHashCode(); - } + /// + /// Gets a value that determines if and have been specified. + /// + [MemberNotNullWhen(true, nameof(From), nameof(To))] + public bool HasRange // e.g. "Content-Range: bytes */1234" + { + get { return From != null && To != null; } + } - return result; - } + /// + public override bool Equals(object? obj) + { + var other = obj as ContentRangeHeaderValue; - /// - public override string ToString() + if (other == null) { - var sb = new StringBuilder(); - sb.Append(Unit.AsSpan()); - sb.Append(' '); + return false; + } - if (HasRange) - { - sb.Append(From.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo)); - sb.Append('-'); - sb.Append(To.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo)); - } - else - { - sb.Append('*'); - } + return ((From == other.From) && (To == other.To) && (Length == other.Length) && + StringSegment.Equals(Unit, other.Unit, StringComparison.OrdinalIgnoreCase)); + } - sb.Append('/'); - if (HasLength) - { - sb.Append(Length.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo)); - } - else - { - sb.Append('*'); - } + /// + public override int GetHashCode() + { + var result = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(Unit); - return sb.ToString(); + if (HasRange) + { + result = result ^ From.GetHashCode() ^ To.GetHashCode(); } - /// - /// Parses as a value. - /// - /// The values to parse. - /// The parsed values. - public static ContentRangeHeaderValue Parse(StringSegment input) + if (HasLength) { - var index = 0; - return Parser.ParseValue(input, ref index)!; + result = result ^ Length.GetHashCode(); } - /// - /// Attempts to parse the specified as a . - /// - /// The value to parse. - /// The parsed value. - /// if input is a valid , otherwise . - public static bool TryParse(StringSegment input, [NotNullWhen(true)] out ContentRangeHeaderValue parsedValue) + return result; + } + + /// + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(Unit.AsSpan()); + sb.Append(' '); + + if (HasRange) + { + sb.Append(From.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo)); + sb.Append('-'); + sb.Append(To.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo)); + } + else { - var index = 0; - return Parser.TryParseValue(input, ref index, out parsedValue!); + sb.Append('*'); } - private static int GetContentRangeLength(StringSegment input, int startIndex, out ContentRangeHeaderValue? parsedValue) + sb.Append('/'); + if (HasLength) + { + sb.Append(Length.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo)); + } + else { - Contract.Requires(startIndex >= 0); + sb.Append('*'); + } - parsedValue = null; + return sb.ToString(); + } - if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) - { - return 0; - } + /// + /// Parses as a value. + /// + /// The values to parse. + /// The parsed values. + public static ContentRangeHeaderValue Parse(StringSegment input) + { + var index = 0; + return Parser.ParseValue(input, ref index)!; + } - // Parse the unit string: in ' -/' - var unitLength = HttpRuleParser.GetTokenLength(input, startIndex); + /// + /// Attempts to parse the specified as a . + /// + /// The value to parse. + /// The parsed value. + /// if input is a valid , otherwise . + public static bool TryParse(StringSegment input, [NotNullWhen(true)] out ContentRangeHeaderValue parsedValue) + { + var index = 0; + return Parser.TryParseValue(input, ref index, out parsedValue!); + } - if (unitLength == 0) - { - return 0; - } + private static int GetContentRangeLength(StringSegment input, int startIndex, out ContentRangeHeaderValue? parsedValue) + { + Contract.Requires(startIndex >= 0); - var unit = input.Subsegment(startIndex, unitLength); - var current = startIndex + unitLength; - var separatorLength = HttpRuleParser.GetWhitespaceLength(input, current); + parsedValue = null; - if (separatorLength == 0) - { - return 0; - } + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) + { + return 0; + } - current = current + separatorLength; + // Parse the unit string: in ' -/' + var unitLength = HttpRuleParser.GetTokenLength(input, startIndex); - if (current == input.Length) - { - return 0; - } + if (unitLength == 0) + { + return 0; + } - // Read range values and in ' -/' - var fromStartIndex = current; - var fromLength = 0; - var toStartIndex = 0; - var toLength = 0; - if (!TryGetRangeLength(input, ref current, out fromLength, out toStartIndex, out toLength)) - { - return 0; - } + var unit = input.Subsegment(startIndex, unitLength); + var current = startIndex + unitLength; + var separatorLength = HttpRuleParser.GetWhitespaceLength(input, current); - // After the range is read we expect the length separator '/' - if ((current == input.Length) || (input[current] != '/')) - { - return 0; - } + if (separatorLength == 0) + { + return 0; + } - current++; // Skip '/' separator - current = current + HttpRuleParser.GetWhitespaceLength(input, current); + current = current + separatorLength; - if (current == input.Length) - { - return 0; - } + if (current == input.Length) + { + return 0; + } - // We may not have a length (e.g. 'bytes 1-2/*'). But if we do, parse the length now. - var lengthStartIndex = current; - var lengthLength = 0; - if (!TryGetLengthLength(input, ref current, out lengthLength)) - { - return 0; - } + // Read range values and in ' -/' + var fromStartIndex = current; + var fromLength = 0; + var toStartIndex = 0; + var toLength = 0; + if (!TryGetRangeLength(input, ref current, out fromLength, out toStartIndex, out toLength)) + { + return 0; + } - if (!TryCreateContentRange(input, unit, fromStartIndex, fromLength, toStartIndex, toLength, - lengthStartIndex, lengthLength, out parsedValue)) - { - return 0; - } + // After the range is read we expect the length separator '/' + if ((current == input.Length) || (input[current] != '/')) + { + return 0; + } + + current++; // Skip '/' separator + current = current + HttpRuleParser.GetWhitespaceLength(input, current); - return current - startIndex; + if (current == input.Length) + { + return 0; } - private static bool TryGetLengthLength(StringSegment input, ref int current, out int lengthLength) + // We may not have a length (e.g. 'bytes 1-2/*'). But if we do, parse the length now. + var lengthStartIndex = current; + var lengthLength = 0; + if (!TryGetLengthLength(input, ref current, out lengthLength)) { - lengthLength = 0; + return 0; + } - if (input[current] == '*') - { - current++; - } - else - { - // Parse length value: in ' -/' - lengthLength = HttpRuleParser.GetNumberLength(input, current, false); + if (!TryCreateContentRange(input, unit, fromStartIndex, fromLength, toStartIndex, toLength, + lengthStartIndex, lengthLength, out parsedValue)) + { + return 0; + } - if ((lengthLength == 0) || (lengthLength > HttpRuleParser.MaxInt64Digits)) - { - return false; - } + return current - startIndex; + } - current = current + lengthLength; - } + private static bool TryGetLengthLength(StringSegment input, ref int current, out int lengthLength) + { + lengthLength = 0; - current = current + HttpRuleParser.GetWhitespaceLength(input, current); - return true; + if (input[current] == '*') + { + current++; } - - private static bool TryGetRangeLength(StringSegment input, ref int current, out int fromLength, out int toStartIndex, out int toLength) + else { - fromLength = 0; - toStartIndex = 0; - toLength = 0; + // Parse length value: in ' -/' + lengthLength = HttpRuleParser.GetNumberLength(input, current, false); - // Check if we have a value like 'bytes */133'. If yes, skip the range part and continue parsing the - // length separator '/'. - if (input[current] == '*') + if ((lengthLength == 0) || (lengthLength > HttpRuleParser.MaxInt64Digits)) { - current++; - } - else - { - // Parse first range value: in ' -/' - fromLength = HttpRuleParser.GetNumberLength(input, current, false); - - if ((fromLength == 0) || (fromLength > HttpRuleParser.MaxInt64Digits)) - { - return false; - } - - current = current + fromLength; - current = current + HttpRuleParser.GetWhitespaceLength(input, current); - - // After the first value, the '-' character must follow. - if ((current == input.Length) || (input[current] != '-')) - { - // We need a '-' character otherwise this can't be a valid range. - return false; - } - - current++; // skip the '-' character - current = current + HttpRuleParser.GetWhitespaceLength(input, current); - - if (current == input.Length) - { - return false; - } - - // Parse second range value: in ' -/' - toStartIndex = current; - toLength = HttpRuleParser.GetNumberLength(input, current, false); - - if ((toLength == 0) || (toLength > HttpRuleParser.MaxInt64Digits)) - { - return false; - } - - current = current + toLength; + return false; } - current = current + HttpRuleParser.GetWhitespaceLength(input, current); - return true; + current = current + lengthLength; } - private static bool TryCreateContentRange( - StringSegment input, - StringSegment unit, - int fromStartIndex, - int fromLength, - int toStartIndex, - int toLength, - int lengthStartIndex, - int lengthLength, - [NotNullWhen(true)]out ContentRangeHeaderValue? parsedValue) + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + return true; + } + + private static bool TryGetRangeLength(StringSegment input, ref int current, out int fromLength, out int toStartIndex, out int toLength) + { + fromLength = 0; + toStartIndex = 0; + toLength = 0; + + // Check if we have a value like 'bytes */133'. If yes, skip the range part and continue parsing the + // length separator '/'. + if (input[current] == '*') + { + current++; + } + else { - parsedValue = null; + // Parse first range value: in ' -/' + fromLength = HttpRuleParser.GetNumberLength(input, current, false); - long from = 0; - if ((fromLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(fromStartIndex, fromLength), out from)) + if ((fromLength == 0) || (fromLength > HttpRuleParser.MaxInt64Digits)) { return false; } - long to = 0; - if ((toLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(toStartIndex, toLength), out to)) - { - return false; - } + current = current + fromLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); - // 'from' must not be greater than 'to' - if ((fromLength > 0) && (toLength > 0) && (from > to)) + // After the first value, the '-' character must follow. + if ((current == input.Length) || (input[current] != '-')) { + // We need a '-' character otherwise this can't be a valid range. return false; } - long length = 0; - if ((lengthLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(lengthStartIndex, lengthLength), - out length)) + current++; // skip the '-' character + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + if (current == input.Length) { return false; } - // 'from' and 'to' must be less than 'length' - if ((toLength > 0) && (lengthLength > 0) && (to >= length)) + // Parse second range value: in ' -/' + toStartIndex = current; + toLength = HttpRuleParser.GetNumberLength(input, current, false); + + if ((toLength == 0) || (toLength > HttpRuleParser.MaxInt64Digits)) { return false; } - var result = new ContentRangeHeaderValue(); - result._unit = unit; + current = current + toLength; + } - if (fromLength > 0) - { - result.From = from; - result.To = to; - } + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + return true; + } - if (lengthLength > 0) - { - result.Length = length; - } + private static bool TryCreateContentRange( + StringSegment input, + StringSegment unit, + int fromStartIndex, + int fromLength, + int toStartIndex, + int toLength, + int lengthStartIndex, + int lengthLength, + [NotNullWhen(true)] out ContentRangeHeaderValue? parsedValue) + { + parsedValue = null; + + long from = 0; + if ((fromLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(fromStartIndex, fromLength), out from)) + { + return false; + } + + long to = 0; + if ((toLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(toStartIndex, toLength), out to)) + { + return false; + } + + // 'from' must not be greater than 'to' + if ((fromLength > 0) && (toLength > 0) && (from > to)) + { + return false; + } - parsedValue = result; - return true; + long length = 0; + if ((lengthLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(lengthStartIndex, lengthLength), + out length)) + { + return false; + } + + // 'from' and 'to' must be less than 'length' + if ((toLength > 0) && (lengthLength > 0) && (to >= length)) + { + return false; + } + + var result = new ContentRangeHeaderValue(); + result._unit = unit; + + if (fromLength > 0) + { + result.From = from; + result.To = to; + } + + if (lengthLength > 0) + { + result.Length = length; } + + parsedValue = result; + return true; } } diff --git a/src/Http/Headers/src/CookieHeaderParser.cs b/src/Http/Headers/src/CookieHeaderParser.cs index 00f473b31e..ca65e83d58 100644 --- a/src/Http/Headers/src/CookieHeaderParser.cs +++ b/src/Http/Headers/src/CookieHeaderParser.cs @@ -3,33 +3,32 @@ using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +internal sealed class CookieHeaderParser : HttpHeaderParser { - internal sealed class CookieHeaderParser : HttpHeaderParser + internal CookieHeaderParser(bool supportsMultipleValues) + : base(supportsMultipleValues) + { + } + + public override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue? cookieValue) { - internal CookieHeaderParser(bool supportsMultipleValues) - : base(supportsMultipleValues) + cookieValue = null; + + if (!CookieHeaderParserShared.TryParseValue(value, ref index, SupportsMultipleValues, out var parsedName, out var parsedValue)) { + return false; } - public override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue? cookieValue) + if (parsedName == null || parsedValue == null) { - cookieValue = null; - - if (!CookieHeaderParserShared.TryParseValue(value, ref index, SupportsMultipleValues, out var parsedName, out var parsedValue)) - { - return false; - } - - if (parsedName == null || parsedValue == null) - { - // Successfully parsed, but no values. - return true; - } - - cookieValue = new CookieHeaderValue(parsedName.Value, parsedValue.Value); - + // Successfully parsed, but no values. return true; } + + cookieValue = new CookieHeaderValue(parsedName.Value, parsedValue.Value); + + return true; } } diff --git a/src/Http/Headers/src/CookieHeaderValue.cs b/src/Http/Headers/src/CookieHeaderValue.cs index d89321a0f8..0c207b8274 100644 --- a/src/Http/Headers/src/CookieHeaderValue.cs +++ b/src/Http/Headers/src/CookieHeaderValue.cs @@ -8,209 +8,208 @@ using System.Diagnostics.Contracts; using System.Text; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +// http://tools.ietf.org/html/rfc6265 +/// +/// Represents the HTTP request Cookie header. +/// +public class CookieHeaderValue { - // http://tools.ietf.org/html/rfc6265 + private static readonly CookieHeaderParser SingleValueParser = new CookieHeaderParser(supportsMultipleValues: false); + private static readonly CookieHeaderParser MultipleValueParser = new CookieHeaderParser(supportsMultipleValues: true); + + private StringSegment _name; + private StringSegment _value; + + private CookieHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + /// - /// Represents the HTTP request Cookie header. + /// Initializes a new instance of . /// - public class CookieHeaderValue + /// The cookie name. + public CookieHeaderValue(StringSegment name) + : this(name, StringSegment.Empty) { - private static readonly CookieHeaderParser SingleValueParser = new CookieHeaderParser(supportsMultipleValues: false); - private static readonly CookieHeaderParser MultipleValueParser = new CookieHeaderParser(supportsMultipleValues: true); - - private StringSegment _name; - private StringSegment _value; - - private CookieHeaderValue() + if (name == null) { - // Used by the parser to create a new instance of this type. + throw new ArgumentNullException(nameof(name)); } + } - /// - /// Initializes a new instance of . - /// - /// The cookie name. - public CookieHeaderValue(StringSegment name) - : this(name, StringSegment.Empty) + /// + /// Initializes a new instance of . + /// + /// The cookie name. + /// The cookie value. + public CookieHeaderValue(StringSegment name, StringSegment value) + { + if (name == null) { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } + throw new ArgumentNullException(nameof(name)); } - /// - /// Initializes a new instance of . - /// - /// The cookie name. - /// The cookie value. - public CookieHeaderValue(StringSegment name, StringSegment value) + if (value == null) { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - Name = name; - Value = value; + throw new ArgumentNullException(nameof(value)); } - /// - /// Gets or sets the cookie name. - /// - public StringSegment Name + Name = name; + Value = value; + } + + /// + /// Gets or sets the cookie name. + /// + public StringSegment Name + { + get { return _name; } + set { - get { return _name; } - set - { - CheckNameFormat(value, nameof(value)); - _name = value; - } + CheckNameFormat(value, nameof(value)); + _name = value; } + } - /// - /// Gets or sets the cookie value. - /// - public StringSegment Value + /// + /// Gets or sets the cookie value. + /// + public StringSegment Value + { + get { return _value; } + set { - get { return _value; } - set - { - CheckValueFormat(value, nameof(value)); - _value = value; - } + CheckValueFormat(value, nameof(value)); + _value = value; } + } - /// - // name="val ue"; - public override string ToString() - { - var header = new StringBuilder(); + /// + // name="val ue"; + public override string ToString() + { + var header = new StringBuilder(); - header.Append(_name.AsSpan()); - header.Append('='); - header.Append(_value.AsSpan()); + header.Append(_name.AsSpan()); + header.Append('='); + header.Append(_value.AsSpan()); - return header.ToString(); - } + return header.ToString(); + } - /// - /// Parses as a value. - /// - /// The values to parse. - /// The parsed values. - public static CookieHeaderValue Parse(StringSegment input) - { - var index = 0; - return SingleValueParser.ParseValue(input, ref index)!; - } + /// + /// Parses as a value. + /// + /// The values to parse. + /// The parsed values. + public static CookieHeaderValue Parse(StringSegment input) + { + var index = 0; + return SingleValueParser.ParseValue(input, ref index)!; + } - /// - /// Attempts to parse the specified as a . - /// - /// The value to parse. - /// The parsed value. - /// if input is a valid , otherwise . - public static bool TryParse(StringSegment input, [NotNullWhen(true)] out CookieHeaderValue? parsedValue) - { - var index = 0; - return SingleValueParser.TryParseValue(input, ref index, out parsedValue!); - } + /// + /// Attempts to parse the specified as a . + /// + /// The value to parse. + /// The parsed value. + /// if input is a valid , otherwise . + public static bool TryParse(StringSegment input, [NotNullWhen(true)] out CookieHeaderValue? parsedValue) + { + var index = 0; + return SingleValueParser.TryParseValue(input, ref index, out parsedValue!); + } - /// - /// Parses a sequence of inputs as a sequence of values. - /// - /// The values to parse. - /// The parsed values. - public static IList ParseList(IList? inputs) - { - return MultipleValueParser.ParseValues(inputs); - } + /// + /// Parses a sequence of inputs as a sequence of values. + /// + /// The values to parse. + /// The parsed values. + public static IList ParseList(IList? inputs) + { + return MultipleValueParser.ParseValues(inputs); + } - /// - /// Parses a sequence of inputs as a sequence of values using string parsing rules. - /// - /// The values to parse. - /// The parsed values. - public static IList ParseStrictList(IList? inputs) - { - return MultipleValueParser.ParseStrictValues(inputs); - } + /// + /// Parses a sequence of inputs as a sequence of values using string parsing rules. + /// + /// The values to parse. + /// The parsed values. + public static IList ParseStrictList(IList? inputs) + { + return MultipleValueParser.ParseStrictValues(inputs); + } - /// - /// Attempts to parse the sequence of values as a sequence of . - /// - /// The values to parse. - /// The parsed values. - /// if all inputs are valid , otherwise . - public static bool TryParseList(IList? inputs, [NotNullWhen(true)] out IList? parsedValues) - { - return MultipleValueParser.TryParseValues(inputs, out parsedValues); - } + /// + /// Attempts to parse the sequence of values as a sequence of . + /// + /// The values to parse. + /// The parsed values. + /// if all inputs are valid , otherwise . + public static bool TryParseList(IList? inputs, [NotNullWhen(true)] out IList? parsedValues) + { + return MultipleValueParser.TryParseValues(inputs, out parsedValues); + } - /// - /// Attempts to parse the sequence of values as a sequence of using string parsing rules. - /// - /// The values to parse. - /// The parsed values. - /// if all inputs are valid , otherwise . - public static bool TryParseStrictList(IList? inputs, [NotNullWhen(true)] out IList? parsedValues) + /// + /// Attempts to parse the sequence of values as a sequence of using string parsing rules. + /// + /// The values to parse. + /// The parsed values. + /// if all inputs are valid , otherwise . + public static bool TryParseStrictList(IList? inputs, [NotNullWhen(true)] out IList? parsedValues) + { + return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); + } + + internal static void CheckNameFormat(StringSegment name, string parameterName) + { + if (name == null) { - return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); + throw new ArgumentNullException(nameof(name)); } - internal static void CheckNameFormat(StringSegment name, string parameterName) + if (HttpRuleParser.GetTokenLength(name, 0) != name.Length) { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - if (HttpRuleParser.GetTokenLength(name, 0) != name.Length) - { - throw new ArgumentException("Invalid cookie name: " + name, parameterName); - } + throw new ArgumentException("Invalid cookie name: " + name, parameterName); } + } - internal static void CheckValueFormat(StringSegment value, string parameterName) + internal static void CheckValueFormat(StringSegment value, string parameterName) + { + if (value == null) { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - var offset = 0; - var result = CookieHeaderParserShared.GetCookieValue(value, ref offset); - if (result.Length != value.Length) - { - throw new ArgumentException("Invalid cookie value: " + value, parameterName); - } + throw new ArgumentNullException(nameof(value)); } - /// - public override bool Equals(object? obj) + var offset = 0; + var result = CookieHeaderParserShared.GetCookieValue(value, ref offset); + if (result.Length != value.Length) { - var other = obj as CookieHeaderValue; - - if (other == null) - { - return false; - } - - return StringSegment.Equals(_name, other._name, StringComparison.OrdinalIgnoreCase) - && StringSegment.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase); + throw new ArgumentException("Invalid cookie value: " + value, parameterName); } + } + + /// + public override bool Equals(object? obj) + { + var other = obj as CookieHeaderValue; - /// - public override int GetHashCode() + if (other == null) { - return _name.GetHashCode() ^ _value.GetHashCode(); + return false; } + + return StringSegment.Equals(_name, other._name, StringComparison.OrdinalIgnoreCase) + && StringSegment.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase); + } + + /// + public override int GetHashCode() + { + return _name.GetHashCode() ^ _value.GetHashCode(); } } diff --git a/src/Http/Headers/src/EntityTagHeaderValue.cs b/src/Http/Headers/src/EntityTagHeaderValue.cs index 51555c6a40..2523dc756e 100644 --- a/src/Http/Headers/src/EntityTagHeaderValue.cs +++ b/src/Http/Headers/src/EntityTagHeaderValue.cs @@ -7,273 +7,272 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +/// +/// Represents an entity-tag (etag) header value. +/// +public class EntityTagHeaderValue { + // Note that the ETag header does not allow a * but we're not that strict: We allow both '*' and ETag values in a single value. + // We can't guarantee that a single parsed value will be used directly in an ETag header. + private static readonly HttpHeaderParser SingleValueParser + = new GenericHeaderParser(false, GetEntityTagLength); + // Note that if multiple ETag values are allowed (e.g. 'If-Match', 'If-None-Match'), according to the RFC + // the value must either be '*' or a list of ETag values. It's not allowed to have both '*' and a list of + // ETag values. We're not that strict: We allow both '*' and ETag values in a list. If the server sends such + // an invalid list, we want to be able to represent it using the corresponding header property. + private static readonly HttpHeaderParser MultipleValueParser + = new GenericHeaderParser(true, GetEntityTagLength); + + private StringSegment _tag; + private bool _isWeak; + + private EntityTagHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + /// - /// Represents an entity-tag (etag) header value. + /// Initializes a new instance of the . /// - public class EntityTagHeaderValue + /// A that contains an . + public EntityTagHeaderValue(StringSegment tag) + : this(tag, isWeak: false) { - // Note that the ETag header does not allow a * but we're not that strict: We allow both '*' and ETag values in a single value. - // We can't guarantee that a single parsed value will be used directly in an ETag header. - private static readonly HttpHeaderParser SingleValueParser - = new GenericHeaderParser(false, GetEntityTagLength); - // Note that if multiple ETag values are allowed (e.g. 'If-Match', 'If-None-Match'), according to the RFC - // the value must either be '*' or a list of ETag values. It's not allowed to have both '*' and a list of - // ETag values. We're not that strict: We allow both '*' and ETag values in a list. If the server sends such - // an invalid list, we want to be able to represent it using the corresponding header property. - private static readonly HttpHeaderParser MultipleValueParser - = new GenericHeaderParser(true, GetEntityTagLength); - - private StringSegment _tag; - private bool _isWeak; - - private EntityTagHeaderValue() - { - // Used by the parser to create a new instance of this type. - } + } - /// - /// Initializes a new instance of the . - /// - /// A that contains an . - public EntityTagHeaderValue(StringSegment tag) - : this(tag, isWeak: false) + /// + /// Initializes a new instance of the . + /// + /// A that contains an . + /// A value that indicates if this entity-tag header is a weak validator. + public EntityTagHeaderValue(StringSegment tag, bool isWeak) + { + if (StringSegment.IsNullOrEmpty(tag)) { + throw new ArgumentException("An empty string is not allowed.", nameof(tag)); } - /// - /// Initializes a new instance of the . - /// - /// A that contains an . - /// A value that indicates if this entity-tag header is a weak validator. - public EntityTagHeaderValue(StringSegment tag, bool isWeak) + if (!isWeak && StringSegment.Equals(tag, "*", StringComparison.Ordinal)) { - if (StringSegment.IsNullOrEmpty(tag)) - { - throw new ArgumentException("An empty string is not allowed.", nameof(tag)); - } - - if (!isWeak && StringSegment.Equals(tag, "*", StringComparison.Ordinal)) - { - // * is valid, but W/* isn't. - _tag = tag; - } - else if ((HttpRuleParser.GetQuotedStringLength(tag, 0, out var length) != HttpParseResult.Parsed) || - (length != tag.Length)) - { - // Note that we don't allow 'W/' prefixes for weak ETags in the 'tag' parameter. If the user wants to - // add a weak ETag, they can set 'isWeak' to true. - throw new FormatException("Invalid ETag name"); - } - + // * is valid, but W/* isn't. _tag = tag; - _isWeak = isWeak; + } + else if ((HttpRuleParser.GetQuotedStringLength(tag, 0, out var length) != HttpParseResult.Parsed) || + (length != tag.Length)) + { + // Note that we don't allow 'W/' prefixes for weak ETags in the 'tag' parameter. If the user wants to + // add a weak ETag, they can set 'isWeak' to true. + throw new FormatException("Invalid ETag name"); } - /// - /// Gets the "any" etag. - /// - public static EntityTagHeaderValue Any { get; } = new EntityTagHeaderValue("*", isWeak: false); - - /// - /// Gets the quoted tag. - /// - public StringSegment Tag => _tag; + _tag = tag; + _isWeak = isWeak; + } - /// - /// Gets a value that determines if the entity-tag header is a weak validator. - /// - public bool IsWeak => _isWeak; + /// + /// Gets the "any" etag. + /// + public static EntityTagHeaderValue Any { get; } = new EntityTagHeaderValue("*", isWeak: false); - /// - public override string ToString() - { - if (_isWeak) - { - return "W/" + _tag.ToString(); - } - return _tag.ToString(); - } + /// + /// Gets the quoted tag. + /// + public StringSegment Tag => _tag; - /// - /// Check against another for equality. - /// This equality check should not be used to determine if two values match under the RFC specifications (https://tools.ietf.org/html/rfc7232#section-2.3.2). - /// - /// The other value to check against for equality. - /// - /// true if the strength and tag of the two values match, - /// false if the other value is null, is not an , or if there is a mismatch of strength or tag between the two values. - /// - public override bool Equals(object? obj) - { - // Since the tag is a quoted-string we treat it case-sensitive. - return obj is EntityTagHeaderValue other && _isWeak == other._isWeak && StringSegment.Equals(_tag, other._tag, StringComparison.Ordinal); - } + /// + /// Gets a value that determines if the entity-tag header is a weak validator. + /// + public bool IsWeak => _isWeak; - /// - public override int GetHashCode() + /// + public override string ToString() + { + if (_isWeak) { - // Since the tag is a quoted-string we treat it case-sensitive. - return _tag.GetHashCode() ^ _isWeak.GetHashCode(); + return "W/" + _tag.ToString(); } + return _tag.ToString(); + } - /// - /// Compares against another to see if they match under the RFC specifications (https://tools.ietf.org/html/rfc7232#section-2.3.2). - /// - /// The other to compare against. - /// true to use a strong comparison, false to use a weak comparison - /// - /// true if the match for the given comparison type, - /// false if the other value is null or the comparison failed. - /// - public bool Compare(EntityTagHeaderValue? other, bool useStrongComparison) - { - if (other == null) - { - return false; - } + /// + /// Check against another for equality. + /// This equality check should not be used to determine if two values match under the RFC specifications (https://tools.ietf.org/html/rfc7232#section-2.3.2). + /// + /// The other value to check against for equality. + /// + /// true if the strength and tag of the two values match, + /// false if the other value is null, is not an , or if there is a mismatch of strength or tag between the two values. + /// + public override bool Equals(object? obj) + { + // Since the tag is a quoted-string we treat it case-sensitive. + return obj is EntityTagHeaderValue other && _isWeak == other._isWeak && StringSegment.Equals(_tag, other._tag, StringComparison.Ordinal); + } - if (useStrongComparison) - { - return !IsWeak && !other.IsWeak && StringSegment.Equals(Tag, other.Tag, StringComparison.Ordinal); - } - else - { - return StringSegment.Equals(Tag, other.Tag, StringComparison.Ordinal); - } - } + /// + public override int GetHashCode() + { + // Since the tag is a quoted-string we treat it case-sensitive. + return _tag.GetHashCode() ^ _isWeak.GetHashCode(); + } - /// - /// Parses as a value. - /// - /// The values to parse. - /// The parsed values. - public static EntityTagHeaderValue Parse(StringSegment input) + /// + /// Compares against another to see if they match under the RFC specifications (https://tools.ietf.org/html/rfc7232#section-2.3.2). + /// + /// The other to compare against. + /// true to use a strong comparison, false to use a weak comparison + /// + /// true if the match for the given comparison type, + /// false if the other value is null or the comparison failed. + /// + public bool Compare(EntityTagHeaderValue? other, bool useStrongComparison) + { + if (other == null) { - var index = 0; - return SingleValueParser.ParseValue(input, ref index)!; + return false; } - /// - /// Attempts to parse the specified as a . - /// - /// The value to parse. - /// The parsed value. - /// if input is a valid , otherwise . - public static bool TryParse(StringSegment input, [NotNullWhen(true)] out EntityTagHeaderValue parsedValue) + if (useStrongComparison) { - var index = 0; - return SingleValueParser.TryParseValue(input, ref index, out parsedValue!); + return !IsWeak && !other.IsWeak && StringSegment.Equals(Tag, other.Tag, StringComparison.Ordinal); } - - /// - /// Parses a sequence of inputs as a sequence of values. - /// - /// The values to parse. - /// The parsed values. - public static IList ParseList(IList? inputs) + else { - return MultipleValueParser.ParseValues(inputs); + return StringSegment.Equals(Tag, other.Tag, StringComparison.Ordinal); } + } - /// - /// Parses a sequence of inputs as a sequence of values using string parsing rules. - /// - /// The values to parse. - /// The parsed values. - public static IList ParseStrictList(IList? inputs) - { - return MultipleValueParser.ParseStrictValues(inputs); - } + /// + /// Parses as a value. + /// + /// The values to parse. + /// The parsed values. + public static EntityTagHeaderValue Parse(StringSegment input) + { + var index = 0; + return SingleValueParser.ParseValue(input, ref index)!; + } + + /// + /// Attempts to parse the specified as a . + /// + /// The value to parse. + /// The parsed value. + /// if input is a valid , otherwise . + public static bool TryParse(StringSegment input, [NotNullWhen(true)] out EntityTagHeaderValue parsedValue) + { + var index = 0; + return SingleValueParser.TryParseValue(input, ref index, out parsedValue!); + } + + /// + /// Parses a sequence of inputs as a sequence of values. + /// + /// The values to parse. + /// The parsed values. + public static IList ParseList(IList? inputs) + { + return MultipleValueParser.ParseValues(inputs); + } + + /// + /// Parses a sequence of inputs as a sequence of values using string parsing rules. + /// + /// The values to parse. + /// The parsed values. + public static IList ParseStrictList(IList? inputs) + { + return MultipleValueParser.ParseStrictValues(inputs); + } + + /// + /// Attempts to parse the sequence of values as a sequence of . + /// + /// The values to parse. + /// The parsed values. + /// if all inputs are valid , otherwise . + public static bool TryParseList(IList? inputs, [NotNullWhen(true)] out IList? parsedValues) + { + return MultipleValueParser.TryParseValues(inputs, out parsedValues); + } + + /// + /// Attempts to parse the sequence of values as a sequence of using string parsing rules. + /// + /// The values to parse. + /// The parsed values. + /// if all inputs are valid , otherwise . + public static bool TryParseStrictList(IList? inputs, [NotNullWhen(true)] out IList? parsedValues) + { + return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); + } + + internal static int GetEntityTagLength(StringSegment input, int startIndex, out EntityTagHeaderValue? parsedValue) + { + Contract.Requires(startIndex >= 0); - /// - /// Attempts to parse the sequence of values as a sequence of . - /// - /// The values to parse. - /// The parsed values. - /// if all inputs are valid , otherwise . - public static bool TryParseList(IList? inputs, [NotNullWhen(true)] out IList? parsedValues) + parsedValue = null; + + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) { - return MultipleValueParser.TryParseValues(inputs, out parsedValues); + return 0; } - /// - /// Attempts to parse the sequence of values as a sequence of using string parsing rules. - /// - /// The values to parse. - /// The parsed values. - /// if all inputs are valid , otherwise . - public static bool TryParseStrictList(IList? inputs, [NotNullWhen(true)] out IList? parsedValues) + // Caller must remove leading whitespaces. If not, we'll return 0. + var isWeak = false; + var current = startIndex; + + var firstChar = input[startIndex]; + if (firstChar == '*') { - return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); + // We have '*' value, indicating "any" ETag. + parsedValue = Any; + current++; } - - internal static int GetEntityTagLength(StringSegment input, int startIndex, out EntityTagHeaderValue? parsedValue) + else { - Contract.Requires(startIndex >= 0); - - parsedValue = null; + // The RFC defines 'W/' as prefix, but we'll be flexible and also accept lower-case 'w'. + if ((firstChar == 'W') || (firstChar == 'w')) + { + current++; + // We need at least 3 more chars: the '/' character followed by two quotes. + if ((current + 2 >= input.Length) || (input[current] != '/')) + { + return 0; + } + isWeak = true; + current++; // we have a weak-entity tag. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + } - if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) + var tagStartIndex = current; + var tagLength = 0; + if (HttpRuleParser.GetQuotedStringLength(input, current, out tagLength) != HttpParseResult.Parsed) { return 0; } - // Caller must remove leading whitespaces. If not, we'll return 0. - var isWeak = false; - var current = startIndex; - - var firstChar = input[startIndex]; - if (firstChar == '*') + parsedValue = new EntityTagHeaderValue(); + if (tagLength == input.Length) { - // We have '*' value, indicating "any" ETag. - parsedValue = Any; - current++; + // Most of the time we'll have strong ETags without leading/trailing whitespaces. + Contract.Assert(startIndex == 0); + Contract.Assert(!isWeak); + parsedValue._tag = input; + parsedValue._isWeak = false; } else { - // The RFC defines 'W/' as prefix, but we'll be flexible and also accept lower-case 'w'. - if ((firstChar == 'W') || (firstChar == 'w')) - { - current++; - // We need at least 3 more chars: the '/' character followed by two quotes. - if ((current + 2 >= input.Length) || (input[current] != '/')) - { - return 0; - } - isWeak = true; - current++; // we have a weak-entity tag. - current = current + HttpRuleParser.GetWhitespaceLength(input, current); - } - - var tagStartIndex = current; - var tagLength = 0; - if (HttpRuleParser.GetQuotedStringLength(input, current, out tagLength) != HttpParseResult.Parsed) - { - return 0; - } - - parsedValue = new EntityTagHeaderValue(); - if (tagLength == input.Length) - { - // Most of the time we'll have strong ETags without leading/trailing whitespaces. - Contract.Assert(startIndex == 0); - Contract.Assert(!isWeak); - parsedValue._tag = input; - parsedValue._isWeak = false; - } - else - { - parsedValue._tag = input.Subsegment(tagStartIndex, tagLength); - parsedValue._isWeak = isWeak; - } - - current = current + tagLength; + parsedValue._tag = input.Subsegment(tagStartIndex, tagLength); + parsedValue._isWeak = isWeak; } - current = current + HttpRuleParser.GetWhitespaceLength(input, current); - return current - startIndex; + current = current + tagLength; } + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + return current - startIndex; } } diff --git a/src/Http/Headers/src/GenericHeaderParser.cs b/src/Http/Headers/src/GenericHeaderParser.cs index ac3cdd44c5..3eb3b9cb56 100644 --- a/src/Http/Headers/src/GenericHeaderParser.cs +++ b/src/Http/Headers/src/GenericHeaderParser.cs @@ -4,28 +4,27 @@ using System; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +internal sealed class GenericHeaderParser : BaseHeaderParser { - internal sealed class GenericHeaderParser : BaseHeaderParser - { - internal delegate int GetParsedValueLengthDelegate(StringSegment value, int startIndex, out T? parsedValue); + internal delegate int GetParsedValueLengthDelegate(StringSegment value, int startIndex, out T? parsedValue); - private readonly GetParsedValueLengthDelegate _getParsedValueLength; + private readonly GetParsedValueLengthDelegate _getParsedValueLength; - internal GenericHeaderParser(bool supportsMultipleValues, GetParsedValueLengthDelegate getParsedValueLength) - : base(supportsMultipleValues) + internal GenericHeaderParser(bool supportsMultipleValues, GetParsedValueLengthDelegate getParsedValueLength) + : base(supportsMultipleValues) + { + if (getParsedValueLength == null) { - if (getParsedValueLength == null) - { - throw new ArgumentNullException(nameof(getParsedValueLength)); - } - - _getParsedValueLength = getParsedValueLength; + throw new ArgumentNullException(nameof(getParsedValueLength)); } - protected override int GetParsedValueLength(StringSegment value, int startIndex, out T? parsedValue) - { - return _getParsedValueLength(value, startIndex, out parsedValue); - } + _getParsedValueLength = getParsedValueLength; + } + + protected override int GetParsedValueLength(StringSegment value, int startIndex, out T? parsedValue) + { + return _getParsedValueLength(value, startIndex, out parsedValue); } } diff --git a/src/Http/Headers/src/HeaderNames.cs b/src/Http/Headers/src/HeaderNames.cs index 7d92ae0004..1d7611d519 100644 --- a/src/Http/Headers/src/HeaderNames.cs +++ b/src/Http/Headers/src/HeaderNames.cs @@ -1,303 +1,302 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +/// +/// Defines constants for well-known HTTP headers. +/// +// MODIFICATION POLICY: This list is not intended to be exhaustive, it primarily contains values used by the framework itself. +// Please do not open PRs without first opening an issue to discuss a specific item. +public static class HeaderNames { - /// - /// Defines constants for well-known HTTP headers. - /// - // MODIFICATION POLICY: This list is not intended to be exhaustive, it primarily contains values used by the framework itself. - // Please do not open PRs without first opening an issue to discuss a specific item. - public static class HeaderNames - { - // Use readonly statics rather than constants so ReferenceEquals works + // Use readonly statics rather than constants so ReferenceEquals works - /// Gets the Accept HTTP header name. - public static readonly string Accept = "Accept"; + /// Gets the Accept HTTP header name. + public static readonly string Accept = "Accept"; - /// Gets the Accept-Charset HTTP header name. - public static readonly string AcceptCharset = "Accept-Charset"; + /// Gets the Accept-Charset HTTP header name. + public static readonly string AcceptCharset = "Accept-Charset"; - /// Gets the Accept-Encoding HTTP header name. - public static readonly string AcceptEncoding = "Accept-Encoding"; + /// Gets the Accept-Encoding HTTP header name. + public static readonly string AcceptEncoding = "Accept-Encoding"; - /// Gets the Accept-Language HTTP header name. - public static readonly string AcceptLanguage = "Accept-Language"; + /// Gets the Accept-Language HTTP header name. + public static readonly string AcceptLanguage = "Accept-Language"; - /// Gets the Accept-Ranges HTTP header name. - public static readonly string AcceptRanges = "Accept-Ranges"; + /// Gets the Accept-Ranges HTTP header name. + public static readonly string AcceptRanges = "Accept-Ranges"; - /// Gets the Access-Control-Allow-Credentials HTTP header name. - public static readonly string AccessControlAllowCredentials = "Access-Control-Allow-Credentials"; + /// Gets the Access-Control-Allow-Credentials HTTP header name. + public static readonly string AccessControlAllowCredentials = "Access-Control-Allow-Credentials"; - /// Gets the Access-Control-Allow-Headers HTTP header name. - public static readonly string AccessControlAllowHeaders = "Access-Control-Allow-Headers"; + /// Gets the Access-Control-Allow-Headers HTTP header name. + public static readonly string AccessControlAllowHeaders = "Access-Control-Allow-Headers"; - /// Gets the Access-Control-Allow-Methods HTTP header name. - public static readonly string AccessControlAllowMethods = "Access-Control-Allow-Methods"; + /// Gets the Access-Control-Allow-Methods HTTP header name. + public static readonly string AccessControlAllowMethods = "Access-Control-Allow-Methods"; - /// Gets the Access-Control-Allow-Origin HTTP header name. - public static readonly string AccessControlAllowOrigin = "Access-Control-Allow-Origin"; + /// Gets the Access-Control-Allow-Origin HTTP header name. + public static readonly string AccessControlAllowOrigin = "Access-Control-Allow-Origin"; - /// Gets the Access-Control-Expose-Headers HTTP header name. - public static readonly string AccessControlExposeHeaders = "Access-Control-Expose-Headers"; + /// Gets the Access-Control-Expose-Headers HTTP header name. + public static readonly string AccessControlExposeHeaders = "Access-Control-Expose-Headers"; - /// Gets the Access-Control-Max-Age HTTP header name. - public static readonly string AccessControlMaxAge = "Access-Control-Max-Age"; + /// Gets the Access-Control-Max-Age HTTP header name. + public static readonly string AccessControlMaxAge = "Access-Control-Max-Age"; - /// Gets the Access-Control-Request-Headers HTTP header name. - public static readonly string AccessControlRequestHeaders = "Access-Control-Request-Headers"; + /// Gets the Access-Control-Request-Headers HTTP header name. + public static readonly string AccessControlRequestHeaders = "Access-Control-Request-Headers"; - /// Gets the Access-Control-Request-Method HTTP header name. - public static readonly string AccessControlRequestMethod = "Access-Control-Request-Method"; + /// Gets the Access-Control-Request-Method HTTP header name. + public static readonly string AccessControlRequestMethod = "Access-Control-Request-Method"; - /// Gets the Age HTTP header name. - public static readonly string Age = "Age"; + /// Gets the Age HTTP header name. + public static readonly string Age = "Age"; - /// Gets the Allow HTTP header name. - public static readonly string Allow = "Allow"; + /// Gets the Allow HTTP header name. + public static readonly string Allow = "Allow"; - /// Gets the Alt-Svc HTTP header name. - public static readonly string AltSvc = "Alt-Svc"; + /// Gets the Alt-Svc HTTP header name. + public static readonly string AltSvc = "Alt-Svc"; - /// Gets the :authority HTTP header name. - public static readonly string Authority = ":authority"; + /// Gets the :authority HTTP header name. + public static readonly string Authority = ":authority"; - /// Gets the Authorization HTTP header name. - public static readonly string Authorization = "Authorization"; + /// Gets the Authorization HTTP header name. + public static readonly string Authorization = "Authorization"; - /// Gets the baggage HTTP header name. - public static readonly string Baggage = "baggage"; + /// Gets the baggage HTTP header name. + public static readonly string Baggage = "baggage"; - /// Gets the Cache-Control HTTP header name. - public static readonly string CacheControl = "Cache-Control"; + /// Gets the Cache-Control HTTP header name. + public static readonly string CacheControl = "Cache-Control"; - /// Gets the Connection HTTP header name. - public static readonly string Connection = "Connection"; + /// Gets the Connection HTTP header name. + public static readonly string Connection = "Connection"; - /// Gets the Content-Disposition HTTP header name. - public static readonly string ContentDisposition = "Content-Disposition"; + /// Gets the Content-Disposition HTTP header name. + public static readonly string ContentDisposition = "Content-Disposition"; - /// Gets the Content-Encoding HTTP header name. - public static readonly string ContentEncoding = "Content-Encoding"; + /// Gets the Content-Encoding HTTP header name. + public static readonly string ContentEncoding = "Content-Encoding"; - /// Gets the Content-Language HTTP header name. - public static readonly string ContentLanguage = "Content-Language"; + /// Gets the Content-Language HTTP header name. + public static readonly string ContentLanguage = "Content-Language"; - /// Gets the Content-Length HTTP header name. - public static readonly string ContentLength = "Content-Length"; + /// Gets the Content-Length HTTP header name. + public static readonly string ContentLength = "Content-Length"; - /// Gets the Content-Location HTTP header name. - public static readonly string ContentLocation = "Content-Location"; + /// Gets the Content-Location HTTP header name. + public static readonly string ContentLocation = "Content-Location"; - /// Gets the Content-MD5 HTTP header name. - public static readonly string ContentMD5 = "Content-MD5"; + /// Gets the Content-MD5 HTTP header name. + public static readonly string ContentMD5 = "Content-MD5"; - /// Gets the Content-Range HTTP header name. - public static readonly string ContentRange = "Content-Range"; + /// Gets the Content-Range HTTP header name. + public static readonly string ContentRange = "Content-Range"; - /// Gets the Content-Security-Policy HTTP header name. - public static readonly string ContentSecurityPolicy = "Content-Security-Policy"; + /// Gets the Content-Security-Policy HTTP header name. + public static readonly string ContentSecurityPolicy = "Content-Security-Policy"; - /// Gets the Content-Security-Policy-Report-Only HTTP header name. - public static readonly string ContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only"; + /// Gets the Content-Security-Policy-Report-Only HTTP header name. + public static readonly string ContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only"; - /// Gets the Content-Type HTTP header name. - public static readonly string ContentType = "Content-Type"; + /// Gets the Content-Type HTTP header name. + public static readonly string ContentType = "Content-Type"; - /// Gets the Correlation-Context HTTP header name. - public static readonly string CorrelationContext = "Correlation-Context"; + /// Gets the Correlation-Context HTTP header name. + public static readonly string CorrelationContext = "Correlation-Context"; - /// Gets the Cookie HTTP header name. - public static readonly string Cookie = "Cookie"; + /// Gets the Cookie HTTP header name. + public static readonly string Cookie = "Cookie"; - /// Gets the Date HTTP header name. - public static readonly string Date = "Date"; + /// Gets the Date HTTP header name. + public static readonly string Date = "Date"; - /// Gets the DNT HTTP header name. - public static readonly string DNT = "DNT"; + /// Gets the DNT HTTP header name. + public static readonly string DNT = "DNT"; - /// Gets the ETag HTTP header name. - public static readonly string ETag = "ETag"; + /// Gets the ETag HTTP header name. + public static readonly string ETag = "ETag"; - /// Gets the Expires HTTP header name. - public static readonly string Expires = "Expires"; + /// Gets the Expires HTTP header name. + public static readonly string Expires = "Expires"; - /// Gets the Expect HTTP header name. - public static readonly string Expect = "Expect"; + /// Gets the Expect HTTP header name. + public static readonly string Expect = "Expect"; - /// Gets the From HTTP header name. - public static readonly string From = "From"; + /// Gets the From HTTP header name. + public static readonly string From = "From"; - /// Gets the Grpc-Accept-Encoding HTTP header name. - public static readonly string GrpcAcceptEncoding = "Grpc-Accept-Encoding"; + /// Gets the Grpc-Accept-Encoding HTTP header name. + public static readonly string GrpcAcceptEncoding = "Grpc-Accept-Encoding"; - /// Gets the Grpc-Encoding HTTP header name. - public static readonly string GrpcEncoding = "Grpc-Encoding"; + /// Gets the Grpc-Encoding HTTP header name. + public static readonly string GrpcEncoding = "Grpc-Encoding"; - /// Gets the Grpc-Message HTTP header name. - public static readonly string GrpcMessage = "Grpc-Message"; + /// Gets the Grpc-Message HTTP header name. + public static readonly string GrpcMessage = "Grpc-Message"; - /// Gets the Grpc-Status HTTP header name. - public static readonly string GrpcStatus = "Grpc-Status"; + /// Gets the Grpc-Status HTTP header name. + public static readonly string GrpcStatus = "Grpc-Status"; - /// Gets the Grpc-Timeout HTTP header name. - public static readonly string GrpcTimeout = "Grpc-Timeout"; + /// Gets the Grpc-Timeout HTTP header name. + public static readonly string GrpcTimeout = "Grpc-Timeout"; - /// Gets the Host HTTP header name. - public static readonly string Host = "Host"; + /// Gets the Host HTTP header name. + public static readonly string Host = "Host"; - /// Gets the Keep-Alive HTTP header name. - public static readonly string KeepAlive = "Keep-Alive"; + /// Gets the Keep-Alive HTTP header name. + public static readonly string KeepAlive = "Keep-Alive"; - /// Gets the If-Match HTTP header name. - public static readonly string IfMatch = "If-Match"; + /// Gets the If-Match HTTP header name. + public static readonly string IfMatch = "If-Match"; - /// Gets the If-Modified-Since HTTP header name. - public static readonly string IfModifiedSince = "If-Modified-Since"; + /// Gets the If-Modified-Since HTTP header name. + public static readonly string IfModifiedSince = "If-Modified-Since"; - /// Gets the If-None-Match HTTP header name. - public static readonly string IfNoneMatch = "If-None-Match"; + /// Gets the If-None-Match HTTP header name. + public static readonly string IfNoneMatch = "If-None-Match"; - /// Gets the If-Range HTTP header name. - public static readonly string IfRange = "If-Range"; + /// Gets the If-Range HTTP header name. + public static readonly string IfRange = "If-Range"; - /// Gets the If-Unmodified-Since HTTP header name. - public static readonly string IfUnmodifiedSince = "If-Unmodified-Since"; + /// Gets the If-Unmodified-Since HTTP header name. + public static readonly string IfUnmodifiedSince = "If-Unmodified-Since"; - /// Gets the Last-Modified HTTP header name. - public static readonly string LastModified = "Last-Modified"; + /// Gets the Last-Modified HTTP header name. + public static readonly string LastModified = "Last-Modified"; - /// Gets the Link HTTP header name. - public static readonly string Link = "Link"; + /// Gets the Link HTTP header name. + public static readonly string Link = "Link"; - /// Gets the Location HTTP header name. - public static readonly string Location = "Location"; + /// Gets the Location HTTP header name. + public static readonly string Location = "Location"; - /// Gets the Max-Forwards HTTP header name. - public static readonly string MaxForwards = "Max-Forwards"; + /// Gets the Max-Forwards HTTP header name. + public static readonly string MaxForwards = "Max-Forwards"; - /// Gets the :method HTTP header name. - public static readonly string Method = ":method"; + /// Gets the :method HTTP header name. + public static readonly string Method = ":method"; - /// Gets the Origin HTTP header name. - public static readonly string Origin = "Origin"; + /// Gets the Origin HTTP header name. + public static readonly string Origin = "Origin"; - /// Gets the :path HTTP header name. - public static readonly string Path = ":path"; + /// Gets the :path HTTP header name. + public static readonly string Path = ":path"; - /// Gets the Pragma HTTP header name. - public static readonly string Pragma = "Pragma"; + /// Gets the Pragma HTTP header name. + public static readonly string Pragma = "Pragma"; - /// Gets the Proxy-Authenticate HTTP header name. - public static readonly string ProxyAuthenticate = "Proxy-Authenticate"; + /// Gets the Proxy-Authenticate HTTP header name. + public static readonly string ProxyAuthenticate = "Proxy-Authenticate"; - /// Gets the Proxy-Authorization HTTP header name. - public static readonly string ProxyAuthorization = "Proxy-Authorization"; + /// Gets the Proxy-Authorization HTTP header name. + public static readonly string ProxyAuthorization = "Proxy-Authorization"; - /// Gets the Proxy-Connection HTTP header name. - public static readonly string ProxyConnection = "Proxy-Connection"; + /// Gets the Proxy-Connection HTTP header name. + public static readonly string ProxyConnection = "Proxy-Connection"; - /// Gets the Range HTTP header name. - public static readonly string Range = "Range"; + /// Gets the Range HTTP header name. + public static readonly string Range = "Range"; - /// Gets the Referer HTTP header name. - public static readonly string Referer = "Referer"; + /// Gets the Referer HTTP header name. + public static readonly string Referer = "Referer"; - /// Gets the Retry-After HTTP header name. - public static readonly string RetryAfter = "Retry-After"; + /// Gets the Retry-After HTTP header name. + public static readonly string RetryAfter = "Retry-After"; - /// Gets the Request-Id HTTP header name. - public static readonly string RequestId = "Request-Id"; + /// Gets the Request-Id HTTP header name. + public static readonly string RequestId = "Request-Id"; - /// Gets the :scheme HTTP header name. - public static readonly string Scheme = ":scheme"; + /// Gets the :scheme HTTP header name. + public static readonly string Scheme = ":scheme"; - /// Gets the Sec-WebSocket-Accept HTTP header name. - public static readonly string SecWebSocketAccept = "Sec-WebSocket-Accept"; + /// Gets the Sec-WebSocket-Accept HTTP header name. + public static readonly string SecWebSocketAccept = "Sec-WebSocket-Accept"; - /// Gets the Sec-WebSocket-Key HTTP header name. - public static readonly string SecWebSocketKey = "Sec-WebSocket-Key"; + /// Gets the Sec-WebSocket-Key HTTP header name. + public static readonly string SecWebSocketKey = "Sec-WebSocket-Key"; - /// Gets the Sec-WebSocket-Protocol HTTP header name. - public static readonly string SecWebSocketProtocol = "Sec-WebSocket-Protocol"; + /// Gets the Sec-WebSocket-Protocol HTTP header name. + public static readonly string SecWebSocketProtocol = "Sec-WebSocket-Protocol"; - /// Gets the Sec-WebSocket-Version HTTP header name. - public static readonly string SecWebSocketVersion = "Sec-WebSocket-Version"; + /// Gets the Sec-WebSocket-Version HTTP header name. + public static readonly string SecWebSocketVersion = "Sec-WebSocket-Version"; - /// Gets the Sec-WebSocket-Extensions HTTP header name. - public static readonly string SecWebSocketExtensions = "Sec-WebSocket-Extensions"; + /// Gets the Sec-WebSocket-Extensions HTTP header name. + public static readonly string SecWebSocketExtensions = "Sec-WebSocket-Extensions"; - /// Gets the Server HTTP header name. - public static readonly string Server = "Server"; + /// Gets the Server HTTP header name. + public static readonly string Server = "Server"; - /// Gets the Set-Cookie HTTP header name. - public static readonly string SetCookie = "Set-Cookie"; + /// Gets the Set-Cookie HTTP header name. + public static readonly string SetCookie = "Set-Cookie"; - /// Gets the :status HTTP header name. - public static readonly string Status = ":status"; + /// Gets the :status HTTP header name. + public static readonly string Status = ":status"; - /// Gets the Strict-Transport-Security HTTP header name. - public static readonly string StrictTransportSecurity = "Strict-Transport-Security"; + /// Gets the Strict-Transport-Security HTTP header name. + public static readonly string StrictTransportSecurity = "Strict-Transport-Security"; - /// Gets the TE HTTP header name. - public static readonly string TE = "TE"; + /// Gets the TE HTTP header name. + public static readonly string TE = "TE"; - /// Gets the Trailer HTTP header name. - public static readonly string Trailer = "Trailer"; + /// Gets the Trailer HTTP header name. + public static readonly string Trailer = "Trailer"; - /// Gets the Transfer-Encoding HTTP header name. - public static readonly string TransferEncoding = "Transfer-Encoding"; + /// Gets the Transfer-Encoding HTTP header name. + public static readonly string TransferEncoding = "Transfer-Encoding"; - /// Gets the Translate HTTP header name. - public static readonly string Translate = "Translate"; + /// Gets the Translate HTTP header name. + public static readonly string Translate = "Translate"; - /// Gets the traceparent HTTP header name. - public static readonly string TraceParent = "traceparent"; + /// Gets the traceparent HTTP header name. + public static readonly string TraceParent = "traceparent"; - /// Gets the tracestate HTTP header name. - public static readonly string TraceState = "tracestate"; + /// Gets the tracestate HTTP header name. + public static readonly string TraceState = "tracestate"; - /// Gets the Upgrade HTTP header name. - public static readonly string Upgrade = "Upgrade"; + /// Gets the Upgrade HTTP header name. + public static readonly string Upgrade = "Upgrade"; - /// Gets the Upgrade-Insecure-Requests HTTP header name. - public static readonly string UpgradeInsecureRequests = "Upgrade-Insecure-Requests"; + /// Gets the Upgrade-Insecure-Requests HTTP header name. + public static readonly string UpgradeInsecureRequests = "Upgrade-Insecure-Requests"; - /// Gets the User-Agent HTTP header name. - public static readonly string UserAgent = "User-Agent"; + /// Gets the User-Agent HTTP header name. + public static readonly string UserAgent = "User-Agent"; - /// Gets the Vary HTTP header name. - public static readonly string Vary = "Vary"; + /// Gets the Vary HTTP header name. + public static readonly string Vary = "Vary"; - /// Gets the Via HTTP header name. - public static readonly string Via = "Via"; + /// Gets the Via HTTP header name. + public static readonly string Via = "Via"; - /// Gets the Warning HTTP header name. - public static readonly string Warning = "Warning"; + /// Gets the Warning HTTP header name. + public static readonly string Warning = "Warning"; - /// Gets the Sec-WebSocket-Protocol HTTP header name. - public static readonly string WebSocketSubProtocols = "Sec-WebSocket-Protocol"; + /// Gets the Sec-WebSocket-Protocol HTTP header name. + public static readonly string WebSocketSubProtocols = "Sec-WebSocket-Protocol"; - /// Gets the WWW-Authenticate HTTP header name. - public static readonly string WWWAuthenticate = "WWW-Authenticate"; + /// Gets the WWW-Authenticate HTTP header name. + public static readonly string WWWAuthenticate = "WWW-Authenticate"; - /// Gets the X-Content-Type-Options HTTP header name. - public static readonly string XContentTypeOptions = "X-Content-Type-Options"; + /// Gets the X-Content-Type-Options HTTP header name. + public static readonly string XContentTypeOptions = "X-Content-Type-Options"; - /// Gets the X-Frame-Options HTTP header name. - public static readonly string XFrameOptions = "X-Frame-Options"; + /// Gets the X-Frame-Options HTTP header name. + public static readonly string XFrameOptions = "X-Frame-Options"; - /// Gets the X-Powered-By HTTP header name. - public static readonly string XPoweredBy = "X-Powered-By"; + /// Gets the X-Powered-By HTTP header name. + public static readonly string XPoweredBy = "X-Powered-By"; - /// Gets the X-Requested-With HTTP header name. - public static readonly string XRequestedWith = "X-Requested-With"; + /// Gets the X-Requested-With HTTP header name. + public static readonly string XRequestedWith = "X-Requested-With"; - /// Gets the X-UA-Compatible HTTP header name. - public static readonly string XUACompatible = "X-UA-Compatible"; + /// Gets the X-UA-Compatible HTTP header name. + public static readonly string XUACompatible = "X-UA-Compatible"; - /// Gets the X-XSS-Protection HTTP header name. - public static readonly string XXSSProtection = "X-XSS-Protection"; - } + /// Gets the X-XSS-Protection HTTP header name. + public static readonly string XXSSProtection = "X-XSS-Protection"; } diff --git a/src/Http/Headers/src/HeaderQuality.cs b/src/Http/Headers/src/HeaderQuality.cs index 497733e9ae..60e55193ba 100644 --- a/src/Http/Headers/src/HeaderQuality.cs +++ b/src/Http/Headers/src/HeaderQuality.cs @@ -1,21 +1,20 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +/// +/// Provides HTTP header quality factors. +/// +public static class HeaderQuality { /// - /// Provides HTTP header quality factors. + /// Quality factor to indicate a perfect match. /// - public static class HeaderQuality - { - /// - /// Quality factor to indicate a perfect match. - /// - public const double Match = 1.0; + public const double Match = 1.0; - /// - /// Quality factor to indicate no match. - /// - public const double NoMatch = 0.0; - } + /// + /// Quality factor to indicate no match. + /// + public const double NoMatch = 0.0; } diff --git a/src/Http/Headers/src/HeaderUtilities.cs b/src/Http/Headers/src/HeaderUtilities.cs index bb4d101b12..b8a8c83ff3 100644 --- a/src/Http/Headers/src/HeaderUtilities.cs +++ b/src/Http/Headers/src/HeaderUtilities.cs @@ -9,611 +9,611 @@ using System.Diagnostics.Contracts; using System.Globalization; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +/// +/// Provides utilities to parse and modify HTTP header values. +/// +public static class HeaderUtilities { - /// - /// Provides utilities to parse and modify HTTP header values. - /// - public static class HeaderUtilities - { - private const int _qualityValueMaxCharCount = 10; // Little bit more permissive than RFC7231 5.3.1 - private const string QualityName = "q"; - internal const string BytesUnit = "bytes"; + private const int _qualityValueMaxCharCount = 10; // Little bit more permissive than RFC7231 5.3.1 + private const string QualityName = "q"; + internal const string BytesUnit = "bytes"; - internal static void SetQuality(IList parameters, double? value) + internal static void SetQuality(IList parameters, double? value) + { + var qualityParameter = NameValueHeaderValue.Find(parameters, QualityName); + if (value.HasValue) { - var qualityParameter = NameValueHeaderValue.Find(parameters, QualityName); - if (value.HasValue) + // Note that even if we check the value here, we can't prevent a user from adding an invalid quality + // value using Parameters.Add(). Even if we would prevent the user from adding an invalid value + // using Parameters.Add() they could always add invalid values using HttpHeaders.AddWithoutValidation(). + // So this check is really for convenience to show users that they're trying to add an invalid + // value. + if ((value < 0) || (value > 1)) { - // Note that even if we check the value here, we can't prevent a user from adding an invalid quality - // value using Parameters.Add(). Even if we would prevent the user from adding an invalid value - // using Parameters.Add() they could always add invalid values using HttpHeaders.AddWithoutValidation(). - // So this check is really for convenience to show users that they're trying to add an invalid - // value. - if ((value < 0) || (value > 1)) - { - throw new ArgumentOutOfRangeException(nameof(value)); - } + throw new ArgumentOutOfRangeException(nameof(value)); + } - var qualityString = ((double)value).ToString("0.0##", NumberFormatInfo.InvariantInfo); - if (qualityParameter != null) - { - qualityParameter.Value = qualityString; - } - else - { - parameters.Add(new NameValueHeaderValue(QualityName, qualityString)); - } + var qualityString = ((double)value).ToString("0.0##", NumberFormatInfo.InvariantInfo); + if (qualityParameter != null) + { + qualityParameter.Value = qualityString; } else { - // Remove quality parameter - if (qualityParameter != null) - { - parameters.Remove(qualityParameter); - } + parameters.Add(new NameValueHeaderValue(QualityName, qualityString)); } } - - internal static double? GetQuality(IList? parameters) + else { - var qualityParameter = NameValueHeaderValue.Find(parameters, QualityName); + // Remove quality parameter if (qualityParameter != null) { - // Note that the RFC requires decimal '.' regardless of the culture. I.e. using ',' as decimal - // separator is considered invalid (even if the current culture would allow it). - if (TryParseQualityDouble(qualityParameter.Value, 0, out var qualityValue, out _)) - { - return qualityValue; - } + parameters.Remove(qualityParameter); } - return null; } + } - internal static void CheckValidToken(StringSegment value, string parameterName) + internal static double? GetQuality(IList? parameters) + { + var qualityParameter = NameValueHeaderValue.Find(parameters, QualityName); + if (qualityParameter != null) { - if (StringSegment.IsNullOrEmpty(value)) + // Note that the RFC requires decimal '.' regardless of the culture. I.e. using ',' as decimal + // separator is considered invalid (even if the current culture would allow it). + if (TryParseQualityDouble(qualityParameter.Value, 0, out var qualityValue, out _)) { - throw new ArgumentException("An empty string is not allowed.", parameterName); + return qualityValue; } + } + return null; + } - if (HttpRuleParser.GetTokenLength(value, 0) != value.Length) - { - throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid token '{0}'.", value)); - } + internal static void CheckValidToken(StringSegment value, string parameterName) + { + if (StringSegment.IsNullOrEmpty(value)) + { + throw new ArgumentException("An empty string is not allowed.", parameterName); } - internal static bool AreEqualCollections(ICollection? x, ICollection? y) + if (HttpRuleParser.GetTokenLength(value, 0) != value.Length) { - return AreEqualCollections(x, y, null); + throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid token '{0}'.", value)); } + } + + internal static bool AreEqualCollections(ICollection? x, ICollection? y) + { + return AreEqualCollections(x, y, null); + } - internal static bool AreEqualCollections(ICollection? x, ICollection? y, IEqualityComparer? comparer) + internal static bool AreEqualCollections(ICollection? x, ICollection? y, IEqualityComparer? comparer) + { + if (x == null) { - if (x == null) - { - return (y == null) || (y.Count == 0); - } + return (y == null) || (y.Count == 0); + } - if (y == null) - { - return (x.Count == 0); - } + if (y == null) + { + return (x.Count == 0); + } - if (x.Count != y.Count) - { - return false; - } + if (x.Count != y.Count) + { + return false; + } - if (x.Count == 0) - { - return true; - } + if (x.Count == 0) + { + return true; + } - // We have two unordered lists. So comparison is an O(n*m) operation which is expensive. Usually - // headers have 1-2 parameters (if any), so this comparison shouldn't be too expensive. - var alreadyFound = new bool[x.Count]; - var i = 0; - foreach (var xItem in x) - { - Contract.Assert(xItem != null); + // We have two unordered lists. So comparison is an O(n*m) operation which is expensive. Usually + // headers have 1-2 parameters (if any), so this comparison shouldn't be too expensive. + var alreadyFound = new bool[x.Count]; + var i = 0; + foreach (var xItem in x) + { + Contract.Assert(xItem != null); - i = 0; - var found = false; - foreach (var yItem in y) + i = 0; + var found = false; + foreach (var yItem in y) + { + if (!alreadyFound[i]) { - if (!alreadyFound[i]) + if (((comparer == null) && xItem.Equals(yItem)) || + ((comparer != null) && comparer.Equals(xItem, yItem))) { - if (((comparer == null) && xItem.Equals(yItem)) || - ((comparer != null) && comparer.Equals(xItem, yItem))) - { - alreadyFound[i] = true; - found = true; - break; - } + alreadyFound[i] = true; + found = true; + break; } - i++; - } - - if (!found) - { - return false; } + i++; } - // Since we never re-use a "found" value in 'y', we expected 'alreadyFound' to have all fields set to 'true'. - // Otherwise the two collections can't be equal and we should not get here. - Contract.Assert(Contract.ForAll(alreadyFound, value => { return value; }), - "Expected all values in 'alreadyFound' to be true since collections are considered equal."); - - return true; + if (!found) + { + return false; + } } - internal static int GetNextNonEmptyOrWhitespaceIndex( - StringSegment input, - int startIndex, - bool skipEmptyValues, - out bool separatorFound) - { - Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length. - - separatorFound = false; - var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex); + // Since we never re-use a "found" value in 'y', we expected 'alreadyFound' to have all fields set to 'true'. + // Otherwise the two collections can't be equal and we should not get here. + Contract.Assert(Contract.ForAll(alreadyFound, value => { return value; }), + "Expected all values in 'alreadyFound' to be true since collections are considered equal."); - if ((current == input.Length) || (input[current] != ',')) - { - return current; - } + return true; + } - // If we have a separator, skip the separator and all following whitespaces. If we support - // empty values, continue until the current character is neither a separator nor a whitespace. - separatorFound = true; - current++; // skip delimiter. - current = current + HttpRuleParser.GetWhitespaceLength(input, current); + internal static int GetNextNonEmptyOrWhitespaceIndex( + StringSegment input, + int startIndex, + bool skipEmptyValues, + out bool separatorFound) + { + Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length. - if (skipEmptyValues) - { - while ((current < input.Length) && (input[current] == ',')) - { - current++; // skip delimiter. - current = current + HttpRuleParser.GetWhitespaceLength(input, current); - } - } + separatorFound = false; + var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex); + if ((current == input.Length) || (input[current] != ',')) + { return current; } - private static int AdvanceCacheDirectiveIndex(int current, string headerValue) - { - // Skip until the next potential name - current += HttpRuleParser.GetWhitespaceLength(headerValue, current); + // If we have a separator, skip the separator and all following whitespaces. If we support + // empty values, continue until the current character is neither a separator nor a whitespace. + separatorFound = true; + current++; // skip delimiter. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); - // Skip the value if present - if (current < headerValue.Length && headerValue[current] == '=') + if (skipEmptyValues) + { + while ((current < input.Length) && (input[current] == ',')) { - current++; // skip '=' - current += NameValueHeaderValue.GetValueLength(headerValue, current); + current++; // skip delimiter. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); } + } - // Find the next delimiter - current = headerValue.IndexOf(',', current); + return current; + } - if (current == -1) - { - // If no delimiter found, skip to the end - return headerValue.Length; - } + private static int AdvanceCacheDirectiveIndex(int current, string headerValue) + { + // Skip until the next potential name + current += HttpRuleParser.GetWhitespaceLength(headerValue, current); + + // Skip the value if present + if (current < headerValue.Length && headerValue[current] == '=') + { + current++; // skip '=' + current += NameValueHeaderValue.GetValueLength(headerValue, current); + } - current++; // skip ',' - current += HttpRuleParser.GetWhitespaceLength(headerValue, current); + // Find the next delimiter + current = headerValue.IndexOf(',', current); - return current; + if (current == -1) + { + // If no delimiter found, skip to the end + return headerValue.Length; } - /// - /// Try to find a target header value among the set of given header values and parse it as a - /// . - /// - /// - /// The containing the set of header values to search. - /// - /// - /// The target header value to look for. - /// - /// - /// When this method returns, contains the parsed , if the parsing succeeded, or - /// null if the parsing failed. The conversion fails if the was not - /// found or could not be parsed as a . This parameter is passed uninitialized; - /// any value originally supplied in result will be overwritten. - /// - /// - /// if is found and successfully parsed; otherwise, - /// . - /// - // e.g. { "headerValue=10, targetHeaderValue=30" } - public static bool TryParseSeconds(StringValues headerValues, string targetValue, [NotNullWhen(true)] out TimeSpan? value) - { - if (StringValues.IsNullOrEmpty(headerValues) || string.IsNullOrEmpty(targetValue)) - { - value = null; - return false; - } + current++; // skip ',' + current += HttpRuleParser.GetWhitespaceLength(headerValue, current); - for (var i = 0; i < headerValues.Count; i++) - { - var segment = headerValues[i] ?? string.Empty; + return current; + } + + /// + /// Try to find a target header value among the set of given header values and parse it as a + /// . + /// + /// + /// The containing the set of header values to search. + /// + /// + /// The target header value to look for. + /// + /// + /// When this method returns, contains the parsed , if the parsing succeeded, or + /// null if the parsing failed. The conversion fails if the was not + /// found or could not be parsed as a . This parameter is passed uninitialized; + /// any value originally supplied in result will be overwritten. + /// + /// + /// if is found and successfully parsed; otherwise, + /// . + /// + // e.g. { "headerValue=10, targetHeaderValue=30" } + public static bool TryParseSeconds(StringValues headerValues, string targetValue, [NotNullWhen(true)] out TimeSpan? value) + { + if (StringValues.IsNullOrEmpty(headerValues) || string.IsNullOrEmpty(targetValue)) + { + value = null; + return false; + } - // Trim leading white space - var current = HttpRuleParser.GetWhitespaceLength(segment, 0); + for (var i = 0; i < headerValues.Count; i++) + { + var segment = headerValues[i] ?? string.Empty; - while (current < segment.Length) + // Trim leading white space + var current = HttpRuleParser.GetWhitespaceLength(segment, 0); + + while (current < segment.Length) + { + long seconds; + var initial = current; + var tokenLength = HttpRuleParser.GetTokenLength(headerValues[i], current); + if (tokenLength == targetValue.Length + && string.Compare(headerValues[i], current, targetValue, 0, tokenLength, StringComparison.OrdinalIgnoreCase) == 0 + && TryParseNonNegativeInt64FromHeaderValue(current + tokenLength, segment, out seconds)) { - long seconds; - var initial = current; - var tokenLength = HttpRuleParser.GetTokenLength(headerValues[i], current); - if (tokenLength == targetValue.Length - && string.Compare(headerValues[i], current, targetValue, 0, tokenLength, StringComparison.OrdinalIgnoreCase) == 0 - && TryParseNonNegativeInt64FromHeaderValue(current + tokenLength, segment, out seconds)) - { - // Token matches target value and seconds were parsed - value = TimeSpan.FromSeconds(seconds); - return true; - } + // Token matches target value and seconds were parsed + value = TimeSpan.FromSeconds(seconds); + return true; + } - current = AdvanceCacheDirectiveIndex(current + tokenLength, segment); + current = AdvanceCacheDirectiveIndex(current + tokenLength, segment); - // Ensure index was advanced - if (current <= initial) - { - Debug.Assert(false, $"Index '{nameof(current)}' not advanced, this is a bug."); - value = null; - return false; - } + // Ensure index was advanced + if (current <= initial) + { + Debug.Assert(false, $"Index '{nameof(current)}' not advanced, this is a bug."); + value = null; + return false; } } - value = null; + } + value = null; + return false; + } + + /// + /// Check if a target directive exists among the set of given cache control directives. + /// + /// + /// The containing the set of cache control directives. + /// + /// + /// The target cache control directives to look for. + /// + /// + /// if is contained in ; + /// otherwise, . + /// + public static bool ContainsCacheDirective(StringValues cacheControlDirectives, string targetDirectives) + { + if (StringValues.IsNullOrEmpty(cacheControlDirectives) || string.IsNullOrEmpty(targetDirectives)) + { return false; } - /// - /// Check if a target directive exists among the set of given cache control directives. - /// - /// - /// The containing the set of cache control directives. - /// - /// - /// The target cache control directives to look for. - /// - /// - /// if is contained in ; - /// otherwise, . - /// - public static bool ContainsCacheDirective(StringValues cacheControlDirectives, string targetDirectives) - { - if (StringValues.IsNullOrEmpty(cacheControlDirectives) || string.IsNullOrEmpty(targetDirectives)) - { - return false; - } + for (var i = 0; i < cacheControlDirectives.Count; i++) + { + var segment = cacheControlDirectives[i] ?? string.Empty; - for (var i = 0; i < cacheControlDirectives.Count; i++) - { - var segment = cacheControlDirectives[i] ?? string.Empty; + // Trim leading white space + var current = HttpRuleParser.GetWhitespaceLength(segment, 0); - // Trim leading white space - var current = HttpRuleParser.GetWhitespaceLength(segment, 0); + while (current < segment.Length) + { + var initial = current; - while (current < segment.Length) + var tokenLength = HttpRuleParser.GetTokenLength(segment, current); + if (tokenLength == targetDirectives.Length + && string.Compare(segment, current, targetDirectives, 0, tokenLength, StringComparison.OrdinalIgnoreCase) == 0) { - var initial = current; - - var tokenLength = HttpRuleParser.GetTokenLength(segment, current); - if (tokenLength == targetDirectives.Length - && string.Compare(segment, current, targetDirectives, 0, tokenLength, StringComparison.OrdinalIgnoreCase) == 0) - { - // Token matches target value - return true; - } + // Token matches target value + return true; + } - current = AdvanceCacheDirectiveIndex(current + tokenLength, segment); + current = AdvanceCacheDirectiveIndex(current + tokenLength, segment); - // Ensure index was advanced - if (current <= initial) - { - Debug.Assert(false, $"Index '{nameof(current)}' not advanced, this is a bug."); - return false; - } + // Ensure index was advanced + if (current <= initial) + { + Debug.Assert(false, $"Index '{nameof(current)}' not advanced, this is a bug."); + return false; } } + } + return false; + } + + private static bool TryParseNonNegativeInt64FromHeaderValue(int startIndex, string headerValue, out long result) + { + // Trim leading whitespace + startIndex += HttpRuleParser.GetWhitespaceLength(headerValue, startIndex); + + // Match and skip '=', it also can't be the last character in the headerValue + if (startIndex >= headerValue.Length - 1 || headerValue[startIndex] != '=') + { + result = 0; return false; } + startIndex++; + + // Trim trailing whitespace + startIndex += HttpRuleParser.GetWhitespaceLength(headerValue, startIndex); - private static bool TryParseNonNegativeInt64FromHeaderValue(int startIndex, string headerValue, out long result) + // Try parse the number + if (TryParseNonNegativeInt64(new StringSegment(headerValue, startIndex, HttpRuleParser.GetNumberLength(headerValue, startIndex, false)), out result)) { - // Trim leading whitespace - startIndex += HttpRuleParser.GetWhitespaceLength(headerValue, startIndex); + return true; + } - // Match and skip '=', it also can't be the last character in the headerValue - if (startIndex >= headerValue.Length - 1 || headerValue[startIndex] != '=') - { - result = 0; - return false; - } - startIndex++; + result = 0; + return false; + } - // Trim trailing whitespace - startIndex += HttpRuleParser.GetWhitespaceLength(headerValue, startIndex); + /// + /// Try to convert a string representation of a positive number to its 64-bit signed integer equivalent. + /// A return value indicates whether the conversion succeeded or failed. + /// + /// + /// A string containing a number to convert. + /// + /// + /// When this method returns, contains the 64-bit signed integer value equivalent of the number contained + /// in the string, if the conversion succeeded, or zero if the conversion failed. The conversion fails if + /// the string is null or String.Empty, is not of the correct format, is negative, or represents a number + /// greater than Int64.MaxValue. This parameter is passed uninitialized; any value originally supplied in + /// result will be overwritten. + /// + /// if parsing succeeded; otherwise, . + public static bool TryParseNonNegativeInt32(StringSegment value, out int result) + { + if (string.IsNullOrEmpty(value.Buffer) || value.Length == 0) + { + result = 0; + return false; + } - // Try parse the number - if (TryParseNonNegativeInt64(new StringSegment(headerValue, startIndex, HttpRuleParser.GetNumberLength(headerValue, startIndex, false)), out result)) - { - return true; - } + return int.TryParse(value.AsSpan(), NumberStyles.None, NumberFormatInfo.InvariantInfo, out result); + } + /// + /// Try to convert a representation of a positive number to its 64-bit signed + /// integer equivalent. A return value indicates whether the conversion succeeded or failed. + /// + /// + /// A containing a number to convert. + /// + /// + /// When this method returns, contains the 64-bit signed integer value equivalent of the number contained + /// in the string, if the conversion succeeded, or zero if the conversion failed. The conversion fails if + /// the is null or String.Empty, is not of the correct format, is negative, or + /// represents a number greater than Int64.MaxValue. This parameter is passed uninitialized; any value + /// originally supplied in result will be overwritten. + /// + /// if parsing succeeded; otherwise, . + public static bool TryParseNonNegativeInt64(StringSegment value, out long result) + { + if (string.IsNullOrEmpty(value.Buffer) || value.Length == 0) + { result = 0; return false; } + return long.TryParse(value.AsSpan(), NumberStyles.None, NumberFormatInfo.InvariantInfo, out result); + } - /// - /// Try to convert a string representation of a positive number to its 64-bit signed integer equivalent. - /// A return value indicates whether the conversion succeeded or failed. - /// - /// - /// A string containing a number to convert. - /// - /// - /// When this method returns, contains the 64-bit signed integer value equivalent of the number contained - /// in the string, if the conversion succeeded, or zero if the conversion failed. The conversion fails if - /// the string is null or String.Empty, is not of the correct format, is negative, or represents a number - /// greater than Int64.MaxValue. This parameter is passed uninitialized; any value originally supplied in - /// result will be overwritten. - /// - /// if parsing succeeded; otherwise, . - public static bool TryParseNonNegativeInt32(StringSegment value, out int result) - { - if (string.IsNullOrEmpty(value.Buffer) || value.Length == 0) - { - result = 0; - return false; - } + // Strict and fast RFC7231 5.3.1 Quality value parser (and without memory allocation) + // See https://tools.ietf.org/html/rfc7231#section-5.3.1 + // Check is made to verify if the value is between 0 and 1 (and it returns False if the check fails). + internal static bool TryParseQualityDouble(StringSegment input, int startIndex, out double quality, out int length) + { + quality = 0; + length = 0; - return int.TryParse(value.AsSpan(), NumberStyles.None, NumberFormatInfo.InvariantInfo, out result); - } - - /// - /// Try to convert a representation of a positive number to its 64-bit signed - /// integer equivalent. A return value indicates whether the conversion succeeded or failed. - /// - /// - /// A containing a number to convert. - /// - /// - /// When this method returns, contains the 64-bit signed integer value equivalent of the number contained - /// in the string, if the conversion succeeded, or zero if the conversion failed. The conversion fails if - /// the is null or String.Empty, is not of the correct format, is negative, or - /// represents a number greater than Int64.MaxValue. This parameter is passed uninitialized; any value - /// originally supplied in result will be overwritten. - /// - /// if parsing succeeded; otherwise, . - public static bool TryParseNonNegativeInt64(StringSegment value, out long result) - { - if (string.IsNullOrEmpty(value.Buffer) || value.Length == 0) - { - result = 0; - return false; - } - return long.TryParse(value.AsSpan(), NumberStyles.None, NumberFormatInfo.InvariantInfo, out result); - } + var inputLength = input.Length; + var current = startIndex; + var limit = startIndex + _qualityValueMaxCharCount; - // Strict and fast RFC7231 5.3.1 Quality value parser (and without memory allocation) - // See https://tools.ietf.org/html/rfc7231#section-5.3.1 - // Check is made to verify if the value is between 0 and 1 (and it returns False if the check fails). - internal static bool TryParseQualityDouble(StringSegment input, int startIndex, out double quality, out int length) - { - quality = 0; - length = 0; + var intPart = 0; + var decPart = 0; + var decPow = 1; - var inputLength = input.Length; - var current = startIndex; - var limit = startIndex + _qualityValueMaxCharCount; + if (current >= inputLength) + { + return false; + } - var intPart = 0; - var decPart = 0; - var decPow = 1; + var ch = input[current]; - if (current >= inputLength) - { - return false; - } + if (ch >= '0' && ch <= '1') // Only values between 0 and 1 are accepted, according to RFC + { + intPart = ch - '0'; + current++; + } + else + { + // The RFC doesn't allow decimal values starting with dot. I.e. value ".123" is invalid. It must be in the + // form "0.123". + return false; + } - var ch = input[current]; + if (current < inputLength) + { + ch = input[current]; - if (ch >= '0' && ch <= '1') // Only values between 0 and 1 are accepted, according to RFC - { - intPart = ch - '0'; - current++; - } - else + if (ch >= '0' && ch <= '9') { - // The RFC doesn't allow decimal values starting with dot. I.e. value ".123" is invalid. It must be in the - // form "0.123". + // The RFC accepts only one digit before the dot return false; } - if (current < inputLength) + if (ch == '.') { - ch = input[current]; - - if (ch >= '0' && ch <= '9') - { - // The RFC accepts only one digit before the dot - return false; - } + current++; - if (ch == '.') + while (current < inputLength) { - current++; - - while (current < inputLength) + ch = input[current]; + if (ch >= '0' && ch <= '9') { - ch = input[current]; - if (ch >= '0' && ch <= '9') - { - if (current >= limit) - { - return false; - } - - decPart = decPart * 10 + ch - '0'; - decPow *= 10; - current++; - } - else + if (current >= limit) { - break; + return false; } + + decPart = decPart * 10 + ch - '0'; + decPow *= 10; + current++; + } + else + { + break; } } } - - if (decPart != 0) - { - quality = intPart + decPart / (double)decPow; - } - else - { - quality = intPart; - } - - if (quality > 1) - { - // reset quality - quality = 0; - return false; - } - - length = current - startIndex; - return true; } - /// - /// Converts the non-negative 64-bit numeric value to its equivalent string representation. - /// - /// - /// The number to convert. - /// - /// - /// The string representation of the value of this instance, consisting of a sequence of digits ranging from 0 to 9 with no leading zeroes. - /// - public static string FormatNonNegativeInt64(long value) + if (decPart != 0) { - if (value < 0) - { - throw new ArgumentOutOfRangeException(nameof(value), value, "The value to be formatted must be non-negative."); - } - - if (value == 0) - { - return "0"; - } - - return ((ulong)value).ToString(NumberFormatInfo.InvariantInfo); + quality = intPart + decPart / (double)decPow; } - - /// - ///Attempts to parse the specified as a value. - /// - /// The input value. - /// The parsed value. - /// - /// if can be parsed as a date, otherwise . - /// - public static bool TryParseDate(StringSegment input, out DateTimeOffset result) + else { - return HttpRuleParser.TryStringToDate(input, out result); + quality = intPart; } - /// - /// Formats the using the RFC1123 format specifier. - /// - /// The date to format. - /// The formatted date. - public static string FormatDate(DateTimeOffset dateTime) + if (quality > 1) { - return FormatDate(dateTime, quoted: false); + // reset quality + quality = 0; + return false; } - /// - /// Formats the using the RFC1123 format specifier and optionally quotes it. - /// - /// The date to format. - /// Determines if the formatted date should be quoted. - /// The formatted date. - public static string FormatDate(DateTimeOffset dateTime, bool quoted) + length = current - startIndex; + return true; + } + + /// + /// Converts the non-negative 64-bit numeric value to its equivalent string representation. + /// + /// + /// The number to convert. + /// + /// + /// The string representation of the value of this instance, consisting of a sequence of digits ranging from 0 to 9 with no leading zeroes. + /// + public static string FormatNonNegativeInt64(long value) + { + if (value < 0) { - if (quoted) - { - return string.Create(31, dateTime, (span, dt) => - { - span[0] = span[30] = '"'; - dt.TryFormat(span.Slice(1), out _, "r"); - }); - } + throw new ArgumentOutOfRangeException(nameof(value), value, "The value to be formatted must be non-negative."); + } - return dateTime.ToString("r", CultureInfo.InvariantCulture); + if (value == 0) + { + return "0"; } - /// - /// Removes quotes from the specified if quoted. - /// - /// The input to remove quotes from. - /// The value without quotes. - public static StringSegment RemoveQuotes(StringSegment input) + return ((ulong)value).ToString(NumberFormatInfo.InvariantInfo); + } + + /// + ///Attempts to parse the specified as a value. + /// + /// The input value. + /// The parsed value. + /// + /// if can be parsed as a date, otherwise . + /// + public static bool TryParseDate(StringSegment input, out DateTimeOffset result) + { + return HttpRuleParser.TryStringToDate(input, out result); + } + + /// + /// Formats the using the RFC1123 format specifier. + /// + /// The date to format. + /// The formatted date. + public static string FormatDate(DateTimeOffset dateTime) + { + return FormatDate(dateTime, quoted: false); + } + + /// + /// Formats the using the RFC1123 format specifier and optionally quotes it. + /// + /// The date to format. + /// Determines if the formatted date should be quoted. + /// The formatted date. + public static string FormatDate(DateTimeOffset dateTime, bool quoted) + { + if (quoted) { - if (IsQuoted(input)) + return string.Create(31, dateTime, (span, dt) => { - input = input.Subsegment(1, input.Length - 2); - } - return input; + span[0] = span[30] = '"'; + dt.TryFormat(span.Slice(1), out _, "r"); + }); } - /// - /// Determines if the specified is quoted. - /// - /// The value to inspect. - /// if the value is quoted, otherwise . - public static bool IsQuoted(StringSegment input) + return dateTime.ToString("r", CultureInfo.InvariantCulture); + } + + /// + /// Removes quotes from the specified if quoted. + /// + /// The input to remove quotes from. + /// The value without quotes. + public static StringSegment RemoveQuotes(StringSegment input) + { + if (IsQuoted(input)) { - return !StringSegment.IsNullOrEmpty(input) && input.Length >= 2 && input[0] == '"' && input[input.Length - 1] == '"'; + input = input.Subsegment(1, input.Length - 2); } + return input; + } - /// - /// Given a quoted-string as defined by the RFC specification, - /// removes quotes and unescapes backslashes and quotes. This assumes that the input is a valid quoted-string. - /// - /// The quoted-string to be unescaped. - /// An unescaped version of the quoted-string. - public static StringSegment UnescapeAsQuotedString(StringSegment input) - { - input = RemoveQuotes(input); + /// + /// Determines if the specified is quoted. + /// + /// The value to inspect. + /// if the value is quoted, otherwise . + public static bool IsQuoted(StringSegment input) + { + return !StringSegment.IsNullOrEmpty(input) && input.Length >= 2 && input[0] == '"' && input[input.Length - 1] == '"'; + } + + /// + /// Given a quoted-string as defined by the RFC specification, + /// removes quotes and unescapes backslashes and quotes. This assumes that the input is a valid quoted-string. + /// + /// The quoted-string to be unescaped. + /// An unescaped version of the quoted-string. + public static StringSegment UnescapeAsQuotedString(StringSegment input) + { + input = RemoveQuotes(input); - // First pass to calculate the size of the string - var backSlashCount = CountBackslashesForDecodingQuotedString(input); + // First pass to calculate the size of the string + var backSlashCount = CountBackslashesForDecodingQuotedString(input); - if (backSlashCount == 0) - { - return input; - } + if (backSlashCount == 0) + { + return input; + } - return string.Create(input.Length - backSlashCount, input, (span, segment) => + return string.Create(input.Length - backSlashCount, input, (span, segment) => + { + var spanIndex = 0; + var spanLength = span.Length; + for (var i = 0; i < segment.Length && (uint)spanIndex < (uint)spanLength; i++) { - var spanIndex = 0; - var spanLength = span.Length; - for (var i = 0; i < segment.Length && (uint)spanIndex < (uint)spanLength; i++) + int nextIndex = i + 1; + if ((uint)nextIndex < (uint)segment.Length && segment[i] == '\\') { - int nextIndex = i + 1; - if ((uint)nextIndex < (uint)segment.Length && segment[i] == '\\') - { // If there is an backslash character as the last character in the string, // we will assume that it should be included literally in the unescaped string // Ex: "hello\\" => "hello\\" @@ -621,102 +621,101 @@ namespace Microsoft.Net.Http.Headers // we will assume it is over escaping and just add a n to the string. // Ex: "he\\llo" => "hello" span[spanIndex] = segment[nextIndex]; - i++; - } - else - { - span[spanIndex] = segment[i]; - } - - spanIndex++; + i++; } - }); - } + else + { + span[spanIndex] = segment[i]; + } + + spanIndex++; + } + }); + } - private static int CountBackslashesForDecodingQuotedString(StringSegment input) + private static int CountBackslashesForDecodingQuotedString(StringSegment input) + { + var numberBackSlashes = 0; + for (var i = 0; i < input.Length; i++) { - var numberBackSlashes = 0; - for (var i = 0; i < input.Length; i++) - { - if (i < input.Length - 1 && input[i] == '\\') + if (i < input.Length - 1 && input[i] == '\\') + { + // If there is an backslash character as the last character in the string, + // we will assume that it should be included literally in the unescaped string + // Ex: "hello\\" => "hello\\" + // Also, if a sender adds a quoted pair like '\\''n', + // we will assume it is over escaping and just add a n to the string. + // Ex: "he\\llo" => "hello" + if (input[i + 1] == '\\') { - // If there is an backslash character as the last character in the string, - // we will assume that it should be included literally in the unescaped string - // Ex: "hello\\" => "hello\\" - // Also, if a sender adds a quoted pair like '\\''n', - // we will assume it is over escaping and just add a n to the string. - // Ex: "he\\llo" => "hello" - if (input[i + 1] == '\\') - { - // Only count escaped backslashes once - i++; - } - numberBackSlashes++; + // Only count escaped backslashes once + i++; } + numberBackSlashes++; } - return numberBackSlashes; - } - - /// - /// Escapes a as a quoted-string, which is defined by - /// the RFC specification. - /// - /// - /// This will add a backslash before each backslash and quote and add quotes - /// around the input. Assumes that the input does not have quotes around it, - /// as this method will add them. Throws if the input contains any invalid escape characters, - /// as defined by rfc7230. - /// - /// The input to be escaped. - /// An escaped version of the quoted-string. - public static StringSegment EscapeAsQuotedString(StringSegment input) - { - // By calling this, we know that the string requires quotes around it to be a valid token. - var backSlashCount = CountAndCheckCharactersNeedingBackslashesWhenEncoding(input); - - // 2 for quotes - return string.Create(input.Length + backSlashCount + 2, input, (span, segment) => - { + } + return numberBackSlashes; + } + + /// + /// Escapes a as a quoted-string, which is defined by + /// the RFC specification. + /// + /// + /// This will add a backslash before each backslash and quote and add quotes + /// around the input. Assumes that the input does not have quotes around it, + /// as this method will add them. Throws if the input contains any invalid escape characters, + /// as defined by rfc7230. + /// + /// The input to be escaped. + /// An escaped version of the quoted-string. + public static StringSegment EscapeAsQuotedString(StringSegment input) + { + // By calling this, we know that the string requires quotes around it to be a valid token. + var backSlashCount = CountAndCheckCharactersNeedingBackslashesWhenEncoding(input); + + // 2 for quotes + return string.Create(input.Length + backSlashCount + 2, input, (span, segment) => + { // Helps to elide the bounds check for span[0] span[span.Length - 1] = span[0] = '\"'; - var spanIndex = 1; - for (var i = 0; i < segment.Length; i++) + var spanIndex = 1; + for (var i = 0; i < segment.Length; i++) + { + if (segment[i] == '\\' || segment[i] == '\"') + { + span[spanIndex++] = '\\'; + } + else if ((segment[i] <= 0x1F || segment[i] == 0x7F) && segment[i] != 0x09) { - if (segment[i] == '\\' || segment[i] == '\"') - { - span[spanIndex++] = '\\'; - } - else if ((segment[i] <= 0x1F || segment[i] == 0x7F) && segment[i] != 0x09) - { // Control characters are not allowed in a quoted-string, which include all characters // below 0x1F (except for 0x09 (TAB)) and 0x7F. throw new FormatException($"Invalid control character '{segment[i]}' in input."); - } - span[spanIndex++] = segment[i]; } - }); - } + span[spanIndex++] = segment[i]; + } + }); + } - private static int CountAndCheckCharactersNeedingBackslashesWhenEncoding(StringSegment input) + private static int CountAndCheckCharactersNeedingBackslashesWhenEncoding(StringSegment input) + { + var numberOfCharactersNeedingEscaping = 0; + for (var i = 0; i < input.Length; i++) { - var numberOfCharactersNeedingEscaping = 0; - for (var i = 0; i < input.Length; i++) + if (input[i] == '\\' || input[i] == '\"') { - if (input[i] == '\\' || input[i] == '\"') - { - numberOfCharactersNeedingEscaping++; - } + numberOfCharactersNeedingEscaping++; } - return numberOfCharactersNeedingEscaping; } + return numberOfCharactersNeedingEscaping; + } - internal static void ThrowIfReadOnly(bool isReadOnly) + internal static void ThrowIfReadOnly(bool isReadOnly) + { + if (isReadOnly) { - if (isReadOnly) - { - throw new InvalidOperationException("The object cannot be modified because it is read-only."); - } + throw new InvalidOperationException("The object cannot be modified because it is read-only."); } } } diff --git a/src/Http/Headers/src/HttpHeaderParser.cs b/src/Http/Headers/src/HttpHeaderParser.cs index eb654fca3f..ba7a5874c0 100644 --- a/src/Http/Headers/src/HttpHeaderParser.cs +++ b/src/Http/Headers/src/HttpHeaderParser.cs @@ -8,161 +8,160 @@ using System.Diagnostics.Contracts; using System.Globalization; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +internal abstract class HttpHeaderParser { - internal abstract class HttpHeaderParser + private readonly bool _supportsMultipleValues; + + protected HttpHeaderParser(bool supportsMultipleValues) { - private readonly bool _supportsMultipleValues; + _supportsMultipleValues = supportsMultipleValues; + } - protected HttpHeaderParser(bool supportsMultipleValues) - { - _supportsMultipleValues = supportsMultipleValues; - } + public bool SupportsMultipleValues + { + get { return _supportsMultipleValues; } + } - public bool SupportsMultipleValues - { - get { return _supportsMultipleValues; } - } + // If a parser supports multiple values, a call to ParseValue/TryParseValue should return a value for 'index' + // pointing to the next non-whitespace character after a delimiter. E.g. if called with a start index of 0 + // for string "value , second_value", then after the call completes, 'index' must point to 's', i.e. the first + // non-whitespace after the separator ','. + public abstract bool TryParseValue(StringSegment value, ref int index, out T? parsedValue); - // If a parser supports multiple values, a call to ParseValue/TryParseValue should return a value for 'index' - // pointing to the next non-whitespace character after a delimiter. E.g. if called with a start index of 0 - // for string "value , second_value", then after the call completes, 'index' must point to 's', i.e. the first - // non-whitespace after the separator ','. - public abstract bool TryParseValue(StringSegment value, ref int index, out T? parsedValue); + public T? ParseValue(StringSegment value, ref int index) + { + // Index may be value.Length (e.g. both 0). This may be allowed for some headers (e.g. Accept but not + // allowed by others (e.g. Content-Length). The parser has to decide if this is valid or not. + Contract.Requires((value == null) || ((index >= 0) && (index <= value.Length))); - public T? ParseValue(StringSegment value, ref int index) + // If a parser returns 'null', it means there was no value, but that's valid (e.g. "Accept: "). The caller + // can ignore the value. + if (!TryParseValue(value, ref index, out var result)) { - // Index may be value.Length (e.g. both 0). This may be allowed for some headers (e.g. Accept but not - // allowed by others (e.g. Content-Length). The parser has to decide if this is valid or not. - Contract.Requires((value == null) || ((index >= 0) && (index <= value.Length))); - - // If a parser returns 'null', it means there was no value, but that's valid (e.g. "Accept: "). The caller - // can ignore the value. - if (!TryParseValue(value, ref index, out var result)) - { - throw new FormatException(string.Format(CultureInfo.InvariantCulture, - "The header contains invalid values at index {0}: '{1}'", index, value.Value ?? "")); - } - return result; + throw new FormatException(string.Format(CultureInfo.InvariantCulture, + "The header contains invalid values at index {0}: '{1}'", index, value.Value ?? "")); } + return result; + } - public virtual bool TryParseValues(IList? values, [NotNullWhen(true)] out IList? parsedValues) - { - return TryParseValues(values, strict: false, parsedValues: out parsedValues); - } + public virtual bool TryParseValues(IList? values, [NotNullWhen(true)] out IList? parsedValues) + { + return TryParseValues(values, strict: false, parsedValues: out parsedValues); + } - public virtual bool TryParseStrictValues(IList? values, [NotNullWhen(true)] out IList? parsedValues) + public virtual bool TryParseStrictValues(IList? values, [NotNullWhen(true)] out IList? parsedValues) + { + return TryParseValues(values, strict: true, parsedValues: out parsedValues); + } + + protected virtual bool TryParseValues(IList? values, bool strict, [NotNullWhen(true)] out IList? parsedValues) + { + Contract.Assert(_supportsMultipleValues); + // If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller + // can ignore the value. + parsedValues = null; + List? results = null; + if (values == null) { - return TryParseValues(values, strict: true, parsedValues: out parsedValues); + return false; } - - protected virtual bool TryParseValues(IList? values, bool strict, [NotNullWhen(true)] out IList? parsedValues) + for (var i = 0; i < values.Count; i++) { - Contract.Assert(_supportsMultipleValues); - // If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller - // can ignore the value. - parsedValues = null; - List? results = null; - if (values == null) - { - return false; - } - for (var i = 0; i < values.Count; i++) - { - var value = values[i]; - var index = 0; + var value = values[i]; + var index = 0; - while (!string.IsNullOrEmpty(value) && index < value.Length) + while (!string.IsNullOrEmpty(value) && index < value.Length) + { + if (TryParseValue(value, ref index, out var output)) { - if (TryParseValue(value, ref index, out var output)) + // The entry may not contain an actual value, like " , " + if (output != null) { - // The entry may not contain an actual value, like " , " - if (output != null) + if (results == null) { - if (results == null) - { - results = new List(); // Allocate it only when used - } - results.Add(output); + results = new List(); // Allocate it only when used } - } - else if (strict) - { - return false; - } - else - { - // Skip the invalid values and keep trying. - index++; + results.Add(output); } } + else if (strict) + { + return false; + } + else + { + // Skip the invalid values and keep trying. + index++; + } } - if (results != null) - { - parsedValues = results; - return true; - } - return false; } - - public virtual IList ParseValues(IList? values) + if (results != null) { - return ParseValues(values, strict: false); + parsedValues = results; + return true; } + return false; + } + + public virtual IList ParseValues(IList? values) + { + return ParseValues(values, strict: false); + } - public virtual IList ParseStrictValues(IList? values) + public virtual IList ParseStrictValues(IList? values) + { + return ParseValues(values, strict: true); + } + + protected virtual IList ParseValues(IList? values, bool strict) + { + Contract.Assert(_supportsMultipleValues); + // If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller + // can ignore the value. + var parsedValues = new List(); + if (values == null) { - return ParseValues(values, strict: true); + return parsedValues; } - - protected virtual IList ParseValues(IList? values, bool strict) + foreach (var value in values) { - Contract.Assert(_supportsMultipleValues); - // If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller - // can ignore the value. - var parsedValues = new List(); - if (values == null) - { - return parsedValues; - } - foreach (var value in values) - { - int index = 0; + int index = 0; - while (!string.IsNullOrEmpty(value) && index < value.Length) + while (!string.IsNullOrEmpty(value) && index < value.Length) + { + if (TryParseValue(value, ref index, out var output)) { - if (TryParseValue(value, ref index, out var output)) + // The entry may not contain an actual value, like " , " + if (output != null) { - // The entry may not contain an actual value, like " , " - if (output != null) - { - parsedValues.Add(output); - } - } - else if (strict) - { - throw new FormatException(string.Format(CultureInfo.InvariantCulture, - "The header contains invalid values at index {0}: '{1}'", index, value)); - } - else - { - // Skip the invalid values and keep trying. - index++; + parsedValues.Add(output); } } + else if (strict) + { + throw new FormatException(string.Format(CultureInfo.InvariantCulture, + "The header contains invalid values at index {0}: '{1}'", index, value)); + } + else + { + // Skip the invalid values and keep trying. + index++; + } } - return parsedValues; } + return parsedValues; + } - // If ValueType is a custom header value type (e.g. NameValueHeaderValue) it implements ToString() correctly. - // However for existing types like int, byte[], DateTimeOffset we can't override ToString(). Therefore the - // parser provides a ToString() virtual method that can be overridden by derived types to correctly serialize - // values (e.g. byte[] to Base64 encoded string). - // The default implementation is to just call ToString() on the value itself which is the right thing to do - // for most headers (custom types, string, etc.). - public virtual string ToString(object value) - { - return value.ToString()!; - } + // If ValueType is a custom header value type (e.g. NameValueHeaderValue) it implements ToString() correctly. + // However for existing types like int, byte[], DateTimeOffset we can't override ToString(). Therefore the + // parser provides a ToString() virtual method that can be overridden by derived types to correctly serialize + // values (e.g. byte[] to Base64 encoded string). + // The default implementation is to just call ToString() on the value itself which is the right thing to do + // for most headers (custom types, string, etc.). + public virtual string ToString(object value) + { + return value.ToString()!; } } diff --git a/src/Http/Headers/src/MediaTypeHeaderValue.cs b/src/Http/Headers/src/MediaTypeHeaderValue.cs index 5b21693433..126d246e92 100644 --- a/src/Http/Headers/src/MediaTypeHeaderValue.cs +++ b/src/Http/Headers/src/MediaTypeHeaderValue.cs @@ -11,829 +11,828 @@ using System.Runtime.InteropServices; using System.Text; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +/// +/// Representation of the media type header. See . +/// +public class MediaTypeHeaderValue { - /// - /// Representation of the media type header. See . - /// - public class MediaTypeHeaderValue - { - private const string BoundaryString = "boundary"; - private const string CharsetString = "charset"; - private const string MatchesAllString = "*/*"; - private const string QualityString = "q"; - private const string WildcardString = "*"; + private const string BoundaryString = "boundary"; + private const string CharsetString = "charset"; + private const string MatchesAllString = "*/*"; + private const string QualityString = "q"; + private const string WildcardString = "*"; - private const char ForwardSlashCharacter = '/'; - private const char PeriodCharacter = '.'; - private const char PlusCharacter = '+'; + private const char ForwardSlashCharacter = '/'; + private const char PeriodCharacter = '.'; + private const char PlusCharacter = '+'; - private static readonly char[] PeriodCharacterArray = new char[] { PeriodCharacter }; + private static readonly char[] PeriodCharacterArray = new char[] { PeriodCharacter }; - private static readonly HttpHeaderParser SingleValueParser - = new GenericHeaderParser(false, GetMediaTypeLength); - private static readonly HttpHeaderParser MultipleValueParser - = new GenericHeaderParser(true, GetMediaTypeLength); + private static readonly HttpHeaderParser SingleValueParser + = new GenericHeaderParser(false, GetMediaTypeLength); + private static readonly HttpHeaderParser MultipleValueParser + = new GenericHeaderParser(true, GetMediaTypeLength); - // Use a collection instead of a dictionary since we may have multiple parameters with the same name. - private ObjectCollection? _parameters; - private StringSegment _mediaType; - private bool _isReadOnly; + // Use a collection instead of a dictionary since we may have multiple parameters with the same name. + private ObjectCollection? _parameters; + private StringSegment _mediaType; + private bool _isReadOnly; - private MediaTypeHeaderValue() - { - // Used by the parser to create a new instance of this type. - } + private MediaTypeHeaderValue() + { + // Used by the parser to create a new instance of this type. + } - /// - /// Initializes a instance. - /// - /// A representation of a media type. - /// The text provided must be a single media type without parameters. - public MediaTypeHeaderValue(StringSegment mediaType) - { - CheckMediaTypeFormat(mediaType, nameof(mediaType)); - _mediaType = mediaType; - } + /// + /// Initializes a instance. + /// + /// A representation of a media type. + /// The text provided must be a single media type without parameters. + public MediaTypeHeaderValue(StringSegment mediaType) + { + CheckMediaTypeFormat(mediaType, nameof(mediaType)); + _mediaType = mediaType; + } - /// - /// Initializes a instance. - /// - /// A representation of a media type. - /// The text provided must be a single media type without parameters. - /// The with the quality of the media type. - public MediaTypeHeaderValue(StringSegment mediaType, double quality) - : this(mediaType) - { - Quality = quality; - } + /// + /// Initializes a instance. + /// + /// A representation of a media type. + /// The text provided must be a single media type without parameters. + /// The with the quality of the media type. + public MediaTypeHeaderValue(StringSegment mediaType, double quality) + : this(mediaType) + { + Quality = quality; + } - /// - /// Gets or sets the value of the charset parameter. Returns - /// if there is no charset. - /// - public StringSegment Charset + /// + /// Gets or sets the value of the charset parameter. Returns + /// if there is no charset. + /// + public StringSegment Charset + { + get { - get - { - return NameValueHeaderValue.Find(_parameters, CharsetString)?.Value ?? default; - } - set - { - HeaderUtilities.ThrowIfReadOnly(IsReadOnly); - // We don't prevent a user from setting whitespace-only charsets. Like we can't prevent a user from - // setting a non-existing charset. - var charsetParameter = NameValueHeaderValue.Find(_parameters, CharsetString); - if (StringSegment.IsNullOrEmpty(value)) - { - // Remove charset parameter - if (charsetParameter != null) - { - Parameters.Remove(charsetParameter); - } - } - else - { - if (charsetParameter != null) - { - charsetParameter.Value = value; - } - else - { - Parameters.Add(new NameValueHeaderValue(CharsetString, value)); - } - } - } + return NameValueHeaderValue.Find(_parameters, CharsetString)?.Value ?? default; } - - /// - /// Gets or sets the value of the Encoding parameter. Setting the Encoding will set - /// the to . - /// - public Encoding? Encoding + set { - get + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + // We don't prevent a user from setting whitespace-only charsets. Like we can't prevent a user from + // setting a non-existing charset. + var charsetParameter = NameValueHeaderValue.Find(_parameters, CharsetString); + if (StringSegment.IsNullOrEmpty(value)) { - var charset = Charset; - - // Check HasValue; IsNullOrEmpty lacks [MemberNotNullWhen(false, nameof(Value))]. - if (charset.HasValue && !StringSegment.IsNullOrEmpty(charset)) + // Remove charset parameter + if (charsetParameter != null) { - try - { - return Encoding.GetEncoding(charset.Value); - } - catch (ArgumentException) - { - // Invalid or not supported - } + Parameters.Remove(charsetParameter); } - return null; } - set + else { - HeaderUtilities.ThrowIfReadOnly(IsReadOnly); - if (value == null) + if (charsetParameter != null) { - Charset = null; + charsetParameter.Value = value; } else { - Charset = value.WebName; + Parameters.Add(new NameValueHeaderValue(CharsetString, value)); } } } + } - /// - /// Gets or sets the value of the boundary parameter. Returns - /// if there is no boundary. - /// - public StringSegment Boundary + /// + /// Gets or sets the value of the Encoding parameter. Setting the Encoding will set + /// the to . + /// + public Encoding? Encoding + { + get { - get - { - return NameValueHeaderValue.Find(_parameters, BoundaryString)?.Value ?? default(StringSegment); - } - set + var charset = Charset; + + // Check HasValue; IsNullOrEmpty lacks [MemberNotNullWhen(false, nameof(Value))]. + if (charset.HasValue && !StringSegment.IsNullOrEmpty(charset)) { - HeaderUtilities.ThrowIfReadOnly(IsReadOnly); - var boundaryParameter = NameValueHeaderValue.Find(_parameters, BoundaryString); - if (StringSegment.IsNullOrEmpty(value)) + try { - // Remove charset parameter - if (boundaryParameter != null) - { - Parameters.Remove(boundaryParameter); - } + return Encoding.GetEncoding(charset.Value); } - else + catch (ArgumentException) { - if (boundaryParameter != null) - { - boundaryParameter.Value = value; - } - else - { - Parameters.Add(new NameValueHeaderValue(BoundaryString, value)); - } + // Invalid or not supported } } + return null; } - - /// - /// Gets or sets the media type's parameters. Returns an empty - /// if there are no parameters. - /// - public IList Parameters + set { - get + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + if (value == null) { - if (_parameters == null) - { - if (IsReadOnly) - { - _parameters = ObjectCollection.EmptyReadOnlyCollection; - } - else - { - _parameters = new ObjectCollection(); - } - } - return _parameters; + Charset = null; } - } - - /// - /// Gets or sets the value of the quality parameter. Returns null - /// if there is no quality. - /// - public double? Quality - { - get => HeaderUtilities.GetQuality(_parameters); - set - { - HeaderUtilities.ThrowIfReadOnly(IsReadOnly); - HeaderUtilities.SetQuality(Parameters, value); - } - } - - /// - /// Gets or sets the value of the media type. Returns - /// if there is no media type. - /// - /// - /// For the media type "application/json", the property gives the value - /// "application/json". - /// - public StringSegment MediaType - { - get { return _mediaType; } - set + else { - HeaderUtilities.ThrowIfReadOnly(IsReadOnly); - CheckMediaTypeFormat(value, nameof(value)); - _mediaType = value; + Charset = value.WebName; } } + } - /// - /// Gets the type of the . - /// - /// - /// For the media type "application/json", the property gives the value "application". - /// - /// See for more details on the type. - public StringSegment Type + /// + /// Gets or sets the value of the boundary parameter. Returns + /// if there is no boundary. + /// + public StringSegment Boundary + { + get { - get - { - return _mediaType.Subsegment(0, _mediaType.IndexOf(ForwardSlashCharacter)); - } + return NameValueHeaderValue.Find(_parameters, BoundaryString)?.Value ?? default(StringSegment); } - - /// - /// Gets the subtype of the . - /// - /// - /// For the media type "application/vnd.example+json", the property gives the value - /// "vnd.example+json". - /// - /// See for more details on the subtype. - public StringSegment SubType + set { - get + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + var boundaryParameter = NameValueHeaderValue.Find(_parameters, BoundaryString); + if (StringSegment.IsNullOrEmpty(value)) { - return _mediaType.Subsegment(_mediaType.IndexOf(ForwardSlashCharacter) + 1); + // Remove charset parameter + if (boundaryParameter != null) + { + Parameters.Remove(boundaryParameter); + } } - } - - /// - /// Gets subtype of the , excluding any structured syntax suffix. Returns - /// if there is no subtype without suffix. - /// - /// - /// For the media type "application/vnd.example+json", the property gives the value - /// "vnd.example". - /// - public StringSegment SubTypeWithoutSuffix - { - get + else { - var subType = SubType; - var startOfSuffix = subType.LastIndexOf(PlusCharacter); - if (startOfSuffix == -1) + if (boundaryParameter != null) { - return subType; + boundaryParameter.Value = value; } else { - return subType.Subsegment(0, startOfSuffix); + Parameters.Add(new NameValueHeaderValue(BoundaryString, value)); } } } + } - /// - /// Gets the structured syntax suffix of the if it has one. - /// See The RFC documentation on structured syntaxes. - /// - /// - /// For the media type "application/vnd.example+json", the property gives the value - /// "json". - /// - public StringSegment Suffix + /// + /// Gets or sets the media type's parameters. Returns an empty + /// if there are no parameters. + /// + public IList Parameters + { + get { - get + if (_parameters == null) { - var subType = SubType; - var startOfSuffix = subType.LastIndexOf(PlusCharacter); - if (startOfSuffix == -1) + if (IsReadOnly) { - return default(StringSegment); + _parameters = ObjectCollection.EmptyReadOnlyCollection; } else { - return subType.Subsegment(startOfSuffix + 1); + _parameters = new ObjectCollection(); } } + return _parameters; } + } - - /// - /// Get a of facets of the . Facets are a - /// period separated list of StringSegments in the . - /// See The RFC documentation on facets. - /// - /// - /// For the media type "application/vnd.example+json", the property gives the value: - /// {"vnd", "example"} - /// - public IEnumerable Facets + /// + /// Gets or sets the value of the quality parameter. Returns null + /// if there is no quality. + /// + public double? Quality + { + get => HeaderUtilities.GetQuality(_parameters); + set { - get - { - return SubTypeWithoutSuffix.Split(PeriodCharacterArray); - } + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + HeaderUtilities.SetQuality(Parameters, value); } + } - /// - /// Gets whether this matches all types. - /// - public bool MatchesAllTypes => MediaType.Equals(MatchesAllString, StringComparison.Ordinal); - - /// - /// Gets whether this matches all subtypes. - /// - /// - /// For the media type "application/*", this property is true. - /// - /// - /// For the media type "application/json", this property is false. - /// - public bool MatchesAllSubTypes => SubType.Equals(WildcardString, StringComparison.Ordinal); - - /// - /// Gets whether this matches all subtypes, ignoring any structured syntax suffix. - /// - /// - /// For the media type "application/*+json", this property is true. - /// - /// - /// For the media type "application/vnd.example+json", this property is false. - /// - public bool MatchesAllSubTypesWithoutSuffix => - SubTypeWithoutSuffix.Equals(WildcardString, StringComparison.OrdinalIgnoreCase); - - /// - /// Gets whether the is readonly. - /// - public bool IsReadOnly - { - get { return _isReadOnly; } - } - - /// - /// Gets a value indicating whether this is a subset of - /// . A "subset" is defined as the same or a more specific media type - /// according to the precedence described in https://www.ietf.org/rfc/rfc2068.txt section 14.1, Accept. - /// - /// The to compare. - /// - /// A value indicating whether this is a subset of - /// . - /// - /// - /// For example "multipart/mixed; boundary=1234" is a subset of "multipart/mixed; boundary=1234", - /// "multipart/mixed", "multipart/*", and "*/*" but not "multipart/mixed; boundary=2345" or - /// "multipart/message; boundary=1234". - /// - public bool IsSubsetOf(MediaTypeHeaderValue otherMediaType) - { - if (otherMediaType == null) - { - return false; - } - - // "text/plain" is a subset of "text/plain", "text/*" and "*/*". "*/*" is a subset only of "*/*". - return MatchesType(otherMediaType) && - MatchesSubtype(otherMediaType) && - MatchesParameters(otherMediaType); + /// + /// Gets or sets the value of the media type. Returns + /// if there is no media type. + /// + /// + /// For the media type "application/json", the property gives the value + /// "application/json". + /// + public StringSegment MediaType + { + get { return _mediaType; } + set + { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + CheckMediaTypeFormat(value, nameof(value)); + _mediaType = value; } + } - /// - /// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components, - /// while avoiding the cost of re-validating the components. - /// - /// A deep copy. - public MediaTypeHeaderValue Copy() + /// + /// Gets the type of the . + /// + /// + /// For the media type "application/json", the property gives the value "application". + /// + /// See for more details on the type. + public StringSegment Type + { + get { - var other = new MediaTypeHeaderValue(); - other._mediaType = _mediaType; + return _mediaType.Subsegment(0, _mediaType.IndexOf(ForwardSlashCharacter)); + } + } - if (_parameters != null) - { - other._parameters = new ObjectCollection( - _parameters.Select(item => item.Copy())); - } - return other; + /// + /// Gets the subtype of the . + /// + /// + /// For the media type "application/vnd.example+json", the property gives the value + /// "vnd.example+json". + /// + /// See for more details on the subtype. + public StringSegment SubType + { + get + { + return _mediaType.Subsegment(_mediaType.IndexOf(ForwardSlashCharacter) + 1); } + } - /// - /// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components, - /// while avoiding the cost of re-validating the components. This copy is read-only. - /// - /// A deep, read-only, copy. - public MediaTypeHeaderValue CopyAsReadOnly() + /// + /// Gets subtype of the , excluding any structured syntax suffix. Returns + /// if there is no subtype without suffix. + /// + /// + /// For the media type "application/vnd.example+json", the property gives the value + /// "vnd.example". + /// + public StringSegment SubTypeWithoutSuffix + { + get { - if (IsReadOnly) + var subType = SubType; + var startOfSuffix = subType.LastIndexOf(PlusCharacter); + if (startOfSuffix == -1) + { + return subType; + } + else { - return this; + return subType.Subsegment(0, startOfSuffix); } + } + } - var other = new MediaTypeHeaderValue(); - other._mediaType = _mediaType; - if (_parameters != null) + /// + /// Gets the structured syntax suffix of the if it has one. + /// See The RFC documentation on structured syntaxes. + /// + /// + /// For the media type "application/vnd.example+json", the property gives the value + /// "json". + /// + public StringSegment Suffix + { + get + { + var subType = SubType; + var startOfSuffix = subType.LastIndexOf(PlusCharacter); + if (startOfSuffix == -1) { - other._parameters = new ObjectCollection( - _parameters.Select(item => item.CopyAsReadOnly()), isReadOnly: true); + return default(StringSegment); } - other._isReadOnly = true; - return other; - } - - /// - /// Gets a value indicating whether is a subset of - /// this in terms of type/subType. A "subset" is defined as the same or a more specific media type - /// according to the precedence described in https://www.ietf.org/rfc/rfc2068.txt section 14.1, Accept. - /// - /// The to compare. - /// - /// A value indicating whether is a subset of - /// this . - /// - /// - /// For example "multipart/mixed" is a subset of "multipart/mixed", - /// "multipart/*", and "*/*" but not "multipart/message." - /// - public bool MatchesMediaType(StringSegment otherMediaType) - { - if (StringSegment.IsNullOrEmpty(otherMediaType)) + else { - return false; + return subType.Subsegment(startOfSuffix + 1); } - GetMediaTypeExpressionLength(otherMediaType, 0, out var mediaType); - - return MatchesType(mediaType) && MatchesSubtype(mediaType); } + } - /// - public override string ToString() + + /// + /// Get a of facets of the . Facets are a + /// period separated list of StringSegments in the . + /// See The RFC documentation on facets. + /// + /// + /// For the media type "application/vnd.example+json", the property gives the value: + /// {"vnd", "example"} + /// + public IEnumerable Facets + { + get { - var builder = new StringBuilder(); - builder.Append(_mediaType.AsSpan()); - NameValueHeaderValue.ToString(_parameters, separator: ';', leadingSeparator: true, destination: builder); - return builder.ToString(); + return SubTypeWithoutSuffix.Split(PeriodCharacterArray); } + } - /// - public override bool Equals(object? obj) - { - var other = obj as MediaTypeHeaderValue; + /// + /// Gets whether this matches all types. + /// + public bool MatchesAllTypes => MediaType.Equals(MatchesAllString, StringComparison.Ordinal); - if (other == null) - { - return false; - } + /// + /// Gets whether this matches all subtypes. + /// + /// + /// For the media type "application/*", this property is true. + /// + /// + /// For the media type "application/json", this property is false. + /// + public bool MatchesAllSubTypes => SubType.Equals(WildcardString, StringComparison.Ordinal); - return _mediaType.Equals(other._mediaType, StringComparison.OrdinalIgnoreCase) && - HeaderUtilities.AreEqualCollections(_parameters, other._parameters); - } + /// + /// Gets whether this matches all subtypes, ignoring any structured syntax suffix. + /// + /// + /// For the media type "application/*+json", this property is true. + /// + /// + /// For the media type "application/vnd.example+json", this property is false. + /// + public bool MatchesAllSubTypesWithoutSuffix => + SubTypeWithoutSuffix.Equals(WildcardString, StringComparison.OrdinalIgnoreCase); - /// - public override int GetHashCode() - { - // The media-type string is case-insensitive. - return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_mediaType) ^ NameValueHeaderValue.GetHashCode(_parameters); - } + /// + /// Gets whether the is readonly. + /// + public bool IsReadOnly + { + get { return _isReadOnly; } + } - /// - /// Takes a media type and parses it into the and its associated parameters. - /// - /// The with the media type. - /// The parsed . - public static MediaTypeHeaderValue Parse(StringSegment input) + /// + /// Gets a value indicating whether this is a subset of + /// . A "subset" is defined as the same or a more specific media type + /// according to the precedence described in https://www.ietf.org/rfc/rfc2068.txt section 14.1, Accept. + /// + /// The to compare. + /// + /// A value indicating whether this is a subset of + /// . + /// + /// + /// For example "multipart/mixed; boundary=1234" is a subset of "multipart/mixed; boundary=1234", + /// "multipart/mixed", "multipart/*", and "*/*" but not "multipart/mixed; boundary=2345" or + /// "multipart/message; boundary=1234". + /// + public bool IsSubsetOf(MediaTypeHeaderValue otherMediaType) + { + if (otherMediaType == null) { - var index = 0; - return SingleValueParser.ParseValue(input, ref index)!; + return false; } - /// - /// Takes a media type, which can include parameters, and parses it into the and its associated parameters. - /// - /// The with the media type. The media type constructed here must not have an y - /// The parsed - /// True if the value was successfully parsed. - public static bool TryParse(StringSegment input, [NotNullWhen(true)] out MediaTypeHeaderValue? parsedValue) + // "text/plain" is a subset of "text/plain", "text/*" and "*/*". "*/*" is a subset only of "*/*". + return MatchesType(otherMediaType) && + MatchesSubtype(otherMediaType) && + MatchesParameters(otherMediaType); + } + + /// + /// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components, + /// while avoiding the cost of re-validating the components. + /// + /// A deep copy. + public MediaTypeHeaderValue Copy() + { + var other = new MediaTypeHeaderValue(); + other._mediaType = _mediaType; + + if (_parameters != null) { - var index = 0; - return SingleValueParser.TryParseValue(input, ref index, out parsedValue!); + other._parameters = new ObjectCollection( + _parameters.Select(item => item.Copy())); } + return other; + } - /// - /// Takes an of and parses it into the and its associated parameters. - /// - /// A list of media types - /// The parsed . - public static IList ParseList(IList? inputs) + /// + /// Performs a deep copy of this object and all of it's NameValueHeaderValue sub components, + /// while avoiding the cost of re-validating the components. This copy is read-only. + /// + /// A deep, read-only, copy. + public MediaTypeHeaderValue CopyAsReadOnly() + { + if (IsReadOnly) { - return MultipleValueParser.ParseValues(inputs); + return this; } - /// - /// Takes an of and parses it into the and its associated parameters. - /// Throws if there is invalid data in a string. - /// - /// A list of media types - /// The parsed . - public static IList ParseStrictList(IList? inputs) + var other = new MediaTypeHeaderValue(); + other._mediaType = _mediaType; + if (_parameters != null) { - return MultipleValueParser.ParseStrictValues(inputs); + other._parameters = new ObjectCollection( + _parameters.Select(item => item.CopyAsReadOnly()), isReadOnly: true); } + other._isReadOnly = true; + return other; + } - /// - /// Takes an of and parses it into the and its associated parameters. - /// - /// A list of media types - /// The parsed . - /// True if the value was successfully parsed. - public static bool TryParseList(IList? inputs, [NotNullWhen(true)] out IList? parsedValues) + /// + /// Gets a value indicating whether is a subset of + /// this in terms of type/subType. A "subset" is defined as the same or a more specific media type + /// according to the precedence described in https://www.ietf.org/rfc/rfc2068.txt section 14.1, Accept. + /// + /// The to compare. + /// + /// A value indicating whether is a subset of + /// this . + /// + /// + /// For example "multipart/mixed" is a subset of "multipart/mixed", + /// "multipart/*", and "*/*" but not "multipart/message." + /// + public bool MatchesMediaType(StringSegment otherMediaType) + { + if (StringSegment.IsNullOrEmpty(otherMediaType)) { - return MultipleValueParser.TryParseValues(inputs, out parsedValues); + return false; } + GetMediaTypeExpressionLength(otherMediaType, 0, out var mediaType); + + return MatchesType(mediaType) && MatchesSubtype(mediaType); + } + + /// + public override string ToString() + { + var builder = new StringBuilder(); + builder.Append(_mediaType.AsSpan()); + NameValueHeaderValue.ToString(_parameters, separator: ';', leadingSeparator: true, destination: builder); + return builder.ToString(); + } - /// - /// Takes an of and parses it into the and its associated parameters. - /// - /// A list of media types - /// The parsed . - /// True if the value was successfully parsed. - public static bool TryParseStrictList(IList? inputs, [NotNullWhen(true)] out IList? parsedValues) + /// + public override bool Equals(object? obj) + { + var other = obj as MediaTypeHeaderValue; + + if (other == null) { - return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); + return false; } - private static int GetMediaTypeLength(StringSegment input, int startIndex, out MediaTypeHeaderValue? parsedValue) - { - Contract.Requires(startIndex >= 0); + return _mediaType.Equals(other._mediaType, StringComparison.OrdinalIgnoreCase) && + HeaderUtilities.AreEqualCollections(_parameters, other._parameters); + } - parsedValue = null; + /// + public override int GetHashCode() + { + // The media-type string is case-insensitive. + return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_mediaType) ^ NameValueHeaderValue.GetHashCode(_parameters); + } - if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) - { - return 0; - } + /// + /// Takes a media type and parses it into the and its associated parameters. + /// + /// The with the media type. + /// The parsed . + public static MediaTypeHeaderValue Parse(StringSegment input) + { + var index = 0; + return SingleValueParser.ParseValue(input, ref index)!; + } - // Caller must remove leading whitespace. If not, we'll return 0. - var mediaTypeLength = MediaTypeHeaderValue.GetMediaTypeExpressionLength(input, startIndex, out var mediaType); + /// + /// Takes a media type, which can include parameters, and parses it into the and its associated parameters. + /// + /// The with the media type. The media type constructed here must not have an y + /// The parsed + /// True if the value was successfully parsed. + public static bool TryParse(StringSegment input, [NotNullWhen(true)] out MediaTypeHeaderValue? parsedValue) + { + var index = 0; + return SingleValueParser.TryParseValue(input, ref index, out parsedValue!); + } - if (mediaTypeLength == 0) - { - return 0; - } + /// + /// Takes an of and parses it into the and its associated parameters. + /// + /// A list of media types + /// The parsed . + public static IList ParseList(IList? inputs) + { + return MultipleValueParser.ParseValues(inputs); + } - var current = startIndex + mediaTypeLength; - current = current + HttpRuleParser.GetWhitespaceLength(input, current); - MediaTypeHeaderValue? mediaTypeHeader = null; + /// + /// Takes an of and parses it into the and its associated parameters. + /// Throws if there is invalid data in a string. + /// + /// A list of media types + /// The parsed . + public static IList ParseStrictList(IList? inputs) + { + return MultipleValueParser.ParseStrictValues(inputs); + } - // If we're not done and we have a parameter delimiter, then we have a list of parameters. - if ((current < input.Length) && (input[current] == ';')) - { - mediaTypeHeader = new MediaTypeHeaderValue(); - mediaTypeHeader._mediaType = mediaType; + /// + /// Takes an of and parses it into the and its associated parameters. + /// + /// A list of media types + /// The parsed . + /// True if the value was successfully parsed. + public static bool TryParseList(IList? inputs, [NotNullWhen(true)] out IList? parsedValues) + { + return MultipleValueParser.TryParseValues(inputs, out parsedValues); + } - current++; // skip delimiter. - var parameterLength = NameValueHeaderValue.GetNameValueListLength(input, current, ';', - mediaTypeHeader.Parameters); + /// + /// Takes an of and parses it into the and its associated parameters. + /// + /// A list of media types + /// The parsed . + /// True if the value was successfully parsed. + public static bool TryParseStrictList(IList? inputs, [NotNullWhen(true)] out IList? parsedValues) + { + return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); + } - parsedValue = mediaTypeHeader; - return current + parameterLength - startIndex; - } + private static int GetMediaTypeLength(StringSegment input, int startIndex, out MediaTypeHeaderValue? parsedValue) + { + Contract.Requires(startIndex >= 0); - // We have a media type without parameters. - mediaTypeHeader = new MediaTypeHeaderValue(); - mediaTypeHeader._mediaType = mediaType; - parsedValue = mediaTypeHeader; - return current - startIndex; - } + parsedValue = null; - private static int GetMediaTypeExpressionLength(StringSegment input, int startIndex, out StringSegment mediaType) + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) { - Contract.Requires((input.Length > 0) && (startIndex < input.Length)); + return 0; + } - // This method just parses the "type/subtype" string, it does not parse parameters. - mediaType = null; + // Caller must remove leading whitespace. If not, we'll return 0. + var mediaTypeLength = MediaTypeHeaderValue.GetMediaTypeExpressionLength(input, startIndex, out var mediaType); - // Parse the type, i.e. in media type string "/; param1=value1; param2=value2" - var typeLength = HttpRuleParser.GetTokenLength(input, startIndex); + if (mediaTypeLength == 0) + { + return 0; + } - if (typeLength == 0) - { - return 0; - } + var current = startIndex + mediaTypeLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + MediaTypeHeaderValue? mediaTypeHeader = null; - var current = startIndex + typeLength; - current = current + HttpRuleParser.GetWhitespaceLength(input, current); + // If we're not done and we have a parameter delimiter, then we have a list of parameters. + if ((current < input.Length) && (input[current] == ';')) + { + mediaTypeHeader = new MediaTypeHeaderValue(); + mediaTypeHeader._mediaType = mediaType; - // Parse the separator between type and subtype - if ((current >= input.Length) || (input[current] != '/')) - { - return 0; - } current++; // skip delimiter. - current = current + HttpRuleParser.GetWhitespaceLength(input, current); + var parameterLength = NameValueHeaderValue.GetNameValueListLength(input, current, ';', + mediaTypeHeader.Parameters); - // Parse the subtype, i.e. in media type string "/; param1=value1; param2=value2" - var subtypeLength = HttpRuleParser.GetTokenLength(input, current); + parsedValue = mediaTypeHeader; + return current + parameterLength - startIndex; + } - if (subtypeLength == 0) - { - return 0; - } + // We have a media type without parameters. + mediaTypeHeader = new MediaTypeHeaderValue(); + mediaTypeHeader._mediaType = mediaType; + parsedValue = mediaTypeHeader; + return current - startIndex; + } - // If there is no whitespace between and in / get the media type using - // one Substring call. Otherwise get substrings for and and combine them. - var mediaTypeLength = current + subtypeLength - startIndex; - if (typeLength + subtypeLength + 1 == mediaTypeLength) - { - mediaType = input.Subsegment(startIndex, mediaTypeLength); - } - else - { - mediaType = string.Concat(input.AsSpan().Slice(startIndex, typeLength), "/", input.AsSpan().Slice(current, subtypeLength)); - } + private static int GetMediaTypeExpressionLength(StringSegment input, int startIndex, out StringSegment mediaType) + { + Contract.Requires((input.Length > 0) && (startIndex < input.Length)); - return mediaTypeLength; - } + // This method just parses the "type/subtype" string, it does not parse parameters. + mediaType = null; - private static void CheckMediaTypeFormat(StringSegment mediaType, string parameterName) - { - if (StringSegment.IsNullOrEmpty(mediaType)) - { - throw new ArgumentException("An empty string is not allowed.", parameterName); - } + // Parse the type, i.e. in media type string "/; param1=value1; param2=value2" + var typeLength = HttpRuleParser.GetTokenLength(input, startIndex); - // When adding values using strongly typed objects, no leading/trailing LWS (whitespace) is allowed. - // Also no LWS between type and subtype is allowed. - var mediaTypeLength = GetMediaTypeExpressionLength(mediaType, 0, out var tempMediaType); - if ((mediaTypeLength == 0) || (tempMediaType.Length != mediaType.Length)) - { - throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid media type '{0}'.", mediaType)); - } + if (typeLength == 0) + { + return 0; } - private bool MatchesType(MediaTypeHeaderValue set) + var current = startIndex + typeLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + // Parse the separator between type and subtype + if ((current >= input.Length) || (input[current] != '/')) { - return set.MatchesAllTypes || - set.Type.Equals(Type, StringComparison.OrdinalIgnoreCase); + return 0; } + current++; // skip delimiter. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); - private bool MatchesType(StringSegment mediaType) - { - var type = mediaType.Subsegment(0, mediaType.IndexOf(ForwardSlashCharacter)); + // Parse the subtype, i.e. in media type string "/; param1=value1; param2=value2" + var subtypeLength = HttpRuleParser.GetTokenLength(input, current); - return MatchesAllTypes || - Type.Equals(type, StringComparison.OrdinalIgnoreCase); + if (subtypeLength == 0) + { + return 0; } - private bool MatchesSubtype(MediaTypeHeaderValue set) + // If there is no whitespace between and in / get the media type using + // one Substring call. Otherwise get substrings for and and combine them. + var mediaTypeLength = current + subtypeLength - startIndex; + if (typeLength + subtypeLength + 1 == mediaTypeLength) { - if (set.MatchesAllSubTypes) - { - return true; - } + mediaType = input.Subsegment(startIndex, mediaTypeLength); + } + else + { + mediaType = string.Concat(input.AsSpan().Slice(startIndex, typeLength), "/", input.AsSpan().Slice(current, subtypeLength)); + } - if (set.Suffix.HasValue) - { - if (Suffix.HasValue) - { - return MatchesSubtypeWithoutSuffix(set) && MatchesSubtypeSuffix(set); - } - else - { - return false; - } - } - else - { - // If this subtype or suffix matches the subtype of the set, - // it is considered a subtype. - // Ex: application/json > application/val+json - return MatchesEitherSubtypeOrSuffix(set); - } + return mediaTypeLength; + } + + private static void CheckMediaTypeFormat(StringSegment mediaType, string parameterName) + { + if (StringSegment.IsNullOrEmpty(mediaType)) + { + throw new ArgumentException("An empty string is not allowed.", parameterName); } - private bool MatchesSubtype(StringSegment mediaType) + // When adding values using strongly typed objects, no leading/trailing LWS (whitespace) is allowed. + // Also no LWS between type and subtype is allowed. + var mediaTypeLength = GetMediaTypeExpressionLength(mediaType, 0, out var tempMediaType); + if ((mediaTypeLength == 0) || (tempMediaType.Length != mediaType.Length)) { - if (MatchesAllSubTypes) - { - return true; - } + throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid media type '{0}'.", mediaType)); + } + } - var subType = mediaType.Subsegment(mediaType.IndexOf(ForwardSlashCharacter) + 1); + private bool MatchesType(MediaTypeHeaderValue set) + { + return set.MatchesAllTypes || + set.Type.Equals(Type, StringComparison.OrdinalIgnoreCase); + } - StringSegment suffix; - var startOfSuffix = subType.LastIndexOf(PlusCharacter); - if (startOfSuffix == -1) - { - suffix = default(StringSegment); - } - else - { - suffix = subType.Subsegment(startOfSuffix + 1); - } + private bool MatchesType(StringSegment mediaType) + { + var type = mediaType.Subsegment(0, mediaType.IndexOf(ForwardSlashCharacter)); + + return MatchesAllTypes || + Type.Equals(type, StringComparison.OrdinalIgnoreCase); + } + private bool MatchesSubtype(MediaTypeHeaderValue set) + { + if (set.MatchesAllSubTypes) + { + return true; + } + + if (set.Suffix.HasValue) + { if (Suffix.HasValue) { - if (suffix.HasValue) - { - return MatchesSubtypeWithoutSuffix(subType, startOfSuffix) && MatchesSubtypeSuffix(suffix); - } - else - { - return false; - } + return MatchesSubtypeWithoutSuffix(set) && MatchesSubtypeSuffix(set); } else { - // If this subtype or suffix matches the subtype of the mediaType, - // it is considered a subtype. - // Ex: application/json > application/val+json - return MatchesEitherSubtypeOrSuffix(subType, suffix); + return false; } } + else + { + // If this subtype or suffix matches the subtype of the set, + // it is considered a subtype. + // Ex: application/json > application/val+json + return MatchesEitherSubtypeOrSuffix(set); + } + } - private bool MatchesSubtypeWithoutSuffix(MediaTypeHeaderValue set) + private bool MatchesSubtype(StringSegment mediaType) + { + if (MatchesAllSubTypes) { - return set.MatchesAllSubTypesWithoutSuffix || - set.SubTypeWithoutSuffix.Equals(SubTypeWithoutSuffix, StringComparison.OrdinalIgnoreCase); + return true; } - private bool MatchesSubtypeWithoutSuffix(StringSegment subType, int startOfSuffix) + var subType = mediaType.Subsegment(mediaType.IndexOf(ForwardSlashCharacter) + 1); + + StringSegment suffix; + var startOfSuffix = subType.LastIndexOf(PlusCharacter); + if (startOfSuffix == -1) { - StringSegment subTypeWithoutSuffix; - if (startOfSuffix == -1) + suffix = default(StringSegment); + } + else + { + suffix = subType.Subsegment(startOfSuffix + 1); + } + + if (Suffix.HasValue) + { + if (suffix.HasValue) { - subTypeWithoutSuffix = subType; + return MatchesSubtypeWithoutSuffix(subType, startOfSuffix) && MatchesSubtypeSuffix(suffix); } else { - subTypeWithoutSuffix = subType.Subsegment(0, startOfSuffix); + return false; } - return SubTypeWithoutSuffix.Equals(WildcardString, StringComparison.OrdinalIgnoreCase) || - SubTypeWithoutSuffix.Equals(subTypeWithoutSuffix, StringComparison.OrdinalIgnoreCase); } - - private bool MatchesEitherSubtypeOrSuffix(MediaTypeHeaderValue set) + else { - return set.SubType.Equals(SubType, StringComparison.OrdinalIgnoreCase) || - set.SubType.Equals(Suffix, StringComparison.OrdinalIgnoreCase); + // If this subtype or suffix matches the subtype of the mediaType, + // it is considered a subtype. + // Ex: application/json > application/val+json + return MatchesEitherSubtypeOrSuffix(subType, suffix); } + } - private bool MatchesEitherSubtypeOrSuffix(StringSegment subType, StringSegment suffix) + private bool MatchesSubtypeWithoutSuffix(MediaTypeHeaderValue set) + { + return set.MatchesAllSubTypesWithoutSuffix || + set.SubTypeWithoutSuffix.Equals(SubTypeWithoutSuffix, StringComparison.OrdinalIgnoreCase); + } + + private bool MatchesSubtypeWithoutSuffix(StringSegment subType, int startOfSuffix) + { + StringSegment subTypeWithoutSuffix; + if (startOfSuffix == -1) { - return subType.Equals(SubType, StringComparison.OrdinalIgnoreCase) || - SubType.Equals(suffix, StringComparison.OrdinalIgnoreCase); + subTypeWithoutSuffix = subType; } + else + { + subTypeWithoutSuffix = subType.Subsegment(0, startOfSuffix); + } + return SubTypeWithoutSuffix.Equals(WildcardString, StringComparison.OrdinalIgnoreCase) || + SubTypeWithoutSuffix.Equals(subTypeWithoutSuffix, StringComparison.OrdinalIgnoreCase); + } + + private bool MatchesEitherSubtypeOrSuffix(MediaTypeHeaderValue set) + { + return set.SubType.Equals(SubType, StringComparison.OrdinalIgnoreCase) || + set.SubType.Equals(Suffix, StringComparison.OrdinalIgnoreCase); + } - private bool MatchesParameters(MediaTypeHeaderValue set) + private bool MatchesEitherSubtypeOrSuffix(StringSegment subType, StringSegment suffix) + { + return subType.Equals(SubType, StringComparison.OrdinalIgnoreCase) || + SubType.Equals(suffix, StringComparison.OrdinalIgnoreCase); + } + + private bool MatchesParameters(MediaTypeHeaderValue set) + { + if (set._parameters != null && set._parameters.Count != 0) { - if (set._parameters != null && set._parameters.Count != 0) + // Make sure all parameters in the potential superset are included locally. Fine to have additional + // parameters locally; they make this one more specific. + foreach (var parameter in set._parameters) { - // Make sure all parameters in the potential superset are included locally. Fine to have additional - // parameters locally; they make this one more specific. - foreach (var parameter in set._parameters) + if (parameter.Name.Equals(WildcardString, StringComparison.OrdinalIgnoreCase)) { - if (parameter.Name.Equals(WildcardString, StringComparison.OrdinalIgnoreCase)) - { - // A parameter named "*" has no effect on media type matching, as it is only used as an indication - // that the entire media type string should be treated as a wildcard. - continue; - } - - if (parameter.Name.Equals(QualityString, StringComparison.OrdinalIgnoreCase)) - { - // "q" and later parameters are not involved in media type matching. Quoting the RFC: The first - // "q" parameter (if any) separates the media-range parameter(s) from the accept-params. - break; - } - - var localParameter = NameValueHeaderValue.Find(_parameters, parameter.Name); - if (localParameter == null) - { - // Not found. - return false; - } - - if (!StringSegment.Equals(parameter.Value, localParameter.Value, StringComparison.OrdinalIgnoreCase)) - { - return false; - } + // A parameter named "*" has no effect on media type matching, as it is only used as an indication + // that the entire media type string should be treated as a wildcard. + continue; + } + + if (parameter.Name.Equals(QualityString, StringComparison.OrdinalIgnoreCase)) + { + // "q" and later parameters are not involved in media type matching. Quoting the RFC: The first + // "q" parameter (if any) separates the media-range parameter(s) from the accept-params. + break; + } + + var localParameter = NameValueHeaderValue.Find(_parameters, parameter.Name); + if (localParameter == null) + { + // Not found. + return false; + } + + if (!StringSegment.Equals(parameter.Value, localParameter.Value, StringComparison.OrdinalIgnoreCase)) + { + return false; } } - return true; } + return true; + } - private bool MatchesSubtypeSuffix(MediaTypeHeaderValue set) - { - // We don't have support for wildcards on suffixes alone (e.g., "application/entity+*") - // because there's no clear use case for it. - return set.Suffix.Equals(Suffix, StringComparison.OrdinalIgnoreCase); - } + private bool MatchesSubtypeSuffix(MediaTypeHeaderValue set) + { + // We don't have support for wildcards on suffixes alone (e.g., "application/entity+*") + // because there's no clear use case for it. + return set.Suffix.Equals(Suffix, StringComparison.OrdinalIgnoreCase); + } - private bool MatchesSubtypeSuffix(StringSegment suffix) - { - // We don't have support for wildcards on suffixes alone (e.g., "application/entity+*") - // because there's no clear use case for it. - return Suffix.Equals(suffix, StringComparison.OrdinalIgnoreCase); - } + private bool MatchesSubtypeSuffix(StringSegment suffix) + { + // We don't have support for wildcards on suffixes alone (e.g., "application/entity+*") + // because there's no clear use case for it. + return Suffix.Equals(suffix, StringComparison.OrdinalIgnoreCase); } } diff --git a/src/Http/Headers/src/MediaTypeHeaderValueComparer.cs b/src/Http/Headers/src/MediaTypeHeaderValueComparer.cs index d5f25cc68f..df46786efc 100644 --- a/src/Http/Headers/src/MediaTypeHeaderValueComparer.cs +++ b/src/Http/Headers/src/MediaTypeHeaderValueComparer.cs @@ -4,136 +4,135 @@ using System; using System.Collections.Generic; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +/// +/// Implementation of that can compare accept media type header fields +/// based on their quality values (a.k.a q-values). +/// +public class MediaTypeHeaderValueComparer : IComparer { + private MediaTypeHeaderValueComparer() + { + } + /// - /// Implementation of that can compare accept media type header fields - /// based on their quality values (a.k.a q-values). + /// Gets the instance. /// - public class MediaTypeHeaderValueComparer : IComparer + public static MediaTypeHeaderValueComparer QualityComparer { get; } = new MediaTypeHeaderValueComparer(); + + /// + /// + /// Performs comparisons based on the arguments' quality values + /// (aka their "q-value"). Values with identical q-values are considered equal (i.e. the result is 0) + /// with the exception that suffixed subtype wildcards are considered less than subtype wildcards, subtype wildcards + /// are considered less than specific media types and full wildcards are considered less than + /// subtype wildcards. This allows callers to sort a sequence of following + /// their q-values in the order of specific media types, subtype wildcards, and last any full wildcards. + /// + /// + /// If we had a list of media types (comma separated): { text/*;q=0.8, text/*+json;q=0.8, */*;q=1, */*;q=0.8, text/plain;q=0.8 } + /// Sorting them using Compare would return: { */*;q=0.8, text/*;q=0.8, text/*+json;q=0.8, text/plain;q=0.8, */*;q=1 } + /// + public int Compare(MediaTypeHeaderValue? mediaType1, MediaTypeHeaderValue? mediaType2) { - private MediaTypeHeaderValueComparer() + if (object.ReferenceEquals(mediaType1, mediaType2)) { + return 0; } - /// - /// Gets the instance. - /// - public static MediaTypeHeaderValueComparer QualityComparer { get; } = new MediaTypeHeaderValueComparer(); - - /// - /// - /// Performs comparisons based on the arguments' quality values - /// (aka their "q-value"). Values with identical q-values are considered equal (i.e. the result is 0) - /// with the exception that suffixed subtype wildcards are considered less than subtype wildcards, subtype wildcards - /// are considered less than specific media types and full wildcards are considered less than - /// subtype wildcards. This allows callers to sort a sequence of following - /// their q-values in the order of specific media types, subtype wildcards, and last any full wildcards. - /// - /// - /// If we had a list of media types (comma separated): { text/*;q=0.8, text/*+json;q=0.8, */*;q=1, */*;q=0.8, text/plain;q=0.8 } - /// Sorting them using Compare would return: { */*;q=0.8, text/*;q=0.8, text/*+json;q=0.8, text/plain;q=0.8, */*;q=1 } - /// - public int Compare(MediaTypeHeaderValue? mediaType1, MediaTypeHeaderValue? mediaType2) + if (mediaType1 is null) { - if (object.ReferenceEquals(mediaType1, mediaType2)) - { - return 0; - } - - if (mediaType1 is null) - { - return -1; - } + return -1; + } - if (mediaType2 is null) - { - return 1; - } + if (mediaType2 is null) + { + return 1; + } - var returnValue = CompareBasedOnQualityFactor(mediaType1, mediaType2); + var returnValue = CompareBasedOnQualityFactor(mediaType1, mediaType2); - if (returnValue == 0) + if (returnValue == 0) + { + if (!mediaType1.Type.Equals(mediaType2.Type, StringComparison.OrdinalIgnoreCase)) { - if (!mediaType1.Type.Equals(mediaType2.Type, StringComparison.OrdinalIgnoreCase)) + if (mediaType1.MatchesAllTypes) + { + return -1; + } + else if (mediaType2.MatchesAllTypes) + { + return 1; + } + else if (mediaType1.MatchesAllSubTypes && !mediaType2.MatchesAllSubTypes) { - if (mediaType1.MatchesAllTypes) - { - return -1; - } - else if (mediaType2.MatchesAllTypes) - { - return 1; - } - else if (mediaType1.MatchesAllSubTypes && !mediaType2.MatchesAllSubTypes) - { - return -1; - } - else if (!mediaType1.MatchesAllSubTypes && mediaType2.MatchesAllSubTypes) - { - return 1; - } - else if (mediaType1.MatchesAllSubTypesWithoutSuffix && !mediaType2.MatchesAllSubTypesWithoutSuffix) - { - return -1; - } - else if (!mediaType1.MatchesAllSubTypesWithoutSuffix && mediaType2.MatchesAllSubTypesWithoutSuffix) - { - return 1; - } + return -1; } - else if (!mediaType1.SubType.Equals(mediaType2.SubType, StringComparison.OrdinalIgnoreCase)) + else if (!mediaType1.MatchesAllSubTypes && mediaType2.MatchesAllSubTypes) { - if (mediaType1.MatchesAllSubTypes) - { - return -1; - } - else if (mediaType2.MatchesAllSubTypes) - { - return 1; - } - else if (mediaType1.MatchesAllSubTypesWithoutSuffix && !mediaType2.MatchesAllSubTypesWithoutSuffix) - { - return -1; - } - else if (!mediaType1.MatchesAllSubTypesWithoutSuffix && mediaType2.MatchesAllSubTypesWithoutSuffix) - { - return 1; - } + return 1; } - else if (!mediaType1.Suffix.Equals(mediaType2.Suffix, StringComparison.OrdinalIgnoreCase)) + else if (mediaType1.MatchesAllSubTypesWithoutSuffix && !mediaType2.MatchesAllSubTypesWithoutSuffix) { - if (mediaType1.MatchesAllSubTypesWithoutSuffix) - { - return -1; - } - else if (mediaType2.MatchesAllSubTypesWithoutSuffix) - { - return 1; - } + return -1; + } + else if (!mediaType1.MatchesAllSubTypesWithoutSuffix && mediaType2.MatchesAllSubTypesWithoutSuffix) + { + return 1; } } - - return returnValue; - } - - private static int CompareBasedOnQualityFactor( - MediaTypeHeaderValue mediaType1, - MediaTypeHeaderValue mediaType2) - { - var mediaType1Quality = mediaType1.Quality ?? HeaderQuality.Match; - var mediaType2Quality = mediaType2.Quality ?? HeaderQuality.Match; - var qualityDifference = mediaType1Quality - mediaType2Quality; - if (qualityDifference < 0) + else if (!mediaType1.SubType.Equals(mediaType2.SubType, StringComparison.OrdinalIgnoreCase)) { - return -1; + if (mediaType1.MatchesAllSubTypes) + { + return -1; + } + else if (mediaType2.MatchesAllSubTypes) + { + return 1; + } + else if (mediaType1.MatchesAllSubTypesWithoutSuffix && !mediaType2.MatchesAllSubTypesWithoutSuffix) + { + return -1; + } + else if (!mediaType1.MatchesAllSubTypesWithoutSuffix && mediaType2.MatchesAllSubTypesWithoutSuffix) + { + return 1; + } } - else if (qualityDifference > 0) + else if (!mediaType1.Suffix.Equals(mediaType2.Suffix, StringComparison.OrdinalIgnoreCase)) { - return 1; + if (mediaType1.MatchesAllSubTypesWithoutSuffix) + { + return -1; + } + else if (mediaType2.MatchesAllSubTypesWithoutSuffix) + { + return 1; + } } + } - return 0; + return returnValue; + } + + private static int CompareBasedOnQualityFactor( + MediaTypeHeaderValue mediaType1, + MediaTypeHeaderValue mediaType2) + { + var mediaType1Quality = mediaType1.Quality ?? HeaderQuality.Match; + var mediaType2Quality = mediaType2.Quality ?? HeaderQuality.Match; + var qualityDifference = mediaType1Quality - mediaType2Quality; + if (qualityDifference < 0) + { + return -1; } + else if (qualityDifference > 0) + { + return 1; + } + + return 0; } } diff --git a/src/Http/Headers/src/NameValueHeaderValue.cs b/src/Http/Headers/src/NameValueHeaderValue.cs index 86988652a0..ef58172196 100644 --- a/src/Http/Headers/src/NameValueHeaderValue.cs +++ b/src/Http/Headers/src/NameValueHeaderValue.cs @@ -9,489 +9,488 @@ using System.Globalization; using System.Text; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +// According to the RFC, in places where a "parameter" is required, the value is mandatory +// (e.g. Media-Type, Accept). However, we don't introduce a dedicated type for it. So NameValueHeaderValue supports +// name-only values in addition to name/value pairs. +/// +/// Represents a name/value pair used in various headers as defined in RFC 2616. +/// +public class NameValueHeaderValue { - // According to the RFC, in places where a "parameter" is required, the value is mandatory - // (e.g. Media-Type, Accept). However, we don't introduce a dedicated type for it. So NameValueHeaderValue supports - // name-only values in addition to name/value pairs. + private static readonly HttpHeaderParser SingleValueParser + = new GenericHeaderParser(false, GetNameValueLength); + internal static readonly HttpHeaderParser MultipleValueParser + = new GenericHeaderParser(true, GetNameValueLength); + + private StringSegment _name; + private StringSegment _value; + private bool _isReadOnly; + + private NameValueHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + /// - /// Represents a name/value pair used in various headers as defined in RFC 2616. + /// Initializes a new instance of . /// - public class NameValueHeaderValue + /// The header name. + public NameValueHeaderValue(StringSegment name) + : this(name, null) { - private static readonly HttpHeaderParser SingleValueParser - = new GenericHeaderParser(false, GetNameValueLength); - internal static readonly HttpHeaderParser MultipleValueParser - = new GenericHeaderParser(true, GetNameValueLength); + } - private StringSegment _name; - private StringSegment _value; - private bool _isReadOnly; + /// + /// Initializes a new instance of . + /// + /// The header name. + /// The header value. + public NameValueHeaderValue(StringSegment name, StringSegment value) + { + CheckNameValueFormat(name, value); - private NameValueHeaderValue() - { - // Used by the parser to create a new instance of this type. - } + _name = name; + _value = value; + } - /// - /// Initializes a new instance of . - /// - /// The header name. - public NameValueHeaderValue(StringSegment name) - : this(name, null) - { - } + /// + /// Gets the header name. + /// + public StringSegment Name + { + get { return _name; } + } - /// - /// Initializes a new instance of . - /// - /// The header name. - /// The header value. - public NameValueHeaderValue(StringSegment name, StringSegment value) + /// + /// Gets or sets the header value. + /// + public StringSegment Value + { + get { return _value; } + set { - CheckNameValueFormat(name, value); - - _name = name; + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + CheckValueFormat(value); _value = value; } + } - /// - /// Gets the header name. - /// - public StringSegment Name - { - get { return _name; } - } - - /// - /// Gets or sets the header value. - /// - public StringSegment Value - { - get { return _value; } - set - { - HeaderUtilities.ThrowIfReadOnly(IsReadOnly); - CheckValueFormat(value); - _value = value; - } - } - - /// - /// Gets a value that determines if this header is read only. - /// - public bool IsReadOnly { get { return _isReadOnly; } } + /// + /// Gets a value that determines if this header is read only. + /// + public bool IsReadOnly { get { return _isReadOnly; } } - /// - /// Provides a copy of this object without the cost of re-validating the values. - /// - /// A copy. - public NameValueHeaderValue Copy() + /// + /// Provides a copy of this object without the cost of re-validating the values. + /// + /// A copy. + public NameValueHeaderValue Copy() + { + return new NameValueHeaderValue() { - return new NameValueHeaderValue() - { - _name = _name, - _value = _value - }; - } + _name = _name, + _value = _value + }; + } - /// - /// Provides a copy of this instance while making it immutable. - /// - /// The readonly . - public NameValueHeaderValue CopyAsReadOnly() + /// + /// Provides a copy of this instance while making it immutable. + /// + /// The readonly . + public NameValueHeaderValue CopyAsReadOnly() + { + if (IsReadOnly) { - if (IsReadOnly) - { - return this; - } - - return new NameValueHeaderValue() - { - _name = _name, - _value = _value, - _isReadOnly = true - }; + return this; } - /// - public override int GetHashCode() + return new NameValueHeaderValue() { - Contract.Assert(_name != null); - - var nameHashCode = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name); + _name = _name, + _value = _value, + _isReadOnly = true + }; + } - if (!StringSegment.IsNullOrEmpty(_value)) - { - // If we have a quoted-string, then just use the hash code. If we have a token, convert to lowercase - // and retrieve the hash code. - if (_value[0] == '"') - { - return nameHashCode ^ _value.GetHashCode(); - } - - return nameHashCode ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value); - } + /// + public override int GetHashCode() + { + Contract.Assert(_name != null); - return nameHashCode; - } + var nameHashCode = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name); - /// - public override bool Equals(object? obj) + if (!StringSegment.IsNullOrEmpty(_value)) { - var other = obj as NameValueHeaderValue; - - if (other == null) + // If we have a quoted-string, then just use the hash code. If we have a token, convert to lowercase + // and retrieve the hash code. + if (_value[0] == '"') { - return false; + return nameHashCode ^ _value.GetHashCode(); } - if (!_name.Equals(other._name, StringComparison.OrdinalIgnoreCase)) - { - return false; - } + return nameHashCode ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value); + } - // RFC2616: 14.20: unquoted tokens should use case-INsensitive comparison; quoted-strings should use - // case-sensitive comparison. The RFC doesn't mention how to compare quoted-strings outside the "Expect" - // header. We treat all quoted-strings the same: case-sensitive comparison. + return nameHashCode; + } - if (StringSegment.IsNullOrEmpty(_value)) - { - return StringSegment.IsNullOrEmpty(other._value); - } + /// + public override bool Equals(object? obj) + { + var other = obj as NameValueHeaderValue; - if (_value[0] == '"') - { - // We have a quoted string, so we need to do case-sensitive comparison. - return (_value.Equals(other._value, StringComparison.Ordinal)); - } - else - { - return (_value.Equals(other._value, StringComparison.OrdinalIgnoreCase)); - } + if (other == null) + { + return false; } - /// - /// If the value is a quoted-string as defined by the RFC specification, - /// removes quotes and unescapes backslashes and quotes. - /// - /// An unescaped version of . - public StringSegment GetUnescapedValue() + if (!_name.Equals(other._name, StringComparison.OrdinalIgnoreCase)) { - if (!HeaderUtilities.IsQuoted(_value)) - { - return _value; - } - return HeaderUtilities.UnescapeAsQuotedString(_value); + return false; } - /// - /// Sets after it has been quoted as defined by the RFC specification. - /// - /// - public void SetAndEscapeValue(StringSegment value) + // RFC2616: 14.20: unquoted tokens should use case-INsensitive comparison; quoted-strings should use + // case-sensitive comparison. The RFC doesn't mention how to compare quoted-strings outside the "Expect" + // header. We treat all quoted-strings the same: case-sensitive comparison. + + if (StringSegment.IsNullOrEmpty(_value)) { - HeaderUtilities.ThrowIfReadOnly(IsReadOnly); - if (StringSegment.IsNullOrEmpty(value) || (GetValueLength(value, 0) == value.Length)) - { - _value = value; - } - else - { - Value = HeaderUtilities.EscapeAsQuotedString(value); - } + return StringSegment.IsNullOrEmpty(other._value); } - /// - /// Parses as a value. - /// - /// The values to parse. - /// The parsed values. - public static NameValueHeaderValue Parse(StringSegment input) + if (_value[0] == '"') { - var index = 0; - return SingleValueParser.ParseValue(input, ref index)!; + // We have a quoted string, so we need to do case-sensitive comparison. + return (_value.Equals(other._value, StringComparison.Ordinal)); } - - /// - /// Attempts to parse the specified as a . - /// - /// The value to parse. - /// The parsed value. - /// if input is a valid , otherwise . - public static bool TryParse(StringSegment input, [NotNullWhen(true)] out NameValueHeaderValue? parsedValue) + else { - var index = 0; - return SingleValueParser.TryParseValue(input, ref index, out parsedValue!); + return (_value.Equals(other._value, StringComparison.OrdinalIgnoreCase)); } + } - /// - /// Parses a sequence of inputs as a sequence of values. - /// - /// The values to parse. - /// The parsed values. - public static IList ParseList(IList? input) + /// + /// If the value is a quoted-string as defined by the RFC specification, + /// removes quotes and unescapes backslashes and quotes. + /// + /// An unescaped version of . + public StringSegment GetUnescapedValue() + { + if (!HeaderUtilities.IsQuoted(_value)) { - return MultipleValueParser.ParseValues(input); + return _value; } + return HeaderUtilities.UnescapeAsQuotedString(_value); + } - /// - /// Parses a sequence of inputs as a sequence of values using string parsing rules. - /// - /// The values to parse. - /// The parsed values. - public static IList ParseStrictList(IList? input) + /// + /// Sets after it has been quoted as defined by the RFC specification. + /// + /// + public void SetAndEscapeValue(StringSegment value) + { + HeaderUtilities.ThrowIfReadOnly(IsReadOnly); + if (StringSegment.IsNullOrEmpty(value) || (GetValueLength(value, 0) == value.Length)) { - return MultipleValueParser.ParseStrictValues(input); + _value = value; } - - /// - /// Attempts to parse the sequence of values as a sequence of . - /// - /// The values to parse. - /// The parsed values. - /// if all inputs are valid , otherwise . - public static bool TryParseList(IList? input, [NotNullWhen(true)] out IList? parsedValues) + else { - return MultipleValueParser.TryParseValues(input, out parsedValues); + Value = HeaderUtilities.EscapeAsQuotedString(value); } + } + + /// + /// Parses as a value. + /// + /// The values to parse. + /// The parsed values. + public static NameValueHeaderValue Parse(StringSegment input) + { + var index = 0; + return SingleValueParser.ParseValue(input, ref index)!; + } + + /// + /// Attempts to parse the specified as a . + /// + /// The value to parse. + /// The parsed value. + /// if input is a valid , otherwise . + public static bool TryParse(StringSegment input, [NotNullWhen(true)] out NameValueHeaderValue? parsedValue) + { + var index = 0; + return SingleValueParser.TryParseValue(input, ref index, out parsedValue!); + } + + /// + /// Parses a sequence of inputs as a sequence of values. + /// + /// The values to parse. + /// The parsed values. + public static IList ParseList(IList? input) + { + return MultipleValueParser.ParseValues(input); + } - /// - /// Attempts to parse the sequence of values as a sequence of using string parsing rules. - /// - /// The values to parse. - /// The parsed values. - /// if all inputs are valid , otherwise . - public static bool TryParseStrictList(IList? input, [NotNullWhen(true)] out IList? parsedValues) + /// + /// Parses a sequence of inputs as a sequence of values using string parsing rules. + /// + /// The values to parse. + /// The parsed values. + public static IList ParseStrictList(IList? input) + { + return MultipleValueParser.ParseStrictValues(input); + } + + /// + /// Attempts to parse the sequence of values as a sequence of . + /// + /// The values to parse. + /// The parsed values. + /// if all inputs are valid , otherwise . + public static bool TryParseList(IList? input, [NotNullWhen(true)] out IList? parsedValues) + { + return MultipleValueParser.TryParseValues(input, out parsedValues); + } + + /// + /// Attempts to parse the sequence of values as a sequence of using string parsing rules. + /// + /// The values to parse. + /// The parsed values. + /// if all inputs are valid , otherwise . + public static bool TryParseStrictList(IList? input, [NotNullWhen(true)] out IList? parsedValues) + { + return MultipleValueParser.TryParseStrictValues(input, out parsedValues); + } + + /// + public override string ToString() + { + if (!StringSegment.IsNullOrEmpty(_value)) { - return MultipleValueParser.TryParseStrictValues(input, out parsedValues); + return _name + "=" + _value; } + return _name.ToString(); + } - /// - public override string ToString() + internal static void ToString( + IList? values, + char separator, + bool leadingSeparator, + StringBuilder destination) + { + Contract.Assert(destination != null); + + if ((values == null) || (values.Count == 0)) { - if (!StringSegment.IsNullOrEmpty(_value)) - { - return _name + "=" + _value; - } - return _name.ToString(); + return; } - internal static void ToString( - IList? values, - char separator, - bool leadingSeparator, - StringBuilder destination) + for (var i = 0; i < values.Count; i++) { - Contract.Assert(destination != null); - - if ((values == null) || (values.Count == 0)) + if (leadingSeparator || (destination.Length > 0)) { - return; + destination.Append(separator); + destination.Append(' '); } - - for (var i = 0; i < values.Count; i++) + destination.Append(values[i].Name.AsSpan()); + if (!StringSegment.IsNullOrEmpty(values[i].Value)) { - if (leadingSeparator || (destination.Length > 0)) - { - destination.Append(separator); - destination.Append(' '); - } - destination.Append(values[i].Name.AsSpan()); - if (!StringSegment.IsNullOrEmpty(values[i].Value)) - { - destination.Append('='); - destination.Append(values[i].Value.AsSpan()); - } + destination.Append('='); + destination.Append(values[i].Value.AsSpan()); } } + } - internal static string? ToString(IList? values, char separator, bool leadingSeparator) + internal static string? ToString(IList? values, char separator, bool leadingSeparator) + { + if ((values == null) || (values.Count == 0)) { - if ((values == null) || (values.Count == 0)) - { - return null; - } + return null; + } - var sb = new StringBuilder(); + var sb = new StringBuilder(); - ToString(values, separator, leadingSeparator, sb); + ToString(values, separator, leadingSeparator, sb); - return sb.ToString(); - } + return sb.ToString(); + } - internal static int GetHashCode(IList? values) + internal static int GetHashCode(IList? values) + { + if ((values == null) || (values.Count == 0)) { - if ((values == null) || (values.Count == 0)) - { - return 0; - } - - var result = 0; - for (var i = 0; i < values.Count; i++) - { - result = result ^ values[i].GetHashCode(); - } - return result; + return 0; } - private static int GetNameValueLength(StringSegment input, int startIndex, out NameValueHeaderValue? parsedValue) + var result = 0; + for (var i = 0; i < values.Count; i++) { - Contract.Requires(startIndex >= 0); - - parsedValue = null; + result = result ^ values[i].GetHashCode(); + } + return result; + } - if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) - { - return 0; - } + private static int GetNameValueLength(StringSegment input, int startIndex, out NameValueHeaderValue? parsedValue) + { + Contract.Requires(startIndex >= 0); - // Parse the name, i.e. in name/value string "=". Caller must remove - // leading whitespaces. - var nameLength = HttpRuleParser.GetTokenLength(input, startIndex); + parsedValue = null; - if (nameLength == 0) - { - return 0; - } - - var name = input.Subsegment(startIndex, nameLength); - var current = startIndex + nameLength; - current = current + HttpRuleParser.GetWhitespaceLength(input, current); + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) + { + return 0; + } - // Parse the separator between name and value - if ((current == input.Length) || (input[current] != '=')) - { - // We only have a name and that's OK. Return. - parsedValue = new NameValueHeaderValue(); - parsedValue._name = name; - current = current + HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces - return current - startIndex; - } + // Parse the name, i.e. in name/value string "=". Caller must remove + // leading whitespaces. + var nameLength = HttpRuleParser.GetTokenLength(input, startIndex); - current++; // skip delimiter. - current = current + HttpRuleParser.GetWhitespaceLength(input, current); + if (nameLength == 0) + { + return 0; + } - // Parse the value, i.e. in name/value string "=" - int valueLength = GetValueLength(input, current); + var name = input.Subsegment(startIndex, nameLength); + var current = startIndex + nameLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); - // Value after the '=' may be empty - // Use parameterless ctor to avoid double-parsing of name and value, i.e. skip public ctor validation. + // Parse the separator between name and value + if ((current == input.Length) || (input[current] != '=')) + { + // We only have a name and that's OK. Return. parsedValue = new NameValueHeaderValue(); parsedValue._name = name; - parsedValue._value = input.Subsegment(current, valueLength); - current = current + valueLength; current = current + HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces return current - startIndex; } - // Returns the length of a name/value list, separated by 'delimiter'. E.g. "a=b, c=d, e=f" adds 3 - // name/value pairs to 'nameValueCollection' if 'delimiter' equals ','. - internal static int GetNameValueListLength( - StringSegment input, - int startIndex, - char delimiter, - IList nameValueCollection) - { - Contract.Requires(startIndex >= 0); + current++; // skip delimiter. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); - if ((StringSegment.IsNullOrEmpty(input)) || (startIndex >= input.Length)) - { - return 0; - } + // Parse the value, i.e. in name/value string "=" + int valueLength = GetValueLength(input, current); - var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex); - while (true) - { - var nameValueLength = GetNameValueLength(input, current, out var parameter); - - if (nameValueLength == 0) - { - // There may be a trailing ';' - return current - startIndex; - } - - nameValueCollection!.Add(parameter!); - current = current + nameValueLength; - current = current + HttpRuleParser.GetWhitespaceLength(input, current); - - if ((current == input.Length) || (input[current] != delimiter)) - { - // We're done and we have at least one valid name/value pair. - return current - startIndex; - } - - // input[current] is 'delimiter'. Skip the delimiter and whitespaces and try to parse again. - current++; // skip delimiter. - current = current + HttpRuleParser.GetWhitespaceLength(input, current); - } + // Value after the '=' may be empty + // Use parameterless ctor to avoid double-parsing of name and value, i.e. skip public ctor validation. + parsedValue = new NameValueHeaderValue(); + parsedValue._name = name; + parsedValue._value = input.Subsegment(current, valueLength); + current = current + valueLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces + return current - startIndex; + } + + // Returns the length of a name/value list, separated by 'delimiter'. E.g. "a=b, c=d, e=f" adds 3 + // name/value pairs to 'nameValueCollection' if 'delimiter' equals ','. + internal static int GetNameValueListLength( + StringSegment input, + int startIndex, + char delimiter, + IList nameValueCollection) + { + Contract.Requires(startIndex >= 0); + + if ((StringSegment.IsNullOrEmpty(input)) || (startIndex >= input.Length)) + { + return 0; } - /// - /// Finds a with the specified . - /// - /// The collection to search. - /// The name to find. - /// The if found, otherwise . - public static NameValueHeaderValue? Find(IList? values, StringSegment name) + var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex); + while (true) { - Contract.Requires(name.Length > 0); + var nameValueLength = GetNameValueLength(input, current, out var parameter); - if ((values == null) || (values.Count == 0)) + if (nameValueLength == 0) { - return null; + // There may be a trailing ';' + return current - startIndex; } - for (var i = 0; i < values.Count; i++) + nameValueCollection!.Add(parameter!); + current = current + nameValueLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + if ((current == input.Length) || (input[current] != delimiter)) { - var value = values[i]; - if (value.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) - { - return value; - } + // We're done and we have at least one valid name/value pair. + return current - startIndex; } - return null; + + // input[current] is 'delimiter'. Skip the delimiter and whitespaces and try to parse again. + current++; // skip delimiter. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); } + } - internal static int GetValueLength(StringSegment input, int startIndex) - { - if (startIndex >= input.Length) - { - return 0; - } + /// + /// Finds a with the specified . + /// + /// The collection to search. + /// The name to find. + /// The if found, otherwise . + public static NameValueHeaderValue? Find(IList? values, StringSegment name) + { + Contract.Requires(name.Length > 0); - var valueLength = HttpRuleParser.GetTokenLength(input, startIndex); + if ((values == null) || (values.Count == 0)) + { + return null; + } - if (valueLength == 0) + for (var i = 0; i < values.Count; i++) + { + var value = values[i]; + if (value.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) { - // A value can either be a token or a quoted string. Check if it is a quoted string. - if (HttpRuleParser.GetQuotedStringLength(input, startIndex, out valueLength) != HttpParseResult.Parsed) - { - // We have an invalid value. Reset the name and return. - return 0; - } + return value; } - return valueLength; } + return null; + } - private static void CheckNameValueFormat(StringSegment name, StringSegment value) + internal static int GetValueLength(StringSegment input, int startIndex) + { + if (startIndex >= input.Length) { - HeaderUtilities.CheckValidToken(name, nameof(name)); - CheckValueFormat(value); + return 0; } - private static void CheckValueFormat(StringSegment value) + var valueLength = HttpRuleParser.GetTokenLength(input, startIndex); + + if (valueLength == 0) { - // Either value is null/empty or a valid token/quoted string - if (!(StringSegment.IsNullOrEmpty(value) || (GetValueLength(value, 0) == value.Length))) + // A value can either be a token or a quoted string. Check if it is a quoted string. + if (HttpRuleParser.GetQuotedStringLength(input, startIndex, out valueLength) != HttpParseResult.Parsed) { - throw new FormatException(string.Format(CultureInfo.InvariantCulture, "The header value is invalid: '{0}'", value)); + // We have an invalid value. Reset the name and return. + return 0; } } + return valueLength; + } - private static NameValueHeaderValue CreateNameValue() + private static void CheckNameValueFormat(StringSegment name, StringSegment value) + { + HeaderUtilities.CheckValidToken(name, nameof(name)); + CheckValueFormat(value); + } + + private static void CheckValueFormat(StringSegment value) + { + // Either value is null/empty or a valid token/quoted string + if (!(StringSegment.IsNullOrEmpty(value) || (GetValueLength(value, 0) == value.Length))) { - return new NameValueHeaderValue(); + throw new FormatException(string.Format(CultureInfo.InvariantCulture, "The header value is invalid: '{0}'", value)); } } + + private static NameValueHeaderValue CreateNameValue() + { + return new NameValueHeaderValue(); + } } diff --git a/src/Http/Headers/src/ObjectCollection.cs b/src/Http/Headers/src/ObjectCollection.cs index a8edc0498c..ad35d39898 100644 --- a/src/Http/Headers/src/ObjectCollection.cs +++ b/src/Http/Headers/src/ObjectCollection.cs @@ -5,77 +5,76 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +// List allows 'null' values to be added. This is not what we want so we use a custom Collection derived +// type to throw if 'null' gets added. Collection internally uses List which comes at some cost. In addition +// Collection.Add() calls List.InsertItem() which is an O(n) operation (compared to O(1) for List.Add()). +// This type is only used for very small collections (1-2 items) to keep the impact of using Collection small. +internal sealed class ObjectCollection : Collection { - // List allows 'null' values to be added. This is not what we want so we use a custom Collection derived - // type to throw if 'null' gets added. Collection internally uses List which comes at some cost. In addition - // Collection.Add() calls List.InsertItem() which is an O(n) operation (compared to O(1) for List.Add()). - // This type is only used for very small collections (1-2 items) to keep the impact of using Collection small. - internal sealed class ObjectCollection : Collection - { - internal static readonly Action DefaultValidator = CheckNotNull; - internal static readonly ObjectCollection EmptyReadOnlyCollection - = new ObjectCollection(DefaultValidator, isReadOnly: true); + internal static readonly Action DefaultValidator = CheckNotNull; + internal static readonly ObjectCollection EmptyReadOnlyCollection + = new ObjectCollection(DefaultValidator, isReadOnly: true); - private readonly Action _validator; + private readonly Action _validator; - // We need to create a 'read-only' inner list for Collection to do the right - // thing. - private static IList CreateInnerList(bool isReadOnly, IEnumerable? other = null) - { - var list = other == null ? new List() : new List(other); - if (isReadOnly) - { - return new ReadOnlyCollection(list); - } - else - { - return list; - } - } - - public ObjectCollection() - : this(DefaultValidator) + // We need to create a 'read-only' inner list for Collection to do the right + // thing. + private static IList CreateInnerList(bool isReadOnly, IEnumerable? other = null) + { + var list = other == null ? new List() : new List(other); + if (isReadOnly) { + return new ReadOnlyCollection(list); } - - public ObjectCollection(Action validator, bool isReadOnly = false) - : base(CreateInnerList(isReadOnly)) + else { - _validator = validator; + return list; } + } - public ObjectCollection(IEnumerable other, bool isReadOnly = false) - : base(CreateInnerList(isReadOnly, other)) - { - _validator = DefaultValidator; - foreach (T item in Items) - { - _validator(item); - } - } + public ObjectCollection() + : this(DefaultValidator) + { + } - public bool IsReadOnly => ((ICollection)this).IsReadOnly; + public ObjectCollection(Action validator, bool isReadOnly = false) + : base(CreateInnerList(isReadOnly)) + { + _validator = validator; + } - protected override void InsertItem(int index, T item) + public ObjectCollection(IEnumerable other, bool isReadOnly = false) + : base(CreateInnerList(isReadOnly, other)) + { + _validator = DefaultValidator; + foreach (T item in Items) { _validator(item); - base.InsertItem(index, item); } + } - protected override void SetItem(int index, T item) - { - _validator(item); - base.SetItem(index, item); - } + public bool IsReadOnly => ((ICollection)this).IsReadOnly; - private static void CheckNotNull(T item) + protected override void InsertItem(int index, T item) + { + _validator(item); + base.InsertItem(index, item); + } + + protected override void SetItem(int index, T item) + { + _validator(item); + base.SetItem(index, item); + } + + private static void CheckNotNull(T item) + { + // null values cannot be added to the collection. + if (item == null) { - // null values cannot be added to the collection. - if (item == null) - { - throw new ArgumentNullException(nameof(item)); - } + throw new ArgumentNullException(nameof(item)); } } } diff --git a/src/Http/Headers/src/RangeConditionHeaderValue.cs b/src/Http/Headers/src/RangeConditionHeaderValue.cs index a26aef8873..f4df007298 100644 --- a/src/Http/Headers/src/RangeConditionHeaderValue.cs +++ b/src/Http/Headers/src/RangeConditionHeaderValue.cs @@ -6,198 +6,197 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +/// +/// Represents an If-Range header value which can either be a date/time or an entity-tag value. +/// +public class RangeConditionHeaderValue { + private static readonly HttpHeaderParser Parser + = new GenericHeaderParser(false, GetRangeConditionLength); + + private DateTimeOffset? _lastModified; + private EntityTagHeaderValue? _entityTag; + + private RangeConditionHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + /// - /// Represents an If-Range header value which can either be a date/time or an entity-tag value. + /// Initializes a new instance of . /// - public class RangeConditionHeaderValue + /// A date value used to initialize the new instance. + public RangeConditionHeaderValue(DateTimeOffset lastModified) { - private static readonly HttpHeaderParser Parser - = new GenericHeaderParser(false, GetRangeConditionLength); - - private DateTimeOffset? _lastModified; - private EntityTagHeaderValue? _entityTag; + _lastModified = lastModified; + } - private RangeConditionHeaderValue() + /// + /// Initializes a new instance of . + /// + /// An entity tag uniquely representing the requested resource. + public RangeConditionHeaderValue(EntityTagHeaderValue entityTag) + { + if (entityTag == null) { - // Used by the parser to create a new instance of this type. + throw new ArgumentNullException(nameof(entityTag)); } - /// - /// Initializes a new instance of . - /// - /// A date value used to initialize the new instance. - public RangeConditionHeaderValue(DateTimeOffset lastModified) - { - _lastModified = lastModified; - } + _entityTag = entityTag; + } - /// - /// Initializes a new instance of . - /// - /// An entity tag uniquely representing the requested resource. - public RangeConditionHeaderValue(EntityTagHeaderValue entityTag) - { - if (entityTag == null) - { - throw new ArgumentNullException(nameof(entityTag)); - } + /// + /// Initializes a new instance of . + /// + /// An entity tag uniquely representing the requested resource. + public RangeConditionHeaderValue(string? entityTag) + : this(new EntityTagHeaderValue(entityTag)) + { + } - _entityTag = entityTag; - } + /// + /// Gets the LastModified date from header. + /// + public DateTimeOffset? LastModified + { + get { return _lastModified; } + } - /// - /// Initializes a new instance of . - /// - /// An entity tag uniquely representing the requested resource. - public RangeConditionHeaderValue(string? entityTag) - : this(new EntityTagHeaderValue(entityTag)) - { - } + /// + /// Gets the from header. + /// + public EntityTagHeaderValue? EntityTag + { + get { return _entityTag; } + } - /// - /// Gets the LastModified date from header. - /// - public DateTimeOffset? LastModified + /// + public override string ToString() + { + if (_entityTag == null) { - get { return _lastModified; } + return HeaderUtilities.FormatDate(_lastModified.GetValueOrDefault()); } + return _entityTag.ToString(); + } + + /// + public override bool Equals(object? obj) + { + var other = obj as RangeConditionHeaderValue; - /// - /// Gets the from header. - /// - public EntityTagHeaderValue? EntityTag + if (other == null) { - get { return _entityTag; } + return false; } - /// - public override string ToString() + if (_entityTag == null) { - if (_entityTag == null) - { - return HeaderUtilities.FormatDate(_lastModified.GetValueOrDefault()); - } - return _entityTag.ToString(); + return (other._lastModified != null) && (_lastModified.GetValueOrDefault() == other._lastModified.GetValueOrDefault()); } - /// - public override bool Equals(object? obj) + return _entityTag.Equals(other._entityTag); + } + + /// + public override int GetHashCode() + { + if (_entityTag == null) { - var other = obj as RangeConditionHeaderValue; + return _lastModified.GetValueOrDefault().GetHashCode(); + } - if (other == null) - { - return false; - } + return _entityTag.GetHashCode(); + } - if (_entityTag == null) - { - return (other._lastModified != null) && (_lastModified.GetValueOrDefault() == other._lastModified.GetValueOrDefault()); - } + /// + /// Parses as a value. + /// + /// The values to parse. + /// The parsed values. + public static RangeConditionHeaderValue Parse(StringSegment input) + { + var index = 0; + return Parser.ParseValue(input, ref index)!; + } - return _entityTag.Equals(other._entityTag); - } + /// + /// Attempts to parse the specified as a . + /// + /// The value to parse. + /// The parsed value. + /// if input is a valid , otherwise . + public static bool TryParse(StringSegment input, [NotNullWhen(true)] out RangeConditionHeaderValue? parsedValue) + { + var index = 0; + return Parser.TryParseValue(input, ref index, out parsedValue!); + } - /// - public override int GetHashCode() - { - if (_entityTag == null) - { - return _lastModified.GetValueOrDefault().GetHashCode(); - } + private static int GetRangeConditionLength(StringSegment input, int startIndex, out RangeConditionHeaderValue? parsedValue) + { + Contract.Requires(startIndex >= 0); - return _entityTag.GetHashCode(); - } + parsedValue = null; - /// - /// Parses as a value. - /// - /// The values to parse. - /// The parsed values. - public static RangeConditionHeaderValue Parse(StringSegment input) + // Make sure we have at least 2 characters + if (StringSegment.IsNullOrEmpty(input) || (startIndex + 1 >= input.Length)) { - var index = 0; - return Parser.ParseValue(input, ref index)!; + return 0; } - /// - /// Attempts to parse the specified as a . - /// - /// The value to parse. - /// The parsed value. - /// if input is a valid , otherwise . - public static bool TryParse(StringSegment input, [NotNullWhen(true)] out RangeConditionHeaderValue? parsedValue) - { - var index = 0; - return Parser.TryParseValue(input, ref index, out parsedValue!); - } + var current = startIndex; - private static int GetRangeConditionLength(StringSegment input, int startIndex, out RangeConditionHeaderValue? parsedValue) - { - Contract.Requires(startIndex >= 0); + // Caller must remove leading whitespaces. + DateTimeOffset date = DateTimeOffset.MinValue; + EntityTagHeaderValue? entityTag = null; - parsedValue = null; + // Entity tags are quoted strings optionally preceded by "W/". By looking at the first two character we + // can determine whether the string is en entity tag or a date. + var firstChar = input[current]; + var secondChar = input[current + 1]; - // Make sure we have at least 2 characters - if (StringSegment.IsNullOrEmpty(input) || (startIndex + 1 >= input.Length)) + if ((firstChar == '\"') || (((firstChar == 'w') || (firstChar == 'W')) && (secondChar == '/'))) + { + // trailing whitespaces are removed by GetEntityTagLength() + var entityTagLength = EntityTagHeaderValue.GetEntityTagLength(input, current, out entityTag); + + if (entityTagLength == 0) { return 0; } - var current = startIndex; - - // Caller must remove leading whitespaces. - DateTimeOffset date = DateTimeOffset.MinValue; - EntityTagHeaderValue? entityTag = null; + current = current + entityTagLength; - // Entity tags are quoted strings optionally preceded by "W/". By looking at the first two character we - // can determine whether the string is en entity tag or a date. - var firstChar = input[current]; - var secondChar = input[current + 1]; - - if ((firstChar == '\"') || (((firstChar == 'w') || (firstChar == 'W')) && (secondChar == '/'))) + // RangeConditionHeaderValue only allows 1 value. There must be no delimiter/other chars after an + // entity tag. + if (current != input.Length) { - // trailing whitespaces are removed by GetEntityTagLength() - var entityTagLength = EntityTagHeaderValue.GetEntityTagLength(input, current, out entityTag); - - if (entityTagLength == 0) - { - return 0; - } - - current = current + entityTagLength; - - // RangeConditionHeaderValue only allows 1 value. There must be no delimiter/other chars after an - // entity tag. - if (current != input.Length) - { - return 0; - } + return 0; } - else + } + else + { + if (!HttpRuleParser.TryStringToDate(input.Subsegment(current), out date)) { - if (!HttpRuleParser.TryStringToDate(input.Subsegment(current), out date)) - { - return 0; - } - - // If we got a valid date, then the parser consumed the whole string (incl. trailing whitespaces). - current = input.Length; + return 0; } - parsedValue = new RangeConditionHeaderValue(); - if (entityTag == null) - { - parsedValue._lastModified = date; - } - else - { - parsedValue._entityTag = entityTag; - } + // If we got a valid date, then the parser consumed the whole string (incl. trailing whitespaces). + current = input.Length; + } - return current - startIndex; + parsedValue = new RangeConditionHeaderValue(); + if (entityTag == null) + { + parsedValue._lastModified = date; } + else + { + parsedValue._entityTag = entityTag; + } + + return current - startIndex; } } diff --git a/src/Http/Headers/src/RangeHeaderValue.cs b/src/Http/Headers/src/RangeHeaderValue.cs index 6270722f46..4cfd368326 100644 --- a/src/Http/Headers/src/RangeHeaderValue.cs +++ b/src/Http/Headers/src/RangeHeaderValue.cs @@ -8,193 +8,192 @@ using System.Diagnostics.Contracts; using System.Text; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +/// +/// Represents a Range header value. +/// +/// The class provides support for the Range header as defined in +/// RFC 2616. +/// +/// +public class RangeHeaderValue { + private static readonly HttpHeaderParser Parser + = new GenericHeaderParser(false, GetRangeLength); + + private StringSegment _unit; + private ICollection? _ranges; + /// - /// Represents a Range header value. - /// - /// The class provides support for the Range header as defined in - /// RFC 2616. - /// + /// Initializes a new instance of . /// - public class RangeHeaderValue + public RangeHeaderValue() { - private static readonly HttpHeaderParser Parser - = new GenericHeaderParser(false, GetRangeLength); - - private StringSegment _unit; - private ICollection? _ranges; + _unit = HeaderUtilities.BytesUnit; + } - /// - /// Initializes a new instance of . - /// - public RangeHeaderValue() - { - _unit = HeaderUtilities.BytesUnit; - } + /// + /// Initializes a new instance of . + /// + /// The position at which to start sending data. + /// The position at which to stop sending data. + public RangeHeaderValue(long? from, long? to) + { + // convenience ctor: "Range: bytes=from-to" + _unit = HeaderUtilities.BytesUnit; + Ranges.Add(new RangeItemHeaderValue(from, to)); + } - /// - /// Initializes a new instance of . - /// - /// The position at which to start sending data. - /// The position at which to stop sending data. - public RangeHeaderValue(long? from, long? to) + /// + /// Gets or sets the unit from the header. + /// + /// Defaults to bytes. + public StringSegment Unit + { + get { return _unit; } + set { - // convenience ctor: "Range: bytes=from-to" - _unit = HeaderUtilities.BytesUnit; - Ranges.Add(new RangeItemHeaderValue(from, to)); + HeaderUtilities.CheckValidToken(value, nameof(value)); + _unit = value; } + } - /// - /// Gets or sets the unit from the header. - /// - /// Defaults to bytes. - public StringSegment Unit + /// + /// Gets the ranges specified in the header. + /// + public ICollection Ranges + { + get { - get { return _unit; } - set + if (_ranges == null) { - HeaderUtilities.CheckValidToken(value, nameof(value)); - _unit = value; + _ranges = new ObjectCollection(); } + return _ranges; } + } + + /// + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(_unit.AsSpan()); + sb.Append('='); - /// - /// Gets the ranges specified in the header. - /// - public ICollection Ranges + var first = true; + foreach (var range in Ranges) { - get + if (first) { - if (_ranges == null) - { - _ranges = new ObjectCollection(); - } - return _ranges; + first = false; } - } - - /// - public override string ToString() - { - var sb = new StringBuilder(); - sb.Append(_unit.AsSpan()); - sb.Append('='); - - var first = true; - foreach (var range in Ranges) + else { - if (first) - { - first = false; - } - else - { - sb.Append(", "); - } - - sb.Append(range.From); - sb.Append('-'); - sb.Append(range.To); + sb.Append(", "); } - return sb.ToString(); + sb.Append(range.From); + sb.Append('-'); + sb.Append(range.To); } - /// - public override bool Equals(object? obj) - { - var other = obj as RangeHeaderValue; - - if (other == null) - { - return false; - } + return sb.ToString(); + } - return StringSegment.Equals(_unit, other._unit, StringComparison.OrdinalIgnoreCase) && - HeaderUtilities.AreEqualCollections(Ranges, other.Ranges); - } + /// + public override bool Equals(object? obj) + { + var other = obj as RangeHeaderValue; - /// - public override int GetHashCode() + if (other == null) { - var result = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_unit); + return false; + } - foreach (var range in Ranges) - { - result = result ^ range.GetHashCode(); - } + return StringSegment.Equals(_unit, other._unit, StringComparison.OrdinalIgnoreCase) && + HeaderUtilities.AreEqualCollections(Ranges, other.Ranges); + } - return result; - } + /// + public override int GetHashCode() + { + var result = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_unit); - /// - /// Parses as a value. - /// - /// The values to parse. - /// The parsed values. - public static RangeHeaderValue Parse(StringSegment input) + foreach (var range in Ranges) { - var index = 0; - return Parser.ParseValue(input, ref index)!; + result = result ^ range.GetHashCode(); } - /// - /// Attempts to parse the specified as a . - /// - /// The value to parse. - /// The parsed value. - /// if input is a valid , otherwise . - public static bool TryParse(StringSegment input, [NotNullWhen(true)] out RangeHeaderValue parsedValue) - { - var index = 0; - return Parser.TryParseValue(input, ref index, out parsedValue!); - } + return result; + } - private static int GetRangeLength(StringSegment input, int startIndex, out RangeHeaderValue? parsedValue) - { - Contract.Requires(startIndex >= 0); + /// + /// Parses as a value. + /// + /// The values to parse. + /// The parsed values. + public static RangeHeaderValue Parse(StringSegment input) + { + var index = 0; + return Parser.ParseValue(input, ref index)!; + } - parsedValue = null; + /// + /// Attempts to parse the specified as a . + /// + /// The value to parse. + /// The parsed value. + /// if input is a valid , otherwise . + public static bool TryParse(StringSegment input, [NotNullWhen(true)] out RangeHeaderValue parsedValue) + { + var index = 0; + return Parser.TryParseValue(input, ref index, out parsedValue!); + } - if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) - { - return 0; - } + private static int GetRangeLength(StringSegment input, int startIndex, out RangeHeaderValue? parsedValue) + { + Contract.Requires(startIndex >= 0); - // Parse the unit string: in '=-, -' - var unitLength = HttpRuleParser.GetTokenLength(input, startIndex); + parsedValue = null; - if (unitLength == 0) - { - return 0; - } + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) + { + return 0; + } - RangeHeaderValue result = new RangeHeaderValue(); - result._unit = input.Subsegment(startIndex, unitLength); - var current = startIndex + unitLength; - current = current + HttpRuleParser.GetWhitespaceLength(input, current); + // Parse the unit string: in '=-, -' + var unitLength = HttpRuleParser.GetTokenLength(input, startIndex); - if ((current == input.Length) || (input[current] != '=')) - { - return 0; - } + if (unitLength == 0) + { + return 0; + } - current++; // skip '=' separator - current = current + HttpRuleParser.GetWhitespaceLength(input, current); + RangeHeaderValue result = new RangeHeaderValue(); + result._unit = input.Subsegment(startIndex, unitLength); + var current = startIndex + unitLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); - var rangesLength = RangeItemHeaderValue.GetRangeItemListLength(input, current, result.Ranges); + if ((current == input.Length) || (input[current] != '=')) + { + return 0; + } - if (rangesLength == 0) - { - return 0; - } + current++; // skip '=' separator + current = current + HttpRuleParser.GetWhitespaceLength(input, current); - current = current + rangesLength; - Contract.Assert(current == input.Length, "GetRangeItemListLength() should consume the whole string or fail."); + var rangesLength = RangeItemHeaderValue.GetRangeItemListLength(input, current, result.Ranges); - parsedValue = result; - return current - startIndex; + if (rangesLength == 0) + { + return 0; } + + current = current + rangesLength; + Contract.Assert(current == input.Length, "GetRangeItemListLength() should consume the whole string or fail."); + + parsedValue = result; + return current - startIndex; } } diff --git a/src/Http/Headers/src/RangeItemHeaderValue.cs b/src/Http/Headers/src/RangeItemHeaderValue.cs index 7f5f1da441..cd48094c4f 100644 --- a/src/Http/Headers/src/RangeItemHeaderValue.cs +++ b/src/Http/Headers/src/RangeItemHeaderValue.cs @@ -7,235 +7,234 @@ using System.Diagnostics.Contracts; using System.Globalization; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +/// +/// Represents a byte range in a Range header value. +/// +/// The class provides support for a byte range in a Range as defined +/// in RFC 2616. +/// +/// +public class RangeItemHeaderValue { + private readonly long? _from; + private readonly long? _to; + /// - /// Represents a byte range in a Range header value. - /// - /// The class provides support for a byte range in a Range as defined - /// in RFC 2616. - /// + /// Initializes a new instance of the class. /// - public class RangeItemHeaderValue + /// The position at which to start sending data. + /// The position at which to stop sending data. + public RangeItemHeaderValue(long? from, long? to) { - private readonly long? _from; - private readonly long? _to; - - /// - /// Initializes a new instance of the class. - /// - /// The position at which to start sending data. - /// The position at which to stop sending data. - public RangeItemHeaderValue(long? from, long? to) + if (!from.HasValue && !to.HasValue) { - if (!from.HasValue && !to.HasValue) - { - throw new ArgumentException("Invalid header range."); - } - if (from.HasValue && (from.GetValueOrDefault() < 0)) - { - throw new ArgumentOutOfRangeException(nameof(from)); - } - if (to.HasValue && (to.GetValueOrDefault() < 0)) - { - throw new ArgumentOutOfRangeException(nameof(to)); - } - if (from.HasValue && to.HasValue && (from.GetValueOrDefault() > to.GetValueOrDefault())) - { - throw new ArgumentOutOfRangeException(nameof(from)); - } - - _from = from; - _to = to; + throw new ArgumentException("Invalid header range."); } - - /// - /// Gets the position at which to start sending data. - /// - public long? From + if (from.HasValue && (from.GetValueOrDefault() < 0)) { - get { return _from; } + throw new ArgumentOutOfRangeException(nameof(from)); } + if (to.HasValue && (to.GetValueOrDefault() < 0)) + { + throw new ArgumentOutOfRangeException(nameof(to)); + } + if (from.HasValue && to.HasValue && (from.GetValueOrDefault() > to.GetValueOrDefault())) + { + throw new ArgumentOutOfRangeException(nameof(from)); + } + + _from = from; + _to = to; + } + + /// + /// Gets the position at which to start sending data. + /// + public long? From + { + get { return _from; } + } + + /// + /// Gets the position at which to stop sending data. + /// + public long? To + { + get { return _to; } + } - /// - /// Gets the position at which to stop sending data. - /// - public long? To + /// + public override string ToString() + { + if (!_from.HasValue) { - get { return _to; } + return "-" + _to.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo); } + else if (!_to.HasValue) + { + return _from.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo) + "-"; + } + return _from.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo) + "-" + + _to.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo); + } - /// - public override string ToString() + /// + public override bool Equals(object? obj) + { + return obj is RangeItemHeaderValue other && ((_from == other._from) && (_to == other._to)); + } + + /// + public override int GetHashCode() + { + if (!_from.HasValue) { - if (!_from.HasValue) - { - return "-" + _to.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo); - } - else if (!_to.HasValue) - { - return _from.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo) + "-"; - } - return _from.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo) + "-" + - _to.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo); + return _to.GetValueOrDefault().GetHashCode(); + } + else if (!_to.HasValue) + { + return _from.GetValueOrDefault().GetHashCode(); } + return _from.GetValueOrDefault().GetHashCode() ^ _to.GetValueOrDefault().GetHashCode(); + } + + // Returns the length of a range list. E.g. "1-2, 3-4, 5-6" adds 3 ranges to 'rangeCollection'. Note that empty + // list segments are allowed, e.g. ",1-2, , 3-4,,". + internal static int GetRangeItemListLength( + StringSegment input, + int startIndex, + ICollection rangeCollection) + { + Contract.Requires(startIndex >= 0); + Contract.Ensures((Contract.Result() == 0) || (rangeCollection.Count > 0), + "If we can parse the string, then we expect to have at least one range item."); - /// - public override bool Equals(object? obj) + if ((StringSegment.IsNullOrEmpty(input)) || (startIndex >= input.Length)) { - return obj is RangeItemHeaderValue other && ((_from == other._from) && (_to == other._to)); + return 0; } - /// - public override int GetHashCode() + // Empty segments are allowed, so skip all delimiter-only segments (e.g. ", ,"). + var current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(input, startIndex, true, out var separatorFound); + // It's OK if we didn't find leading separator characters. Ignore 'separatorFound'. + + if (current == input.Length) { - if (!_from.HasValue) - { - return _to.GetValueOrDefault().GetHashCode(); - } - else if (!_to.HasValue) - { - return _from.GetValueOrDefault().GetHashCode(); - } - return _from.GetValueOrDefault().GetHashCode() ^ _to.GetValueOrDefault().GetHashCode(); + return 0; } - // Returns the length of a range list. E.g. "1-2, 3-4, 5-6" adds 3 ranges to 'rangeCollection'. Note that empty - // list segments are allowed, e.g. ",1-2, , 3-4,,". - internal static int GetRangeItemListLength( - StringSegment input, - int startIndex, - ICollection rangeCollection) + while (true) { - Contract.Requires(startIndex >= 0); - Contract.Ensures((Contract.Result() == 0) || (rangeCollection.Count > 0), - "If we can parse the string, then we expect to have at least one range item."); + var rangeLength = GetRangeItemLength(input, current, out var range); - if ((StringSegment.IsNullOrEmpty(input)) || (startIndex >= input.Length)) + if (rangeLength == 0) { return 0; } - // Empty segments are allowed, so skip all delimiter-only segments (e.g. ", ,"). - var current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(input, startIndex, true, out var separatorFound); - // It's OK if we didn't find leading separator characters. Ignore 'separatorFound'. + rangeCollection!.Add(range!); - if (current == input.Length) + current = current + rangeLength; + current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(input, current, true, out separatorFound); + + // If the string is not consumed, we must have a delimiter, otherwise the string is not a valid + // range list. + if ((current < input.Length) && !separatorFound) { return 0; } - while (true) + if (current == input.Length) { - var rangeLength = GetRangeItemLength(input, current, out var range); - - if (rangeLength == 0) - { - return 0; - } + return current - startIndex; + } + } + } - rangeCollection!.Add(range!); + internal static int GetRangeItemLength(StringSegment input, int startIndex, out RangeItemHeaderValue? parsedValue) + { + Contract.Requires(startIndex >= 0); - current = current + rangeLength; - current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(input, current, true, out separatorFound); + // This parser parses number ranges: e.g. '1-2', '1-', '-2'. - // If the string is not consumed, we must have a delimiter, otherwise the string is not a valid - // range list. - if ((current < input.Length) && !separatorFound) - { - return 0; - } + parsedValue = null; - if (current == input.Length) - { - return current - startIndex; - } - } + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) + { + return 0; } - internal static int GetRangeItemLength(StringSegment input, int startIndex, out RangeItemHeaderValue? parsedValue) - { - Contract.Requires(startIndex >= 0); + // Caller must remove leading whitespaces. If not, we'll return 0. + var current = startIndex; - // This parser parses number ranges: e.g. '1-2', '1-', '-2'. + // Try parse the first value of a value pair. + var fromStartIndex = current; + var fromLength = HttpRuleParser.GetNumberLength(input, current, false); - parsedValue = null; + if (fromLength > HttpRuleParser.MaxInt64Digits) + { + return 0; + } - if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) - { - return 0; - } + current = current + fromLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); - // Caller must remove leading whitespaces. If not, we'll return 0. - var current = startIndex; + // After the first value, the '-' character must follow. + if ((current == input.Length) || (input[current] != '-')) + { + // We need a '-' character otherwise this can't be a valid range. + return 0; + } - // Try parse the first value of a value pair. - var fromStartIndex = current; - var fromLength = HttpRuleParser.GetNumberLength(input, current, false); + current++; // skip the '-' character + current = current + HttpRuleParser.GetWhitespaceLength(input, current); - if (fromLength > HttpRuleParser.MaxInt64Digits) - { - return 0; - } + var toStartIndex = current; + var toLength = 0; - current = current + fromLength; - current = current + HttpRuleParser.GetWhitespaceLength(input, current); + // If we didn't reach the end of the string, try parse the second value of the range. + if (current < input.Length) + { + toLength = HttpRuleParser.GetNumberLength(input, current, false); - // After the first value, the '-' character must follow. - if ((current == input.Length) || (input[current] != '-')) + if (toLength > HttpRuleParser.MaxInt64Digits) { - // We need a '-' character otherwise this can't be a valid range. return 0; } - current++; // skip the '-' character + current = current + toLength; current = current + HttpRuleParser.GetWhitespaceLength(input, current); + } - var toStartIndex = current; - var toLength = 0; - - // If we didn't reach the end of the string, try parse the second value of the range. - if (current < input.Length) - { - toLength = HttpRuleParser.GetNumberLength(input, current, false); - - if (toLength > HttpRuleParser.MaxInt64Digits) - { - return 0; - } - - current = current + toLength; - current = current + HttpRuleParser.GetWhitespaceLength(input, current); - } - - if ((fromLength == 0) && (toLength == 0)) - { - return 0; // At least one value must be provided in order to be a valid range. - } - - // Try convert first value to int64 - long from = 0; - if ((fromLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(fromStartIndex, fromLength), out from)) - { - return 0; - } + if ((fromLength == 0) && (toLength == 0)) + { + return 0; // At least one value must be provided in order to be a valid range. + } - // Try convert second value to int64 - long to = 0; - if ((toLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(toStartIndex, toLength), out to)) - { - return 0; - } + // Try convert first value to int64 + long from = 0; + if ((fromLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(fromStartIndex, fromLength), out from)) + { + return 0; + } - // 'from' must not be greater than 'to' - if ((fromLength > 0) && (toLength > 0) && (from > to)) - { - return 0; - } + // Try convert second value to int64 + long to = 0; + if ((toLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(toStartIndex, toLength), out to)) + { + return 0; + } - parsedValue = new RangeItemHeaderValue((fromLength == 0 ? (long?)null : (long?)from), - (toLength == 0 ? (long?)null : (long?)to)); - return current - startIndex; + // 'from' must not be greater than 'to' + if ((fromLength > 0) && (toLength > 0) && (from > to)) + { + return 0; } + + parsedValue = new RangeItemHeaderValue((fromLength == 0 ? (long?)null : (long?)from), + (toLength == 0 ? (long?)null : (long?)to)); + return current - startIndex; } } diff --git a/src/Http/Headers/src/SameSiteMode.cs b/src/Http/Headers/src/SameSiteMode.cs index 8b5904880d..3e3ce2675b 100644 --- a/src/Http/Headers/src/SameSiteMode.cs +++ b/src/Http/Headers/src/SameSiteMode.cs @@ -1,22 +1,21 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +/// +/// Indicates if the client should include a cookie on "same-site" or "cross-site" requests. +/// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1 +/// +// This mirrors Microsoft.AspNetCore.Http.SameSiteMode +public enum SameSiteMode { - /// - /// Indicates if the client should include a cookie on "same-site" or "cross-site" requests. - /// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1 - /// - // This mirrors Microsoft.AspNetCore.Http.SameSiteMode - public enum SameSiteMode - { - /// No SameSite field will be set, the client should follow its default cookie policy. - Unspecified = -1, - /// Indicates the client should disable same-site restrictions. - None = 0, - /// Indicates the client should send the cookie with "same-site" requests, and with "cross-site" top-level navigations. - Lax, - /// Indicates the client should only send the cookie with "same-site" requests. - Strict - } + /// No SameSite field will be set, the client should follow its default cookie policy. + Unspecified = -1, + /// Indicates the client should disable same-site restrictions. + None = 0, + /// Indicates the client should send the cookie with "same-site" requests, and with "cross-site" top-level navigations. + Lax, + /// Indicates the client should only send the cookie with "same-site" requests. + Strict } diff --git a/src/Http/Headers/src/SetCookieHeaderValue.cs b/src/Http/Headers/src/SetCookieHeaderValue.cs index cd5b424203..a03593ec8f 100644 --- a/src/Http/Headers/src/SetCookieHeaderValue.cs +++ b/src/Http/Headers/src/SetCookieHeaderValue.cs @@ -10,722 +10,721 @@ using System.Linq; using System.Text; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +/// +/// Represents the Set-Cookie header. +/// +/// See http://tools.ietf.org/html/rfc6265 for the Set-Cookie header specification. +/// +/// +public class SetCookieHeaderValue { + private const string ExpiresToken = "expires"; + private const string MaxAgeToken = "max-age"; + private const string DomainToken = "domain"; + private const string PathToken = "path"; + private const string SecureToken = "secure"; + // RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 + private const string SameSiteToken = "samesite"; + private static readonly string SameSiteNoneToken = SameSiteMode.None.ToString().ToLowerInvariant(); + private static readonly string SameSiteLaxToken = SameSiteMode.Lax.ToString().ToLowerInvariant(); + private static readonly string SameSiteStrictToken = SameSiteMode.Strict.ToString().ToLowerInvariant(); + + private const string HttpOnlyToken = "httponly"; + private const string SeparatorToken = "; "; + private const string EqualsToken = "="; + private const int ExpiresDateLength = 29; + private const string ExpiresDateFormat = "r"; + + private static readonly HttpHeaderParser SingleValueParser + = new GenericHeaderParser(false, GetSetCookieLength); + private static readonly HttpHeaderParser MultipleValueParser + = new GenericHeaderParser(true, GetSetCookieLength); + + private StringSegment _name; + private StringSegment _value; + + private SetCookieHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + /// - /// Represents the Set-Cookie header. - /// - /// See http://tools.ietf.org/html/rfc6265 for the Set-Cookie header specification. - /// + /// Initializes a new instance of . /// - public class SetCookieHeaderValue + /// The cookie name. + public SetCookieHeaderValue(StringSegment name) + : this(name, StringSegment.Empty) { - private const string ExpiresToken = "expires"; - private const string MaxAgeToken = "max-age"; - private const string DomainToken = "domain"; - private const string PathToken = "path"; - private const string SecureToken = "secure"; - // RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 - private const string SameSiteToken = "samesite"; - private static readonly string SameSiteNoneToken = SameSiteMode.None.ToString().ToLowerInvariant(); - private static readonly string SameSiteLaxToken = SameSiteMode.Lax.ToString().ToLowerInvariant(); - private static readonly string SameSiteStrictToken = SameSiteMode.Strict.ToString().ToLowerInvariant(); - - private const string HttpOnlyToken = "httponly"; - private const string SeparatorToken = "; "; - private const string EqualsToken = "="; - private const int ExpiresDateLength = 29; - private const string ExpiresDateFormat = "r"; - - private static readonly HttpHeaderParser SingleValueParser - = new GenericHeaderParser(false, GetSetCookieLength); - private static readonly HttpHeaderParser MultipleValueParser - = new GenericHeaderParser(true, GetSetCookieLength); - - private StringSegment _name; - private StringSegment _value; + } - private SetCookieHeaderValue() + /// + /// Initializes a new instance of . + /// + /// The cookie name. + /// The cookie value. + public SetCookieHeaderValue(StringSegment name, StringSegment value) + { + if (name == null) { - // Used by the parser to create a new instance of this type. + throw new ArgumentNullException(nameof(name)); } - /// - /// Initializes a new instance of . - /// - /// The cookie name. - public SetCookieHeaderValue(StringSegment name) - : this(name, StringSegment.Empty) + if (value == null) { + throw new ArgumentNullException(nameof(value)); } - /// - /// Initializes a new instance of . - /// - /// The cookie name. - /// The cookie value. - public SetCookieHeaderValue(StringSegment name, StringSegment value) - { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - Name = name; - Value = value; - } + Name = name; + Value = value; + } - /// - /// Gets or sets the cookie name. - /// - public StringSegment Name + /// + /// Gets or sets the cookie name. + /// + public StringSegment Name + { + get { return _name; } + set { - get { return _name; } - set - { - CookieHeaderValue.CheckNameFormat(value, nameof(value)); - _name = value; - } + CookieHeaderValue.CheckNameFormat(value, nameof(value)); + _name = value; } + } - /// - /// Gets or sets the cookie value. - /// - public StringSegment Value + /// + /// Gets or sets the cookie value. + /// + public StringSegment Value + { + get { return _value; } + set { - get { return _value; } - set - { - CookieHeaderValue.CheckValueFormat(value, nameof(value)); - _value = value; - } + CookieHeaderValue.CheckValueFormat(value, nameof(value)); + _value = value; } + } - /// - /// Gets or sets a value for the Expires cookie attribute. - /// - /// The Expires attribute indicates the maximum lifetime of the cookie, - /// represented as the date and time at which the cookie expires. - /// - /// - /// See https://tools.ietf.org/html/rfc6265#section-4.1.2.1 - public DateTimeOffset? Expires { get; set; } - - /// - /// Gets or sets a value for the Max-Age cookie attribute. - /// - /// The Max-Age attribute indicates the maximum lifetime of the cookie, - /// represented as the number of seconds until the cookie expires. - /// - /// - /// See https://tools.ietf.org/html/rfc6265#section-4.1.2.2 - public TimeSpan? MaxAge { get; set; } - - /// - /// Gets or sets a value for the Domain cookie attribute. - /// - /// The Domain attribute specifies those hosts to which the cookie will - /// be sent. - /// - /// - /// See https://tools.ietf.org/html/rfc6265#section-4.1.2.3 - public StringSegment Domain { get; set; } - - /// - /// Gets or sets a value for the Path cookie attribute. - /// - /// The path attribute specifies those hosts to which the cookie will - /// be sent. - /// - /// - /// See https://tools.ietf.org/html/rfc6265#section-4.1.2.4 - public StringSegment Path { get; set; } - - /// - /// Gets or sets a value for the Secure cookie attribute. - /// - /// The Secure attribute limits the scope of the cookie to "secure" - /// channels. - /// - /// - /// See https://tools.ietf.org/html/rfc6265#section-4.1.2.5 - public bool Secure { get; set; } - - /// - /// Gets or sets a value for the SameSite cookie attribute. - /// - /// "SameSite" cookies offer a robust defense against CSRF attack when - /// deployed in strict mode, and when supported by the client. - /// - /// - /// See https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-05#section-8.8 - public SameSiteMode SameSite { get; set; } = SameSiteMode.Unspecified; - - /// - /// Gets or sets a value for the HttpOnly cookie attribute. - /// - /// HttpOnly instructs the user agent to - /// omit the cookie when providing access to cookies via "non-HTTP" APIs - /// (such as a web browser API that exposes cookies to scripts). - /// - /// - /// See https://tools.ietf.org/html/rfc6265#section-4.1.2.6 - public bool HttpOnly { get; set; } - - /// - /// Gets a collection of additional values to append to the cookie. - /// - public IList Extensions { get; } = new List(); - - // name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={strict|lax|none}; httponly - /// - public override string ToString() - { - var length = _name.Length + EqualsToken.Length + _value.Length; - - string? maxAge = null; - string? sameSite = null; - - if (Expires.HasValue) - { - length += SeparatorToken.Length + ExpiresToken.Length + EqualsToken.Length + ExpiresDateLength; - } - - if (MaxAge.HasValue) - { - maxAge = HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.GetValueOrDefault().TotalSeconds); - length += SeparatorToken.Length + MaxAgeToken.Length + EqualsToken.Length + maxAge.Length; - } - - if (Domain != null) - { - length += SeparatorToken.Length + DomainToken.Length + EqualsToken.Length + Domain.Length; - } - - if (Path != null) - { - length += SeparatorToken.Length + PathToken.Length + EqualsToken.Length + Path.Length; - } - - if (Secure) - { - length += SeparatorToken.Length + SecureToken.Length; - } + /// + /// Gets or sets a value for the Expires cookie attribute. + /// + /// The Expires attribute indicates the maximum lifetime of the cookie, + /// represented as the date and time at which the cookie expires. + /// + /// + /// See https://tools.ietf.org/html/rfc6265#section-4.1.2.1 + public DateTimeOffset? Expires { get; set; } - // Allow for Unspecified (-1) to skip SameSite - if (SameSite == SameSiteMode.None) - { - sameSite = SameSiteNoneToken; - length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length; - } - else if (SameSite == SameSiteMode.Lax) - { - sameSite = SameSiteLaxToken; - length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length; - } - else if (SameSite == SameSiteMode.Strict) - { - sameSite = SameSiteStrictToken; - length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length; - } + /// + /// Gets or sets a value for the Max-Age cookie attribute. + /// + /// The Max-Age attribute indicates the maximum lifetime of the cookie, + /// represented as the number of seconds until the cookie expires. + /// + /// + /// See https://tools.ietf.org/html/rfc6265#section-4.1.2.2 + public TimeSpan? MaxAge { get; set; } - if (HttpOnly) - { - length += SeparatorToken.Length + HttpOnlyToken.Length; - } + /// + /// Gets or sets a value for the Domain cookie attribute. + /// + /// The Domain attribute specifies those hosts to which the cookie will + /// be sent. + /// + /// + /// See https://tools.ietf.org/html/rfc6265#section-4.1.2.3 + public StringSegment Domain { get; set; } - foreach (var extension in Extensions) - { - length += SeparatorToken.Length + extension.Length; - } + /// + /// Gets or sets a value for the Path cookie attribute. + /// + /// The path attribute specifies those hosts to which the cookie will + /// be sent. + /// + /// + /// See https://tools.ietf.org/html/rfc6265#section-4.1.2.4 + public StringSegment Path { get; set; } - return string.Create(length, (this, maxAge, sameSite), (span, tuple) => - { - var (headerValue, maxAgeValue, sameSite) = tuple; + /// + /// Gets or sets a value for the Secure cookie attribute. + /// + /// The Secure attribute limits the scope of the cookie to "secure" + /// channels. + /// + /// + /// See https://tools.ietf.org/html/rfc6265#section-4.1.2.5 + public bool Secure { get; set; } - Append(ref span, headerValue._name); - Append(ref span, EqualsToken); - Append(ref span, headerValue._value); + /// + /// Gets or sets a value for the SameSite cookie attribute. + /// + /// "SameSite" cookies offer a robust defense against CSRF attack when + /// deployed in strict mode, and when supported by the client. + /// + /// + /// See https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-05#section-8.8 + public SameSiteMode SameSite { get; set; } = SameSiteMode.Unspecified; - if (headerValue.Expires is DateTimeOffset expiresValue) - { - Append(ref span, SeparatorToken); - Append(ref span, ExpiresToken); - Append(ref span, EqualsToken); + /// + /// Gets or sets a value for the HttpOnly cookie attribute. + /// + /// HttpOnly instructs the user agent to + /// omit the cookie when providing access to cookies via "non-HTTP" APIs + /// (such as a web browser API that exposes cookies to scripts). + /// + /// + /// See https://tools.ietf.org/html/rfc6265#section-4.1.2.6 + public bool HttpOnly { get; set; } - var formatted = expiresValue.TryFormat(span, out var charsWritten, ExpiresDateFormat); - span = span.Slice(charsWritten); + /// + /// Gets a collection of additional values to append to the cookie. + /// + public IList Extensions { get; } = new List(); - Debug.Assert(formatted); - } + // name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={strict|lax|none}; httponly + /// + public override string ToString() + { + var length = _name.Length + EqualsToken.Length + _value.Length; - if (maxAgeValue != null) - { - AppendSegment(ref span, MaxAgeToken, maxAgeValue); - } + string? maxAge = null; + string? sameSite = null; - if (headerValue.Domain != null) - { - AppendSegment(ref span, DomainToken, headerValue.Domain); - } + if (Expires.HasValue) + { + length += SeparatorToken.Length + ExpiresToken.Length + EqualsToken.Length + ExpiresDateLength; + } - if (headerValue.Path != null) - { - AppendSegment(ref span, PathToken, headerValue.Path); - } + if (MaxAge.HasValue) + { + maxAge = HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.GetValueOrDefault().TotalSeconds); + length += SeparatorToken.Length + MaxAgeToken.Length + EqualsToken.Length + maxAge.Length; + } - if (headerValue.Secure) - { - AppendSegment(ref span, SecureToken, null); - } + if (Domain != null) + { + length += SeparatorToken.Length + DomainToken.Length + EqualsToken.Length + Domain.Length; + } - if (sameSite != null) - { - AppendSegment(ref span, SameSiteToken, sameSite); - } + if (Path != null) + { + length += SeparatorToken.Length + PathToken.Length + EqualsToken.Length + Path.Length; + } - if (headerValue.HttpOnly) - { - AppendSegment(ref span, HttpOnlyToken, null); - } + if (Secure) + { + length += SeparatorToken.Length + SecureToken.Length; + } - foreach (var extension in Extensions) - { - AppendSegment(ref span, extension, null); - } - }); + // Allow for Unspecified (-1) to skip SameSite + if (SameSite == SameSiteMode.None) + { + sameSite = SameSiteNoneToken; + length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length; + } + else if (SameSite == SameSiteMode.Lax) + { + sameSite = SameSiteLaxToken; + length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length; + } + else if (SameSite == SameSiteMode.Strict) + { + sameSite = SameSiteStrictToken; + length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length; } - private static void AppendSegment(ref Span span, StringSegment name, StringSegment value) + if (HttpOnly) { - Append(ref span, SeparatorToken); - Append(ref span, name.AsSpan()); - if (value != null) - { - Append(ref span, EqualsToken); - Append(ref span, value.AsSpan()); - } + length += SeparatorToken.Length + HttpOnlyToken.Length; } - private static void Append(ref Span span, ReadOnlySpan other) + foreach (var extension in Extensions) { - other.CopyTo(span); - span = span.Slice(other.Length); + length += SeparatorToken.Length + extension.Length; } - /// - /// Append string representation of this to given - /// . - /// - /// - /// The to receive the string representation of this - /// . - /// - public void AppendToStringBuilder(StringBuilder builder) + return string.Create(length, (this, maxAge, sameSite), (span, tuple) => { - builder.Append(_name.AsSpan()); - builder.Append('='); - builder.Append(_value.AsSpan()); + var (headerValue, maxAgeValue, sameSite) = tuple; - if (Expires.HasValue) - { - AppendSegment(builder, ExpiresToken, HeaderUtilities.FormatDate(Expires.GetValueOrDefault())); - } + Append(ref span, headerValue._name); + Append(ref span, EqualsToken); + Append(ref span, headerValue._value); - if (MaxAge.HasValue) + if (headerValue.Expires is DateTimeOffset expiresValue) { - AppendSegment(builder, MaxAgeToken, HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.GetValueOrDefault().TotalSeconds)); - } + Append(ref span, SeparatorToken); + Append(ref span, ExpiresToken); + Append(ref span, EqualsToken); - if (Domain != null) - { - AppendSegment(builder, DomainToken, Domain); + var formatted = expiresValue.TryFormat(span, out var charsWritten, ExpiresDateFormat); + span = span.Slice(charsWritten); + + Debug.Assert(formatted); } - if (Path != null) + if (maxAgeValue != null) { - AppendSegment(builder, PathToken, Path); + AppendSegment(ref span, MaxAgeToken, maxAgeValue); } - if (Secure) + if (headerValue.Domain != null) { - AppendSegment(builder, SecureToken, null); + AppendSegment(ref span, DomainToken, headerValue.Domain); } - // Allow for Unspecified (-1) to skip SameSite - if (SameSite == SameSiteMode.None) + if (headerValue.Path != null) { - AppendSegment(builder, SameSiteToken, SameSiteNoneToken); + AppendSegment(ref span, PathToken, headerValue.Path); } - else if (SameSite == SameSiteMode.Lax) + + if (headerValue.Secure) { - AppendSegment(builder, SameSiteToken, SameSiteLaxToken); + AppendSegment(ref span, SecureToken, null); } - else if (SameSite == SameSiteMode.Strict) + + if (sameSite != null) { - AppendSegment(builder, SameSiteToken, SameSiteStrictToken); + AppendSegment(ref span, SameSiteToken, sameSite); } - if (HttpOnly) + if (headerValue.HttpOnly) { - AppendSegment(builder, HttpOnlyToken, null); + AppendSegment(ref span, HttpOnlyToken, null); } foreach (var extension in Extensions) { - AppendSegment(builder, extension, null); + AppendSegment(ref span, extension, null); } + }); + } + + private static void AppendSegment(ref Span span, StringSegment name, StringSegment value) + { + Append(ref span, SeparatorToken); + Append(ref span, name.AsSpan()); + if (value != null) + { + Append(ref span, EqualsToken); + Append(ref span, value.AsSpan()); } + } - private static void AppendSegment(StringBuilder builder, StringSegment name, StringSegment value) + private static void Append(ref Span span, ReadOnlySpan other) + { + other.CopyTo(span); + span = span.Slice(other.Length); + } + + /// + /// Append string representation of this to given + /// . + /// + /// + /// The to receive the string representation of this + /// . + /// + public void AppendToStringBuilder(StringBuilder builder) + { + builder.Append(_name.AsSpan()); + builder.Append('='); + builder.Append(_value.AsSpan()); + + if (Expires.HasValue) { - builder.Append("; "); - builder.Append(name.AsSpan()); - if (value != null) - { - builder.Append('='); - builder.Append(value.AsSpan()); - } + AppendSegment(builder, ExpiresToken, HeaderUtilities.FormatDate(Expires.GetValueOrDefault())); + } + + if (MaxAge.HasValue) + { + AppendSegment(builder, MaxAgeToken, HeaderUtilities.FormatNonNegativeInt64((long)MaxAge.GetValueOrDefault().TotalSeconds)); + } + + if (Domain != null) + { + AppendSegment(builder, DomainToken, Domain); } - /// - /// Parses as a value. - /// - /// The values to parse. - /// The parsed values. - public static SetCookieHeaderValue Parse(StringSegment input) + if (Path != null) { - var index = 0; - return SingleValueParser.ParseValue(input, ref index)!; + AppendSegment(builder, PathToken, Path); } - /// - /// Attempts to parse the specified as a . - /// - /// The value to parse. - /// The parsed value. - /// if input is a valid , otherwise . - public static bool TryParse(StringSegment input, [NotNullWhen(true)] out SetCookieHeaderValue? parsedValue) + if (Secure) { - var index = 0; - return SingleValueParser.TryParseValue(input, ref index, out parsedValue!); + AppendSegment(builder, SecureToken, null); } - /// - /// Parses a sequence of inputs as a sequence of values. - /// - /// The values to parse. - /// The parsed values. - public static IList ParseList(IList? inputs) + // Allow for Unspecified (-1) to skip SameSite + if (SameSite == SameSiteMode.None) + { + AppendSegment(builder, SameSiteToken, SameSiteNoneToken); + } + else if (SameSite == SameSiteMode.Lax) + { + AppendSegment(builder, SameSiteToken, SameSiteLaxToken); + } + else if (SameSite == SameSiteMode.Strict) { - return MultipleValueParser.ParseValues(inputs); + AppendSegment(builder, SameSiteToken, SameSiteStrictToken); } - /// - /// Parses a sequence of inputs as a sequence of values using string parsing rules. - /// - /// The values to parse. - /// The parsed values. - public static IList ParseStrictList(IList? inputs) + if (HttpOnly) { - return MultipleValueParser.ParseStrictValues(inputs); + AppendSegment(builder, HttpOnlyToken, null); } - /// - /// Attempts to parse the sequence of values as a sequence of . - /// - /// The values to parse. - /// The parsed values. - /// if all inputs are valid , otherwise . - public static bool TryParseList(IList? inputs, [NotNullWhen(true)] out IList? parsedValues) + foreach (var extension in Extensions) { - return MultipleValueParser.TryParseValues(inputs, out parsedValues); + AppendSegment(builder, extension, null); } + } - /// - /// Attempts to parse the sequence of values as a sequence of using string parsing rules. - /// - /// The values to parse. - /// The parsed values. - /// if all inputs are valid , otherwise . - public static bool TryParseStrictList(IList? inputs, [NotNullWhen(true)] out IList? parsedValues) + private static void AppendSegment(StringBuilder builder, StringSegment name, StringSegment value) + { + builder.Append("; "); + builder.Append(name.AsSpan()); + if (value != null) { - return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); + builder.Append('='); + builder.Append(value.AsSpan()); } + } + + /// + /// Parses as a value. + /// + /// The values to parse. + /// The parsed values. + public static SetCookieHeaderValue Parse(StringSegment input) + { + var index = 0; + return SingleValueParser.ParseValue(input, ref index)!; + } + + /// + /// Attempts to parse the specified as a . + /// + /// The value to parse. + /// The parsed value. + /// if input is a valid , otherwise . + public static bool TryParse(StringSegment input, [NotNullWhen(true)] out SetCookieHeaderValue? parsedValue) + { + var index = 0; + return SingleValueParser.TryParseValue(input, ref index, out parsedValue!); + } + + /// + /// Parses a sequence of inputs as a sequence of values. + /// + /// The values to parse. + /// The parsed values. + public static IList ParseList(IList? inputs) + { + return MultipleValueParser.ParseValues(inputs); + } + + /// + /// Parses a sequence of inputs as a sequence of values using string parsing rules. + /// + /// The values to parse. + /// The parsed values. + public static IList ParseStrictList(IList? inputs) + { + return MultipleValueParser.ParseStrictValues(inputs); + } + + /// + /// Attempts to parse the sequence of values as a sequence of . + /// + /// The values to parse. + /// The parsed values. + /// if all inputs are valid , otherwise . + public static bool TryParseList(IList? inputs, [NotNullWhen(true)] out IList? parsedValues) + { + return MultipleValueParser.TryParseValues(inputs, out parsedValues); + } + + /// + /// Attempts to parse the sequence of values as a sequence of using string parsing rules. + /// + /// The values to parse. + /// The parsed values. + /// if all inputs are valid , otherwise . + public static bool TryParseStrictList(IList? inputs, [NotNullWhen(true)] out IList? parsedValues) + { + return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); + } + + // name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax|None}; httponly + private static int GetSetCookieLength(StringSegment input, int startIndex, out SetCookieHeaderValue? parsedValue) + { + Contract.Requires(startIndex >= 0); + var offset = startIndex; + + parsedValue = null; - // name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax|None}; httponly - private static int GetSetCookieLength(StringSegment input, int startIndex, out SetCookieHeaderValue? parsedValue) + if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length)) { - Contract.Requires(startIndex >= 0); - var offset = startIndex; + return 0; + } - parsedValue = null; + var result = new SetCookieHeaderValue(); - if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length)) - { - return 0; - } + // The caller should have already consumed any leading whitespace, commas, etc.. - var result = new SetCookieHeaderValue(); + // Name=value; - // The caller should have already consumed any leading whitespace, commas, etc.. + // Name + var itemLength = HttpRuleParser.GetTokenLength(input, offset); + if (itemLength == 0) + { + return 0; + } + result._name = input.Subsegment(offset, itemLength); + offset += itemLength; - // Name=value; + // = (no spaces) + if (!ReadEqualsSign(input, ref offset)) + { + return 0; + } - // Name - var itemLength = HttpRuleParser.GetTokenLength(input, offset); - if (itemLength == 0) + // value or "quoted value" + // The value may be empty + result._value = CookieHeaderParserShared.GetCookieValue(input, ref offset); + + // *(';' SP cookie-av) + while (offset < input.Length) + { + if (input[offset] == ',') { - return 0; + // Divider between headers + break; } - result._name = input.Subsegment(offset, itemLength); - offset += itemLength; - - // = (no spaces) - if (!ReadEqualsSign(input, ref offset)) + if (input[offset] != ';') { + // Expecting a ';' between parameters return 0; } + offset++; + + offset += HttpRuleParser.GetWhitespaceLength(input, offset); - // value or "quoted value" - // The value may be empty - result._value = CookieHeaderParserShared.GetCookieValue(input, ref offset); + // cookie-av = expires-av / max-age-av / domain-av / path-av / secure-av / samesite-av / httponly-av / extension-av + itemLength = HttpRuleParser.GetTokenLength(input, offset); + if (itemLength == 0) + { + // Trailing ';' or leading into garbage. Let the next parser fail. + break; + } + var token = input.Subsegment(offset, itemLength); + offset += itemLength; - // *(';' SP cookie-av) - while (offset < input.Length) + // expires-av = "Expires=" sane-cookie-date + if (StringSegment.Equals(token, ExpiresToken, StringComparison.OrdinalIgnoreCase)) { - if (input[offset] == ',') + // = (no spaces) + if (!ReadEqualsSign(input, ref offset)) { - // Divider between headers - break; + return 0; } - if (input[offset] != ';') + // We don't want to include comma, becouse date may contain it (eg. Sun, 06 Nov...) + var dateString = ReadToSemicolonOrEnd(input, ref offset, includeComma: false); + DateTimeOffset expirationDate; + if (!HttpRuleParser.TryStringToDate(dateString, out expirationDate)) { - // Expecting a ';' between parameters + // Invalid expiration date, abort return 0; } - offset++; - - offset += HttpRuleParser.GetWhitespaceLength(input, offset); - - // cookie-av = expires-av / max-age-av / domain-av / path-av / secure-av / samesite-av / httponly-av / extension-av - itemLength = HttpRuleParser.GetTokenLength(input, offset); - if (itemLength == 0) + result.Expires = expirationDate; + } + // max-age-av = "Max-Age=" non-zero-digit *DIGIT + else if (StringSegment.Equals(token, MaxAgeToken, StringComparison.OrdinalIgnoreCase)) + { + // = (no spaces) + if (!ReadEqualsSign(input, ref offset)) { - // Trailing ';' or leading into garbage. Let the next parser fail. - break; + return 0; } - var token = input.Subsegment(offset, itemLength); - offset += itemLength; - // expires-av = "Expires=" sane-cookie-date - if (StringSegment.Equals(token, ExpiresToken, StringComparison.OrdinalIgnoreCase)) + itemLength = HttpRuleParser.GetNumberLength(input, offset, allowDecimal: false); + if (itemLength == 0) { - // = (no spaces) - if (!ReadEqualsSign(input, ref offset)) - { - return 0; - } - // We don't want to include comma, becouse date may contain it (eg. Sun, 06 Nov...) - var dateString = ReadToSemicolonOrEnd(input, ref offset, includeComma: false); - DateTimeOffset expirationDate; - if (!HttpRuleParser.TryStringToDate(dateString, out expirationDate)) - { - // Invalid expiration date, abort - return 0; - } - result.Expires = expirationDate; + return 0; } - // max-age-av = "Max-Age=" non-zero-digit *DIGIT - else if (StringSegment.Equals(token, MaxAgeToken, StringComparison.OrdinalIgnoreCase)) + var numberString = input.Subsegment(offset, itemLength); + long maxAge; + if (!HeaderUtilities.TryParseNonNegativeInt64(numberString, out maxAge)) { - // = (no spaces) - if (!ReadEqualsSign(input, ref offset)) - { - return 0; - } - - itemLength = HttpRuleParser.GetNumberLength(input, offset, allowDecimal: false); - if (itemLength == 0) - { - return 0; - } - var numberString = input.Subsegment(offset, itemLength); - long maxAge; - if (!HeaderUtilities.TryParseNonNegativeInt64(numberString, out maxAge)) - { - // Invalid expiration date, abort - return 0; - } - result.MaxAge = TimeSpan.FromSeconds(maxAge); - offset += itemLength; + // Invalid expiration date, abort + return 0; } - // domain-av = "Domain=" domain-value - // domain-value = ; defined in [RFC1034], Section 3.5, as enhanced by [RFC1123], Section 2.1 - else if (StringSegment.Equals(token, DomainToken, StringComparison.OrdinalIgnoreCase)) + result.MaxAge = TimeSpan.FromSeconds(maxAge); + offset += itemLength; + } + // domain-av = "Domain=" domain-value + // domain-value = ; defined in [RFC1034], Section 3.5, as enhanced by [RFC1123], Section 2.1 + else if (StringSegment.Equals(token, DomainToken, StringComparison.OrdinalIgnoreCase)) + { + // = (no spaces) + if (!ReadEqualsSign(input, ref offset)) { - // = (no spaces) - if (!ReadEqualsSign(input, ref offset)) - { - return 0; - } - // We don't do any detailed validation on the domain. - result.Domain = ReadToSemicolonOrEnd(input, ref offset); + return 0; } - // path-av = "Path=" path-value - // path-value = - else if (StringSegment.Equals(token, PathToken, StringComparison.OrdinalIgnoreCase)) + // We don't do any detailed validation on the domain. + result.Domain = ReadToSemicolonOrEnd(input, ref offset); + } + // path-av = "Path=" path-value + // path-value = + else if (StringSegment.Equals(token, PathToken, StringComparison.OrdinalIgnoreCase)) + { + // = (no spaces) + if (!ReadEqualsSign(input, ref offset)) { - // = (no spaces) - if (!ReadEqualsSign(input, ref offset)) - { - return 0; - } - // We don't do any detailed validation on the path. - result.Path = ReadToSemicolonOrEnd(input, ref offset); + return 0; } - // secure-av = "Secure" - else if (StringSegment.Equals(token, SecureToken, StringComparison.OrdinalIgnoreCase)) + // We don't do any detailed validation on the path. + result.Path = ReadToSemicolonOrEnd(input, ref offset); + } + // secure-av = "Secure" + else if (StringSegment.Equals(token, SecureToken, StringComparison.OrdinalIgnoreCase)) + { + result.Secure = true; + } + // samesite-av = "SameSite=" samesite-value + // samesite-value = "Strict" / "Lax" / "None" + else if (StringSegment.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase)) + { + if (!ReadEqualsSign(input, ref offset)) { - result.Secure = true; + result.SameSite = SameSiteMode.Unspecified; } - // samesite-av = "SameSite=" samesite-value - // samesite-value = "Strict" / "Lax" / "None" - else if (StringSegment.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase)) + else { - if (!ReadEqualsSign(input, ref offset)) + var enforcementMode = ReadToSemicolonOrEnd(input, ref offset); + + if (StringSegment.Equals(enforcementMode, SameSiteStrictToken, StringComparison.OrdinalIgnoreCase)) { - result.SameSite = SameSiteMode.Unspecified; + result.SameSite = SameSiteMode.Strict; + } + else if (StringSegment.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase)) + { + result.SameSite = SameSiteMode.Lax; + } + else if (StringSegment.Equals(enforcementMode, SameSiteNoneToken, StringComparison.OrdinalIgnoreCase)) + { + result.SameSite = SameSiteMode.None; } else { - var enforcementMode = ReadToSemicolonOrEnd(input, ref offset); - - if (StringSegment.Equals(enforcementMode, SameSiteStrictToken, StringComparison.OrdinalIgnoreCase)) - { - result.SameSite = SameSiteMode.Strict; - } - else if (StringSegment.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase)) - { - result.SameSite = SameSiteMode.Lax; - } - else if (StringSegment.Equals(enforcementMode, SameSiteNoneToken, StringComparison.OrdinalIgnoreCase)) - { - result.SameSite = SameSiteMode.None; - } - else - { - result.SameSite = SameSiteMode.Unspecified; - } + result.SameSite = SameSiteMode.Unspecified; } } - // httponly-av = "HttpOnly" - else if (StringSegment.Equals(token, HttpOnlyToken, StringComparison.OrdinalIgnoreCase)) - { - result.HttpOnly = true; - } - // extension-av = - else - { - var tokenStart = offset - itemLength; - ReadToSemicolonOrEnd(input, ref offset, includeComma: true); - result.Extensions.Add(input.Subsegment(tokenStart, offset - tokenStart)); - } } - - parsedValue = result; - return offset - startIndex; + // httponly-av = "HttpOnly" + else if (StringSegment.Equals(token, HttpOnlyToken, StringComparison.OrdinalIgnoreCase)) + { + result.HttpOnly = true; + } + // extension-av = + else + { + var tokenStart = offset - itemLength; + ReadToSemicolonOrEnd(input, ref offset, includeComma: true); + result.Extensions.Add(input.Subsegment(tokenStart, offset - tokenStart)); + } } - private static bool ReadEqualsSign(StringSegment input, ref int offset) + parsedValue = result; + return offset - startIndex; + } + + private static bool ReadEqualsSign(StringSegment input, ref int offset) + { + // = (no spaces) + if (offset >= input.Length || input[offset] != '=') { - // = (no spaces) - if (offset >= input.Length || input[offset] != '=') - { - return false; - } - offset++; - return true; + return false; } + offset++; + return true; + } - private static StringSegment ReadToSemicolonOrEnd(StringSegment input, ref int offset, bool includeComma = true) + private static StringSegment ReadToSemicolonOrEnd(StringSegment input, ref int offset, bool includeComma = true) + { + var end = input.IndexOf(';', offset); + if (end < 0) { - var end = input.IndexOf(';', offset); - if (end < 0) + // Also valid end of cookie + if (includeComma) { - // Also valid end of cookie - if (includeComma) - { - end = input.IndexOf(',', offset); - } + end = input.IndexOf(',', offset); } - else if (includeComma) - { - var commaPosition = input.IndexOf(',', offset); - if (commaPosition >= 0 && commaPosition < end) - { - end = commaPosition; - } - } - - if (end < 0) + } + else if (includeComma) + { + var commaPosition = input.IndexOf(',', offset); + if (commaPosition >= 0 && commaPosition < end) { - // Remainder of the string - end = input.Length; + end = commaPosition; } - - var itemLength = end - offset; - var result = input.Subsegment(offset, itemLength); - offset += itemLength; - return result; } - /// - public override bool Equals(object? obj) + if (end < 0) { - var other = obj as SetCookieHeaderValue; + // Remainder of the string + end = input.Length; + } - if (other == null) - { - return false; - } + var itemLength = end - offset; + var result = input.Subsegment(offset, itemLength); + offset += itemLength; + return result; + } - return StringSegment.Equals(_name, other._name, StringComparison.OrdinalIgnoreCase) - && StringSegment.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase) - && Expires.Equals(other.Expires) - && MaxAge.Equals(other.MaxAge) - && StringSegment.Equals(Domain, other.Domain, StringComparison.OrdinalIgnoreCase) - && StringSegment.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase) - && Secure == other.Secure - && SameSite == other.SameSite - && HttpOnly == other.HttpOnly - && HeaderUtilities.AreEqualCollections(Extensions, other.Extensions, StringSegmentComparer.OrdinalIgnoreCase); - } - - /// - public override int GetHashCode() - { - var hash = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name) - ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value) - ^ (Expires.HasValue ? Expires.GetHashCode() : 0) - ^ (MaxAge.HasValue ? MaxAge.GetHashCode() : 0) - ^ (Domain != null ? StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(Domain) : 0) - ^ (Path != null ? StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(Path) : 0) - ^ Secure.GetHashCode() - ^ SameSite.GetHashCode() - ^ HttpOnly.GetHashCode(); + /// + public override bool Equals(object? obj) + { + var other = obj as SetCookieHeaderValue; - foreach (var extension in Extensions) - { - hash ^= extension.GetHashCode(); - } + if (other == null) + { + return false; + } - return hash; + return StringSegment.Equals(_name, other._name, StringComparison.OrdinalIgnoreCase) + && StringSegment.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase) + && Expires.Equals(other.Expires) + && MaxAge.Equals(other.MaxAge) + && StringSegment.Equals(Domain, other.Domain, StringComparison.OrdinalIgnoreCase) + && StringSegment.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase) + && Secure == other.Secure + && SameSite == other.SameSite + && HttpOnly == other.HttpOnly + && HeaderUtilities.AreEqualCollections(Extensions, other.Extensions, StringSegmentComparer.OrdinalIgnoreCase); + } + + /// + public override int GetHashCode() + { + var hash = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name) + ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value) + ^ (Expires.HasValue ? Expires.GetHashCode() : 0) + ^ (MaxAge.HasValue ? MaxAge.GetHashCode() : 0) + ^ (Domain != null ? StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(Domain) : 0) + ^ (Path != null ? StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(Path) : 0) + ^ Secure.GetHashCode() + ^ SameSite.GetHashCode() + ^ HttpOnly.GetHashCode(); + + foreach (var extension in Extensions) + { + hash ^= extension.GetHashCode(); } + + return hash; } } diff --git a/src/Http/Headers/src/StringWithQualityHeaderValue.cs b/src/Http/Headers/src/StringWithQualityHeaderValue.cs index 6c18794217..20cc699259 100644 --- a/src/Http/Headers/src/StringWithQualityHeaderValue.cs +++ b/src/Http/Headers/src/StringWithQualityHeaderValue.cs @@ -8,264 +8,263 @@ using System.Diagnostics.Contracts; using System.Globalization; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +/// +/// A string header value with an optional quality. +/// +public class StringWithQualityHeaderValue { + private static readonly HttpHeaderParser SingleValueParser + = new GenericHeaderParser(false, GetStringWithQualityLength); + private static readonly HttpHeaderParser MultipleValueParser + = new GenericHeaderParser(true, GetStringWithQualityLength); + + private StringSegment _value; + private double? _quality; + + private StringWithQualityHeaderValue() + { + // Used by the parser to create a new instance of this type. + } + /// - /// A string header value with an optional quality. + /// Initializes a new instance of . /// - public class StringWithQualityHeaderValue + /// The used to initialize the new instance. + public StringWithQualityHeaderValue(StringSegment value) { - private static readonly HttpHeaderParser SingleValueParser - = new GenericHeaderParser(false, GetStringWithQualityLength); - private static readonly HttpHeaderParser MultipleValueParser - = new GenericHeaderParser(true, GetStringWithQualityLength); + HeaderUtilities.CheckValidToken(value, nameof(value)); - private StringSegment _value; - private double? _quality; + _value = value; + } - private StringWithQualityHeaderValue() - { - // Used by the parser to create a new instance of this type. - } + /// + /// Initializes a new instance of . + /// + /// The used to initialize the new instance. + /// The quality factor. + public StringWithQualityHeaderValue(StringSegment value, double quality) + { + HeaderUtilities.CheckValidToken(value, nameof(value)); - /// - /// Initializes a new instance of . - /// - /// The used to initialize the new instance. - public StringWithQualityHeaderValue(StringSegment value) + if ((quality < 0) || (quality > 1)) { - HeaderUtilities.CheckValidToken(value, nameof(value)); - - _value = value; + throw new ArgumentOutOfRangeException(nameof(quality)); } - /// - /// Initializes a new instance of . - /// - /// The used to initialize the new instance. - /// The quality factor. - public StringWithQualityHeaderValue(StringSegment value, double quality) - { - HeaderUtilities.CheckValidToken(value, nameof(value)); + _value = value; + _quality = quality; + } - if ((quality < 0) || (quality > 1)) - { - throw new ArgumentOutOfRangeException(nameof(quality)); - } + /// + /// Gets the string header value. + /// + public StringSegment Value => _value; - _value = value; - _quality = quality; + /// + /// Gets the quality factor. + /// + public double? Quality => _quality; + + /// + public override string ToString() + { + if (_quality.HasValue) + { + return _value + "; q=" + _quality.GetValueOrDefault().ToString("0.0##", NumberFormatInfo.InvariantInfo); } - /// - /// Gets the string header value. - /// - public StringSegment Value => _value; + return _value.ToString(); + } - /// - /// Gets the quality factor. - /// - public double? Quality => _quality; + /// + public override bool Equals(object? obj) + { + var other = obj as StringWithQualityHeaderValue; - /// - public override string ToString() + if (other == null) { - if (_quality.HasValue) - { - return _value + "; q=" + _quality.GetValueOrDefault().ToString("0.0##", NumberFormatInfo.InvariantInfo); - } - - return _value.ToString(); + return false; } - /// - public override bool Equals(object? obj) + if (!StringSegment.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase)) { - var other = obj as StringWithQualityHeaderValue; - - if (other == null) - { - return false; - } - - if (!StringSegment.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - if (_quality.HasValue) - { - // Note that we don't consider double.Epsilon here. We really consider two values equal if they're - // actually equal. This makes sure that we also get the same hashcode for two values considered equal - // by Equals(). - return other._quality.HasValue && (_quality.GetValueOrDefault() == other._quality.Value); - } - - // If we don't have a quality value, then 'other' must also have no quality assigned in order to be - // considered equal. - return !other._quality.HasValue; + return false; } - /// - public override int GetHashCode() + if (_quality.HasValue) { - var result = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value); - - if (_quality.HasValue) - { - result = result ^ _quality.GetValueOrDefault().GetHashCode(); - } - - return result; + // Note that we don't consider double.Epsilon here. We really consider two values equal if they're + // actually equal. This makes sure that we also get the same hashcode for two values considered equal + // by Equals(). + return other._quality.HasValue && (_quality.GetValueOrDefault() == other._quality.Value); } - /// - /// Parses the specified as a . - /// - /// The value to parse. - /// The parsed value. - public static StringWithQualityHeaderValue Parse(StringSegment input) - { - var index = 0; - return SingleValueParser.ParseValue(input, ref index)!; - } + // If we don't have a quality value, then 'other' must also have no quality assigned in order to be + // considered equal. + return !other._quality.HasValue; + } - /// - /// Attempts to parse the specified as a . - /// - /// The value to parse. - /// The parsed value. - /// if input is a valid , otherwise . - public static bool TryParse(StringSegment input, [NotNullWhen(true)] out StringWithQualityHeaderValue parsedValue) - { - var index = 0; - return SingleValueParser.TryParseValue(input, ref index, out parsedValue!); - } + /// + public override int GetHashCode() + { + var result = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value); - /// - /// Parses a sequence of inputs as a sequence of values. - /// - /// The values to parse. - /// The parsed values. - public static IList ParseList(IList? input) + if (_quality.HasValue) { - return MultipleValueParser.ParseValues(input); + result = result ^ _quality.GetValueOrDefault().GetHashCode(); } - /// - /// Parses a sequence of inputs as a sequence of values using string parsing rules. - /// - /// The values to parse. - /// The parsed values. - public static IList ParseStrictList(IList? input) - { - return MultipleValueParser.ParseStrictValues(input); - } + return result; + } - /// - /// Attempts to parse the sequence of values as a sequence of . - /// - /// The values to parse. - /// The parsed values. - /// if all inputs are valid , otherwise . - public static bool TryParseList(IList? input, [NotNullWhen(true)] out IList? parsedValues) - { - return MultipleValueParser.TryParseValues(input, out parsedValues); - } + /// + /// Parses the specified as a . + /// + /// The value to parse. + /// The parsed value. + public static StringWithQualityHeaderValue Parse(StringSegment input) + { + var index = 0; + return SingleValueParser.ParseValue(input, ref index)!; + } - /// - /// Attempts to parse the sequence of values as a sequence of using string parsing rules. - /// - /// The values to parse. - /// The parsed values. - /// if all inputs are valid , otherwise . - public static bool TryParseStrictList(IList? input, [NotNullWhen(true)] out IList? parsedValues) - { - return MultipleValueParser.TryParseStrictValues(input, out parsedValues); - } + /// + /// Attempts to parse the specified as a . + /// + /// The value to parse. + /// The parsed value. + /// if input is a valid , otherwise . + public static bool TryParse(StringSegment input, [NotNullWhen(true)] out StringWithQualityHeaderValue parsedValue) + { + var index = 0; + return SingleValueParser.TryParseValue(input, ref index, out parsedValue!); + } - private static int GetStringWithQualityLength(StringSegment input, int startIndex, out StringWithQualityHeaderValue? parsedValue) - { - Contract.Requires(startIndex >= 0); + /// + /// Parses a sequence of inputs as a sequence of values. + /// + /// The values to parse. + /// The parsed values. + public static IList ParseList(IList? input) + { + return MultipleValueParser.ParseValues(input); + } - parsedValue = null; + /// + /// Parses a sequence of inputs as a sequence of values using string parsing rules. + /// + /// The values to parse. + /// The parsed values. + public static IList ParseStrictList(IList? input) + { + return MultipleValueParser.ParseStrictValues(input); + } - if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) - { - return 0; - } + /// + /// Attempts to parse the sequence of values as a sequence of . + /// + /// The values to parse. + /// The parsed values. + /// if all inputs are valid , otherwise . + public static bool TryParseList(IList? input, [NotNullWhen(true)] out IList? parsedValues) + { + return MultipleValueParser.TryParseValues(input, out parsedValues); + } - // Parse the value string: in '; q=' - var valueLength = HttpRuleParser.GetTokenLength(input, startIndex); + /// + /// Attempts to parse the sequence of values as a sequence of using string parsing rules. + /// + /// The values to parse. + /// The parsed values. + /// if all inputs are valid , otherwise . + public static bool TryParseStrictList(IList? input, [NotNullWhen(true)] out IList? parsedValues) + { + return MultipleValueParser.TryParseStrictValues(input, out parsedValues); + } - if (valueLength == 0) - { - return 0; - } + private static int GetStringWithQualityLength(StringSegment input, int startIndex, out StringWithQualityHeaderValue? parsedValue) + { + Contract.Requires(startIndex >= 0); - StringWithQualityHeaderValue result = new StringWithQualityHeaderValue(); - result._value = input.Subsegment(startIndex, valueLength); - var current = startIndex + valueLength; - current = current + HttpRuleParser.GetWhitespaceLength(input, current); + parsedValue = null; - if ((current == input.Length) || (input[current] != ';')) - { - parsedValue = result; - return current - startIndex; // we have a valid token, but no quality. - } + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) + { + return 0; + } + + // Parse the value string: in '; q=' + var valueLength = HttpRuleParser.GetTokenLength(input, startIndex); - current++; // skip ';' separator - current = current + HttpRuleParser.GetWhitespaceLength(input, current); + if (valueLength == 0) + { + return 0; + } - // If we found a ';' separator, it must be followed by a quality information - if (!TryReadQuality(input, result, ref current)) - { - return 0; - } + StringWithQualityHeaderValue result = new StringWithQualityHeaderValue(); + result._value = input.Subsegment(startIndex, valueLength); + var current = startIndex + valueLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + if ((current == input.Length) || (input[current] != ';')) + { parsedValue = result; - return current - startIndex; + return current - startIndex; // we have a valid token, but no quality. } - private static bool TryReadQuality(StringSegment input, StringWithQualityHeaderValue result, ref int index) - { - var current = index; + current++; // skip ';' separator + current = current + HttpRuleParser.GetWhitespaceLength(input, current); - // See if we have a quality value by looking for "q" - if ((current == input.Length) || ((input[current] != 'q') && (input[current] != 'Q'))) - { - return false; - } + // If we found a ';' separator, it must be followed by a quality information + if (!TryReadQuality(input, result, ref current)) + { + return 0; + } - current++; // skip 'q' identifier - current = current + HttpRuleParser.GetWhitespaceLength(input, current); + parsedValue = result; + return current - startIndex; + } - // If we found "q" it must be followed by "=" - if ((current == input.Length) || (input[current] != '=')) - { - return false; - } + private static bool TryReadQuality(StringSegment input, StringWithQualityHeaderValue result, ref int index) + { + var current = index; - current++; // skip '=' separator - current = current + HttpRuleParser.GetWhitespaceLength(input, current); + // See if we have a quality value by looking for "q" + if ((current == input.Length) || ((input[current] != 'q') && (input[current] != 'Q'))) + { + return false; + } - if (current == input.Length) - { - return false; - } + current++; // skip 'q' identifier + current = current + HttpRuleParser.GetWhitespaceLength(input, current); - if (!HeaderUtilities.TryParseQualityDouble(input, current, out var quality, out var qualityLength)) - { - return false; - } + // If we found "q" it must be followed by "=" + if ((current == input.Length) || (input[current] != '=')) + { + return false; + } - result._quality = quality; + current++; // skip '=' separator + current = current + HttpRuleParser.GetWhitespaceLength(input, current); - current = current + qualityLength; - current = current + HttpRuleParser.GetWhitespaceLength(input, current); + if (current == input.Length) + { + return false; + } - index = current; - return true; + if (!HeaderUtilities.TryParseQualityDouble(input, current, out var quality, out var qualityLength)) + { + return false; } + + result._quality = quality; + + current = current + qualityLength; + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + index = current; + return true; } } diff --git a/src/Http/Headers/src/StringWithQualityHeaderValueComparer.cs b/src/Http/Headers/src/StringWithQualityHeaderValueComparer.cs index 9d3cc91e09..967d2aea46 100644 --- a/src/Http/Headers/src/StringWithQualityHeaderValueComparer.cs +++ b/src/Http/Headers/src/StringWithQualityHeaderValueComparer.cs @@ -5,76 +5,75 @@ using System; using System.Collections.Generic; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +/// +/// Implementation of that can compare content negotiation header fields +/// based on their quality values (a.k.a q-values). This applies to values used in accept-charset, +/// accept-encoding, accept-language and related header fields with similar syntax rules. See +/// for a comparer for media type +/// q-values. +/// +public class StringWithQualityHeaderValueComparer : IComparer { + private StringWithQualityHeaderValueComparer() + { + } + + /// + /// Gets the default instance of . + /// + public static StringWithQualityHeaderValueComparer QualityComparer { get; } = new StringWithQualityHeaderValueComparer(); + /// - /// Implementation of that can compare content negotiation header fields - /// based on their quality values (a.k.a q-values). This applies to values used in accept-charset, - /// accept-encoding, accept-language and related header fields with similar syntax rules. See - /// for a comparer for media type - /// q-values. + /// Compares two based on their quality value + /// (a.k.a their "q-value"). + /// Values with identical q-values are considered equal (i.e the result is 0) with the exception of wild-card + /// values (i.e. a value of "*") which are considered less than non-wild-card values. This allows to sort + /// a sequence of following their q-values ending up with any + /// wild-cards at the end. /// - public class StringWithQualityHeaderValueComparer : IComparer + /// The first value to compare. + /// The second value to compare + /// The result of the comparison. + public int Compare( + StringWithQualityHeaderValue? stringWithQuality1, + StringWithQualityHeaderValue? stringWithQuality2) { - private StringWithQualityHeaderValueComparer() + if (stringWithQuality1 == null) { + throw new ArgumentNullException(nameof(stringWithQuality1)); } - /// - /// Gets the default instance of . - /// - public static StringWithQualityHeaderValueComparer QualityComparer { get; } = new StringWithQualityHeaderValueComparer(); - - /// - /// Compares two based on their quality value - /// (a.k.a their "q-value"). - /// Values with identical q-values are considered equal (i.e the result is 0) with the exception of wild-card - /// values (i.e. a value of "*") which are considered less than non-wild-card values. This allows to sort - /// a sequence of following their q-values ending up with any - /// wild-cards at the end. - /// - /// The first value to compare. - /// The second value to compare - /// The result of the comparison. - public int Compare( - StringWithQualityHeaderValue? stringWithQuality1, - StringWithQualityHeaderValue? stringWithQuality2) + if (stringWithQuality2 == null) { - if (stringWithQuality1 == null) - { - throw new ArgumentNullException(nameof(stringWithQuality1)); - } + throw new ArgumentNullException(nameof(stringWithQuality2)); + } - if (stringWithQuality2 == null) - { - throw new ArgumentNullException(nameof(stringWithQuality2)); - } + var quality1 = stringWithQuality1.Quality ?? HeaderQuality.Match; + var quality2 = stringWithQuality2.Quality ?? HeaderQuality.Match; + var qualityDifference = quality1 - quality2; + if (qualityDifference < 0) + { + return -1; + } + else if (qualityDifference > 0) + { + return 1; + } - var quality1 = stringWithQuality1.Quality ?? HeaderQuality.Match; - var quality2 = stringWithQuality2.Quality ?? HeaderQuality.Match; - var qualityDifference = quality1 - quality2; - if (qualityDifference < 0) + if (!StringSegment.Equals(stringWithQuality1.Value, stringWithQuality2.Value, StringComparison.OrdinalIgnoreCase)) + { + if (StringSegment.Equals(stringWithQuality1.Value, "*", StringComparison.Ordinal)) { return -1; } - else if (qualityDifference > 0) + else if (StringSegment.Equals(stringWithQuality2.Value, "*", StringComparison.Ordinal)) { return 1; } - - if (!StringSegment.Equals(stringWithQuality1.Value, stringWithQuality2.Value, StringComparison.OrdinalIgnoreCase)) - { - if (StringSegment.Equals(stringWithQuality1.Value, "*", StringComparison.Ordinal)) - { - return -1; - } - else if (StringSegment.Equals(stringWithQuality2.Value, "*", StringComparison.Ordinal)) - { - return 1; - } - } - - return 0; } + + return 0; } } diff --git a/src/Http/Headers/test/CacheControlHeaderValueTest.cs b/src/Http/Headers/test/CacheControlHeaderValueTest.cs index bd40a63345..f2f077c705 100644 --- a/src/Http/Headers/test/CacheControlHeaderValueTest.cs +++ b/src/Http/Headers/test/CacheControlHeaderValueTest.cs @@ -5,593 +5,592 @@ using System; using System.Linq; using Xunit; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +public class CacheControlHeaderValueTest { - public class CacheControlHeaderValueTest + [Fact] + public void Properties_SetAndGetAllProperties_SetValueReturnedInGetter() { - [Fact] - public void Properties_SetAndGetAllProperties_SetValueReturnedInGetter() - { - var cacheControl = new CacheControlHeaderValue(); - - // Bool properties - cacheControl.NoCache = true; - Assert.True(cacheControl.NoCache, "NoCache"); - cacheControl.NoStore = true; - Assert.True(cacheControl.NoStore, "NoStore"); - cacheControl.MaxStale = true; - Assert.True(cacheControl.MaxStale, "MaxStale"); - cacheControl.NoTransform = true; - Assert.True(cacheControl.NoTransform, "NoTransform"); - cacheControl.OnlyIfCached = true; - Assert.True(cacheControl.OnlyIfCached, "OnlyIfCached"); - cacheControl.Public = true; - Assert.True(cacheControl.Public, "Public"); - cacheControl.Private = true; - Assert.True(cacheControl.Private, "Private"); - cacheControl.MustRevalidate = true; - Assert.True(cacheControl.MustRevalidate, "MustRevalidate"); - cacheControl.ProxyRevalidate = true; - Assert.True(cacheControl.ProxyRevalidate, "ProxyRevalidate"); - - // TimeSpan properties - TimeSpan timeSpan = new TimeSpan(1, 2, 3); - cacheControl.MaxAge = timeSpan; - Assert.Equal(timeSpan, cacheControl.MaxAge); - cacheControl.SharedMaxAge = timeSpan; - Assert.Equal(timeSpan, cacheControl.SharedMaxAge); - cacheControl.MaxStaleLimit = timeSpan; - Assert.Equal(timeSpan, cacheControl.MaxStaleLimit); - cacheControl.MinFresh = timeSpan; - Assert.Equal(timeSpan, cacheControl.MinFresh); - - // String collection properties - Assert.NotNull(cacheControl.NoCacheHeaders); - Assert.Throws(() => cacheControl.NoCacheHeaders.Add(null)); - Assert.Throws(() => cacheControl.NoCacheHeaders.Add("invalid PLACEHOLDER")); - cacheControl.NoCacheHeaders.Add("PLACEHOLDER"); - Assert.Equal(1, cacheControl.NoCacheHeaders.Count); - Assert.Equal("PLACEHOLDER", cacheControl.NoCacheHeaders.First()); - - Assert.NotNull(cacheControl.PrivateHeaders); - Assert.Throws(() => cacheControl.PrivateHeaders.Add(null)); - Assert.Throws(() => cacheControl.PrivateHeaders.Add("invalid PLACEHOLDER")); - cacheControl.PrivateHeaders.Add("PLACEHOLDER"); - Assert.Equal(1, cacheControl.PrivateHeaders.Count); - Assert.Equal("PLACEHOLDER", cacheControl.PrivateHeaders.First()); - - // NameValueHeaderValue collection property - Assert.NotNull(cacheControl.Extensions); - Assert.Throws(() => cacheControl.Extensions.Add(null!)); - cacheControl.Extensions.Add(new NameValueHeaderValue("name", "value")); - Assert.Equal(1, cacheControl.Extensions.Count); - Assert.Equal(new NameValueHeaderValue("name", "value"), cacheControl.Extensions.First()); - } + var cacheControl = new CacheControlHeaderValue(); + + // Bool properties + cacheControl.NoCache = true; + Assert.True(cacheControl.NoCache, "NoCache"); + cacheControl.NoStore = true; + Assert.True(cacheControl.NoStore, "NoStore"); + cacheControl.MaxStale = true; + Assert.True(cacheControl.MaxStale, "MaxStale"); + cacheControl.NoTransform = true; + Assert.True(cacheControl.NoTransform, "NoTransform"); + cacheControl.OnlyIfCached = true; + Assert.True(cacheControl.OnlyIfCached, "OnlyIfCached"); + cacheControl.Public = true; + Assert.True(cacheControl.Public, "Public"); + cacheControl.Private = true; + Assert.True(cacheControl.Private, "Private"); + cacheControl.MustRevalidate = true; + Assert.True(cacheControl.MustRevalidate, "MustRevalidate"); + cacheControl.ProxyRevalidate = true; + Assert.True(cacheControl.ProxyRevalidate, "ProxyRevalidate"); + + // TimeSpan properties + TimeSpan timeSpan = new TimeSpan(1, 2, 3); + cacheControl.MaxAge = timeSpan; + Assert.Equal(timeSpan, cacheControl.MaxAge); + cacheControl.SharedMaxAge = timeSpan; + Assert.Equal(timeSpan, cacheControl.SharedMaxAge); + cacheControl.MaxStaleLimit = timeSpan; + Assert.Equal(timeSpan, cacheControl.MaxStaleLimit); + cacheControl.MinFresh = timeSpan; + Assert.Equal(timeSpan, cacheControl.MinFresh); + + // String collection properties + Assert.NotNull(cacheControl.NoCacheHeaders); + Assert.Throws(() => cacheControl.NoCacheHeaders.Add(null)); + Assert.Throws(() => cacheControl.NoCacheHeaders.Add("invalid PLACEHOLDER")); + cacheControl.NoCacheHeaders.Add("PLACEHOLDER"); + Assert.Equal(1, cacheControl.NoCacheHeaders.Count); + Assert.Equal("PLACEHOLDER", cacheControl.NoCacheHeaders.First()); + + Assert.NotNull(cacheControl.PrivateHeaders); + Assert.Throws(() => cacheControl.PrivateHeaders.Add(null)); + Assert.Throws(() => cacheControl.PrivateHeaders.Add("invalid PLACEHOLDER")); + cacheControl.PrivateHeaders.Add("PLACEHOLDER"); + Assert.Equal(1, cacheControl.PrivateHeaders.Count); + Assert.Equal("PLACEHOLDER", cacheControl.PrivateHeaders.First()); + + // NameValueHeaderValue collection property + Assert.NotNull(cacheControl.Extensions); + Assert.Throws(() => cacheControl.Extensions.Add(null!)); + cacheControl.Extensions.Add(new NameValueHeaderValue("name", "value")); + Assert.Equal(1, cacheControl.Extensions.Count); + Assert.Equal(new NameValueHeaderValue("name", "value"), cacheControl.Extensions.First()); + } - [Fact] - public void ToString_UseRequestDirectiveValues_AllSerializedCorrectly() - { - var cacheControl = new CacheControlHeaderValue(); - Assert.Equal("", cacheControl.ToString()); - - // Note that we allow all combinations of all properties even though the RFC specifies rules what value - // can be used together. - // Also for property pairs (bool property + collection property) like 'NoCache' and 'NoCacheHeaders' the - // caller needs to set the bool property in order for the collection to be populated as string. - - // Cache Request Directive sample - cacheControl.NoStore = true; - Assert.Equal("no-store", cacheControl.ToString()); - cacheControl.NoCache = true; - Assert.Equal("no-store, no-cache", cacheControl.ToString()); - cacheControl.MaxAge = new TimeSpan(0, 1, 10); - Assert.Equal("no-store, no-cache, max-age=70", cacheControl.ToString()); - cacheControl.MaxStale = true; - Assert.Equal("no-store, no-cache, max-age=70, max-stale", cacheControl.ToString()); - cacheControl.MaxStaleLimit = new TimeSpan(0, 2, 5); - Assert.Equal("no-store, no-cache, max-age=70, max-stale=125", cacheControl.ToString()); - cacheControl.MinFresh = new TimeSpan(0, 3, 0); - Assert.Equal("no-store, no-cache, max-age=70, max-stale=125, min-fresh=180", cacheControl.ToString()); - - cacheControl = new CacheControlHeaderValue(); - cacheControl.NoTransform = true; - Assert.Equal("no-transform", cacheControl.ToString()); - cacheControl.OnlyIfCached = true; - Assert.Equal("no-transform, only-if-cached", cacheControl.ToString()); - cacheControl.Extensions.Add(new NameValueHeaderValue("custom")); - cacheControl.Extensions.Add(new NameValueHeaderValue("customName", "customValue")); - Assert.Equal("no-transform, only-if-cached, custom, customName=customValue", cacheControl.ToString()); - - cacheControl = new CacheControlHeaderValue(); - cacheControl.Extensions.Add(new NameValueHeaderValue("custom")); - Assert.Equal("custom", cacheControl.ToString()); - } + [Fact] + public void ToString_UseRequestDirectiveValues_AllSerializedCorrectly() + { + var cacheControl = new CacheControlHeaderValue(); + Assert.Equal("", cacheControl.ToString()); + + // Note that we allow all combinations of all properties even though the RFC specifies rules what value + // can be used together. + // Also for property pairs (bool property + collection property) like 'NoCache' and 'NoCacheHeaders' the + // caller needs to set the bool property in order for the collection to be populated as string. + + // Cache Request Directive sample + cacheControl.NoStore = true; + Assert.Equal("no-store", cacheControl.ToString()); + cacheControl.NoCache = true; + Assert.Equal("no-store, no-cache", cacheControl.ToString()); + cacheControl.MaxAge = new TimeSpan(0, 1, 10); + Assert.Equal("no-store, no-cache, max-age=70", cacheControl.ToString()); + cacheControl.MaxStale = true; + Assert.Equal("no-store, no-cache, max-age=70, max-stale", cacheControl.ToString()); + cacheControl.MaxStaleLimit = new TimeSpan(0, 2, 5); + Assert.Equal("no-store, no-cache, max-age=70, max-stale=125", cacheControl.ToString()); + cacheControl.MinFresh = new TimeSpan(0, 3, 0); + Assert.Equal("no-store, no-cache, max-age=70, max-stale=125, min-fresh=180", cacheControl.ToString()); + + cacheControl = new CacheControlHeaderValue(); + cacheControl.NoTransform = true; + Assert.Equal("no-transform", cacheControl.ToString()); + cacheControl.OnlyIfCached = true; + Assert.Equal("no-transform, only-if-cached", cacheControl.ToString()); + cacheControl.Extensions.Add(new NameValueHeaderValue("custom")); + cacheControl.Extensions.Add(new NameValueHeaderValue("customName", "customValue")); + Assert.Equal("no-transform, only-if-cached, custom, customName=customValue", cacheControl.ToString()); + + cacheControl = new CacheControlHeaderValue(); + cacheControl.Extensions.Add(new NameValueHeaderValue("custom")); + Assert.Equal("custom", cacheControl.ToString()); + } + + [Fact] + public void ToString_UseResponseDirectiveValues_AllSerializedCorrectly() + { + var cacheControl = new CacheControlHeaderValue(); + Assert.Equal("", cacheControl.ToString()); + + cacheControl.NoCache = true; + Assert.Equal("no-cache", cacheControl.ToString()); + cacheControl.NoCacheHeaders.Add("PLACEHOLDER1"); + Assert.Equal("no-cache=\"PLACEHOLDER1\"", cacheControl.ToString()); + cacheControl.Public = true; + Assert.Equal("public, no-cache=\"PLACEHOLDER1\"", cacheControl.ToString()); + + cacheControl = new CacheControlHeaderValue(); + cacheControl.Private = true; + Assert.Equal("private", cacheControl.ToString()); + cacheControl.PrivateHeaders.Add("PLACEHOLDER2"); + cacheControl.PrivateHeaders.Add("PLACEHOLDER3"); + Assert.Equal("private=\"PLACEHOLDER2, PLACEHOLDER3\"", cacheControl.ToString()); + cacheControl.MustRevalidate = true; + Assert.Equal("must-revalidate, private=\"PLACEHOLDER2, PLACEHOLDER3\"", cacheControl.ToString()); + cacheControl.ProxyRevalidate = true; + Assert.Equal("must-revalidate, proxy-revalidate, private=\"PLACEHOLDER2, PLACEHOLDER3\"", cacheControl.ToString()); + } - [Fact] - public void ToString_UseResponseDirectiveValues_AllSerializedCorrectly() + [Fact] + public void GetHashCode_CompareValuesWithBoolFieldsSet_MatchExpectation() + { + // Verify that different bool fields return different hash values. + var values = new CacheControlHeaderValue[9]; + + for (int i = 0; i < values.Length; i++) { - var cacheControl = new CacheControlHeaderValue(); - Assert.Equal("", cacheControl.ToString()); - - cacheControl.NoCache = true; - Assert.Equal("no-cache", cacheControl.ToString()); - cacheControl.NoCacheHeaders.Add("PLACEHOLDER1"); - Assert.Equal("no-cache=\"PLACEHOLDER1\"", cacheControl.ToString()); - cacheControl.Public = true; - Assert.Equal("public, no-cache=\"PLACEHOLDER1\"", cacheControl.ToString()); - - cacheControl = new CacheControlHeaderValue(); - cacheControl.Private = true; - Assert.Equal("private", cacheControl.ToString()); - cacheControl.PrivateHeaders.Add("PLACEHOLDER2"); - cacheControl.PrivateHeaders.Add("PLACEHOLDER3"); - Assert.Equal("private=\"PLACEHOLDER2, PLACEHOLDER3\"", cacheControl.ToString()); - cacheControl.MustRevalidate = true; - Assert.Equal("must-revalidate, private=\"PLACEHOLDER2, PLACEHOLDER3\"", cacheControl.ToString()); - cacheControl.ProxyRevalidate = true; - Assert.Equal("must-revalidate, proxy-revalidate, private=\"PLACEHOLDER2, PLACEHOLDER3\"", cacheControl.ToString()); + values[i] = new CacheControlHeaderValue(); } - [Fact] - public void GetHashCode_CompareValuesWithBoolFieldsSet_MatchExpectation() + values[0].ProxyRevalidate = true; + values[1].NoCache = true; + values[2].NoStore = true; + values[3].MaxStale = true; + values[4].NoTransform = true; + values[5].OnlyIfCached = true; + values[6].Public = true; + values[7].Private = true; + values[8].MustRevalidate = true; + + // Only one bool field set. All hash codes should differ + for (int i = 0; i < values.Length; i++) { - // Verify that different bool fields return different hash values. - var values = new CacheControlHeaderValue[9]; - - for (int i = 0; i < values.Length; i++) + for (int j = 0; j < values.Length; j++) { - values[i] = new CacheControlHeaderValue(); - } - - values[0].ProxyRevalidate = true; - values[1].NoCache = true; - values[2].NoStore = true; - values[3].MaxStale = true; - values[4].NoTransform = true; - values[5].OnlyIfCached = true; - values[6].Public = true; - values[7].Private = true; - values[8].MustRevalidate = true; - - // Only one bool field set. All hash codes should differ - for (int i = 0; i < values.Length; i++) - { - for (int j = 0; j < values.Length; j++) + if (i != j) { - if (i != j) - { - CompareHashCodes(values[i], values[j], false); - } + CompareHashCodes(values[i], values[j], false); } } - - // Validate that two instances with the same bool fields set are equal. - values[0].NoCache = true; - CompareHashCodes(values[0], values[1], false); - values[1].ProxyRevalidate = true; - CompareHashCodes(values[0], values[1], true); } - [Fact] - public void GetHashCode_CompareValuesWithTimeSpanFieldsSet_MatchExpectation() - { - // Verify that different timespan fields return different hash values. - var values = new CacheControlHeaderValue[4]; + // Validate that two instances with the same bool fields set are equal. + values[0].NoCache = true; + CompareHashCodes(values[0], values[1], false); + values[1].ProxyRevalidate = true; + CompareHashCodes(values[0], values[1], true); + } - for (int i = 0; i < values.Length; i++) - { - values[i] = new CacheControlHeaderValue(); - } + [Fact] + public void GetHashCode_CompareValuesWithTimeSpanFieldsSet_MatchExpectation() + { + // Verify that different timespan fields return different hash values. + var values = new CacheControlHeaderValue[4]; + + for (int i = 0; i < values.Length; i++) + { + values[i] = new CacheControlHeaderValue(); + } - values[0].MaxAge = new TimeSpan(0, 1, 1); - values[1].MaxStaleLimit = new TimeSpan(0, 1, 1); - values[2].MinFresh = new TimeSpan(0, 1, 1); - values[3].SharedMaxAge = new TimeSpan(0, 1, 1); + values[0].MaxAge = new TimeSpan(0, 1, 1); + values[1].MaxStaleLimit = new TimeSpan(0, 1, 1); + values[2].MinFresh = new TimeSpan(0, 1, 1); + values[3].SharedMaxAge = new TimeSpan(0, 1, 1); - // Only one timespan field set. All hash codes should differ - for (int i = 0; i < values.Length; i++) + // Only one timespan field set. All hash codes should differ + for (int i = 0; i < values.Length; i++) + { + for (int j = 0; j < values.Length; j++) { - for (int j = 0; j < values.Length; j++) + if (i != j) { - if (i != j) - { - CompareHashCodes(values[i], values[j], false); - } + CompareHashCodes(values[i], values[j], false); } } + } - values[0].MaxStaleLimit = new TimeSpan(0, 1, 2); - CompareHashCodes(values[0], values[1], false); + values[0].MaxStaleLimit = new TimeSpan(0, 1, 2); + CompareHashCodes(values[0], values[1], false); - values[1].MaxAge = new TimeSpan(0, 1, 1); - values[1].MaxStaleLimit = new TimeSpan(0, 1, 2); - CompareHashCodes(values[0], values[1], true); - } + values[1].MaxAge = new TimeSpan(0, 1, 1); + values[1].MaxStaleLimit = new TimeSpan(0, 1, 2); + CompareHashCodes(values[0], values[1], true); + } - [Fact] - public void GetHashCode_CompareCollectionFieldsSet_MatchExpectation() - { - var cacheControl1 = new CacheControlHeaderValue(); - var cacheControl2 = new CacheControlHeaderValue(); - var cacheControl3 = new CacheControlHeaderValue(); - var cacheControl4 = new CacheControlHeaderValue(); - var cacheControl5 = new CacheControlHeaderValue(); + [Fact] + public void GetHashCode_CompareCollectionFieldsSet_MatchExpectation() + { + var cacheControl1 = new CacheControlHeaderValue(); + var cacheControl2 = new CacheControlHeaderValue(); + var cacheControl3 = new CacheControlHeaderValue(); + var cacheControl4 = new CacheControlHeaderValue(); + var cacheControl5 = new CacheControlHeaderValue(); - cacheControl1.NoCache = true; - cacheControl1.NoCacheHeaders.Add("PLACEHOLDER2"); + cacheControl1.NoCache = true; + cacheControl1.NoCacheHeaders.Add("PLACEHOLDER2"); - cacheControl2.NoCache = true; - cacheControl2.NoCacheHeaders.Add("PLACEHOLDER1"); - cacheControl2.NoCacheHeaders.Add("PLACEHOLDER2"); + cacheControl2.NoCache = true; + cacheControl2.NoCacheHeaders.Add("PLACEHOLDER1"); + cacheControl2.NoCacheHeaders.Add("PLACEHOLDER2"); - CompareHashCodes(cacheControl1, cacheControl2, false); + CompareHashCodes(cacheControl1, cacheControl2, false); - cacheControl1.NoCacheHeaders.Add("PLACEHOLDER1"); - CompareHashCodes(cacheControl1, cacheControl2, true); + cacheControl1.NoCacheHeaders.Add("PLACEHOLDER1"); + CompareHashCodes(cacheControl1, cacheControl2, true); - // Since NoCache and Private generate different hash codes, even if NoCacheHeaders and PrivateHeaders - // have the same values, the hash code will be different. - cacheControl3.Private = true; - cacheControl3.PrivateHeaders.Add("PLACEHOLDER2"); - CompareHashCodes(cacheControl1, cacheControl3, false); + // Since NoCache and Private generate different hash codes, even if NoCacheHeaders and PrivateHeaders + // have the same values, the hash code will be different. + cacheControl3.Private = true; + cacheControl3.PrivateHeaders.Add("PLACEHOLDER2"); + CompareHashCodes(cacheControl1, cacheControl3, false); - cacheControl4.Extensions.Add(new NameValueHeaderValue("custom")); - CompareHashCodes(cacheControl1, cacheControl4, false); + cacheControl4.Extensions.Add(new NameValueHeaderValue("custom")); + CompareHashCodes(cacheControl1, cacheControl4, false); - cacheControl5.Extensions.Add(new NameValueHeaderValue("customN", "customV")); - cacheControl5.Extensions.Add(new NameValueHeaderValue("custom")); - CompareHashCodes(cacheControl4, cacheControl5, false); + cacheControl5.Extensions.Add(new NameValueHeaderValue("customN", "customV")); + cacheControl5.Extensions.Add(new NameValueHeaderValue("custom")); + CompareHashCodes(cacheControl4, cacheControl5, false); - cacheControl4.Extensions.Add(new NameValueHeaderValue("customN", "customV")); - CompareHashCodes(cacheControl4, cacheControl5, true); - } + cacheControl4.Extensions.Add(new NameValueHeaderValue("customN", "customV")); + CompareHashCodes(cacheControl4, cacheControl5, true); + } - [Fact] - public void Equals_CompareValuesWithBoolFieldsSet_MatchExpectation() - { - // Verify that different bool fields return different hash values. - var values = new CacheControlHeaderValue[9]; + [Fact] + public void Equals_CompareValuesWithBoolFieldsSet_MatchExpectation() + { + // Verify that different bool fields return different hash values. + var values = new CacheControlHeaderValue[9]; - for (int i = 0; i < values.Length; i++) - { - values[i] = new CacheControlHeaderValue(); - } + for (int i = 0; i < values.Length; i++) + { + values[i] = new CacheControlHeaderValue(); + } - values[0].ProxyRevalidate = true; - values[1].NoCache = true; - values[2].NoStore = true; - values[3].MaxStale = true; - values[4].NoTransform = true; - values[5].OnlyIfCached = true; - values[6].Public = true; - values[7].Private = true; - values[8].MustRevalidate = true; - - // Only one bool field set. All hash codes should differ - for (int i = 0; i < values.Length; i++) + values[0].ProxyRevalidate = true; + values[1].NoCache = true; + values[2].NoStore = true; + values[3].MaxStale = true; + values[4].NoTransform = true; + values[5].OnlyIfCached = true; + values[6].Public = true; + values[7].Private = true; + values[8].MustRevalidate = true; + + // Only one bool field set. All hash codes should differ + for (int i = 0; i < values.Length; i++) + { + for (int j = 0; j < values.Length; j++) { - for (int j = 0; j < values.Length; j++) + if (i != j) { - if (i != j) - { - CompareValues(values[i], values[j], false); - } + CompareValues(values[i], values[j], false); } } - - // Validate that two instances with the same bool fields set are equal. - values[0].NoCache = true; - CompareValues(values[0], values[1], false); - values[1].ProxyRevalidate = true; - CompareValues(values[0], values[1], true); } - [Fact] - public void Equals_CompareValuesWithTimeSpanFieldsSet_MatchExpectation() - { - // Verify that different timespan fields return different hash values. - var values = new CacheControlHeaderValue[4]; + // Validate that two instances with the same bool fields set are equal. + values[0].NoCache = true; + CompareValues(values[0], values[1], false); + values[1].ProxyRevalidate = true; + CompareValues(values[0], values[1], true); + } - for (int i = 0; i < values.Length; i++) - { - values[i] = new CacheControlHeaderValue(); - } + [Fact] + public void Equals_CompareValuesWithTimeSpanFieldsSet_MatchExpectation() + { + // Verify that different timespan fields return different hash values. + var values = new CacheControlHeaderValue[4]; - values[0].MaxAge = new TimeSpan(0, 1, 1); - values[1].MaxStaleLimit = new TimeSpan(0, 1, 1); - values[2].MinFresh = new TimeSpan(0, 1, 1); - values[3].SharedMaxAge = new TimeSpan(0, 1, 1); + for (int i = 0; i < values.Length; i++) + { + values[i] = new CacheControlHeaderValue(); + } + + values[0].MaxAge = new TimeSpan(0, 1, 1); + values[1].MaxStaleLimit = new TimeSpan(0, 1, 1); + values[2].MinFresh = new TimeSpan(0, 1, 1); + values[3].SharedMaxAge = new TimeSpan(0, 1, 1); - // Only one timespan field set. All hash codes should differ - for (int i = 0; i < values.Length; i++) + // Only one timespan field set. All hash codes should differ + for (int i = 0; i < values.Length; i++) + { + for (int j = 0; j < values.Length; j++) { - for (int j = 0; j < values.Length; j++) + if (i != j) { - if (i != j) - { - CompareValues(values[i], values[j], false); - } + CompareValues(values[i], values[j], false); } } + } - values[0].MaxStaleLimit = new TimeSpan(0, 1, 2); - CompareValues(values[0], values[1], false); + values[0].MaxStaleLimit = new TimeSpan(0, 1, 2); + CompareValues(values[0], values[1], false); - values[1].MaxAge = new TimeSpan(0, 1, 1); - values[1].MaxStaleLimit = new TimeSpan(0, 1, 2); - CompareValues(values[0], values[1], true); + values[1].MaxAge = new TimeSpan(0, 1, 1); + values[1].MaxStaleLimit = new TimeSpan(0, 1, 2); + CompareValues(values[0], values[1], true); - var value1 = new CacheControlHeaderValue(); - value1.MaxStale = true; - var value2 = new CacheControlHeaderValue(); - value2.MaxStale = true; - CompareValues(value1, value2, true); + var value1 = new CacheControlHeaderValue(); + value1.MaxStale = true; + var value2 = new CacheControlHeaderValue(); + value2.MaxStale = true; + CompareValues(value1, value2, true); - value2.MaxStaleLimit = new TimeSpan(1, 2, 3); - CompareValues(value1, value2, false); - } + value2.MaxStaleLimit = new TimeSpan(1, 2, 3); + CompareValues(value1, value2, false); + } - [Fact] - public void Equals_CompareCollectionFieldsSet_MatchExpectation() - { - var cacheControl1 = new CacheControlHeaderValue(); - var cacheControl2 = new CacheControlHeaderValue(); - var cacheControl3 = new CacheControlHeaderValue(); - var cacheControl4 = new CacheControlHeaderValue(); - var cacheControl5 = new CacheControlHeaderValue(); - var cacheControl6 = new CacheControlHeaderValue(); + [Fact] + public void Equals_CompareCollectionFieldsSet_MatchExpectation() + { + var cacheControl1 = new CacheControlHeaderValue(); + var cacheControl2 = new CacheControlHeaderValue(); + var cacheControl3 = new CacheControlHeaderValue(); + var cacheControl4 = new CacheControlHeaderValue(); + var cacheControl5 = new CacheControlHeaderValue(); + var cacheControl6 = new CacheControlHeaderValue(); - cacheControl1.NoCache = true; - cacheControl1.NoCacheHeaders.Add("PLACEHOLDER2"); + cacheControl1.NoCache = true; + cacheControl1.NoCacheHeaders.Add("PLACEHOLDER2"); - Assert.False(cacheControl1.Equals(null), "Compare with 'null'"); + Assert.False(cacheControl1.Equals(null), "Compare with 'null'"); - cacheControl2.NoCache = true; - cacheControl2.NoCacheHeaders.Add("PLACEHOLDER1"); - cacheControl2.NoCacheHeaders.Add("PLACEHOLDER2"); + cacheControl2.NoCache = true; + cacheControl2.NoCacheHeaders.Add("PLACEHOLDER1"); + cacheControl2.NoCacheHeaders.Add("PLACEHOLDER2"); - CompareValues(cacheControl1!, cacheControl2, false); + CompareValues(cacheControl1!, cacheControl2, false); - cacheControl1!.NoCacheHeaders.Add("PLACEHOLDER1"); - CompareValues(cacheControl1, cacheControl2, true); + cacheControl1!.NoCacheHeaders.Add("PLACEHOLDER1"); + CompareValues(cacheControl1, cacheControl2, true); - // Since NoCache and Private generate different hash codes, even if NoCacheHeaders and PrivateHeaders - // have the same values, the hash code will be different. - cacheControl3.Private = true; - cacheControl3.PrivateHeaders.Add("PLACEHOLDER2"); - CompareValues(cacheControl1, cacheControl3, false); + // Since NoCache and Private generate different hash codes, even if NoCacheHeaders and PrivateHeaders + // have the same values, the hash code will be different. + cacheControl3.Private = true; + cacheControl3.PrivateHeaders.Add("PLACEHOLDER2"); + CompareValues(cacheControl1, cacheControl3, false); - cacheControl4.Private = true; - cacheControl4.PrivateHeaders.Add("PLACEHOLDER3"); - CompareValues(cacheControl3, cacheControl4, false); + cacheControl4.Private = true; + cacheControl4.PrivateHeaders.Add("PLACEHOLDER3"); + CompareValues(cacheControl3, cacheControl4, false); - cacheControl5.Extensions.Add(new NameValueHeaderValue("custom")); - CompareValues(cacheControl1, cacheControl5, false); + cacheControl5.Extensions.Add(new NameValueHeaderValue("custom")); + CompareValues(cacheControl1, cacheControl5, false); - cacheControl6.Extensions.Add(new NameValueHeaderValue("customN", "customV")); - cacheControl6.Extensions.Add(new NameValueHeaderValue("custom")); - CompareValues(cacheControl5, cacheControl6, false); + cacheControl6.Extensions.Add(new NameValueHeaderValue("customN", "customV")); + cacheControl6.Extensions.Add(new NameValueHeaderValue("custom")); + CompareValues(cacheControl5, cacheControl6, false); - cacheControl5.Extensions.Add(new NameValueHeaderValue("customN", "customV")); - CompareValues(cacheControl5, cacheControl6, true); - } + cacheControl5.Extensions.Add(new NameValueHeaderValue("customN", "customV")); + CompareValues(cacheControl5, cacheControl6, true); + } - [Fact] - public void TryParse_DifferentValidScenarios_AllReturnTrue() - { - var expected = new CacheControlHeaderValue(); - expected.NoCache = true; - CheckValidTryParse(" , no-cache ,,", expected); - - expected = new CacheControlHeaderValue(); - expected.NoCache = true; - expected.NoCacheHeaders.Add("PLACEHOLDER1"); - expected.NoCacheHeaders.Add("PLACEHOLDER2"); - CheckValidTryParse("no-cache=\"PLACEHOLDER1, PLACEHOLDER2\"", expected); - - expected = new CacheControlHeaderValue(); - expected.NoStore = true; - expected.MaxAge = new TimeSpan(0, 0, 125); - expected.MaxStale = true; - CheckValidTryParse(" no-store , max-age = 125, max-stale,", expected); - - expected = new CacheControlHeaderValue(); - expected.MinFresh = new TimeSpan(0, 0, 123); - expected.NoTransform = true; - expected.OnlyIfCached = true; - expected.Extensions.Add(new NameValueHeaderValue("custom")); - CheckValidTryParse("min-fresh=123, no-transform, only-if-cached, custom", expected); - - expected = new CacheControlHeaderValue(); - expected.Public = true; - expected.Private = true; - expected.PrivateHeaders.Add("PLACEHOLDER1"); - expected.MustRevalidate = true; - expected.ProxyRevalidate = true; - expected.Extensions.Add(new NameValueHeaderValue("c", "d")); - expected.Extensions.Add(new NameValueHeaderValue("a", "b")); - CheckValidTryParse(",public, , private=\"PLACEHOLDER1\", must-revalidate, c=d, proxy-revalidate, a=b", expected); - - expected = new CacheControlHeaderValue(); - expected.Private = true; - expected.SharedMaxAge = new TimeSpan(0, 0, 1234567890); - expected.MaxAge = new TimeSpan(0, 0, 987654321); - CheckValidTryParse("s-maxage=1234567890, private, max-age = 987654321,", expected); - - expected = new CacheControlHeaderValue(); - expected.Extensions.Add(new NameValueHeaderValue("custom", "")); - CheckValidTryParse("custom=", expected); - } + [Fact] + public void TryParse_DifferentValidScenarios_AllReturnTrue() + { + var expected = new CacheControlHeaderValue(); + expected.NoCache = true; + CheckValidTryParse(" , no-cache ,,", expected); + + expected = new CacheControlHeaderValue(); + expected.NoCache = true; + expected.NoCacheHeaders.Add("PLACEHOLDER1"); + expected.NoCacheHeaders.Add("PLACEHOLDER2"); + CheckValidTryParse("no-cache=\"PLACEHOLDER1, PLACEHOLDER2\"", expected); + + expected = new CacheControlHeaderValue(); + expected.NoStore = true; + expected.MaxAge = new TimeSpan(0, 0, 125); + expected.MaxStale = true; + CheckValidTryParse(" no-store , max-age = 125, max-stale,", expected); + + expected = new CacheControlHeaderValue(); + expected.MinFresh = new TimeSpan(0, 0, 123); + expected.NoTransform = true; + expected.OnlyIfCached = true; + expected.Extensions.Add(new NameValueHeaderValue("custom")); + CheckValidTryParse("min-fresh=123, no-transform, only-if-cached, custom", expected); + + expected = new CacheControlHeaderValue(); + expected.Public = true; + expected.Private = true; + expected.PrivateHeaders.Add("PLACEHOLDER1"); + expected.MustRevalidate = true; + expected.ProxyRevalidate = true; + expected.Extensions.Add(new NameValueHeaderValue("c", "d")); + expected.Extensions.Add(new NameValueHeaderValue("a", "b")); + CheckValidTryParse(",public, , private=\"PLACEHOLDER1\", must-revalidate, c=d, proxy-revalidate, a=b", expected); + + expected = new CacheControlHeaderValue(); + expected.Private = true; + expected.SharedMaxAge = new TimeSpan(0, 0, 1234567890); + expected.MaxAge = new TimeSpan(0, 0, 987654321); + CheckValidTryParse("s-maxage=1234567890, private, max-age = 987654321,", expected); + + expected = new CacheControlHeaderValue(); + expected.Extensions.Add(new NameValueHeaderValue("custom", "")); + CheckValidTryParse("custom=", expected); + } - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - // PLACEHOLDER-only values - [InlineData("no-store=15")] - [InlineData("no-store=")] - [InlineData("no-transform=a")] - [InlineData("no-transform=")] - [InlineData("only-if-cached=\"x\"")] - [InlineData("only-if-cached=")] - [InlineData("public=\"x\"")] - [InlineData("public=")] - [InlineData("must-revalidate=\"1\"")] - [InlineData("must-revalidate=")] - [InlineData("proxy-revalidate=x")] - [InlineData("proxy-revalidate=")] - // PLACEHOLDER with optional field-name list - [InlineData("no-cache=")] - [InlineData("no-cache=PLACEHOLDER")] - [InlineData("no-cache=\"PLACEHOLDER")] - [InlineData("no-cache=\"\"")] // at least one PLACEHOLDER expected as value - [InlineData("private=")] - [InlineData("private=PLACEHOLDER")] - [InlineData("private=\"PLACEHOLDER")] - [InlineData("private=\",\"")] // at least one PLACEHOLDER expected as value - [InlineData("private=\"=\"")] - // PLACEHOLDER with delta-seconds value - [InlineData("max-age")] - [InlineData("max-age=")] - [InlineData("max-age=a")] - [InlineData("max-age=\"1\"")] - [InlineData("max-age=1.5")] - [InlineData("max-stale=")] - [InlineData("max-stale=a")] - [InlineData("max-stale=\"1\"")] - [InlineData("max-stale=1.5")] - [InlineData("min-fresh")] - [InlineData("min-fresh=")] - [InlineData("min-fresh=a")] - [InlineData("min-fresh=\"1\"")] - [InlineData("min-fresh=1.5")] - [InlineData("s-maxage")] - [InlineData("s-maxage=")] - [InlineData("s-maxage=a")] - [InlineData("s-maxage=\"1\"")] - [InlineData("s-maxage=1.5")] - // Invalid Extension values - [InlineData("custom value")] - public void TryParse_DifferentInvalidScenarios_ReturnsFalse(string input) - { - CheckInvalidTryParse(input); - } + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + // PLACEHOLDER-only values + [InlineData("no-store=15")] + [InlineData("no-store=")] + [InlineData("no-transform=a")] + [InlineData("no-transform=")] + [InlineData("only-if-cached=\"x\"")] + [InlineData("only-if-cached=")] + [InlineData("public=\"x\"")] + [InlineData("public=")] + [InlineData("must-revalidate=\"1\"")] + [InlineData("must-revalidate=")] + [InlineData("proxy-revalidate=x")] + [InlineData("proxy-revalidate=")] + // PLACEHOLDER with optional field-name list + [InlineData("no-cache=")] + [InlineData("no-cache=PLACEHOLDER")] + [InlineData("no-cache=\"PLACEHOLDER")] + [InlineData("no-cache=\"\"")] // at least one PLACEHOLDER expected as value + [InlineData("private=")] + [InlineData("private=PLACEHOLDER")] + [InlineData("private=\"PLACEHOLDER")] + [InlineData("private=\",\"")] // at least one PLACEHOLDER expected as value + [InlineData("private=\"=\"")] + // PLACEHOLDER with delta-seconds value + [InlineData("max-age")] + [InlineData("max-age=")] + [InlineData("max-age=a")] + [InlineData("max-age=\"1\"")] + [InlineData("max-age=1.5")] + [InlineData("max-stale=")] + [InlineData("max-stale=a")] + [InlineData("max-stale=\"1\"")] + [InlineData("max-stale=1.5")] + [InlineData("min-fresh")] + [InlineData("min-fresh=")] + [InlineData("min-fresh=a")] + [InlineData("min-fresh=\"1\"")] + [InlineData("min-fresh=1.5")] + [InlineData("s-maxage")] + [InlineData("s-maxage=")] + [InlineData("s-maxage=a")] + [InlineData("s-maxage=\"1\"")] + [InlineData("s-maxage=1.5")] + // Invalid Extension values + [InlineData("custom value")] + public void TryParse_DifferentInvalidScenarios_ReturnsFalse(string input) + { + CheckInvalidTryParse(input); + } - [Fact] - public void Parse_SetOfValidValueStrings_ParsedCorrectly() - { - // Just verify parser is implemented correctly. Don't try to test syntax parsed by CacheControlHeaderValue. - var expected = new CacheControlHeaderValue(); - expected.NoStore = true; - expected.MinFresh = new TimeSpan(0, 2, 3); - CheckValidParse(" , no-store, min-fresh=123", expected); - - expected = new CacheControlHeaderValue(); - expected.MaxStale = true; - expected.NoCache = true; - expected.NoCacheHeaders.Add("t"); - CheckValidParse("max-stale, no-cache=\"t\", ,,", expected); - - expected = new CacheControlHeaderValue(); - expected.Extensions.Add(new NameValueHeaderValue("custom")); - CheckValidParse("custom =", expected); - - expected = new CacheControlHeaderValue(); - expected.Extensions.Add(new NameValueHeaderValue("custom", "")); - CheckValidParse("custom =", expected); - } + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + // Just verify parser is implemented correctly. Don't try to test syntax parsed by CacheControlHeaderValue. + var expected = new CacheControlHeaderValue(); + expected.NoStore = true; + expected.MinFresh = new TimeSpan(0, 2, 3); + CheckValidParse(" , no-store, min-fresh=123", expected); + + expected = new CacheControlHeaderValue(); + expected.MaxStale = true; + expected.NoCache = true; + expected.NoCacheHeaders.Add("t"); + CheckValidParse("max-stale, no-cache=\"t\", ,,", expected); + + expected = new CacheControlHeaderValue(); + expected.Extensions.Add(new NameValueHeaderValue("custom")); + CheckValidParse("custom =", expected); + + expected = new CacheControlHeaderValue(); + expected.Extensions.Add(new NameValueHeaderValue("custom", "")); + CheckValidParse("custom =", expected); + } - [Fact] - public void Parse_SetOfInvalidValueStrings_Throws() - { - CheckInvalidParse(null); - CheckInvalidParse(""); - CheckInvalidParse(" "); - CheckInvalidParse("no-cache,="); - CheckInvalidParse("max-age=123x"); - CheckInvalidParse("=no-cache"); - CheckInvalidParse("no-cache no-store"); - CheckInvalidParse("会"); - } + [Fact] + public void Parse_SetOfInvalidValueStrings_Throws() + { + CheckInvalidParse(null); + CheckInvalidParse(""); + CheckInvalidParse(" "); + CheckInvalidParse("no-cache,="); + CheckInvalidParse("max-age=123x"); + CheckInvalidParse("=no-cache"); + CheckInvalidParse("no-cache no-store"); + CheckInvalidParse("会"); + } - [Fact] - public void TryParse_SetOfValidValueStrings_ParsedCorrectly() - { - // Just verify parser is implemented correctly. Don't try to test syntax parsed by CacheControlHeaderValue. - var expected = new CacheControlHeaderValue(); - expected.NoStore = true; - expected.MinFresh = new TimeSpan(0, 2, 3); - CheckValidTryParse(" , no-store, min-fresh=123", expected); - - expected = new CacheControlHeaderValue(); - expected.MaxStale = true; - expected.NoCache = true; - expected.NoCacheHeaders.Add("t"); - CheckValidTryParse("max-stale, no-cache=\"t\", ,,", expected); - - expected = new CacheControlHeaderValue(); - expected.Extensions.Add(new NameValueHeaderValue("custom")); - CheckValidTryParse("custom = ", expected); - - expected = new CacheControlHeaderValue(); - expected.Extensions.Add(new NameValueHeaderValue("custom", "")); - CheckValidTryParse("custom =", expected); - } + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + // Just verify parser is implemented correctly. Don't try to test syntax parsed by CacheControlHeaderValue. + var expected = new CacheControlHeaderValue(); + expected.NoStore = true; + expected.MinFresh = new TimeSpan(0, 2, 3); + CheckValidTryParse(" , no-store, min-fresh=123", expected); + + expected = new CacheControlHeaderValue(); + expected.MaxStale = true; + expected.NoCache = true; + expected.NoCacheHeaders.Add("t"); + CheckValidTryParse("max-stale, no-cache=\"t\", ,,", expected); + + expected = new CacheControlHeaderValue(); + expected.Extensions.Add(new NameValueHeaderValue("custom")); + CheckValidTryParse("custom = ", expected); + + expected = new CacheControlHeaderValue(); + expected.Extensions.Add(new NameValueHeaderValue("custom", "")); + CheckValidTryParse("custom =", expected); + } - [Fact] - public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() - { - CheckInvalidTryParse("no-cache,="); - CheckInvalidTryParse("max-age=123x"); - CheckInvalidTryParse("=no-cache"); - CheckInvalidTryParse("no-cache no-store"); - CheckInvalidTryParse("会"); - } + [Fact] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() + { + CheckInvalidTryParse("no-cache,="); + CheckInvalidTryParse("max-age=123x"); + CheckInvalidTryParse("=no-cache"); + CheckInvalidTryParse("no-cache no-store"); + CheckInvalidTryParse("会"); + } - #region Helper methods + #region Helper methods - private void CompareHashCodes(CacheControlHeaderValue x, CacheControlHeaderValue y, bool areEqual) + private void CompareHashCodes(CacheControlHeaderValue x, CacheControlHeaderValue y, bool areEqual) + { + if (areEqual) { - if (areEqual) - { - Assert.Equal(x.GetHashCode(), y.GetHashCode()); - } - else - { - Assert.NotEqual(x.GetHashCode(), y.GetHashCode()); - } + Assert.Equal(x.GetHashCode(), y.GetHashCode()); } - - private void CompareValues(CacheControlHeaderValue x, CacheControlHeaderValue y, bool areEqual) + else { - Assert.Equal(areEqual, x.Equals(y)); - Assert.Equal(areEqual, y.Equals(x)); + Assert.NotEqual(x.GetHashCode(), y.GetHashCode()); } + } - private void CheckValidParse(string? input, CacheControlHeaderValue expectedResult) - { - var result = CacheControlHeaderValue.Parse(input); - Assert.Equal(expectedResult, result); - } + private void CompareValues(CacheControlHeaderValue x, CacheControlHeaderValue y, bool areEqual) + { + Assert.Equal(areEqual, x.Equals(y)); + Assert.Equal(areEqual, y.Equals(x)); + } - private void CheckInvalidParse(string? input) - { - Assert.Throws(() => CacheControlHeaderValue.Parse(input)); - } + private void CheckValidParse(string? input, CacheControlHeaderValue expectedResult) + { + var result = CacheControlHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); + } - private void CheckValidTryParse(string? input, CacheControlHeaderValue expectedResult) - { - Assert.True(CacheControlHeaderValue.TryParse(input, out var result)); - Assert.Equal(expectedResult, result); - } + private void CheckInvalidParse(string? input) + { + Assert.Throws(() => CacheControlHeaderValue.Parse(input)); + } - private void CheckInvalidTryParse(string? input) - { - Assert.False(CacheControlHeaderValue.TryParse(input, out var result)); - Assert.Null(result); - } + private void CheckValidTryParse(string? input, CacheControlHeaderValue expectedResult) + { + Assert.True(CacheControlHeaderValue.TryParse(input, out var result)); + Assert.Equal(expectedResult, result); + } - #endregion + private void CheckInvalidTryParse(string? input) + { + Assert.False(CacheControlHeaderValue.TryParse(input, out var result)); + Assert.Null(result); } + + #endregion } diff --git a/src/Http/Headers/test/ContentDispositionHeaderValueTest.cs b/src/Http/Headers/test/ContentDispositionHeaderValueTest.cs index c0c45133de..d579a990c0 100644 --- a/src/Http/Headers/test/ContentDispositionHeaderValueTest.cs +++ b/src/Http/Headers/test/ContentDispositionHeaderValueTest.cs @@ -7,488 +7,488 @@ using System.Globalization; using System.Linq; using Xunit; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +public class ContentDispositionHeaderValueTest { - public class ContentDispositionHeaderValueTest + [Fact] + public void Ctor_ContentDispositionNull_Throw() { - [Fact] - public void Ctor_ContentDispositionNull_Throw() - { - Assert.Throws(() => new ContentDispositionHeaderValue(null)); - } + Assert.Throws(() => new ContentDispositionHeaderValue(null)); + } - [Fact] - public void Ctor_ContentDispositionEmpty_Throw() - { - // null and empty should be treated the same. So we also throw for empty strings. - Assert.Throws(() => new ContentDispositionHeaderValue(string.Empty)); - } + [Fact] + public void Ctor_ContentDispositionEmpty_Throw() + { + // null and empty should be treated the same. So we also throw for empty strings. + Assert.Throws(() => new ContentDispositionHeaderValue(string.Empty)); + } - [Fact] - public void Ctor_ContentDispositionInvalidFormat_ThrowFormatException() - { - // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. - AssertFormatException(" inline "); - AssertFormatException(" inline"); - AssertFormatException("inline "); - AssertFormatException("\"inline\""); - AssertFormatException("te xt"); - AssertFormatException("te=xt"); - AssertFormatException("teäxt"); - AssertFormatException("text;"); - AssertFormatException("te/xt;"); - AssertFormatException("inline; name=someName; "); - AssertFormatException("text;name=someName"); // ctor takes only disposition-type name, no parameters - } + [Fact] + public void Ctor_ContentDispositionInvalidFormat_ThrowFormatException() + { + // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. + AssertFormatException(" inline "); + AssertFormatException(" inline"); + AssertFormatException("inline "); + AssertFormatException("\"inline\""); + AssertFormatException("te xt"); + AssertFormatException("te=xt"); + AssertFormatException("teäxt"); + AssertFormatException("text;"); + AssertFormatException("te/xt;"); + AssertFormatException("inline; name=someName; "); + AssertFormatException("text;name=someName"); // ctor takes only disposition-type name, no parameters + } - [Fact] - public void Ctor_ContentDispositionValidFormat_SuccessfullyCreated() - { - var contentDisposition = new ContentDispositionHeaderValue("inline"); - Assert.Equal("inline", contentDisposition.DispositionType); - Assert.Equal(0, contentDisposition.Parameters.Count); - Assert.Null(contentDisposition.Name.Value); - Assert.Null(contentDisposition.FileName.Value); - Assert.Null(contentDisposition.CreationDate); - Assert.Null(contentDisposition.ModificationDate); - Assert.Null(contentDisposition.ReadDate); - Assert.Null(contentDisposition.Size); - } + [Fact] + public void Ctor_ContentDispositionValidFormat_SuccessfullyCreated() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + Assert.Equal("inline", contentDisposition.DispositionType); + Assert.Equal(0, contentDisposition.Parameters.Count); + Assert.Null(contentDisposition.Name.Value); + Assert.Null(contentDisposition.FileName.Value); + Assert.Null(contentDisposition.CreationDate); + Assert.Null(contentDisposition.ModificationDate); + Assert.Null(contentDisposition.ReadDate); + Assert.Null(contentDisposition.Size); + } - [Fact] - public void Parameters_AddNull_Throw() - { - var contentDisposition = new ContentDispositionHeaderValue("inline"); - Assert.Throws(() => contentDisposition.Parameters.Add(null!)); - } + [Fact] + public void Parameters_AddNull_Throw() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + Assert.Throws(() => contentDisposition.Parameters.Add(null!)); + } - [Fact] - public void ContentDisposition_SetAndGetContentDisposition_MatchExpectations() - { - var contentDisposition = new ContentDispositionHeaderValue("inline"); - Assert.Equal("inline", contentDisposition.DispositionType); + [Fact] + public void ContentDisposition_SetAndGetContentDisposition_MatchExpectations() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + Assert.Equal("inline", contentDisposition.DispositionType); - contentDisposition.DispositionType = "attachment"; - Assert.Equal("attachment", contentDisposition.DispositionType); - } + contentDisposition.DispositionType = "attachment"; + Assert.Equal("attachment", contentDisposition.DispositionType); + } - [Fact] - public void Name_SetNameAndValidateObject_ParametersEntryForNameAdded() - { - var contentDisposition = new ContentDispositionHeaderValue("inline"); - contentDisposition.Name = "myname"; - Assert.Equal("myname", contentDisposition.Name); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("name", contentDisposition.Parameters.First().Name); - - contentDisposition.Name = null; - Assert.Null(contentDisposition.Name.Value); - Assert.Equal(0, contentDisposition.Parameters.Count); - contentDisposition.Name = null; // It's OK to set it again to null; no exception. - } + [Fact] + public void Name_SetNameAndValidateObject_ParametersEntryForNameAdded() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + contentDisposition.Name = "myname"; + Assert.Equal("myname", contentDisposition.Name); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("name", contentDisposition.Parameters.First().Name); + + contentDisposition.Name = null; + Assert.Null(contentDisposition.Name.Value); + Assert.Equal(0, contentDisposition.Parameters.Count); + contentDisposition.Name = null; // It's OK to set it again to null; no exception. + } - [Fact] - public void Name_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten() - { - var contentDisposition = new ContentDispositionHeaderValue("inline"); + [Fact] + public void Name_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); - // Note that uppercase letters are used. Comparison should happen case-insensitive. - NameValueHeaderValue name = new NameValueHeaderValue("NAME", "old_name"); - contentDisposition.Parameters.Add(name); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("NAME", contentDisposition.Parameters.First().Name); + // Note that uppercase letters are used. Comparison should happen case-insensitive. + NameValueHeaderValue name = new NameValueHeaderValue("NAME", "old_name"); + contentDisposition.Parameters.Add(name); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("NAME", contentDisposition.Parameters.First().Name); - contentDisposition.Name = "new_name"; - Assert.Equal("new_name", contentDisposition.Name); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("NAME", contentDisposition.Parameters.First().Name); + contentDisposition.Name = "new_name"; + Assert.Equal("new_name", contentDisposition.Name); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("NAME", contentDisposition.Parameters.First().Name); - contentDisposition.Parameters.Remove(name); - Assert.Null(contentDisposition.Name.Value); - } + contentDisposition.Parameters.Remove(name); + Assert.Null(contentDisposition.Name.Value); + } - [Fact] - public void FileName_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten() - { - var contentDisposition = new ContentDispositionHeaderValue("inline"); + [Fact] + public void FileName_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); - // Note that uppercase letters are used. Comparison should happen case-insensitive. - var fileName = new NameValueHeaderValue("FILENAME", "old_name"); - contentDisposition.Parameters.Add(fileName); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name); + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var fileName = new NameValueHeaderValue("FILENAME", "old_name"); + contentDisposition.Parameters.Add(fileName); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name); - contentDisposition.FileName = "new_name"; - Assert.Equal("new_name", contentDisposition.FileName); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name); + contentDisposition.FileName = "new_name"; + Assert.Equal("new_name", contentDisposition.FileName); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name); - contentDisposition.Parameters.Remove(fileName); - Assert.Null(contentDisposition.FileName.Value); - } + contentDisposition.Parameters.Remove(fileName); + Assert.Null(contentDisposition.FileName.Value); + } - [Fact] - public void FileName_NeedsEncoding_EncodedAndDecodedCorrectly() - { - var contentDisposition = new ContentDispositionHeaderValue("inline"); + [Fact] + public void FileName_NeedsEncoding_EncodedAndDecodedCorrectly() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); - contentDisposition.FileName = "FileÃName.bat"; - Assert.Equal("FileÃName.bat", contentDisposition.FileName); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("filename", contentDisposition.Parameters.First().Name); - Assert.Equal("\"=?utf-8?B?RmlsZcODTmFtZS5iYXQ=?=\"", contentDisposition.Parameters.First().Value); + contentDisposition.FileName = "FileÃName.bat"; + Assert.Equal("FileÃName.bat", contentDisposition.FileName); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("filename", contentDisposition.Parameters.First().Name); + Assert.Equal("\"=?utf-8?B?RmlsZcODTmFtZS5iYXQ=?=\"", contentDisposition.Parameters.First().Value); - contentDisposition.Parameters.Remove(contentDisposition.Parameters.First()); - Assert.Null(contentDisposition.FileName.Value); - } + contentDisposition.Parameters.Remove(contentDisposition.Parameters.First()); + Assert.Null(contentDisposition.FileName.Value); + } - [Fact] - public void FileName_NeedsEncodingBecauseOfNewLine_EncodedAndDecodedCorrectly() - { - var contentDisposition = new ContentDispositionHeaderValue("inline"); + [Fact] + public void FileName_NeedsEncodingBecauseOfNewLine_EncodedAndDecodedCorrectly() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); - contentDisposition.FileName = "File\nName.bat"; - Assert.Equal("File\nName.bat", contentDisposition.FileName); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("filename", contentDisposition.Parameters.First().Name); - Assert.Equal("\"=?utf-8?B?RmlsZQpOYW1lLmJhdA==?=\"", contentDisposition.Parameters.First().Value); + contentDisposition.FileName = "File\nName.bat"; + Assert.Equal("File\nName.bat", contentDisposition.FileName); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("filename", contentDisposition.Parameters.First().Name); + Assert.Equal("\"=?utf-8?B?RmlsZQpOYW1lLmJhdA==?=\"", contentDisposition.Parameters.First().Value); - contentDisposition.Parameters.Remove(contentDisposition.Parameters.First()); - Assert.Null(contentDisposition.FileName.Value); - } + contentDisposition.Parameters.Remove(contentDisposition.Parameters.First()); + Assert.Null(contentDisposition.FileName.Value); + } - [Fact] - public void FileName_UnknownOrBadEncoding_PropertyFails() - { - var contentDisposition = new ContentDispositionHeaderValue("inline"); - - // Note that uppercase letters are used. Comparison should happen case-insensitive. - var fileName = new NameValueHeaderValue("FILENAME", "\"=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=\""); - contentDisposition.Parameters.Add(fileName); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name); - Assert.Equal("\"=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=\"", contentDisposition.Parameters.First().Value); - Assert.Equal("=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=", contentDisposition.FileName); - - contentDisposition.FileName = "new_name"; - Assert.Equal("new_name", contentDisposition.FileName); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name); - - contentDisposition.Parameters.Remove(fileName); - Assert.Null(contentDisposition.FileName.Value); - } + [Fact] + public void FileName_UnknownOrBadEncoding_PropertyFails() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var fileName = new NameValueHeaderValue("FILENAME", "\"=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=\""); + contentDisposition.Parameters.Add(fileName); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name); + Assert.Equal("\"=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=\"", contentDisposition.Parameters.First().Value); + Assert.Equal("=?utf-99?Q?R=mlsZcODTmFtZS5iYXQ=?=", contentDisposition.FileName); + + contentDisposition.FileName = "new_name"; + Assert.Equal("new_name", contentDisposition.FileName); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name); + + contentDisposition.Parameters.Remove(fileName); + Assert.Null(contentDisposition.FileName.Value); + } - [Fact] - public void FileNameStar_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten() - { - var contentDisposition = new ContentDispositionHeaderValue("inline"); - - // Note that uppercase letters are used. Comparison should happen case-insensitive. - var fileNameStar = new NameValueHeaderValue("FILENAME*", "old_name"); - contentDisposition.Parameters.Add(fileNameStar); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name); - Assert.Null(contentDisposition.FileNameStar.Value); // Decode failure - - contentDisposition.FileNameStar = "new_name"; - Assert.Equal("new_name", contentDisposition.FileNameStar); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name); - Assert.Equal("UTF-8\'\'new_name", contentDisposition.Parameters.First().Value); - - contentDisposition.Parameters.Remove(fileNameStar); - Assert.Null(contentDisposition.FileNameStar.Value); - } + [Fact] + public void FileNameStar_AddNameParameterThenUseProperty_ParametersEntryIsOverwritten() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var fileNameStar = new NameValueHeaderValue("FILENAME*", "old_name"); + contentDisposition.Parameters.Add(fileNameStar); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name); + Assert.Null(contentDisposition.FileNameStar.Value); // Decode failure + + contentDisposition.FileNameStar = "new_name"; + Assert.Equal("new_name", contentDisposition.FileNameStar); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name); + Assert.Equal("UTF-8\'\'new_name", contentDisposition.Parameters.First().Value); + + contentDisposition.Parameters.Remove(fileNameStar); + Assert.Null(contentDisposition.FileNameStar.Value); + } - [Fact] - public void FileNameStar_NeedsEncoding_EncodedAndDecodedCorrectly() - { - var contentDisposition = new ContentDispositionHeaderValue("inline"); + [Fact] + public void FileNameStar_NeedsEncoding_EncodedAndDecodedCorrectly() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); - contentDisposition.FileNameStar = "FileÃName.bat"; - Assert.Equal("FileÃName.bat", contentDisposition.FileNameStar); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("filename*", contentDisposition.Parameters.First().Name); - Assert.Equal("UTF-8\'\'File%C3%83Name.bat", contentDisposition.Parameters.First().Value); + contentDisposition.FileNameStar = "FileÃName.bat"; + Assert.Equal("FileÃName.bat", contentDisposition.FileNameStar); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("filename*", contentDisposition.Parameters.First().Name); + Assert.Equal("UTF-8\'\'File%C3%83Name.bat", contentDisposition.Parameters.First().Value); - contentDisposition.Parameters.Remove(contentDisposition.Parameters.First()); - Assert.Null(contentDisposition.FileNameStar.Value); - } + contentDisposition.Parameters.Remove(contentDisposition.Parameters.First()); + Assert.Null(contentDisposition.FileNameStar.Value); + } - [Fact] - public void FileNameStar_UnknownOrBadEncoding_PropertyFails() - { - var contentDisposition = new ContentDispositionHeaderValue("inline"); - - // Note that uppercase letters are used. Comparison should happen case-insensitive. - var fileNameStar = new NameValueHeaderValue("FILENAME*", "utf-99'lang'File%CZName.bat"); - contentDisposition.Parameters.Add(fileNameStar); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name); - Assert.Equal("utf-99'lang'File%CZName.bat", contentDisposition.Parameters.First().Value); - Assert.Null(contentDisposition.FileNameStar.Value); // Decode failure - - contentDisposition.FileNameStar = "new_name"; - Assert.Equal("new_name", contentDisposition.FileNameStar); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name); - - contentDisposition.Parameters.Remove(fileNameStar); - Assert.Null(contentDisposition.FileNameStar.Value); - } + [Fact] + public void FileNameStar_UnknownOrBadEncoding_PropertyFails() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var fileNameStar = new NameValueHeaderValue("FILENAME*", "utf-99'lang'File%CZName.bat"); + contentDisposition.Parameters.Add(fileNameStar); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name); + Assert.Equal("utf-99'lang'File%CZName.bat", contentDisposition.Parameters.First().Value); + Assert.Null(contentDisposition.FileNameStar.Value); // Decode failure + + contentDisposition.FileNameStar = "new_name"; + Assert.Equal("new_name", contentDisposition.FileNameStar); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name); + + contentDisposition.Parameters.Remove(fileNameStar); + Assert.Null(contentDisposition.FileNameStar.Value); + } - [Theory] - [InlineData("FileName.bat", "FileName.bat")] - [InlineData("FileÃName.bat", "File_Name.bat")] - [InlineData("File\nName.bat", "File_Name.bat")] - public void SetHttpFileName_ShouldSanitizeFileNameWhereNeeded(string httpFileName, string expectedFileName) - { - var contentDisposition = new ContentDispositionHeaderValue("inline"); - contentDisposition.SetHttpFileName(httpFileName); - Assert.Equal(expectedFileName, contentDisposition.FileName); - } + [Theory] + [InlineData("FileName.bat", "FileName.bat")] + [InlineData("FileÃName.bat", "File_Name.bat")] + [InlineData("File\nName.bat", "File_Name.bat")] + public void SetHttpFileName_ShouldSanitizeFileNameWhereNeeded(string httpFileName, string expectedFileName) + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + contentDisposition.SetHttpFileName(httpFileName); + Assert.Equal(expectedFileName, contentDisposition.FileName); + } - [Fact] - public void Dates_AddDateParameterThenUseProperty_ParametersEntryIsOverwritten() - { - string validDateString = "\"Tue, 15 Nov 1994 08:12:31 GMT\""; - DateTimeOffset validDate = DateTimeOffset.Parse("Tue, 15 Nov 1994 08:12:31 GMT", CultureInfo.InvariantCulture); + [Fact] + public void Dates_AddDateParameterThenUseProperty_ParametersEntryIsOverwritten() + { + string validDateString = "\"Tue, 15 Nov 1994 08:12:31 GMT\""; + DateTimeOffset validDate = DateTimeOffset.Parse("Tue, 15 Nov 1994 08:12:31 GMT", CultureInfo.InvariantCulture); - var contentDisposition = new ContentDispositionHeaderValue("inline"); + var contentDisposition = new ContentDispositionHeaderValue("inline"); - // Note that uppercase letters are used. Comparison should happen case-insensitive. - var dateParameter = new NameValueHeaderValue("Creation-DATE", validDateString); - contentDisposition.Parameters.Add(dateParameter); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("Creation-DATE", contentDisposition.Parameters.First().Name); + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var dateParameter = new NameValueHeaderValue("Creation-DATE", validDateString); + contentDisposition.Parameters.Add(dateParameter); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("Creation-DATE", contentDisposition.Parameters.First().Name); - Assert.Equal(validDate, contentDisposition.CreationDate); + Assert.Equal(validDate, contentDisposition.CreationDate); - var newDate = validDate.AddSeconds(1); - contentDisposition.CreationDate = newDate; - Assert.Equal(newDate, contentDisposition.CreationDate); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("Creation-DATE", contentDisposition.Parameters.First().Name); - Assert.Equal("\"Tue, 15 Nov 1994 08:12:32 GMT\"", contentDisposition.Parameters.First().Value); + var newDate = validDate.AddSeconds(1); + contentDisposition.CreationDate = newDate; + Assert.Equal(newDate, contentDisposition.CreationDate); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("Creation-DATE", contentDisposition.Parameters.First().Name); + Assert.Equal("\"Tue, 15 Nov 1994 08:12:32 GMT\"", contentDisposition.Parameters.First().Value); - contentDisposition.Parameters.Remove(dateParameter); - Assert.Null(contentDisposition.CreationDate); - } + contentDisposition.Parameters.Remove(dateParameter); + Assert.Null(contentDisposition.CreationDate); + } - [Fact] - public void Dates_InvalidDates_PropertyFails() - { - string invalidDateString = "\"Tue, 15 Nov 94 08:12 GMT\""; + [Fact] + public void Dates_InvalidDates_PropertyFails() + { + string invalidDateString = "\"Tue, 15 Nov 94 08:12 GMT\""; - var contentDisposition = new ContentDispositionHeaderValue("inline"); + var contentDisposition = new ContentDispositionHeaderValue("inline"); - // Note that uppercase letters are used. Comparison should happen case-insensitive. - var dateParameter = new NameValueHeaderValue("read-DATE", invalidDateString); - contentDisposition.Parameters.Add(dateParameter); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("read-DATE", contentDisposition.Parameters.First().Name); + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var dateParameter = new NameValueHeaderValue("read-DATE", invalidDateString); + contentDisposition.Parameters.Add(dateParameter); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("read-DATE", contentDisposition.Parameters.First().Name); - Assert.Null(contentDisposition.ReadDate); + Assert.Null(contentDisposition.ReadDate); - contentDisposition.ReadDate = null; - Assert.Null(contentDisposition.ReadDate); - Assert.Equal(0, contentDisposition.Parameters.Count); - } + contentDisposition.ReadDate = null; + Assert.Null(contentDisposition.ReadDate); + Assert.Equal(0, contentDisposition.Parameters.Count); + } - [Fact] - public void Size_AddSizeParameterThenUseProperty_ParametersEntryIsOverwritten() - { - var contentDisposition = new ContentDispositionHeaderValue("inline"); - - // Note that uppercase letters are used. Comparison should happen case-insensitive. - var sizeParameter = new NameValueHeaderValue("SIZE", "279172874239"); - contentDisposition.Parameters.Add(sizeParameter); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("SIZE", contentDisposition.Parameters.First().Name); - Assert.Equal(279172874239, contentDisposition.Size); - - contentDisposition.Size = 279172874240; - Assert.Equal(279172874240, contentDisposition.Size); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("SIZE", contentDisposition.Parameters.First().Name); - - contentDisposition.Parameters.Remove(sizeParameter); - Assert.Null(contentDisposition.Size); - } + [Fact] + public void Size_AddSizeParameterThenUseProperty_ParametersEntryIsOverwritten() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var sizeParameter = new NameValueHeaderValue("SIZE", "279172874239"); + contentDisposition.Parameters.Add(sizeParameter); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("SIZE", contentDisposition.Parameters.First().Name); + Assert.Equal(279172874239, contentDisposition.Size); + + contentDisposition.Size = 279172874240; + Assert.Equal(279172874240, contentDisposition.Size); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("SIZE", contentDisposition.Parameters.First().Name); + + contentDisposition.Parameters.Remove(sizeParameter); + Assert.Null(contentDisposition.Size); + } - [Fact] - public void Size_InvalidSizes_PropertyFails() - { - var contentDisposition = new ContentDispositionHeaderValue("inline"); - - // Note that uppercase letters are used. Comparison should happen case-insensitive. - var sizeParameter = new NameValueHeaderValue("SIZE", "-279172874239"); - contentDisposition.Parameters.Add(sizeParameter); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("SIZE", contentDisposition.Parameters.First().Name); - Assert.Null(contentDisposition.Size); - - // Negatives not allowed - Assert.Throws(() => contentDisposition.Size = -279172874240); - Assert.Null(contentDisposition.Size); - Assert.Equal(1, contentDisposition.Parameters.Count); - Assert.Equal("SIZE", contentDisposition.Parameters.First().Name); - - contentDisposition.Parameters.Remove(sizeParameter); - Assert.Null(contentDisposition.Size); - } + [Fact] + public void Size_InvalidSizes_PropertyFails() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var sizeParameter = new NameValueHeaderValue("SIZE", "-279172874239"); + contentDisposition.Parameters.Add(sizeParameter); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("SIZE", contentDisposition.Parameters.First().Name); + Assert.Null(contentDisposition.Size); + + // Negatives not allowed + Assert.Throws(() => contentDisposition.Size = -279172874240); + Assert.Null(contentDisposition.Size); + Assert.Equal(1, contentDisposition.Parameters.Count); + Assert.Equal("SIZE", contentDisposition.Parameters.First().Name); + + contentDisposition.Parameters.Remove(sizeParameter); + Assert.Null(contentDisposition.Size); + } - [Fact] - public void ToString_UseDifferentContentDispositions_AllSerializedCorrectly() - { - var contentDisposition = new ContentDispositionHeaderValue("inline"); - Assert.Equal("inline", contentDisposition.ToString()); + [Fact] + public void ToString_UseDifferentContentDispositions_AllSerializedCorrectly() + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + Assert.Equal("inline", contentDisposition.ToString()); - contentDisposition.Name = "myname"; - Assert.Equal("inline; name=myname", contentDisposition.ToString()); + contentDisposition.Name = "myname"; + Assert.Equal("inline; name=myname", contentDisposition.ToString()); - contentDisposition.FileName = "my File Name"; - Assert.Equal("inline; name=myname; filename=\"my File Name\"", contentDisposition.ToString()); + contentDisposition.FileName = "my File Name"; + Assert.Equal("inline; name=myname; filename=\"my File Name\"", contentDisposition.ToString()); - contentDisposition.CreationDate = new DateTimeOffset(new DateTime(2011, 2, 15), new TimeSpan(-8, 0, 0)); - Assert.Equal("inline; name=myname; filename=\"my File Name\"; creation-date=" - + "\"Tue, 15 Feb 2011 08:00:00 GMT\"", contentDisposition.ToString()); + contentDisposition.CreationDate = new DateTimeOffset(new DateTime(2011, 2, 15), new TimeSpan(-8, 0, 0)); + Assert.Equal("inline; name=myname; filename=\"my File Name\"; creation-date=" + + "\"Tue, 15 Feb 2011 08:00:00 GMT\"", contentDisposition.ToString()); - contentDisposition.Parameters.Add(new NameValueHeaderValue("custom", "\"custom value\"")); - Assert.Equal("inline; name=myname; filename=\"my File Name\"; creation-date=" - + "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"", contentDisposition.ToString()); + contentDisposition.Parameters.Add(new NameValueHeaderValue("custom", "\"custom value\"")); + Assert.Equal("inline; name=myname; filename=\"my File Name\"; creation-date=" + + "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"", contentDisposition.ToString()); - contentDisposition.Name = null; - Assert.Equal("inline; filename=\"my File Name\"; creation-date=" - + "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"", contentDisposition.ToString()); + contentDisposition.Name = null; + Assert.Equal("inline; filename=\"my File Name\"; creation-date=" + + "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"", contentDisposition.ToString()); - contentDisposition.FileNameStar = "File%Name"; - Assert.Equal("inline; filename=\"my File Name\"; creation-date=" - + "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"; filename*=UTF-8\'\'File%25Name", - contentDisposition.ToString()); + contentDisposition.FileNameStar = "File%Name"; + Assert.Equal("inline; filename=\"my File Name\"; creation-date=" + + "\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\"; filename*=UTF-8\'\'File%25Name", + contentDisposition.ToString()); - contentDisposition.FileName = null; - Assert.Equal("inline; creation-date=\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\";" - + " filename*=UTF-8\'\'File%25Name", contentDisposition.ToString()); + contentDisposition.FileName = null; + Assert.Equal("inline; creation-date=\"Tue, 15 Feb 2011 08:00:00 GMT\"; custom=\"custom value\";" + + " filename*=UTF-8\'\'File%25Name", contentDisposition.ToString()); - contentDisposition.CreationDate = null; - Assert.Equal("inline; custom=\"custom value\"; filename*=UTF-8\'\'File%25Name", - contentDisposition.ToString()); - } + contentDisposition.CreationDate = null; + Assert.Equal("inline; custom=\"custom value\"; filename*=UTF-8\'\'File%25Name", + contentDisposition.ToString()); + } - [Fact] - public void GetHashCode_UseContentDispositionWithAndWithoutParameters_SameOrDifferentHashCodes() - { - var contentDisposition1 = new ContentDispositionHeaderValue("inline"); - var contentDisposition2 = new ContentDispositionHeaderValue("inline"); - contentDisposition2.Name = "myname"; - var contentDisposition3 = new ContentDispositionHeaderValue("inline"); - contentDisposition3.Parameters.Add(new NameValueHeaderValue("name", "value")); - var contentDisposition4 = new ContentDispositionHeaderValue("INLINE"); - var contentDisposition5 = new ContentDispositionHeaderValue("INLINE"); - contentDisposition5.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME")); - - Assert.NotEqual(contentDisposition1.GetHashCode(), contentDisposition2.GetHashCode()); - Assert.NotEqual(contentDisposition1.GetHashCode(), contentDisposition3.GetHashCode()); - Assert.NotEqual(contentDisposition2.GetHashCode(), contentDisposition3.GetHashCode()); - Assert.Equal(contentDisposition1.GetHashCode(), contentDisposition4.GetHashCode()); - Assert.Equal(contentDisposition2.GetHashCode(), contentDisposition5.GetHashCode()); - } + [Fact] + public void GetHashCode_UseContentDispositionWithAndWithoutParameters_SameOrDifferentHashCodes() + { + var contentDisposition1 = new ContentDispositionHeaderValue("inline"); + var contentDisposition2 = new ContentDispositionHeaderValue("inline"); + contentDisposition2.Name = "myname"; + var contentDisposition3 = new ContentDispositionHeaderValue("inline"); + contentDisposition3.Parameters.Add(new NameValueHeaderValue("name", "value")); + var contentDisposition4 = new ContentDispositionHeaderValue("INLINE"); + var contentDisposition5 = new ContentDispositionHeaderValue("INLINE"); + contentDisposition5.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME")); + + Assert.NotEqual(contentDisposition1.GetHashCode(), contentDisposition2.GetHashCode()); + Assert.NotEqual(contentDisposition1.GetHashCode(), contentDisposition3.GetHashCode()); + Assert.NotEqual(contentDisposition2.GetHashCode(), contentDisposition3.GetHashCode()); + Assert.Equal(contentDisposition1.GetHashCode(), contentDisposition4.GetHashCode()); + Assert.Equal(contentDisposition2.GetHashCode(), contentDisposition5.GetHashCode()); + } - [Fact] - public void Equals_UseContentDispositionWithAndWithoutParameters_EqualOrNotEqualNoExceptions() - { - var contentDisposition1 = new ContentDispositionHeaderValue("inline"); - var contentDisposition2 = new ContentDispositionHeaderValue("inline"); - contentDisposition2.Name = "myName"; - var contentDisposition3 = new ContentDispositionHeaderValue("inline"); - contentDisposition3.Parameters.Add(new NameValueHeaderValue("name", "value")); - var contentDisposition4 = new ContentDispositionHeaderValue("INLINE"); - var contentDisposition5 = new ContentDispositionHeaderValue("INLINE"); - contentDisposition5.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME")); - var contentDisposition6 = new ContentDispositionHeaderValue("INLINE"); - contentDisposition6.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME")); - contentDisposition6.Parameters.Add(new NameValueHeaderValue("custom", "value")); - var contentDisposition7 = new ContentDispositionHeaderValue("attachment"); - - Assert.False(contentDisposition1.Equals(contentDisposition2), "No params vs. name."); - Assert.False(contentDisposition2.Equals(contentDisposition1), "name vs. no params."); - Assert.False(contentDisposition1.Equals(null), "No params vs. ."); - Assert.False(contentDisposition1!.Equals(contentDisposition3), "No params vs. custom param."); - Assert.False(contentDisposition2.Equals(contentDisposition3), "name vs. custom param."); - Assert.True(contentDisposition1.Equals(contentDisposition4), "Different casing."); - Assert.True(contentDisposition2.Equals(contentDisposition5), "Different casing in name."); - Assert.False(contentDisposition5.Equals(contentDisposition6), "name vs. custom param."); - Assert.False(contentDisposition1.Equals(contentDisposition7), "inline vs. text/other."); - } + [Fact] + public void Equals_UseContentDispositionWithAndWithoutParameters_EqualOrNotEqualNoExceptions() + { + var contentDisposition1 = new ContentDispositionHeaderValue("inline"); + var contentDisposition2 = new ContentDispositionHeaderValue("inline"); + contentDisposition2.Name = "myName"; + var contentDisposition3 = new ContentDispositionHeaderValue("inline"); + contentDisposition3.Parameters.Add(new NameValueHeaderValue("name", "value")); + var contentDisposition4 = new ContentDispositionHeaderValue("INLINE"); + var contentDisposition5 = new ContentDispositionHeaderValue("INLINE"); + contentDisposition5.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME")); + var contentDisposition6 = new ContentDispositionHeaderValue("INLINE"); + contentDisposition6.Parameters.Add(new NameValueHeaderValue("NAME", "MYNAME")); + contentDisposition6.Parameters.Add(new NameValueHeaderValue("custom", "value")); + var contentDisposition7 = new ContentDispositionHeaderValue("attachment"); + + Assert.False(contentDisposition1.Equals(contentDisposition2), "No params vs. name."); + Assert.False(contentDisposition2.Equals(contentDisposition1), "name vs. no params."); + Assert.False(contentDisposition1.Equals(null), "No params vs. ."); + Assert.False(contentDisposition1!.Equals(contentDisposition3), "No params vs. custom param."); + Assert.False(contentDisposition2.Equals(contentDisposition3), "name vs. custom param."); + Assert.True(contentDisposition1.Equals(contentDisposition4), "Different casing."); + Assert.True(contentDisposition2.Equals(contentDisposition5), "Different casing in name."); + Assert.False(contentDisposition5.Equals(contentDisposition6), "name vs. custom param."); + Assert.False(contentDisposition1.Equals(contentDisposition7), "inline vs. text/other."); + } - [Fact] - public void Parse_SetOfValidValueStrings_ParsedCorrectly() - { - var expected = new ContentDispositionHeaderValue("inline"); - CheckValidParse("\r\n inline ", expected); - CheckValidParse("inline", expected); - - // We don't have to test all possible input strings, since most of the pieces are handled by other parsers. - // The purpose of this test is to verify that these other parsers are combined correctly to build a - // Content-Disposition parser. - expected.Name = "myName"; - CheckValidParse("\r\n inline ; name = myName ", expected); - CheckValidParse(" inline;name=myName", expected); - - expected.Name = null; - expected.DispositionType = "attachment"; - expected.FileName = "foo-ae.html"; - expected.Parameters.Add(new NameValueHeaderValue("filename*", "UTF-8''foo-%c3%a4.html")); - CheckValidParse(@"attachment; filename*=UTF-8''foo-%c3%a4.html; filename=foo-ae.html", expected); - } + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + var expected = new ContentDispositionHeaderValue("inline"); + CheckValidParse("\r\n inline ", expected); + CheckValidParse("inline", expected); + + // We don't have to test all possible input strings, since most of the pieces are handled by other parsers. + // The purpose of this test is to verify that these other parsers are combined correctly to build a + // Content-Disposition parser. + expected.Name = "myName"; + CheckValidParse("\r\n inline ; name = myName ", expected); + CheckValidParse(" inline;name=myName", expected); + + expected.Name = null; + expected.DispositionType = "attachment"; + expected.FileName = "foo-ae.html"; + expected.Parameters.Add(new NameValueHeaderValue("filename*", "UTF-8''foo-%c3%a4.html")); + CheckValidParse(@"attachment; filename*=UTF-8''foo-%c3%a4.html; filename=foo-ae.html", expected); + } - [Fact] - public void Parse_SetOfInvalidValueStrings_Throws() - { - CheckInvalidParse(""); - CheckInvalidParse(" "); - CheckInvalidParse(null); - CheckInvalidParse("inline会"); - CheckInvalidParse("inline ,"); - CheckInvalidParse("inline,"); - CheckInvalidParse("inline; name=myName ,"); - CheckInvalidParse("inline; name=myName,"); - CheckInvalidParse("inline; name=my会Name"); - CheckInvalidParse("inline/"); - } + [Fact] + public void Parse_SetOfInvalidValueStrings_Throws() + { + CheckInvalidParse(""); + CheckInvalidParse(" "); + CheckInvalidParse(null); + CheckInvalidParse("inline会"); + CheckInvalidParse("inline ,"); + CheckInvalidParse("inline,"); + CheckInvalidParse("inline; name=myName ,"); + CheckInvalidParse("inline; name=myName,"); + CheckInvalidParse("inline; name=my会Name"); + CheckInvalidParse("inline/"); + } - [Fact] - public void TryParse_SetOfValidValueStrings_ParsedCorrectly() - { - var expected = new ContentDispositionHeaderValue("inline"); - CheckValidTryParse("\r\n inline ", expected); - CheckValidTryParse("inline", expected); - - // We don't have to test all possible input strings, since most of the pieces are handled by other parsers. - // The purpose of this test is to verify that these other parsers are combined correctly to build a - // Content-Disposition parser. - expected.Name = "myName"; - CheckValidTryParse("\r\n inline ; name = myName ", expected); - CheckValidTryParse(" inline;name=myName", expected); - } + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + var expected = new ContentDispositionHeaderValue("inline"); + CheckValidTryParse("\r\n inline ", expected); + CheckValidTryParse("inline", expected); + + // We don't have to test all possible input strings, since most of the pieces are handled by other parsers. + // The purpose of this test is to verify that these other parsers are combined correctly to build a + // Content-Disposition parser. + expected.Name = "myName"; + CheckValidTryParse("\r\n inline ; name = myName ", expected); + CheckValidTryParse(" inline;name=myName", expected); + } - [Fact] - public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() - { - CheckInvalidTryParse(""); - CheckInvalidTryParse(" "); - CheckInvalidTryParse(null); - CheckInvalidTryParse("inline会"); - CheckInvalidTryParse("inline ,"); - CheckInvalidTryParse("inline,"); - CheckInvalidTryParse("inline; name=myName ,"); - CheckInvalidTryParse("inline; name=myName,"); - CheckInvalidTryParse("text/"); - } + [Fact] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() + { + CheckInvalidTryParse(""); + CheckInvalidTryParse(" "); + CheckInvalidTryParse(null); + CheckInvalidTryParse("inline会"); + CheckInvalidTryParse("inline ,"); + CheckInvalidTryParse("inline,"); + CheckInvalidTryParse("inline; name=myName ,"); + CheckInvalidTryParse("inline; name=myName,"); + CheckInvalidTryParse("text/"); + } - public static TheoryData ValidContentDispositionTestCases = new TheoryData() + public static TheoryData ValidContentDispositionTestCases = new TheoryData() { { "inline", new ContentDispositionHeaderValue("inline") }, // @"This should be equivalent to not including the header at all." { "inline;", new ContentDispositionHeaderValue("inline") }, @@ -540,119 +540,118 @@ namespace Microsoft.Net.Http.Headers { @"attachment; filename=foo.html ;", new ContentDispositionHeaderValue("attachment") { FileName="foo.html" } }, // 'attachment', specifying a filename of foo.html using a token instead of a quoted-string, and adding a trailing semicolon., }; - [Theory] - [MemberData(nameof(ValidContentDispositionTestCases))] - public void ContentDispositionHeaderValue_ParseValid_Success(string input, ContentDispositionHeaderValue expected) - { - // System.Diagnostics.Debugger.Launch(); - var result = ContentDispositionHeaderValue.Parse(input); - Assert.Equal(expected, result); - } + [Theory] + [MemberData(nameof(ValidContentDispositionTestCases))] + public void ContentDispositionHeaderValue_ParseValid_Success(string input, ContentDispositionHeaderValue expected) + { + // System.Diagnostics.Debugger.Launch(); + var result = ContentDispositionHeaderValue.Parse(input); + Assert.Equal(expected, result); + } - [Theory] - // Invalid values - [InlineData(@"""inline""")] // @"'inline' only, using double quotes", false) }, - [InlineData(@"""attachment""")] // @"'attachment' only, using double quotes", false) }, - [InlineData(@"attachment; filename=foo bar.html")] // @"'attachment', specifying a filename of foo bar.html without using quoting.", false) }, - // Duplicate file name parameter - // @"attachment; filename=""foo.html""; // filename=""bar.html""", @"'attachment', specifying two filename parameters. This is invalid syntax.", false) }, - [InlineData(@"attachment; filename=foo[1](2).html")] // @"'attachment', specifying a filename of foo[1](2).html, but missing the quotes. Also, ""["", ""]"", ""("" and "")"" are not allowed in the HTTP token production.", false) }, - [InlineData(@"attachment; filename=foo-ä.html")] // @"'attachment', specifying a filename of foo-ä.html, but missing the quotes.", false) }, - // HTML escaping, not supported - // @"attachment; filename=foo-ä.html", // "'attachment', specifying a filename of foo-ä.html (which happens to be foo-ä.html using UTF-8 encoding) but missing the quotes.", false) }, - [InlineData(@"filename=foo.html")] // @"Disposition type missing, filename specified.", false) }, - [InlineData(@"x=y; filename=foo.html")] // @"Disposition type missing, filename specified after extension parameter.", false) }, - [InlineData(@"""foo; filename=bar;baz""; filename=qux")] // @"Disposition type missing, filename ""qux"". Can it be more broken? (Probably)", false) }, - [InlineData(@"filename=foo.html, filename=bar.html")] // @"Disposition type missing, two filenames specified separated by a comma (this is syntactically equivalent to have two instances of the header with one filename parameter each).", false) }, - [InlineData(@"; filename=foo.html")] // @"Disposition type missing (but delimiter present), filename specified.", false) }, - // This is permitted as a parameter without a value - // @"inline; attachment; filename=foo.html", // @"Both disposition types specified.", false) }, - // This is permitted as a parameter without a value - // @"inline; attachment; filename=foo.html", // @"Both disposition types specified.", false) }, - [InlineData(@"attachment; filename=""foo.html"".txt")] // @"'attachment', specifying a filename parameter that is broken (quoted-string followed by more characters). This is invalid syntax. ", false) }, - [InlineData(@"attachment; filename=""bar")] // @"'attachment', specifying a filename parameter that is broken (missing ending double quote). This is invalid syntax.", false) }, - [InlineData(@"attachment; filename=foo""bar;baz""qux")] // @"'attachment', specifying a filename parameter that is broken (disallowed characters in token syntax). This is invalid syntax.", false) }, - [InlineData(@"attachment; filename=foo.html, attachment; filename=bar.html")] // @"'attachment', two comma-separated instances of the header field. As Content-Disposition doesn't use a list-style syntax, this is invalid syntax and, according to RFC 2616, Section 4.2, roughly equivalent to having two separate header field instances.", false) }, - [InlineData(@"filename=foo.html; attachment")] // @"filename parameter and disposition type reversed.", false) }, - // Escaping is not verified - // @"attachment; filename*=iso-8859-1''foo-%c3%a4-%e2%82%ac.html", // @"'attachment', specifying a filename of foo-ä-€.html, using RFC2231 encoded UTF-8, but declaring ISO-8859-1", false) }, - // Escaping is not verified - // @"attachment; filename *=UTF-8''foo-%c3%a4.html", // @"'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded UTF-8, with whitespace before ""*=""", false) }, - // Escaping is not verified - // @"attachment; filename*=""UTF-8''foo-%c3%a4.html""", // @"'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded UTF-8, with double quotes around the parameter value.", false) }, - [InlineData(@"attachment; filename==?ISO-8859-1?Q?foo-=E4.html?=")] // @"Uses RFC 2047 style encoded word. ""="" is invalid inside the token production, so this is invalid.", false) }, - [InlineData(@"attachment; filename==?utf-8?B?Zm9vLeQuaHRtbA==?=")] // @"Uses RFC 2047 style encoded word. ""="" is invalid inside the token production, so this is invalid.", false) }, - public void ContentDispositionHeaderValue_ParseInvalid_Throws(string input) - { - Assert.Throws(() => ContentDispositionHeaderValue.Parse(input)); - } + [Theory] + // Invalid values + [InlineData(@"""inline""")] // @"'inline' only, using double quotes", false) }, + [InlineData(@"""attachment""")] // @"'attachment' only, using double quotes", false) }, + [InlineData(@"attachment; filename=foo bar.html")] // @"'attachment', specifying a filename of foo bar.html without using quoting.", false) }, + // Duplicate file name parameter + // @"attachment; filename=""foo.html""; // filename=""bar.html""", @"'attachment', specifying two filename parameters. This is invalid syntax.", false) }, + [InlineData(@"attachment; filename=foo[1](2).html")] // @"'attachment', specifying a filename of foo[1](2).html, but missing the quotes. Also, ""["", ""]"", ""("" and "")"" are not allowed in the HTTP token production.", false) }, + [InlineData(@"attachment; filename=foo-ä.html")] // @"'attachment', specifying a filename of foo-ä.html, but missing the quotes.", false) }, + // HTML escaping, not supported + // @"attachment; filename=foo-ä.html", // "'attachment', specifying a filename of foo-ä.html (which happens to be foo-ä.html using UTF-8 encoding) but missing the quotes.", false) }, + [InlineData(@"filename=foo.html")] // @"Disposition type missing, filename specified.", false) }, + [InlineData(@"x=y; filename=foo.html")] // @"Disposition type missing, filename specified after extension parameter.", false) }, + [InlineData(@"""foo; filename=bar;baz""; filename=qux")] // @"Disposition type missing, filename ""qux"". Can it be more broken? (Probably)", false) }, + [InlineData(@"filename=foo.html, filename=bar.html")] // @"Disposition type missing, two filenames specified separated by a comma (this is syntactically equivalent to have two instances of the header with one filename parameter each).", false) }, + [InlineData(@"; filename=foo.html")] // @"Disposition type missing (but delimiter present), filename specified.", false) }, + // This is permitted as a parameter without a value + // @"inline; attachment; filename=foo.html", // @"Both disposition types specified.", false) }, + // This is permitted as a parameter without a value + // @"inline; attachment; filename=foo.html", // @"Both disposition types specified.", false) }, + [InlineData(@"attachment; filename=""foo.html"".txt")] // @"'attachment', specifying a filename parameter that is broken (quoted-string followed by more characters). This is invalid syntax. ", false) }, + [InlineData(@"attachment; filename=""bar")] // @"'attachment', specifying a filename parameter that is broken (missing ending double quote). This is invalid syntax.", false) }, + [InlineData(@"attachment; filename=foo""bar;baz""qux")] // @"'attachment', specifying a filename parameter that is broken (disallowed characters in token syntax). This is invalid syntax.", false) }, + [InlineData(@"attachment; filename=foo.html, attachment; filename=bar.html")] // @"'attachment', two comma-separated instances of the header field. As Content-Disposition doesn't use a list-style syntax, this is invalid syntax and, according to RFC 2616, Section 4.2, roughly equivalent to having two separate header field instances.", false) }, + [InlineData(@"filename=foo.html; attachment")] // @"filename parameter and disposition type reversed.", false) }, + // Escaping is not verified + // @"attachment; filename*=iso-8859-1''foo-%c3%a4-%e2%82%ac.html", // @"'attachment', specifying a filename of foo-ä-€.html, using RFC2231 encoded UTF-8, but declaring ISO-8859-1", false) }, + // Escaping is not verified + // @"attachment; filename *=UTF-8''foo-%c3%a4.html", // @"'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded UTF-8, with whitespace before ""*=""", false) }, + // Escaping is not verified + // @"attachment; filename*=""UTF-8''foo-%c3%a4.html""", // @"'attachment', specifying a filename of foo-ä.html, using RFC2231 encoded UTF-8, with double quotes around the parameter value.", false) }, + [InlineData(@"attachment; filename==?ISO-8859-1?Q?foo-=E4.html?=")] // @"Uses RFC 2047 style encoded word. ""="" is invalid inside the token production, so this is invalid.", false) }, + [InlineData(@"attachment; filename==?utf-8?B?Zm9vLeQuaHRtbA==?=")] // @"Uses RFC 2047 style encoded word. ""="" is invalid inside the token production, so this is invalid.", false) }, + public void ContentDispositionHeaderValue_ParseInvalid_Throws(string input) + { + Assert.Throws(() => ContentDispositionHeaderValue.Parse(input)); + } - [Fact] - public void HeaderNamesWithQuotes_ExpectNamesToNotHaveQuotes() - { - var contentDispositionLine = "form-data; name =\"dotnet\"; filename=\"example.png\""; - var expectedName = "dotnet"; - var expectedFileName = "example.png"; + [Fact] + public void HeaderNamesWithQuotes_ExpectNamesToNotHaveQuotes() + { + var contentDispositionLine = "form-data; name =\"dotnet\"; filename=\"example.png\""; + var expectedName = "dotnet"; + var expectedFileName = "example.png"; - var result = ContentDispositionHeaderValue.Parse(contentDispositionLine); + var result = ContentDispositionHeaderValue.Parse(contentDispositionLine); - Assert.Equal(expectedName, result.Name); - Assert.Equal(expectedFileName, result.FileName); - } + Assert.Equal(expectedName, result.Name); + Assert.Equal(expectedFileName, result.FileName); + } - [Fact] - public void FileNameWithSurrogatePairs_EncodedCorrectly() - { - var contentDisposition = new ContentDispositionHeaderValue("attachment"); + [Fact] + public void FileNameWithSurrogatePairs_EncodedCorrectly() + { + var contentDisposition = new ContentDispositionHeaderValue("attachment"); - contentDisposition.SetHttpFileName("File 🤩 name.txt"); - Assert.Equal("File __ name.txt", contentDisposition.FileName); - Assert.Equal(2, contentDisposition.Parameters.Count); - Assert.Equal("UTF-8\'\'File%20%F0%9F%A4%A9%20name.txt", contentDisposition.Parameters[1].Value); - } + contentDisposition.SetHttpFileName("File 🤩 name.txt"); + Assert.Equal("File __ name.txt", contentDisposition.FileName); + Assert.Equal(2, contentDisposition.Parameters.Count); + Assert.Equal("UTF-8\'\'File%20%F0%9F%A4%A9%20name.txt", contentDisposition.Parameters[1].Value); + } - public class ContentDispositionValue + public class ContentDispositionValue + { + public ContentDispositionValue(string value, string description, bool valid) { - public ContentDispositionValue(string value, string description, bool valid) - { - Value = value; - Description = description; - Valid = valid; - } + Value = value; + Description = description; + Valid = valid; + } - public string Value { get; } + public string Value { get; } - public string Description { get; } + public string Description { get; } - public bool Valid { get; } - } + public bool Valid { get; } + } - private void CheckValidParse(string? input, ContentDispositionHeaderValue expectedResult) - { - var result = ContentDispositionHeaderValue.Parse(input); - Assert.Equal(expectedResult, result); - } + private void CheckValidParse(string? input, ContentDispositionHeaderValue expectedResult) + { + var result = ContentDispositionHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); + } - private void CheckInvalidParse(string? input) - { - Assert.Throws(() => ContentDispositionHeaderValue.Parse(input)); - } + private void CheckInvalidParse(string? input) + { + Assert.Throws(() => ContentDispositionHeaderValue.Parse(input)); + } - private void CheckValidTryParse(string? input, ContentDispositionHeaderValue expectedResult) - { - Assert.True(ContentDispositionHeaderValue.TryParse(input, out var result), input); - Assert.Equal(expectedResult, result); - } + private void CheckValidTryParse(string? input, ContentDispositionHeaderValue expectedResult) + { + Assert.True(ContentDispositionHeaderValue.TryParse(input, out var result), input); + Assert.Equal(expectedResult, result); + } - private void CheckInvalidTryParse(string? input) - { - Assert.False(ContentDispositionHeaderValue.TryParse(input, out var result), input); - Assert.Null(result); - } + private void CheckInvalidTryParse(string? input) + { + Assert.False(ContentDispositionHeaderValue.TryParse(input, out var result), input); + Assert.Null(result); + } - private static void AssertFormatException(string contentDisposition) - { - Assert.Throws(() => new ContentDispositionHeaderValue(contentDisposition)); - } + private static void AssertFormatException(string contentDisposition) + { + Assert.Throws(() => new ContentDispositionHeaderValue(contentDisposition)); } } diff --git a/src/Http/Headers/test/ContentRangeHeaderValueTest.cs b/src/Http/Headers/test/ContentRangeHeaderValueTest.cs index bbb5456878..76cc4ad214 100644 --- a/src/Http/Headers/test/ContentRangeHeaderValueTest.cs +++ b/src/Http/Headers/test/ContentRangeHeaderValueTest.cs @@ -4,266 +4,265 @@ using System; using Xunit; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +public class ContentRangeHeaderValueTest { - public class ContentRangeHeaderValueTest + [Fact] + public void Ctor_LengthOnlyOverloadUseInvalidValues_Throw() + { + Assert.Throws(() => new ContentRangeHeaderValue(-1)); + } + + [Fact] + public void Ctor_LengthOnlyOverloadValidValues_ValuesCorrectlySet() + { + var range = new ContentRangeHeaderValue(5); + + Assert.False(range.HasRange, "HasRange"); + Assert.True(range.HasLength, "HasLength"); + Assert.Equal("bytes", range.Unit); + Assert.Null(range.From); + Assert.Null(range.To); + Assert.Equal(5, range.Length); + } + + [Fact] + public void Ctor_FromAndToOverloadUseInvalidValues_Throw() + { + Assert.Throws(() => new ContentRangeHeaderValue(-1, 1)); + Assert.Throws(() => new ContentRangeHeaderValue(0, -1)); + Assert.Throws(() => new ContentRangeHeaderValue(2, 1)); + } + + [Fact] + public void Ctor_FromAndToOverloadValidValues_ValuesCorrectlySet() + { + var range = new ContentRangeHeaderValue(0, 1); + + Assert.True(range.HasRange, "HasRange"); + Assert.False(range.HasLength, "HasLength"); + Assert.Equal("bytes", range.Unit); + Assert.Equal(0, range.From); + Assert.Equal(1, range.To); + Assert.Null(range.Length); + } + + [Fact] + public void Ctor_FromToAndLengthOverloadUseInvalidValues_Throw() + { + Assert.Throws(() => new ContentRangeHeaderValue(-1, 1, 2)); + Assert.Throws(() => new ContentRangeHeaderValue(0, -1, 2)); + Assert.Throws(() => new ContentRangeHeaderValue(0, 1, -1)); + Assert.Throws(() => new ContentRangeHeaderValue(2, 1, 3)); + Assert.Throws(() => new ContentRangeHeaderValue(1, 2, 1)); + } + + [Fact] + public void Ctor_FromToAndLengthOverloadValidValues_ValuesCorrectlySet() + { + var range = new ContentRangeHeaderValue(0, 1, 2); + + Assert.True(range.HasRange, "HasRange"); + Assert.True(range.HasLength, "HasLength"); + Assert.Equal("bytes", range.Unit); + Assert.Equal(0, range.From); + Assert.Equal(1, range.To); + Assert.Equal(2, range.Length); + } + + [Fact] + public void Unit_GetAndSetValidAndInvalidValues_MatchExpectation() + { + var range = new ContentRangeHeaderValue(0); + range.Unit = "myunit"; + Assert.Equal("myunit", range.Unit); + + Assert.Throws(() => range.Unit = null); + Assert.Throws(() => range.Unit = ""); + Assert.Throws(() => range.Unit = " x"); + Assert.Throws(() => range.Unit = "x "); + Assert.Throws(() => range.Unit = "x y"); + } + + [Fact] + public void ToString_UseDifferentRanges_AllSerializedCorrectly() + { + var range = new ContentRangeHeaderValue(1, 2, 3); + range.Unit = "myunit"; + Assert.Equal("myunit 1-2/3", range.ToString()); + + range = new ContentRangeHeaderValue(123456789012345678, 123456789012345679); + Assert.Equal("bytes 123456789012345678-123456789012345679/*", range.ToString()); + + range = new ContentRangeHeaderValue(150); + Assert.Equal("bytes */150", range.ToString()); + } + + [Fact] + public void GetHashCode_UseSameAndDifferentRanges_SameOrDifferentHashCodes() + { + var range1 = new ContentRangeHeaderValue(1, 2, 5); + var range2 = new ContentRangeHeaderValue(1, 2); + var range3 = new ContentRangeHeaderValue(5); + var range4 = new ContentRangeHeaderValue(1, 2, 5); + range4.Unit = "BYTES"; + var range5 = new ContentRangeHeaderValue(1, 2, 5); + range5.Unit = "myunit"; + + Assert.NotEqual(range1.GetHashCode(), range2.GetHashCode()); + Assert.NotEqual(range1.GetHashCode(), range3.GetHashCode()); + Assert.NotEqual(range2.GetHashCode(), range3.GetHashCode()); + Assert.Equal(range1.GetHashCode(), range4.GetHashCode()); + Assert.NotEqual(range1.GetHashCode(), range5.GetHashCode()); + } + + [Fact] + public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions() + { + var range1 = new ContentRangeHeaderValue(1, 2, 5); + var range2 = new ContentRangeHeaderValue(1, 2); + var range3 = new ContentRangeHeaderValue(5); + var range4 = new ContentRangeHeaderValue(1, 2, 5); + range4.Unit = "BYTES"; + var range5 = new ContentRangeHeaderValue(1, 2, 5); + range5.Unit = "myunit"; + var range6 = new ContentRangeHeaderValue(1, 3, 5); + var range7 = new ContentRangeHeaderValue(2, 2, 5); + var range8 = new ContentRangeHeaderValue(1, 2, 6); + + Assert.False(range1.Equals(null), "bytes 1-2/5 vs. "); + Assert.False(range1!.Equals(range2), "bytes 1-2/5 vs. bytes 1-2/*"); + Assert.False(range1.Equals(range3), "bytes 1-2/5 vs. bytes */5"); + Assert.False(range2.Equals(range3), "bytes 1-2/* vs. bytes */5"); + Assert.True(range1.Equals(range4), "bytes 1-2/5 vs. BYTES 1-2/5"); + Assert.True(range4.Equals(range1), "BYTES 1-2/5 vs. bytes 1-2/5"); + Assert.False(range1.Equals(range5), "bytes 1-2/5 vs. myunit 1-2/5"); + Assert.False(range1.Equals(range6), "bytes 1-2/5 vs. bytes 1-3/5"); + Assert.False(range1.Equals(range7), "bytes 1-2/5 vs. bytes 2-2/5"); + Assert.False(range1.Equals(range8), "bytes 1-2/5 vs. bytes 1-2/6"); + } + + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidParse(" bytes 1-2/3 ", new ContentRangeHeaderValue(1, 2, 3)); + CheckValidParse("bytes * / 3", new ContentRangeHeaderValue(3)); + + CheckValidParse(" custom 1234567890123456789-1234567890123456799/*", + new ContentRangeHeaderValue(1234567890123456789, 1234567890123456799) { Unit = "custom" }); + + CheckValidParse(" custom * / 123 ", + new ContentRangeHeaderValue(123) { Unit = "custom" }); + + // Note that we don't have a public constructor for value 'bytes */*' since the RFC doesn't mention a + // scenario for it. However, if a server returns this value, we're flexible and accept it. + var result = ContentRangeHeaderValue.Parse("bytes */*"); + Assert.Equal("bytes", result.Unit); + Assert.Null(result.From); + Assert.Null(result.To); + Assert.Null(result.Length); + Assert.False(result.HasRange, "HasRange"); + Assert.False(result.HasLength, "HasLength"); + } + + [Theory] + [InlineData("bytes 1-2/3,")] // no character after 'length' allowed + [InlineData("x bytes 1-2/3")] + [InlineData("bytes 1-2/3.4")] + [InlineData(null)] + [InlineData("")] + [InlineData("bytes 3-2/5")] + [InlineData("bytes 6-6/5")] + [InlineData("bytes 1-6/5")] + [InlineData("bytes 1-2/")] + [InlineData("bytes 1-2")] + [InlineData("bytes 1-/")] + [InlineData("bytes 1-")] + [InlineData("bytes 1")] + [InlineData("bytes ")] + [InlineData("bytes a-2/3")] + [InlineData("bytes 1-b/3")] + [InlineData("bytes 1-2/c")] + [InlineData("bytes1-2/3")] + // More than 19 digits >>Int64.MaxValue + [InlineData("bytes 1-12345678901234567890/3")] + [InlineData("bytes 12345678901234567890-3/3")] + [InlineData("bytes 1-2/12345678901234567890")] + // Exceed Int64.MaxValue, but use 19 digits + [InlineData("bytes 1-9999999999999999999/3")] + [InlineData("bytes 9999999999999999999-3/3")] + [InlineData("bytes 1-2/9999999999999999999")] + public void Parse_SetOfInvalidValueStrings_Throws(string? input) + { + Assert.Throws(() => ContentRangeHeaderValue.Parse(input)); + } + + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidTryParse(" bytes 1-2/3 ", new ContentRangeHeaderValue(1, 2, 3)); + CheckValidTryParse("bytes * / 3", new ContentRangeHeaderValue(3)); + + CheckValidTryParse(" custom 1234567890123456789-1234567890123456799/*", + new ContentRangeHeaderValue(1234567890123456789, 1234567890123456799) { Unit = "custom" }); + + CheckValidTryParse(" custom * / 123 ", + new ContentRangeHeaderValue(123) { Unit = "custom" }); + + // Note that we don't have a public constructor for value 'bytes */*' since the RFC doesn't mention a + // scenario for it. However, if a server returns this value, we're flexible and accept it. + Assert.True(ContentRangeHeaderValue.TryParse("bytes */*", out var result)); + Assert.Equal("bytes", result.Unit); + Assert.Null(result.From); + Assert.Null(result.To); + Assert.Null(result.Length); + Assert.False(result.HasRange, "HasRange"); + Assert.False(result.HasLength, "HasLength"); + } + + [Theory] + [InlineData("bytes 1-2/3,")] // no character after 'length' allowed + [InlineData("x bytes 1-2/3")] + [InlineData("bytes 1-2/3.4")] + [InlineData(null)] + [InlineData("")] + [InlineData("bytes 3-2/5")] + [InlineData("bytes 6-6/5")] + [InlineData("bytes 1-6/5")] + [InlineData("bytes 1-2/")] + [InlineData("bytes 1-2")] + [InlineData("bytes 1-/")] + [InlineData("bytes 1-")] + [InlineData("bytes 1")] + [InlineData("bytes ")] + [InlineData("bytes a-2/3")] + [InlineData("bytes 1-b/3")] + [InlineData("bytes 1-2/c")] + [InlineData("bytes1-2/3")] + // More than 19 digits >>Int64.MaxValue + [InlineData("bytes 1-12345678901234567890/3")] + [InlineData("bytes 12345678901234567890-3/3")] + [InlineData("bytes 1-2/12345678901234567890")] + // Exceed Int64.MaxValue, but use 19 digits + [InlineData("bytes 1-9999999999999999999/3")] + [InlineData("bytes 9999999999999999999-3/3")] + [InlineData("bytes 1-2/9999999999999999999")] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse(string? input) + { + Assert.False(ContentRangeHeaderValue.TryParse(input, out var result)); + Assert.Null(result); + } + + private void CheckValidParse(string? input, ContentRangeHeaderValue expectedResult) + { + var result = ContentRangeHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); + } + + private void CheckValidTryParse(string? input, ContentRangeHeaderValue expectedResult) { - [Fact] - public void Ctor_LengthOnlyOverloadUseInvalidValues_Throw() - { - Assert.Throws(() => new ContentRangeHeaderValue(-1)); - } - - [Fact] - public void Ctor_LengthOnlyOverloadValidValues_ValuesCorrectlySet() - { - var range = new ContentRangeHeaderValue(5); - - Assert.False(range.HasRange, "HasRange"); - Assert.True(range.HasLength, "HasLength"); - Assert.Equal("bytes", range.Unit); - Assert.Null(range.From); - Assert.Null(range.To); - Assert.Equal(5, range.Length); - } - - [Fact] - public void Ctor_FromAndToOverloadUseInvalidValues_Throw() - { - Assert.Throws(() => new ContentRangeHeaderValue(-1, 1)); - Assert.Throws(() => new ContentRangeHeaderValue(0, -1)); - Assert.Throws(() => new ContentRangeHeaderValue(2, 1)); - } - - [Fact] - public void Ctor_FromAndToOverloadValidValues_ValuesCorrectlySet() - { - var range = new ContentRangeHeaderValue(0, 1); - - Assert.True(range.HasRange, "HasRange"); - Assert.False(range.HasLength, "HasLength"); - Assert.Equal("bytes", range.Unit); - Assert.Equal(0, range.From); - Assert.Equal(1, range.To); - Assert.Null(range.Length); - } - - [Fact] - public void Ctor_FromToAndLengthOverloadUseInvalidValues_Throw() - { - Assert.Throws(() => new ContentRangeHeaderValue(-1, 1, 2)); - Assert.Throws(() => new ContentRangeHeaderValue(0, -1, 2)); - Assert.Throws(() => new ContentRangeHeaderValue(0, 1, -1)); - Assert.Throws(() => new ContentRangeHeaderValue(2, 1, 3)); - Assert.Throws(() => new ContentRangeHeaderValue(1, 2, 1)); - } - - [Fact] - public void Ctor_FromToAndLengthOverloadValidValues_ValuesCorrectlySet() - { - var range = new ContentRangeHeaderValue(0, 1, 2); - - Assert.True(range.HasRange, "HasRange"); - Assert.True(range.HasLength, "HasLength"); - Assert.Equal("bytes", range.Unit); - Assert.Equal(0, range.From); - Assert.Equal(1, range.To); - Assert.Equal(2, range.Length); - } - - [Fact] - public void Unit_GetAndSetValidAndInvalidValues_MatchExpectation() - { - var range = new ContentRangeHeaderValue(0); - range.Unit = "myunit"; - Assert.Equal("myunit", range.Unit); - - Assert.Throws(() => range.Unit = null); - Assert.Throws(() => range.Unit = ""); - Assert.Throws(() => range.Unit = " x"); - Assert.Throws(() => range.Unit = "x "); - Assert.Throws(() => range.Unit = "x y"); - } - - [Fact] - public void ToString_UseDifferentRanges_AllSerializedCorrectly() - { - var range = new ContentRangeHeaderValue(1, 2, 3); - range.Unit = "myunit"; - Assert.Equal("myunit 1-2/3", range.ToString()); - - range = new ContentRangeHeaderValue(123456789012345678, 123456789012345679); - Assert.Equal("bytes 123456789012345678-123456789012345679/*", range.ToString()); - - range = new ContentRangeHeaderValue(150); - Assert.Equal("bytes */150", range.ToString()); - } - - [Fact] - public void GetHashCode_UseSameAndDifferentRanges_SameOrDifferentHashCodes() - { - var range1 = new ContentRangeHeaderValue(1, 2, 5); - var range2 = new ContentRangeHeaderValue(1, 2); - var range3 = new ContentRangeHeaderValue(5); - var range4 = new ContentRangeHeaderValue(1, 2, 5); - range4.Unit = "BYTES"; - var range5 = new ContentRangeHeaderValue(1, 2, 5); - range5.Unit = "myunit"; - - Assert.NotEqual(range1.GetHashCode(), range2.GetHashCode()); - Assert.NotEqual(range1.GetHashCode(), range3.GetHashCode()); - Assert.NotEqual(range2.GetHashCode(), range3.GetHashCode()); - Assert.Equal(range1.GetHashCode(), range4.GetHashCode()); - Assert.NotEqual(range1.GetHashCode(), range5.GetHashCode()); - } - - [Fact] - public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions() - { - var range1 = new ContentRangeHeaderValue(1, 2, 5); - var range2 = new ContentRangeHeaderValue(1, 2); - var range3 = new ContentRangeHeaderValue(5); - var range4 = new ContentRangeHeaderValue(1, 2, 5); - range4.Unit = "BYTES"; - var range5 = new ContentRangeHeaderValue(1, 2, 5); - range5.Unit = "myunit"; - var range6 = new ContentRangeHeaderValue(1, 3, 5); - var range7 = new ContentRangeHeaderValue(2, 2, 5); - var range8 = new ContentRangeHeaderValue(1, 2, 6); - - Assert.False(range1.Equals(null), "bytes 1-2/5 vs. "); - Assert.False(range1!.Equals(range2), "bytes 1-2/5 vs. bytes 1-2/*"); - Assert.False(range1.Equals(range3), "bytes 1-2/5 vs. bytes */5"); - Assert.False(range2.Equals(range3), "bytes 1-2/* vs. bytes */5"); - Assert.True(range1.Equals(range4), "bytes 1-2/5 vs. BYTES 1-2/5"); - Assert.True(range4.Equals(range1), "BYTES 1-2/5 vs. bytes 1-2/5"); - Assert.False(range1.Equals(range5), "bytes 1-2/5 vs. myunit 1-2/5"); - Assert.False(range1.Equals(range6), "bytes 1-2/5 vs. bytes 1-3/5"); - Assert.False(range1.Equals(range7), "bytes 1-2/5 vs. bytes 2-2/5"); - Assert.False(range1.Equals(range8), "bytes 1-2/5 vs. bytes 1-2/6"); - } - - [Fact] - public void Parse_SetOfValidValueStrings_ParsedCorrectly() - { - CheckValidParse(" bytes 1-2/3 ", new ContentRangeHeaderValue(1, 2, 3)); - CheckValidParse("bytes * / 3", new ContentRangeHeaderValue(3)); - - CheckValidParse(" custom 1234567890123456789-1234567890123456799/*", - new ContentRangeHeaderValue(1234567890123456789, 1234567890123456799) { Unit = "custom" }); - - CheckValidParse(" custom * / 123 ", - new ContentRangeHeaderValue(123) { Unit = "custom" }); - - // Note that we don't have a public constructor for value 'bytes */*' since the RFC doesn't mention a - // scenario for it. However, if a server returns this value, we're flexible and accept it. - var result = ContentRangeHeaderValue.Parse("bytes */*"); - Assert.Equal("bytes", result.Unit); - Assert.Null(result.From); - Assert.Null(result.To); - Assert.Null(result.Length); - Assert.False(result.HasRange, "HasRange"); - Assert.False(result.HasLength, "HasLength"); - } - - [Theory] - [InlineData("bytes 1-2/3,")] // no character after 'length' allowed - [InlineData("x bytes 1-2/3")] - [InlineData("bytes 1-2/3.4")] - [InlineData(null)] - [InlineData("")] - [InlineData("bytes 3-2/5")] - [InlineData("bytes 6-6/5")] - [InlineData("bytes 1-6/5")] - [InlineData("bytes 1-2/")] - [InlineData("bytes 1-2")] - [InlineData("bytes 1-/")] - [InlineData("bytes 1-")] - [InlineData("bytes 1")] - [InlineData("bytes ")] - [InlineData("bytes a-2/3")] - [InlineData("bytes 1-b/3")] - [InlineData("bytes 1-2/c")] - [InlineData("bytes1-2/3")] - // More than 19 digits >>Int64.MaxValue - [InlineData("bytes 1-12345678901234567890/3")] - [InlineData("bytes 12345678901234567890-3/3")] - [InlineData("bytes 1-2/12345678901234567890")] - // Exceed Int64.MaxValue, but use 19 digits - [InlineData("bytes 1-9999999999999999999/3")] - [InlineData("bytes 9999999999999999999-3/3")] - [InlineData("bytes 1-2/9999999999999999999")] - public void Parse_SetOfInvalidValueStrings_Throws(string? input) - { - Assert.Throws(() => ContentRangeHeaderValue.Parse(input)); - } - - [Fact] - public void TryParse_SetOfValidValueStrings_ParsedCorrectly() - { - CheckValidTryParse(" bytes 1-2/3 ", new ContentRangeHeaderValue(1, 2, 3)); - CheckValidTryParse("bytes * / 3", new ContentRangeHeaderValue(3)); - - CheckValidTryParse(" custom 1234567890123456789-1234567890123456799/*", - new ContentRangeHeaderValue(1234567890123456789, 1234567890123456799) { Unit = "custom" }); - - CheckValidTryParse(" custom * / 123 ", - new ContentRangeHeaderValue(123) { Unit = "custom" }); - - // Note that we don't have a public constructor for value 'bytes */*' since the RFC doesn't mention a - // scenario for it. However, if a server returns this value, we're flexible and accept it. - Assert.True(ContentRangeHeaderValue.TryParse("bytes */*", out var result)); - Assert.Equal("bytes", result.Unit); - Assert.Null(result.From); - Assert.Null(result.To); - Assert.Null(result.Length); - Assert.False(result.HasRange, "HasRange"); - Assert.False(result.HasLength, "HasLength"); - } - - [Theory] - [InlineData("bytes 1-2/3,")] // no character after 'length' allowed - [InlineData("x bytes 1-2/3")] - [InlineData("bytes 1-2/3.4")] - [InlineData(null)] - [InlineData("")] - [InlineData("bytes 3-2/5")] - [InlineData("bytes 6-6/5")] - [InlineData("bytes 1-6/5")] - [InlineData("bytes 1-2/")] - [InlineData("bytes 1-2")] - [InlineData("bytes 1-/")] - [InlineData("bytes 1-")] - [InlineData("bytes 1")] - [InlineData("bytes ")] - [InlineData("bytes a-2/3")] - [InlineData("bytes 1-b/3")] - [InlineData("bytes 1-2/c")] - [InlineData("bytes1-2/3")] - // More than 19 digits >>Int64.MaxValue - [InlineData("bytes 1-12345678901234567890/3")] - [InlineData("bytes 12345678901234567890-3/3")] - [InlineData("bytes 1-2/12345678901234567890")] - // Exceed Int64.MaxValue, but use 19 digits - [InlineData("bytes 1-9999999999999999999/3")] - [InlineData("bytes 9999999999999999999-3/3")] - [InlineData("bytes 1-2/9999999999999999999")] - public void TryParse_SetOfInvalidValueStrings_ReturnsFalse(string? input) - { - Assert.False(ContentRangeHeaderValue.TryParse(input, out var result)); - Assert.Null(result); - } - - private void CheckValidParse(string? input, ContentRangeHeaderValue expectedResult) - { - var result = ContentRangeHeaderValue.Parse(input); - Assert.Equal(expectedResult, result); - } - - private void CheckValidTryParse(string? input, ContentRangeHeaderValue expectedResult) - { - Assert.True(ContentRangeHeaderValue.TryParse(input, out var result)); - Assert.Equal(expectedResult, result); - } + Assert.True(ContentRangeHeaderValue.TryParse(input, out var result)); + Assert.Equal(expectedResult, result); } } diff --git a/src/Http/Headers/test/CookieHeaderValueTest.cs b/src/Http/Headers/test/CookieHeaderValueTest.cs index 67466caac3..fe04133375 100644 --- a/src/Http/Headers/test/CookieHeaderValueTest.cs +++ b/src/Http/Headers/test/CookieHeaderValueTest.cs @@ -6,49 +6,49 @@ using System.Collections.Generic; using System.Linq; using Xunit; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +public class CookieHeaderValueTest { - public class CookieHeaderValueTest + public static TheoryData CookieHeaderDataSet { - public static TheoryData CookieHeaderDataSet + get { - get - { - var dataset = new TheoryData(); - var header1 = new CookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3"); - dataset.Add(header1, "name1=n1=v1&n2=v2&n3=v3"); + var dataset = new TheoryData(); + var header1 = new CookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3"); + dataset.Add(header1, "name1=n1=v1&n2=v2&n3=v3"); - var header2 = new CookieHeaderValue("name2", ""); - dataset.Add(header2, "name2="); + var header2 = new CookieHeaderValue("name2", ""); + dataset.Add(header2, "name2="); - var header3 = new CookieHeaderValue("name3", "value3"); - dataset.Add(header3, "name3=value3"); + var header3 = new CookieHeaderValue("name3", "value3"); + dataset.Add(header3, "name3=value3"); - var header4 = new CookieHeaderValue("name4", "\"value4\""); - dataset.Add(header4, "name4=\"value4\""); + var header4 = new CookieHeaderValue("name4", "\"value4\""); + dataset.Add(header4, "name4=\"value4\""); - return dataset; - } + return dataset; } + } - public static TheoryData InvalidCookieHeaderDataSet + public static TheoryData InvalidCookieHeaderDataSet + { + get { - get - { - return new TheoryData + return new TheoryData { "=value", "name=value;", "name=value,", }; - } } + } - public static TheoryData InvalidCookieNames + public static TheoryData InvalidCookieNames + { + get { - get - { - return new TheoryData + return new TheoryData { "", "{acb}", @@ -59,14 +59,14 @@ namespace Microsoft.Net.Http.Headers "a\\b", "a b", }; - } } + } - public static TheoryData InvalidCookieValues + public static TheoryData InvalidCookieValues + { + get { - get - { - return new TheoryData + return new TheoryData { { "\"" }, { "a,b" }, @@ -77,250 +77,249 @@ namespace Microsoft.Net.Http.Headers { "abc\"" }, { "a b" }, }; - } } + } - public static TheoryData, string?[]> ListOfCookieHeaderDataSet + public static TheoryData, string?[]> ListOfCookieHeaderDataSet + { + get { - get - { - var dataset = new TheoryData, string?[]>(); - var header1 = new CookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3"); - var string1 = "name1=n1=v1&n2=v2&n3=v3"; - - var header2 = new CookieHeaderValue("name2", "value2"); - var string2 = "name2=value2"; - - var header3 = new CookieHeaderValue("name3", "value3"); - var string3 = "name3=value3"; - - var header4 = new CookieHeaderValue("name4", "\"value4\""); - var string4 = "name4=\"value4\""; - - dataset.Add(new[] { header1 }.ToList(), new[] { string1 }); - dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 }); - dataset.Add(new[] { header1, header1 }.ToList(), new [] { string1, null, "", " ", ";", " , ", string1 }); - dataset.Add(new[] { header2 }.ToList(), new[] { string2 }); - dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1, string2 }); - dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1 + ", " + string2 }); - dataset.Add(new[] { header2, header1 }.ToList(), new[] { string2 + "; " + string1 }); - dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4 }); - dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) }); - dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(";", string1, string2, string3, string4) }); - - return dataset; - } + var dataset = new TheoryData, string?[]>(); + var header1 = new CookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3"); + var string1 = "name1=n1=v1&n2=v2&n3=v3"; + + var header2 = new CookieHeaderValue("name2", "value2"); + var string2 = "name2=value2"; + + var header3 = new CookieHeaderValue("name3", "value3"); + var string3 = "name3=value3"; + + var header4 = new CookieHeaderValue("name4", "\"value4\""); + var string4 = "name4=\"value4\""; + + dataset.Add(new[] { header1 }.ToList(), new[] { string1 }); + dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 }); + dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ";", " , ", string1 }); + dataset.Add(new[] { header2 }.ToList(), new[] { string2 }); + dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1, string2 }); + dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1 + ", " + string2 }); + dataset.Add(new[] { header2, header1 }.ToList(), new[] { string2 + "; " + string1 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(";", string1, string2, string3, string4) }); + + return dataset; } + } - public static TheoryData?, string?[]> ListWithInvalidCookieHeaderDataSet + public static TheoryData?, string?[]> ListWithInvalidCookieHeaderDataSet + { + get { - get - { - var dataset = new TheoryData?, string?[]>(); - var header1 = new CookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3"); - var validString1 = "name1=n1=v1&n2=v2&n3=v3"; - - var header2 = new CookieHeaderValue("name2", "value2"); - var validString2 = "name2=value2"; - - var header3 = new CookieHeaderValue("name3", "value3"); - var validString3 = "name3=value3"; - - var invalidString1 = "ipt={\"v\":{\"L\":3},\"pt\":{\"d\":3},ct\":{},\"_t\":44,\"_v\":\"2\"}"; - - dataset.Add(null, new[] { invalidString1 }); - dataset.Add(new[] { header1 }.ToList(), new[] { validString1, invalidString1 }); - dataset.Add(new[] { header1 }.ToList(), new[] { validString1, null, "", " ", ";", " , ", invalidString1 }); - dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1, null, "", " ", ";", " , ", validString1 }); - dataset.Add(new[] { header1 }.ToList(), new[] { validString1 + ", " + invalidString1 }); - dataset.Add(new[] { header2 }.ToList(), new[] { invalidString1 + ", " + validString2 }); - dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1 + "; " + validString1 }); - dataset.Add(new[] { header2 }.ToList(), new[] { validString2 + "; " + invalidString1 }); - dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { invalidString1, validString1, validString2, validString3 }); - dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, invalidString1, validString2, validString3 }); - dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, validString2, invalidString1, validString3 }); - dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, validString2, validString3, invalidString1 }); - dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", invalidString1, validString1, validString2, validString3) }); - dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, invalidString1, validString2, validString3) }); - dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, validString2, invalidString1, validString3) }); - dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, validString2, validString3, invalidString1) }); - dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", invalidString1, validString1, validString2, validString3) }); - dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", validString1, invalidString1, validString2, validString3) }); - dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", validString1, validString2, invalidString1, validString3) }); - dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", validString1, validString2, validString3, invalidString1) }); - - return dataset; - } + var dataset = new TheoryData?, string?[]>(); + var header1 = new CookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3"); + var validString1 = "name1=n1=v1&n2=v2&n3=v3"; + + var header2 = new CookieHeaderValue("name2", "value2"); + var validString2 = "name2=value2"; + + var header3 = new CookieHeaderValue("name3", "value3"); + var validString3 = "name3=value3"; + + var invalidString1 = "ipt={\"v\":{\"L\":3},\"pt\":{\"d\":3},ct\":{},\"_t\":44,\"_v\":\"2\"}"; + + dataset.Add(null, new[] { invalidString1 }); + dataset.Add(new[] { header1 }.ToList(), new[] { validString1, invalidString1 }); + dataset.Add(new[] { header1 }.ToList(), new[] { validString1, null, "", " ", ";", " , ", invalidString1 }); + dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1, null, "", " ", ";", " , ", validString1 }); + dataset.Add(new[] { header1 }.ToList(), new[] { validString1 + ", " + invalidString1 }); + dataset.Add(new[] { header2 }.ToList(), new[] { invalidString1 + ", " + validString2 }); + dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1 + "; " + validString1 }); + dataset.Add(new[] { header2 }.ToList(), new[] { validString2 + "; " + invalidString1 }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { invalidString1, validString1, validString2, validString3 }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, invalidString1, validString2, validString3 }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, validString2, invalidString1, validString3 }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, validString2, validString3, invalidString1 }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", invalidString1, validString1, validString2, validString3) }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, invalidString1, validString2, validString3) }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, validString2, invalidString1, validString3) }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, validString2, validString3, invalidString1) }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", invalidString1, validString1, validString2, validString3) }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", validString1, invalidString1, validString2, validString3) }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", validString1, validString2, invalidString1, validString3) }); + dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", validString1, validString2, validString3, invalidString1) }); + + return dataset; } + } - [Fact] - public void CookieHeaderValue_CtorThrowsOnNullName() - { - Assert.Throws(() => new CookieHeaderValue(null, "value")); - } + [Fact] + public void CookieHeaderValue_CtorThrowsOnNullName() + { + Assert.Throws(() => new CookieHeaderValue(null, "value")); + } - [Theory] - [MemberData(nameof(InvalidCookieNames))] - public void CookieHeaderValue_CtorThrowsOnInvalidName(string name) - { - Assert.Throws(() => new CookieHeaderValue(name, "value")); - } + [Theory] + [MemberData(nameof(InvalidCookieNames))] + public void CookieHeaderValue_CtorThrowsOnInvalidName(string name) + { + Assert.Throws(() => new CookieHeaderValue(name, "value")); + } - [Theory] - [MemberData(nameof(InvalidCookieValues))] - public void CookieHeaderValue_CtorThrowsOnInvalidValue(string value) - { - Assert.Throws(() => new CookieHeaderValue("name", value)); - } + [Theory] + [MemberData(nameof(InvalidCookieValues))] + public void CookieHeaderValue_CtorThrowsOnInvalidValue(string value) + { + Assert.Throws(() => new CookieHeaderValue("name", value)); + } - [Fact] - public void CookieHeaderValue_Ctor1_InitializesCorrectly() - { - var header = new CookieHeaderValue("cookie"); - Assert.Equal("cookie", header.Name); - Assert.Equal(string.Empty, header.Value); - } + [Fact] + public void CookieHeaderValue_Ctor1_InitializesCorrectly() + { + var header = new CookieHeaderValue("cookie"); + Assert.Equal("cookie", header.Name); + Assert.Equal(string.Empty, header.Value); + } - [Theory] - [InlineData("name", "")] - [InlineData("name", "value")] - [InlineData("name", "\"acb\"")] - public void CookieHeaderValue_Ctor2InitializesCorrectly(string name, string value) - { - var header = new CookieHeaderValue(name, value); - Assert.Equal(name, header.Name); - Assert.Equal(value, header.Value); - } + [Theory] + [InlineData("name", "")] + [InlineData("name", "value")] + [InlineData("name", "\"acb\"")] + public void CookieHeaderValue_Ctor2InitializesCorrectly(string name, string value) + { + var header = new CookieHeaderValue(name, value); + Assert.Equal(name, header.Name); + Assert.Equal(value, header.Value); + } - [Fact] - public void CookieHeaderValue_Value() - { - var cookie = new CookieHeaderValue("name"); - Assert.Equal(string.Empty, cookie.Value); + [Fact] + public void CookieHeaderValue_Value() + { + var cookie = new CookieHeaderValue("name"); + Assert.Equal(string.Empty, cookie.Value); - cookie.Value = "value1"; - Assert.Equal("value1", cookie.Value); - } + cookie.Value = "value1"; + Assert.Equal("value1", cookie.Value); + } - [Theory] - [MemberData(nameof(CookieHeaderDataSet))] - public void CookieHeaderValue_ToString(CookieHeaderValue input, string expectedValue) - { - Assert.Equal(expectedValue, input.ToString()); - } + [Theory] + [MemberData(nameof(CookieHeaderDataSet))] + public void CookieHeaderValue_ToString(CookieHeaderValue input, string expectedValue) + { + Assert.Equal(expectedValue, input.ToString()); + } - [Theory] - [MemberData(nameof(CookieHeaderDataSet))] - public void CookieHeaderValue_Parse_AcceptsValidValues(CookieHeaderValue cookie, string expectedValue) - { - var header = CookieHeaderValue.Parse(expectedValue); + [Theory] + [MemberData(nameof(CookieHeaderDataSet))] + public void CookieHeaderValue_Parse_AcceptsValidValues(CookieHeaderValue cookie, string expectedValue) + { + var header = CookieHeaderValue.Parse(expectedValue); - Assert.Equal(cookie, header); - Assert.Equal(expectedValue, header.ToString()); - } + Assert.Equal(cookie, header); + Assert.Equal(expectedValue, header.ToString()); + } - [Theory] - [MemberData(nameof(CookieHeaderDataSet))] - public void CookieHeaderValue_TryParse_AcceptsValidValues(CookieHeaderValue cookie, string expectedValue) - { - Assert.True(CookieHeaderValue.TryParse(expectedValue, out var header)); + [Theory] + [MemberData(nameof(CookieHeaderDataSet))] + public void CookieHeaderValue_TryParse_AcceptsValidValues(CookieHeaderValue cookie, string expectedValue) + { + Assert.True(CookieHeaderValue.TryParse(expectedValue, out var header)); - Assert.Equal(cookie, header); - Assert.Equal(expectedValue, header!.ToString()); - } + Assert.Equal(cookie, header); + Assert.Equal(expectedValue, header!.ToString()); + } - [Theory] - [MemberData(nameof(InvalidCookieHeaderDataSet))] - public void CookieHeaderValue_Parse_RejectsInvalidValues(string value) - { - Assert.Throws(() => CookieHeaderValue.Parse(value)); - } + [Theory] + [MemberData(nameof(InvalidCookieHeaderDataSet))] + public void CookieHeaderValue_Parse_RejectsInvalidValues(string value) + { + Assert.Throws(() => CookieHeaderValue.Parse(value)); + } - [Theory] - [MemberData(nameof(InvalidCookieHeaderDataSet))] - public void CookieHeaderValue_TryParse_RejectsInvalidValues(string value) - { - Assert.False(CookieHeaderValue.TryParse(value, out var _)); - } + [Theory] + [MemberData(nameof(InvalidCookieHeaderDataSet))] + public void CookieHeaderValue_TryParse_RejectsInvalidValues(string value) + { + Assert.False(CookieHeaderValue.TryParse(value, out var _)); + } - [Theory] - [MemberData(nameof(ListOfCookieHeaderDataSet))] - public void CookieHeaderValue_ParseList_AcceptsValidValues(IList cookies, string[] input) - { - var results = CookieHeaderValue.ParseList(input); + [Theory] + [MemberData(nameof(ListOfCookieHeaderDataSet))] + public void CookieHeaderValue_ParseList_AcceptsValidValues(IList cookies, string[] input) + { + var results = CookieHeaderValue.ParseList(input); - Assert.Equal(cookies, results); - } + Assert.Equal(cookies, results); + } - [Theory] - [MemberData(nameof(ListOfCookieHeaderDataSet))] - public void CookieHeaderValue_ParseStrictList_AcceptsValidValues(IList cookies, string[] input) - { - var results = CookieHeaderValue.ParseStrictList(input); + [Theory] + [MemberData(nameof(ListOfCookieHeaderDataSet))] + public void CookieHeaderValue_ParseStrictList_AcceptsValidValues(IList cookies, string[] input) + { + var results = CookieHeaderValue.ParseStrictList(input); - Assert.Equal(cookies, results); - } + Assert.Equal(cookies, results); + } - [Theory] - [MemberData(nameof(ListOfCookieHeaderDataSet))] - public void CookieHeaderValue_TryParseList_AcceptsValidValues(IList cookies, string[] input) - { - var result = CookieHeaderValue.TryParseList(input, out var results); - Assert.True(result); + [Theory] + [MemberData(nameof(ListOfCookieHeaderDataSet))] + public void CookieHeaderValue_TryParseList_AcceptsValidValues(IList cookies, string[] input) + { + var result = CookieHeaderValue.TryParseList(input, out var results); + Assert.True(result); - Assert.Equal(cookies, results); - } + Assert.Equal(cookies, results); + } - [Theory] - [MemberData(nameof(ListOfCookieHeaderDataSet))] - public void CookieHeaderValue_TryParseStrictList_AcceptsValidValues(IList cookies, string[] input) - { - var result = CookieHeaderValue.TryParseStrictList(input, out var results); - Assert.True(result); + [Theory] + [MemberData(nameof(ListOfCookieHeaderDataSet))] + public void CookieHeaderValue_TryParseStrictList_AcceptsValidValues(IList cookies, string[] input) + { + var result = CookieHeaderValue.TryParseStrictList(input, out var results); + Assert.True(result); - Assert.Equal(cookies, results); - } + Assert.Equal(cookies, results); + } - [Theory] - [MemberData(nameof(ListWithInvalidCookieHeaderDataSet))] - public void CookieHeaderValue_ParseList_ExcludesInvalidValues(IList cookies, string[] input) - { - var results = CookieHeaderValue.ParseList(input); - // ParseList always returns a list, even if empty. TryParseList may return null (via out). - Assert.Equal(cookies ?? new List(), results); - } + [Theory] + [MemberData(nameof(ListWithInvalidCookieHeaderDataSet))] + public void CookieHeaderValue_ParseList_ExcludesInvalidValues(IList cookies, string[] input) + { + var results = CookieHeaderValue.ParseList(input); + // ParseList always returns a list, even if empty. TryParseList may return null (via out). + Assert.Equal(cookies ?? new List(), results); + } - [Theory] - [MemberData(nameof(ListWithInvalidCookieHeaderDataSet))] - public void CookieHeaderValue_TryParseList_ExcludesInvalidValues(IList cookies, string[] input) - { - var result = CookieHeaderValue.TryParseList(input, out var results); - Assert.Equal(cookies, results); - Assert.Equal(cookies?.Count > 0, result); - } + [Theory] + [MemberData(nameof(ListWithInvalidCookieHeaderDataSet))] + public void CookieHeaderValue_TryParseList_ExcludesInvalidValues(IList cookies, string[] input) + { + var result = CookieHeaderValue.TryParseList(input, out var results); + Assert.Equal(cookies, results); + Assert.Equal(cookies?.Count > 0, result); + } - [Theory] - [MemberData(nameof(ListWithInvalidCookieHeaderDataSet))] - public void CookieHeaderValue_ParseStrictList_ThrowsForAnyInvalidValues( + [Theory] + [MemberData(nameof(ListWithInvalidCookieHeaderDataSet))] + public void CookieHeaderValue_ParseStrictList_ThrowsForAnyInvalidValues( #pragma warning disable xUnit1026 // Theory methods should use all of their parameters IList cookies, #pragma warning restore xUnit1026 // Theory methods should use all of their parameters string[] input) - { - Assert.Throws(() => CookieHeaderValue.ParseStrictList(input)); - } + { + Assert.Throws(() => CookieHeaderValue.ParseStrictList(input)); + } - [Theory] - [MemberData(nameof(ListWithInvalidCookieHeaderDataSet))] - public void CookieHeaderValue_TryParseStrictList_FailsForAnyInvalidValues( + [Theory] + [MemberData(nameof(ListWithInvalidCookieHeaderDataSet))] + public void CookieHeaderValue_TryParseStrictList_FailsForAnyInvalidValues( #pragma warning disable xUnit1026 // Theory methods should use all of their parameters IList cookies, #pragma warning restore xUnit1026 // Theory methods should use all of their parameters string[] input) - { - var result = CookieHeaderValue.TryParseStrictList(input, out var results); - Assert.Null(results); - Assert.False(result); - } + { + var result = CookieHeaderValue.TryParseStrictList(input, out var results); + Assert.Null(results); + Assert.False(result); } } diff --git a/src/Http/Headers/test/DateParserTest.cs b/src/Http/Headers/test/DateParserTest.cs index be168e35b2..70b88aae15 100644 --- a/src/Http/Headers/test/DateParserTest.cs +++ b/src/Http/Headers/test/DateParserTest.cs @@ -5,52 +5,51 @@ using System; using System.Collections.Generic; using Xunit; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +public class DateParserTest { - public class DateParserTest + [Theory] + [MemberData(nameof(ValidStringData))] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly(string input, DateTimeOffset expected) + { + // We don't need to validate all possible date values, since they're already tested in HttpRuleParserTest. + // Just make sure the parser calls HttpRuleParser methods correctly. + Assert.True(HeaderUtilities.TryParseDate(input, out var result)); + Assert.Equal(expected, result); + } + + public static IEnumerable ValidStringData() + { + yield return new object[] { "Tue, 15 Nov 1994 08:12:31 GMT", new DateTimeOffset(1994, 11, 15, 8, 12, 31, TimeSpan.Zero) }; + yield return new object[] { " Sunday, 06-Nov-94 08:49:37 GMT ", new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero) }; + yield return new object[] { " Tue,\r\n 15 Nov\r\n 1994 08:12:31 GMT ", new DateTimeOffset(1994, 11, 15, 8, 12, 31, TimeSpan.Zero) }; + yield return new object[] { "Sat, 09-Dec-2017 07:07:03 GMT ", new DateTimeOffset(2017, 12, 09, 7, 7, 3, TimeSpan.Zero) }; + } + + [Theory] + [MemberData(nameof(InvalidStringData))] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse(string input) { - [Theory] - [MemberData(nameof(ValidStringData))] - public void TryParse_SetOfValidValueStrings_ParsedCorrectly(string input, DateTimeOffset expected) - { - // We don't need to validate all possible date values, since they're already tested in HttpRuleParserTest. - // Just make sure the parser calls HttpRuleParser methods correctly. - Assert.True(HeaderUtilities.TryParseDate(input, out var result)); - Assert.Equal(expected, result); - } - - public static IEnumerable ValidStringData() - { - yield return new object[] { "Tue, 15 Nov 1994 08:12:31 GMT", new DateTimeOffset(1994, 11, 15, 8, 12, 31, TimeSpan.Zero) }; - yield return new object[] { " Sunday, 06-Nov-94 08:49:37 GMT ", new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero) }; - yield return new object[] { " Tue,\r\n 15 Nov\r\n 1994 08:12:31 GMT ", new DateTimeOffset(1994, 11, 15, 8, 12, 31, TimeSpan.Zero) }; - yield return new object[] { "Sat, 09-Dec-2017 07:07:03 GMT ", new DateTimeOffset(2017, 12, 09, 7, 7, 3, TimeSpan.Zero) }; - } - - [Theory] - [MemberData(nameof(InvalidStringData))] - public void TryParse_SetOfInvalidValueStrings_ReturnsFalse(string input) - { - Assert.False(HeaderUtilities.TryParseDate(input, out var result)); - Assert.Equal(new DateTimeOffset(), result); - } - - public static IEnumerable InvalidStringData() - { - yield return new object?[] { null }; - yield return new object[] { string.Empty }; - yield return new object[] { " " }; - yield return new object[] { "!!Sunday, 06-Nov-94 08:49:37 GMT" }; - } - - [Fact] - public void ToString_UseDifferentValues_MatchExpectation() - { - Assert.Equal("Sat, 31 Jul 2010 15:38:57 GMT", - HeaderUtilities.FormatDate(new DateTimeOffset(2010, 7, 31, 15, 38, 57, TimeSpan.Zero))); - - Assert.Equal("Fri, 01 Jan 2010 01:01:01 GMT", - HeaderUtilities.FormatDate(new DateTimeOffset(2010, 1, 1, 1, 1, 1, TimeSpan.Zero))); - } + Assert.False(HeaderUtilities.TryParseDate(input, out var result)); + Assert.Equal(new DateTimeOffset(), result); + } + + public static IEnumerable InvalidStringData() + { + yield return new object?[] { null }; + yield return new object[] { string.Empty }; + yield return new object[] { " " }; + yield return new object[] { "!!Sunday, 06-Nov-94 08:49:37 GMT" }; + } + + [Fact] + public void ToString_UseDifferentValues_MatchExpectation() + { + Assert.Equal("Sat, 31 Jul 2010 15:38:57 GMT", + HeaderUtilities.FormatDate(new DateTimeOffset(2010, 7, 31, 15, 38, 57, TimeSpan.Zero))); + + Assert.Equal("Fri, 01 Jan 2010 01:01:01 GMT", + HeaderUtilities.FormatDate(new DateTimeOffset(2010, 1, 1, 1, 1, 1, TimeSpan.Zero))); } } diff --git a/src/Http/Headers/test/EntityTagHeaderValueTest.cs b/src/Http/Headers/test/EntityTagHeaderValueTest.cs index fefb5d5ab8..9357944dfe 100644 --- a/src/Http/Headers/test/EntityTagHeaderValueTest.cs +++ b/src/Http/Headers/test/EntityTagHeaderValueTest.cs @@ -6,111 +6,111 @@ using System.Collections.Generic; using System.Linq; using Xunit; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +public class EntityTagHeaderValueTest { - public class EntityTagHeaderValueTest + [Fact] + public void Ctor_ETagNull_Throw() { - [Fact] - public void Ctor_ETagNull_Throw() - { - Assert.Throws(() => new EntityTagHeaderValue(null)); - // null and empty should be treated the same. So we also throw for empty strings. - Assert.Throws(() => new EntityTagHeaderValue(string.Empty)); - } + Assert.Throws(() => new EntityTagHeaderValue(null)); + // null and empty should be treated the same. So we also throw for empty strings. + Assert.Throws(() => new EntityTagHeaderValue(string.Empty)); + } - [Fact] - public void Ctor_ETagInvalidFormat_ThrowFormatException() - { - // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. - AssertFormatException("tag"); - AssertFormatException(" tag "); - AssertFormatException("\"tag\" invalid"); - AssertFormatException("\"tag"); - AssertFormatException("tag\""); - AssertFormatException("\"tag\"\""); - AssertFormatException("\"\"tag\"\""); - AssertFormatException("\"\"tag\""); - AssertFormatException("W/\"tag\""); // tag value must not contain 'W/' - } + [Fact] + public void Ctor_ETagInvalidFormat_ThrowFormatException() + { + // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. + AssertFormatException("tag"); + AssertFormatException(" tag "); + AssertFormatException("\"tag\" invalid"); + AssertFormatException("\"tag"); + AssertFormatException("tag\""); + AssertFormatException("\"tag\"\""); + AssertFormatException("\"\"tag\"\""); + AssertFormatException("\"\"tag\""); + AssertFormatException("W/\"tag\""); // tag value must not contain 'W/' + } - [Fact] - public void Ctor_ETagValidFormat_SuccessfullyCreated() - { - var etag = new EntityTagHeaderValue("\"tag\""); - Assert.Equal("\"tag\"", etag.Tag); - Assert.False(etag.IsWeak, "IsWeak"); - } + [Fact] + public void Ctor_ETagValidFormat_SuccessfullyCreated() + { + var etag = new EntityTagHeaderValue("\"tag\""); + Assert.Equal("\"tag\"", etag.Tag); + Assert.False(etag.IsWeak, "IsWeak"); + } - [Fact] - public void Ctor_ETagValidFormatAndIsWeak_SuccessfullyCreated() - { - var etag = new EntityTagHeaderValue("\"e tag\"", true); - Assert.Equal("\"e tag\"", etag.Tag); - Assert.True(etag.IsWeak, "IsWeak"); - } + [Fact] + public void Ctor_ETagValidFormatAndIsWeak_SuccessfullyCreated() + { + var etag = new EntityTagHeaderValue("\"e tag\"", true); + Assert.Equal("\"e tag\"", etag.Tag); + Assert.True(etag.IsWeak, "IsWeak"); + } - [Fact] - public void ToString_UseDifferentETags_AllSerializedCorrectly() - { - var etag = new EntityTagHeaderValue("\"e tag\""); - Assert.Equal("\"e tag\"", etag.ToString()); + [Fact] + public void ToString_UseDifferentETags_AllSerializedCorrectly() + { + var etag = new EntityTagHeaderValue("\"e tag\""); + Assert.Equal("\"e tag\"", etag.ToString()); - etag = new EntityTagHeaderValue("\"e tag\"", true); - Assert.Equal("W/\"e tag\"", etag.ToString()); + etag = new EntityTagHeaderValue("\"e tag\"", true); + Assert.Equal("W/\"e tag\"", etag.ToString()); - etag = new EntityTagHeaderValue("\"\"", false); - Assert.Equal("\"\"", etag.ToString()); - } + etag = new EntityTagHeaderValue("\"\"", false); + Assert.Equal("\"\"", etag.ToString()); + } - [Fact] - public void GetHashCode_UseSameAndDifferentETags_SameOrDifferentHashCodes() - { - var etag1 = new EntityTagHeaderValue("\"tag\""); - var etag2 = new EntityTagHeaderValue("\"TAG\""); - var etag3 = new EntityTagHeaderValue("\"tag\"", true); - var etag4 = new EntityTagHeaderValue("\"tag1\""); - var etag5 = new EntityTagHeaderValue("\"tag\""); - var etag6 = EntityTagHeaderValue.Any; - - Assert.NotEqual(etag1.GetHashCode(), etag2.GetHashCode()); - Assert.NotEqual(etag1.GetHashCode(), etag3.GetHashCode()); - Assert.NotEqual(etag1.GetHashCode(), etag4.GetHashCode()); - Assert.NotEqual(etag1.GetHashCode(), etag6.GetHashCode()); - Assert.Equal(etag1.GetHashCode(), etag5.GetHashCode()); - } + [Fact] + public void GetHashCode_UseSameAndDifferentETags_SameOrDifferentHashCodes() + { + var etag1 = new EntityTagHeaderValue("\"tag\""); + var etag2 = new EntityTagHeaderValue("\"TAG\""); + var etag3 = new EntityTagHeaderValue("\"tag\"", true); + var etag4 = new EntityTagHeaderValue("\"tag1\""); + var etag5 = new EntityTagHeaderValue("\"tag\""); + var etag6 = EntityTagHeaderValue.Any; + + Assert.NotEqual(etag1.GetHashCode(), etag2.GetHashCode()); + Assert.NotEqual(etag1.GetHashCode(), etag3.GetHashCode()); + Assert.NotEqual(etag1.GetHashCode(), etag4.GetHashCode()); + Assert.NotEqual(etag1.GetHashCode(), etag6.GetHashCode()); + Assert.Equal(etag1.GetHashCode(), etag5.GetHashCode()); + } - [Fact] - public void Equals_UseSameAndDifferentETags_EqualOrNotEqualNoExceptions() - { - var etag1 = new EntityTagHeaderValue("\"tag\""); - var etag2 = new EntityTagHeaderValue("\"TAG\""); - var etag3 = new EntityTagHeaderValue("\"tag\"", true); - var etag4 = new EntityTagHeaderValue("\"tag1\""); - var etag5 = new EntityTagHeaderValue("\"tag\""); - var etag6 = EntityTagHeaderValue.Any; - - Assert.False(etag1.Equals(etag2), "Different casing."); - Assert.False(etag2.Equals(etag1), "Different casing."); - Assert.False(etag1.Equals(null), "tag vs. ."); - Assert.False(etag1!.Equals(etag3), "strong vs. weak."); - Assert.False(etag3.Equals(etag1), "weak vs. strong."); - Assert.False(etag1.Equals(etag4), "tag vs. tag1."); - Assert.False(etag1.Equals(etag6), "tag vs. *."); - Assert.True(etag1.Equals(etag5), "tag vs. tag.."); - } + [Fact] + public void Equals_UseSameAndDifferentETags_EqualOrNotEqualNoExceptions() + { + var etag1 = new EntityTagHeaderValue("\"tag\""); + var etag2 = new EntityTagHeaderValue("\"TAG\""); + var etag3 = new EntityTagHeaderValue("\"tag\"", true); + var etag4 = new EntityTagHeaderValue("\"tag1\""); + var etag5 = new EntityTagHeaderValue("\"tag\""); + var etag6 = EntityTagHeaderValue.Any; + + Assert.False(etag1.Equals(etag2), "Different casing."); + Assert.False(etag2.Equals(etag1), "Different casing."); + Assert.False(etag1.Equals(null), "tag vs. ."); + Assert.False(etag1!.Equals(etag3), "strong vs. weak."); + Assert.False(etag3.Equals(etag1), "weak vs. strong."); + Assert.False(etag1.Equals(etag4), "tag vs. tag1."); + Assert.False(etag1.Equals(etag6), "tag vs. *."); + Assert.True(etag1.Equals(etag5), "tag vs. tag.."); + } - [Fact] - public void Compare_WithNull_ReturnsFalse() - { - Assert.False(EntityTagHeaderValue.Any.Compare(null, useStrongComparison: true)); - Assert.False(EntityTagHeaderValue.Any.Compare(null, useStrongComparison: false)); - } + [Fact] + public void Compare_WithNull_ReturnsFalse() + { + Assert.False(EntityTagHeaderValue.Any.Compare(null, useStrongComparison: true)); + Assert.False(EntityTagHeaderValue.Any.Compare(null, useStrongComparison: false)); + } - public static TheoryData NotEquivalentUnderStrongComparison + public static TheoryData NotEquivalentUnderStrongComparison + { + get { - get - { - return new TheoryData + return new TheoryData { { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"TAG\"") }, { new EntityTagHeaderValue("\"tag\"", true), new EntityTagHeaderValue("\"tag\"", true) }, @@ -118,162 +118,162 @@ namespace Microsoft.Net.Http.Headers { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag1\"") }, { new EntityTagHeaderValue("\"tag\""), EntityTagHeaderValue.Any }, }; - } } + } - [Theory] - [MemberData(nameof(NotEquivalentUnderStrongComparison))] - public void CompareUsingStrongComparison_NonEquivalentPairs_ReturnFalse(EntityTagHeaderValue left, EntityTagHeaderValue right) - { - Assert.False(left.Compare(right, useStrongComparison: true)); - Assert.False(right.Compare(left, useStrongComparison: true)); - } + [Theory] + [MemberData(nameof(NotEquivalentUnderStrongComparison))] + public void CompareUsingStrongComparison_NonEquivalentPairs_ReturnFalse(EntityTagHeaderValue left, EntityTagHeaderValue right) + { + Assert.False(left.Compare(right, useStrongComparison: true)); + Assert.False(right.Compare(left, useStrongComparison: true)); + } - public static TheoryData EquivalentUnderStrongComparison + public static TheoryData EquivalentUnderStrongComparison + { + get { - get - { - return new TheoryData + return new TheoryData { { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"") }, }; - } } + } - [Theory] - [MemberData(nameof(EquivalentUnderStrongComparison))] - public void CompareUsingStrongComparison_EquivalentPairs_ReturnTrue(EntityTagHeaderValue left, EntityTagHeaderValue right) - { - Assert.True(left.Compare(right, useStrongComparison: true)); - Assert.True(right.Compare(left, useStrongComparison: true)); - } + [Theory] + [MemberData(nameof(EquivalentUnderStrongComparison))] + public void CompareUsingStrongComparison_EquivalentPairs_ReturnTrue(EntityTagHeaderValue left, EntityTagHeaderValue right) + { + Assert.True(left.Compare(right, useStrongComparison: true)); + Assert.True(right.Compare(left, useStrongComparison: true)); + } - public static TheoryData NotEquivalentUnderWeakComparison + public static TheoryData NotEquivalentUnderWeakComparison + { + get { - get - { - return new TheoryData + return new TheoryData { { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"TAG\"") }, { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag1\"") }, { new EntityTagHeaderValue("\"tag\""), EntityTagHeaderValue.Any }, }; - } } + } - [Theory] - [MemberData(nameof(NotEquivalentUnderWeakComparison))] - public void CompareUsingWeakComparison_NonEquivalentPairs_ReturnFalse(EntityTagHeaderValue left, EntityTagHeaderValue right) - { - Assert.False(left.Compare(right, useStrongComparison: false)); - Assert.False(right.Compare(left, useStrongComparison: false)); - } + [Theory] + [MemberData(nameof(NotEquivalentUnderWeakComparison))] + public void CompareUsingWeakComparison_NonEquivalentPairs_ReturnFalse(EntityTagHeaderValue left, EntityTagHeaderValue right) + { + Assert.False(left.Compare(right, useStrongComparison: false)); + Assert.False(right.Compare(left, useStrongComparison: false)); + } - public static TheoryData EquivalentUnderWeakComparison + public static TheoryData EquivalentUnderWeakComparison + { + get { - get - { - return new TheoryData + return new TheoryData { { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"") }, { new EntityTagHeaderValue("\"tag\"", true), new EntityTagHeaderValue("\"tag\"", true) }, { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"", true) }, }; - } } + } - [Theory] - [MemberData(nameof(EquivalentUnderWeakComparison))] - public void CompareUsingWeakComparison_EquivalentPairs_ReturnTrue(EntityTagHeaderValue left, EntityTagHeaderValue right) - { - Assert.True(left.Compare(right, useStrongComparison: false)); - Assert.True(right.Compare(left, useStrongComparison: false)); - } + [Theory] + [MemberData(nameof(EquivalentUnderWeakComparison))] + public void CompareUsingWeakComparison_EquivalentPairs_ReturnTrue(EntityTagHeaderValue left, EntityTagHeaderValue right) + { + Assert.True(left.Compare(right, useStrongComparison: false)); + Assert.True(right.Compare(left, useStrongComparison: false)); + } - [Fact] - public void Parse_SetOfValidValueStrings_ParsedCorrectly() - { - CheckValidParse("\"tag\"", new EntityTagHeaderValue("\"tag\"")); - CheckValidParse(" \"tag\" ", new EntityTagHeaderValue("\"tag\"")); - CheckValidParse("\r\n \"tag\"\r\n ", new EntityTagHeaderValue("\"tag\"")); - CheckValidParse("\"tag\"", new EntityTagHeaderValue("\"tag\"")); - CheckValidParse("\"tag会\"", new EntityTagHeaderValue("\"tag会\"")); - CheckValidParse("W/\"tag\"", new EntityTagHeaderValue("\"tag\"", true)); - CheckValidParse("*", new EntityTagHeaderValue("*")); - } + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidParse("\"tag\"", new EntityTagHeaderValue("\"tag\"")); + CheckValidParse(" \"tag\" ", new EntityTagHeaderValue("\"tag\"")); + CheckValidParse("\r\n \"tag\"\r\n ", new EntityTagHeaderValue("\"tag\"")); + CheckValidParse("\"tag\"", new EntityTagHeaderValue("\"tag\"")); + CheckValidParse("\"tag会\"", new EntityTagHeaderValue("\"tag会\"")); + CheckValidParse("W/\"tag\"", new EntityTagHeaderValue("\"tag\"", true)); + CheckValidParse("*", new EntityTagHeaderValue("*")); + } - [Fact] - public void Parse_SetOfInvalidValueStrings_Throws() - { - CheckInvalidParse(null); - CheckInvalidParse(string.Empty); - CheckInvalidParse(" "); - CheckInvalidParse(" !"); - CheckInvalidParse("tag\" !"); - CheckInvalidParse("!\"tag\""); - CheckInvalidParse("\"tag\","); - CheckInvalidParse("W"); - CheckInvalidParse("W/"); - CheckInvalidParse("W/\""); - CheckInvalidParse("\"tag\" \"tag2\""); - CheckInvalidParse("/\"tag\""); - } + [Fact] + public void Parse_SetOfInvalidValueStrings_Throws() + { + CheckInvalidParse(null); + CheckInvalidParse(string.Empty); + CheckInvalidParse(" "); + CheckInvalidParse(" !"); + CheckInvalidParse("tag\" !"); + CheckInvalidParse("!\"tag\""); + CheckInvalidParse("\"tag\","); + CheckInvalidParse("W"); + CheckInvalidParse("W/"); + CheckInvalidParse("W/\""); + CheckInvalidParse("\"tag\" \"tag2\""); + CheckInvalidParse("/\"tag\""); + } - [Fact] - public void TryParse_SetOfValidValueStrings_ParsedCorrectly() - { - CheckValidTryParse("\"tag\"", new EntityTagHeaderValue("\"tag\"")); - CheckValidTryParse(" \"tag\" ", new EntityTagHeaderValue("\"tag\"")); - CheckValidTryParse("\r\n \"tag\"\r\n ", new EntityTagHeaderValue("\"tag\"")); - CheckValidTryParse("\"tag\"", new EntityTagHeaderValue("\"tag\"")); - CheckValidTryParse("\"tag会\"", new EntityTagHeaderValue("\"tag会\"")); - CheckValidTryParse("W/\"tag\"", new EntityTagHeaderValue("\"tag\"", true)); - CheckValidTryParse("*", new EntityTagHeaderValue("*")); - } + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidTryParse("\"tag\"", new EntityTagHeaderValue("\"tag\"")); + CheckValidTryParse(" \"tag\" ", new EntityTagHeaderValue("\"tag\"")); + CheckValidTryParse("\r\n \"tag\"\r\n ", new EntityTagHeaderValue("\"tag\"")); + CheckValidTryParse("\"tag\"", new EntityTagHeaderValue("\"tag\"")); + CheckValidTryParse("\"tag会\"", new EntityTagHeaderValue("\"tag会\"")); + CheckValidTryParse("W/\"tag\"", new EntityTagHeaderValue("\"tag\"", true)); + CheckValidTryParse("*", new EntityTagHeaderValue("*")); + } - [Fact] - public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() - { - CheckInvalidTryParse(null); - CheckInvalidTryParse(string.Empty); - CheckInvalidTryParse(" "); - CheckInvalidTryParse(" !"); - CheckInvalidTryParse("tag\" !"); - CheckInvalidTryParse("!\"tag\""); - CheckInvalidTryParse("\"tag\","); - CheckInvalidTryParse("\"tag\" \"tag2\""); - CheckInvalidTryParse("/\"tag\""); - } + [Fact] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() + { + CheckInvalidTryParse(null); + CheckInvalidTryParse(string.Empty); + CheckInvalidTryParse(" "); + CheckInvalidTryParse(" !"); + CheckInvalidTryParse("tag\" !"); + CheckInvalidTryParse("!\"tag\""); + CheckInvalidTryParse("\"tag\","); + CheckInvalidTryParse("\"tag\" \"tag2\""); + CheckInvalidTryParse("/\"tag\""); + } - [Fact] - public void ParseList_NullOrEmptyArray_ReturnsEmptyList() - { - var result = EntityTagHeaderValue.ParseList(null); - Assert.NotNull(result); - Assert.Equal(0, result.Count); + [Fact] + public void ParseList_NullOrEmptyArray_ReturnsEmptyList() + { + var result = EntityTagHeaderValue.ParseList(null); + Assert.NotNull(result); + Assert.Equal(0, result.Count); - result = EntityTagHeaderValue.ParseList(new string[0]); - Assert.NotNull(result); - Assert.Equal(0, result.Count); + result = EntityTagHeaderValue.ParseList(new string[0]); + Assert.NotNull(result); + Assert.Equal(0, result.Count); - result = EntityTagHeaderValue.ParseList(new string[] { "" }); - Assert.NotNull(result); - Assert.Equal(0, result.Count); - } + result = EntityTagHeaderValue.ParseList(new string[] { "" }); + Assert.NotNull(result); + Assert.Equal(0, result.Count); + } - [Fact] - public void TryParseList_NullOrEmptyArray_ReturnsFalse() - { - Assert.False(EntityTagHeaderValue.TryParseList(null, out var results)); - Assert.False(EntityTagHeaderValue.TryParseList(new string[0], out results)); - Assert.False(EntityTagHeaderValue.TryParseList(new string[] { "" }, out results)); - } + [Fact] + public void TryParseList_NullOrEmptyArray_ReturnsFalse() + { + Assert.False(EntityTagHeaderValue.TryParseList(null, out var results)); + Assert.False(EntityTagHeaderValue.TryParseList(new string[0], out results)); + Assert.False(EntityTagHeaderValue.TryParseList(new string[] { "" }, out results)); + } - [Fact] - public void ParseList_SetOfValidValueStrings_ParsedCorrectly() + [Fact] + public void ParseList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] { - var inputs = new[] - { "", "\"tag\"", "", @@ -284,10 +284,10 @@ namespace Microsoft.Net.Http.Headers "\"tag\", \"tag\"", "W/\"tag\"", }; - IList results = EntityTagHeaderValue.ParseList(inputs); + IList results = EntityTagHeaderValue.ParseList(inputs); - var expectedResults = new[] - { + var expectedResults = new[] + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\""), @@ -299,14 +299,14 @@ namespace Microsoft.Net.Http.Headers new EntityTagHeaderValue("\"tag\"", true), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void ParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + [Fact] + public void ParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] { - var inputs = new[] - { "", "\"tag\"", "", @@ -317,10 +317,10 @@ namespace Microsoft.Net.Http.Headers "\"tag\", \"tag\"", "W/\"tag\"", }; - IList results = EntityTagHeaderValue.ParseStrictList(inputs); + IList results = EntityTagHeaderValue.ParseStrictList(inputs); - var expectedResults = new[] - { + var expectedResults = new[] + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\""), @@ -332,14 +332,14 @@ namespace Microsoft.Net.Http.Headers new EntityTagHeaderValue("\"tag\"", true), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void TryParseList_SetOfValidValueStrings_ParsedCorrectly() + [Fact] + public void TryParseList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] { - var inputs = new[] - { "", "\"tag\"", "", @@ -350,9 +350,9 @@ namespace Microsoft.Net.Http.Headers "\"tag\", \"tag\"", "W/\"tag\"", }; - Assert.True(EntityTagHeaderValue.TryParseList(inputs, out var results)); - var expectedResults = new[] - { + Assert.True(EntityTagHeaderValue.TryParseList(inputs, out var results)); + var expectedResults = new[] + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\""), @@ -364,14 +364,14 @@ namespace Microsoft.Net.Http.Headers new EntityTagHeaderValue("\"tag\"", true), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void TryParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + [Fact] + public void TryParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] { - var inputs = new[] - { "", "\"tag\"", "", @@ -382,9 +382,9 @@ namespace Microsoft.Net.Http.Headers "\"tag\", \"tag\"", "W/\"tag\"", }; - Assert.True(EntityTagHeaderValue.TryParseStrictList(inputs, out var results)); - var expectedResults = new[] - { + Assert.True(EntityTagHeaderValue.TryParseStrictList(inputs, out var results)); + var expectedResults = new[] + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\""), @@ -396,14 +396,14 @@ namespace Microsoft.Net.Http.Headers new EntityTagHeaderValue("\"tag\"", true), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void ParseList_WithSomeInvalidValues_ExcludesInvalidValues() + [Fact] + public void ParseList_WithSomeInvalidValues_ExcludesInvalidValues() + { + var inputs = new[] { - var inputs = new[] - { "", "\"tag\", tag, \"tag\"", "tag, \"tag\"", @@ -414,9 +414,9 @@ namespace Microsoft.Net.Http.Headers "\"tag\", \"tag\"", "W/\"tag\"", }; - var results = EntityTagHeaderValue.ParseList(inputs); - var expectedResults = new[] - { + var results = EntityTagHeaderValue.ParseList(inputs); + var expectedResults = new[] + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\""), @@ -426,14 +426,14 @@ namespace Microsoft.Net.Http.Headers new EntityTagHeaderValue("\"tag\"", true), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void ParseStrictList_WithSomeInvalidValues_Throws() + [Fact] + public void ParseStrictList_WithSomeInvalidValues_Throws() + { + var inputs = new[] { - var inputs = new[] - { "", "\"tag\", tag, \"tag\"", "tag, \"tag\"", @@ -444,14 +444,14 @@ namespace Microsoft.Net.Http.Headers "\"tag\", \"tag\"", "W/\"tag\"", }; - Assert.Throws(() => EntityTagHeaderValue.ParseStrictList(inputs)); - } + Assert.Throws(() => EntityTagHeaderValue.ParseStrictList(inputs)); + } - [Fact] - public void TryParseList_WithSomeInvalidValues_ExcludesInvalidValues() + [Fact] + public void TryParseList_WithSomeInvalidValues_ExcludesInvalidValues() + { + var inputs = new[] { - var inputs = new[] - { "", "\"tag\", tag, \"tag\"", "tag, \"tag\"", @@ -462,9 +462,9 @@ namespace Microsoft.Net.Http.Headers "\"tag\", \"tag\"", "W/\"tag\"", }; - Assert.True(EntityTagHeaderValue.TryParseList(inputs, out var results)); - var expectedResults = new[] - { + Assert.True(EntityTagHeaderValue.TryParseList(inputs, out var results)); + var expectedResults = new[] + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\""), @@ -474,14 +474,14 @@ namespace Microsoft.Net.Http.Headers new EntityTagHeaderValue("\"tag\"", true), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void TryParseStrictList_WithSomeInvalidValues_ReturnsFalse() + [Fact] + public void TryParseStrictList_WithSomeInvalidValues_ReturnsFalse() + { + var inputs = new[] { - var inputs = new[] - { "", "\"tag\", tag, \"tag\"", "tag, \"tag\"", @@ -492,35 +492,34 @@ namespace Microsoft.Net.Http.Headers "\"tag\", \"tag\"", "W/\"tag\"", }; - Assert.False(EntityTagHeaderValue.TryParseStrictList(inputs, out var results)); - } + Assert.False(EntityTagHeaderValue.TryParseStrictList(inputs, out var results)); + } - private void CheckValidParse(string? input, EntityTagHeaderValue expectedResult) - { - var result = EntityTagHeaderValue.Parse(input); - Assert.Equal(expectedResult, result); - } + private void CheckValidParse(string? input, EntityTagHeaderValue expectedResult) + { + var result = EntityTagHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); + } - private void CheckInvalidParse(string? input) - { - Assert.Throws(() => EntityTagHeaderValue.Parse(input)); - } + private void CheckInvalidParse(string? input) + { + Assert.Throws(() => EntityTagHeaderValue.Parse(input)); + } - private void CheckValidTryParse(string? input, EntityTagHeaderValue expectedResult) - { - Assert.True(EntityTagHeaderValue.TryParse(input, out var result)); - Assert.Equal(expectedResult, result); - } + private void CheckValidTryParse(string? input, EntityTagHeaderValue expectedResult) + { + Assert.True(EntityTagHeaderValue.TryParse(input, out var result)); + Assert.Equal(expectedResult, result); + } - private void CheckInvalidTryParse(string? input) - { - Assert.False(EntityTagHeaderValue.TryParse(input, out var result)); - Assert.Null(result); - } + private void CheckInvalidTryParse(string? input) + { + Assert.False(EntityTagHeaderValue.TryParse(input, out var result)); + Assert.Null(result); + } - private static void AssertFormatException(string tag) - { - Assert.Throws(() => new EntityTagHeaderValue(tag)); - } + private static void AssertFormatException(string tag) + { + Assert.Throws(() => new EntityTagHeaderValue(tag)); } } diff --git a/src/Http/Headers/test/HeaderUtilitiesTest.cs b/src/Http/Headers/test/HeaderUtilitiesTest.cs index a467bcaf77..dd9acd6b67 100644 --- a/src/Http/Headers/test/HeaderUtilitiesTest.cs +++ b/src/Http/Headers/test/HeaderUtilitiesTest.cs @@ -6,280 +6,279 @@ using System.Globalization; using Microsoft.Extensions.Primitives; using Xunit; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +public class HeaderUtilitiesTest { - public class HeaderUtilitiesTest + private const string Rfc1123Format = "r"; + + [Theory] + [MemberData(nameof(TestValues))] + public void ReturnsSameResultAsRfc1123String(DateTimeOffset dateTime, bool quoted) { - private const string Rfc1123Format = "r"; + var formatted = dateTime.ToString(Rfc1123Format); + var expected = quoted ? $"\"{formatted}\"" : formatted; + var actual = HeaderUtilities.FormatDate(dateTime, quoted); - [Theory] - [MemberData(nameof(TestValues))] - public void ReturnsSameResultAsRfc1123String(DateTimeOffset dateTime, bool quoted) + Assert.Equal(expected, actual); + } + + public static TheoryData TestValues + { + get { - var formatted = dateTime.ToString(Rfc1123Format); - var expected = quoted ? $"\"{formatted}\"" : formatted; - var actual = HeaderUtilities.FormatDate(dateTime, quoted); + var data = new TheoryData(); - Assert.Equal(expected, actual); - } + var date = new DateTimeOffset(new DateTime(2018, 1, 1, 1, 1, 1)); - public static TheoryData TestValues - { - get + foreach (var quoted in new[] { true, false }) { - var data = new TheoryData(); - - var date = new DateTimeOffset(new DateTime(2018, 1, 1, 1, 1, 1)); + data.Add(date, quoted); - foreach (var quoted in new[] { true, false }) + for (var i = 1; i < 60; i++) { - data.Add(date, quoted); - - for (var i = 1; i < 60; i++) - { - data.Add(date.AddSeconds(i), quoted); - data.Add(date.AddMinutes(i), quoted); - } - - for (var i = 1; i < DateTime.DaysInMonth(date.Year, date.Month); i++) - { - data.Add(date.AddDays(i), quoted); - } + data.Add(date.AddSeconds(i), quoted); + data.Add(date.AddMinutes(i), quoted); + } - for (var i = 1; i < 11; i++) - { - data.Add(date.AddMonths(i), quoted); - } + for (var i = 1; i < DateTime.DaysInMonth(date.Year, date.Month); i++) + { + data.Add(date.AddDays(i), quoted); + } - for (var i = 1; i < 5; i++) - { - data.Add(date.AddYears(i), quoted); - } + for (var i = 1; i < 11; i++) + { + data.Add(date.AddMonths(i), quoted); } - return data; + for (var i = 1; i < 5; i++) + { + data.Add(date.AddYears(i), quoted); + } } - } - [Theory] - [InlineData("h=1", "h", 1)] - [InlineData("directive1=3, directive2=10", "directive1", 3)] - [InlineData("directive1 =45, directive2=80", "directive1", 45)] - [InlineData("directive1= 89 , directive2=22", "directive1", 89)] - [InlineData("directive1= 89 , directive2= 42", "directive2", 42)] - [InlineData("directive1= 89 , directive= 42", "directive", 42)] - [InlineData("directive1,,,,,directive2 = 42 ", "directive2", 42)] - [InlineData("directive1=;,directive2 = 42 ", "directive2", 42)] - [InlineData("directive1;;,;;,directive2 = 42 ", "directive2", 42)] - [InlineData("directive1=value;q=0.6,directive2 = 42 ", "directive2", 42)] - public void TryParseSeconds_Succeeds(string headerValues, string targetValue, int expectedValue) - { - TimeSpan? value; - Assert.True(HeaderUtilities.TryParseSeconds(new StringValues(headerValues), targetValue, out value)); - Assert.Equal(TimeSpan.FromSeconds(expectedValue), value); + return data; } + } - [Theory] - [InlineData("", "")] - [InlineData(null, null)] - [InlineData("h=", "h")] - [InlineData("directive1=, directive2=10", "directive1")] - [InlineData("directive1 , directive2=80", "directive1")] - [InlineData("h=10", "directive")] - [InlineData("directive1", "directive")] - [InlineData("directive1,,,,,,,", "directive")] - [InlineData("h=directive", "directive")] - [InlineData("directive1, directive2=80", "directive")] - [InlineData("directive1=;, directive2=10", "directive1")] - [InlineData("directive1;directive2=10", "directive2")] - public void TryParseSeconds_Fails(string headerValues, string targetValue) - { - TimeSpan? value; - Assert.False(HeaderUtilities.TryParseSeconds(new StringValues(headerValues), targetValue, out value)); - } + [Theory] + [InlineData("h=1", "h", 1)] + [InlineData("directive1=3, directive2=10", "directive1", 3)] + [InlineData("directive1 =45, directive2=80", "directive1", 45)] + [InlineData("directive1= 89 , directive2=22", "directive1", 89)] + [InlineData("directive1= 89 , directive2= 42", "directive2", 42)] + [InlineData("directive1= 89 , directive= 42", "directive", 42)] + [InlineData("directive1,,,,,directive2 = 42 ", "directive2", 42)] + [InlineData("directive1=;,directive2 = 42 ", "directive2", 42)] + [InlineData("directive1;;,;;,directive2 = 42 ", "directive2", 42)] + [InlineData("directive1=value;q=0.6,directive2 = 42 ", "directive2", 42)] + public void TryParseSeconds_Succeeds(string headerValues, string targetValue, int expectedValue) + { + TimeSpan? value; + Assert.True(HeaderUtilities.TryParseSeconds(new StringValues(headerValues), targetValue, out value)); + Assert.Equal(TimeSpan.FromSeconds(expectedValue), value); + } - [Theory] - [InlineData(0)] - [InlineData(1)] - [InlineData(1234567890)] - [InlineData(long.MaxValue)] - public void FormatNonNegativeInt64_MatchesToString(long value) - { - Assert.Equal(value.ToString(CultureInfo.InvariantCulture), HeaderUtilities.FormatNonNegativeInt64(value)); - } + [Theory] + [InlineData("", "")] + [InlineData(null, null)] + [InlineData("h=", "h")] + [InlineData("directive1=, directive2=10", "directive1")] + [InlineData("directive1 , directive2=80", "directive1")] + [InlineData("h=10", "directive")] + [InlineData("directive1", "directive")] + [InlineData("directive1,,,,,,,", "directive")] + [InlineData("h=directive", "directive")] + [InlineData("directive1, directive2=80", "directive")] + [InlineData("directive1=;, directive2=10", "directive1")] + [InlineData("directive1;directive2=10", "directive2")] + public void TryParseSeconds_Fails(string headerValues, string targetValue) + { + TimeSpan? value; + Assert.False(HeaderUtilities.TryParseSeconds(new StringValues(headerValues), targetValue, out value)); + } - [Theory] - [InlineData(-1)] - [InlineData(-1234567890)] - [InlineData(long.MinValue)] - public void FormatNonNegativeInt64_Throws_ForNegativeValues(long value) - { - Assert.Throws(() => HeaderUtilities.FormatNonNegativeInt64(value)); - } + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(1234567890)] + [InlineData(long.MaxValue)] + public void FormatNonNegativeInt64_MatchesToString(long value) + { + Assert.Equal(value.ToString(CultureInfo.InvariantCulture), HeaderUtilities.FormatNonNegativeInt64(value)); + } - [Theory] - [InlineData("h", "h", true)] - [InlineData("h=", "h", true)] - [InlineData("h=1", "h", true)] - [InlineData("H", "h", true)] - [InlineData("H=", "h", true)] - [InlineData("H=1", "h", true)] - [InlineData("h", "H", true)] - [InlineData("h=", "H", true)] - [InlineData("h=1", "H", true)] - [InlineData("directive1, directive=10", "directive1", true)] - [InlineData("directive1=, directive=10", "directive1", true)] - [InlineData("directive1=3, directive=10", "directive1", true)] - [InlineData("directive1 , directive=80", "directive1", true)] - [InlineData(" directive1, directive=80", "directive1", true)] - [InlineData("directive1 =45, directive=80", "directive1", true)] - [InlineData("directive1= 89 , directive=22", "directive1", true)] - [InlineData("directive1, directive", "directive", true)] - [InlineData("directive1, directive=", "directive", true)] - [InlineData("directive1, directive=10", "directive", true)] - [InlineData("directive1=3, directive", "directive", true)] - [InlineData("directive1=3, directive=", "directive", true)] - [InlineData("directive1=3, directive=10", "directive", true)] - [InlineData("directive1= 89 , directive= 42", "directive", true)] - [InlineData("directive1= 89 , directive = 42", "directive", true)] - [InlineData("directive1,,,,,directive2 = 42 ", "directive2", true)] - [InlineData("directive1;;,;;,directive2 = 42 ", "directive2", true)] - [InlineData("directive1=;,directive2 = 42 ", "directive2", true)] - [InlineData("directive1=value;q=0.6,directive2 = 42 ", "directive2", true)] - [InlineData(null, null, false)] - [InlineData(null, "", false)] - [InlineData("", null, false)] - [InlineData("", "", false)] - [InlineData("h=10", "directive", false)] - [InlineData("directive1", "directive", false)] - [InlineData("directive1,,,,,,,", "directive", false)] - [InlineData("h=directive", "directive", false)] - [InlineData("directive1, directive2=80", "directive", false)] - [InlineData("directive1;, directive2=80", "directive", false)] - [InlineData("directive1=value;q=0.6;directive2 = 42 ", "directive2", false)] - public void ContainsCacheDirective_MatchesExactValue(string headerValues, string targetValue, bool contains) - { - Assert.Equal(contains, HeaderUtilities.ContainsCacheDirective(new StringValues(headerValues), targetValue)); - } + [Theory] + [InlineData(-1)] + [InlineData(-1234567890)] + [InlineData(long.MinValue)] + public void FormatNonNegativeInt64_Throws_ForNegativeValues(long value) + { + Assert.Throws(() => HeaderUtilities.FormatNonNegativeInt64(value)); + } - [Theory] - [InlineData("")] - [InlineData(null)] - [InlineData("-1")] - [InlineData("a")] - [InlineData("1.1")] - [InlineData("9223372036854775808")] // long.MaxValue + 1 - public void TryParseNonNegativeInt64_Fails(string valueString) - { - long value = 1; - Assert.False(HeaderUtilities.TryParseNonNegativeInt64(valueString, out value)); - Assert.Equal(0, value); - } + [Theory] + [InlineData("h", "h", true)] + [InlineData("h=", "h", true)] + [InlineData("h=1", "h", true)] + [InlineData("H", "h", true)] + [InlineData("H=", "h", true)] + [InlineData("H=1", "h", true)] + [InlineData("h", "H", true)] + [InlineData("h=", "H", true)] + [InlineData("h=1", "H", true)] + [InlineData("directive1, directive=10", "directive1", true)] + [InlineData("directive1=, directive=10", "directive1", true)] + [InlineData("directive1=3, directive=10", "directive1", true)] + [InlineData("directive1 , directive=80", "directive1", true)] + [InlineData(" directive1, directive=80", "directive1", true)] + [InlineData("directive1 =45, directive=80", "directive1", true)] + [InlineData("directive1= 89 , directive=22", "directive1", true)] + [InlineData("directive1, directive", "directive", true)] + [InlineData("directive1, directive=", "directive", true)] + [InlineData("directive1, directive=10", "directive", true)] + [InlineData("directive1=3, directive", "directive", true)] + [InlineData("directive1=3, directive=", "directive", true)] + [InlineData("directive1=3, directive=10", "directive", true)] + [InlineData("directive1= 89 , directive= 42", "directive", true)] + [InlineData("directive1= 89 , directive = 42", "directive", true)] + [InlineData("directive1,,,,,directive2 = 42 ", "directive2", true)] + [InlineData("directive1;;,;;,directive2 = 42 ", "directive2", true)] + [InlineData("directive1=;,directive2 = 42 ", "directive2", true)] + [InlineData("directive1=value;q=0.6,directive2 = 42 ", "directive2", true)] + [InlineData(null, null, false)] + [InlineData(null, "", false)] + [InlineData("", null, false)] + [InlineData("", "", false)] + [InlineData("h=10", "directive", false)] + [InlineData("directive1", "directive", false)] + [InlineData("directive1,,,,,,,", "directive", false)] + [InlineData("h=directive", "directive", false)] + [InlineData("directive1, directive2=80", "directive", false)] + [InlineData("directive1;, directive2=80", "directive", false)] + [InlineData("directive1=value;q=0.6;directive2 = 42 ", "directive2", false)] + public void ContainsCacheDirective_MatchesExactValue(string headerValues, string targetValue, bool contains) + { + Assert.Equal(contains, HeaderUtilities.ContainsCacheDirective(new StringValues(headerValues), targetValue)); + } - [Theory] - [InlineData("0", 0)] - [InlineData("9223372036854775807", 9223372036854775807)] // long.MaxValue - public void TryParseNonNegativeInt64_Succeeds(string valueString, long expected) - { - long value = 1; - Assert.True(HeaderUtilities.TryParseNonNegativeInt64(valueString, out value)); - Assert.Equal(expected, value); - } + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData("-1")] + [InlineData("a")] + [InlineData("1.1")] + [InlineData("9223372036854775808")] // long.MaxValue + 1 + public void TryParseNonNegativeInt64_Fails(string valueString) + { + long value = 1; + Assert.False(HeaderUtilities.TryParseNonNegativeInt64(valueString, out value)); + Assert.Equal(0, value); + } - [Theory] - [InlineData("")] - [InlineData(null)] - [InlineData("-1")] - [InlineData("a")] - [InlineData("1.1")] - [InlineData("1,000")] - [InlineData("2147483648")] // int.MaxValue + 1 - public void TryParseNonNegativeInt32_Fails(string valueString) - { - int value = 1; - Assert.False(HeaderUtilities.TryParseNonNegativeInt32(valueString, out value)); - Assert.Equal(0, value); - } + [Theory] + [InlineData("0", 0)] + [InlineData("9223372036854775807", 9223372036854775807)] // long.MaxValue + public void TryParseNonNegativeInt64_Succeeds(string valueString, long expected) + { + long value = 1; + Assert.True(HeaderUtilities.TryParseNonNegativeInt64(valueString, out value)); + Assert.Equal(expected, value); + } - [Theory] - [InlineData("0", 0)] - [InlineData("2147483647", 2147483647)] // int.MaxValue - public void TryParseNonNegativeInt32_Succeeds(string valueString, long expected) - { - int value = 1; - Assert.True(HeaderUtilities.TryParseNonNegativeInt32(valueString, out value)); - Assert.Equal(expected, value); - } + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData("-1")] + [InlineData("a")] + [InlineData("1.1")] + [InlineData("1,000")] + [InlineData("2147483648")] // int.MaxValue + 1 + public void TryParseNonNegativeInt32_Fails(string valueString) + { + int value = 1; + Assert.False(HeaderUtilities.TryParseNonNegativeInt32(valueString, out value)); + Assert.Equal(0, value); + } - [Theory] - [InlineData("\"hello\"", "hello")] - [InlineData("\"hello", "\"hello")] - [InlineData("hello\"", "hello\"")] - [InlineData("\"\"hello\"\"", "\"hello\"")] - public void RemoveQuotes_BehaviorCheck(string input, string expected) - { - var actual = HeaderUtilities.RemoveQuotes(input); + [Theory] + [InlineData("0", 0)] + [InlineData("2147483647", 2147483647)] // int.MaxValue + public void TryParseNonNegativeInt32_Succeeds(string valueString, long expected) + { + int value = 1; + Assert.True(HeaderUtilities.TryParseNonNegativeInt32(valueString, out value)); + Assert.Equal(expected, value); + } - Assert.Equal(expected, actual); - } - [Theory] - [InlineData("\"hello\"", true)] - [InlineData("\"hello", false)] - [InlineData("hello\"", false)] - [InlineData("\"\"hello\"\"", true)] - public void IsQuoted_BehaviorCheck(string input, bool expected) - { - var actual = HeaderUtilities.IsQuoted(input); + [Theory] + [InlineData("\"hello\"", "hello")] + [InlineData("\"hello", "\"hello")] + [InlineData("hello\"", "hello\"")] + [InlineData("\"\"hello\"\"", "\"hello\"")] + public void RemoveQuotes_BehaviorCheck(string input, string expected) + { + var actual = HeaderUtilities.RemoveQuotes(input); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } + [Theory] + [InlineData("\"hello\"", true)] + [InlineData("\"hello", false)] + [InlineData("hello\"", false)] + [InlineData("\"\"hello\"\"", true)] + public void IsQuoted_BehaviorCheck(string input, bool expected) + { + var actual = HeaderUtilities.IsQuoted(input); - [Theory] - [InlineData("value", "value")] - [InlineData("\"value\"", "value")] - [InlineData("\"hello\\\\\"", "hello\\")] - [InlineData("\"hello\\\"\"", "hello\"")] - [InlineData("\"hello\\\"foo\\\\bar\\\\baz\\\\\"", "hello\"foo\\bar\\baz\\")] - [InlineData("\"quoted value\"", "quoted value")] - [InlineData("\"quoted\\\"valuewithquote\"", "quoted\"valuewithquote")] - [InlineData("\"hello\\\"", "hello\\")] - public void UnescapeAsQuotedString_BehaviorCheck(string input, string expected) - { - var actual = HeaderUtilities.UnescapeAsQuotedString(input); + Assert.Equal(expected, actual); + } - Assert.Equal(expected, actual); - } + [Theory] + [InlineData("value", "value")] + [InlineData("\"value\"", "value")] + [InlineData("\"hello\\\\\"", "hello\\")] + [InlineData("\"hello\\\"\"", "hello\"")] + [InlineData("\"hello\\\"foo\\\\bar\\\\baz\\\\\"", "hello\"foo\\bar\\baz\\")] + [InlineData("\"quoted value\"", "quoted value")] + [InlineData("\"quoted\\\"valuewithquote\"", "quoted\"valuewithquote")] + [InlineData("\"hello\\\"", "hello\\")] + public void UnescapeAsQuotedString_BehaviorCheck(string input, string expected) + { + var actual = HeaderUtilities.UnescapeAsQuotedString(input); - [Theory] - [InlineData("value", "\"value\"")] - [InlineData("23", "\"23\"")] - [InlineData(";;;", "\";;;\"")] - [InlineData("\"value\"", "\"\\\"value\\\"\"")] - [InlineData("unquoted \"value", "\"unquoted \\\"value\"")] - [InlineData("value\\morevalues\\evenmorevalues", "\"value\\\\morevalues\\\\evenmorevalues\"")] - // We have to assume that the input needs to be quoted here - [InlineData("\"\"double quoted string\"\"", "\"\\\"\\\"double quoted string\\\"\\\"\"")] - [InlineData("\t", "\"\t\"")] - public void SetAndEscapeValue_BehaviorCheck(string input, string expected) - { - var actual = HeaderUtilities.EscapeAsQuotedString(input); + Assert.Equal(expected, actual); + } - Assert.Equal(expected, actual); - } + [Theory] + [InlineData("value", "\"value\"")] + [InlineData("23", "\"23\"")] + [InlineData(";;;", "\";;;\"")] + [InlineData("\"value\"", "\"\\\"value\\\"\"")] + [InlineData("unquoted \"value", "\"unquoted \\\"value\"")] + [InlineData("value\\morevalues\\evenmorevalues", "\"value\\\\morevalues\\\\evenmorevalues\"")] + // We have to assume that the input needs to be quoted here + [InlineData("\"\"double quoted string\"\"", "\"\\\"\\\"double quoted string\\\"\\\"\"")] + [InlineData("\t", "\"\t\"")] + public void SetAndEscapeValue_BehaviorCheck(string input, string expected) + { + var actual = HeaderUtilities.EscapeAsQuotedString(input); - [Theory] - [InlineData("\n")] - [InlineData("\b")] - [InlineData("\r")] - public void SetAndEscapeValue_ControlCharactersThrowFormatException(string input) - { - Assert.Throws(() => { var actual = HeaderUtilities.EscapeAsQuotedString(input); }); - } + Assert.Equal(expected, actual); + } - [Fact] - public void SetAndEscapeValue_ThrowsFormatExceptionOnDelCharacter() - { - Assert.Throws(() => { var actual = HeaderUtilities.EscapeAsQuotedString($"{(char)0x7F}"); }); - } + [Theory] + [InlineData("\n")] + [InlineData("\b")] + [InlineData("\r")] + public void SetAndEscapeValue_ControlCharactersThrowFormatException(string input) + { + Assert.Throws(() => { var actual = HeaderUtilities.EscapeAsQuotedString(input); }); + } + + [Fact] + public void SetAndEscapeValue_ThrowsFormatExceptionOnDelCharacter() + { + Assert.Throws(() => { var actual = HeaderUtilities.EscapeAsQuotedString($"{(char)0x7F}"); }); } } diff --git a/src/Http/Headers/test/MediaTypeHeaderValueComparerTests.cs b/src/Http/Headers/test/MediaTypeHeaderValueComparerTests.cs index e0e4fc3726..34899be64a 100644 --- a/src/Http/Headers/test/MediaTypeHeaderValueComparerTests.cs +++ b/src/Http/Headers/test/MediaTypeHeaderValueComparerTests.cs @@ -5,15 +5,15 @@ using System.Collections.Generic; using System.Linq; using Xunit; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +public class MediaTypeHeaderValueComparerTests { - public class MediaTypeHeaderValueComparerTests + public static IEnumerable SortValues { - public static IEnumerable SortValues + get { - get - { - yield return new object[] { + yield return new object[] { new string[] { "application/*", @@ -57,19 +57,18 @@ namespace Microsoft.Net.Http.Headers "text/plain;q=0", } }; - } } + } - [Theory] - [MemberData(nameof(SortValues))] - public void SortMediaTypeHeaderValuesByQFactor_SortsCorrectly(IEnumerable unsorted, IEnumerable expectedSorted) - { - var unsortedValues = MediaTypeHeaderValue.ParseList(unsorted.ToList()); - var expectedSortedValues = MediaTypeHeaderValue.ParseList(expectedSorted.ToList()); + [Theory] + [MemberData(nameof(SortValues))] + public void SortMediaTypeHeaderValuesByQFactor_SortsCorrectly(IEnumerable unsorted, IEnumerable expectedSorted) + { + var unsortedValues = MediaTypeHeaderValue.ParseList(unsorted.ToList()); + var expectedSortedValues = MediaTypeHeaderValue.ParseList(expectedSorted.ToList()); - var actualSorted = unsortedValues.OrderByDescending(m => m, MediaTypeHeaderValueComparer.QualityComparer).ToList(); + var actualSorted = unsortedValues.OrderByDescending(m => m, MediaTypeHeaderValueComparer.QualityComparer).ToList(); - Assert.Equal(expectedSortedValues, actualSorted); - } + Assert.Equal(expectedSortedValues, actualSorted); } } diff --git a/src/Http/Headers/test/MediaTypeHeaderValueTest.cs b/src/Http/Headers/test/MediaTypeHeaderValueTest.cs index faf69d24aa..69d4945b2a 100644 --- a/src/Http/Headers/test/MediaTypeHeaderValueTest.cs +++ b/src/Http/Headers/test/MediaTypeHeaderValueTest.cs @@ -8,548 +8,548 @@ using Microsoft.Extensions.Primitives; using NuGet.Frameworks; using Xunit; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +public class MediaTypeHeaderValueTest { - public class MediaTypeHeaderValueTest + [Fact] + public void Ctor_MediaTypeNull_Throw() { - [Fact] - public void Ctor_MediaTypeNull_Throw() - { - Assert.Throws(() => new MediaTypeHeaderValue(null)); - // null and empty should be treated the same. So we also throw for empty strings. - Assert.Throws(() => new MediaTypeHeaderValue(string.Empty)); - } + Assert.Throws(() => new MediaTypeHeaderValue(null)); + // null and empty should be treated the same. So we also throw for empty strings. + Assert.Throws(() => new MediaTypeHeaderValue(string.Empty)); + } - [Fact] - public void Ctor_MediaTypeInvalidFormat_ThrowFormatException() - { - // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. - AssertFormatException(" text/plain "); - AssertFormatException("text / plain"); - AssertFormatException("text/ plain"); - AssertFormatException("text /plain"); - AssertFormatException("text/plain "); - AssertFormatException(" text/plain"); - AssertFormatException("te xt/plain"); - AssertFormatException("te=xt/plain"); - AssertFormatException("teäxt/plain"); - AssertFormatException("text/pläin"); - AssertFormatException("text"); - AssertFormatException("\"text/plain\""); - AssertFormatException("text/plain; charset=utf-8; "); - AssertFormatException("text/plain;"); - AssertFormatException("text/plain;charset=utf-8"); // ctor takes only media-type name, no parameters - } - - public static TheoryData MediaTypesWithSuffixes => - new TheoryData - { + [Fact] + public void Ctor_MediaTypeInvalidFormat_ThrowFormatException() + { + // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. + AssertFormatException(" text/plain "); + AssertFormatException("text / plain"); + AssertFormatException("text/ plain"); + AssertFormatException("text /plain"); + AssertFormatException("text/plain "); + AssertFormatException(" text/plain"); + AssertFormatException("te xt/plain"); + AssertFormatException("te=xt/plain"); + AssertFormatException("teäxt/plain"); + AssertFormatException("text/pläin"); + AssertFormatException("text"); + AssertFormatException("\"text/plain\""); + AssertFormatException("text/plain; charset=utf-8; "); + AssertFormatException("text/plain;"); + AssertFormatException("text/plain;charset=utf-8"); // ctor takes only media-type name, no parameters + } + + public static TheoryData MediaTypesWithSuffixes => + new TheoryData + { // See https://tools.ietf.org/html/rfc6838#section-4.2 for allowed names spec { "application/json", "json", null }, { "application/json+", "json", "" }, { "application/+json", "", "json" }, { "application/entitytype+json", "entitytype", "json" }, { "applica+tion/entitytype+json", "entitytype", "json" }, - }; + }; - [Theory] - [MemberData(nameof(MediaTypesWithSuffixes))] - public void Ctor_CanParseSuffixedMediaTypes(string mediaType, string expectedSubTypeWithoutSuffix, string expectedSubTypeSuffix) - { - var result = new MediaTypeHeaderValue(mediaType); + [Theory] + [MemberData(nameof(MediaTypesWithSuffixes))] + public void Ctor_CanParseSuffixedMediaTypes(string mediaType, string expectedSubTypeWithoutSuffix, string expectedSubTypeSuffix) + { + var result = new MediaTypeHeaderValue(mediaType); - Assert.Equal(new StringSegment(expectedSubTypeWithoutSuffix), result.SubTypeWithoutSuffix); // TODO consider overloading to have SubTypeWithoutSuffix? - Assert.Equal(new StringSegment(expectedSubTypeSuffix), result.Suffix); - } + Assert.Equal(new StringSegment(expectedSubTypeWithoutSuffix), result.SubTypeWithoutSuffix); // TODO consider overloading to have SubTypeWithoutSuffix? + Assert.Equal(new StringSegment(expectedSubTypeSuffix), result.Suffix); + } - public static TheoryData MediaTypesWithSuffixesAndSpaces => - new TheoryData - { + public static TheoryData MediaTypesWithSuffixesAndSpaces => + new TheoryData + { // See https://tools.ietf.org/html/rfc6838#section-4.2 for allowed names spec { " application / json+xml", "json", "xml" }, { " application / vnd.com-pany.some+entity!.v2+js.#$&^_n ; q=\"0.3+1\"", "vnd.com-pany.some+entity!.v2", "js.#$&^_n"}, { " application/ +json", "", "json" }, { " application/ entitytype+json ", "entitytype", "json" }, { " applica+tion/ entitytype+json ", "entitytype", "json" } - }; + }; - [Theory] - [MemberData(nameof(MediaTypesWithSuffixesAndSpaces))] - public void Parse_CanParseSuffixedMediaTypes(string mediaType, string expectedSubTypeWithoutSuffix, string expectedSubTypeSuffix) - { - var result = MediaTypeHeaderValue.Parse(mediaType); - - Assert.Equal(new StringSegment(expectedSubTypeWithoutSuffix), result.SubTypeWithoutSuffix); // TODO consider overloading to have SubTypeWithoutSuffix? - Assert.Equal(new StringSegment(expectedSubTypeSuffix), result.Suffix); - } - - [Theory] - [InlineData("*/*", true)] - [InlineData("text/*", true)] - [InlineData("text/*+suffix", true)] - [InlineData("text/*+", true)] - [InlineData("text/*+*", true)] - [InlineData("text/json+suffix", false)] - [InlineData("*/json+*", false)] - public void MatchesAllSubTypesWithoutSuffix_ReturnsExpectedResult(string value, bool expectedReturnValue) - { - // Arrange - var mediaType = new MediaTypeHeaderValue(value); + [Theory] + [MemberData(nameof(MediaTypesWithSuffixesAndSpaces))] + public void Parse_CanParseSuffixedMediaTypes(string mediaType, string expectedSubTypeWithoutSuffix, string expectedSubTypeSuffix) + { + var result = MediaTypeHeaderValue.Parse(mediaType); - // Act - var result = mediaType.MatchesAllSubTypesWithoutSuffix; + Assert.Equal(new StringSegment(expectedSubTypeWithoutSuffix), result.SubTypeWithoutSuffix); // TODO consider overloading to have SubTypeWithoutSuffix? + Assert.Equal(new StringSegment(expectedSubTypeSuffix), result.Suffix); + } - // Assert - Assert.Equal(expectedReturnValue, result); - } + [Theory] + [InlineData("*/*", true)] + [InlineData("text/*", true)] + [InlineData("text/*+suffix", true)] + [InlineData("text/*+", true)] + [InlineData("text/*+*", true)] + [InlineData("text/json+suffix", false)] + [InlineData("*/json+*", false)] + public void MatchesAllSubTypesWithoutSuffix_ReturnsExpectedResult(string value, bool expectedReturnValue) + { + // Arrange + var mediaType = new MediaTypeHeaderValue(value); - [Fact] - public void Ctor_MediaTypeValidFormat_SuccessfullyCreated() - { - var mediaType = new MediaTypeHeaderValue("text/plain"); - Assert.Equal("text/plain", mediaType.MediaType); - Assert.Equal(0, mediaType.Parameters.Count); - Assert.Null(mediaType.Charset.Value); - } - - [Fact] - public void Ctor_AddNameAndQuality_QualityParameterAdded() - { - var mediaType = new MediaTypeHeaderValue("application/xml", 0.08); - Assert.Equal(0.08, mediaType.Quality); - Assert.Equal("application/xml", mediaType.MediaType); - Assert.Equal(1, mediaType.Parameters.Count); - } - - [Fact] - public void Parameters_AddNull_Throw() - { - var mediaType = new MediaTypeHeaderValue("text/plain"); - Assert.Throws(() => mediaType.Parameters.Add(null!)); - } + // Act + var result = mediaType.MatchesAllSubTypesWithoutSuffix; - [Fact] - public void Copy_SimpleMediaType_Copied() - { - var mediaType0 = new MediaTypeHeaderValue("text/plain"); - var mediaType1 = mediaType0.Copy(); - Assert.NotSame(mediaType0, mediaType1); - Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value); - Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); - Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); - } - - [Fact] - public void CopyAsReadOnly_SimpleMediaType_CopiedAndReadOnly() - { - var mediaType0 = new MediaTypeHeaderValue("text/plain"); - var mediaType1 = mediaType0.CopyAsReadOnly(); - Assert.NotSame(mediaType0, mediaType1); - Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value); - Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); - Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); - - Assert.False(mediaType0.IsReadOnly); - Assert.True(mediaType1.IsReadOnly); - Assert.Throws(() => { mediaType1.MediaType = "some/value"; }); - } - - [Fact] - public void Copy_WithParameters_Copied() - { - var mediaType0 = new MediaTypeHeaderValue("text/plain"); - mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value")); - var mediaType1 = mediaType0.Copy(); - Assert.NotSame(mediaType0, mediaType1); - Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value); - Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); - Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); - var pair0 = mediaType0.Parameters.First(); - var pair1 = mediaType1.Parameters.First(); - Assert.NotSame(pair0, pair1); - Assert.Same(pair0.Name.Value, pair1.Name.Value); - Assert.Same(pair0.Value.Value, pair1.Value.Value); - } - - [Fact] - public void CopyAsReadOnly_WithParameters_CopiedAndReadOnly() - { - var mediaType0 = new MediaTypeHeaderValue("text/plain"); - mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value")); - var mediaType1 = mediaType0.CopyAsReadOnly(); - Assert.NotSame(mediaType0, mediaType1); - Assert.False(mediaType0.IsReadOnly); - Assert.True(mediaType1.IsReadOnly); - Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value); - - Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); - Assert.False(mediaType0.Parameters.IsReadOnly); - Assert.True(mediaType1.Parameters.IsReadOnly); - Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); - Assert.Throws(() => mediaType1.Parameters.Add(new NameValueHeaderValue("name"))); - Assert.Throws(() => mediaType1.Parameters.Remove(new NameValueHeaderValue("name"))); - Assert.Throws(() => mediaType1.Parameters.Clear()); - - var pair0 = mediaType0.Parameters.First(); - var pair1 = mediaType1.Parameters.First(); - Assert.NotSame(pair0, pair1); - Assert.False(pair0.IsReadOnly); - Assert.True(pair1.IsReadOnly); - Assert.Same(pair0.Name.Value, pair1.Name.Value); - Assert.Same(pair0.Value.Value, pair1.Value.Value); - } - - [Fact] - public void CopyFromReadOnly_WithParameters_CopiedAsNonReadOnly() - { - var mediaType0 = new MediaTypeHeaderValue("text/plain"); - mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value")); - var mediaType1 = mediaType0.CopyAsReadOnly(); - var mediaType2 = mediaType1.Copy(); - - Assert.NotSame(mediaType2, mediaType1); - Assert.Same(mediaType2.MediaType.Value, mediaType1.MediaType.Value); - Assert.True(mediaType1.IsReadOnly); - Assert.False(mediaType2.IsReadOnly); - Assert.NotSame(mediaType2.Parameters, mediaType1.Parameters); - Assert.Equal(mediaType2.Parameters.Count, mediaType1.Parameters.Count); - var pair2 = mediaType2.Parameters.First(); - var pair1 = mediaType1.Parameters.First(); - Assert.NotSame(pair2, pair1); - Assert.True(pair1.IsReadOnly); - Assert.False(pair2.IsReadOnly); - Assert.Same(pair2.Name.Value, pair1.Name.Value); - Assert.Same(pair2.Value.Value, pair1.Value.Value); - } - - [Fact] - public void MediaType_SetAndGetMediaType_MatchExpectations() - { - var mediaType = new MediaTypeHeaderValue("text/plain"); - Assert.Equal("text/plain", mediaType.MediaType); + // Assert + Assert.Equal(expectedReturnValue, result); + } - mediaType.MediaType = "application/xml"; - Assert.Equal("application/xml", mediaType.MediaType); - } + [Fact] + public void Ctor_MediaTypeValidFormat_SuccessfullyCreated() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); + Assert.Equal("text/plain", mediaType.MediaType); + Assert.Equal(0, mediaType.Parameters.Count); + Assert.Null(mediaType.Charset.Value); + } - [Fact] - public void Charset_SetCharsetAndValidateObject_ParametersEntryForCharsetAdded() - { - var mediaType = new MediaTypeHeaderValue("text/plain"); - mediaType.Charset = "mycharset"; - Assert.Equal("mycharset", mediaType.Charset); - Assert.Equal(1, mediaType.Parameters.Count); - Assert.Equal("charset", mediaType.Parameters.First().Name); - - mediaType.Charset = null; - Assert.Null(mediaType.Charset.Value); - Assert.Equal(0, mediaType.Parameters.Count); - mediaType.Charset = null; // It's OK to set it again to null; no exception. - } - - [Fact] - public void Charset_AddCharsetParameterThenUseProperty_ParametersEntryIsOverwritten() - { - var mediaType = new MediaTypeHeaderValue("text/plain"); + [Fact] + public void Ctor_AddNameAndQuality_QualityParameterAdded() + { + var mediaType = new MediaTypeHeaderValue("application/xml", 0.08); + Assert.Equal(0.08, mediaType.Quality); + Assert.Equal("application/xml", mediaType.MediaType); + Assert.Equal(1, mediaType.Parameters.Count); + } - // Note that uppercase letters are used. Comparison should happen case-insensitive. - var charset = new NameValueHeaderValue("CHARSET", "old_charset"); - mediaType.Parameters.Add(charset); - Assert.Equal(1, mediaType.Parameters.Count); - Assert.Equal("CHARSET", mediaType.Parameters.First().Name); + [Fact] + public void Parameters_AddNull_Throw() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); + Assert.Throws(() => mediaType.Parameters.Add(null!)); + } - mediaType.Charset = "new_charset"; - Assert.Equal("new_charset", mediaType.Charset); - Assert.Equal(1, mediaType.Parameters.Count); - Assert.Equal("CHARSET", mediaType.Parameters.First().Name); + [Fact] + public void Copy_SimpleMediaType_Copied() + { + var mediaType0 = new MediaTypeHeaderValue("text/plain"); + var mediaType1 = mediaType0.Copy(); + Assert.NotSame(mediaType0, mediaType1); + Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value); + Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); + Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); + } - mediaType.Parameters.Remove(charset); - Assert.Null(mediaType.Charset.Value); - } + [Fact] + public void CopyAsReadOnly_SimpleMediaType_CopiedAndReadOnly() + { + var mediaType0 = new MediaTypeHeaderValue("text/plain"); + var mediaType1 = mediaType0.CopyAsReadOnly(); + Assert.NotSame(mediaType0, mediaType1); + Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value); + Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); + Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); + + Assert.False(mediaType0.IsReadOnly); + Assert.True(mediaType1.IsReadOnly); + Assert.Throws(() => { mediaType1.MediaType = "some/value"; }); + } - [Fact] - public void Quality_SetCharsetAndValidateObject_ParametersEntryForCharsetAdded() - { - var mediaType = new MediaTypeHeaderValue("text/plain"); - mediaType.Quality = 0.563156454; - Assert.Equal(0.563, mediaType.Quality); - Assert.Equal(1, mediaType.Parameters.Count); - Assert.Equal("q", mediaType.Parameters.First().Name); - Assert.Equal("0.563", mediaType.Parameters.First().Value); - - mediaType.Quality = null; - Assert.Null(mediaType.Quality); - Assert.Equal(0, mediaType.Parameters.Count); - mediaType.Quality = null; // It's OK to set it again to null; no exception. - } - - [Fact] - public void Quality_AddQualityParameterThenUseProperty_ParametersEntryIsOverwritten() - { - var mediaType = new MediaTypeHeaderValue("text/plain"); + [Fact] + public void Copy_WithParameters_Copied() + { + var mediaType0 = new MediaTypeHeaderValue("text/plain"); + mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value")); + var mediaType1 = mediaType0.Copy(); + Assert.NotSame(mediaType0, mediaType1); + Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value); + Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); + Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); + var pair0 = mediaType0.Parameters.First(); + var pair1 = mediaType1.Parameters.First(); + Assert.NotSame(pair0, pair1); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Same(pair0.Value.Value, pair1.Value.Value); + } - var quality = new NameValueHeaderValue("q", "0.132"); - mediaType.Parameters.Add(quality); - Assert.Equal(1, mediaType.Parameters.Count); - Assert.Equal("q", mediaType.Parameters.First().Name); - Assert.Equal(0.132, mediaType.Quality); + [Fact] + public void CopyAsReadOnly_WithParameters_CopiedAndReadOnly() + { + var mediaType0 = new MediaTypeHeaderValue("text/plain"); + mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value")); + var mediaType1 = mediaType0.CopyAsReadOnly(); + Assert.NotSame(mediaType0, mediaType1); + Assert.False(mediaType0.IsReadOnly); + Assert.True(mediaType1.IsReadOnly); + Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value); + + Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); + Assert.False(mediaType0.Parameters.IsReadOnly); + Assert.True(mediaType1.Parameters.IsReadOnly); + Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); + Assert.Throws(() => mediaType1.Parameters.Add(new NameValueHeaderValue("name"))); + Assert.Throws(() => mediaType1.Parameters.Remove(new NameValueHeaderValue("name"))); + Assert.Throws(() => mediaType1.Parameters.Clear()); + + var pair0 = mediaType0.Parameters.First(); + var pair1 = mediaType1.Parameters.First(); + Assert.NotSame(pair0, pair1); + Assert.False(pair0.IsReadOnly); + Assert.True(pair1.IsReadOnly); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Same(pair0.Value.Value, pair1.Value.Value); + } - mediaType.Quality = 0.9; - Assert.Equal(0.9, mediaType.Quality); - Assert.Equal(1, mediaType.Parameters.Count); - Assert.Equal("q", mediaType.Parameters.First().Name); + [Fact] + public void CopyFromReadOnly_WithParameters_CopiedAsNonReadOnly() + { + var mediaType0 = new MediaTypeHeaderValue("text/plain"); + mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value")); + var mediaType1 = mediaType0.CopyAsReadOnly(); + var mediaType2 = mediaType1.Copy(); + + Assert.NotSame(mediaType2, mediaType1); + Assert.Same(mediaType2.MediaType.Value, mediaType1.MediaType.Value); + Assert.True(mediaType1.IsReadOnly); + Assert.False(mediaType2.IsReadOnly); + Assert.NotSame(mediaType2.Parameters, mediaType1.Parameters); + Assert.Equal(mediaType2.Parameters.Count, mediaType1.Parameters.Count); + var pair2 = mediaType2.Parameters.First(); + var pair1 = mediaType1.Parameters.First(); + Assert.NotSame(pair2, pair1); + Assert.True(pair1.IsReadOnly); + Assert.False(pair2.IsReadOnly); + Assert.Same(pair2.Name.Value, pair1.Name.Value); + Assert.Same(pair2.Value.Value, pair1.Value.Value); + } - mediaType.Parameters.Remove(quality); - Assert.Null(mediaType.Quality); - } + [Fact] + public void MediaType_SetAndGetMediaType_MatchExpectations() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); + Assert.Equal("text/plain", mediaType.MediaType); - [Fact] - public void Quality_AddQualityParameterUpperCase_CaseInsensitiveComparison() - { - var mediaType = new MediaTypeHeaderValue("text/plain"); + mediaType.MediaType = "application/xml"; + Assert.Equal("application/xml", mediaType.MediaType); + } - var quality = new NameValueHeaderValue("Q", "0.132"); - mediaType.Parameters.Add(quality); - Assert.Equal(1, mediaType.Parameters.Count); - Assert.Equal("Q", mediaType.Parameters.First().Name); - Assert.Equal(0.132, mediaType.Quality); - } + [Fact] + public void Charset_SetCharsetAndValidateObject_ParametersEntryForCharsetAdded() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); + mediaType.Charset = "mycharset"; + Assert.Equal("mycharset", mediaType.Charset); + Assert.Equal(1, mediaType.Parameters.Count); + Assert.Equal("charset", mediaType.Parameters.First().Name); + + mediaType.Charset = null; + Assert.Null(mediaType.Charset.Value); + Assert.Equal(0, mediaType.Parameters.Count); + mediaType.Charset = null; // It's OK to set it again to null; no exception. + } - [Fact] - public void Quality_LessThanZero_Throw() - { - Assert.Throws(() => new MediaTypeHeaderValue("application/xml", -0.01)); - } + [Fact] + public void Charset_AddCharsetParameterThenUseProperty_ParametersEntryIsOverwritten() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); - [Fact] - public void Quality_GreaterThanOne_Throw() - { - var mediaType = new MediaTypeHeaderValue("application/xml"); - Assert.Throws(() => mediaType.Quality = 1.01); - } + // Note that uppercase letters are used. Comparison should happen case-insensitive. + var charset = new NameValueHeaderValue("CHARSET", "old_charset"); + mediaType.Parameters.Add(charset); + Assert.Equal(1, mediaType.Parameters.Count); + Assert.Equal("CHARSET", mediaType.Parameters.First().Name); - [Fact] - public void ToString_UseDifferentMediaTypes_AllSerializedCorrectly() - { - var mediaType = new MediaTypeHeaderValue("text/plain"); - Assert.Equal("text/plain", mediaType.ToString()); + mediaType.Charset = "new_charset"; + Assert.Equal("new_charset", mediaType.Charset); + Assert.Equal(1, mediaType.Parameters.Count); + Assert.Equal("CHARSET", mediaType.Parameters.First().Name); - mediaType.Charset = "utf-8"; - Assert.Equal("text/plain; charset=utf-8", mediaType.ToString()); + mediaType.Parameters.Remove(charset); + Assert.Null(mediaType.Charset.Value); + } - mediaType.Parameters.Add(new NameValueHeaderValue("custom", "\"custom value\"")); - Assert.Equal("text/plain; charset=utf-8; custom=\"custom value\"", mediaType.ToString()); + [Fact] + public void Quality_SetCharsetAndValidateObject_ParametersEntryForCharsetAdded() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); + mediaType.Quality = 0.563156454; + Assert.Equal(0.563, mediaType.Quality); + Assert.Equal(1, mediaType.Parameters.Count); + Assert.Equal("q", mediaType.Parameters.First().Name); + Assert.Equal("0.563", mediaType.Parameters.First().Value); + + mediaType.Quality = null; + Assert.Null(mediaType.Quality); + Assert.Equal(0, mediaType.Parameters.Count); + mediaType.Quality = null; // It's OK to set it again to null; no exception. + } - mediaType.Charset = null; - Assert.Equal("text/plain; custom=\"custom value\"", mediaType.ToString()); - } + [Fact] + public void Quality_AddQualityParameterThenUseProperty_ParametersEntryIsOverwritten() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); - [Fact] - public void GetHashCode_UseMediaTypeWithAndWithoutParameters_SameOrDifferentHashCodes() - { - var mediaType1 = new MediaTypeHeaderValue("text/plain"); - var mediaType2 = new MediaTypeHeaderValue("text/plain"); - mediaType2.Charset = "utf-8"; - var mediaType3 = new MediaTypeHeaderValue("text/plain"); - mediaType3.Parameters.Add(new NameValueHeaderValue("name", "value")); - var mediaType4 = new MediaTypeHeaderValue("TEXT/plain"); - var mediaType5 = new MediaTypeHeaderValue("TEXT/plain"); - mediaType5.Parameters.Add(new NameValueHeaderValue("CHARSET", "UTF-8")); - - Assert.NotEqual(mediaType1.GetHashCode(), mediaType2.GetHashCode()); - Assert.NotEqual(mediaType1.GetHashCode(), mediaType3.GetHashCode()); - Assert.NotEqual(mediaType2.GetHashCode(), mediaType3.GetHashCode()); - Assert.Equal(mediaType1.GetHashCode(), mediaType4.GetHashCode()); - Assert.Equal(mediaType2.GetHashCode(), mediaType5.GetHashCode()); - } - - [Fact] - public void Equals_UseMediaTypeWithAndWithoutParameters_EqualOrNotEqualNoExceptions() - { - var mediaType1 = new MediaTypeHeaderValue("text/plain"); - var mediaType2 = new MediaTypeHeaderValue("text/plain"); - mediaType2.Charset = "utf-8"; - var mediaType3 = new MediaTypeHeaderValue("text/plain"); - mediaType3.Parameters.Add(new NameValueHeaderValue("name", "value")); - var mediaType4 = new MediaTypeHeaderValue("TEXT/plain"); - var mediaType5 = new MediaTypeHeaderValue("TEXT/plain"); - mediaType5.Parameters.Add(new NameValueHeaderValue("CHARSET", "UTF-8")); - var mediaType6 = new MediaTypeHeaderValue("TEXT/plain"); - mediaType6.Parameters.Add(new NameValueHeaderValue("CHARSET", "UTF-8")); - mediaType6.Parameters.Add(new NameValueHeaderValue("custom", "value")); - var mediaType7 = new MediaTypeHeaderValue("text/other"); - - Assert.False(mediaType1.Equals(mediaType2), "No params vs. charset."); - Assert.False(mediaType2.Equals(mediaType1), "charset vs. no params."); - Assert.False(mediaType1.Equals(null), "No params vs. ."); - Assert.False(mediaType1!.Equals(mediaType3), "No params vs. custom param."); - Assert.False(mediaType2.Equals(mediaType3), "charset vs. custom param."); - Assert.True(mediaType1.Equals(mediaType4), "Different casing."); - Assert.True(mediaType2.Equals(mediaType5), "Different casing in charset."); - Assert.False(mediaType5.Equals(mediaType6), "charset vs. custom param."); - Assert.False(mediaType1.Equals(mediaType7), "text/plain vs. text/other."); - } - - [Fact] - public void Parse_SetOfValidValueStrings_ParsedCorrectly() - { - CheckValidParse("\r\n text/plain ", new MediaTypeHeaderValue("text/plain")); - CheckValidParse("text/plain", new MediaTypeHeaderValue("text/plain")); + var quality = new NameValueHeaderValue("q", "0.132"); + mediaType.Parameters.Add(quality); + Assert.Equal(1, mediaType.Parameters.Count); + Assert.Equal("q", mediaType.Parameters.First().Name); + Assert.Equal(0.132, mediaType.Quality); + + mediaType.Quality = 0.9; + Assert.Equal(0.9, mediaType.Quality); + Assert.Equal(1, mediaType.Parameters.Count); + Assert.Equal("q", mediaType.Parameters.First().Name); + + mediaType.Parameters.Remove(quality); + Assert.Null(mediaType.Quality); + } + + [Fact] + public void Quality_AddQualityParameterUpperCase_CaseInsensitiveComparison() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); + + var quality = new NameValueHeaderValue("Q", "0.132"); + mediaType.Parameters.Add(quality); + Assert.Equal(1, mediaType.Parameters.Count); + Assert.Equal("Q", mediaType.Parameters.First().Name); + Assert.Equal(0.132, mediaType.Quality); + } + + [Fact] + public void Quality_LessThanZero_Throw() + { + Assert.Throws(() => new MediaTypeHeaderValue("application/xml", -0.01)); + } + + [Fact] + public void Quality_GreaterThanOne_Throw() + { + var mediaType = new MediaTypeHeaderValue("application/xml"); + Assert.Throws(() => mediaType.Quality = 1.01); + } - CheckValidParse("\r\n text / plain ; charset = utf-8 ", new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" }); - CheckValidParse(" text/plain;charset=utf-8", new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" }); + [Fact] + public void ToString_UseDifferentMediaTypes_AllSerializedCorrectly() + { + var mediaType = new MediaTypeHeaderValue("text/plain"); + Assert.Equal("text/plain", mediaType.ToString()); - CheckValidParse("text/plain; charset=iso-8859-1", new MediaTypeHeaderValue("text/plain") { Charset = "iso-8859-1" }); + mediaType.Charset = "utf-8"; + Assert.Equal("text/plain; charset=utf-8", mediaType.ToString()); - var expected = new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" }; - expected.Parameters.Add(new NameValueHeaderValue("custom", "value")); - CheckValidParse(" text/plain; custom=value;charset=utf-8", expected); + mediaType.Parameters.Add(new NameValueHeaderValue("custom", "\"custom value\"")); + Assert.Equal("text/plain; charset=utf-8; custom=\"custom value\"", mediaType.ToString()); - expected = new MediaTypeHeaderValue("text/plain"); - expected.Parameters.Add(new NameValueHeaderValue("custom")); - CheckValidParse(" text/plain; custom", expected); + mediaType.Charset = null; + Assert.Equal("text/plain; custom=\"custom value\"", mediaType.ToString()); + } - expected = new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" }; - expected.Parameters.Add(new NameValueHeaderValue("custom", "\"x\"")); - CheckValidParse("text / plain ; custom =\r\n \"x\" ; charset = utf-8 ", expected); + [Fact] + public void GetHashCode_UseMediaTypeWithAndWithoutParameters_SameOrDifferentHashCodes() + { + var mediaType1 = new MediaTypeHeaderValue("text/plain"); + var mediaType2 = new MediaTypeHeaderValue("text/plain"); + mediaType2.Charset = "utf-8"; + var mediaType3 = new MediaTypeHeaderValue("text/plain"); + mediaType3.Parameters.Add(new NameValueHeaderValue("name", "value")); + var mediaType4 = new MediaTypeHeaderValue("TEXT/plain"); + var mediaType5 = new MediaTypeHeaderValue("TEXT/plain"); + mediaType5.Parameters.Add(new NameValueHeaderValue("CHARSET", "UTF-8")); + + Assert.NotEqual(mediaType1.GetHashCode(), mediaType2.GetHashCode()); + Assert.NotEqual(mediaType1.GetHashCode(), mediaType3.GetHashCode()); + Assert.NotEqual(mediaType2.GetHashCode(), mediaType3.GetHashCode()); + Assert.Equal(mediaType1.GetHashCode(), mediaType4.GetHashCode()); + Assert.Equal(mediaType2.GetHashCode(), mediaType5.GetHashCode()); + } - expected = new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" }; - expected.Parameters.Add(new NameValueHeaderValue("custom", "\"x\"")); - CheckValidParse("text/plain;custom=\"x\";charset=utf-8", expected); + [Fact] + public void Equals_UseMediaTypeWithAndWithoutParameters_EqualOrNotEqualNoExceptions() + { + var mediaType1 = new MediaTypeHeaderValue("text/plain"); + var mediaType2 = new MediaTypeHeaderValue("text/plain"); + mediaType2.Charset = "utf-8"; + var mediaType3 = new MediaTypeHeaderValue("text/plain"); + mediaType3.Parameters.Add(new NameValueHeaderValue("name", "value")); + var mediaType4 = new MediaTypeHeaderValue("TEXT/plain"); + var mediaType5 = new MediaTypeHeaderValue("TEXT/plain"); + mediaType5.Parameters.Add(new NameValueHeaderValue("CHARSET", "UTF-8")); + var mediaType6 = new MediaTypeHeaderValue("TEXT/plain"); + mediaType6.Parameters.Add(new NameValueHeaderValue("CHARSET", "UTF-8")); + mediaType6.Parameters.Add(new NameValueHeaderValue("custom", "value")); + var mediaType7 = new MediaTypeHeaderValue("text/other"); + + Assert.False(mediaType1.Equals(mediaType2), "No params vs. charset."); + Assert.False(mediaType2.Equals(mediaType1), "charset vs. no params."); + Assert.False(mediaType1.Equals(null), "No params vs. ."); + Assert.False(mediaType1!.Equals(mediaType3), "No params vs. custom param."); + Assert.False(mediaType2.Equals(mediaType3), "charset vs. custom param."); + Assert.True(mediaType1.Equals(mediaType4), "Different casing."); + Assert.True(mediaType2.Equals(mediaType5), "Different casing in charset."); + Assert.False(mediaType5.Equals(mediaType6), "charset vs. custom param."); + Assert.False(mediaType1.Equals(mediaType7), "text/plain vs. text/other."); + } - expected = new MediaTypeHeaderValue("text/plain"); - CheckValidParse("text/plain;", expected); + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidParse("\r\n text/plain ", new MediaTypeHeaderValue("text/plain")); + CheckValidParse("text/plain", new MediaTypeHeaderValue("text/plain")); - expected = new MediaTypeHeaderValue("text/plain"); - expected.Parameters.Add(new NameValueHeaderValue("name", "")); - CheckValidParse("text/plain;name=", expected); + CheckValidParse("\r\n text / plain ; charset = utf-8 ", new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" }); + CheckValidParse(" text/plain;charset=utf-8", new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" }); - expected = new MediaTypeHeaderValue("text/plain"); - expected.Parameters.Add(new NameValueHeaderValue("name", "value")); - CheckValidParse("text/plain;name=value;", expected); + CheckValidParse("text/plain; charset=iso-8859-1", new MediaTypeHeaderValue("text/plain") { Charset = "iso-8859-1" }); - expected = new MediaTypeHeaderValue("text/plain"); - expected.Charset = "iso-8859-1"; - expected.Quality = 1.0; - CheckValidParse("text/plain; charset=iso-8859-1; q=1.0", expected); + var expected = new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" }; + expected.Parameters.Add(new NameValueHeaderValue("custom", "value")); + CheckValidParse(" text/plain; custom=value;charset=utf-8", expected); - expected = new MediaTypeHeaderValue("*/xml"); - expected.Charset = "utf-8"; - expected.Quality = 0.5; - CheckValidParse("\r\n */xml; charset=utf-8; q=0.5", expected); + expected = new MediaTypeHeaderValue("text/plain"); + expected.Parameters.Add(new NameValueHeaderValue("custom")); + CheckValidParse(" text/plain; custom", expected); - expected = new MediaTypeHeaderValue("*/*"); - CheckValidParse("*/*", expected); + expected = new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" }; + expected.Parameters.Add(new NameValueHeaderValue("custom", "\"x\"")); + CheckValidParse("text / plain ; custom =\r\n \"x\" ; charset = utf-8 ", expected); - expected = new MediaTypeHeaderValue("text/*"); - expected.Charset = "utf-8"; - expected.Parameters.Add(new NameValueHeaderValue("foo", "bar")); - CheckValidParse("text/*; charset=utf-8; foo=bar", expected); + expected = new MediaTypeHeaderValue("text/plain") { Charset = "utf-8" }; + expected.Parameters.Add(new NameValueHeaderValue("custom", "\"x\"")); + CheckValidParse("text/plain;custom=\"x\";charset=utf-8", expected); - expected = new MediaTypeHeaderValue("text/plain"); - expected.Charset = "utf-8"; - expected.Quality = 0; - expected.Parameters.Add(new NameValueHeaderValue("foo", "bar")); - CheckValidParse("text/plain; charset=utf-8; foo=bar; q=0.0", expected); - } + expected = new MediaTypeHeaderValue("text/plain"); + CheckValidParse("text/plain;", expected); - [Fact] - public void Parse_SetOfInvalidValueStrings_Throws() - { - CheckInvalidParse(""); - CheckInvalidParse(" "); - CheckInvalidParse(null); - CheckInvalidParse("text/plain会"); - CheckInvalidParse("text/plain ,"); - CheckInvalidParse("text/plain,"); - CheckInvalidParse("text/plain; charset=utf-8 ,"); - CheckInvalidParse("text/plain; charset=utf-8,"); - CheckInvalidParse("textplain"); - CheckInvalidParse("text/"); - CheckInvalidParse(",, , ,,text/plain; charset=iso-8859-1; q=1.0,\r\n */xml; charset=utf-8; q=0.5,,,"); - CheckInvalidParse("text/plain; charset=iso-8859-1; q=1.0, */xml; charset=utf-8; q=0.5"); - CheckInvalidParse(" , */xml; charset=utf-8; q=0.5 "); - CheckInvalidParse("text/plain; charset=iso-8859-1; q=1.0 , "); - } - - [Fact] - public void TryParse_SetOfValidValueStrings_ParsedCorrectly() - { - var expected = new MediaTypeHeaderValue("text/plain"); - CheckValidTryParse("\r\n text/plain ", expected); - CheckValidTryParse("text/plain", expected); + expected = new MediaTypeHeaderValue("text/plain"); + expected.Parameters.Add(new NameValueHeaderValue("name", "")); + CheckValidParse("text/plain;name=", expected); - // We don't have to test all possible input strings, since most of the pieces are handled by other parsers. - // The purpose of this test is to verify that these other parsers are combined correctly to build a - // media-type parser. - expected.Charset = "utf-8"; - CheckValidTryParse("\r\n text / plain ; charset = utf-8 ", expected); - CheckValidTryParse(" text/plain;charset=utf-8", expected); + expected = new MediaTypeHeaderValue("text/plain"); + expected.Parameters.Add(new NameValueHeaderValue("name", "value")); + CheckValidParse("text/plain;name=value;", expected); - var value1 = new MediaTypeHeaderValue("text/plain"); - value1.Charset = "iso-8859-1"; - value1.Quality = 1.0; + expected = new MediaTypeHeaderValue("text/plain"); + expected.Charset = "iso-8859-1"; + expected.Quality = 1.0; + CheckValidParse("text/plain; charset=iso-8859-1; q=1.0", expected); - CheckValidTryParse("text/plain; charset=iso-8859-1; q=1.0", value1); + expected = new MediaTypeHeaderValue("*/xml"); + expected.Charset = "utf-8"; + expected.Quality = 0.5; + CheckValidParse("\r\n */xml; charset=utf-8; q=0.5", expected); - var value2 = new MediaTypeHeaderValue("*/xml"); - value2.Charset = "utf-8"; - value2.Quality = 0.5; + expected = new MediaTypeHeaderValue("*/*"); + CheckValidParse("*/*", expected); - CheckValidTryParse("\r\n */xml; charset=utf-8; q=0.5", value2); - } + expected = new MediaTypeHeaderValue("text/*"); + expected.Charset = "utf-8"; + expected.Parameters.Add(new NameValueHeaderValue("foo", "bar")); + CheckValidParse("text/*; charset=utf-8; foo=bar", expected); - [Fact] - public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() - { - CheckInvalidTryParse(""); - CheckInvalidTryParse(" "); - CheckInvalidTryParse(null); - CheckInvalidTryParse("text/plain会"); - CheckInvalidTryParse("text/plain ,"); - CheckInvalidTryParse("text/plain,"); - CheckInvalidTryParse("text/plain; charset=utf-8 ,"); - CheckInvalidTryParse("text/plain; charset=utf-8,"); - CheckInvalidTryParse("textplain"); - CheckInvalidTryParse("text/"); - CheckInvalidTryParse(",, , ,,text/plain; charset=iso-8859-1; q=1.0,\r\n */xml; charset=utf-8; q=0.5,,,"); - CheckInvalidTryParse("text/plain; charset=iso-8859-1; q=1.0, */xml; charset=utf-8; q=0.5"); - CheckInvalidTryParse(" , */xml; charset=utf-8; q=0.5 "); - CheckInvalidTryParse("text/plain; charset=iso-8859-1; q=1.0 , "); - } - - [Fact] - public void ParseList_NullOrEmptyArray_ReturnsEmptyList() - { - var results = MediaTypeHeaderValue.ParseList(null); - Assert.NotNull(results); - Assert.Equal(0, results.Count); + expected = new MediaTypeHeaderValue("text/plain"); + expected.Charset = "utf-8"; + expected.Quality = 0; + expected.Parameters.Add(new NameValueHeaderValue("foo", "bar")); + CheckValidParse("text/plain; charset=utf-8; foo=bar; q=0.0", expected); + } - results = MediaTypeHeaderValue.ParseList(new string[0]); - Assert.NotNull(results); - Assert.Equal(0, results.Count); + [Fact] + public void Parse_SetOfInvalidValueStrings_Throws() + { + CheckInvalidParse(""); + CheckInvalidParse(" "); + CheckInvalidParse(null); + CheckInvalidParse("text/plain会"); + CheckInvalidParse("text/plain ,"); + CheckInvalidParse("text/plain,"); + CheckInvalidParse("text/plain; charset=utf-8 ,"); + CheckInvalidParse("text/plain; charset=utf-8,"); + CheckInvalidParse("textplain"); + CheckInvalidParse("text/"); + CheckInvalidParse(",, , ,,text/plain; charset=iso-8859-1; q=1.0,\r\n */xml; charset=utf-8; q=0.5,,,"); + CheckInvalidParse("text/plain; charset=iso-8859-1; q=1.0, */xml; charset=utf-8; q=0.5"); + CheckInvalidParse(" , */xml; charset=utf-8; q=0.5 "); + CheckInvalidParse("text/plain; charset=iso-8859-1; q=1.0 , "); + } - results = MediaTypeHeaderValue.ParseList(new string[] { "" }); - Assert.NotNull(results); - Assert.Equal(0, results.Count); - } + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + var expected = new MediaTypeHeaderValue("text/plain"); + CheckValidTryParse("\r\n text/plain ", expected); + CheckValidTryParse("text/plain", expected); - [Fact] - public void TryParseList_NullOrEmptyArray_ReturnsFalse() - { - Assert.False(MediaTypeHeaderValue.TryParseList(null, out var results)); - Assert.False(MediaTypeHeaderValue.TryParseList(new string[0], out results)); - Assert.False(MediaTypeHeaderValue.TryParseList(new string[] { "" }, out results)); - } + // We don't have to test all possible input strings, since most of the pieces are handled by other parsers. + // The purpose of this test is to verify that these other parsers are combined correctly to build a + // media-type parser. + expected.Charset = "utf-8"; + CheckValidTryParse("\r\n text / plain ; charset = utf-8 ", expected); + CheckValidTryParse(" text/plain;charset=utf-8", expected); - [Fact] - public void ParseList_SetOfValidValueStrings_ReturnsValues() - { - var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" }; - var results = MediaTypeHeaderValue.ParseList(inputs); + var value1 = new MediaTypeHeaderValue("text/plain"); + value1.Charset = "iso-8859-1"; + value1.Quality = 1.0; - var expectedResults = new[] - { + CheckValidTryParse("text/plain; charset=iso-8859-1; q=1.0", value1); + + var value2 = new MediaTypeHeaderValue("*/xml"); + value2.Charset = "utf-8"; + value2.Quality = 0.5; + + CheckValidTryParse("\r\n */xml; charset=utf-8; q=0.5", value2); + } + + [Fact] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() + { + CheckInvalidTryParse(""); + CheckInvalidTryParse(" "); + CheckInvalidTryParse(null); + CheckInvalidTryParse("text/plain会"); + CheckInvalidTryParse("text/plain ,"); + CheckInvalidTryParse("text/plain,"); + CheckInvalidTryParse("text/plain; charset=utf-8 ,"); + CheckInvalidTryParse("text/plain; charset=utf-8,"); + CheckInvalidTryParse("textplain"); + CheckInvalidTryParse("text/"); + CheckInvalidTryParse(",, , ,,text/plain; charset=iso-8859-1; q=1.0,\r\n */xml; charset=utf-8; q=0.5,,,"); + CheckInvalidTryParse("text/plain; charset=iso-8859-1; q=1.0, */xml; charset=utf-8; q=0.5"); + CheckInvalidTryParse(" , */xml; charset=utf-8; q=0.5 "); + CheckInvalidTryParse("text/plain; charset=iso-8859-1; q=1.0 , "); + } + + [Fact] + public void ParseList_NullOrEmptyArray_ReturnsEmptyList() + { + var results = MediaTypeHeaderValue.ParseList(null); + Assert.NotNull(results); + Assert.Equal(0, results.Count); + + results = MediaTypeHeaderValue.ParseList(new string[0]); + Assert.NotNull(results); + Assert.Equal(0, results.Count); + + results = MediaTypeHeaderValue.ParseList(new string[] { "" }); + Assert.NotNull(results); + Assert.Equal(0, results.Count); + } + + [Fact] + public void TryParseList_NullOrEmptyArray_ReturnsFalse() + { + Assert.False(MediaTypeHeaderValue.TryParseList(null, out var results)); + Assert.False(MediaTypeHeaderValue.TryParseList(new string[0], out results)); + Assert.False(MediaTypeHeaderValue.TryParseList(new string[] { "" }, out results)); + } + + [Fact] + public void ParseList_SetOfValidValueStrings_ReturnsValues() + { + var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" }; + var results = MediaTypeHeaderValue.ParseList(inputs); + + var expectedResults = new[] + { new MediaTypeHeaderValue("text/html"), new MediaTypeHeaderValue("application/xhtml+xml"), new MediaTypeHeaderValue("application/xml", 0.9), @@ -557,17 +557,17 @@ namespace Microsoft.Net.Http.Headers new MediaTypeHeaderValue("*/*", 0.8), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void ParseStrictList_SetOfValidValueStrings_ReturnsValues() - { - var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" }; - var results = MediaTypeHeaderValue.ParseStrictList(inputs); + [Fact] + public void ParseStrictList_SetOfValidValueStrings_ReturnsValues() + { + var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" }; + var results = MediaTypeHeaderValue.ParseStrictList(inputs); - var expectedResults = new[] - { + var expectedResults = new[] + { new MediaTypeHeaderValue("text/html"), new MediaTypeHeaderValue("application/xhtml+xml"), new MediaTypeHeaderValue("application/xml", 0.9), @@ -575,17 +575,17 @@ namespace Microsoft.Net.Http.Headers new MediaTypeHeaderValue("*/*", 0.8), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void TryParseList_SetOfValidValueStrings_ReturnsTrue() - { - var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" }; - Assert.True(MediaTypeHeaderValue.TryParseList(inputs, out var results)); + [Fact] + public void TryParseList_SetOfValidValueStrings_ReturnsTrue() + { + var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" }; + Assert.True(MediaTypeHeaderValue.TryParseList(inputs, out var results)); - var expectedResults = new[] - { + var expectedResults = new[] + { new MediaTypeHeaderValue("text/html"), new MediaTypeHeaderValue("application/xhtml+xml"), new MediaTypeHeaderValue("application/xml", 0.9), @@ -593,17 +593,17 @@ namespace Microsoft.Net.Http.Headers new MediaTypeHeaderValue("*/*", 0.8), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } + + [Fact] + public void TryParseStrictList_SetOfValidValueStrings_ReturnsTrue() + { + var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" }; + Assert.True(MediaTypeHeaderValue.TryParseStrictList(inputs, out var results)); - [Fact] - public void TryParseStrictList_SetOfValidValueStrings_ReturnsTrue() + var expectedResults = new[] { - var inputs = new[] { "text/html,application/xhtml+xml,", "application/xml;q=0.9,image/webp,*/*;q=0.8" }; - Assert.True(MediaTypeHeaderValue.TryParseStrictList(inputs, out var results)); - - var expectedResults = new[] - { new MediaTypeHeaderValue("text/html"), new MediaTypeHeaderValue("application/xhtml+xml"), new MediaTypeHeaderValue("application/xml", 0.9), @@ -611,21 +611,21 @@ namespace Microsoft.Net.Http.Headers new MediaTypeHeaderValue("*/*", 0.8), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void ParseList_WithSomeInvalidValues_IgnoresInvalidValues() + [Fact] + public void ParseList_WithSomeInvalidValues_IgnoresInvalidValues() + { + var inputs = new[] { - var inputs = new[] - { "text/html,application/xhtml+xml, ignore-this, ignore/this", "application/xml;q=0.9,image/webp,*/*;q=0.8" }; - var results = MediaTypeHeaderValue.ParseList(inputs); + var results = MediaTypeHeaderValue.ParseList(inputs); - var expectedResults = new[] - { + var expectedResults = new[] + { new MediaTypeHeaderValue("text/html"), new MediaTypeHeaderValue("application/xhtml+xml"), new MediaTypeHeaderValue("ignore/this"), @@ -634,33 +634,33 @@ namespace Microsoft.Net.Http.Headers new MediaTypeHeaderValue("*/*", 0.8), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void ParseStrictList_WithSomeInvalidValues_Throws() + [Fact] + public void ParseStrictList_WithSomeInvalidValues_Throws() + { + var inputs = new[] { - var inputs = new[] - { "text/html,application/xhtml+xml, ignore-this, ignore/this", "application/xml;q=0.9,image/webp,*/*;q=0.8" }; - Assert.Throws(() => MediaTypeHeaderValue.ParseStrictList(inputs)); - } + Assert.Throws(() => MediaTypeHeaderValue.ParseStrictList(inputs)); + } - [Fact] - public void TryParseList_WithSomeInvalidValues_IgnoresInvalidValues() + [Fact] + public void TryParseList_WithSomeInvalidValues_IgnoresInvalidValues() + { + var inputs = new[] { - var inputs = new[] - { "text/html,application/xhtml+xml, ignore-this, ignore/this", "application/xml;q=0.9,image/webp,*/*;q=0.8", "application/xml;q=0 4" }; - Assert.True(MediaTypeHeaderValue.TryParseList(inputs, out var results)); + Assert.True(MediaTypeHeaderValue.TryParseList(inputs, out var results)); - var expectedResults = new[] - { + var expectedResults = new[] + { new MediaTypeHeaderValue("text/html"), new MediaTypeHeaderValue("application/xhtml+xml"), new MediaTypeHeaderValue("ignore/this"), @@ -669,235 +669,235 @@ namespace Microsoft.Net.Http.Headers new MediaTypeHeaderValue("*/*", 0.8), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void TryParseStrictList_WithSomeInvalidValues_ReturnsFalse() + [Fact] + public void TryParseStrictList_WithSomeInvalidValues_ReturnsFalse() + { + var inputs = new[] { - var inputs = new[] - { "text/html,application/xhtml+xml, ignore-this, ignore/this", "application/xml;q=0.9,image/webp,*/*;q=0.8", "application/xml;q=0 4" }; - Assert.False(MediaTypeHeaderValue.TryParseStrictList(inputs, out var results)); - } - - [Theory] - [InlineData("*/*;", "*/*")] - [InlineData("text/*", "text/*")] - [InlineData("text/*", "text/plain")] - [InlineData("*/*;", "text/plain")] - [InlineData("text/plain", "text/plain")] - [InlineData("text/plain;", "text/plain")] - [InlineData("text/plain;", "TEXT/PLAIN")] - public void MatchesMediaType_PositiveCases(string mediaType1, string mediaType2) - { - // Arrange - var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1); - var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2); - - // Act - var matches = parsedMediaType1.MatchesMediaType(mediaType2); - var isSubsetOf = parsedMediaType2.IsSubsetOf(parsedMediaType1); - - // Assert - Assert.True(matches); - //Make sure that MatchesMediaType produces consistent result with IsSubsetOf - Assert.Equal(matches, isSubsetOf); - } - - [Theory] - [InlineData("application/html", "text/*")] - [InlineData("application/json", "application/html")] - [InlineData("text/plain;", "*/*")] - public void MatchesMediaType_NegativeCases(string mediaType1, string mediaType2) - { - // Arrange - var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1); - var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2); - - // Act - var matches = parsedMediaType1.MatchesMediaType(mediaType2); - var isSubsetOf = parsedMediaType2.IsSubsetOf(parsedMediaType1); - - // Assert - Assert.False(matches); - //Make sure that MatchesMediaType produces consistent result with IsSubsetOf - Assert.Equal(matches, isSubsetOf); - } - - [Theory] - [InlineData("application/entity+json", "application/entity+json")] - [InlineData("application/json", "application/entity+json")] - [InlineData("application/*+json", "application/entity+json")] - [InlineData("application/*+json", "application/*+json")] - [InlineData("application/json", "application/problem+json")] - [InlineData("application/json", "application/vnd.restful+json")] - [InlineData("application/*", "application/*+JSON")] - [InlineData("application/*", "application/entity+JSON")] - [InlineData("*/*", "application/entity+json")] - public void MatchesMediaTypeWithSuffixes_PositiveCases(string mediaType1, string mediaType2) - { - // Arrange - var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1); - var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2); - - // Act - var result = parsedMediaType1.MatchesMediaType(mediaType2); - var isSubsetOf = parsedMediaType2.IsSubsetOf(parsedMediaType1); - - // Assert - Assert.True(result); - //Make sure that MatchesMediaType produces consistent result with IsSubsetOf - Assert.Equal(result, isSubsetOf); - } - - [Theory] - [InlineData("application/entity+json", "application/entity+txt")] - [InlineData("application/entity+json", "application/json")] - [InlineData("application/entity+json", "application/entity.v2+json")] - [InlineData("application/*+json", "application/entity+txt")] - [InlineData("application/*+*", "application/json")] - [InlineData("application/entity", "application/entity+")] - [InlineData("application/entity+*", "application/entity+json")] // We don't allow suffixes to be wildcards - [InlineData("application/*+*", "application/entity+json")] // We don't allow suffixes to be wildcards - [InlineData("application/entity+json", "application/entity")] - public void MatchesMediaTypeWithSuffixes_NegativeCases(string mediaType1, string mediaType2) - { - // Arrange - var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1); - var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2); - - // Arrange - var result = parsedMediaType1.MatchesMediaType(mediaType2); - var isSubsetOf = parsedMediaType2.IsSubsetOf(parsedMediaType1); - - // Assert - Assert.False(result); - //Make sure that MatchesMediaType produces consistent result with IsSubsetOf - Assert.Equal(result, isSubsetOf); - } - - [Fact] - public void MatchesMediaType_IgnoresParameters() - { - // Arrange - var parsedMediaType1 = MediaTypeHeaderValue.Parse("application/json;param=1"); - - // Arrange - var result = parsedMediaType1.MatchesMediaType("application/json;param2=1"); - - // Assert - Assert.True(result); - } - - [Theory] - [InlineData("*/*;", "*/*")] - [InlineData("text/*", "text/*")] - [InlineData("text/*;", "*/*")] - [InlineData("text/plain;", "text/plain")] - [InlineData("text/plain", "text/*")] - [InlineData("text/plain;", "*/*")] - [InlineData("*/*;missingparam=4", "*/*")] - [InlineData("text/*;missingparam=4;", "*/*;")] - [InlineData("text/plain;missingparam=4", "*/*;")] - [InlineData("text/plain;missingparam=4", "text/*")] - [InlineData("text/plain;charset=utf-8", "text/plain;charset=utf-8")] - [InlineData("text/plain;version=v1", "Text/plain;Version=v1")] - [InlineData("text/plain;version=v1", "tExT/plain;version=V1")] - [InlineData("text/plain;version=v1", "TEXT/PLAIN;VERSION=V1")] - [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/plain;charset=utf-8;foo=bar;q=0.0")] - [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/plain;foo=bar;q=0.0;charset=utf-8")] // different order of parameters - [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/*;charset=utf-8;foo=bar;q=0.0")] - [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "*/*;charset=utf-8;foo=bar;q=0.0")] - [InlineData("application/json;v=2", "application/json;*")] - [InlineData("application/json;v=2;charset=utf-8", "application/json;v=2;*")] - public void IsSubsetOf_PositiveCases(string mediaType1, string mediaType2) - { - // Arrange - var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1); - var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2); - - // Act - var isSubset = parsedMediaType1.IsSubsetOf(parsedMediaType2); - - // Assert - Assert.True(isSubset); - } - - [Theory] - [InlineData("application/html", "text/*")] - [InlineData("application/json", "application/html")] - [InlineData("text/plain;version=v1", "text/plain;version=")] - [InlineData("*/*;", "text/plain;charset=utf-8;foo=bar;q=0.0")] - [InlineData("text/*;", "text/plain;charset=utf-8;foo=bar;q=0.0")] - [InlineData("text/*;charset=utf-8;foo=bar;q=0.0", "text/plain;missingparam=4;")] - [InlineData("*/*;charset=utf-8;foo=bar;q=0.0", "text/plain;missingparam=4;")] - [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/plain;missingparam=4;")] - [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/*;missingparam=4;")] - [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "*/*;missingparam=4;")] - public void IsSubsetOf_NegativeCases(string mediaType1, string mediaType2) - { - // Arrange - var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1); - var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2); - - // Act - var isSubset = parsedMediaType1.IsSubsetOf(parsedMediaType2); - - // Assert - Assert.False(isSubset); - } - - [Theory] - [InlineData("application/entity+json", "application/entity+json")] - [InlineData("application/*+json", "application/entity+json")] - [InlineData("application/*+json", "application/*+json")] - [InlineData("application/json", "application/problem+json")] - [InlineData("application/json", "application/vnd.restful+json")] - [InlineData("application/*", "application/*+JSON")] - [InlineData("application/vnd.github+json", "application/vnd.github+json")] - [InlineData("application/*", "application/entity+JSON")] - [InlineData("*/*", "application/entity+json")] - public void IsSubsetOfWithSuffixes_PositiveCases(string set, string subset) - { - // Arrange - var setMediaType = MediaTypeHeaderValue.Parse(set); - var subSetMediaType = MediaTypeHeaderValue.Parse(subset); - - // Act - var result = subSetMediaType.IsSubsetOf(setMediaType); - - // Assert - Assert.True(result); - } - - [Theory] - [InlineData("application/entity+json", "application/entity+txt")] - [InlineData("application/entity+json", "application/entity.v2+json")] - [InlineData("application/*+json", "application/entity+txt")] - [InlineData("application/*+*", "application/json")] - [InlineData("application/entity+*", "application/entity+json")] // We don't allow suffixes to be wildcards - [InlineData("application/*+*", "application/entity+json")] // We don't allow suffixes to be wildcards - [InlineData("application/entity+json", "application/entity")] - public void IsSubSetOfWithSuffixes_NegativeCases(string set, string subset) - { - // Arrange - var setMediaType = MediaTypeHeaderValue.Parse(set); - var subSetMediaType = MediaTypeHeaderValue.Parse(subset); + Assert.False(MediaTypeHeaderValue.TryParseStrictList(inputs, out var results)); + } + + [Theory] + [InlineData("*/*;", "*/*")] + [InlineData("text/*", "text/*")] + [InlineData("text/*", "text/plain")] + [InlineData("*/*;", "text/plain")] + [InlineData("text/plain", "text/plain")] + [InlineData("text/plain;", "text/plain")] + [InlineData("text/plain;", "TEXT/PLAIN")] + public void MatchesMediaType_PositiveCases(string mediaType1, string mediaType2) + { + // Arrange + var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1); + var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2); + + // Act + var matches = parsedMediaType1.MatchesMediaType(mediaType2); + var isSubsetOf = parsedMediaType2.IsSubsetOf(parsedMediaType1); + + // Assert + Assert.True(matches); + //Make sure that MatchesMediaType produces consistent result with IsSubsetOf + Assert.Equal(matches, isSubsetOf); + } + + [Theory] + [InlineData("application/html", "text/*")] + [InlineData("application/json", "application/html")] + [InlineData("text/plain;", "*/*")] + public void MatchesMediaType_NegativeCases(string mediaType1, string mediaType2) + { + // Arrange + var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1); + var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2); + + // Act + var matches = parsedMediaType1.MatchesMediaType(mediaType2); + var isSubsetOf = parsedMediaType2.IsSubsetOf(parsedMediaType1); + + // Assert + Assert.False(matches); + //Make sure that MatchesMediaType produces consistent result with IsSubsetOf + Assert.Equal(matches, isSubsetOf); + } + + [Theory] + [InlineData("application/entity+json", "application/entity+json")] + [InlineData("application/json", "application/entity+json")] + [InlineData("application/*+json", "application/entity+json")] + [InlineData("application/*+json", "application/*+json")] + [InlineData("application/json", "application/problem+json")] + [InlineData("application/json", "application/vnd.restful+json")] + [InlineData("application/*", "application/*+JSON")] + [InlineData("application/*", "application/entity+JSON")] + [InlineData("*/*", "application/entity+json")] + public void MatchesMediaTypeWithSuffixes_PositiveCases(string mediaType1, string mediaType2) + { + // Arrange + var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1); + var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2); + + // Act + var result = parsedMediaType1.MatchesMediaType(mediaType2); + var isSubsetOf = parsedMediaType2.IsSubsetOf(parsedMediaType1); + + // Assert + Assert.True(result); + //Make sure that MatchesMediaType produces consistent result with IsSubsetOf + Assert.Equal(result, isSubsetOf); + } + + [Theory] + [InlineData("application/entity+json", "application/entity+txt")] + [InlineData("application/entity+json", "application/json")] + [InlineData("application/entity+json", "application/entity.v2+json")] + [InlineData("application/*+json", "application/entity+txt")] + [InlineData("application/*+*", "application/json")] + [InlineData("application/entity", "application/entity+")] + [InlineData("application/entity+*", "application/entity+json")] // We don't allow suffixes to be wildcards + [InlineData("application/*+*", "application/entity+json")] // We don't allow suffixes to be wildcards + [InlineData("application/entity+json", "application/entity")] + public void MatchesMediaTypeWithSuffixes_NegativeCases(string mediaType1, string mediaType2) + { + // Arrange + var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1); + var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2); + + // Arrange + var result = parsedMediaType1.MatchesMediaType(mediaType2); + var isSubsetOf = parsedMediaType2.IsSubsetOf(parsedMediaType1); + + // Assert + Assert.False(result); + //Make sure that MatchesMediaType produces consistent result with IsSubsetOf + Assert.Equal(result, isSubsetOf); + } + + [Fact] + public void MatchesMediaType_IgnoresParameters() + { + // Arrange + var parsedMediaType1 = MediaTypeHeaderValue.Parse("application/json;param=1"); + + // Arrange + var result = parsedMediaType1.MatchesMediaType("application/json;param2=1"); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("*/*;", "*/*")] + [InlineData("text/*", "text/*")] + [InlineData("text/*;", "*/*")] + [InlineData("text/plain;", "text/plain")] + [InlineData("text/plain", "text/*")] + [InlineData("text/plain;", "*/*")] + [InlineData("*/*;missingparam=4", "*/*")] + [InlineData("text/*;missingparam=4;", "*/*;")] + [InlineData("text/plain;missingparam=4", "*/*;")] + [InlineData("text/plain;missingparam=4", "text/*")] + [InlineData("text/plain;charset=utf-8", "text/plain;charset=utf-8")] + [InlineData("text/plain;version=v1", "Text/plain;Version=v1")] + [InlineData("text/plain;version=v1", "tExT/plain;version=V1")] + [InlineData("text/plain;version=v1", "TEXT/PLAIN;VERSION=V1")] + [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/plain;charset=utf-8;foo=bar;q=0.0")] + [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/plain;foo=bar;q=0.0;charset=utf-8")] // different order of parameters + [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/*;charset=utf-8;foo=bar;q=0.0")] + [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "*/*;charset=utf-8;foo=bar;q=0.0")] + [InlineData("application/json;v=2", "application/json;*")] + [InlineData("application/json;v=2;charset=utf-8", "application/json;v=2;*")] + public void IsSubsetOf_PositiveCases(string mediaType1, string mediaType2) + { + // Arrange + var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1); + var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2); + + // Act + var isSubset = parsedMediaType1.IsSubsetOf(parsedMediaType2); + + // Assert + Assert.True(isSubset); + } + + [Theory] + [InlineData("application/html", "text/*")] + [InlineData("application/json", "application/html")] + [InlineData("text/plain;version=v1", "text/plain;version=")] + [InlineData("*/*;", "text/plain;charset=utf-8;foo=bar;q=0.0")] + [InlineData("text/*;", "text/plain;charset=utf-8;foo=bar;q=0.0")] + [InlineData("text/*;charset=utf-8;foo=bar;q=0.0", "text/plain;missingparam=4;")] + [InlineData("*/*;charset=utf-8;foo=bar;q=0.0", "text/plain;missingparam=4;")] + [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/plain;missingparam=4;")] + [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "text/*;missingparam=4;")] + [InlineData("text/plain;charset=utf-8;foo=bar;q=0.0", "*/*;missingparam=4;")] + public void IsSubsetOf_NegativeCases(string mediaType1, string mediaType2) + { + // Arrange + var parsedMediaType1 = MediaTypeHeaderValue.Parse(mediaType1); + var parsedMediaType2 = MediaTypeHeaderValue.Parse(mediaType2); + + // Act + var isSubset = parsedMediaType1.IsSubsetOf(parsedMediaType2); + + // Assert + Assert.False(isSubset); + } + + [Theory] + [InlineData("application/entity+json", "application/entity+json")] + [InlineData("application/*+json", "application/entity+json")] + [InlineData("application/*+json", "application/*+json")] + [InlineData("application/json", "application/problem+json")] + [InlineData("application/json", "application/vnd.restful+json")] + [InlineData("application/*", "application/*+JSON")] + [InlineData("application/vnd.github+json", "application/vnd.github+json")] + [InlineData("application/*", "application/entity+JSON")] + [InlineData("*/*", "application/entity+json")] + public void IsSubsetOfWithSuffixes_PositiveCases(string set, string subset) + { + // Arrange + var setMediaType = MediaTypeHeaderValue.Parse(set); + var subSetMediaType = MediaTypeHeaderValue.Parse(subset); + + // Act + var result = subSetMediaType.IsSubsetOf(setMediaType); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("application/entity+json", "application/entity+txt")] + [InlineData("application/entity+json", "application/entity.v2+json")] + [InlineData("application/*+json", "application/entity+txt")] + [InlineData("application/*+*", "application/json")] + [InlineData("application/entity+*", "application/entity+json")] // We don't allow suffixes to be wildcards + [InlineData("application/*+*", "application/entity+json")] // We don't allow suffixes to be wildcards + [InlineData("application/entity+json", "application/entity")] + public void IsSubSetOfWithSuffixes_NegativeCases(string set, string subset) + { + // Arrange + var setMediaType = MediaTypeHeaderValue.Parse(set); + var subSetMediaType = MediaTypeHeaderValue.Parse(subset); - // Act - var result = subSetMediaType.IsSubsetOf(setMediaType); + // Act + var result = subSetMediaType.IsSubsetOf(setMediaType); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - public static TheoryData> MediaTypesWithFacets => - new TheoryData> - { + public static TheoryData> MediaTypesWithFacets => + new TheoryData> + { { "application/vdn.github", new List(){ "vdn", "github" } }, { "application/vdn.github+json", @@ -906,48 +906,47 @@ namespace Microsoft.Net.Http.Headers new List(){ "vdn", "github", "v3" } }, { "application/vdn.github.+json", new List(){ "vdn", "github", "" } }, - }; + }; - [Theory] - [MemberData(nameof(MediaTypesWithFacets))] - public void Facets_TestPositiveCases(string input, List expected) - { - // Arrange - var mediaType = MediaTypeHeaderValue.Parse(input); + [Theory] + [MemberData(nameof(MediaTypesWithFacets))] + public void Facets_TestPositiveCases(string input, List expected) + { + // Arrange + var mediaType = MediaTypeHeaderValue.Parse(input); - // Act - var result = mediaType.Facets; + // Act + var result = mediaType.Facets; - // Assert - Assert.Equal(expected, result); - } + // Assert + Assert.Equal(expected, result); + } - private void CheckValidParse(string? input, MediaTypeHeaderValue expectedResult) - { - var result = MediaTypeHeaderValue.Parse(input); - Assert.Equal(expectedResult, result); - } + private void CheckValidParse(string? input, MediaTypeHeaderValue expectedResult) + { + var result = MediaTypeHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); + } - private void CheckInvalidParse(string? input) - { - Assert.Throws(() => MediaTypeHeaderValue.Parse(input)); - } + private void CheckInvalidParse(string? input) + { + Assert.Throws(() => MediaTypeHeaderValue.Parse(input)); + } - private void CheckValidTryParse(string? input, MediaTypeHeaderValue expectedResult) - { - Assert.True(MediaTypeHeaderValue.TryParse(input, out var result)); - Assert.Equal(expectedResult, result); - } + private void CheckValidTryParse(string? input, MediaTypeHeaderValue expectedResult) + { + Assert.True(MediaTypeHeaderValue.TryParse(input, out var result)); + Assert.Equal(expectedResult, result); + } - private void CheckInvalidTryParse(string? input) - { - Assert.False(MediaTypeHeaderValue.TryParse(input, out var result)); - Assert.Null(result); - } + private void CheckInvalidTryParse(string? input) + { + Assert.False(MediaTypeHeaderValue.TryParse(input, out var result)); + Assert.Null(result); + } - private static void AssertFormatException(string mediaType) - { - Assert.Throws(() => new MediaTypeHeaderValue(mediaType)); - } + private static void AssertFormatException(string mediaType) + { + Assert.Throws(() => new MediaTypeHeaderValue(mediaType)); } } diff --git a/src/Http/Headers/test/NameValueHeaderValueTest.cs b/src/Http/Headers/test/NameValueHeaderValueTest.cs index d12de484a3..fb4d857eee 100644 --- a/src/Http/Headers/test/NameValueHeaderValueTest.cs +++ b/src/Http/Headers/test/NameValueHeaderValueTest.cs @@ -6,336 +6,336 @@ using System.Collections.Generic; using System.Linq; using Xunit; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +public class NameValueHeaderValueTest { - public class NameValueHeaderValueTest + [Fact] + public void Ctor_NameNull_Throw() { - [Fact] - public void Ctor_NameNull_Throw() - { - Assert.Throws(() => new NameValueHeaderValue(null)); - // null and empty should be treated the same. So we also throw for empty strings. - Assert.Throws(() => new NameValueHeaderValue(string.Empty)); - } + Assert.Throws(() => new NameValueHeaderValue(null)); + // null and empty should be treated the same. So we also throw for empty strings. + Assert.Throws(() => new NameValueHeaderValue(string.Empty)); + } - [Fact] - public void Ctor_NameInvalidFormat_ThrowFormatException() - { - // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. - AssertFormatException(" text ", null); - AssertFormatException("text ", null); - AssertFormatException(" text", null); - AssertFormatException("te xt", null); - AssertFormatException("te=xt", null); // The ctor takes a name which must not contain '='. - AssertFormatException("teäxt", null); - } - - [Fact] - public void Ctor_NameValidFormat_SuccessfullyCreated() - { - var nameValue = new NameValueHeaderValue("text", null); - Assert.Equal("text", nameValue.Name); - } + [Fact] + public void Ctor_NameInvalidFormat_ThrowFormatException() + { + // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. + AssertFormatException(" text ", null); + AssertFormatException("text ", null); + AssertFormatException(" text", null); + AssertFormatException("te xt", null); + AssertFormatException("te=xt", null); // The ctor takes a name which must not contain '='. + AssertFormatException("teäxt", null); + } - [Fact] - public void Ctor_ValueInvalidFormat_ThrowFormatException() - { - // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. - AssertFormatException("text", " token "); - AssertFormatException("text", "token "); - AssertFormatException("text", " token"); - AssertFormatException("text", "token string"); - AssertFormatException("text", "\"quoted string with \" quotes\""); - AssertFormatException("text", "\"quoted string with \"two\" quotes\""); - } - - [Fact] - public void Ctor_ValueValidFormat_SuccessfullyCreated() - { - CheckValue(null); - CheckValue(string.Empty); - CheckValue("token_string"); - CheckValue("\"quoted string\""); - CheckValue("\"quoted string with quoted \\\" quote-pair\""); - } - - [Fact] - public void Copy_NameOnly_SuccessfullyCopied() - { - var pair0 = new NameValueHeaderValue("name"); - var pair1 = pair0.Copy(); - Assert.NotSame(pair0, pair1); - Assert.Same(pair0.Name.Value, pair1.Name.Value); - Assert.Null(pair0.Value.Value); - Assert.Null(pair1.Value.Value); - - // Change one value and verify the other is unchanged. - pair0.Value = "othervalue"; - Assert.Equal("othervalue", pair0.Value); - Assert.Null(pair1.Value.Value); - } - - [Fact] - public void CopyAsReadOnly_NameOnly_CopiedAndReadOnly() - { - var pair0 = new NameValueHeaderValue("name"); - var pair1 = pair0.CopyAsReadOnly(); - Assert.NotSame(pair0, pair1); - Assert.Same(pair0.Name.Value, pair1.Name.Value); - Assert.Null(pair0.Value.Value); - Assert.Null(pair1.Value.Value); - Assert.False(pair0.IsReadOnly); - Assert.True(pair1.IsReadOnly); - - // Change one value and verify the other is unchanged. - pair0.Value = "othervalue"; - Assert.Equal("othervalue", pair0.Value); - Assert.Null(pair1.Value.Value); - Assert.Throws(() => { pair1.Value = "othervalue"; }); - } - - [Fact] - public void Copy_NameAndValue_SuccessfullyCopied() - { - var pair0 = new NameValueHeaderValue("name", "value"); - var pair1 = pair0.Copy(); - Assert.NotSame(pair0, pair1); - Assert.Same(pair0.Name.Value, pair1.Name.Value); - Assert.Same(pair0.Value.Value, pair1.Value.Value); - - // Change one value and verify the other is unchanged. - pair0.Value = "othervalue"; - Assert.Equal("othervalue", pair0.Value); - Assert.Equal("value", pair1.Value); - } - - [Fact] - public void CopyAsReadOnly_NameAndValue_CopiedAndReadOnly() - { - var pair0 = new NameValueHeaderValue("name", "value"); - var pair1 = pair0.CopyAsReadOnly(); - Assert.NotSame(pair0, pair1); - Assert.Same(pair0.Name.Value, pair1.Name.Value); - Assert.Same(pair0.Value.Value, pair1.Value.Value); - Assert.False(pair0.IsReadOnly); - Assert.True(pair1.IsReadOnly); - - // Change one value and verify the other is unchanged. - pair0.Value = "othervalue"; - Assert.Equal("othervalue", pair0.Value); - Assert.Equal("value", pair1.Value); - Assert.Throws(() => { pair1.Value = "othervalue"; }); - } - - [Fact] - public void CopyFromReadOnly_NameAndValue_CopiedAsNonReadOnly() - { - var pair0 = new NameValueHeaderValue("name", "value"); - var pair1 = pair0.CopyAsReadOnly(); - var pair2 = pair1.Copy(); - Assert.NotSame(pair0, pair1); - Assert.Same(pair0.Name.Value, pair1.Name.Value); - Assert.Same(pair0.Value.Value, pair1.Value.Value); - - // Change one value and verify the other is unchanged. - pair2.Value = "othervalue"; - Assert.Equal("othervalue", pair2.Value); - Assert.Equal("value", pair1.Value); - } - - [Fact] - public void Value_CallSetterWithInvalidValues_Throw() - { - // Just verify that the setter calls the same validation the ctor invokes. - Assert.Throws(() => { var x = new NameValueHeaderValue("name"); x.Value = " x "; }); - Assert.Throws(() => { var x = new NameValueHeaderValue("name"); x.Value = "x y"; }); - } + [Fact] + public void Ctor_NameValidFormat_SuccessfullyCreated() + { + var nameValue = new NameValueHeaderValue("text", null); + Assert.Equal("text", nameValue.Name); + } - [Fact] - public void ToString_UseNoValueAndTokenAndQuotedStringValues_SerializedCorrectly() - { - var nameValue = new NameValueHeaderValue("text", "token"); - Assert.Equal("text=token", nameValue.ToString()); + [Fact] + public void Ctor_ValueInvalidFormat_ThrowFormatException() + { + // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. + AssertFormatException("text", " token "); + AssertFormatException("text", "token "); + AssertFormatException("text", " token"); + AssertFormatException("text", "token string"); + AssertFormatException("text", "\"quoted string with \" quotes\""); + AssertFormatException("text", "\"quoted string with \"two\" quotes\""); + } + + [Fact] + public void Ctor_ValueValidFormat_SuccessfullyCreated() + { + CheckValue(null); + CheckValue(string.Empty); + CheckValue("token_string"); + CheckValue("\"quoted string\""); + CheckValue("\"quoted string with quoted \\\" quote-pair\""); + } + + [Fact] + public void Copy_NameOnly_SuccessfullyCopied() + { + var pair0 = new NameValueHeaderValue("name"); + var pair1 = pair0.Copy(); + Assert.NotSame(pair0, pair1); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Null(pair0.Value.Value); + Assert.Null(pair1.Value.Value); + + // Change one value and verify the other is unchanged. + pair0.Value = "othervalue"; + Assert.Equal("othervalue", pair0.Value); + Assert.Null(pair1.Value.Value); + } - nameValue.Value = "\"quoted string\""; - Assert.Equal("text=\"quoted string\"", nameValue.ToString()); + [Fact] + public void CopyAsReadOnly_NameOnly_CopiedAndReadOnly() + { + var pair0 = new NameValueHeaderValue("name"); + var pair1 = pair0.CopyAsReadOnly(); + Assert.NotSame(pair0, pair1); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Null(pair0.Value.Value); + Assert.Null(pair1.Value.Value); + Assert.False(pair0.IsReadOnly); + Assert.True(pair1.IsReadOnly); + + // Change one value and verify the other is unchanged. + pair0.Value = "othervalue"; + Assert.Equal("othervalue", pair0.Value); + Assert.Null(pair1.Value.Value); + Assert.Throws(() => { pair1.Value = "othervalue"; }); + } - nameValue.Value = null; - Assert.Equal("text", nameValue.ToString()); + [Fact] + public void Copy_NameAndValue_SuccessfullyCopied() + { + var pair0 = new NameValueHeaderValue("name", "value"); + var pair1 = pair0.Copy(); + Assert.NotSame(pair0, pair1); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Same(pair0.Value.Value, pair1.Value.Value); + + // Change one value and verify the other is unchanged. + pair0.Value = "othervalue"; + Assert.Equal("othervalue", pair0.Value); + Assert.Equal("value", pair1.Value); + } - nameValue.Value = string.Empty; - Assert.Equal("text", nameValue.ToString()); - } + [Fact] + public void CopyAsReadOnly_NameAndValue_CopiedAndReadOnly() + { + var pair0 = new NameValueHeaderValue("name", "value"); + var pair1 = pair0.CopyAsReadOnly(); + Assert.NotSame(pair0, pair1); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Same(pair0.Value.Value, pair1.Value.Value); + Assert.False(pair0.IsReadOnly); + Assert.True(pair1.IsReadOnly); + + // Change one value and verify the other is unchanged. + pair0.Value = "othervalue"; + Assert.Equal("othervalue", pair0.Value); + Assert.Equal("value", pair1.Value); + Assert.Throws(() => { pair1.Value = "othervalue"; }); + } - [Fact] - public void GetHashCode_ValuesUseDifferentValues_HashDiffersAccordingToRfc() - { - var nameValue1 = new NameValueHeaderValue("text"); - var nameValue2 = new NameValueHeaderValue("text"); + [Fact] + public void CopyFromReadOnly_NameAndValue_CopiedAsNonReadOnly() + { + var pair0 = new NameValueHeaderValue("name", "value"); + var pair1 = pair0.CopyAsReadOnly(); + var pair2 = pair1.Copy(); + Assert.NotSame(pair0, pair1); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Same(pair0.Value.Value, pair1.Value.Value); + + // Change one value and verify the other is unchanged. + pair2.Value = "othervalue"; + Assert.Equal("othervalue", pair2.Value); + Assert.Equal("value", pair1.Value); + } - nameValue1.Value = null; - nameValue2.Value = null; - Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + [Fact] + public void Value_CallSetterWithInvalidValues_Throw() + { + // Just verify that the setter calls the same validation the ctor invokes. + Assert.Throws(() => { var x = new NameValueHeaderValue("name"); x.Value = " x "; }); + Assert.Throws(() => { var x = new NameValueHeaderValue("name"); x.Value = "x y"; }); + } - nameValue1.Value = "token"; - nameValue2.Value = null; - Assert.NotEqual(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + [Fact] + public void ToString_UseNoValueAndTokenAndQuotedStringValues_SerializedCorrectly() + { + var nameValue = new NameValueHeaderValue("text", "token"); + Assert.Equal("text=token", nameValue.ToString()); - nameValue1.Value = "token"; - nameValue2.Value = string.Empty; - Assert.NotEqual(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + nameValue.Value = "\"quoted string\""; + Assert.Equal("text=\"quoted string\"", nameValue.ToString()); - nameValue1.Value = null; - nameValue2.Value = string.Empty; - Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + nameValue.Value = null; + Assert.Equal("text", nameValue.ToString()); - nameValue1.Value = "token"; - nameValue2.Value = "TOKEN"; - Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + nameValue.Value = string.Empty; + Assert.Equal("text", nameValue.ToString()); + } - nameValue1.Value = "token"; - nameValue2.Value = "token"; - Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + [Fact] + public void GetHashCode_ValuesUseDifferentValues_HashDiffersAccordingToRfc() + { + var nameValue1 = new NameValueHeaderValue("text"); + var nameValue2 = new NameValueHeaderValue("text"); - nameValue1.Value = "\"quoted string\""; - nameValue2.Value = "\"QUOTED STRING\""; - Assert.NotEqual(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + nameValue1.Value = null; + nameValue2.Value = null; + Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); - nameValue1.Value = "\"quoted string\""; - nameValue2.Value = "\"quoted string\""; - Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); - } + nameValue1.Value = "token"; + nameValue2.Value = null; + Assert.NotEqual(nameValue1.GetHashCode(), nameValue2.GetHashCode()); - [Fact] - public void GetHashCode_NameUseDifferentCasing_HashDiffersAccordingToRfc() - { - var nameValue1 = new NameValueHeaderValue("text"); - var nameValue2 = new NameValueHeaderValue("TEXT"); - Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); - } + nameValue1.Value = "token"; + nameValue2.Value = string.Empty; + Assert.NotEqual(nameValue1.GetHashCode(), nameValue2.GetHashCode()); - [Fact] - public void Equals_ValuesUseDifferentValues_ValuesAreEqualOrDifferentAccordingToRfc() - { - var nameValue1 = new NameValueHeaderValue("text"); - var nameValue2 = new NameValueHeaderValue("text"); + nameValue1.Value = null; + nameValue2.Value = string.Empty; + Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); - nameValue1.Value = null; - nameValue2.Value = null; - Assert.True(nameValue1.Equals(nameValue2), " vs. ."); + nameValue1.Value = "token"; + nameValue2.Value = "TOKEN"; + Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); - nameValue1.Value = "token"; - nameValue2.Value = null; - Assert.False(nameValue1.Equals(nameValue2), "token vs. ."); + nameValue1.Value = "token"; + nameValue2.Value = "token"; + Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); - nameValue1.Value = null; - nameValue2.Value = "token"; - Assert.False(nameValue1.Equals(nameValue2), " vs. token."); + nameValue1.Value = "\"quoted string\""; + nameValue2.Value = "\"QUOTED STRING\""; + Assert.NotEqual(nameValue1.GetHashCode(), nameValue2.GetHashCode()); - nameValue1.Value = string.Empty; - nameValue2.Value = "token"; - Assert.False(nameValue1.Equals(nameValue2), "string.Empty vs. token."); + nameValue1.Value = "\"quoted string\""; + nameValue2.Value = "\"quoted string\""; + Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + } - nameValue1.Value = null; - nameValue2.Value = string.Empty; - Assert.True(nameValue1.Equals(nameValue2), " vs. string.Empty."); + [Fact] + public void GetHashCode_NameUseDifferentCasing_HashDiffersAccordingToRfc() + { + var nameValue1 = new NameValueHeaderValue("text"); + var nameValue2 = new NameValueHeaderValue("TEXT"); + Assert.Equal(nameValue1.GetHashCode(), nameValue2.GetHashCode()); + } - nameValue1.Value = "token"; - nameValue2.Value = "TOKEN"; - Assert.True(nameValue1.Equals(nameValue2), "token vs. TOKEN."); + [Fact] + public void Equals_ValuesUseDifferentValues_ValuesAreEqualOrDifferentAccordingToRfc() + { + var nameValue1 = new NameValueHeaderValue("text"); + var nameValue2 = new NameValueHeaderValue("text"); - nameValue1.Value = "token"; - nameValue2.Value = "token"; - Assert.True(nameValue1.Equals(nameValue2), "token vs. token."); + nameValue1.Value = null; + nameValue2.Value = null; + Assert.True(nameValue1.Equals(nameValue2), " vs. ."); - nameValue1.Value = "\"quoted string\""; - nameValue2.Value = "\"QUOTED STRING\""; - Assert.False(nameValue1.Equals(nameValue2), "\"quoted string\" vs. \"QUOTED STRING\"."); + nameValue1.Value = "token"; + nameValue2.Value = null; + Assert.False(nameValue1.Equals(nameValue2), "token vs. ."); - nameValue1.Value = "\"quoted string\""; - nameValue2.Value = "\"quoted string\""; - Assert.True(nameValue1.Equals(nameValue2), "\"quoted string\" vs. \"quoted string\"."); + nameValue1.Value = null; + nameValue2.Value = "token"; + Assert.False(nameValue1.Equals(nameValue2), " vs. token."); - Assert.False(nameValue1.Equals(null), "\"quoted string\" vs. ."); - } + nameValue1.Value = string.Empty; + nameValue2.Value = "token"; + Assert.False(nameValue1.Equals(nameValue2), "string.Empty vs. token."); - [Fact] - public void Equals_NameUseDifferentCasing_ConsideredEqual() - { - var nameValue1 = new NameValueHeaderValue("text"); - var nameValue2 = new NameValueHeaderValue("TEXT"); - Assert.True(nameValue1.Equals(nameValue2), "text vs. TEXT."); - } + nameValue1.Value = null; + nameValue2.Value = string.Empty; + Assert.True(nameValue1.Equals(nameValue2), " vs. string.Empty."); - [Fact] - public void Parse_SetOfValidValueStrings_ParsedCorrectly() - { - CheckValidParse(" name = value ", new NameValueHeaderValue("name", "value")); - CheckValidParse(" name", new NameValueHeaderValue("name")); - CheckValidParse(" name ", new NameValueHeaderValue("name")); - CheckValidParse(" name=\"value\"", new NameValueHeaderValue("name", "\"value\"")); - CheckValidParse("name=value", new NameValueHeaderValue("name", "value")); - CheckValidParse("name=\"quoted str\"", new NameValueHeaderValue("name", "\"quoted str\"")); - CheckValidParse("name\t =va1ue", new NameValueHeaderValue("name", "va1ue")); - CheckValidParse("name= va*ue ", new NameValueHeaderValue("name", "va*ue")); - CheckValidParse("name=", new NameValueHeaderValue("name", "")); - } - - [Fact] - public void Parse_SetOfInvalidValueStrings_Throws() - { - CheckInvalidParse("name[value"); - CheckInvalidParse("name=value="); - CheckInvalidParse("name=会"); - CheckInvalidParse("name==value"); - CheckInvalidParse("name= va:ue"); - CheckInvalidParse("=value"); - CheckInvalidParse("name value"); - CheckInvalidParse("name=,value"); - CheckInvalidParse("会"); - CheckInvalidParse(null); - CheckInvalidParse(string.Empty); - CheckInvalidParse(" "); - CheckInvalidParse(" ,,"); - CheckInvalidParse(" , , name = value , "); - CheckInvalidParse(" name,"); - CheckInvalidParse(" ,name=\"value\""); - } - - [Fact] - public void TryParse_SetOfValidValueStrings_ParsedCorrectly() - { - CheckValidTryParse(" name = value ", new NameValueHeaderValue("name", "value")); - CheckValidTryParse(" name", new NameValueHeaderValue("name")); - CheckValidTryParse(" name=\"value\"", new NameValueHeaderValue("name", "\"value\"")); - CheckValidTryParse("name=value", new NameValueHeaderValue("name", "value")); - } - - [Fact] - public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() - { - CheckInvalidTryParse("name[value"); - CheckInvalidTryParse("name=value="); - CheckInvalidTryParse("name=会"); - CheckInvalidTryParse("name==value"); - CheckInvalidTryParse("=value"); - CheckInvalidTryParse("name value"); - CheckInvalidTryParse("name=,value"); - CheckInvalidTryParse("会"); - CheckInvalidTryParse(null); - CheckInvalidTryParse(string.Empty); - CheckInvalidTryParse(" "); - CheckInvalidTryParse(" ,,"); - CheckInvalidTryParse(" , , name = value , "); - CheckInvalidTryParse(" name,"); - CheckInvalidTryParse(" ,name=\"value\""); - } - - [Fact] - public void ParseList_SetOfValidValueStrings_ParsedCorrectly() + nameValue1.Value = "token"; + nameValue2.Value = "TOKEN"; + Assert.True(nameValue1.Equals(nameValue2), "token vs. TOKEN."); + + nameValue1.Value = "token"; + nameValue2.Value = "token"; + Assert.True(nameValue1.Equals(nameValue2), "token vs. token."); + + nameValue1.Value = "\"quoted string\""; + nameValue2.Value = "\"QUOTED STRING\""; + Assert.False(nameValue1.Equals(nameValue2), "\"quoted string\" vs. \"QUOTED STRING\"."); + + nameValue1.Value = "\"quoted string\""; + nameValue2.Value = "\"quoted string\""; + Assert.True(nameValue1.Equals(nameValue2), "\"quoted string\" vs. \"quoted string\"."); + + Assert.False(nameValue1.Equals(null), "\"quoted string\" vs. ."); + } + + [Fact] + public void Equals_NameUseDifferentCasing_ConsideredEqual() + { + var nameValue1 = new NameValueHeaderValue("text"); + var nameValue2 = new NameValueHeaderValue("TEXT"); + Assert.True(nameValue1.Equals(nameValue2), "text vs. TEXT."); + } + + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidParse(" name = value ", new NameValueHeaderValue("name", "value")); + CheckValidParse(" name", new NameValueHeaderValue("name")); + CheckValidParse(" name ", new NameValueHeaderValue("name")); + CheckValidParse(" name=\"value\"", new NameValueHeaderValue("name", "\"value\"")); + CheckValidParse("name=value", new NameValueHeaderValue("name", "value")); + CheckValidParse("name=\"quoted str\"", new NameValueHeaderValue("name", "\"quoted str\"")); + CheckValidParse("name\t =va1ue", new NameValueHeaderValue("name", "va1ue")); + CheckValidParse("name= va*ue ", new NameValueHeaderValue("name", "va*ue")); + CheckValidParse("name=", new NameValueHeaderValue("name", "")); + } + + [Fact] + public void Parse_SetOfInvalidValueStrings_Throws() + { + CheckInvalidParse("name[value"); + CheckInvalidParse("name=value="); + CheckInvalidParse("name=会"); + CheckInvalidParse("name==value"); + CheckInvalidParse("name= va:ue"); + CheckInvalidParse("=value"); + CheckInvalidParse("name value"); + CheckInvalidParse("name=,value"); + CheckInvalidParse("会"); + CheckInvalidParse(null); + CheckInvalidParse(string.Empty); + CheckInvalidParse(" "); + CheckInvalidParse(" ,,"); + CheckInvalidParse(" , , name = value , "); + CheckInvalidParse(" name,"); + CheckInvalidParse(" ,name=\"value\""); + } + + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidTryParse(" name = value ", new NameValueHeaderValue("name", "value")); + CheckValidTryParse(" name", new NameValueHeaderValue("name")); + CheckValidTryParse(" name=\"value\"", new NameValueHeaderValue("name", "\"value\"")); + CheckValidTryParse("name=value", new NameValueHeaderValue("name", "value")); + } + + [Fact] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() + { + CheckInvalidTryParse("name[value"); + CheckInvalidTryParse("name=value="); + CheckInvalidTryParse("name=会"); + CheckInvalidTryParse("name==value"); + CheckInvalidTryParse("=value"); + CheckInvalidTryParse("name value"); + CheckInvalidTryParse("name=,value"); + CheckInvalidTryParse("会"); + CheckInvalidTryParse(null); + CheckInvalidTryParse(string.Empty); + CheckInvalidTryParse(" "); + CheckInvalidTryParse(" ,,"); + CheckInvalidTryParse(" , , name = value , "); + CheckInvalidTryParse(" name,"); + CheckInvalidTryParse(" ,name=\"value\""); + } + + [Fact] + public void ParseList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] { - var inputs = new[] - { "", "name=value1", "", @@ -346,10 +346,10 @@ namespace Microsoft.Net.Http.Headers "name=value6,name=value7", "name=\"value 8\", name= \"value 9\"", }; - var results = NameValueHeaderValue.ParseList(inputs); + var results = NameValueHeaderValue.ParseList(inputs); - var expectedResults = new[] - { + var expectedResults = new[] + { new NameValueHeaderValue("name", "value1"), new NameValueHeaderValue("name", "value2"), new NameValueHeaderValue("name", "value3"), @@ -361,14 +361,14 @@ namespace Microsoft.Net.Http.Headers new NameValueHeaderValue("name", "\"value 9\""), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void ParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + [Fact] + public void ParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] { - var inputs = new[] - { "", "name=value1", "", @@ -379,10 +379,10 @@ namespace Microsoft.Net.Http.Headers "name=value6,name=value7", "name=\"value 8\", name= \"value 9\"", }; - var results = NameValueHeaderValue.ParseStrictList(inputs); + var results = NameValueHeaderValue.ParseStrictList(inputs); - var expectedResults = new[] - { + var expectedResults = new[] + { new NameValueHeaderValue("name", "value1"), new NameValueHeaderValue("name", "value2"), new NameValueHeaderValue("name", "value3"), @@ -394,14 +394,14 @@ namespace Microsoft.Net.Http.Headers new NameValueHeaderValue("name", "\"value 9\""), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void TryParseList_SetOfValidValueStrings_ParsedCorrectly() + [Fact] + public void TryParseList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] { - var inputs = new[] - { "", "name=value1", "", @@ -412,10 +412,10 @@ namespace Microsoft.Net.Http.Headers "name=value6,name=value7", "name=\"value 8\", name= \"value 9\"", }; - Assert.True(NameValueHeaderValue.TryParseList(inputs, out var results)); + Assert.True(NameValueHeaderValue.TryParseList(inputs, out var results)); - var expectedResults = new[] - { + var expectedResults = new[] + { new NameValueHeaderValue("name", "value1"), new NameValueHeaderValue("name", "value2"), new NameValueHeaderValue("name", "value3"), @@ -427,14 +427,14 @@ namespace Microsoft.Net.Http.Headers new NameValueHeaderValue("name", "\"value 9\""), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void TryParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + [Fact] + public void TryParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] { - var inputs = new[] - { "", "name=value1", "", @@ -445,10 +445,10 @@ namespace Microsoft.Net.Http.Headers "name=value6,name=value7", "name=\"value 8\", name= \"value 9\"", }; - Assert.True(NameValueHeaderValue.TryParseStrictList(inputs, out var results)); + Assert.True(NameValueHeaderValue.TryParseStrictList(inputs, out var results)); - var expectedResults = new[] - { + var expectedResults = new[] + { new NameValueHeaderValue("name", "value1"), new NameValueHeaderValue("name", "value2"), new NameValueHeaderValue("name", "value3"), @@ -460,14 +460,14 @@ namespace Microsoft.Net.Http.Headers new NameValueHeaderValue("name", "\"value 9\""), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void ParseList_WithSomeInvalidValues_ExcludesInvalidValues() + [Fact] + public void ParseList_WithSomeInvalidValues_ExcludesInvalidValues() + { + var inputs = new[] { - var inputs = new[] - { "", "name1=value1", "name2", @@ -478,10 +478,10 @@ namespace Microsoft.Net.Http.Headers "name8=value8,name9=value9", "name10=\"value 10\", name11= \"value 11\"", }; - var results = NameValueHeaderValue.ParseList(inputs); + var results = NameValueHeaderValue.ParseList(inputs); - var expectedResults = new[] - { + var expectedResults = new[] + { new NameValueHeaderValue("name1", "value1"), new NameValueHeaderValue("name2"), new NameValueHeaderValue("name3", "3"), @@ -496,14 +496,14 @@ namespace Microsoft.Net.Http.Headers new NameValueHeaderValue("name11", "\"value 11\""), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void ParseStrictList_WithSomeInvalidValues_Throws() + [Fact] + public void ParseStrictList_WithSomeInvalidValues_Throws() + { + var inputs = new[] { - var inputs = new[] - { "", "name1=value1", "name2", @@ -514,14 +514,14 @@ namespace Microsoft.Net.Http.Headers "name8=value8,name9=value9", "name10=\"value 10\", name11= \"value 11\"", }; - Assert.Throws(() => NameValueHeaderValue.ParseStrictList(inputs)); - } + Assert.Throws(() => NameValueHeaderValue.ParseStrictList(inputs)); + } - [Fact] - public void TryParseList_WithSomeInvalidValues_ExcludesInvalidValues() + [Fact] + public void TryParseList_WithSomeInvalidValues_ExcludesInvalidValues() + { + var inputs = new[] { - var inputs = new[] - { "", "name1=value1", "name2", @@ -532,10 +532,10 @@ namespace Microsoft.Net.Http.Headers "name8=value8,name9=value9", "name10=\"value 10\", name11= \"value 11\"", }; - Assert.True(NameValueHeaderValue.TryParseList(inputs, out var results)); + Assert.True(NameValueHeaderValue.TryParseList(inputs, out var results)); - var expectedResults = new[] - { + var expectedResults = new[] + { new NameValueHeaderValue("name1", "value1"), new NameValueHeaderValue("name2"), new NameValueHeaderValue("name3", "3"), @@ -550,14 +550,14 @@ namespace Microsoft.Net.Http.Headers new NameValueHeaderValue("name11", "\"value 11\""), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void TryParseStrictList_WithSomeInvalidValues_ReturnsFalse() + [Fact] + public void TryParseStrictList_WithSomeInvalidValues_ReturnsFalse() + { + var inputs = new[] { - var inputs = new[] - { "", "name1=value1", "name2", @@ -568,126 +568,125 @@ namespace Microsoft.Net.Http.Headers "name8=value8,name9=value9", "name10=\"value 10\", name11= \"value 11\"", }; - Assert.False(NameValueHeaderValue.TryParseStrictList(inputs, out var results)); - } - - [Theory] - [InlineData("value", "value")] - [InlineData("\"value\"", "value")] - [InlineData("\"hello\\\\\"", "hello\\")] - [InlineData("\"hello\\\"\"", "hello\"")] - [InlineData("\"hello\\\"foo\\\\bar\\\\baz\\\\\"", "hello\"foo\\bar\\baz\\")] - [InlineData("\"quoted value\"", "quoted value")] - [InlineData("\"quoted\\\"valuewithquote\"", "quoted\"valuewithquote")] - [InlineData("\"hello\\\"", "hello\\")] - public void GetUnescapedValue_ReturnsExpectedValue(string input, string expected) - { - var header = new NameValueHeaderValue("test", input); - - var actual = header.GetUnescapedValue(); - - Assert.Equal(expected, actual); - } - - [Theory] - [InlineData("value", "value")] - [InlineData("23", "23")] - [InlineData(";;;", "\";;;\"")] - [InlineData("\"value\"", "\"value\"")] - [InlineData("\"assumes already encoded \\\"\"", "\"assumes already encoded \\\"\"")] - [InlineData("unquoted \"value", "\"unquoted \\\"value\"")] - [InlineData("value\\morevalues\\evenmorevalues", "\"value\\\\morevalues\\\\evenmorevalues\"")] - // We have to assume that the input needs to be quoted here - [InlineData("\"\"double quoted string\"\"", "\"\\\"\\\"double quoted string\\\"\\\"\"")] - [InlineData("\t", "\"\t\"")] - public void SetAndEscapeValue_ReturnsExpectedValue(string input, string expected) - { - var header = new NameValueHeaderValue("test"); - header.SetAndEscapeValue(input); + Assert.False(NameValueHeaderValue.TryParseStrictList(inputs, out var results)); + } - var actual = header.Value; + [Theory] + [InlineData("value", "value")] + [InlineData("\"value\"", "value")] + [InlineData("\"hello\\\\\"", "hello\\")] + [InlineData("\"hello\\\"\"", "hello\"")] + [InlineData("\"hello\\\"foo\\\\bar\\\\baz\\\\\"", "hello\"foo\\bar\\baz\\")] + [InlineData("\"quoted value\"", "quoted value")] + [InlineData("\"quoted\\\"valuewithquote\"", "quoted\"valuewithquote")] + [InlineData("\"hello\\\"", "hello\\")] + public void GetUnescapedValue_ReturnsExpectedValue(string input, string expected) + { + var header = new NameValueHeaderValue("test", input); - Assert.Equal(expected, actual); - } + var actual = header.GetUnescapedValue(); + Assert.Equal(expected, actual); + } - [Theory] - [InlineData("\n")] - [InlineData("\b")] - [InlineData("\r")] - public void SetAndEscapeValue_ThrowsOnInvalidValues(string input) - { - var header = new NameValueHeaderValue("test"); - Assert.Throws(() => header.SetAndEscapeValue(input)); - } - - [Theory] - [InlineData("value")] - [InlineData("\"value\\\\morevalues\\\\evenmorevalues\"")] - [InlineData("\"quoted \\\"value\"")] - public void GetAndSetEncodeValueRoundTrip_ReturnsExpectedValue(string input) - { - var header = new NameValueHeaderValue("test"); - header.Value = input; - var valueHeader = header.GetUnescapedValue(); - header.SetAndEscapeValue(valueHeader); + [Theory] + [InlineData("value", "value")] + [InlineData("23", "23")] + [InlineData(";;;", "\";;;\"")] + [InlineData("\"value\"", "\"value\"")] + [InlineData("\"assumes already encoded \\\"\"", "\"assumes already encoded \\\"\"")] + [InlineData("unquoted \"value", "\"unquoted \\\"value\"")] + [InlineData("value\\morevalues\\evenmorevalues", "\"value\\\\morevalues\\\\evenmorevalues\"")] + // We have to assume that the input needs to be quoted here + [InlineData("\"\"double quoted string\"\"", "\"\\\"\\\"double quoted string\\\"\\\"\"")] + [InlineData("\t", "\"\t\"")] + public void SetAndEscapeValue_ReturnsExpectedValue(string input, string expected) + { + var header = new NameValueHeaderValue("test"); + header.SetAndEscapeValue(input); - var actual = header.Value; + var actual = header.Value; - Assert.Equal(input, actual); - } + Assert.Equal(expected, actual); + } - [Theory] - [InlineData("val\\nue")] - [InlineData("val\\bue")] - public void OverescapingValuesDoNotRoundTrip(string input) - { - var header = new NameValueHeaderValue("test"); - header.SetAndEscapeValue(input); - var valueHeader = header.GetUnescapedValue(); - var actual = header.Value; + [Theory] + [InlineData("\n")] + [InlineData("\b")] + [InlineData("\r")] + public void SetAndEscapeValue_ThrowsOnInvalidValues(string input) + { + var header = new NameValueHeaderValue("test"); + Assert.Throws(() => header.SetAndEscapeValue(input)); + } - Assert.NotEqual(input, actual); - } + [Theory] + [InlineData("value")] + [InlineData("\"value\\\\morevalues\\\\evenmorevalues\"")] + [InlineData("\"quoted \\\"value\"")] + public void GetAndSetEncodeValueRoundTrip_ReturnsExpectedValue(string input) + { + var header = new NameValueHeaderValue("test"); + header.Value = input; + var valueHeader = header.GetUnescapedValue(); + header.SetAndEscapeValue(valueHeader); + var actual = header.Value; - #region Helper methods + Assert.Equal(input, actual); + } - private void CheckValidParse(string? input, NameValueHeaderValue expectedResult) - { - var result = NameValueHeaderValue.Parse(input); - Assert.Equal(expectedResult, result); - } + [Theory] + [InlineData("val\\nue")] + [InlineData("val\\bue")] + public void OverescapingValuesDoNotRoundTrip(string input) + { + var header = new NameValueHeaderValue("test"); + header.SetAndEscapeValue(input); + var valueHeader = header.GetUnescapedValue(); - private void CheckInvalidParse(string? input) - { - Assert.Throws(() => NameValueHeaderValue.Parse(input)); - } + var actual = header.Value; - private void CheckValidTryParse(string? input, NameValueHeaderValue expectedResult) - { - Assert.True(NameValueHeaderValue.TryParse(input, out var result)); - Assert.Equal(expectedResult, result); - } + Assert.NotEqual(input, actual); + } - private void CheckInvalidTryParse(string? input) - { - Assert.False(NameValueHeaderValue.TryParse(input, out var result)); - Assert.Null(result); - } - private static void CheckValue(string? value) - { - var nameValue = new NameValueHeaderValue("text", value); - Assert.Equal(value, nameValue.Value); - } + #region Helper methods - private static void AssertFormatException(string name, string? value) - { - Assert.Throws(() => new NameValueHeaderValue(name, value)); - } + private void CheckValidParse(string? input, NameValueHeaderValue expectedResult) + { + var result = NameValueHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); + } + + private void CheckInvalidParse(string? input) + { + Assert.Throws(() => NameValueHeaderValue.Parse(input)); + } + + private void CheckValidTryParse(string? input, NameValueHeaderValue expectedResult) + { + Assert.True(NameValueHeaderValue.TryParse(input, out var result)); + Assert.Equal(expectedResult, result); + } - #endregion + private void CheckInvalidTryParse(string? input) + { + Assert.False(NameValueHeaderValue.TryParse(input, out var result)); + Assert.Null(result); + } + + private static void CheckValue(string? value) + { + var nameValue = new NameValueHeaderValue("text", value); + Assert.Equal(value, nameValue.Value); } + + private static void AssertFormatException(string name, string? value) + { + Assert.Throws(() => new NameValueHeaderValue(name, value)); + } + + #endregion } diff --git a/src/Http/Headers/test/RangeConditionHeaderValueTest.cs b/src/Http/Headers/test/RangeConditionHeaderValueTest.cs index 24f7a3a7fd..cdf8f1489c 100644 --- a/src/Http/Headers/test/RangeConditionHeaderValueTest.cs +++ b/src/Http/Headers/test/RangeConditionHeaderValueTest.cs @@ -4,169 +4,168 @@ using System; using Xunit; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +public class RangeConditionHeaderValueTest { - public class RangeConditionHeaderValueTest + [Fact] + public void Ctor_EntityTagOverload_MatchExpectation() + { + var rangeCondition = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\"")); + Assert.Equal(new EntityTagHeaderValue("\"x\""), rangeCondition.EntityTag); + Assert.Null(rangeCondition.LastModified); + + EntityTagHeaderValue input = null!; + Assert.Throws(() => new RangeConditionHeaderValue(input)); + } + + [Fact] + public void Ctor_EntityTagStringOverload_MatchExpectation() + { + var rangeCondition = new RangeConditionHeaderValue("\"y\""); + Assert.Equal(new EntityTagHeaderValue("\"y\""), rangeCondition.EntityTag); + Assert.Null(rangeCondition.LastModified); + + Assert.Throws(() => new RangeConditionHeaderValue((string?)null)); + } + + [Fact] + public void Ctor_DateOverload_MatchExpectation() + { + var rangeCondition = new RangeConditionHeaderValue( + new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); + Assert.Null(rangeCondition.EntityTag); + Assert.Equal(new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero), rangeCondition.LastModified); + } + + [Fact] + public void ToString_UseDifferentRangeConditions_AllSerializedCorrectly() + { + var rangeCondition = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\"")); + Assert.Equal("\"x\"", rangeCondition.ToString()); + + rangeCondition = new RangeConditionHeaderValue(new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); + Assert.Equal("Thu, 15 Jul 2010 12:33:57 GMT", rangeCondition.ToString()); + } + + [Fact] + public void GetHashCode_UseSameAndDifferentRangeConditions_SameOrDifferentHashCodes() { - [Fact] - public void Ctor_EntityTagOverload_MatchExpectation() - { - var rangeCondition = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\"")); - Assert.Equal(new EntityTagHeaderValue("\"x\""), rangeCondition.EntityTag); - Assert.Null(rangeCondition.LastModified); - - EntityTagHeaderValue input = null!; - Assert.Throws(() => new RangeConditionHeaderValue(input)); - } - - [Fact] - public void Ctor_EntityTagStringOverload_MatchExpectation() - { - var rangeCondition = new RangeConditionHeaderValue("\"y\""); - Assert.Equal(new EntityTagHeaderValue("\"y\""), rangeCondition.EntityTag); - Assert.Null(rangeCondition.LastModified); - - Assert.Throws(() => new RangeConditionHeaderValue((string?)null)); - } - - [Fact] - public void Ctor_DateOverload_MatchExpectation() - { - var rangeCondition = new RangeConditionHeaderValue( - new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); - Assert.Null(rangeCondition.EntityTag); - Assert.Equal(new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero), rangeCondition.LastModified); - } - - [Fact] - public void ToString_UseDifferentRangeConditions_AllSerializedCorrectly() - { - var rangeCondition = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\"")); - Assert.Equal("\"x\"", rangeCondition.ToString()); - - rangeCondition = new RangeConditionHeaderValue(new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); - Assert.Equal("Thu, 15 Jul 2010 12:33:57 GMT", rangeCondition.ToString()); - } - - [Fact] - public void GetHashCode_UseSameAndDifferentRangeConditions_SameOrDifferentHashCodes() - { - var rangeCondition1 = new RangeConditionHeaderValue("\"x\""); - var rangeCondition2 = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\"")); - var rangeCondition3 = new RangeConditionHeaderValue( - new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); - var rangeCondition4 = new RangeConditionHeaderValue( - new DateTimeOffset(2008, 8, 16, 13, 44, 10, TimeSpan.Zero)); - var rangeCondition5 = new RangeConditionHeaderValue( - new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); - var rangeCondition6 = new RangeConditionHeaderValue( - new EntityTagHeaderValue("\"x\"", true)); - - Assert.Equal(rangeCondition1.GetHashCode(), rangeCondition2.GetHashCode()); - Assert.NotEqual(rangeCondition1.GetHashCode(), rangeCondition3.GetHashCode()); - Assert.NotEqual(rangeCondition3.GetHashCode(), rangeCondition4.GetHashCode()); - Assert.Equal(rangeCondition3.GetHashCode(), rangeCondition5.GetHashCode()); - Assert.NotEqual(rangeCondition1.GetHashCode(), rangeCondition6.GetHashCode()); - } - - [Fact] - public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions() - { - var rangeCondition1 = new RangeConditionHeaderValue("\"x\""); - var rangeCondition2 = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\"")); - var rangeCondition3 = new RangeConditionHeaderValue( - new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); - var rangeCondition4 = new RangeConditionHeaderValue( - new DateTimeOffset(2008, 8, 16, 13, 44, 10, TimeSpan.Zero)); - var rangeCondition5 = new RangeConditionHeaderValue( - new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); - var rangeCondition6 = new RangeConditionHeaderValue( - new EntityTagHeaderValue("\"x\"", true)); - - Assert.False(rangeCondition1.Equals(null), "\"x\" vs. "); - Assert.True(rangeCondition1!.Equals(rangeCondition2), "\"x\" vs. \"x\""); - Assert.False(rangeCondition1.Equals(rangeCondition3), "\"x\" vs. date"); - Assert.False(rangeCondition3.Equals(rangeCondition1), "date vs. \"x\""); - Assert.False(rangeCondition3.Equals(rangeCondition4), "date vs. different date"); - Assert.True(rangeCondition3.Equals(rangeCondition5), "date vs. date"); - Assert.False(rangeCondition1.Equals(rangeCondition6), "\"x\" vs. W/\"x\""); - } - - [Fact] - public void Parse_SetOfValidValueStrings_ParsedCorrectly() - { - CheckValidParse(" \"x\" ", new RangeConditionHeaderValue("\"x\"")); - CheckValidParse(" Sun, 06 Nov 1994 08:49:37 GMT ", - new RangeConditionHeaderValue(new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero))); - CheckValidParse("Wed, 09 Nov 1994 08:49:37 GMT", - new RangeConditionHeaderValue(new DateTimeOffset(1994, 11, 9, 8, 49, 37, TimeSpan.Zero))); - CheckValidParse(" W/ \"tag\" ", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true))); - CheckValidParse(" w/\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true))); - CheckValidParse("\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\""))); - } - - [Theory] - [InlineData("\"x\" ,")] // no delimiter allowed - [InlineData("Sun, 06 Nov 1994 08:49:37 GMT ,")] // no delimiter allowed - [InlineData("\"x\" Sun, 06 Nov 1994 08:49:37 GMT")] - [InlineData("Sun, 06 Nov 1994 08:49:37 GMT \"x\"")] - [InlineData(null)] - [InlineData("")] - [InlineData(" Wed 09 Nov 1994 08:49:37 GMT")] - [InlineData("\"x")] - [InlineData("Wed, 09 Nov")] - [InlineData("W/Wed 09 Nov 1994 08:49:37 GMT")] - [InlineData("\"x\",")] - [InlineData("Wed 09 Nov 1994 08:49:37 GMT,")] - public void Parse_SetOfInvalidValueStrings_Throws(string input) - { - Assert.Throws(() => RangeConditionHeaderValue.Parse(input)); - } - - [Fact] - public void TryParse_SetOfValidValueStrings_ParsedCorrectly() - { - CheckValidTryParse(" \"x\" ", new RangeConditionHeaderValue("\"x\"")); - CheckValidTryParse(" Sun, 06 Nov 1994 08:49:37 GMT ", - new RangeConditionHeaderValue(new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero))); - CheckValidTryParse(" W/ \"tag\" ", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true))); - CheckValidTryParse(" w/\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true))); - CheckValidTryParse("\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\""))); - } - - [Theory] - [InlineData("\"x\" ,")] // no delimiter allowed - [InlineData("Sun, 06 Nov 1994 08:49:37 GMT ,")] // no delimiter allowed - [InlineData("\"x\" Sun, 06 Nov 1994 08:49:37 GMT")] - [InlineData("Sun, 06 Nov 1994 08:49:37 GMT \"x\"")] - [InlineData(null)] - [InlineData("")] - [InlineData(" Wed 09 Nov 1994 08:49:37 GMT")] - [InlineData("\"x")] - [InlineData("Wed, 09 Nov")] - [InlineData("W/Wed 09 Nov 1994 08:49:37 GMT")] - [InlineData("\"x\",")] - [InlineData("Wed 09 Nov 1994 08:49:37 GMT,")] - public void TryParse_SetOfInvalidValueStrings_ReturnsFalse(string input) - { - Assert.False(RangeConditionHeaderValue.TryParse(input, out var result)); - Assert.Null(result); - } - - #region Helper methods - - private void CheckValidParse(string input, RangeConditionHeaderValue expectedResult) - { - var result = RangeConditionHeaderValue.Parse(input); - Assert.Equal(expectedResult, result); - } - - private void CheckValidTryParse(string input, RangeConditionHeaderValue expectedResult) - { - Assert.True(RangeConditionHeaderValue.TryParse(input, out var result)); - Assert.Equal(expectedResult, result); - } - - #endregion + var rangeCondition1 = new RangeConditionHeaderValue("\"x\""); + var rangeCondition2 = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\"")); + var rangeCondition3 = new RangeConditionHeaderValue( + new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); + var rangeCondition4 = new RangeConditionHeaderValue( + new DateTimeOffset(2008, 8, 16, 13, 44, 10, TimeSpan.Zero)); + var rangeCondition5 = new RangeConditionHeaderValue( + new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); + var rangeCondition6 = new RangeConditionHeaderValue( + new EntityTagHeaderValue("\"x\"", true)); + + Assert.Equal(rangeCondition1.GetHashCode(), rangeCondition2.GetHashCode()); + Assert.NotEqual(rangeCondition1.GetHashCode(), rangeCondition3.GetHashCode()); + Assert.NotEqual(rangeCondition3.GetHashCode(), rangeCondition4.GetHashCode()); + Assert.Equal(rangeCondition3.GetHashCode(), rangeCondition5.GetHashCode()); + Assert.NotEqual(rangeCondition1.GetHashCode(), rangeCondition6.GetHashCode()); } + + [Fact] + public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions() + { + var rangeCondition1 = new RangeConditionHeaderValue("\"x\""); + var rangeCondition2 = new RangeConditionHeaderValue(new EntityTagHeaderValue("\"x\"")); + var rangeCondition3 = new RangeConditionHeaderValue( + new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); + var rangeCondition4 = new RangeConditionHeaderValue( + new DateTimeOffset(2008, 8, 16, 13, 44, 10, TimeSpan.Zero)); + var rangeCondition5 = new RangeConditionHeaderValue( + new DateTimeOffset(2010, 7, 15, 12, 33, 57, TimeSpan.Zero)); + var rangeCondition6 = new RangeConditionHeaderValue( + new EntityTagHeaderValue("\"x\"", true)); + + Assert.False(rangeCondition1.Equals(null), "\"x\" vs. "); + Assert.True(rangeCondition1!.Equals(rangeCondition2), "\"x\" vs. \"x\""); + Assert.False(rangeCondition1.Equals(rangeCondition3), "\"x\" vs. date"); + Assert.False(rangeCondition3.Equals(rangeCondition1), "date vs. \"x\""); + Assert.False(rangeCondition3.Equals(rangeCondition4), "date vs. different date"); + Assert.True(rangeCondition3.Equals(rangeCondition5), "date vs. date"); + Assert.False(rangeCondition1.Equals(rangeCondition6), "\"x\" vs. W/\"x\""); + } + + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidParse(" \"x\" ", new RangeConditionHeaderValue("\"x\"")); + CheckValidParse(" Sun, 06 Nov 1994 08:49:37 GMT ", + new RangeConditionHeaderValue(new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero))); + CheckValidParse("Wed, 09 Nov 1994 08:49:37 GMT", + new RangeConditionHeaderValue(new DateTimeOffset(1994, 11, 9, 8, 49, 37, TimeSpan.Zero))); + CheckValidParse(" W/ \"tag\" ", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true))); + CheckValidParse(" w/\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true))); + CheckValidParse("\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\""))); + } + + [Theory] + [InlineData("\"x\" ,")] // no delimiter allowed + [InlineData("Sun, 06 Nov 1994 08:49:37 GMT ,")] // no delimiter allowed + [InlineData("\"x\" Sun, 06 Nov 1994 08:49:37 GMT")] + [InlineData("Sun, 06 Nov 1994 08:49:37 GMT \"x\"")] + [InlineData(null)] + [InlineData("")] + [InlineData(" Wed 09 Nov 1994 08:49:37 GMT")] + [InlineData("\"x")] + [InlineData("Wed, 09 Nov")] + [InlineData("W/Wed 09 Nov 1994 08:49:37 GMT")] + [InlineData("\"x\",")] + [InlineData("Wed 09 Nov 1994 08:49:37 GMT,")] + public void Parse_SetOfInvalidValueStrings_Throws(string input) + { + Assert.Throws(() => RangeConditionHeaderValue.Parse(input)); + } + + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidTryParse(" \"x\" ", new RangeConditionHeaderValue("\"x\"")); + CheckValidTryParse(" Sun, 06 Nov 1994 08:49:37 GMT ", + new RangeConditionHeaderValue(new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero))); + CheckValidTryParse(" W/ \"tag\" ", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true))); + CheckValidTryParse(" w/\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\"", true))); + CheckValidTryParse("\"tag\"", new RangeConditionHeaderValue(new EntityTagHeaderValue("\"tag\""))); + } + + [Theory] + [InlineData("\"x\" ,")] // no delimiter allowed + [InlineData("Sun, 06 Nov 1994 08:49:37 GMT ,")] // no delimiter allowed + [InlineData("\"x\" Sun, 06 Nov 1994 08:49:37 GMT")] + [InlineData("Sun, 06 Nov 1994 08:49:37 GMT \"x\"")] + [InlineData(null)] + [InlineData("")] + [InlineData(" Wed 09 Nov 1994 08:49:37 GMT")] + [InlineData("\"x")] + [InlineData("Wed, 09 Nov")] + [InlineData("W/Wed 09 Nov 1994 08:49:37 GMT")] + [InlineData("\"x\",")] + [InlineData("Wed 09 Nov 1994 08:49:37 GMT,")] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse(string input) + { + Assert.False(RangeConditionHeaderValue.TryParse(input, out var result)); + Assert.Null(result); + } + + #region Helper methods + + private void CheckValidParse(string input, RangeConditionHeaderValue expectedResult) + { + var result = RangeConditionHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); + } + + private void CheckValidTryParse(string input, RangeConditionHeaderValue expectedResult) + { + Assert.True(RangeConditionHeaderValue.TryParse(input, out var result)); + Assert.Equal(expectedResult, result); + } + + #endregion } diff --git a/src/Http/Headers/test/RangeHeaderValueTest.cs b/src/Http/Headers/test/RangeHeaderValueTest.cs index 908e23b3f5..c0c92ceff5 100644 --- a/src/Http/Headers/test/RangeHeaderValueTest.cs +++ b/src/Http/Headers/test/RangeHeaderValueTest.cs @@ -5,177 +5,176 @@ using System; using System.Linq; using Xunit; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +public class RangeHeaderValueTest { - public class RangeHeaderValueTest + [Fact] + public void Ctor_InvalidRange_Throw() + { + Assert.Throws(() => new RangeHeaderValue(5, 2)); + } + + [Fact] + public void Unit_GetAndSetValidAndInvalidValues_MatchExpectation() + { + var range = new RangeHeaderValue(); + range.Unit = "myunit"; + Assert.Equal("myunit", range.Unit); + + Assert.Throws(() => range.Unit = null); + Assert.Throws(() => range.Unit = ""); + Assert.Throws(() => range.Unit = " x"); + Assert.Throws(() => range.Unit = "x "); + Assert.Throws(() => range.Unit = "x y"); + } + + [Fact] + public void ToString_UseDifferentRanges_AllSerializedCorrectly() + { + var range = new RangeHeaderValue(); + range.Unit = "myunit"; + range.Ranges.Add(new RangeItemHeaderValue(1, 3)); + Assert.Equal("myunit=1-3", range.ToString()); + + range.Ranges.Add(new RangeItemHeaderValue(5, null)); + range.Ranges.Add(new RangeItemHeaderValue(null, 17)); + Assert.Equal("myunit=1-3, 5-, -17", range.ToString()); + } + + [Fact] + public void GetHashCode_UseSameAndDifferentRanges_SameOrDifferentHashCodes() + { + var range1 = new RangeHeaderValue(1, 2); + var range2 = new RangeHeaderValue(1, 2); + range2.Unit = "BYTES"; + var range3 = new RangeHeaderValue(1, null); + var range4 = new RangeHeaderValue(null, 2); + var range5 = new RangeHeaderValue(); + range5.Ranges.Add(new RangeItemHeaderValue(1, 2)); + range5.Ranges.Add(new RangeItemHeaderValue(3, 4)); + var range6 = new RangeHeaderValue(); + range6.Ranges.Add(new RangeItemHeaderValue(3, 4)); // reverse order of range5 + range6.Ranges.Add(new RangeItemHeaderValue(1, 2)); + + Assert.Equal(range1.GetHashCode(), range2.GetHashCode()); + Assert.NotEqual(range1.GetHashCode(), range3.GetHashCode()); + Assert.NotEqual(range1.GetHashCode(), range4.GetHashCode()); + Assert.NotEqual(range1.GetHashCode(), range5.GetHashCode()); + Assert.Equal(range5.GetHashCode(), range6.GetHashCode()); + } + + [Fact] + public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions() + { + var range1 = new RangeHeaderValue(1, 2); + var range2 = new RangeHeaderValue(1, 2); + range2.Unit = "BYTES"; + var range3 = new RangeHeaderValue(1, null); + var range4 = new RangeHeaderValue(null, 2); + var range5 = new RangeHeaderValue(); + range5.Ranges.Add(new RangeItemHeaderValue(1, 2)); + range5.Ranges.Add(new RangeItemHeaderValue(3, 4)); + var range6 = new RangeHeaderValue(); + range6.Ranges.Add(new RangeItemHeaderValue(3, 4)); // reverse order of range5 + range6.Ranges.Add(new RangeItemHeaderValue(1, 2)); + var range7 = new RangeHeaderValue(1, 2); + range7.Unit = "other"; + + Assert.False(range1.Equals(null), "bytes=1-2 vs. "); + Assert.True(range1!.Equals(range2), "bytes=1-2 vs. BYTES=1-2"); + Assert.False(range1.Equals(range3), "bytes=1-2 vs. bytes=1-"); + Assert.False(range1.Equals(range4), "bytes=1-2 vs. bytes=-2"); + Assert.False(range1.Equals(range5), "bytes=1-2 vs. bytes=1-2,3-4"); + Assert.True(range5.Equals(range6), "bytes=1-2,3-4 vs. bytes=3-4,1-2"); + Assert.False(range1.Equals(range7), "bytes=1-2 vs. other=1-2"); + } + + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidParse(" bytes=1-2 ", new RangeHeaderValue(1, 2)); + + var expected = new RangeHeaderValue(); + expected.Unit = "custom"; + expected.Ranges.Add(new RangeItemHeaderValue(null, 5)); + expected.Ranges.Add(new RangeItemHeaderValue(1, 4)); + CheckValidParse("custom = - 5 , 1 - 4 ,,", expected); + + expected = new RangeHeaderValue(); + expected.Unit = "custom"; + expected.Ranges.Add(new RangeItemHeaderValue(1, 2)); + CheckValidParse(" custom = 1 - 2", expected); + + expected = new RangeHeaderValue(); + expected.Ranges.Add(new RangeItemHeaderValue(1, 2)); + expected.Ranges.Add(new RangeItemHeaderValue(3, null)); + expected.Ranges.Add(new RangeItemHeaderValue(null, 4)); + CheckValidParse("bytes =1-2,,3-, , ,-4,,", expected); + } + + [Fact] + public void Parse_SetOfInvalidValueStrings_Throws() + { + CheckInvalidParse("bytes=1-2x"); // only delimiter ',' allowed after last range + CheckInvalidParse("x bytes=1-2"); + CheckInvalidParse("bytes=1-2.4"); + CheckInvalidParse(null); + CheckInvalidParse(string.Empty); + + CheckInvalidParse("bytes=1"); + CheckInvalidParse("bytes="); + CheckInvalidParse("bytes"); + CheckInvalidParse("bytes 1-2"); + CheckInvalidParse("bytes= ,,, , ,,"); + } + + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidTryParse(" bytes=1-2 ", new RangeHeaderValue(1, 2)); + + var expected = new RangeHeaderValue(); + expected.Unit = "custom"; + expected.Ranges.Add(new RangeItemHeaderValue(null, 5)); + expected.Ranges.Add(new RangeItemHeaderValue(1, 4)); + CheckValidTryParse("custom = - 5 , 1 - 4 ,,", expected); + } + + [Fact] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() + { + CheckInvalidTryParse("bytes=1-2x"); // only delimiter ',' allowed after last range + CheckInvalidTryParse("x bytes=1-2"); + CheckInvalidTryParse("bytes=1-2.4"); + CheckInvalidTryParse(null); + CheckInvalidTryParse(string.Empty); + } + + #region Helper methods + + private void CheckValidParse(string? input, RangeHeaderValue expectedResult) { - [Fact] - public void Ctor_InvalidRange_Throw() - { - Assert.Throws(() => new RangeHeaderValue(5, 2)); - } - - [Fact] - public void Unit_GetAndSetValidAndInvalidValues_MatchExpectation() - { - var range = new RangeHeaderValue(); - range.Unit = "myunit"; - Assert.Equal("myunit", range.Unit); - - Assert.Throws(() => range.Unit = null); - Assert.Throws(() => range.Unit = ""); - Assert.Throws(() => range.Unit = " x"); - Assert.Throws(() => range.Unit = "x "); - Assert.Throws(() => range.Unit = "x y"); - } - - [Fact] - public void ToString_UseDifferentRanges_AllSerializedCorrectly() - { - var range = new RangeHeaderValue(); - range.Unit = "myunit"; - range.Ranges.Add(new RangeItemHeaderValue(1, 3)); - Assert.Equal("myunit=1-3", range.ToString()); - - range.Ranges.Add(new RangeItemHeaderValue(5, null)); - range.Ranges.Add(new RangeItemHeaderValue(null, 17)); - Assert.Equal("myunit=1-3, 5-, -17", range.ToString()); - } - - [Fact] - public void GetHashCode_UseSameAndDifferentRanges_SameOrDifferentHashCodes() - { - var range1 = new RangeHeaderValue(1, 2); - var range2 = new RangeHeaderValue(1, 2); - range2.Unit = "BYTES"; - var range3 = new RangeHeaderValue(1, null); - var range4 = new RangeHeaderValue(null, 2); - var range5 = new RangeHeaderValue(); - range5.Ranges.Add(new RangeItemHeaderValue(1, 2)); - range5.Ranges.Add(new RangeItemHeaderValue(3, 4)); - var range6 = new RangeHeaderValue(); - range6.Ranges.Add(new RangeItemHeaderValue(3, 4)); // reverse order of range5 - range6.Ranges.Add(new RangeItemHeaderValue(1, 2)); - - Assert.Equal(range1.GetHashCode(), range2.GetHashCode()); - Assert.NotEqual(range1.GetHashCode(), range3.GetHashCode()); - Assert.NotEqual(range1.GetHashCode(), range4.GetHashCode()); - Assert.NotEqual(range1.GetHashCode(), range5.GetHashCode()); - Assert.Equal(range5.GetHashCode(), range6.GetHashCode()); - } - - [Fact] - public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions() - { - var range1 = new RangeHeaderValue(1, 2); - var range2 = new RangeHeaderValue(1, 2); - range2.Unit = "BYTES"; - var range3 = new RangeHeaderValue(1, null); - var range4 = new RangeHeaderValue(null, 2); - var range5 = new RangeHeaderValue(); - range5.Ranges.Add(new RangeItemHeaderValue(1, 2)); - range5.Ranges.Add(new RangeItemHeaderValue(3, 4)); - var range6 = new RangeHeaderValue(); - range6.Ranges.Add(new RangeItemHeaderValue(3, 4)); // reverse order of range5 - range6.Ranges.Add(new RangeItemHeaderValue(1, 2)); - var range7 = new RangeHeaderValue(1, 2); - range7.Unit = "other"; - - Assert.False(range1.Equals(null), "bytes=1-2 vs. "); - Assert.True(range1!.Equals(range2), "bytes=1-2 vs. BYTES=1-2"); - Assert.False(range1.Equals(range3), "bytes=1-2 vs. bytes=1-"); - Assert.False(range1.Equals(range4), "bytes=1-2 vs. bytes=-2"); - Assert.False(range1.Equals(range5), "bytes=1-2 vs. bytes=1-2,3-4"); - Assert.True(range5.Equals(range6), "bytes=1-2,3-4 vs. bytes=3-4,1-2"); - Assert.False(range1.Equals(range7), "bytes=1-2 vs. other=1-2"); - } - - [Fact] - public void Parse_SetOfValidValueStrings_ParsedCorrectly() - { - CheckValidParse(" bytes=1-2 ", new RangeHeaderValue(1, 2)); - - var expected = new RangeHeaderValue(); - expected.Unit = "custom"; - expected.Ranges.Add(new RangeItemHeaderValue(null, 5)); - expected.Ranges.Add(new RangeItemHeaderValue(1, 4)); - CheckValidParse("custom = - 5 , 1 - 4 ,,", expected); - - expected = new RangeHeaderValue(); - expected.Unit = "custom"; - expected.Ranges.Add(new RangeItemHeaderValue(1, 2)); - CheckValidParse(" custom = 1 - 2", expected); - - expected = new RangeHeaderValue(); - expected.Ranges.Add(new RangeItemHeaderValue(1, 2)); - expected.Ranges.Add(new RangeItemHeaderValue(3, null)); - expected.Ranges.Add(new RangeItemHeaderValue(null, 4)); - CheckValidParse("bytes =1-2,,3-, , ,-4,,", expected); - } - - [Fact] - public void Parse_SetOfInvalidValueStrings_Throws() - { - CheckInvalidParse("bytes=1-2x"); // only delimiter ',' allowed after last range - CheckInvalidParse("x bytes=1-2"); - CheckInvalidParse("bytes=1-2.4"); - CheckInvalidParse(null); - CheckInvalidParse(string.Empty); - - CheckInvalidParse("bytes=1"); - CheckInvalidParse("bytes="); - CheckInvalidParse("bytes"); - CheckInvalidParse("bytes 1-2"); - CheckInvalidParse("bytes= ,,, , ,,"); - } - - [Fact] - public void TryParse_SetOfValidValueStrings_ParsedCorrectly() - { - CheckValidTryParse(" bytes=1-2 ", new RangeHeaderValue(1, 2)); - - var expected = new RangeHeaderValue(); - expected.Unit = "custom"; - expected.Ranges.Add(new RangeItemHeaderValue(null, 5)); - expected.Ranges.Add(new RangeItemHeaderValue(1, 4)); - CheckValidTryParse("custom = - 5 , 1 - 4 ,,", expected); - } - - [Fact] - public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() - { - CheckInvalidTryParse("bytes=1-2x"); // only delimiter ',' allowed after last range - CheckInvalidTryParse("x bytes=1-2"); - CheckInvalidTryParse("bytes=1-2.4"); - CheckInvalidTryParse(null); - CheckInvalidTryParse(string.Empty); - } - - #region Helper methods - - private void CheckValidParse(string? input, RangeHeaderValue expectedResult) - { - var result = RangeHeaderValue.Parse(input); - Assert.Equal(expectedResult, result); - } - - private void CheckInvalidParse(string? input) - { - Assert.Throws(() => RangeHeaderValue.Parse(input)); - } - - private void CheckValidTryParse(string? input, RangeHeaderValue expectedResult) - { - Assert.True(RangeHeaderValue.TryParse(input, out var result)); - Assert.Equal(expectedResult, result); - } - - private void CheckInvalidTryParse(string? input) - { - Assert.False(RangeHeaderValue.TryParse(input, out var result)); - Assert.Null(result); - } - - #endregion + var result = RangeHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); } + + private void CheckInvalidParse(string? input) + { + Assert.Throws(() => RangeHeaderValue.Parse(input)); + } + + private void CheckValidTryParse(string? input, RangeHeaderValue expectedResult) + { + Assert.True(RangeHeaderValue.TryParse(input, out var result)); + Assert.Equal(expectedResult, result); + } + + private void CheckInvalidTryParse(string? input) + { + Assert.False(RangeHeaderValue.TryParse(input, out var result)); + Assert.Null(result); + } + + #endregion } diff --git a/src/Http/Headers/test/RangeItemHeaderValueTest.cs b/src/Http/Headers/test/RangeItemHeaderValueTest.cs index 2522c81841..e0a173054e 100644 --- a/src/Http/Headers/test/RangeItemHeaderValueTest.cs +++ b/src/Http/Headers/test/RangeItemHeaderValueTest.cs @@ -6,157 +6,156 @@ using System.Collections.Generic; using System.Linq; using Xunit; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +public class RangeItemHeaderValueTest { - public class RangeItemHeaderValueTest + [Fact] + public void Ctor_BothValuesNull_Throw() { - [Fact] - public void Ctor_BothValuesNull_Throw() - { - Assert.Throws(() => new RangeItemHeaderValue(null, null)); - } + Assert.Throws(() => new RangeItemHeaderValue(null, null)); + } - [Fact] - public void Ctor_FromValueNegative_Throw() - { - Assert.Throws(() => new RangeItemHeaderValue(-1, null)); - } + [Fact] + public void Ctor_FromValueNegative_Throw() + { + Assert.Throws(() => new RangeItemHeaderValue(-1, null)); + } - [Fact] - public void Ctor_FromGreaterThanToValue_Throw() - { - Assert.Throws(() => new RangeItemHeaderValue(2, 1)); - } + [Fact] + public void Ctor_FromGreaterThanToValue_Throw() + { + Assert.Throws(() => new RangeItemHeaderValue(2, 1)); + } - [Fact] - public void Ctor_ToValueNegative_Throw() - { - Assert.Throws(() => new RangeItemHeaderValue(null, -1)); - } + [Fact] + public void Ctor_ToValueNegative_Throw() + { + Assert.Throws(() => new RangeItemHeaderValue(null, -1)); + } - [Fact] - public void Ctor_ValidFormat_SuccessfullyCreated() - { - var rangeItem = new RangeItemHeaderValue(1, 2); - Assert.Equal(1, rangeItem.From); - Assert.Equal(2, rangeItem.To); - } + [Fact] + public void Ctor_ValidFormat_SuccessfullyCreated() + { + var rangeItem = new RangeItemHeaderValue(1, 2); + Assert.Equal(1, rangeItem.From); + Assert.Equal(2, rangeItem.To); + } - [Fact] - public void ToString_UseDifferentRangeItems_AllSerializedCorrectly() - { - // Make sure ToString() doesn't add any separators. - var rangeItem = new RangeItemHeaderValue(1000000000, 2000000000); - Assert.Equal("1000000000-2000000000", rangeItem.ToString()); + [Fact] + public void ToString_UseDifferentRangeItems_AllSerializedCorrectly() + { + // Make sure ToString() doesn't add any separators. + var rangeItem = new RangeItemHeaderValue(1000000000, 2000000000); + Assert.Equal("1000000000-2000000000", rangeItem.ToString()); - rangeItem = new RangeItemHeaderValue(5, null); - Assert.Equal("5-", rangeItem.ToString()); + rangeItem = new RangeItemHeaderValue(5, null); + Assert.Equal("5-", rangeItem.ToString()); - rangeItem = new RangeItemHeaderValue(null, 10); - Assert.Equal("-10", rangeItem.ToString()); - } + rangeItem = new RangeItemHeaderValue(null, 10); + Assert.Equal("-10", rangeItem.ToString()); + } - [Fact] - public void GetHashCode_UseSameAndDifferentRangeItems_SameOrDifferentHashCodes() - { - var rangeItem1 = new RangeItemHeaderValue(1, 2); - var rangeItem2 = new RangeItemHeaderValue(1, null); - var rangeItem3 = new RangeItemHeaderValue(null, 2); - var rangeItem4 = new RangeItemHeaderValue(2, 2); - var rangeItem5 = new RangeItemHeaderValue(1, 2); - - Assert.NotEqual(rangeItem1.GetHashCode(), rangeItem2.GetHashCode()); - Assert.NotEqual(rangeItem1.GetHashCode(), rangeItem3.GetHashCode()); - Assert.NotEqual(rangeItem1.GetHashCode(), rangeItem4.GetHashCode()); - Assert.Equal(rangeItem1.GetHashCode(), rangeItem5.GetHashCode()); - } + [Fact] + public void GetHashCode_UseSameAndDifferentRangeItems_SameOrDifferentHashCodes() + { + var rangeItem1 = new RangeItemHeaderValue(1, 2); + var rangeItem2 = new RangeItemHeaderValue(1, null); + var rangeItem3 = new RangeItemHeaderValue(null, 2); + var rangeItem4 = new RangeItemHeaderValue(2, 2); + var rangeItem5 = new RangeItemHeaderValue(1, 2); + + Assert.NotEqual(rangeItem1.GetHashCode(), rangeItem2.GetHashCode()); + Assert.NotEqual(rangeItem1.GetHashCode(), rangeItem3.GetHashCode()); + Assert.NotEqual(rangeItem1.GetHashCode(), rangeItem4.GetHashCode()); + Assert.Equal(rangeItem1.GetHashCode(), rangeItem5.GetHashCode()); + } - [Fact] - public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions() - { - var rangeItem1 = new RangeItemHeaderValue(1, 2); - var rangeItem2 = new RangeItemHeaderValue(1, null); - var rangeItem3 = new RangeItemHeaderValue(null, 2); - var rangeItem4 = new RangeItemHeaderValue(2, 2); - var rangeItem5 = new RangeItemHeaderValue(1, 2); - - Assert.False(rangeItem1.Equals(rangeItem2), "1-2 vs. 1-."); - Assert.False(rangeItem2.Equals(rangeItem1), "1- vs. 1-2."); - Assert.False(rangeItem1.Equals(null), "1-2 vs. null."); - Assert.False(rangeItem1!.Equals(rangeItem3), "1-2 vs. -2."); - Assert.False(rangeItem3.Equals(rangeItem1), "-2 vs. 1-2."); - Assert.False(rangeItem1.Equals(rangeItem4), "1-2 vs. 2-2."); - Assert.True(rangeItem1.Equals(rangeItem5), "1-2 vs. 1-2."); - } + [Fact] + public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions() + { + var rangeItem1 = new RangeItemHeaderValue(1, 2); + var rangeItem2 = new RangeItemHeaderValue(1, null); + var rangeItem3 = new RangeItemHeaderValue(null, 2); + var rangeItem4 = new RangeItemHeaderValue(2, 2); + var rangeItem5 = new RangeItemHeaderValue(1, 2); + + Assert.False(rangeItem1.Equals(rangeItem2), "1-2 vs. 1-."); + Assert.False(rangeItem2.Equals(rangeItem1), "1- vs. 1-2."); + Assert.False(rangeItem1.Equals(null), "1-2 vs. null."); + Assert.False(rangeItem1!.Equals(rangeItem3), "1-2 vs. -2."); + Assert.False(rangeItem3.Equals(rangeItem1), "-2 vs. 1-2."); + Assert.False(rangeItem1.Equals(rangeItem4), "1-2 vs. 2-2."); + Assert.True(rangeItem1.Equals(rangeItem5), "1-2 vs. 1-2."); + } - [Fact] - public void TryParse_DifferentValidScenarios_AllReturnNonZero() - { - CheckValidTryParse("1-2", 1, 2); - CheckValidTryParse(" 1-2", 1, 2); - CheckValidTryParse("0-0", 0, 0); - CheckValidTryParse(" 1-", 1, null); - CheckValidTryParse(" -2", null, 2); + [Fact] + public void TryParse_DifferentValidScenarios_AllReturnNonZero() + { + CheckValidTryParse("1-2", 1, 2); + CheckValidTryParse(" 1-2", 1, 2); + CheckValidTryParse("0-0", 0, 0); + CheckValidTryParse(" 1-", 1, null); + CheckValidTryParse(" -2", null, 2); - CheckValidTryParse(" 684684 - 123456789012345 ", 684684, 123456789012345); + CheckValidTryParse(" 684684 - 123456789012345 ", 684684, 123456789012345); - // The separator doesn't matter. It only parses until the first non-whitespace - CheckValidTryParse(" 1 - 2 ,", 1, 2); + // The separator doesn't matter. It only parses until the first non-whitespace + CheckValidTryParse(" 1 - 2 ,", 1, 2); - CheckValidTryParse(",,1-2, 3 - , , -6 , ,,", new Tuple(1, 2), new Tuple(3, null), - new Tuple(null, 6)); - CheckValidTryParse("1-2,", new Tuple(1, 2)); - CheckValidTryParse("1-", new Tuple(1, null)); - } + CheckValidTryParse(",,1-2, 3 - , , -6 , ,,", new Tuple(1, 2), new Tuple(3, null), + new Tuple(null, 6)); + CheckValidTryParse("1-2,", new Tuple(1, 2)); + CheckValidTryParse("1-", new Tuple(1, null)); + } - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(",,")] - [InlineData("1")] - [InlineData("1-2,3")] - [InlineData("1--2")] - [InlineData("1,-2")] - [InlineData("-")] - [InlineData("--")] - [InlineData("2-1")] - [InlineData("12345678901234567890123-")] // >>Int64.MaxValue - [InlineData("-12345678901234567890123")] // >>Int64.MaxValue - [InlineData("9999999999999999999-")] // 19-digit numbers outside the Int64 range. - [InlineData("-9999999999999999999")] // 19-digit numbers outside the Int64 range. - public void TryParse_DifferentInvalidScenarios_AllReturnFalse(string input) - { - RangeHeaderValue result; - Assert.False(RangeHeaderValue.TryParse("byte=" + input, out result)); - } + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(",,")] + [InlineData("1")] + [InlineData("1-2,3")] + [InlineData("1--2")] + [InlineData("1,-2")] + [InlineData("-")] + [InlineData("--")] + [InlineData("2-1")] + [InlineData("12345678901234567890123-")] // >>Int64.MaxValue + [InlineData("-12345678901234567890123")] // >>Int64.MaxValue + [InlineData("9999999999999999999-")] // 19-digit numbers outside the Int64 range. + [InlineData("-9999999999999999999")] // 19-digit numbers outside the Int64 range. + public void TryParse_DifferentInvalidScenarios_AllReturnFalse(string input) + { + RangeHeaderValue result; + Assert.False(RangeHeaderValue.TryParse("byte=" + input, out result)); + } - private static void CheckValidTryParse(string input, long? expectedFrom, long? expectedTo) - { - RangeHeaderValue result; - Assert.True(RangeHeaderValue.TryParse("byte=" + input, out result), input); + private static void CheckValidTryParse(string input, long? expectedFrom, long? expectedTo) + { + RangeHeaderValue result; + Assert.True(RangeHeaderValue.TryParse("byte=" + input, out result), input); - var ranges = result.Ranges.ToArray(); - Assert.Single(ranges); + var ranges = result.Ranges.ToArray(); + Assert.Single(ranges); - var range = ranges.First(); + var range = ranges.First(); - Assert.Equal(expectedFrom, range.From); - Assert.Equal(expectedTo, range.To); - } + Assert.Equal(expectedFrom, range.From); + Assert.Equal(expectedTo, range.To); + } - private static void CheckValidTryParse(string input, params Tuple[] expectedRanges) - { - RangeHeaderValue result; - Assert.True(RangeHeaderValue.TryParse("byte=" + input, out result), input); + private static void CheckValidTryParse(string input, params Tuple[] expectedRanges) + { + RangeHeaderValue result; + Assert.True(RangeHeaderValue.TryParse("byte=" + input, out result), input); - var ranges = result.Ranges.ToArray(); - Assert.Equal(expectedRanges.Length, ranges.Length); + var ranges = result.Ranges.ToArray(); + Assert.Equal(expectedRanges.Length, ranges.Length); - for (int i = 0; i < expectedRanges.Length; i++) - { - Assert.Equal(expectedRanges[i].Item1, ranges[i].From); - Assert.Equal(expectedRanges[i].Item2, ranges[i].To); - } + for (int i = 0; i < expectedRanges.Length; i++) + { + Assert.Equal(expectedRanges[i].Item1, ranges[i].From); + Assert.Equal(expectedRanges[i].Item2, ranges[i].To); } } } diff --git a/src/Http/Headers/test/SetCookieHeaderValueTest.cs b/src/Http/Headers/test/SetCookieHeaderValueTest.cs index ae4ac05b91..c21b0e1331 100644 --- a/src/Http/Headers/test/SetCookieHeaderValueTest.cs +++ b/src/Http/Headers/test/SetCookieHeaderValueTest.cs @@ -10,87 +10,87 @@ using Microsoft.Extensions.Primitives; using Moq; using Xunit; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +public class SetCookieHeaderValueTest { - public class SetCookieHeaderValueTest + public static TheoryData SetCookieHeaderDataSet { - public static TheoryData SetCookieHeaderDataSet + get { - get + var dataset = new TheoryData(); + var header1 = new SetCookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3") { - var dataset = new TheoryData(); - var header1 = new SetCookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3") - { - Domain = "domain1", - Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), - SameSite = SameSiteMode.Strict, - HttpOnly = true, - MaxAge = TimeSpan.FromDays(1), - Path = "path1", - Secure = true, - }; - header1.Extensions.Add("extension1"); - header1.Extensions.Add("extension2=value"); - dataset.Add(header1, "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=strict; httponly; extension1; extension2=value"); - - var header2 = new SetCookieHeaderValue("name2", ""); - dataset.Add(header2, "name2="); - - var header3 = new SetCookieHeaderValue("name2", "value2"); - dataset.Add(header3, "name2=value2"); - - var header4 = new SetCookieHeaderValue("name4", "value4") - { - MaxAge = TimeSpan.FromDays(1), - }; - dataset.Add(header4, "name4=value4; max-age=86400"); + Domain = "domain1", + Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), + SameSite = SameSiteMode.Strict, + HttpOnly = true, + MaxAge = TimeSpan.FromDays(1), + Path = "path1", + Secure = true, + }; + header1.Extensions.Add("extension1"); + header1.Extensions.Add("extension2=value"); + dataset.Add(header1, "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=strict; httponly; extension1; extension2=value"); + + var header2 = new SetCookieHeaderValue("name2", ""); + dataset.Add(header2, "name2="); + + var header3 = new SetCookieHeaderValue("name2", "value2"); + dataset.Add(header3, "name2=value2"); + + var header4 = new SetCookieHeaderValue("name4", "value4") + { + MaxAge = TimeSpan.FromDays(1), + }; + dataset.Add(header4, "name4=value4; max-age=86400"); - var header5 = new SetCookieHeaderValue("name5", "value5") - { - Domain = "domain1", - Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), - }; - dataset.Add(header5, "name5=value5; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1"); + var header5 = new SetCookieHeaderValue("name5", "value5") + { + Domain = "domain1", + Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), + }; + dataset.Add(header5, "name5=value5; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1"); - var header6 = new SetCookieHeaderValue("name6", "value6") - { - SameSite = SameSiteMode.Lax, - }; - dataset.Add(header6, "name6=value6; samesite=lax"); + var header6 = new SetCookieHeaderValue("name6", "value6") + { + SameSite = SameSiteMode.Lax, + }; + dataset.Add(header6, "name6=value6; samesite=lax"); - var header7 = new SetCookieHeaderValue("name7", "value7") - { - SameSite = SameSiteMode.None, - }; - dataset.Add(header7, "name7=value7; samesite=none"); + var header7 = new SetCookieHeaderValue("name7", "value7") + { + SameSite = SameSiteMode.None, + }; + dataset.Add(header7, "name7=value7; samesite=none"); - var header8 = new SetCookieHeaderValue("name8", "value8"); - header8.Extensions.Add("extension1"); - header8.Extensions.Add("extension2=value"); - dataset.Add(header8, "name8=value8; extension1; extension2=value"); + var header8 = new SetCookieHeaderValue("name8", "value8"); + header8.Extensions.Add("extension1"); + header8.Extensions.Add("extension2=value"); + dataset.Add(header8, "name8=value8; extension1; extension2=value"); - return dataset; - } + return dataset; } + } - public static TheoryData InvalidSetCookieHeaderDataSet + public static TheoryData InvalidSetCookieHeaderDataSet + { + get { - get - { - return new TheoryData + return new TheoryData { "expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1", "name=value; expires=Sun, 06 Nov 1994 08:49:37 ZZZ; max-age=86400; domain=domain1", "name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=-86400; domain=domain1", }; - } } + } - public static TheoryData InvalidCookieNames + public static TheoryData InvalidCookieNames + { + get { - get - { - return new TheoryData + return new TheoryData { "", "{acb}", @@ -100,14 +100,14 @@ namespace Microsoft.Net.Http.Headers "a;b", "a\\b", }; - } } + } - public static TheoryData InvalidCookieValues + public static TheoryData InvalidCookieValues + { + get { - get - { - return new TheoryData + return new TheoryData { { "\"" }, { "a,b" }, @@ -117,371 +117,370 @@ namespace Microsoft.Net.Http.Headers { "a\"bc" }, { "abc\"" }, }; - } } + } - public static TheoryData, string?[]> ListOfSetCookieHeaderDataSet + public static TheoryData, string?[]> ListOfSetCookieHeaderDataSet + { + get { - get + var dataset = new TheoryData, string?[]>(); + var header1 = new SetCookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3") { - var dataset = new TheoryData, string?[]>(); - var header1 = new SetCookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3") - { - Domain = "domain1", - Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), - SameSite = SameSiteMode.Strict, - HttpOnly = true, - MaxAge = TimeSpan.FromDays(1), - Path = "path1", - Secure = true - }; - header1.Extensions.Add("extension1"); - header1.Extensions.Add("extension2=value"); - - var string1 = "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=strict; httponly; extension1; extension2=value"; - - var header2 = new SetCookieHeaderValue("name2", "value2"); - var string2 = "name2=value2"; + Domain = "domain1", + Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), + SameSite = SameSiteMode.Strict, + HttpOnly = true, + MaxAge = TimeSpan.FromDays(1), + Path = "path1", + Secure = true + }; + header1.Extensions.Add("extension1"); + header1.Extensions.Add("extension2=value"); + + var string1 = "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=strict; httponly; extension1; extension2=value"; + + var header2 = new SetCookieHeaderValue("name2", "value2"); + var string2 = "name2=value2"; + + var header3 = new SetCookieHeaderValue("name3", "value3") + { + MaxAge = TimeSpan.FromDays(1), + }; + var string3 = "name3=value3; max-age=86400"; - var header3 = new SetCookieHeaderValue("name3", "value3") - { - MaxAge = TimeSpan.FromDays(1), - }; - var string3 = "name3=value3; max-age=86400"; + var header4 = new SetCookieHeaderValue("name4", "value4") + { + Domain = "domain1", + Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), + }; + var string4 = "name4=value4; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1"; - var header4 = new SetCookieHeaderValue("name4", "value4") - { - Domain = "domain1", - Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), - }; - var string4 = "name4=value4; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1"; + var header5 = new SetCookieHeaderValue("name5", "value5") + { + SameSite = SameSiteMode.Lax + }; + var string5a = "name5=value5; samesite=lax"; + var string5b = "name5=value5; samesite=Lax"; - var header5 = new SetCookieHeaderValue("name5", "value5") - { - SameSite = SameSiteMode.Lax - }; - var string5a = "name5=value5; samesite=lax"; - var string5b = "name5=value5; samesite=Lax"; + var header6 = new SetCookieHeaderValue("name6", "value6") + { + SameSite = SameSiteMode.Strict + }; + var string6 = "name6=value6; samesite=Strict"; - var header6 = new SetCookieHeaderValue("name6", "value6") - { - SameSite = SameSiteMode.Strict - }; - var string6 = "name6=value6; samesite=Strict"; + var header7 = new SetCookieHeaderValue("name7", "value7") + { + SameSite = SameSiteMode.None + }; + var string7 = "name7=value7; samesite=None"; - var header7 = new SetCookieHeaderValue("name7", "value7") - { - SameSite = SameSiteMode.None - }; - var string7 = "name7=value7; samesite=None"; + var header8 = new SetCookieHeaderValue("name8", "value8") + { + SameSite = SameSiteMode.Unspecified + }; + var string8a = "name8=value8; samesite"; + var string8b = "name8=value8; samesite=invalid"; + + var header9 = new SetCookieHeaderValue("name9", "value9"); + header9.Extensions.Add("extension1"); + header9.Extensions.Add("extension2=value"); + var string9 = "name9=value9; extension1; extension2=value"; + + + dataset.Add(new[] { header1 }.ToList(), new[] { string1 }); + dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 }); + dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ",", " , ", string1 }); + dataset.Add(new[] { header2 }.ToList(), new[] { string2 }); + dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1, string2 }); + dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1 + ", " + string2 }); + dataset.Add(new[] { header2, header1 }.ToList(), new[] { string2 + ", " + string1 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) }); + dataset.Add(new[] { header5 }.ToList(), new[] { string5a }); + dataset.Add(new[] { header5 }.ToList(), new[] { string5b }); + dataset.Add(new[] { header6 }.ToList(), new[] { string6 }); + dataset.Add(new[] { header7 }.ToList(), new[] { string7 }); + dataset.Add(new[] { header8 }.ToList(), new[] { string8a }); + dataset.Add(new[] { header8 }.ToList(), new[] { string8b }); + dataset.Add(new[] { header9 }.ToList(), new[] { string9 }); + + foreach (var item1 in SetCookieHeaderDataSet) + { + var pair_cookie1 = (SetCookieHeaderValue)item1[0]; + var pair_string1 = item1[1].ToString(); - var header8 = new SetCookieHeaderValue("name8", "value8") - { - SameSite = SameSiteMode.Unspecified - }; - var string8a = "name8=value8; samesite"; - var string8b = "name8=value8; samesite=invalid"; - - var header9 = new SetCookieHeaderValue("name9", "value9"); - header9.Extensions.Add("extension1"); - header9.Extensions.Add("extension2=value"); - var string9 = "name9=value9; extension1; extension2=value"; - - - dataset.Add(new[] { header1 }.ToList(), new[] { string1 }); - dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 }); - dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ",", " , ", string1 }); - dataset.Add(new[] { header2 }.ToList(), new[] { string2 }); - dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1, string2 }); - dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1 + ", " + string2 }); - dataset.Add(new[] { header2, header1 }.ToList(), new[] { string2 + ", " + string1 }); - dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4 }); - dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) }); - dataset.Add(new[] { header5 }.ToList(), new[] { string5a }); - dataset.Add(new[] { header5 }.ToList(), new[] { string5b }); - dataset.Add(new[] { header6 }.ToList(), new[] { string6 }); - dataset.Add(new[] { header7 }.ToList(), new[] { string7 }); - dataset.Add(new[] { header8 }.ToList(), new[] { string8a }); - dataset.Add(new[] { header8 }.ToList(), new[] { string8b }); - dataset.Add(new[] { header9 }.ToList(), new[] { string9 }); - - foreach (var item1 in SetCookieHeaderDataSet) + foreach (var item2 in SetCookieHeaderDataSet) { - var pair_cookie1 = (SetCookieHeaderValue)item1[0]; - var pair_string1 = item1[1].ToString(); - - foreach (var item2 in SetCookieHeaderDataSet) - { - var pair_cookie2 = (SetCookieHeaderValue)item2[0]; - var pair_string2 = item2[1].ToString(); + var pair_cookie2 = (SetCookieHeaderValue)item2[0]; + var pair_string2 = item2[1].ToString(); - dataset.Add(new[] { pair_cookie1, pair_cookie2 }.ToList(), new[] { string.Join(", ", pair_string1, pair_string2) }); + dataset.Add(new[] { pair_cookie1, pair_cookie2 }.ToList(), new[] { string.Join(", ", pair_string1, pair_string2) }); - } } - - return dataset; } + + return dataset; } + } - public static TheoryData?, string?[]> ListWithInvalidSetCookieHeaderDataSet + public static TheoryData?, string?[]> ListWithInvalidSetCookieHeaderDataSet + { + get { - get + var dataset = new TheoryData?, string?[]>(); + var header1 = new SetCookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3") { - var dataset = new TheoryData?, string?[]>(); - var header1 = new SetCookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3") - { - Domain = "domain1", - Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), - SameSite = SameSiteMode.Strict, - HttpOnly = true, - MaxAge = TimeSpan.FromDays(1), - Path = "path1", - Secure = true - }; - var string1 = "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=Strict; httponly"; - - var header2 = new SetCookieHeaderValue("name2", "value2"); - var string2 = "name2=value2"; - - var header3 = new SetCookieHeaderValue("name3", "value3") - { - MaxAge = TimeSpan.FromDays(1), - }; - var string3 = "name3=value3; max-age=86400"; + Domain = "domain1", + Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), + SameSite = SameSiteMode.Strict, + HttpOnly = true, + MaxAge = TimeSpan.FromDays(1), + Path = "path1", + Secure = true + }; + var string1 = "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=Strict; httponly"; + + var header2 = new SetCookieHeaderValue("name2", "value2"); + var string2 = "name2=value2"; + + var header3 = new SetCookieHeaderValue("name3", "value3") + { + MaxAge = TimeSpan.FromDays(1), + }; + var string3 = "name3=value3; max-age=86400"; - var header4 = new SetCookieHeaderValue("name4", "value4") - { - Domain = "domain1", - Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), - }; - var string4 = "name4=value4; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1;"; + var header4 = new SetCookieHeaderValue("name4", "value4") + { + Domain = "domain1", + Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), + }; + var string4 = "name4=value4; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1;"; - var invalidString1 = "ipt={\"v\":{\"L\":3},\"pt:{\"d\":3},\"ct\":{},\"_t\":44,\"_v\":\"2\"}"; + var invalidString1 = "ipt={\"v\":{\"L\":3},\"pt:{\"d\":3},\"ct\":{},\"_t\":44,\"_v\":\"2\"}"; - var invalidHeader2a = new SetCookieHeaderValue("expires", "Sun"); - var invalidHeader2b = new SetCookieHeaderValue("domain", "domain1"); - var invalidString2 = "ipt={\"v\":{\"L\":3},\"pt\":{d\":3},\"ct\":{},\"_t\":44,\"_v\":\"2\"}; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1"; + var invalidHeader2a = new SetCookieHeaderValue("expires", "Sun"); + var invalidHeader2b = new SetCookieHeaderValue("domain", "domain1"); + var invalidString2 = "ipt={\"v\":{\"L\":3},\"pt\":{d\":3},\"ct\":{},\"_t\":44,\"_v\":\"2\"}; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1"; - var invalidHeader3 = new SetCookieHeaderValue("domain", "domain1") - { - Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), - }; - var invalidString3 = "ipt={\"v\":{\"L\":3},\"pt\":{\"d:3},\"ct\":{},\"_t\":44,\"_v\":\"2\"}; domain=domain1; expires=Sun, 06 Nov 1994 08:49:37 GMT"; - - dataset.Add(null, new[] { invalidString1 }); - dataset.Add(new[] { invalidHeader2a, invalidHeader2b }.ToList(), new[] { invalidString2 }); - dataset.Add(new[] { invalidHeader3 }.ToList(), new[] { invalidString3 }); - dataset.Add(new[] { header1 }.ToList(), new[] { string1, invalidString1 }); - dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1, null, "", " ", ",", " , ", string1 }); - dataset.Add(new[] { header1 }.ToList(), new[] { string1 + ", " + invalidString1 }); - dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1 + ", " + string1 }); - dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { invalidString1, string1, string2, string3, string4 }); - dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, invalidString1, string2, string3, string4 }); - dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, invalidString1, string3, string4 }); - dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, invalidString1, string4 }); - dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4, invalidString1 }); - dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", invalidString1, string1, string2, string3, string4) }); - dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, invalidString1, string2, string3, string4) }); - dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, invalidString1, string3, string4) }); - dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, invalidString1, string4) }); - dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4, invalidString1) }); - - return dataset; - } + var invalidHeader3 = new SetCookieHeaderValue("domain", "domain1") + { + Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), + }; + var invalidString3 = "ipt={\"v\":{\"L\":3},\"pt\":{\"d:3},\"ct\":{},\"_t\":44,\"_v\":\"2\"}; domain=domain1; expires=Sun, 06 Nov 1994 08:49:37 GMT"; + + dataset.Add(null, new[] { invalidString1 }); + dataset.Add(new[] { invalidHeader2a, invalidHeader2b }.ToList(), new[] { invalidString2 }); + dataset.Add(new[] { invalidHeader3 }.ToList(), new[] { invalidString3 }); + dataset.Add(new[] { header1 }.ToList(), new[] { string1, invalidString1 }); + dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1, null, "", " ", ",", " , ", string1 }); + dataset.Add(new[] { header1 }.ToList(), new[] { string1 + ", " + invalidString1 }); + dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1 + ", " + string1 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { invalidString1, string1, string2, string3, string4 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, invalidString1, string2, string3, string4 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, invalidString1, string3, string4 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, invalidString1, string4 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4, invalidString1 }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", invalidString1, string1, string2, string3, string4) }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, invalidString1, string2, string3, string4) }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, invalidString1, string3, string4) }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, invalidString1, string4) }); + dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4, invalidString1) }); + + return dataset; } + } - [Fact] - public void SetCookieHeaderValue_CtorThrowsOnNullName() - { - Assert.Throws(() => new SetCookieHeaderValue(null, "value")); - } + [Fact] + public void SetCookieHeaderValue_CtorThrowsOnNullName() + { + Assert.Throws(() => new SetCookieHeaderValue(null, "value")); + } - [Theory] - [MemberData(nameof(InvalidCookieNames))] - public void SetCookieHeaderValue_CtorThrowsOnInvalidName(string name) - { - Assert.Throws(() => new SetCookieHeaderValue(name, "value")); - } + [Theory] + [MemberData(nameof(InvalidCookieNames))] + public void SetCookieHeaderValue_CtorThrowsOnInvalidName(string name) + { + Assert.Throws(() => new SetCookieHeaderValue(name, "value")); + } - [Theory] - [MemberData(nameof(InvalidCookieValues))] - public void SetCookieHeaderValue_CtorThrowsOnInvalidValue(string value) - { - Assert.Throws(() => new SetCookieHeaderValue("name", value)); - } + [Theory] + [MemberData(nameof(InvalidCookieValues))] + public void SetCookieHeaderValue_CtorThrowsOnInvalidValue(string value) + { + Assert.Throws(() => new SetCookieHeaderValue("name", value)); + } - [Fact] - public void SetCookieHeaderValue_Ctor1_InitializesCorrectly() - { - var header = new SetCookieHeaderValue("cookie"); - Assert.Equal("cookie", header.Name); - Assert.Equal(string.Empty, header.Value); - } + [Fact] + public void SetCookieHeaderValue_Ctor1_InitializesCorrectly() + { + var header = new SetCookieHeaderValue("cookie"); + Assert.Equal("cookie", header.Name); + Assert.Equal(string.Empty, header.Value); + } - [Theory] - [InlineData("name", "")] - [InlineData("name", "value")] - [InlineData("name", "\"acb\"")] - public void SetCookieHeaderValue_Ctor2InitializesCorrectly(string name, string value) - { - var header = new SetCookieHeaderValue(name, value); - Assert.Equal(name, header.Name); - Assert.Equal(value, header.Value); - } + [Theory] + [InlineData("name", "")] + [InlineData("name", "value")] + [InlineData("name", "\"acb\"")] + public void SetCookieHeaderValue_Ctor2InitializesCorrectly(string name, string value) + { + var header = new SetCookieHeaderValue(name, value); + Assert.Equal(name, header.Name); + Assert.Equal(value, header.Value); + } - [Fact] - public void SetCookieHeaderValue_Value() - { - var cookie = new SetCookieHeaderValue("name"); - Assert.Equal(string.Empty, cookie.Value); + [Fact] + public void SetCookieHeaderValue_Value() + { + var cookie = new SetCookieHeaderValue("name"); + Assert.Equal(string.Empty, cookie.Value); - cookie.Value = "value1"; - Assert.Equal("value1", cookie.Value); - } + cookie.Value = "value1"; + Assert.Equal("value1", cookie.Value); + } - [Theory] - [MemberData(nameof(SetCookieHeaderDataSet))] - public void SetCookieHeaderValue_ToString(SetCookieHeaderValue input, string expectedValue) - { - Assert.Equal(expectedValue, input.ToString()); - } + [Theory] + [MemberData(nameof(SetCookieHeaderDataSet))] + public void SetCookieHeaderValue_ToString(SetCookieHeaderValue input, string expectedValue) + { + Assert.Equal(expectedValue, input.ToString()); + } - [Theory] - [MemberData(nameof(SetCookieHeaderDataSet))] - public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue input, string expectedValue) - { - var builder = new StringBuilder(); + [Theory] + [MemberData(nameof(SetCookieHeaderDataSet))] + public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue input, string expectedValue) + { + var builder = new StringBuilder(); - input.AppendToStringBuilder(builder); + input.AppendToStringBuilder(builder); - Assert.Equal(expectedValue, builder.ToString()); - } + Assert.Equal(expectedValue, builder.ToString()); + } - [Theory] - [MemberData(nameof(SetCookieHeaderDataSet))] - public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue) - { - var header = SetCookieHeaderValue.Parse(expectedValue); + [Theory] + [MemberData(nameof(SetCookieHeaderDataSet))] + public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue) + { + var header = SetCookieHeaderValue.Parse(expectedValue); - Assert.Equal(cookie, header); - Assert.Equal(expectedValue, header.ToString()); - } + Assert.Equal(cookie, header); + Assert.Equal(expectedValue, header.ToString()); + } - [Theory] - [MemberData(nameof(SetCookieHeaderDataSet))] - public void SetCookieHeaderValue_TryParse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue) - { - Assert.True(SetCookieHeaderValue.TryParse(expectedValue, out var header)); + [Theory] + [MemberData(nameof(SetCookieHeaderDataSet))] + public void SetCookieHeaderValue_TryParse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue) + { + Assert.True(SetCookieHeaderValue.TryParse(expectedValue, out var header)); - Assert.Equal(cookie, header); - Assert.Equal(expectedValue, header!.ToString()); - } + Assert.Equal(cookie, header); + Assert.Equal(expectedValue, header!.ToString()); + } - [Theory] - [MemberData(nameof(InvalidSetCookieHeaderDataSet))] - public void SetCookieHeaderValue_Parse_RejectsInvalidValues(string value) - { - Assert.Throws(() => SetCookieHeaderValue.Parse(value)); - } + [Theory] + [MemberData(nameof(InvalidSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_Parse_RejectsInvalidValues(string value) + { + Assert.Throws(() => SetCookieHeaderValue.Parse(value)); + } - [Theory] - [MemberData(nameof(InvalidSetCookieHeaderDataSet))] - public void SetCookieHeaderValue_TryParse_RejectsInvalidValues(string value) - { - Assert.False(SetCookieHeaderValue.TryParse(value, out var _)); - } + [Theory] + [MemberData(nameof(InvalidSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_TryParse_RejectsInvalidValues(string value) + { + Assert.False(SetCookieHeaderValue.TryParse(value, out var _)); + } - [Theory] - [MemberData(nameof(ListOfSetCookieHeaderDataSet))] - public void SetCookieHeaderValue_ParseList_AcceptsValidValues(IList cookies, string[] input) - { - var results = SetCookieHeaderValue.ParseList(input); + [Theory] + [MemberData(nameof(ListOfSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_ParseList_AcceptsValidValues(IList cookies, string[] input) + { + var results = SetCookieHeaderValue.ParseList(input); - Assert.Equal(cookies, results); - } + Assert.Equal(cookies, results); + } - [Theory] - [MemberData(nameof(ListOfSetCookieHeaderDataSet))] - public void SetCookieHeaderValue_TryParseList_AcceptsValidValues(IList cookies, string[] input) - { - bool result = SetCookieHeaderValue.TryParseList(input, out var results); - Assert.True(result); + [Theory] + [MemberData(nameof(ListOfSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_TryParseList_AcceptsValidValues(IList cookies, string[] input) + { + bool result = SetCookieHeaderValue.TryParseList(input, out var results); + Assert.True(result); - Assert.Equal(cookies, results); - } + Assert.Equal(cookies, results); + } - [Fact] - public void SetCookieHeaderValue_TryParse_ExtensionOrderDoesntMatter() - { - string cookieHeaderValue1 = "cookiename=value; extensionname1=value; extensionname2=value;"; - string cookieHeaderValue2 = "cookiename=value; extensionname2=value; extensionname1=value;"; + [Fact] + public void SetCookieHeaderValue_TryParse_ExtensionOrderDoesntMatter() + { + string cookieHeaderValue1 = "cookiename=value; extensionname1=value; extensionname2=value;"; + string cookieHeaderValue2 = "cookiename=value; extensionname2=value; extensionname1=value;"; - SetCookieHeaderValue.TryParse(cookieHeaderValue1, out var setCookieHeaderValue1); - SetCookieHeaderValue.TryParse(cookieHeaderValue2, out var setCookieHeaderValue2); + SetCookieHeaderValue.TryParse(cookieHeaderValue1, out var setCookieHeaderValue1); + SetCookieHeaderValue.TryParse(cookieHeaderValue2, out var setCookieHeaderValue2); - Assert.Equal(setCookieHeaderValue1, setCookieHeaderValue2); - } + Assert.Equal(setCookieHeaderValue1, setCookieHeaderValue2); + } - [Theory] - [MemberData(nameof(ListOfSetCookieHeaderDataSet))] - public void SetCookieHeaderValue_ParseStrictList_AcceptsValidValues(IList cookies, string[] input) - { - var results = SetCookieHeaderValue.ParseStrictList(input); + [Theory] + [MemberData(nameof(ListOfSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_ParseStrictList_AcceptsValidValues(IList cookies, string[] input) + { + var results = SetCookieHeaderValue.ParseStrictList(input); - Assert.Equal(cookies, results); - } + Assert.Equal(cookies, results); + } - [Theory] - [MemberData(nameof(ListOfSetCookieHeaderDataSet))] - public void SetCookieHeaderValue_TryParseStrictList_AcceptsValidValues(IList cookies, string[] input) - { - bool result = SetCookieHeaderValue.TryParseStrictList(input, out var results); - Assert.True(result); + [Theory] + [MemberData(nameof(ListOfSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_TryParseStrictList_AcceptsValidValues(IList cookies, string[] input) + { + bool result = SetCookieHeaderValue.TryParseStrictList(input, out var results); + Assert.True(result); - Assert.Equal(cookies, results); - } + Assert.Equal(cookies, results); + } - [Theory] - [MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))] - public void SetCookieHeaderValue_ParseList_ExcludesInvalidValues(IList cookies, string[] input) - { - var results = SetCookieHeaderValue.ParseList(input); - // ParseList always returns a list, even if empty. TryParseList may return null (via out). - Assert.Equal(cookies ?? new List(), results); - } + [Theory] + [MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_ParseList_ExcludesInvalidValues(IList cookies, string[] input) + { + var results = SetCookieHeaderValue.ParseList(input); + // ParseList always returns a list, even if empty. TryParseList may return null (via out). + Assert.Equal(cookies ?? new List(), results); + } - [Theory] - [MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))] - public void SetCookieHeaderValue_TryParseList_ExcludesInvalidValues(IList cookies, string[] input) - { - bool result = SetCookieHeaderValue.TryParseList(input, out var results); - Assert.Equal(cookies, results); - Assert.Equal(cookies?.Count > 0, result); - } + [Theory] + [MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_TryParseList_ExcludesInvalidValues(IList cookies, string[] input) + { + bool result = SetCookieHeaderValue.TryParseList(input, out var results); + Assert.Equal(cookies, results); + Assert.Equal(cookies?.Count > 0, result); + } - [Theory] - [MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))] - public void SetCookieHeaderValue_ParseStrictList_ThrowsForAnyInvalidValues( + [Theory] + [MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_ParseStrictList_ThrowsForAnyInvalidValues( #pragma warning disable xUnit1026 // Theory methods should use all of their parameters IList cookies, #pragma warning restore xUnit1026 // Theory methods should use all of their parameters string[] input) - { - Assert.Throws(() => SetCookieHeaderValue.ParseStrictList(input)); - } + { + Assert.Throws(() => SetCookieHeaderValue.ParseStrictList(input)); + } - [Theory] - [MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))] - public void SetCookieHeaderValue_TryParseStrictList_FailsForAnyInvalidValues( + [Theory] + [MemberData(nameof(ListWithInvalidSetCookieHeaderDataSet))] + public void SetCookieHeaderValue_TryParseStrictList_FailsForAnyInvalidValues( #pragma warning disable xUnit1026 // Theory methods should use all of their parameters IList cookies, #pragma warning restore xUnit1026 // Theory methods should use all of their parameters string[] input) - { - bool result = SetCookieHeaderValue.TryParseStrictList(input, out var results); - Assert.Null(results); - Assert.False(result); - } + { + bool result = SetCookieHeaderValue.TryParseStrictList(input, out var results); + Assert.Null(results); + Assert.False(result); } } diff --git a/src/Http/Headers/test/StringWithQualityHeaderValueComparerTest.cs b/src/Http/Headers/test/StringWithQualityHeaderValueComparerTest.cs index 12e5da6bec..bf9ecaa59f 100644 --- a/src/Http/Headers/test/StringWithQualityHeaderValueComparerTest.cs +++ b/src/Http/Headers/test/StringWithQualityHeaderValueComparerTest.cs @@ -5,15 +5,15 @@ using System.Collections.Generic; using System.Linq; using Xunit; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +public class StringWithQualityHeaderValueComparerTest { - public class StringWithQualityHeaderValueComparerTest + public static TheoryData StringWithQualityHeaderValueComparerTestsBeforeAfterSortedValues { - public static TheoryData StringWithQualityHeaderValueComparerTestsBeforeAfterSortedValues + get { - get - { - return new TheoryData + return new TheoryData { { new string[] @@ -46,19 +46,18 @@ namespace Microsoft.Net.Http.Headers } } }; - } } + } - [Theory] - [MemberData(nameof(StringWithQualityHeaderValueComparerTestsBeforeAfterSortedValues))] - public void SortStringWithQualityHeaderValuesByQFactor_SortsCorrectly(IEnumerable unsorted, IEnumerable expectedSorted) - { - var unsortedValues = StringWithQualityHeaderValue.ParseList(unsorted.ToList()); - var expectedSortedValues = StringWithQualityHeaderValue.ParseList(expectedSorted.ToList()); + [Theory] + [MemberData(nameof(StringWithQualityHeaderValueComparerTestsBeforeAfterSortedValues))] + public void SortStringWithQualityHeaderValuesByQFactor_SortsCorrectly(IEnumerable unsorted, IEnumerable expectedSorted) + { + var unsortedValues = StringWithQualityHeaderValue.ParseList(unsorted.ToList()); + var expectedSortedValues = StringWithQualityHeaderValue.ParseList(expectedSorted.ToList()); - var actualSorted = unsortedValues.OrderByDescending(k => k, StringWithQualityHeaderValueComparer.QualityComparer).ToList(); + var actualSorted = unsortedValues.OrderByDescending(k => k, StringWithQualityHeaderValueComparer.QualityComparer).ToList(); - Assert.True(expectedSortedValues.SequenceEqual(actualSorted)); - } + Assert.True(expectedSortedValues.SequenceEqual(actualSorted)); } } diff --git a/src/Http/Headers/test/StringWithQualityHeaderValueTest.cs b/src/Http/Headers/test/StringWithQualityHeaderValueTest.cs index bf9770d3b6..1e7bb73f6c 100644 --- a/src/Http/Headers/test/StringWithQualityHeaderValueTest.cs +++ b/src/Http/Headers/test/StringWithQualityHeaderValueTest.cs @@ -6,208 +6,208 @@ using System.Collections.Generic; using System.Linq; using Xunit; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +public class StringWithQualityHeaderValueTest { - public class StringWithQualityHeaderValueTest + [Fact] + public void Ctor_StringOnlyOverload_MatchExpectation() { - [Fact] - public void Ctor_StringOnlyOverload_MatchExpectation() - { - var value = new StringWithQualityHeaderValue("token"); - Assert.Equal("token", value.Value); - Assert.Null(value.Quality); + var value = new StringWithQualityHeaderValue("token"); + Assert.Equal("token", value.Value); + Assert.Null(value.Quality); - Assert.Throws(() => new StringWithQualityHeaderValue(null)); - Assert.Throws(() => new StringWithQualityHeaderValue("")); - Assert.Throws(() => new StringWithQualityHeaderValue("in valid")); - } + Assert.Throws(() => new StringWithQualityHeaderValue(null)); + Assert.Throws(() => new StringWithQualityHeaderValue("")); + Assert.Throws(() => new StringWithQualityHeaderValue("in valid")); + } - [Fact] - public void Ctor_StringWithQualityOverload_MatchExpectation() - { - var value = new StringWithQualityHeaderValue("token", 0.5); - Assert.Equal("token", value.Value); - Assert.Equal(0.5, value.Quality); + [Fact] + public void Ctor_StringWithQualityOverload_MatchExpectation() + { + var value = new StringWithQualityHeaderValue("token", 0.5); + Assert.Equal("token", value.Value); + Assert.Equal(0.5, value.Quality); - Assert.Throws(() => new StringWithQualityHeaderValue(null, 0.1)); - Assert.Throws(() => new StringWithQualityHeaderValue("", 0.1)); - Assert.Throws(() => new StringWithQualityHeaderValue("in valid", 0.1)); + Assert.Throws(() => new StringWithQualityHeaderValue(null, 0.1)); + Assert.Throws(() => new StringWithQualityHeaderValue("", 0.1)); + Assert.Throws(() => new StringWithQualityHeaderValue("in valid", 0.1)); - Assert.Throws(() => new StringWithQualityHeaderValue("t", 1.1)); - Assert.Throws(() => new StringWithQualityHeaderValue("t", -0.1)); - } + Assert.Throws(() => new StringWithQualityHeaderValue("t", 1.1)); + Assert.Throws(() => new StringWithQualityHeaderValue("t", -0.1)); + } - [Fact] - public void ToString_UseDifferentValues_AllSerializedCorrectly() - { - var value = new StringWithQualityHeaderValue("token"); - Assert.Equal("token", value.ToString()); + [Fact] + public void ToString_UseDifferentValues_AllSerializedCorrectly() + { + var value = new StringWithQualityHeaderValue("token"); + Assert.Equal("token", value.ToString()); - value = new StringWithQualityHeaderValue("token", 0.1); - Assert.Equal("token; q=0.1", value.ToString()); + value = new StringWithQualityHeaderValue("token", 0.1); + Assert.Equal("token; q=0.1", value.ToString()); - value = new StringWithQualityHeaderValue("token", 0); - Assert.Equal("token; q=0.0", value.ToString()); + value = new StringWithQualityHeaderValue("token", 0); + Assert.Equal("token; q=0.0", value.ToString()); - value = new StringWithQualityHeaderValue("token", 1); - Assert.Equal("token; q=1.0", value.ToString()); + value = new StringWithQualityHeaderValue("token", 1); + Assert.Equal("token; q=1.0", value.ToString()); - // Note that the quality value gets rounded - value = new StringWithQualityHeaderValue("token", 0.56789); - Assert.Equal("token; q=0.568", value.ToString()); - } + // Note that the quality value gets rounded + value = new StringWithQualityHeaderValue("token", 0.56789); + Assert.Equal("token; q=0.568", value.ToString()); + } - [Fact] - public void GetHashCode_UseSameAndDifferentValues_SameOrDifferentHashCodes() - { - var value1 = new StringWithQualityHeaderValue("t", 0.123); - var value2 = new StringWithQualityHeaderValue("t", 0.123); - var value3 = new StringWithQualityHeaderValue("T", 0.123); - var value4 = new StringWithQualityHeaderValue("t"); - var value5 = new StringWithQualityHeaderValue("x", 0.123); - var value6 = new StringWithQualityHeaderValue("t", 0.5); - var value7 = new StringWithQualityHeaderValue("t", 0.1234); - var value8 = new StringWithQualityHeaderValue("T"); - var value9 = new StringWithQualityHeaderValue("x"); - - Assert.Equal(value1.GetHashCode(), value2.GetHashCode()); - Assert.Equal(value1.GetHashCode(), value3.GetHashCode()); - Assert.NotEqual(value1.GetHashCode(), value4.GetHashCode()); - Assert.NotEqual(value1.GetHashCode(), value5.GetHashCode()); - Assert.NotEqual(value1.GetHashCode(), value6.GetHashCode()); - Assert.NotEqual(value1.GetHashCode(), value7.GetHashCode()); - Assert.Equal(value4.GetHashCode(), value8.GetHashCode()); - Assert.NotEqual(value4.GetHashCode(), value9.GetHashCode()); - } - - [Fact] - public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions() - { - var value1 = new StringWithQualityHeaderValue("t", 0.123); - var value2 = new StringWithQualityHeaderValue("t", 0.123); - var value3 = new StringWithQualityHeaderValue("T", 0.123); - var value4 = new StringWithQualityHeaderValue("t"); - var value5 = new StringWithQualityHeaderValue("x", 0.123); - var value6 = new StringWithQualityHeaderValue("t", 0.5); - var value7 = new StringWithQualityHeaderValue("t", 0.1234); - var value8 = new StringWithQualityHeaderValue("T"); - var value9 = new StringWithQualityHeaderValue("x"); - - Assert.False(value1.Equals(null), "t; q=0.123 vs. "); - Assert.True(value1!.Equals(value2), "t; q=0.123 vs. t; q=0.123"); - Assert.True(value1.Equals(value3), "t; q=0.123 vs. T; q=0.123"); - Assert.False(value1.Equals(value4), "t; q=0.123 vs. t"); - Assert.False(value4.Equals(value1), "t vs. t; q=0.123"); - Assert.False(value1.Equals(value5), "t; q=0.123 vs. x; q=0.123"); - Assert.False(value1.Equals(value6), "t; q=0.123 vs. t; q=0.5"); - Assert.False(value1.Equals(value7), "t; q=0.123 vs. t; q=0.1234"); - Assert.True(value4.Equals(value8), "t vs. T"); - Assert.False(value4.Equals(value9), "t vs. T"); - } - - [Fact] - public void Parse_SetOfValidValueStrings_ParsedCorrectly() - { - CheckValidParse("text", new StringWithQualityHeaderValue("text")); - CheckValidParse("text;q=0.5", new StringWithQualityHeaderValue("text", 0.5)); - CheckValidParse("text ; q = 0.5", new StringWithQualityHeaderValue("text", 0.5)); - CheckValidParse("\r\n text ; q = 0.5 ", new StringWithQualityHeaderValue("text", 0.5)); - CheckValidParse(" text ", new StringWithQualityHeaderValue("text")); - CheckValidParse(" \r\n text \r\n ; \r\n q = 0.123", new StringWithQualityHeaderValue("text", 0.123)); - CheckValidParse(" text ; q = 0.123 ", new StringWithQualityHeaderValue("text", 0.123)); - CheckValidParse("text;q=1 ", new StringWithQualityHeaderValue("text", 1)); - CheckValidParse("*", new StringWithQualityHeaderValue("*")); - CheckValidParse("*;q=0.7", new StringWithQualityHeaderValue("*", 0.7)); - CheckValidParse(" t", new StringWithQualityHeaderValue("t")); - CheckValidParse("t;q=0.", new StringWithQualityHeaderValue("t", 0)); - CheckValidParse("t;q=1.", new StringWithQualityHeaderValue("t", 1)); - CheckValidParse("t;q=1.000", new StringWithQualityHeaderValue("t", 1)); - CheckValidParse("t;q=0.12345678", new StringWithQualityHeaderValue("t", 0.12345678)); - CheckValidParse("t ; q = 0", new StringWithQualityHeaderValue("t", 0)); - CheckValidParse("iso-8859-5", new StringWithQualityHeaderValue("iso-8859-5")); - CheckValidParse("unicode-1-1; q=0.8", new StringWithQualityHeaderValue("unicode-1-1", 0.8)); - } - - [Theory] - [InlineData("text,")] - [InlineData("\r\n text ; q = 0.5, next_text ")] - [InlineData(" text,next_text ")] - [InlineData(" ,, text, , ,next")] - [InlineData(" ,, text, , ,")] - [InlineData(", \r\n text \r\n ; \r\n q = 0.123")] - [InlineData("teäxt")] - [InlineData("text会")] - [InlineData("会")] - [InlineData("t;q=会")] - [InlineData("t;q=")] - [InlineData("t;q")] - [InlineData("t;会=1")] - [InlineData("t;q会=1")] - [InlineData("t y")] - [InlineData("t;q=1 y")] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - [InlineData(" ,,")] - [InlineData("t;q=-1")] - [InlineData("t;q=1.00001")] - [InlineData("t;")] - [InlineData("t;;q=1")] - [InlineData("t;q=a")] - [InlineData("t;qa")] - [InlineData("t;q1")] - [InlineData("integer_part_too_long;q=01")] - [InlineData("integer_part_too_long;q=01.0")] - [InlineData("decimal_part_too_long;q=0.123456789")] - [InlineData("decimal_part_too_long;q=0.123456789 ")] - [InlineData("no_integer_part;q=.1")] - public void Parse_SetOfInvalidValueStrings_Throws(string input) - { - Assert.Throws(() => StringWithQualityHeaderValue.Parse(input)); - } + [Fact] + public void GetHashCode_UseSameAndDifferentValues_SameOrDifferentHashCodes() + { + var value1 = new StringWithQualityHeaderValue("t", 0.123); + var value2 = new StringWithQualityHeaderValue("t", 0.123); + var value3 = new StringWithQualityHeaderValue("T", 0.123); + var value4 = new StringWithQualityHeaderValue("t"); + var value5 = new StringWithQualityHeaderValue("x", 0.123); + var value6 = new StringWithQualityHeaderValue("t", 0.5); + var value7 = new StringWithQualityHeaderValue("t", 0.1234); + var value8 = new StringWithQualityHeaderValue("T"); + var value9 = new StringWithQualityHeaderValue("x"); + + Assert.Equal(value1.GetHashCode(), value2.GetHashCode()); + Assert.Equal(value1.GetHashCode(), value3.GetHashCode()); + Assert.NotEqual(value1.GetHashCode(), value4.GetHashCode()); + Assert.NotEqual(value1.GetHashCode(), value5.GetHashCode()); + Assert.NotEqual(value1.GetHashCode(), value6.GetHashCode()); + Assert.NotEqual(value1.GetHashCode(), value7.GetHashCode()); + Assert.Equal(value4.GetHashCode(), value8.GetHashCode()); + Assert.NotEqual(value4.GetHashCode(), value9.GetHashCode()); + } - [Fact] - public void TryParse_SetOfValidValueStrings_ParsedCorrectly() - { - CheckValidTryParse("text", new StringWithQualityHeaderValue("text")); - CheckValidTryParse("text;q=0.5", new StringWithQualityHeaderValue("text", 0.5)); - CheckValidTryParse("text ; q = 0.5", new StringWithQualityHeaderValue("text", 0.5)); - CheckValidTryParse("\r\n text ; q = 0.5 ", new StringWithQualityHeaderValue("text", 0.5)); - CheckValidTryParse(" text ", new StringWithQualityHeaderValue("text")); - CheckValidTryParse(" \r\n text \r\n ; \r\n q = 0.123", new StringWithQualityHeaderValue("text", 0.123)); - } - - [Fact] - public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() - { - CheckInvalidTryParse("text,"); - CheckInvalidTryParse("\r\n text ; q = 0.5, next_text "); - CheckInvalidTryParse(" text,next_text "); - CheckInvalidTryParse(" ,, text, , ,next"); - CheckInvalidTryParse(" ,, text, , ,"); - CheckInvalidTryParse(", \r\n text \r\n ; \r\n q = 0.123"); - CheckInvalidTryParse("teäxt"); - CheckInvalidTryParse("text会"); - CheckInvalidTryParse("会"); - CheckInvalidTryParse("t;q=会"); - CheckInvalidTryParse("t;q="); - CheckInvalidTryParse("t;q"); - CheckInvalidTryParse("t;会=1"); - CheckInvalidTryParse("t;q会=1"); - CheckInvalidTryParse("t y"); - CheckInvalidTryParse("t;q=1 y"); - - CheckInvalidTryParse(null); - CheckInvalidTryParse(string.Empty); - CheckInvalidTryParse(" "); - CheckInvalidTryParse(" ,,"); - } - - [Fact] - public void ParseList_SetOfValidValueStrings_ParsedCorrectly() + [Fact] + public void Equals_UseSameAndDifferentRanges_EqualOrNotEqualNoExceptions() + { + var value1 = new StringWithQualityHeaderValue("t", 0.123); + var value2 = new StringWithQualityHeaderValue("t", 0.123); + var value3 = new StringWithQualityHeaderValue("T", 0.123); + var value4 = new StringWithQualityHeaderValue("t"); + var value5 = new StringWithQualityHeaderValue("x", 0.123); + var value6 = new StringWithQualityHeaderValue("t", 0.5); + var value7 = new StringWithQualityHeaderValue("t", 0.1234); + var value8 = new StringWithQualityHeaderValue("T"); + var value9 = new StringWithQualityHeaderValue("x"); + + Assert.False(value1.Equals(null), "t; q=0.123 vs. "); + Assert.True(value1!.Equals(value2), "t; q=0.123 vs. t; q=0.123"); + Assert.True(value1.Equals(value3), "t; q=0.123 vs. T; q=0.123"); + Assert.False(value1.Equals(value4), "t; q=0.123 vs. t"); + Assert.False(value4.Equals(value1), "t vs. t; q=0.123"); + Assert.False(value1.Equals(value5), "t; q=0.123 vs. x; q=0.123"); + Assert.False(value1.Equals(value6), "t; q=0.123 vs. t; q=0.5"); + Assert.False(value1.Equals(value7), "t; q=0.123 vs. t; q=0.1234"); + Assert.True(value4.Equals(value8), "t vs. T"); + Assert.False(value4.Equals(value9), "t vs. T"); + } + + [Fact] + public void Parse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidParse("text", new StringWithQualityHeaderValue("text")); + CheckValidParse("text;q=0.5", new StringWithQualityHeaderValue("text", 0.5)); + CheckValidParse("text ; q = 0.5", new StringWithQualityHeaderValue("text", 0.5)); + CheckValidParse("\r\n text ; q = 0.5 ", new StringWithQualityHeaderValue("text", 0.5)); + CheckValidParse(" text ", new StringWithQualityHeaderValue("text")); + CheckValidParse(" \r\n text \r\n ; \r\n q = 0.123", new StringWithQualityHeaderValue("text", 0.123)); + CheckValidParse(" text ; q = 0.123 ", new StringWithQualityHeaderValue("text", 0.123)); + CheckValidParse("text;q=1 ", new StringWithQualityHeaderValue("text", 1)); + CheckValidParse("*", new StringWithQualityHeaderValue("*")); + CheckValidParse("*;q=0.7", new StringWithQualityHeaderValue("*", 0.7)); + CheckValidParse(" t", new StringWithQualityHeaderValue("t")); + CheckValidParse("t;q=0.", new StringWithQualityHeaderValue("t", 0)); + CheckValidParse("t;q=1.", new StringWithQualityHeaderValue("t", 1)); + CheckValidParse("t;q=1.000", new StringWithQualityHeaderValue("t", 1)); + CheckValidParse("t;q=0.12345678", new StringWithQualityHeaderValue("t", 0.12345678)); + CheckValidParse("t ; q = 0", new StringWithQualityHeaderValue("t", 0)); + CheckValidParse("iso-8859-5", new StringWithQualityHeaderValue("iso-8859-5")); + CheckValidParse("unicode-1-1; q=0.8", new StringWithQualityHeaderValue("unicode-1-1", 0.8)); + } + + [Theory] + [InlineData("text,")] + [InlineData("\r\n text ; q = 0.5, next_text ")] + [InlineData(" text,next_text ")] + [InlineData(" ,, text, , ,next")] + [InlineData(" ,, text, , ,")] + [InlineData(", \r\n text \r\n ; \r\n q = 0.123")] + [InlineData("teäxt")] + [InlineData("text会")] + [InlineData("会")] + [InlineData("t;q=会")] + [InlineData("t;q=")] + [InlineData("t;q")] + [InlineData("t;会=1")] + [InlineData("t;q会=1")] + [InlineData("t y")] + [InlineData("t;q=1 y")] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData(" ,,")] + [InlineData("t;q=-1")] + [InlineData("t;q=1.00001")] + [InlineData("t;")] + [InlineData("t;;q=1")] + [InlineData("t;q=a")] + [InlineData("t;qa")] + [InlineData("t;q1")] + [InlineData("integer_part_too_long;q=01")] + [InlineData("integer_part_too_long;q=01.0")] + [InlineData("decimal_part_too_long;q=0.123456789")] + [InlineData("decimal_part_too_long;q=0.123456789 ")] + [InlineData("no_integer_part;q=.1")] + public void Parse_SetOfInvalidValueStrings_Throws(string input) + { + Assert.Throws(() => StringWithQualityHeaderValue.Parse(input)); + } + + [Fact] + public void TryParse_SetOfValidValueStrings_ParsedCorrectly() + { + CheckValidTryParse("text", new StringWithQualityHeaderValue("text")); + CheckValidTryParse("text;q=0.5", new StringWithQualityHeaderValue("text", 0.5)); + CheckValidTryParse("text ; q = 0.5", new StringWithQualityHeaderValue("text", 0.5)); + CheckValidTryParse("\r\n text ; q = 0.5 ", new StringWithQualityHeaderValue("text", 0.5)); + CheckValidTryParse(" text ", new StringWithQualityHeaderValue("text")); + CheckValidTryParse(" \r\n text \r\n ; \r\n q = 0.123", new StringWithQualityHeaderValue("text", 0.123)); + } + + [Fact] + public void TryParse_SetOfInvalidValueStrings_ReturnsFalse() + { + CheckInvalidTryParse("text,"); + CheckInvalidTryParse("\r\n text ; q = 0.5, next_text "); + CheckInvalidTryParse(" text,next_text "); + CheckInvalidTryParse(" ,, text, , ,next"); + CheckInvalidTryParse(" ,, text, , ,"); + CheckInvalidTryParse(", \r\n text \r\n ; \r\n q = 0.123"); + CheckInvalidTryParse("teäxt"); + CheckInvalidTryParse("text会"); + CheckInvalidTryParse("会"); + CheckInvalidTryParse("t;q=会"); + CheckInvalidTryParse("t;q="); + CheckInvalidTryParse("t;q"); + CheckInvalidTryParse("t;会=1"); + CheckInvalidTryParse("t;q会=1"); + CheckInvalidTryParse("t y"); + CheckInvalidTryParse("t;q=1 y"); + + CheckInvalidTryParse(null); + CheckInvalidTryParse(string.Empty); + CheckInvalidTryParse(" "); + CheckInvalidTryParse(" ,,"); + } + + [Fact] + public void ParseList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] { - var inputs = new[] - { "", "text1", "text2,", @@ -219,10 +219,10 @@ namespace Microsoft.Net.Http.Headers "text7,text8;q=0.5", " text9 , text10 ; q = 0.5 ", }; - IList results = StringWithQualityHeaderValue.ParseList(inputs); + IList results = StringWithQualityHeaderValue.ParseList(inputs); - var expectedResults = new[] - { + var expectedResults = new[] + { new StringWithQualityHeaderValue("text1"), new StringWithQualityHeaderValue("text2"), new StringWithQualityHeaderValue("textA"), @@ -237,14 +237,14 @@ namespace Microsoft.Net.Http.Headers new StringWithQualityHeaderValue("text10", 0.5), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void ParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + [Fact] + public void ParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] { - var inputs = new[] - { "", "text1", "text2,", @@ -256,10 +256,10 @@ namespace Microsoft.Net.Http.Headers "text7,text8;q=0.5", " text9 , text10 ; q = 0.5 ", }; - IList results = StringWithQualityHeaderValue.ParseStrictList(inputs); + IList results = StringWithQualityHeaderValue.ParseStrictList(inputs); - var expectedResults = new[] - { + var expectedResults = new[] + { new StringWithQualityHeaderValue("text1"), new StringWithQualityHeaderValue("text2"), new StringWithQualityHeaderValue("textA"), @@ -274,14 +274,14 @@ namespace Microsoft.Net.Http.Headers new StringWithQualityHeaderValue("text10", 0.5), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void TryParseList_SetOfValidValueStrings_ParsedCorrectly() + [Fact] + public void TryParseList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] { - var inputs = new[] - { "", "text1", "text2,", @@ -293,10 +293,10 @@ namespace Microsoft.Net.Http.Headers "text7,text8;q=0.5", " text9 , text10 ; q = 0.5 ", }; - Assert.True(StringWithQualityHeaderValue.TryParseList(inputs, out var results)); + Assert.True(StringWithQualityHeaderValue.TryParseList(inputs, out var results)); - var expectedResults = new[] - { + var expectedResults = new[] + { new StringWithQualityHeaderValue("text1"), new StringWithQualityHeaderValue("text2"), new StringWithQualityHeaderValue("textA"), @@ -311,14 +311,14 @@ namespace Microsoft.Net.Http.Headers new StringWithQualityHeaderValue("text10", 0.5), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void TryParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + [Fact] + public void TryParseStrictList_SetOfValidValueStrings_ParsedCorrectly() + { + var inputs = new[] { - var inputs = new[] - { "", "text1", "text2,", @@ -330,10 +330,10 @@ namespace Microsoft.Net.Http.Headers "text7,text8;q=0.5", " text9 , text10 ; q = 0.5 ", }; - Assert.True(StringWithQualityHeaderValue.TryParseStrictList(inputs, out var results)); + Assert.True(StringWithQualityHeaderValue.TryParseStrictList(inputs, out var results)); - var expectedResults = new[] - { + var expectedResults = new[] + { new StringWithQualityHeaderValue("text1"), new StringWithQualityHeaderValue("text2"), new StringWithQualityHeaderValue("textA"), @@ -348,14 +348,14 @@ namespace Microsoft.Net.Http.Headers new StringWithQualityHeaderValue("text10", 0.5), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void ParseList_WithSomeInvalidValues_IgnoresInvalidValues() + [Fact] + public void ParseList_WithSomeInvalidValues_IgnoresInvalidValues() + { + var inputs = new[] { - var inputs = new[] - { "", "text1", "text 1", @@ -368,10 +368,10 @@ namespace Microsoft.Net.Http.Headers "text7,text8;q=0.5", " text9 , text10 ; q = 0.5 ", }; - var results = StringWithQualityHeaderValue.ParseList(inputs); + var results = StringWithQualityHeaderValue.ParseList(inputs); - var expectedResults = new[] - { + var expectedResults = new[] + { new StringWithQualityHeaderValue("text1"), new StringWithQualityHeaderValue("1"), new StringWithQualityHeaderValue("text2"), @@ -386,14 +386,14 @@ namespace Microsoft.Net.Http.Headers new StringWithQualityHeaderValue("text10", 0.5), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void ParseStrictList_WithSomeInvalidValues_Throws() + [Fact] + public void ParseStrictList_WithSomeInvalidValues_Throws() + { + var inputs = new[] { - var inputs = new[] - { "", "text1", "text 1", @@ -406,14 +406,14 @@ namespace Microsoft.Net.Http.Headers "text7,text8;q=0.5", " text9 , text10 ; q = 0.5 ", }; - Assert.Throws(() => StringWithQualityHeaderValue.ParseStrictList(inputs)); - } + Assert.Throws(() => StringWithQualityHeaderValue.ParseStrictList(inputs)); + } - [Fact] - public void TryParseList_WithSomeInvalidValues_IgnoresInvalidValues() + [Fact] + public void TryParseList_WithSomeInvalidValues_IgnoresInvalidValues() + { + var inputs = new[] { - var inputs = new[] - { "", "text1", "text 1", @@ -426,10 +426,10 @@ namespace Microsoft.Net.Http.Headers "text7,text8;q=0.5", " text9 , text10 ; q = 0.5 ", }; - Assert.True(StringWithQualityHeaderValue.TryParseList(inputs, out var results)); + Assert.True(StringWithQualityHeaderValue.TryParseList(inputs, out var results)); - var expectedResults = new[] - { + var expectedResults = new[] + { new StringWithQualityHeaderValue("text1"), new StringWithQualityHeaderValue("1"), new StringWithQualityHeaderValue("text2"), @@ -444,14 +444,14 @@ namespace Microsoft.Net.Http.Headers new StringWithQualityHeaderValue("text10", 0.5), }.ToList(); - Assert.Equal(expectedResults, results); - } + Assert.Equal(expectedResults, results); + } - [Fact] - public void TryParseStrictList_WithSomeInvalidValues_ReturnsFalse() + [Fact] + public void TryParseStrictList_WithSomeInvalidValues_ReturnsFalse() + { + var inputs = new[] { - var inputs = new[] - { "", "text1", "text 1", @@ -464,29 +464,28 @@ namespace Microsoft.Net.Http.Headers "text7,text8;q=0.5", " text9 , text10 ; q = 0.5 ", }; - Assert.False(StringWithQualityHeaderValue.TryParseStrictList(inputs, out var results)); - } - - #region Helper methods + Assert.False(StringWithQualityHeaderValue.TryParseStrictList(inputs, out var results)); + } - private void CheckValidParse(string? input, StringWithQualityHeaderValue expectedResult) - { - var result = StringWithQualityHeaderValue.Parse(input); - Assert.Equal(expectedResult, result); - } + #region Helper methods - private void CheckValidTryParse(string? input, StringWithQualityHeaderValue expectedResult) - { - Assert.True(StringWithQualityHeaderValue.TryParse(input, out var result)); - Assert.Equal(expectedResult, result); - } + private void CheckValidParse(string? input, StringWithQualityHeaderValue expectedResult) + { + var result = StringWithQualityHeaderValue.Parse(input); + Assert.Equal(expectedResult, result); + } - private void CheckInvalidTryParse(string? input) - { - Assert.False(StringWithQualityHeaderValue.TryParse(input, out var result)); - Assert.Null(result); - } + private void CheckValidTryParse(string? input, StringWithQualityHeaderValue expectedResult) + { + Assert.True(StringWithQualityHeaderValue.TryParse(input, out var result)); + Assert.Equal(expectedResult, result); + } - #endregion + private void CheckInvalidTryParse(string? input) + { + Assert.False(StringWithQualityHeaderValue.TryParse(input, out var result)); + Assert.Null(result); } + + #endregion } diff --git a/src/Http/Http.Abstractions/perf/Microbenchmarks/GetHeaderSplitBenchmark.cs b/src/Http/Http.Abstractions/perf/Microbenchmarks/GetHeaderSplitBenchmark.cs index 8f1c7b2e8f..55133975dc 100644 --- a/src/Http/Http.Abstractions/perf/Microbenchmarks/GetHeaderSplitBenchmark.cs +++ b/src/Http/Http.Abstractions/perf/Microbenchmarks/GetHeaderSplitBenchmark.cs @@ -8,55 +8,54 @@ using BenchmarkDotNet.Configs; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks +namespace Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks; + +public class GetHeaderSplitBenchmark { - public class GetHeaderSplitBenchmark - { - HeaderDictionary _dictionary; + HeaderDictionary _dictionary; - [GlobalSetup] - public void GlobalSetup() - { - var dict = new Dictionary() + [GlobalSetup] + public void GlobalSetup() + { + var dict = new Dictionary() { { "singleValue", new StringValues("single") }, { "singleValueQuoted", new StringValues("\"single\"") }, { "doubleValue", new StringValues(new [] { "first", "second" }) }, { "manyValue", new StringValues(new [] { "first", "second", "third", "fourth", "fifth", "sixth" }) } }; - _dictionary = new HeaderDictionary(dict); - } - - [Benchmark] - public void SplitSingleHeader() - { - var values = ParsingHelpers.GetHeaderSplit(_dictionary, "singleValue"); - if (values.Count != 1) - throw new Exception(); - } - - [Benchmark] - public void SplitSingleQuotedHeader() - { - var values = ParsingHelpers.GetHeaderSplit(_dictionary, "singleValueQuoted"); - if (values.Count != 1) - throw new Exception(); - } - - [Benchmark] - public void SplitDoubleHeader() - { - var values = ParsingHelpers.GetHeaderSplit(_dictionary, "doubleValue"); - if (values.Count != 2) - throw new Exception(); - } - - [Benchmark] - public void SplitManyHeaders() - { - var values = ParsingHelpers.GetHeaderSplit(_dictionary, "manyValue"); - if (values.Count != 6) - throw new Exception(); - } + _dictionary = new HeaderDictionary(dict); + } + + [Benchmark] + public void SplitSingleHeader() + { + var values = ParsingHelpers.GetHeaderSplit(_dictionary, "singleValue"); + if (values.Count != 1) + throw new Exception(); + } + + [Benchmark] + public void SplitSingleQuotedHeader() + { + var values = ParsingHelpers.GetHeaderSplit(_dictionary, "singleValueQuoted"); + if (values.Count != 1) + throw new Exception(); + } + + [Benchmark] + public void SplitDoubleHeader() + { + var values = ParsingHelpers.GetHeaderSplit(_dictionary, "doubleValue"); + if (values.Count != 2) + throw new Exception(); + } + + [Benchmark] + public void SplitManyHeaders() + { + var values = ParsingHelpers.GetHeaderSplit(_dictionary, "manyValue"); + if (values.Count != 6) + throw new Exception(); } } diff --git a/src/Http/Http.Abstractions/perf/Microbenchmarks/PathStringBenchmark.cs b/src/Http/Http.Abstractions/perf/Microbenchmarks/PathStringBenchmark.cs index 143644c0fc..0ef9871917 100644 --- a/src/Http/Http.Abstractions/perf/Microbenchmarks/PathStringBenchmark.cs +++ b/src/Http/Http.Abstractions/perf/Microbenchmarks/PathStringBenchmark.cs @@ -5,32 +5,31 @@ using System; using System.Collections.Generic; using BenchmarkDotNet.Attributes; -namespace Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks +namespace Microsoft.AspNetCore.Http.Abstractions.Microbenchmarks; + +public class PathStringBenchmark { - public class PathStringBenchmark - { - private const string TestPath = "/api/a%2Fb/c"; - private const string LongTestPath = "/thisMustBeAVeryLongPath/SoLongThatItCouldActuallyBeLargerToTheStackAllocThresholdValue/PathsShorterToThisAllocateLessOnHeapByUsingStackAllocation/api/a%20b"; - private const string LongTestPathEarlyPercent = "/t%20hisMustBeAVeryLongPath/SoLongButStillShorterToTheStackAllocThresholdValue/PathsShorterToThisAllocateLessOnHeap/api/a%20b"; + private const string TestPath = "/api/a%2Fb/c"; + private const string LongTestPath = "/thisMustBeAVeryLongPath/SoLongThatItCouldActuallyBeLargerToTheStackAllocThresholdValue/PathsShorterToThisAllocateLessOnHeapByUsingStackAllocation/api/a%20b"; + private const string LongTestPathEarlyPercent = "/t%20hisMustBeAVeryLongPath/SoLongButStillShorterToTheStackAllocThresholdValue/PathsShorterToThisAllocateLessOnHeap/api/a%20b"; - public IEnumerable TestPaths => new[] { TestPath, LongTestPath, LongTestPathEarlyPercent }; + public IEnumerable TestPaths => new[] { TestPath, LongTestPath, LongTestPathEarlyPercent }; - public IEnumerable TestUris => new[] { new Uri($"https://localhost:5001/{TestPath}"), new Uri($"https://localhost:5001/{LongTestPath}"), new Uri($"https://localhost:5001/{LongTestPathEarlyPercent}") }; + public IEnumerable TestUris => new[] { new Uri($"https://localhost:5001/{TestPath}"), new Uri($"https://localhost:5001/{LongTestPath}"), new Uri($"https://localhost:5001/{LongTestPathEarlyPercent}") }; - [Benchmark] - [ArgumentsSource(nameof(TestPaths))] - public string OnPathFromUriComponent(string testPath) - { - var pathString = PathString.FromUriComponent(testPath); - return pathString.Value; - } + [Benchmark] + [ArgumentsSource(nameof(TestPaths))] + public string OnPathFromUriComponent(string testPath) + { + var pathString = PathString.FromUriComponent(testPath); + return pathString.Value; + } - [Benchmark] - [ArgumentsSource(nameof(TestUris))] - public string OnUriFromUriComponent(Uri testUri) - { - var pathString = PathString.FromUriComponent(testUri); - return pathString.Value; - } + [Benchmark] + [ArgumentsSource(nameof(TestUris))] + public string OnUriFromUriComponent(Uri testUri) + { + var pathString = PathString.FromUriComponent(testUri); + return pathString.Value; } } diff --git a/src/Http/Http.Abstractions/src/BadHttpRequestException.cs b/src/Http/Http.Abstractions/src/BadHttpRequestException.cs index b1d466b95f..57a6800859 100644 --- a/src/Http/Http.Abstractions/src/BadHttpRequestException.cs +++ b/src/Http/Http.Abstractions/src/BadHttpRequestException.cs @@ -4,60 +4,59 @@ using System; using System.IO; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Represents an HTTP request error +/// +public class BadHttpRequestException : IOException { /// - /// Represents an HTTP request error + /// Initializes a new instance of the class. /// - public class BadHttpRequestException : IOException + /// The message to associate with this exception. + /// The HTTP status code to associate with this exception. + public BadHttpRequestException(string message, int statusCode) + : base(message) { - /// - /// Initializes a new instance of the class. - /// - /// The message to associate with this exception. - /// The HTTP status code to associate with this exception. - public BadHttpRequestException(string message, int statusCode) - : base(message) - { - StatusCode = statusCode; - } - - /// - /// Initializes a new instance of the class with the set to 400 Bad Request. - /// - /// The message to associate with this exception - public BadHttpRequestException(string message) - : base(message) - { - StatusCode = StatusCodes.Status400BadRequest; - } + StatusCode = statusCode; + } - /// - /// Initializes a new instance of the class. - /// - /// The message to associate with this exception. - /// The HTTP status code to associate with this exception. - /// The inner exception to associate with this exception - public BadHttpRequestException(string message, int statusCode, Exception innerException) - : base(message, innerException) - { - StatusCode = statusCode; - } + /// + /// Initializes a new instance of the class with the set to 400 Bad Request. + /// + /// The message to associate with this exception + public BadHttpRequestException(string message) + : base(message) + { + StatusCode = StatusCodes.Status400BadRequest; + } - /// - /// Initializes a new instance of the class with the set to 400 Bad Request. - /// - /// The message to associate with this exception - /// The inner exception to associate with this exception - public BadHttpRequestException(string message, Exception innerException) - : base(message, innerException) - { - StatusCode = StatusCodes.Status400BadRequest; - } + /// + /// Initializes a new instance of the class. + /// + /// The message to associate with this exception. + /// The HTTP status code to associate with this exception. + /// The inner exception to associate with this exception + public BadHttpRequestException(string message, int statusCode, Exception innerException) + : base(message, innerException) + { + StatusCode = statusCode; + } - /// - /// Gets the HTTP status code for this exception. - /// - public int StatusCode { get; } + /// + /// Initializes a new instance of the class with the set to 400 Bad Request. + /// + /// The message to associate with this exception + /// The inner exception to associate with this exception + public BadHttpRequestException(string message, Exception innerException) + : base(message, innerException) + { + StatusCode = StatusCodes.Status400BadRequest; } + + /// + /// Gets the HTTP status code for this exception. + /// + public int StatusCode { get; } } diff --git a/src/Http/Http.Abstractions/src/ConnectionInfo.cs b/src/Http/Http.Abstractions/src/ConnectionInfo.cs index 9327c58c20..2e208ddc68 100644 --- a/src/Http/Http.Abstractions/src/ConnectionInfo.cs +++ b/src/Http/Http.Abstractions/src/ConnectionInfo.cs @@ -6,55 +6,54 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Represents the underlying connection for a request. +/// +public abstract class ConnectionInfo { /// - /// Represents the underlying connection for a request. + /// Gets or sets a unique identifier to represent this connection. + /// + public abstract string Id { get; set; } + + /// + /// Gets or sets the IP address of the remote target. Can be null. + /// + public abstract IPAddress? RemoteIpAddress { get; set; } + + /// + /// Gets or sets the port of the remote target. + /// + public abstract int RemotePort { get; set; } + + /// + /// Gets or sets the IP address of the local host. /// - public abstract class ConnectionInfo + public abstract IPAddress? LocalIpAddress { get; set; } + + /// + /// Gets or sets the port of the local host. + /// + public abstract int LocalPort { get; set; } + + /// + /// Gets or sets the client certificate. + /// + public abstract X509Certificate2? ClientCertificate { get; set; } + + /// + /// Retrieves the client certificate. + /// + /// Asynchronously returns an . Can be null. + public abstract Task GetClientCertificateAsync(CancellationToken cancellationToken = new CancellationToken()); + + /// + /// Close connection gracefully. + /// + public virtual void RequestClose() { - /// - /// Gets or sets a unique identifier to represent this connection. - /// - public abstract string Id { get; set; } - - /// - /// Gets or sets the IP address of the remote target. Can be null. - /// - public abstract IPAddress? RemoteIpAddress { get; set; } - - /// - /// Gets or sets the port of the remote target. - /// - public abstract int RemotePort { get; set; } - - /// - /// Gets or sets the IP address of the local host. - /// - public abstract IPAddress? LocalIpAddress { get; set; } - - /// - /// Gets or sets the port of the local host. - /// - public abstract int LocalPort { get; set; } - - /// - /// Gets or sets the client certificate. - /// - public abstract X509Certificate2? ClientCertificate { get; set; } - - /// - /// Retrieves the client certificate. - /// - /// Asynchronously returns an . Can be null. - public abstract Task GetClientCertificateAsync(CancellationToken cancellationToken = new CancellationToken()); - - /// - /// Close connection gracefully. - /// - public virtual void RequestClose() - { - - } + } } diff --git a/src/Http/Http.Abstractions/src/CookieBuilder.cs b/src/Http/Http.Abstractions/src/CookieBuilder.cs index e92e2ffaa8..c34dd22090 100644 --- a/src/Http/Http.Abstractions/src/CookieBuilder.cs +++ b/src/Http/Http.Abstractions/src/CookieBuilder.cs @@ -4,111 +4,110 @@ using System; using Microsoft.AspNetCore.Http.Abstractions; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Defines settings used to create a cookie. +/// +public class CookieBuilder { + private string? _name; + /// - /// Defines settings used to create a cookie. + /// The name of the cookie. /// - public class CookieBuilder + public virtual string? Name { - private string? _name; - - /// - /// The name of the cookie. - /// - public virtual string? Name - { - get => _name; - set => _name = !string.IsNullOrEmpty(value) - ? value - : throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(value)); - } + get => _name; + set => _name = !string.IsNullOrEmpty(value) + ? value + : throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(value)); + } - /// - /// The cookie path. - /// - /// - /// Determines the value that will set on . - /// - public virtual string? Path { get; set; } + /// + /// The cookie path. + /// + /// + /// Determines the value that will set on . + /// + public virtual string? Path { get; set; } - /// - /// The domain to associate the cookie with. - /// - /// - /// Determines the value that will set on . - /// - public virtual string? Domain { get; set; } + /// + /// The domain to associate the cookie with. + /// + /// + /// Determines the value that will set on . + /// + public virtual string? Domain { get; set; } - /// - /// Indicates whether a cookie is accessible by client-side script. - /// - /// - /// Determines the value that will set on . - /// - public virtual bool HttpOnly { get; set; } + /// + /// Indicates whether a cookie is accessible by client-side script. + /// + /// + /// Determines the value that will set on . + /// + public virtual bool HttpOnly { get; set; } - /// - /// The SameSite attribute of the cookie. The default value is - /// - /// - /// Determines the value that will set on . - /// - public virtual SameSiteMode SameSite { get; set; } = SameSiteMode.Unspecified; + /// + /// The SameSite attribute of the cookie. The default value is + /// + /// + /// Determines the value that will set on . + /// + public virtual SameSiteMode SameSite { get; set; } = SameSiteMode.Unspecified; - /// - /// The policy that will be used to determine . - /// This is determined from the passed to . - /// - public virtual CookieSecurePolicy SecurePolicy { get; set; } + /// + /// The policy that will be used to determine . + /// This is determined from the passed to . + /// + public virtual CookieSecurePolicy SecurePolicy { get; set; } - /// - /// Gets or sets the lifespan of a cookie. - /// - public virtual TimeSpan? Expiration { get; set; } + /// + /// Gets or sets the lifespan of a cookie. + /// + public virtual TimeSpan? Expiration { get; set; } - /// - /// Gets or sets the max-age for the cookie. - /// - public virtual TimeSpan? MaxAge { get; set; } + /// + /// Gets or sets the max-age for the cookie. + /// + public virtual TimeSpan? MaxAge { get; set; } - /// - /// Indicates if this cookie is essential for the application to function correctly. If true then - /// consent policy checks may be bypassed. The default value is false. - /// - public virtual bool IsEssential { get; set; } + /// + /// Indicates if this cookie is essential for the application to function correctly. If true then + /// consent policy checks may be bypassed. The default value is false. + /// + public virtual bool IsEssential { get; set; } - /// - /// Creates the cookie options from the given . - /// - /// The . - /// The cookie options. - public CookieOptions Build(HttpContext context) => Build(context, DateTimeOffset.Now); + /// + /// Creates the cookie options from the given . + /// + /// The . + /// The cookie options. + public CookieOptions Build(HttpContext context) => Build(context, DateTimeOffset.Now); - /// - /// Creates the cookie options from the given with an expiration based on and . - /// - /// The . - /// The time to use as the base for computing . - /// The cookie options. - public virtual CookieOptions Build(HttpContext context, DateTimeOffset expiresFrom) + /// + /// Creates the cookie options from the given with an expiration based on and . + /// + /// The . + /// The time to use as the base for computing . + /// The cookie options. + public virtual CookieOptions Build(HttpContext context, DateTimeOffset expiresFrom) + { + if (context == null) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - return new CookieOptions - { - Path = Path ?? "/", - SameSite = SameSite, - HttpOnly = HttpOnly, - MaxAge = MaxAge, - Domain = Domain, - IsEssential = IsEssential, - Secure = SecurePolicy == CookieSecurePolicy.Always || (SecurePolicy == CookieSecurePolicy.SameAsRequest && context.Request.IsHttps), - Expires = Expiration.HasValue ? expiresFrom.Add(Expiration.GetValueOrDefault()) : default(DateTimeOffset?) - }; + throw new ArgumentNullException(nameof(context)); } + + return new CookieOptions + { + Path = Path ?? "/", + SameSite = SameSite, + HttpOnly = HttpOnly, + MaxAge = MaxAge, + Domain = Domain, + IsEssential = IsEssential, + Secure = SecurePolicy == CookieSecurePolicy.Always || (SecurePolicy == CookieSecurePolicy.SameAsRequest && context.Request.IsHttps), + Expires = Expiration.HasValue ? expiresFrom.Add(Expiration.GetValueOrDefault()) : default(DateTimeOffset?) + }; } } diff --git a/src/Http/Http.Abstractions/src/CookieSecurePolicy.cs b/src/Http/Http.Abstractions/src/CookieSecurePolicy.cs index eec5f18dd0..91107fb163 100644 --- a/src/Http/Http.Abstractions/src/CookieSecurePolicy.cs +++ b/src/Http/Http.Abstractions/src/CookieSecurePolicy.cs @@ -1,34 +1,33 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Determines how cookie security properties are set. +/// +public enum CookieSecurePolicy { /// - /// Determines how cookie security properties are set. + /// If the URI that provides the cookie is HTTPS, then the cookie will only be returned to the server on + /// subsequent HTTPS requests. Otherwise if the URI that provides the cookie is HTTP, then the cookie will + /// be returned to the server on all HTTP and HTTPS requests. This value ensures + /// HTTPS for all authenticated requests on deployed servers, and also supports HTTP for localhost development + /// and for servers that do not have HTTPS support. /// - public enum CookieSecurePolicy - { - /// - /// If the URI that provides the cookie is HTTPS, then the cookie will only be returned to the server on - /// subsequent HTTPS requests. Otherwise if the URI that provides the cookie is HTTP, then the cookie will - /// be returned to the server on all HTTP and HTTPS requests. This value ensures - /// HTTPS for all authenticated requests on deployed servers, and also supports HTTP for localhost development - /// and for servers that do not have HTTPS support. - /// - SameAsRequest, + SameAsRequest, - /// - /// Secure is always marked true. Use this value when your login page and all subsequent pages - /// requiring the authenticated identity are HTTPS. Local development will also need to be done with HTTPS urls. - /// - Always, + /// + /// Secure is always marked true. Use this value when your login page and all subsequent pages + /// requiring the authenticated identity are HTTPS. Local development will also need to be done with HTTPS urls. + /// + Always, - /// - /// Secure is not marked true. Use this value when your login page is HTTPS, but other pages - /// on the site which are HTTP also require authentication information. This setting is not recommended because - /// the authentication information provided with an HTTP request may be observed and used by other computers - /// on your local network or wireless connection. - /// - None, - } + /// + /// Secure is not marked true. Use this value when your login page is HTTPS, but other pages + /// on the site which are HTTP also require authentication information. This setting is not recommended because + /// the authentication information provided with an HTTP request may be observed and used by other computers + /// on your local network or wireless connection. + /// + None, } diff --git a/src/Http/Http.Abstractions/src/Extensions/EndpointBuilder.cs b/src/Http/Http.Abstractions/src/Extensions/EndpointBuilder.cs index 448b80c087..32e395098c 100644 --- a/src/Http/Http.Abstractions/src/Extensions/EndpointBuilder.cs +++ b/src/Http/Http.Abstractions/src/Extensions/EndpointBuilder.cs @@ -4,32 +4,31 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// A base class for building an new . +/// +public abstract class EndpointBuilder { /// - /// A base class for building an new . + /// Gets or sets the delegate used to process requests for the endpoint. /// - public abstract class EndpointBuilder - { - /// - /// Gets or sets the delegate used to process requests for the endpoint. - /// - public RequestDelegate? RequestDelegate { get; set; } + public RequestDelegate? RequestDelegate { get; set; } - /// - /// Gets or sets the informational display name of this endpoint. - /// - public string? DisplayName { get; set; } + /// + /// Gets or sets the informational display name of this endpoint. + /// + public string? DisplayName { get; set; } - /// - /// Gets the collection of metadata associated with this endpoint. - /// - public IList Metadata { get; } = new List(); + /// + /// Gets the collection of metadata associated with this endpoint. + /// + public IList Metadata { get; } = new List(); - /// - /// Creates an instance of from the . - /// - /// The created . - public abstract Endpoint Build(); - } + /// + /// Creates an instance of from the . + /// + /// The created . + public abstract Endpoint Build(); } diff --git a/src/Http/Http.Abstractions/src/Extensions/HeaderDictionaryExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/HeaderDictionaryExtensions.cs index 598ae4ef39..e80298081c 100644 --- a/src/Http/Http.Abstractions/src/Extensions/HeaderDictionaryExtensions.cs +++ b/src/Http/Http.Abstractions/src/Extensions/HeaderDictionaryExtensions.cs @@ -3,59 +3,58 @@ using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Contains extension methods for modifying an instance. +/// +public static class HeaderDictionaryExtensions { /// - /// Contains extension methods for modifying an instance. + /// Add new values. Each item remains a separate array entry. /// - public static class HeaderDictionaryExtensions + /// The to use. + /// The header name. + /// The header value. + public static void Append(this IHeaderDictionary headers, string key, StringValues value) { - /// - /// Add new values. Each item remains a separate array entry. - /// - /// The to use. - /// The header name. - /// The header value. - public static void Append(this IHeaderDictionary headers, string key, StringValues value) - { - ParsingHelpers.AppendHeaderUnmodified(headers, key, value); - } + ParsingHelpers.AppendHeaderUnmodified(headers, key, value); + } - /// - /// Quotes any values containing commas, and then comma joins all of the values with any existing values. - /// - /// The to use. - /// The header name. - /// The header values. - public static void AppendCommaSeparatedValues(this IHeaderDictionary headers, string key, params string[] values) - { - ParsingHelpers.AppendHeaderJoined(headers, key, values); - } + /// + /// Quotes any values containing commas, and then comma joins all of the values with any existing values. + /// + /// The to use. + /// The header name. + /// The header values. + public static void AppendCommaSeparatedValues(this IHeaderDictionary headers, string key, params string[] values) + { + ParsingHelpers.AppendHeaderJoined(headers, key, values); + } - /// - /// Get the associated values from the collection separated into individual values. - /// Quoted values will not be split, and the quotes will be removed. - /// - /// The to use. - /// The header name. - /// the associated values from the collection separated into individual values, or StringValues.Empty if the key is not present. - public static string[] GetCommaSeparatedValues(this IHeaderDictionary headers, string key) - { - // GetHeaderSplit will return only non-null elements of the given IHeaderDictionary. + /// + /// Get the associated values from the collection separated into individual values. + /// Quoted values will not be split, and the quotes will be removed. + /// + /// The to use. + /// The header name. + /// the associated values from the collection separated into individual values, or StringValues.Empty if the key is not present. + public static string[] GetCommaSeparatedValues(this IHeaderDictionary headers, string key) + { + // GetHeaderSplit will return only non-null elements of the given IHeaderDictionary. #pragma warning disable CS8619 // Nullability of reference types in value doesn't match target type. - return ParsingHelpers.GetHeaderSplit(headers, key).ToArray(); + return ParsingHelpers.GetHeaderSplit(headers, key).ToArray(); #pragma warning restore CS8619 // Nullability of reference types in value doesn't match target type. - } + } - /// - /// Quotes any values containing commas, and then comma joins all of the values. - /// - /// The to use. - /// The header name. - /// The header values. - public static void SetCommaSeparatedValues(this IHeaderDictionary headers, string key, params string[] values) - { - ParsingHelpers.SetHeaderJoined(headers, key, values); - } + /// + /// Quotes any values containing commas, and then comma joins all of the values. + /// + /// The to use. + /// The header name. + /// The header values. + public static void SetCommaSeparatedValues(this IHeaderDictionary headers, string key, params string[] values) + { + ParsingHelpers.SetHeaderJoined(headers, key, values); } } diff --git a/src/Http/Http.Abstractions/src/Extensions/HttpResponseWritingExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/HttpResponseWritingExtensions.cs index 1e0ec65c51..e95847ab2b 100644 --- a/src/Http/Http.Abstractions/src/Extensions/HttpResponseWritingExtensions.cs +++ b/src/Http/Http.Abstractions/src/Extensions/HttpResponseWritingExtensions.cs @@ -8,143 +8,142 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Convenience methods for writing to the response. +/// +public static class HttpResponseWritingExtensions { + private const int UTF8MaxByteLength = 6; + /// - /// Convenience methods for writing to the response. + /// Writes the given text to the response body. UTF-8 encoding will be used. /// - public static class HttpResponseWritingExtensions + /// The . + /// The text to write to the response. + /// Notifies when request operations should be cancelled. + /// A task that represents the completion of the write operation. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static Task WriteAsync(this HttpResponse response, string text, CancellationToken cancellationToken = default(CancellationToken)) { - private const int UTF8MaxByteLength = 6; - - /// - /// Writes the given text to the response body. UTF-8 encoding will be used. - /// - /// The . - /// The text to write to the response. - /// Notifies when request operations should be cancelled. - /// A task that represents the completion of the write operation. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static Task WriteAsync(this HttpResponse response, string text, CancellationToken cancellationToken = default(CancellationToken)) + if (response == null) { - if (response == null) - { - throw new ArgumentNullException(nameof(response)); - } + throw new ArgumentNullException(nameof(response)); + } - if (text == null) - { - throw new ArgumentNullException(nameof(text)); - } + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + return response.WriteAsync(text, Encoding.UTF8, cancellationToken); + } - return response.WriteAsync(text, Encoding.UTF8, cancellationToken); + /// + /// Writes the given text to the response body using the given encoding. + /// + /// The . + /// The text to write to the response. + /// The encoding to use. + /// Notifies when request operations should be cancelled. + /// A task that represents the completion of the write operation. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static Task WriteAsync(this HttpResponse response, string text, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); } - /// - /// Writes the given text to the response body using the given encoding. - /// - /// The . - /// The text to write to the response. - /// The encoding to use. - /// Notifies when request operations should be cancelled. - /// A task that represents the completion of the write operation. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static Task WriteAsync(this HttpResponse response, string text, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) + if (text == null) { - if (response == null) - { - throw new ArgumentNullException(nameof(response)); - } + throw new ArgumentNullException(nameof(text)); + } - if (text == null) - { - throw new ArgumentNullException(nameof(text)); - } + if (encoding == null) + { + throw new ArgumentNullException(nameof(encoding)); + } - if (encoding == null) + // Need to call StartAsync before GetMemory/GetSpan + if (!response.HasStarted) + { + var startAsyncTask = response.StartAsync(cancellationToken); + if (!startAsyncTask.IsCompletedSuccessfully) { - throw new ArgumentNullException(nameof(encoding)); + return StartAndWriteAsyncAwaited(response, text, encoding, cancellationToken, startAsyncTask); } + } - // Need to call StartAsync before GetMemory/GetSpan - if (!response.HasStarted) - { - var startAsyncTask = response.StartAsync(cancellationToken); - if (!startAsyncTask.IsCompletedSuccessfully) - { - return StartAndWriteAsyncAwaited(response, text, encoding, cancellationToken, startAsyncTask); - } - } + Write(response, text, encoding); - Write(response, text, encoding); + var flushAsyncTask = response.BodyWriter.FlushAsync(cancellationToken); + if (flushAsyncTask.IsCompletedSuccessfully) + { + // Most implementations of ValueTask reset state in GetResult, so call it before returning a completed task. + flushAsyncTask.GetAwaiter().GetResult(); + return Task.CompletedTask; + } - var flushAsyncTask = response.BodyWriter.FlushAsync(cancellationToken); - if (flushAsyncTask.IsCompletedSuccessfully) - { - // Most implementations of ValueTask reset state in GetResult, so call it before returning a completed task. - flushAsyncTask.GetAwaiter().GetResult(); - return Task.CompletedTask; - } + return flushAsyncTask.AsTask(); + } - return flushAsyncTask.AsTask(); - } + private static async Task StartAndWriteAsyncAwaited(this HttpResponse response, string text, Encoding encoding, CancellationToken cancellationToken, Task startAsyncTask) + { + await startAsyncTask; + Write(response, text, encoding); + await response.BodyWriter.FlushAsync(cancellationToken); + } + + private static void Write(this HttpResponse response, string text, Encoding encoding) + { + var minimumByteSize = GetEncodingMaxByteSize(encoding); + var pipeWriter = response.BodyWriter; + var encodedLength = encoding.GetByteCount(text); + var destination = pipeWriter.GetSpan(minimumByteSize); - private static async Task StartAndWriteAsyncAwaited(this HttpResponse response, string text, Encoding encoding, CancellationToken cancellationToken, Task startAsyncTask) + if (encodedLength <= destination.Length) { - await startAsyncTask; - Write(response, text, encoding); - await response.BodyWriter.FlushAsync(cancellationToken); + // Just call Encoding.GetBytes if everything will fit into a single segment. + var bytesWritten = encoding.GetBytes(text, destination); + pipeWriter.Advance(bytesWritten); } - - private static void Write(this HttpResponse response, string text, Encoding encoding) + else { - var minimumByteSize = GetEncodingMaxByteSize(encoding); - var pipeWriter = response.BodyWriter; - var encodedLength = encoding.GetByteCount(text); - var destination = pipeWriter.GetSpan(minimumByteSize); - - if (encodedLength <= destination.Length) - { - // Just call Encoding.GetBytes if everything will fit into a single segment. - var bytesWritten = encoding.GetBytes(text, destination); - pipeWriter.Advance(bytesWritten); - } - else - { - WriteMultiSegmentEncoded(pipeWriter, text, encoding, destination, encodedLength, minimumByteSize); - } + WriteMultiSegmentEncoded(pipeWriter, text, encoding, destination, encodedLength, minimumByteSize); } + } - private static int GetEncodingMaxByteSize(Encoding encoding) + private static int GetEncodingMaxByteSize(Encoding encoding) + { + if (encoding == Encoding.UTF8) { - if (encoding == Encoding.UTF8) - { - return UTF8MaxByteLength; - } - - return encoding.GetMaxByteCount(1); + return UTF8MaxByteLength; } - private static void WriteMultiSegmentEncoded(PipeWriter writer, string text, Encoding encoding, Span destination, int encodedLength, int minimumByteSize) + return encoding.GetMaxByteCount(1); + } + + private static void WriteMultiSegmentEncoded(PipeWriter writer, string text, Encoding encoding, Span destination, int encodedLength, int minimumByteSize) + { + var encoder = encoding.GetEncoder(); + var source = text.AsSpan(); + var completed = false; + var totalBytesUsed = 0; + + // This may be a bug, but encoder.Convert returns completed = true for UTF7 too early. + // Therefore, we check encodedLength - totalBytesUsed too. + while (!completed || encodedLength - totalBytesUsed != 0) { - var encoder = encoding.GetEncoder(); - var source = text.AsSpan(); - var completed = false; - var totalBytesUsed = 0; - - // This may be a bug, but encoder.Convert returns completed = true for UTF7 too early. - // Therefore, we check encodedLength - totalBytesUsed too. - while (!completed || encodedLength - totalBytesUsed != 0) - { - // 'text' is a complete string, the converter should always flush its buffer. - encoder.Convert(source, destination, flush: true, out var charsUsed, out var bytesUsed, out completed); - totalBytesUsed += bytesUsed; + // 'text' is a complete string, the converter should always flush its buffer. + encoder.Convert(source, destination, flush: true, out var charsUsed, out var bytesUsed, out completed); + totalBytesUsed += bytesUsed; - writer.Advance(bytesUsed); - source = source.Slice(charsUsed); + writer.Advance(bytesUsed); + source = source.Slice(charsUsed); - destination = writer.GetSpan(minimumByteSize); - } + destination = writer.GetSpan(minimumByteSize); } } } diff --git a/src/Http/Http.Abstractions/src/Extensions/IEndpointConventionBuilder.cs b/src/Http/Http.Abstractions/src/Extensions/IEndpointConventionBuilder.cs index e8053aa5ac..5dba459582 100644 --- a/src/Http/Http.Abstractions/src/Extensions/IEndpointConventionBuilder.cs +++ b/src/Http/Http.Abstractions/src/Extensions/IEndpointConventionBuilder.cs @@ -3,20 +3,19 @@ using System; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Builds conventions that will be used for customization of instances. +/// +/// +/// This interface is used at application startup to customize endpoints for the application. +/// +public interface IEndpointConventionBuilder { /// - /// Builds conventions that will be used for customization of instances. + /// Adds the specified convention to the builder. Conventions are used to customize instances. /// - /// - /// This interface is used at application startup to customize endpoints for the application. - /// - public interface IEndpointConventionBuilder - { - /// - /// Adds the specified convention to the builder. Conventions are used to customize instances. - /// - /// The convention to add to the builder. - void Add(Action convention); - } + /// The convention to add to the builder. + void Add(Action convention); } diff --git a/src/Http/Http.Abstractions/src/Extensions/MapExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/MapExtensions.cs index 7842dbdeaf..41da2b7b5d 100644 --- a/src/Http/Http.Abstractions/src/Extensions/MapExtensions.cs +++ b/src/Http/Http.Abstractions/src/Extensions/MapExtensions.cs @@ -2,80 +2,79 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Builder.Extensions; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Builder; -namespace Microsoft.AspNetCore.Builder +/// +/// Extension methods for the . +/// +public static class MapExtensions { /// - /// Extension methods for the . + /// Branches the request pipeline based on matches of the given request path. If the request path starts with + /// the given path, the branch is executed. + /// + /// The instance. + /// The request path to match. + /// The branch to take for positive path matches. + /// The instance. + public static IApplicationBuilder Map(this IApplicationBuilder app, string pathMatch, Action configuration) + { + return Map(app, pathMatch, preserveMatchedPathSegment: false, configuration); + } + + /// + /// Branches the request pipeline based on matches of the given request path. If the request path starts with + /// the given path, the branch is executed. + /// + /// The instance. + /// The request path to match. + /// The branch to take for positive path matches. + /// The instance. + public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action configuration) + { + return Map(app, pathMatch, preserveMatchedPathSegment: false, configuration); + } + + /// + /// Branches the request pipeline based on matches of the given request path. If the request path starts with + /// the given path, the branch is executed. /// - public static class MapExtensions + /// The instance. + /// The request path to match. + /// if false, matched path would be removed from Request.Path and added to Request.PathBase. + /// The branch to take for positive path matches. + /// The instance. + public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, bool preserveMatchedPathSegment, Action configuration) { - /// - /// Branches the request pipeline based on matches of the given request path. If the request path starts with - /// the given path, the branch is executed. - /// - /// The instance. - /// The request path to match. - /// The branch to take for positive path matches. - /// The instance. - public static IApplicationBuilder Map(this IApplicationBuilder app, string pathMatch, Action configuration) + if (app == null) { - return Map(app, pathMatch, preserveMatchedPathSegment: false, configuration); + throw new ArgumentNullException(nameof(app)); } - /// - /// Branches the request pipeline based on matches of the given request path. If the request path starts with - /// the given path, the branch is executed. - /// - /// The instance. - /// The request path to match. - /// The branch to take for positive path matches. - /// The instance. - public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, Action configuration) + if (configuration == null) { - return Map(app, pathMatch, preserveMatchedPathSegment: false, configuration); + throw new ArgumentNullException(nameof(configuration)); } - /// - /// Branches the request pipeline based on matches of the given request path. If the request path starts with - /// the given path, the branch is executed. - /// - /// The instance. - /// The request path to match. - /// if false, matched path would be removed from Request.Path and added to Request.PathBase. - /// The branch to take for positive path matches. - /// The instance. - public static IApplicationBuilder Map(this IApplicationBuilder app, PathString pathMatch, bool preserveMatchedPathSegment, Action configuration) + if (pathMatch.HasValue && pathMatch.Value!.EndsWith("/", StringComparison.Ordinal)) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } - - if (pathMatch.HasValue && pathMatch.Value!.EndsWith("/", StringComparison.Ordinal)) - { - throw new ArgumentException("The path must not end with a '/'", nameof(pathMatch)); - } + throw new ArgumentException("The path must not end with a '/'", nameof(pathMatch)); + } - // create branch - var branchBuilder = app.New(); - configuration(branchBuilder); - var branch = branchBuilder.Build(); + // create branch + var branchBuilder = app.New(); + configuration(branchBuilder); + var branch = branchBuilder.Build(); - var options = new MapOptions - { - Branch = branch, - PathMatch = pathMatch, - PreserveMatchedPathSegment = preserveMatchedPathSegment - }; - return app.Use(next => new MapMiddleware(next, options).Invoke); - } + var options = new MapOptions + { + Branch = branch, + PathMatch = pathMatch, + PreserveMatchedPathSegment = preserveMatchedPathSegment + }; + return app.Use(next => new MapMiddleware(next, options).Invoke); } } diff --git a/src/Http/Http.Abstractions/src/Extensions/MapMiddleware.cs b/src/Http/Http.Abstractions/src/Extensions/MapMiddleware.cs index 35b25f40e6..b8d2db196d 100644 --- a/src/Http/Http.Abstractions/src/Extensions/MapMiddleware.cs +++ b/src/Http/Http.Abstractions/src/Extensions/MapMiddleware.cs @@ -5,83 +5,82 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder.Extensions +namespace Microsoft.AspNetCore.Builder.Extensions; + +/// +/// Represents a middleware that maps a request path to a sub-request pipeline. +/// +public class MapMiddleware { + private readonly RequestDelegate _next; + private readonly MapOptions _options; + /// - /// Represents a middleware that maps a request path to a sub-request pipeline. + /// Creates a new instance of . /// - public class MapMiddleware + /// The delegate representing the next middleware in the request pipeline. + /// The middleware options. + public MapMiddleware(RequestDelegate next, MapOptions options) { - private readonly RequestDelegate _next; - private readonly MapOptions _options; + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } - /// - /// Creates a new instance of . - /// - /// The delegate representing the next middleware in the request pipeline. - /// The middleware options. - public MapMiddleware(RequestDelegate next, MapOptions options) + if (options == null) { - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } + throw new ArgumentNullException(nameof(options)); + } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } + if (options.Branch == null) + { + throw new ArgumentException("Branch not set on options.", nameof(options)); + } - if (options.Branch == null) - { - throw new ArgumentException("Branch not set on options.", nameof(options)); - } + _next = next; + _options = options; + } - _next = next; - _options = options; + /// + /// Executes the middleware. + /// + /// The for the current request. + /// A task that represents the execution of this middleware. + public Task Invoke(HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); } - /// - /// Executes the middleware. - /// - /// The for the current request. - /// A task that represents the execution of this middleware. - public Task Invoke(HttpContext context) + if (context.Request.Path.StartsWithSegments(_options.PathMatch, out var matchedPath, out var remainingPath)) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (context.Request.Path.StartsWithSegments(_options.PathMatch, out var matchedPath, out var remainingPath)) + if (!_options.PreserveMatchedPathSegment) { - if (!_options.PreserveMatchedPathSegment) - { - return InvokeCore(context, matchedPath, remainingPath); - } - return _options.Branch!(context); + return InvokeCore(context, matchedPath, remainingPath); } - return _next(context); + return _options.Branch!(context); } + return _next(context); + } - private async Task InvokeCore(HttpContext context, string matchedPath, string remainingPath) - { - var path = context.Request.Path; - var pathBase = context.Request.PathBase; + private async Task InvokeCore(HttpContext context, string matchedPath, string remainingPath) + { + var path = context.Request.Path; + var pathBase = context.Request.PathBase; - // Update the path - context.Request.PathBase = pathBase.Add(matchedPath); - context.Request.Path = remainingPath; + // Update the path + context.Request.PathBase = pathBase.Add(matchedPath); + context.Request.Path = remainingPath; - try - { - await _options.Branch!(context); - } - finally - { - context.Request.PathBase = pathBase; - context.Request.Path = path; - } + try + { + await _options.Branch!(context); + } + finally + { + context.Request.PathBase = pathBase; + context.Request.Path = path; } } } diff --git a/src/Http/Http.Abstractions/src/Extensions/MapOptions.cs b/src/Http/Http.Abstractions/src/Extensions/MapOptions.cs index efc50a64b3..16afae0b0c 100644 --- a/src/Http/Http.Abstractions/src/Extensions/MapOptions.cs +++ b/src/Http/Http.Abstractions/src/Extensions/MapOptions.cs @@ -3,27 +3,26 @@ using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder.Extensions +namespace Microsoft.AspNetCore.Builder.Extensions; + +/// +/// Options for the . +/// +public class MapOptions { /// - /// Options for the . + /// The path to match. /// - public class MapOptions - { - /// - /// The path to match. - /// - public PathString PathMatch { get; set; } + public PathString PathMatch { get; set; } - /// - /// The branch taken for a positive match. - /// - public RequestDelegate? Branch { get; set; } + /// + /// The branch taken for a positive match. + /// + public RequestDelegate? Branch { get; set; } - /// - /// If false, matched path would be removed from Request.Path and added to Request.PathBase - /// Defaults to false. - /// - public bool PreserveMatchedPathSegment { get; set; } - } + /// + /// If false, matched path would be removed from Request.Path and added to Request.PathBase + /// Defaults to false. + /// + public bool PreserveMatchedPathSegment { get; set; } } diff --git a/src/Http/Http.Abstractions/src/Extensions/MapWhenExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/MapWhenExtensions.cs index d58a697ab6..04690cc6f3 100644 --- a/src/Http/Http.Abstractions/src/Extensions/MapWhenExtensions.cs +++ b/src/Http/Http.Abstractions/src/Extensions/MapWhenExtensions.cs @@ -2,55 +2,54 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Builder.Extensions; +using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder -{ - using Predicate = Func; +namespace Microsoft.AspNetCore.Builder; + +using Predicate = Func; +/// +/// Extension methods for the . +/// +public static class MapWhenExtensions +{ /// - /// Extension methods for the . + /// Branches the request pipeline based on the result of the given predicate. /// - public static class MapWhenExtensions + /// + /// Invoked with the request environment to determine if the branch should be taken + /// Configures a branch to take + /// + public static IApplicationBuilder MapWhen(this IApplicationBuilder app, Predicate predicate, Action configuration) { - /// - /// Branches the request pipeline based on the result of the given predicate. - /// - /// - /// Invoked with the request environment to determine if the branch should be taken - /// Configures a branch to take - /// - public static IApplicationBuilder MapWhen(this IApplicationBuilder app, Predicate predicate, Action configuration) + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + if (configuration == null) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - if (predicate == null) - { - throw new ArgumentNullException(nameof(predicate)); - } - - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } - - // create branch - var branchBuilder = app.New(); - configuration(branchBuilder); - var branch = branchBuilder.Build(); - - // put middleware in pipeline - var options = new MapWhenOptions - { - Predicate = predicate, - Branch = branch, - }; - return app.Use(next => new MapWhenMiddleware(next, options).Invoke); + throw new ArgumentNullException(nameof(configuration)); } + + // create branch + var branchBuilder = app.New(); + configuration(branchBuilder); + var branch = branchBuilder.Build(); + + // put middleware in pipeline + var options = new MapWhenOptions + { + Predicate = predicate, + Branch = branch, + }; + return app.Use(next => new MapWhenMiddleware(next, options).Invoke); } -} \ No newline at end of file +} diff --git a/src/Http/Http.Abstractions/src/Extensions/MapWhenMiddleware.cs b/src/Http/Http.Abstractions/src/Extensions/MapWhenMiddleware.cs index 9764055696..b75837b88c 100644 --- a/src/Http/Http.Abstractions/src/Extensions/MapWhenMiddleware.cs +++ b/src/Http/Http.Abstractions/src/Extensions/MapWhenMiddleware.cs @@ -5,64 +5,63 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder.Extensions +namespace Microsoft.AspNetCore.Builder.Extensions; + +/// +/// Represents a middleware that runs a sub-request pipeline when a given predicate is matched. +/// +public class MapWhenMiddleware { + private readonly RequestDelegate _next; + private readonly MapWhenOptions _options; + /// - /// Represents a middleware that runs a sub-request pipeline when a given predicate is matched. + /// Creates a new instance of . /// - public class MapWhenMiddleware + /// The delegate representing the next middleware in the request pipeline. + /// The middleware options. + public MapWhenMiddleware(RequestDelegate next, MapWhenOptions options) { - private readonly RequestDelegate _next; - private readonly MapWhenOptions _options; + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } - /// - /// Creates a new instance of . - /// - /// The delegate representing the next middleware in the request pipeline. - /// The middleware options. - public MapWhenMiddleware(RequestDelegate next, MapWhenOptions options) + if (options == null) { - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } + throw new ArgumentNullException(nameof(options)); + } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } + if (options.Predicate == null) + { + throw new ArgumentException("Predicate not set on options.", nameof(options)); + } - if (options.Predicate == null) - { - throw new ArgumentException("Predicate not set on options.", nameof(options)); - } + if (options.Branch == null) + { + throw new ArgumentException("Branch not set on options.", nameof(options)); + } - if (options.Branch == null) - { - throw new ArgumentException("Branch not set on options.", nameof(options)); - } + _next = next; + _options = options; + } - _next = next; - _options = options; + /// + /// Executes the middleware. + /// + /// The for the current request. + /// A task that represents the execution of this middleware. + public Task Invoke(HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); } - /// - /// Executes the middleware. - /// - /// The for the current request. - /// A task that represents the execution of this middleware. - public Task Invoke(HttpContext context) + if (_options.Predicate!(context)) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (_options.Predicate!(context)) - { - return _options.Branch!(context); - } - return _next(context); + return _options.Branch!(context); } + return _next(context); } } diff --git a/src/Http/Http.Abstractions/src/Extensions/MapWhenOptions.cs b/src/Http/Http.Abstractions/src/Extensions/MapWhenOptions.cs index 237a54b650..a06528eae8 100644 --- a/src/Http/Http.Abstractions/src/Extensions/MapWhenOptions.cs +++ b/src/Http/Http.Abstractions/src/Extensions/MapWhenOptions.cs @@ -4,38 +4,37 @@ using System; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder.Extensions +namespace Microsoft.AspNetCore.Builder.Extensions; + +/// +/// Options for the . +/// +public class MapWhenOptions { + private Func? _predicate; + /// - /// Options for the . + /// The user callback that determines if the branch should be taken. /// - public class MapWhenOptions + public Func? Predicate { - private Func? _predicate; - - /// - /// The user callback that determines if the branch should be taken. - /// - public Func? Predicate + get { - get + return _predicate; + } + set + { + if (value == null) { - return _predicate; + throw new ArgumentNullException(nameof(value)); } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - _predicate = value; - } + _predicate = value; } - - /// - /// The branch taken for a positive match. - /// - public RequestDelegate? Branch { get; set; } } + + /// + /// The branch taken for a positive match. + /// + public RequestDelegate? Branch { get; set; } } diff --git a/src/Http/Http.Abstractions/src/Extensions/RequestTrailerExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/RequestTrailerExtensions.cs index bd48fc8378..c1c638d04d 100644 --- a/src/Http/Http.Abstractions/src/Extensions/RequestTrailerExtensions.cs +++ b/src/Http/Http.Abstractions/src/Extensions/RequestTrailerExtensions.cs @@ -6,60 +6,59 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// HttpRequest extensions for working with request trailing headers. +/// +public static class RequestTrailerExtensions { /// - /// HttpRequest extensions for working with request trailing headers. + /// Gets the request "Trailer" header that lists which trailers to expect after the body. /// - public static class RequestTrailerExtensions + /// + /// + public static StringValues GetDeclaredTrailers(this HttpRequest request) { - /// - /// Gets the request "Trailer" header that lists which trailers to expect after the body. - /// - /// - /// - public static StringValues GetDeclaredTrailers(this HttpRequest request) - { - return request.Headers.GetCommaSeparatedValues(HeaderNames.Trailer); - } + return request.Headers.GetCommaSeparatedValues(HeaderNames.Trailer); + } - /// - /// Indicates if the request supports receiving trailer headers. - /// - /// - /// - public static bool SupportsTrailers(this HttpRequest request) - { - return request.HttpContext.Features.Get() != null; - } + /// + /// Indicates if the request supports receiving trailer headers. + /// + /// + /// + public static bool SupportsTrailers(this HttpRequest request) + { + return request.HttpContext.Features.Get() != null; + } - /// - /// Checks if the request supports trailers and they are available to be read now. - /// This does not mean that there are any trailers to read. - /// - /// - /// - public static bool CheckTrailersAvailable(this HttpRequest request) - { - return request.HttpContext.Features.Get()?.Available == true; - } + /// + /// Checks if the request supports trailers and they are available to be read now. + /// This does not mean that there are any trailers to read. + /// + /// + /// + public static bool CheckTrailersAvailable(this HttpRequest request) + { + return request.HttpContext.Features.Get()?.Available == true; + } - /// - /// Gets the requested trailing header from the response. Check - /// or a NotSupportedException may be thrown. - /// Check or an InvalidOperationException may be thrown. - /// - /// - /// - public static StringValues GetTrailer(this HttpRequest request, string trailerName) + /// + /// Gets the requested trailing header from the response. Check + /// or a NotSupportedException may be thrown. + /// Check or an InvalidOperationException may be thrown. + /// + /// + /// + public static StringValues GetTrailer(this HttpRequest request, string trailerName) + { + var feature = request.HttpContext.Features.Get(); + if (feature == null) { - var feature = request.HttpContext.Features.Get(); - if (feature == null) - { - throw new NotSupportedException("This request does not support trailers."); - } - - return feature.Trailers[trailerName]; + throw new NotSupportedException("This request does not support trailers."); } + + return feature.Trailers[trailerName]; } } diff --git a/src/Http/Http.Abstractions/src/Extensions/ResponseTrailerExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/ResponseTrailerExtensions.cs index 704cce3389..af2cd58069 100644 --- a/src/Http/Http.Abstractions/src/Extensions/ResponseTrailerExtensions.cs +++ b/src/Http/Http.Abstractions/src/Extensions/ResponseTrailerExtensions.cs @@ -6,51 +6,50 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Contains extension methods for modifying the `Trailer` response header +/// and trailing headers in an . +/// +public static class ResponseTrailerExtensions { /// - /// Contains extension methods for modifying the `Trailer` response header - /// and trailing headers in an . + /// Adds the given trailer name to the 'Trailer' response header. This must happen before the response headers are sent. /// - public static class ResponseTrailerExtensions + /// + /// + public static void DeclareTrailer(this HttpResponse response, string trailerName) { - /// - /// Adds the given trailer name to the 'Trailer' response header. This must happen before the response headers are sent. - /// - /// - /// - public static void DeclareTrailer(this HttpResponse response, string trailerName) - { - response.Headers.AppendCommaSeparatedValues(HeaderNames.Trailer, trailerName); - } + response.Headers.AppendCommaSeparatedValues(HeaderNames.Trailer, trailerName); + } - /// - /// Indicates if the server supports sending trailer headers for this response. - /// - /// - /// - public static bool SupportsTrailers(this HttpResponse response) - { - var feature = response.HttpContext.Features.Get(); - return feature?.Trailers != null && !feature.Trailers.IsReadOnly; - } + /// + /// Indicates if the server supports sending trailer headers for this response. + /// + /// + /// + public static bool SupportsTrailers(this HttpResponse response) + { + var feature = response.HttpContext.Features.Get(); + return feature?.Trailers != null && !feature.Trailers.IsReadOnly; + } - /// - /// Adds the given trailer header to the trailers collection to be sent at the end of the response body. - /// Check or an InvalidOperationException may be thrown. - /// - /// - /// - /// - public static void AppendTrailer(this HttpResponse response, string trailerName, StringValues trailerValues) + /// + /// Adds the given trailer header to the trailers collection to be sent at the end of the response body. + /// Check or an InvalidOperationException may be thrown. + /// + /// + /// + /// + public static void AppendTrailer(this HttpResponse response, string trailerName, StringValues trailerValues) + { + var feature = response.HttpContext.Features.Get(); + if (feature?.Trailers == null || feature.Trailers.IsReadOnly) { - var feature = response.HttpContext.Features.Get(); - if (feature?.Trailers == null || feature.Trailers.IsReadOnly) - { - throw new InvalidOperationException("Trailers are not supported for this response."); - } - - feature.Trailers.Append(trailerName, trailerValues); + throw new InvalidOperationException("Trailers are not supported for this response."); } + + feature.Trailers.Append(trailerName, trailerValues); } } diff --git a/src/Http/Http.Abstractions/src/Extensions/RunExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/RunExtensions.cs index f4687b8972..cb6b899ff5 100644 --- a/src/Http/Http.Abstractions/src/Extensions/RunExtensions.cs +++ b/src/Http/Http.Abstractions/src/Extensions/RunExtensions.cs @@ -4,31 +4,30 @@ using System; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extension methods for adding terminal middleware. +/// +public static class RunExtensions { /// - /// Extension methods for adding terminal middleware. + /// Adds a terminal middleware delegate to the application's request pipeline. /// - public static class RunExtensions + /// The instance. + /// A delegate that handles the request. + public static void Run(this IApplicationBuilder app, RequestDelegate handler) { - /// - /// Adds a terminal middleware delegate to the application's request pipeline. - /// - /// The instance. - /// A delegate that handles the request. - public static void Run(this IApplicationBuilder app, RequestDelegate handler) + if (app == null) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - if (handler == null) - { - throw new ArgumentNullException(nameof(handler)); - } + throw new ArgumentNullException(nameof(app)); + } - app.Use(_ => handler); + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); } + + app.Use(_ => handler); } } diff --git a/src/Http/Http.Abstractions/src/Extensions/UseExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/UseExtensions.cs index 4e0b1a5784..b8b7381e89 100644 --- a/src/Http/Http.Abstractions/src/Extensions/UseExtensions.cs +++ b/src/Http/Http.Abstractions/src/Extensions/UseExtensions.cs @@ -5,51 +5,50 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extension methods for adding middleware. +/// +public static class UseExtensions { /// - /// Extension methods for adding middleware. + /// Adds a middleware delegate defined in-line to the application's request pipeline. + /// If you aren't calling the next function, use instead. + /// + /// Prefer using for better performance as shown below: + /// + /// app.Use((context, next) => + /// { + /// return next(context); + /// }); + /// + /// /// - public static class UseExtensions + /// The instance. + /// A function that handles the request and calls the given next function. + /// The instance. + public static IApplicationBuilder Use(this IApplicationBuilder app, Func, Task> middleware) { - /// - /// Adds a middleware delegate defined in-line to the application's request pipeline. - /// If you aren't calling the next function, use instead. - /// - /// Prefer using for better performance as shown below: - /// - /// app.Use((context, next) => - /// { - /// return next(context); - /// }); - /// - /// - /// - /// The instance. - /// A function that handles the request and calls the given next function. - /// The instance. - public static IApplicationBuilder Use(this IApplicationBuilder app, Func, Task> middleware) + return app.Use(next => { - return app.Use(next => + return context => { - return context => - { - Func simpleNext = () => next(context); - return middleware(context, simpleNext); - }; - }); - } + Func simpleNext = () => next(context); + return middleware(context, simpleNext); + }; + }); + } - /// - /// Adds a middleware delegate defined in-line to the application's request pipeline. - /// If you aren't calling the next function, use instead. - /// - /// The instance. - /// A function that handles the request and calls the given next function. - /// The instance. - public static IApplicationBuilder Use(this IApplicationBuilder app, Func middleware) - { - return app.Use(next => context => middleware(context, next)); - } + /// + /// Adds a middleware delegate defined in-line to the application's request pipeline. + /// If you aren't calling the next function, use instead. + /// + /// The instance. + /// A function that handles the request and calls the given next function. + /// The instance. + public static IApplicationBuilder Use(this IApplicationBuilder app, Func middleware) + { + return app.Use(next => context => middleware(context, next)); } } diff --git a/src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs index d6b4e2d944..e5e54131b9 100644 --- a/src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs +++ b/src/Http/Http.Abstractions/src/Extensions/UseMiddlewareExtensions.cs @@ -11,218 +11,217 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Abstractions; using Microsoft.Extensions.Internal; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extension methods for adding typed middleware. +/// +public static class UseMiddlewareExtensions { + internal const string InvokeMethodName = "Invoke"; + internal const string InvokeAsyncMethodName = "InvokeAsync"; + + private static readonly MethodInfo GetServiceInfo = typeof(UseMiddlewareExtensions).GetMethod(nameof(GetService), BindingFlags.NonPublic | BindingFlags.Static)!; + + // We're going to keep all public constructors and public methods on middleware + private const DynamicallyAccessedMemberTypes MiddlewareAccessibility = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicMethods; + + /// + /// Adds a middleware type to the application's request pipeline. + /// + /// The middleware type. + /// The instance. + /// The arguments to pass to the middleware type instance's constructor. + /// The instance. + public static IApplicationBuilder UseMiddleware<[DynamicallyAccessedMembers(MiddlewareAccessibility)] TMiddleware>(this IApplicationBuilder app, params object?[] args) + { + return app.UseMiddleware(typeof(TMiddleware), args); + } + /// - /// Extension methods for adding typed middleware. + /// Adds a middleware type to the application's request pipeline. /// - public static class UseMiddlewareExtensions + /// The instance. + /// The middleware type. + /// The arguments to pass to the middleware type instance's constructor. + /// The instance. + public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, [DynamicallyAccessedMembers(MiddlewareAccessibility)] Type middleware, params object?[] args) { - internal const string InvokeMethodName = "Invoke"; - internal const string InvokeAsyncMethodName = "InvokeAsync"; - - private static readonly MethodInfo GetServiceInfo = typeof(UseMiddlewareExtensions).GetMethod(nameof(GetService), BindingFlags.NonPublic | BindingFlags.Static)!; - - // We're going to keep all public constructors and public methods on middleware - private const DynamicallyAccessedMemberTypes MiddlewareAccessibility = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicMethods; - - /// - /// Adds a middleware type to the application's request pipeline. - /// - /// The middleware type. - /// The instance. - /// The arguments to pass to the middleware type instance's constructor. - /// The instance. - public static IApplicationBuilder UseMiddleware<[DynamicallyAccessedMembers(MiddlewareAccessibility)]TMiddleware>(this IApplicationBuilder app, params object?[] args) + if (typeof(IMiddleware).IsAssignableFrom(middleware)) { - return app.UseMiddleware(typeof(TMiddleware), args); + // IMiddleware doesn't support passing args directly since it's + // activated from the container + if (args.Length > 0) + { + throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware))); + } + + return UseMiddlewareInterface(app, middleware); } - /// - /// Adds a middleware type to the application's request pipeline. - /// - /// The instance. - /// The middleware type. - /// The arguments to pass to the middleware type instance's constructor. - /// The instance. - public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, [DynamicallyAccessedMembers(MiddlewareAccessibility)] Type middleware, params object?[] args) + var applicationServices = app.ApplicationServices; + return app.Use(next => { - if (typeof(IMiddleware).IsAssignableFrom(middleware)) - { - // IMiddleware doesn't support passing args directly since it's - // activated from the container - if (args.Length > 0) - { - throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware))); - } + var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public); + var invokeMethods = methods.Where(m => + string.Equals(m.Name, InvokeMethodName, StringComparison.Ordinal) + || string.Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal) + ).ToArray(); - return UseMiddlewareInterface(app, middleware); + if (invokeMethods.Length > 1) + { + throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName)); } - var applicationServices = app.ApplicationServices; - return app.Use(next => + if (invokeMethods.Length == 0) { - var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public); - var invokeMethods = methods.Where(m => - string.Equals(m.Name, InvokeMethodName, StringComparison.Ordinal) - || string.Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal) - ).ToArray(); + throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware)); + } - if (invokeMethods.Length > 1) - { - throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName)); - } + var methodInfo = invokeMethods[0]; + if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType)) + { + throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task))); + } - if (invokeMethods.Length == 0) - { - throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware)); - } + var parameters = methodInfo.GetParameters(); + if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext)) + { + throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext))); + } - var methodInfo = invokeMethods[0]; - if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType)) - { - throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task))); - } + var ctorArgs = new object[args.Length + 1]; + ctorArgs[0] = next; + Array.Copy(args, 0, ctorArgs, 1, args.Length); + var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs); + if (parameters.Length == 1) + { + return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance); + } - var parameters = methodInfo.GetParameters(); - if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext)) - { - throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext))); - } + var factory = Compile(methodInfo, parameters); - var ctorArgs = new object[args.Length + 1]; - ctorArgs[0] = next; - Array.Copy(args, 0, ctorArgs, 1, args.Length); - var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs); - if (parameters.Length == 1) + return context => + { + var serviceProvider = context.RequestServices ?? applicationServices; + if (serviceProvider == null) { - return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance); + throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider))); } - var factory = Compile(methodInfo, parameters); - - return context => - { - var serviceProvider = context.RequestServices ?? applicationServices; - if (serviceProvider == null) - { - throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider))); - } - - return factory(instance, context, serviceProvider); - }; - }); - } + return factory(instance, context, serviceProvider); + }; + }); + } - private static IApplicationBuilder UseMiddlewareInterface(IApplicationBuilder app, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type middlewareType) + private static IApplicationBuilder UseMiddlewareInterface(IApplicationBuilder app, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type middlewareType) + { + return app.Use(next => { - return app.Use(next => + return async context => { - return async context => + var middlewareFactory = (IMiddlewareFactory?)context.RequestServices.GetService(typeof(IMiddlewareFactory)); + if (middlewareFactory == null) { - var middlewareFactory = (IMiddlewareFactory?)context.RequestServices.GetService(typeof(IMiddlewareFactory)); - if (middlewareFactory == null) - { // No middleware factory throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoMiddlewareFactory(typeof(IMiddlewareFactory))); - } + } - var middleware = middlewareFactory.Create(middlewareType); - if (middleware == null) - { + var middleware = middlewareFactory.Create(middlewareType); + if (middleware == null) + { // The factory returned null, it's a broken implementation throw new InvalidOperationException(Resources.FormatException_UseMiddlewareUnableToCreateMiddleware(middlewareFactory.GetType(), middlewareType)); - } - - try - { - await middleware.InvokeAsync(context, next); - } - finally - { - middlewareFactory.Release(middleware); - } - }; - }); - } + } - private static Func Compile(MethodInfo methodInfo, ParameterInfo[] parameters) - { - // If we call something like - // - // public class Middleware - // { - // public Task Invoke(HttpContext context, ILoggerFactory loggerFactory) - // { - // - // } - // } - // - - // We'll end up with something like this: - // Generic version: - // - // Task Invoke(Middleware instance, HttpContext httpContext, IServiceProvider provider) - // { - // return instance.Invoke(httpContext, (ILoggerFactory)UseMiddlewareExtensions.GetService(provider, typeof(ILoggerFactory)); - // } - - // Non generic version: - // - // Task Invoke(object instance, HttpContext httpContext, IServiceProvider provider) - // { - // return ((Middleware)instance).Invoke(httpContext, (ILoggerFactory)UseMiddlewareExtensions.GetService(provider, typeof(ILoggerFactory)); - // } - - var middleware = typeof(T); - - var httpContextArg = Expression.Parameter(typeof(HttpContext), "httpContext"); - var providerArg = Expression.Parameter(typeof(IServiceProvider), "serviceProvider"); - var instanceArg = Expression.Parameter(middleware, "middleware"); - - var methodArguments = new Expression[parameters.Length]; - methodArguments[0] = httpContextArg; - for (int i = 1; i < parameters.Length; i++) - { - var parameterType = parameters[i].ParameterType; - if (parameterType.IsByRef) + try { - throw new NotSupportedException(Resources.FormatException_InvokeDoesNotSupportRefOrOutParams(InvokeMethodName)); + await middleware.InvokeAsync(context, next); } - - var parameterTypeExpression = new Expression[] + finally { + middlewareFactory.Release(middleware); + } + }; + }); + } + + private static Func Compile(MethodInfo methodInfo, ParameterInfo[] parameters) + { + // If we call something like + // + // public class Middleware + // { + // public Task Invoke(HttpContext context, ILoggerFactory loggerFactory) + // { + // + // } + // } + // + + // We'll end up with something like this: + // Generic version: + // + // Task Invoke(Middleware instance, HttpContext httpContext, IServiceProvider provider) + // { + // return instance.Invoke(httpContext, (ILoggerFactory)UseMiddlewareExtensions.GetService(provider, typeof(ILoggerFactory)); + // } + + // Non generic version: + // + // Task Invoke(object instance, HttpContext httpContext, IServiceProvider provider) + // { + // return ((Middleware)instance).Invoke(httpContext, (ILoggerFactory)UseMiddlewareExtensions.GetService(provider, typeof(ILoggerFactory)); + // } + + var middleware = typeof(T); + + var httpContextArg = Expression.Parameter(typeof(HttpContext), "httpContext"); + var providerArg = Expression.Parameter(typeof(IServiceProvider), "serviceProvider"); + var instanceArg = Expression.Parameter(middleware, "middleware"); + + var methodArguments = new Expression[parameters.Length]; + methodArguments[0] = httpContextArg; + for (int i = 1; i < parameters.Length; i++) + { + var parameterType = parameters[i].ParameterType; + if (parameterType.IsByRef) + { + throw new NotSupportedException(Resources.FormatException_InvokeDoesNotSupportRefOrOutParams(InvokeMethodName)); + } + + var parameterTypeExpression = new Expression[] + { providerArg, Expression.Constant(parameterType, typeof(Type)), Expression.Constant(methodInfo.DeclaringType, typeof(Type)) - }; + }; - var getServiceCall = Expression.Call(GetServiceInfo, parameterTypeExpression); - methodArguments[i] = Expression.Convert(getServiceCall, parameterType); - } + var getServiceCall = Expression.Call(GetServiceInfo, parameterTypeExpression); + methodArguments[i] = Expression.Convert(getServiceCall, parameterType); + } - Expression middlewareInstanceArg = instanceArg; - if (methodInfo.DeclaringType != null && methodInfo.DeclaringType != typeof(T)) - { - middlewareInstanceArg = Expression.Convert(middlewareInstanceArg, methodInfo.DeclaringType); - } + Expression middlewareInstanceArg = instanceArg; + if (methodInfo.DeclaringType != null && methodInfo.DeclaringType != typeof(T)) + { + middlewareInstanceArg = Expression.Convert(middlewareInstanceArg, methodInfo.DeclaringType); + } - var body = Expression.Call(middlewareInstanceArg, methodInfo, methodArguments); + var body = Expression.Call(middlewareInstanceArg, methodInfo, methodArguments); - var lambda = Expression.Lambda>(body, instanceArg, httpContextArg, providerArg); + var lambda = Expression.Lambda>(body, instanceArg, httpContextArg, providerArg); - return lambda.Compile(); - } + return lambda.Compile(); + } - private static object GetService(IServiceProvider sp, Type type, Type middleware) + private static object GetService(IServiceProvider sp, Type type, Type middleware) + { + var service = sp.GetService(type); + if (service == null) { - var service = sp.GetService(type); - if (service == null) - { - throw new InvalidOperationException(Resources.FormatException_InvokeMiddlewareNoService(type, middleware)); - } - - return service; + throw new InvalidOperationException(Resources.FormatException_InvokeMiddlewareNoService(type, middleware)); } + + return service; } } diff --git a/src/Http/Http.Abstractions/src/Extensions/UsePathBaseExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/UsePathBaseExtensions.cs index 0bdf4fcfed..5575806bed 100644 --- a/src/Http/Http.Abstractions/src/Extensions/UsePathBaseExtensions.cs +++ b/src/Http/Http.Abstractions/src/Extensions/UsePathBaseExtensions.cs @@ -2,37 +2,36 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Builder.Extensions; +using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extension methods for . +/// +public static class UsePathBaseExtensions { /// - /// Extension methods for . + /// Adds a middleware that extracts the specified path base from request path and postpend it to the request path base. /// - public static class UsePathBaseExtensions + /// The instance. + /// The path base to extract. + /// The instance. + public static IApplicationBuilder UsePathBase(this IApplicationBuilder app, PathString pathBase) { - /// - /// Adds a middleware that extracts the specified path base from request path and postpend it to the request path base. - /// - /// The instance. - /// The path base to extract. - /// The instance. - public static IApplicationBuilder UsePathBase(this IApplicationBuilder app, PathString pathBase) + if (app == null) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - // Strip trailing slashes - pathBase = pathBase.Value?.TrimEnd('/'); - if (!pathBase.HasValue) - { - return app; - } + throw new ArgumentNullException(nameof(app)); + } - return app.UseMiddleware(pathBase); + // Strip trailing slashes + pathBase = pathBase.Value?.TrimEnd('/'); + if (!pathBase.HasValue) + { + return app; } + + return app.UseMiddleware(pathBase); } -} \ No newline at end of file +} diff --git a/src/Http/Http.Abstractions/src/Extensions/UsePathBaseMiddleware.cs b/src/Http/Http.Abstractions/src/Extensions/UsePathBaseMiddleware.cs index 34ffc5738d..c0e6377cc3 100644 --- a/src/Http/Http.Abstractions/src/Extensions/UsePathBaseMiddleware.cs +++ b/src/Http/Http.Abstractions/src/Extensions/UsePathBaseMiddleware.cs @@ -5,72 +5,71 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder.Extensions +namespace Microsoft.AspNetCore.Builder.Extensions; + +/// +/// Represents a middleware that extracts the specified path base from request path and postpend it to the request path base. +/// +public class UsePathBaseMiddleware { + private readonly RequestDelegate _next; + private readonly PathString _pathBase; + /// - /// Represents a middleware that extracts the specified path base from request path and postpend it to the request path base. + /// Creates a new instance of . /// - public class UsePathBaseMiddleware + /// The delegate representing the next middleware in the request pipeline. + /// The path base to extract. + public UsePathBaseMiddleware(RequestDelegate next, PathString pathBase) { - private readonly RequestDelegate _next; - private readonly PathString _pathBase; + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } - /// - /// Creates a new instance of . - /// - /// The delegate representing the next middleware in the request pipeline. - /// The path base to extract. - public UsePathBaseMiddleware(RequestDelegate next, PathString pathBase) + if (!pathBase.HasValue) { - if (next == null) - { - throw new ArgumentNullException(nameof(next)); - } + throw new ArgumentException($"{nameof(pathBase)} cannot be null or empty."); + } - if (!pathBase.HasValue) - { - throw new ArgumentException($"{nameof(pathBase)} cannot be null or empty."); - } + _next = next; + _pathBase = pathBase; + } - _next = next; - _pathBase = pathBase; + /// + /// Executes the middleware. + /// + /// The for the current request. + /// A task that represents the execution of this middleware. + public Task Invoke(HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); } - /// - /// Executes the middleware. - /// - /// The for the current request. - /// A task that represents the execution of this middleware. - public Task Invoke(HttpContext context) + if (context.Request.Path.StartsWithSegments(_pathBase, out var matchedPath, out var remainingPath)) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (context.Request.Path.StartsWithSegments(_pathBase, out var matchedPath, out var remainingPath)) - { - return InvokeCore(context, matchedPath, remainingPath); - } - return _next(context); + return InvokeCore(context, matchedPath, remainingPath); } + return _next(context); + } - private async Task InvokeCore(HttpContext context, string matchedPath, string remainingPath) - { - var originalPath = context.Request.Path; - var originalPathBase = context.Request.PathBase; - context.Request.Path = remainingPath; - context.Request.PathBase = originalPathBase.Add(matchedPath); + private async Task InvokeCore(HttpContext context, string matchedPath, string remainingPath) + { + var originalPath = context.Request.Path; + var originalPathBase = context.Request.PathBase; + context.Request.Path = remainingPath; + context.Request.PathBase = originalPathBase.Add(matchedPath); - try - { - await _next(context); - } - finally - { - context.Request.Path = originalPath; - context.Request.PathBase = originalPathBase; - } + try + { + await _next(context); + } + finally + { + context.Request.Path = originalPath; + context.Request.PathBase = originalPathBase; } } } diff --git a/src/Http/Http.Abstractions/src/Extensions/UseWhenExtensions.cs b/src/Http/Http.Abstractions/src/Extensions/UseWhenExtensions.cs index 03117f32ef..0c1097f4a8 100644 --- a/src/Http/Http.Abstractions/src/Extensions/UseWhenExtensions.cs +++ b/src/Http/Http.Abstractions/src/Extensions/UseWhenExtensions.cs @@ -4,64 +4,63 @@ using System; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Builder -{ - using Predicate = Func; +namespace Microsoft.AspNetCore.Builder; + +using Predicate = Func; +/// +/// Extension methods for . +/// +public static class UseWhenExtensions +{ /// - /// Extension methods for . + /// Conditionally creates a branch in the request pipeline that is rejoined to the main pipeline. /// - public static class UseWhenExtensions + /// + /// Invoked with the request environment to determine if the branch should be taken + /// Configures a branch to take + /// + public static IApplicationBuilder UseWhen(this IApplicationBuilder app, Predicate predicate, Action configuration) { - /// - /// Conditionally creates a branch in the request pipeline that is rejoined to the main pipeline. - /// - /// - /// Invoked with the request environment to determine if the branch should be taken - /// Configures a branch to take - /// - public static IApplicationBuilder UseWhen(this IApplicationBuilder app, Predicate predicate, Action configuration) + if (app == null) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } + throw new ArgumentNullException(nameof(app)); + } - if (predicate == null) - { - throw new ArgumentNullException(nameof(predicate)); - } + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } - // Create and configure the branch builder right away; otherwise, - // we would end up running our branch after all the components - // that were subsequently added to the main builder. - var branchBuilder = app.New(); - configuration(branchBuilder); + // Create and configure the branch builder right away; otherwise, + // we would end up running our branch after all the components + // that were subsequently added to the main builder. + var branchBuilder = app.New(); + configuration(branchBuilder); - return app.Use(main => - { + return app.Use(main => + { // This is called only when the main application builder // is built, not per request. branchBuilder.Run(main); - var branch = branchBuilder.Build(); + var branch = branchBuilder.Build(); - return context => + return context => + { + if (predicate(context)) { - if (predicate(context)) - { - return branch(context); - } - else - { - return main(context); - } - }; - }); - } + return branch(context); + } + else + { + return main(context); + } + }; + }); } -} \ No newline at end of file +} diff --git a/src/Http/Http.Abstractions/src/FragmentString.cs b/src/Http/Http.Abstractions/src/FragmentString.cs index 531e3c16be..137db51814 100644 --- a/src/Http/Http.Abstractions/src/FragmentString.cs +++ b/src/Http/Http.Abstractions/src/FragmentString.cs @@ -3,165 +3,164 @@ using System; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Provides correct handling for FragmentString value when needed to generate a URI string +/// +public readonly struct FragmentString : IEquatable { /// - /// Provides correct handling for FragmentString value when needed to generate a URI string + /// Represents the empty fragment string. This field is read-only. + /// + public static readonly FragmentString Empty = new FragmentString(string.Empty); + + private readonly string _value; + + /// + /// Initialize the fragment string with a given value. This value must be in escaped and delimited format with + /// a leading '#' character. /// - public readonly struct FragmentString : IEquatable + /// The fragment string to be assigned to the Value property. + public FragmentString(string value) { - /// - /// Represents the empty fragment string. This field is read-only. - /// - public static readonly FragmentString Empty = new FragmentString(string.Empty); - - private readonly string _value; - - /// - /// Initialize the fragment string with a given value. This value must be in escaped and delimited format with - /// a leading '#' character. - /// - /// The fragment string to be assigned to the Value property. - public FragmentString(string value) + if (!string.IsNullOrEmpty(value) && value[0] != '#') { - if (!string.IsNullOrEmpty(value) && value[0] != '#') - { - throw new ArgumentException("The leading '#' must be included for a non-empty fragment.", nameof(value)); - } - _value = value; + throw new ArgumentException("The leading '#' must be included for a non-empty fragment.", nameof(value)); } + _value = value; + } - /// - /// The escaped fragment string with the leading '#' character - /// - public string Value - { - get { return _value; } - } + /// + /// The escaped fragment string with the leading '#' character + /// + public string Value + { + get { return _value; } + } - /// - /// True if the fragment string is not empty - /// - public bool HasValue - { - get { return !string.IsNullOrEmpty(_value); } - } + /// + /// True if the fragment string is not empty + /// + public bool HasValue + { + get { return !string.IsNullOrEmpty(_value); } + } - /// - /// Provides the fragment string escaped in a way which is correct for combining into the URI representation. - /// A leading '#' character will be included unless the Value is null or empty. Characters which are potentially - /// dangerous are escaped. - /// - /// The fragment string value - public override string ToString() - { - return ToUriComponent(); - } + /// + /// Provides the fragment string escaped in a way which is correct for combining into the URI representation. + /// A leading '#' character will be included unless the Value is null or empty. Characters which are potentially + /// dangerous are escaped. + /// + /// The fragment string value + public override string ToString() + { + return ToUriComponent(); + } - /// - /// Provides the fragment string escaped in a way which is correct for combining into the URI representation. - /// A leading '#' character will be included unless the Value is null or empty. Characters which are potentially - /// dangerous are escaped. - /// - /// The fragment string value - public string ToUriComponent() - { - // Escape things properly so System.Uri doesn't mis-interpret the data. - return HasValue ? _value : string.Empty; - } + /// + /// Provides the fragment string escaped in a way which is correct for combining into the URI representation. + /// A leading '#' character will be included unless the Value is null or empty. Characters which are potentially + /// dangerous are escaped. + /// + /// The fragment string value + public string ToUriComponent() + { + // Escape things properly so System.Uri doesn't mis-interpret the data. + return HasValue ? _value : string.Empty; + } - /// - /// Returns an FragmentString given the fragment as it is escaped in the URI format. The string MUST NOT contain any - /// value that is not a fragment. - /// - /// The escaped fragment as it appears in the URI format. - /// The resulting FragmentString - public static FragmentString FromUriComponent(string uriComponent) + /// + /// Returns an FragmentString given the fragment as it is escaped in the URI format. The string MUST NOT contain any + /// value that is not a fragment. + /// + /// The escaped fragment as it appears in the URI format. + /// The resulting FragmentString + public static FragmentString FromUriComponent(string uriComponent) + { + if (String.IsNullOrEmpty(uriComponent)) { - if (String.IsNullOrEmpty(uriComponent)) - { - return Empty; - } - return new FragmentString(uriComponent); + return Empty; } + return new FragmentString(uriComponent); + } - /// - /// Returns an FragmentString given the fragment as from a Uri object. Relative Uri objects are not supported. - /// - /// The Uri object - /// The resulting FragmentString - public static FragmentString FromUriComponent(Uri uri) + /// + /// Returns an FragmentString given the fragment as from a Uri object. Relative Uri objects are not supported. + /// + /// The Uri object + /// The resulting FragmentString + public static FragmentString FromUriComponent(Uri uri) + { + if (uri == null) { - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } - - string fragmentValue = uri.GetComponents(UriComponents.Fragment, UriFormat.UriEscaped); - if (!string.IsNullOrEmpty(fragmentValue)) - { - fragmentValue = "#" + fragmentValue; - } - return new FragmentString(fragmentValue); + throw new ArgumentNullException(nameof(uri)); } - /// - /// Evaluates if the current fragment is equal to another fragment . - /// - /// A to compare. - /// if the fragments are equal. - public bool Equals(FragmentString other) + string fragmentValue = uri.GetComponents(UriComponents.Fragment, UriFormat.UriEscaped); + if (!string.IsNullOrEmpty(fragmentValue)) { - if (!HasValue && !other.HasValue) - { - return true; - } - return string.Equals(_value, other._value, StringComparison.Ordinal); + fragmentValue = "#" + fragmentValue; } + return new FragmentString(fragmentValue); + } - /// - /// Evaluates if the current fragment is equal to an object . - /// - /// An object to compare. - /// if the fragments are equal. - public override bool Equals(object? obj) + /// + /// Evaluates if the current fragment is equal to another fragment . + /// + /// A to compare. + /// if the fragments are equal. + public bool Equals(FragmentString other) + { + if (!HasValue && !other.HasValue) { - if (ReferenceEquals(null, obj)) - { - return !HasValue; - } - return obj is FragmentString && Equals((FragmentString)obj); + return true; } + return string.Equals(_value, other._value, StringComparison.Ordinal); + } - /// - /// Gets a hash code for the value. - /// - /// The hash code as an . - public override int GetHashCode() + /// + /// Evaluates if the current fragment is equal to an object . + /// + /// An object to compare. + /// if the fragments are equal. + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - return (HasValue ? _value.GetHashCode() : 0); + return !HasValue; } + return obj is FragmentString && Equals((FragmentString)obj); + } - /// - /// Evaluates if one fragment is equal to another. - /// - /// A instance. - /// A instance. - /// if the fragments are equal. - public static bool operator ==(FragmentString left, FragmentString right) - { - return left.Equals(right); - } + /// + /// Gets a hash code for the value. + /// + /// The hash code as an . + public override int GetHashCode() + { + return (HasValue ? _value.GetHashCode() : 0); + } - /// - /// Evalutes if one framgent is not equal to another. - /// - /// A instance. - /// A instance. - /// if the fragments are not equal. - public static bool operator !=(FragmentString left, FragmentString right) - { - return !left.Equals(right); - } + /// + /// Evaluates if one fragment is equal to another. + /// + /// A instance. + /// A instance. + /// if the fragments are equal. + public static bool operator ==(FragmentString left, FragmentString right) + { + return left.Equals(right); + } + + /// + /// Evalutes if one framgent is not equal to another. + /// + /// A instance. + /// A instance. + /// if the fragments are not equal. + public static bool operator !=(FragmentString left, FragmentString right) + { + return !left.Equals(right); } } diff --git a/src/Http/Http.Abstractions/src/HostString.cs b/src/Http/Http.Abstractions/src/HostString.cs index 27b0a2da0f..99e985154e 100644 --- a/src/Http/Http.Abstractions/src/HostString.cs +++ b/src/Http/Http.Abstractions/src/HostString.cs @@ -7,384 +7,383 @@ using System.Globalization; using Microsoft.AspNetCore.Http.Abstractions; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Represents the host portion of a URI can be used to construct URI's properly formatted and encoded for use in +/// HTTP headers. +/// +public readonly struct HostString : IEquatable { + private readonly string _value; + /// - /// Represents the host portion of a URI can be used to construct URI's properly formatted and encoded for use in - /// HTTP headers. + /// Creates a new HostString without modification. The value should be Unicode rather than punycode, and may have a port. + /// IPv4 and IPv6 addresses are also allowed, and also may have ports. /// - public readonly struct HostString : IEquatable + /// + public HostString(string value) { - private readonly string _value; - - /// - /// Creates a new HostString without modification. The value should be Unicode rather than punycode, and may have a port. - /// IPv4 and IPv6 addresses are also allowed, and also may have ports. - /// - /// - public HostString(string value) - { - _value = value; - } + _value = value; + } - /// - /// Creates a new HostString from its host and port parts. - /// - /// The value should be Unicode rather than punycode. IPv6 addresses must use square braces. - /// A positive, greater than 0 value representing the port in the host string. - public HostString(string host, int port) + /// + /// Creates a new HostString from its host and port parts. + /// + /// The value should be Unicode rather than punycode. IPv6 addresses must use square braces. + /// A positive, greater than 0 value representing the port in the host string. + public HostString(string host, int port) + { + if (host == null) { - if (host == null) - { - throw new ArgumentNullException(nameof(host)); - } - - if (port <= 0) - { - throw new ArgumentOutOfRangeException(nameof(port), Resources.Exception_PortMustBeGreaterThanZero); - } - - int index; - if (host.IndexOf('[') == -1 - && (index = host.IndexOf(':')) >= 0 - && index < host.Length - 1 - && host.IndexOf(':', index + 1) >= 0) - { - // IPv6 without brackets ::1 is the only type of host with 2 or more colons - host = $"[{host}]"; - } - - _value = host + ":" + port.ToString(CultureInfo.InvariantCulture); + throw new ArgumentNullException(nameof(host)); } - /// - /// Returns the original value from the constructor. - /// - public string Value + if (port <= 0) { - get { return _value; } + throw new ArgumentOutOfRangeException(nameof(port), Resources.Exception_PortMustBeGreaterThanZero); } - /// - /// Returns true if the host is set. - /// - public bool HasValue + int index; + if (host.IndexOf('[') == -1 + && (index = host.IndexOf(':')) >= 0 + && index < host.Length - 1 + && host.IndexOf(':', index + 1) >= 0) { - get { return !string.IsNullOrEmpty(_value); } + // IPv6 without brackets ::1 is the only type of host with 2 or more colons + host = $"[{host}]"; } - /// - /// Returns the value of the host part of the value. The port is removed if it was present. - /// IPv6 addresses will have brackets added if they are missing. - /// - /// The host portion of the value. - public string Host + _value = host + ":" + port.ToString(CultureInfo.InvariantCulture); + } + + /// + /// Returns the original value from the constructor. + /// + public string Value + { + get { return _value; } + } + + /// + /// Returns true if the host is set. + /// + public bool HasValue + { + get { return !string.IsNullOrEmpty(_value); } + } + + /// + /// Returns the value of the host part of the value. The port is removed if it was present. + /// IPv6 addresses will have brackets added if they are missing. + /// + /// The host portion of the value. + public string Host + { + get { - get - { - GetParts(_value, out var host, out var port); + GetParts(_value, out var host, out var port); - return host.ToString(); - } + return host.ToString(); } + } - /// - /// Returns the value of the port part of the host, or null if none is found. - /// - /// The port portion of the value. - public int? Port + /// + /// Returns the value of the port part of the host, or null if none is found. + /// + /// The port portion of the value. + public int? Port + { + get { - get - { - GetParts(_value, out var host, out var port); + GetParts(_value, out var host, out var port); - if (!StringSegment.IsNullOrEmpty(port) - && int.TryParse(port.AsSpan(), NumberStyles.None, CultureInfo.InvariantCulture, out var p)) - { - return p; - } - - return null; + if (!StringSegment.IsNullOrEmpty(port) + && int.TryParse(port.AsSpan(), NumberStyles.None, CultureInfo.InvariantCulture, out var p)) + { + return p; } + + return null; } + } - /// - /// Returns the value as normalized by ToUriComponent(). - /// - /// The value as normalized by . - public override string ToString() + /// + /// Returns the value as normalized by ToUriComponent(). + /// + /// The value as normalized by . + public override string ToString() + { + return ToUriComponent(); + } + + /// + /// Returns the value properly formatted and encoded for use in a URI in a HTTP header. + /// Any Unicode is converted to punycode. IPv6 addresses will have brackets added if they are missing. + /// + /// The value formated for use in a URI or HTTP header. + public string ToUriComponent() + { + if (string.IsNullOrEmpty(_value)) { - return ToUriComponent(); + return string.Empty; } - /// - /// Returns the value properly formatted and encoded for use in a URI in a HTTP header. - /// Any Unicode is converted to punycode. IPv6 addresses will have brackets added if they are missing. - /// - /// The value formated for use in a URI or HTTP header. - public string ToUriComponent() + int i; + for (i = 0; i < _value.Length; ++i) { - if (string.IsNullOrEmpty(_value)) + if (!HostStringHelper.IsSafeHostStringChar(_value[i])) { - return string.Empty; - } - - int i; - for (i = 0; i < _value.Length; ++i) - { - if (!HostStringHelper.IsSafeHostStringChar(_value[i])) - { - break; - } + break; } + } - if (i != _value.Length) - { - GetParts(_value, out var host, out var port); - - var mapping = new IdnMapping(); - var encoded = mapping.GetAscii(host.Buffer!, host.Offset, host.Length); + if (i != _value.Length) + { + GetParts(_value, out var host, out var port); - return StringSegment.IsNullOrEmpty(port) - ? encoded - : string.Concat(encoded, ":", port.ToString()); - } + var mapping = new IdnMapping(); + var encoded = mapping.GetAscii(host.Buffer!, host.Offset, host.Length); - return _value; + return StringSegment.IsNullOrEmpty(port) + ? encoded + : string.Concat(encoded, ":", port.ToString()); } - /// - /// Creates a new HostString from the given URI component. - /// Any punycode will be converted to Unicode. - /// - /// The URI component string to create a from. - /// The that was created. - public static HostString FromUriComponent(string uriComponent) + return _value; + } + + /// + /// Creates a new HostString from the given URI component. + /// Any punycode will be converted to Unicode. + /// + /// The URI component string to create a from. + /// The that was created. + public static HostString FromUriComponent(string uriComponent) + { + if (!string.IsNullOrEmpty(uriComponent)) { - if (!string.IsNullOrEmpty(uriComponent)) + int index; + if (uriComponent.IndexOf('[') >= 0) { - int index; - if (uriComponent.IndexOf('[') >= 0) - { - // IPv6 in brackets [::1], maybe with port - } - else if ((index = uriComponent.IndexOf(':')) >= 0 - && index < uriComponent.Length - 1 - && uriComponent.IndexOf(':', index + 1) >= 0) + // IPv6 in brackets [::1], maybe with port + } + else if ((index = uriComponent.IndexOf(':')) >= 0 + && index < uriComponent.Length - 1 + && uriComponent.IndexOf(':', index + 1) >= 0) + { + // IPv6 without brackets ::1 is the only type of host with 2 or more colons + } + else if (uriComponent.IndexOf("xn--", StringComparison.Ordinal) >= 0) + { + // Contains punycode + if (index >= 0) { - // IPv6 without brackets ::1 is the only type of host with 2 or more colons + // Has a port + string port = uriComponent.Substring(index); + var mapping = new IdnMapping(); + uriComponent = mapping.GetUnicode(uriComponent, 0, index) + port; } - else if (uriComponent.IndexOf("xn--", StringComparison.Ordinal) >= 0) + else { - // Contains punycode - if (index >= 0) - { - // Has a port - string port = uriComponent.Substring(index); - var mapping = new IdnMapping(); - uriComponent = mapping.GetUnicode(uriComponent, 0, index) + port; - } - else - { - var mapping = new IdnMapping(); - uriComponent = mapping.GetUnicode(uriComponent); - } + var mapping = new IdnMapping(); + uriComponent = mapping.GetUnicode(uriComponent); } } - return new HostString(uriComponent); } + return new HostString(uriComponent); + } - /// - /// Creates a new HostString from the host and port of the give Uri instance. - /// Punycode will be converted to Unicode. - /// - /// The to create a from. - /// The that was created. - public static HostString FromUriComponent(Uri uri) + /// + /// Creates a new HostString from the host and port of the give Uri instance. + /// Punycode will be converted to Unicode. + /// + /// The to create a from. + /// The that was created. + public static HostString FromUriComponent(Uri uri) + { + if (uri == null) { - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } + throw new ArgumentNullException(nameof(uri)); + } + + return new HostString(uri.GetComponents( + UriComponents.NormalizedHost | // Always convert punycode to Unicode. + UriComponents.HostAndPort, UriFormat.Unescaped)); + } - return new HostString(uri.GetComponents( - UriComponents.NormalizedHost | // Always convert punycode to Unicode. - UriComponents.HostAndPort, UriFormat.Unescaped)); + /// + /// Matches the host portion of a host header value against a list of patterns. + /// The host may be the encoded punycode or decoded unicode form so long as the pattern + /// uses the same format. + /// + /// Host header value with or without a port. + /// A set of pattern to match, without ports. + /// + /// The port on the given value is ignored. The patterns should not have ports. + /// The patterns may be exact matches like "example.com", a top level wildcard "*" + /// that matches all hosts, or a subdomain wildcard like "*.example.com" that matches + /// "abc.example.com:443" but not "example.com:443". + /// Matching is case insensitive. + /// + /// if matches any of the patterns. + public static bool MatchesAny(StringSegment value, IList patterns) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + if (patterns == null) + { + throw new ArgumentNullException(nameof(patterns)); } - /// - /// Matches the host portion of a host header value against a list of patterns. - /// The host may be the encoded punycode or decoded unicode form so long as the pattern - /// uses the same format. - /// - /// Host header value with or without a port. - /// A set of pattern to match, without ports. - /// - /// The port on the given value is ignored. The patterns should not have ports. - /// The patterns may be exact matches like "example.com", a top level wildcard "*" - /// that matches all hosts, or a subdomain wildcard like "*.example.com" that matches - /// "abc.example.com:443" but not "example.com:443". - /// Matching is case insensitive. - /// - /// if matches any of the patterns. - public static bool MatchesAny(StringSegment value, IList patterns) + // Drop the port + GetParts(value, out var host, out var port); + + for (int i = 0; i < port.Length; i++) { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - if (patterns == null) + if (port[i] < '0' || '9' < port[i]) { - throw new ArgumentNullException(nameof(patterns)); + throw new FormatException($"The given host value '{value}' has a malformed port."); } + } - // Drop the port - GetParts(value, out var host, out var port); + var count = patterns.Count; + for (int i = 0; i < count; i++) + { + var pattern = patterns[i]; - for (int i = 0; i < port.Length; i++) + if (pattern == "*") { - if (port[i] < '0' || '9' < port[i]) - { - throw new FormatException($"The given host value '{value}' has a malformed port."); - } + return true; } - var count = patterns.Count; - for (int i = 0; i < count; i++) + if (StringSegment.Equals(pattern, host, StringComparison.OrdinalIgnoreCase)) { - var pattern = patterns[i]; + return true; + } - if (pattern == "*") - { - return true; - } + // Sub-domain wildcards: *.example.com + if (pattern.StartsWith("*.", StringComparison.Ordinal) && host.Length >= pattern.Length) + { + // .example.com + var allowedRoot = pattern.Subsegment(1); - if (StringSegment.Equals(pattern, host, StringComparison.OrdinalIgnoreCase)) + var hostRoot = host.Subsegment(host.Length - allowedRoot.Length); + if (hostRoot.Equals(allowedRoot, StringComparison.OrdinalIgnoreCase)) { return true; } - - // Sub-domain wildcards: *.example.com - if (pattern.StartsWith("*.", StringComparison.Ordinal) && host.Length >= pattern.Length) - { - // .example.com - var allowedRoot = pattern.Subsegment(1); - - var hostRoot = host.Subsegment(host.Length - allowedRoot.Length); - if (hostRoot.Equals(allowedRoot, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } } - - return false; } - /// - /// Compares the equality of the Value property, ignoring case. - /// - /// The to compare against. - /// if they have the same value. - public bool Equals(HostString other) + return false; + } + + /// + /// Compares the equality of the Value property, ignoring case. + /// + /// The to compare against. + /// if they have the same value. + public bool Equals(HostString other) + { + if (!HasValue && !other.HasValue) { - if (!HasValue && !other.HasValue) - { - return true; - } - return string.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase); + return true; } + return string.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase); + } - /// - /// Compares against the given object only if it is a HostString. - /// - /// The to compare against. - /// if they have the same value. - public override bool Equals(object? obj) + /// + /// Compares against the given object only if it is a HostString. + /// + /// The to compare against. + /// if they have the same value. + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - if (ReferenceEquals(null, obj)) - { - return !HasValue; - } - return obj is HostString && Equals((HostString)obj); + return !HasValue; } + return obj is HostString && Equals((HostString)obj); + } - /// - /// Gets a hash code for the value. - /// - /// The hash code as an . - public override int GetHashCode() + /// + /// Gets a hash code for the value. + /// + /// The hash code as an . + public override int GetHashCode() + { + return (HasValue ? StringComparer.OrdinalIgnoreCase.GetHashCode(_value) : 0); + } + + /// + /// Compares the two instances for equality. + /// + /// The left parameter. + /// The right parameter. + /// if both 's have the same value. + public static bool operator ==(HostString left, HostString right) + { + return left.Equals(right); + } + + /// + /// Compares the two instances for inequality. + /// + /// The left parameter. + /// The right parameter. + /// if both 's values are not equal. + public static bool operator !=(HostString left, HostString right) + { + return !left.Equals(right); + } + + /// + /// Parses the current value. IPv6 addresses will have brackets added if they are missing. + /// + /// The value to get the parts of. + /// The portion of the which represents the host. + /// The portion of the which represents the port. + private static void GetParts(StringSegment value, out StringSegment host, out StringSegment port) + { + int index; + port = null; + host = null; + + if (StringSegment.IsNullOrEmpty(value)) { - return (HasValue ? StringComparer.OrdinalIgnoreCase.GetHashCode(_value) : 0); + return; } - - /// - /// Compares the two instances for equality. - /// - /// The left parameter. - /// The right parameter. - /// if both 's have the same value. - public static bool operator ==(HostString left, HostString right) + else if ((index = value.IndexOf(']')) >= 0) + { + // IPv6 in brackets [::1], maybe with port + host = value.Subsegment(0, index + 1); + // Is there a colon and at least one character? + if (index + 2 < value.Length && value[index + 1] == ':') + { + port = value.Subsegment(index + 2); + } + } + else if ((index = value.IndexOf(':')) >= 0 + && index < value.Length - 1 + && value.IndexOf(':', index + 1) >= 0) { - return left.Equals(right); + // IPv6 without brackets ::1 is the only type of host with 2 or more colons + host = $"[{value}]"; + port = null; } - - /// - /// Compares the two instances for inequality. - /// - /// The left parameter. - /// The right parameter. - /// if both 's values are not equal. - public static bool operator !=(HostString left, HostString right) + else if (index >= 0) { - return !left.Equals(right); + // Has a port + host = value.Subsegment(0, index); + port = value.Subsegment(index + 1); } - - /// - /// Parses the current value. IPv6 addresses will have brackets added if they are missing. - /// - /// The value to get the parts of. - /// The portion of the which represents the host. - /// The portion of the which represents the port. - private static void GetParts(StringSegment value, out StringSegment host, out StringSegment port) + else { - int index; + host = value; port = null; - host = null; - - if (StringSegment.IsNullOrEmpty(value)) - { - return; - } - else if ((index = value.IndexOf(']')) >= 0) - { - // IPv6 in brackets [::1], maybe with port - host = value.Subsegment(0, index + 1); - // Is there a colon and at least one character? - if (index + 2 < value.Length && value[index + 1] == ':') - { - port = value.Subsegment(index + 2); - } - } - else if ((index = value.IndexOf(':')) >= 0 - && index < value.Length - 1 - && value.IndexOf(':', index + 1) >= 0) - { - // IPv6 without brackets ::1 is the only type of host with 2 or more colons - host = $"[{value}]"; - port = null; - } - else if (index >= 0) - { - // Has a port - host = value.Subsegment(0, index); - port = value.Subsegment(index + 1); - } - else - { - host = value; - port = null; - } } } } diff --git a/src/Http/Http.Abstractions/src/HttpContext.cs b/src/Http/Http.Abstractions/src/HttpContext.cs index d229180234..eea64901b7 100644 --- a/src/Http/Http.Abstractions/src/HttpContext.cs +++ b/src/Http/Http.Abstractions/src/HttpContext.cs @@ -7,72 +7,71 @@ using System.Security.Claims; using System.Threading; using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Encapsulates all HTTP-specific information about an individual HTTP request. +/// +public abstract class HttpContext { /// - /// Encapsulates all HTTP-specific information about an individual HTTP request. + /// Gets the collection of HTTP features provided by the server and middleware available on this request. /// - public abstract class HttpContext - { - /// - /// Gets the collection of HTTP features provided by the server and middleware available on this request. - /// - public abstract IFeatureCollection Features { get; } + public abstract IFeatureCollection Features { get; } - /// - /// Gets the object for this request. - /// - public abstract HttpRequest Request { get; } + /// + /// Gets the object for this request. + /// + public abstract HttpRequest Request { get; } - /// - /// Gets the object for this request. - /// - public abstract HttpResponse Response { get; } + /// + /// Gets the object for this request. + /// + public abstract HttpResponse Response { get; } - /// - /// Gets information about the underlying connection for this request. - /// - public abstract ConnectionInfo Connection { get; } + /// + /// Gets information about the underlying connection for this request. + /// + public abstract ConnectionInfo Connection { get; } - /// - /// Gets an object that manages the establishment of WebSocket connections for this request. - /// - public abstract WebSocketManager WebSockets { get; } + /// + /// Gets an object that manages the establishment of WebSocket connections for this request. + /// + public abstract WebSocketManager WebSockets { get; } - /// - /// Gets or sets the user for this request. - /// - public abstract ClaimsPrincipal User { get; set; } + /// + /// Gets or sets the user for this request. + /// + public abstract ClaimsPrincipal User { get; set; } - /// - /// Gets or sets a key/value collection that can be used to share data within the scope of this request. - /// - public abstract IDictionary Items { get; set; } + /// + /// Gets or sets a key/value collection that can be used to share data within the scope of this request. + /// + public abstract IDictionary Items { get; set; } - /// - /// Gets or sets the that provides access to the request's service container. - /// - public abstract IServiceProvider RequestServices { get; set; } + /// + /// Gets or sets the that provides access to the request's service container. + /// + public abstract IServiceProvider RequestServices { get; set; } - /// - /// Notifies when the connection underlying this request is aborted and thus request operations should be - /// cancelled. - /// - public abstract CancellationToken RequestAborted { get; set; } + /// + /// Notifies when the connection underlying this request is aborted and thus request operations should be + /// cancelled. + /// + public abstract CancellationToken RequestAborted { get; set; } - /// - /// Gets or sets a unique identifier to represent this request in trace logs. - /// - public abstract string TraceIdentifier { get; set; } + /// + /// Gets or sets a unique identifier to represent this request in trace logs. + /// + public abstract string TraceIdentifier { get; set; } - /// - /// Gets or sets the object used to manage user session data for this request. - /// - public abstract ISession Session { get; set; } + /// + /// Gets or sets the object used to manage user session data for this request. + /// + public abstract ISession Session { get; set; } - /// - /// Aborts the connection underlying this request. - /// - public abstract void Abort(); - } + /// + /// Aborts the connection underlying this request. + /// + public abstract void Abort(); } diff --git a/src/Http/Http.Abstractions/src/HttpMethods.cs b/src/Http/Http.Abstractions/src/HttpMethods.cs index b832792255..2676a58dd7 100644 --- a/src/Http/Http.Abstractions/src/HttpMethods.cs +++ b/src/Http/Http.Abstractions/src/HttpMethods.cs @@ -3,196 +3,195 @@ using System; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Contains methods to verify the request method of an HTTP request. +/// +public static class HttpMethods { + // We are intentionally using 'static readonly' here instead of 'const'. + // 'const' values would be embedded into each assembly that used them + // and each consuming assembly would have a different 'string' instance. + // Using .'static readonly' means that all consumers get these exact same + // 'string' instance, which means the 'ReferenceEquals' checks below work + // and allow us to optimize comparisons when these constants are used. + + // Please do NOT change these to 'const' /// - /// Contains methods to verify the request method of an HTTP request. + /// HTTP "CONNECT" method. /// - public static class HttpMethods - { - // We are intentionally using 'static readonly' here instead of 'const'. - // 'const' values would be embedded into each assembly that used them - // and each consuming assembly would have a different 'string' instance. - // Using .'static readonly' means that all consumers get these exact same - // 'string' instance, which means the 'ReferenceEquals' checks below work - // and allow us to optimize comparisons when these constants are used. - - // Please do NOT change these to 'const' - /// - /// HTTP "CONNECT" method. - /// - public static readonly string Connect = "CONNECT"; - /// - /// HTTP "DELETE" method. - /// - public static readonly string Delete = "DELETE"; - /// - /// HTTP "GET" method. - /// - public static readonly string Get = "GET"; - /// - /// HTTP "HEAD" method. - /// - public static readonly string Head = "HEAD"; - /// - /// HTTP "OPTIONS" method. - /// - public static readonly string Options = "OPTIONS"; - /// - /// HTTP "PATCH" method. - /// - public static readonly string Patch = "PATCH"; - /// - /// HTTP "POST" method. - /// - public static readonly string Post = "POST"; - /// - /// HTTP "PUT" method. - /// - public static readonly string Put = "PUT"; - /// - /// HTTP "TRACE" method. - /// - public static readonly string Trace = "TRACE"; + public static readonly string Connect = "CONNECT"; + /// + /// HTTP "DELETE" method. + /// + public static readonly string Delete = "DELETE"; + /// + /// HTTP "GET" method. + /// + public static readonly string Get = "GET"; + /// + /// HTTP "HEAD" method. + /// + public static readonly string Head = "HEAD"; + /// + /// HTTP "OPTIONS" method. + /// + public static readonly string Options = "OPTIONS"; + /// + /// HTTP "PATCH" method. + /// + public static readonly string Patch = "PATCH"; + /// + /// HTTP "POST" method. + /// + public static readonly string Post = "POST"; + /// + /// HTTP "PUT" method. + /// + public static readonly string Put = "PUT"; + /// + /// HTTP "TRACE" method. + /// + public static readonly string Trace = "TRACE"; - /// - /// Returns a value that indicates if the HTTP request method is CONNECT. - /// - /// The HTTP request method. - /// - /// if the method is CONNECT; otherwise, . - /// - public static bool IsConnect(string method) - { - return Equals(Connect, method); - } + /// + /// Returns a value that indicates if the HTTP request method is CONNECT. + /// + /// The HTTP request method. + /// + /// if the method is CONNECT; otherwise, . + /// + public static bool IsConnect(string method) + { + return Equals(Connect, method); + } - /// - /// Returns a value that indicates if the HTTP request method is DELETE. - /// - /// The HTTP request method. - /// - /// if the method is DELETE; otherwise, . - /// - public static bool IsDelete(string method) - { - return Equals(Delete, method); - } + /// + /// Returns a value that indicates if the HTTP request method is DELETE. + /// + /// The HTTP request method. + /// + /// if the method is DELETE; otherwise, . + /// + public static bool IsDelete(string method) + { + return Equals(Delete, method); + } - /// - /// Returns a value that indicates if the HTTP request method is GET. - /// - /// The HTTP request method. - /// - /// if the method is GET; otherwise, . - /// - public static bool IsGet(string method) - { - return Equals(Get, method); - } + /// + /// Returns a value that indicates if the HTTP request method is GET. + /// + /// The HTTP request method. + /// + /// if the method is GET; otherwise, . + /// + public static bool IsGet(string method) + { + return Equals(Get, method); + } - /// - /// Returns a value that indicates if the HTTP request method is HEAD. - /// - /// The HTTP request method. - /// - /// if the method is HEAD; otherwise, . - /// - public static bool IsHead(string method) - { - return Equals(Head, method); - } + /// + /// Returns a value that indicates if the HTTP request method is HEAD. + /// + /// The HTTP request method. + /// + /// if the method is HEAD; otherwise, . + /// + public static bool IsHead(string method) + { + return Equals(Head, method); + } - /// - /// Returns a value that indicates if the HTTP request method is OPTIONS. - /// - /// The HTTP request method. - /// - /// if the method is OPTIONS; otherwise, . - /// - public static bool IsOptions(string method) - { - return Equals(Options, method); - } + /// + /// Returns a value that indicates if the HTTP request method is OPTIONS. + /// + /// The HTTP request method. + /// + /// if the method is OPTIONS; otherwise, . + /// + public static bool IsOptions(string method) + { + return Equals(Options, method); + } - /// - /// Returns a value that indicates if the HTTP request method is PATCH. - /// - /// The HTTP request method. - /// - /// if the method is PATCH; otherwise, . - /// - public static bool IsPatch(string method) - { - return Equals(Patch, method); - } + /// + /// Returns a value that indicates if the HTTP request method is PATCH. + /// + /// The HTTP request method. + /// + /// if the method is PATCH; otherwise, . + /// + public static bool IsPatch(string method) + { + return Equals(Patch, method); + } - /// - /// Returns a value that indicates if the HTTP request method is POST. - /// - /// The HTTP request method. - /// - /// if the method is POST; otherwise, . - /// - public static bool IsPost(string method) - { - return Equals(Post, method); - } + /// + /// Returns a value that indicates if the HTTP request method is POST. + /// + /// The HTTP request method. + /// + /// if the method is POST; otherwise, . + /// + public static bool IsPost(string method) + { + return Equals(Post, method); + } - /// - /// Returns a value that indicates if the HTTP request method is PUT. - /// - /// The HTTP request method. - /// - /// if the method is PUT; otherwise, . - /// - public static bool IsPut(string method) - { - return Equals(Put, method); - } + /// + /// Returns a value that indicates if the HTTP request method is PUT. + /// + /// The HTTP request method. + /// + /// if the method is PUT; otherwise, . + /// + public static bool IsPut(string method) + { + return Equals(Put, method); + } - /// - /// Returns a value that indicates if the HTTP request method is TRACE. - /// - /// The HTTP request method. - /// - /// if the method is TRACE; otherwise, . - /// - public static bool IsTrace(string method) - { - return Equals(Trace, method); - } + /// + /// Returns a value that indicates if the HTTP request method is TRACE. + /// + /// The HTTP request method. + /// + /// if the method is TRACE; otherwise, . + /// + public static bool IsTrace(string method) + { + return Equals(Trace, method); + } - /// - /// Returns the equivalent static instance, or the original instance if none match. This conversion is optional but allows for performance optimizations when comparing method values elsewhere. - /// - /// - /// - public static string GetCanonicalizedValue(string method) => method switch - { - string _ when IsGet(method) => Get, - string _ when IsPost(method) => Post, - string _ when IsPut(method) => Put, - string _ when IsDelete(method) => Delete, - string _ when IsOptions(method) => Options, - string _ when IsHead(method) => Head, - string _ when IsPatch(method) => Patch, - string _ when IsTrace(method) => Trace, - string _ when IsConnect(method) => Connect, - string _ => method - }; + /// + /// Returns the equivalent static instance, or the original instance if none match. This conversion is optional but allows for performance optimizations when comparing method values elsewhere. + /// + /// + /// + public static string GetCanonicalizedValue(string method) => method switch + { + string _ when IsGet(method) => Get, + string _ when IsPost(method) => Post, + string _ when IsPut(method) => Put, + string _ when IsDelete(method) => Delete, + string _ when IsOptions(method) => Options, + string _ when IsHead(method) => Head, + string _ when IsPatch(method) => Patch, + string _ when IsTrace(method) => Trace, + string _ when IsConnect(method) => Connect, + string _ => method + }; - /// - /// Returns a value that indicates if the HTTP methods are the same. - /// - /// The first HTTP request method to compare. - /// The second HTTP request method to compare. - /// - /// if the methods are the same; otherwise, . - /// - public static bool Equals(string methodA, string methodB) - { - return object.ReferenceEquals(methodA, methodB) || StringComparer.OrdinalIgnoreCase.Equals(methodA, methodB); - } + /// + /// Returns a value that indicates if the HTTP methods are the same. + /// + /// The first HTTP request method to compare. + /// The second HTTP request method to compare. + /// + /// if the methods are the same; otherwise, . + /// + public static bool Equals(string methodA, string methodB) + { + return object.ReferenceEquals(methodA, methodB) || StringComparer.OrdinalIgnoreCase.Equals(methodA, methodB); } } diff --git a/src/Http/Http.Abstractions/src/HttpProtocol.cs b/src/Http/Http.Abstractions/src/HttpProtocol.cs index b21dee762d..f8b4c50ac4 100644 --- a/src/Http/Http.Abstractions/src/HttpProtocol.cs +++ b/src/Http/Http.Abstractions/src/HttpProtocol.cs @@ -3,128 +3,127 @@ using System; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Contains methods to verify the request protocol version of an HTTP request. +/// +public static class HttpProtocol { + // We are intentionally using 'static readonly' here instead of 'const'. + // 'const' values would be embedded into each assembly that used them + // and each consuming assembly would have a different 'string' instance. + // Using .'static readonly' means that all consumers get these exact same + // 'string' instance, which means the 'ReferenceEquals' checks below work + // and allow us to optimize comparisons when these constants are used. + + // Please do NOT change these to 'const' + + /// + /// HTTP protocol version 0.9. + /// + public static readonly string Http09 = "HTTP/0.9"; + + /// + /// HTTP protocol version 1.0. + /// + public static readonly string Http10 = "HTTP/1.0"; + + /// + /// HTTP protocol version 1.1. + /// + public static readonly string Http11 = "HTTP/1.1"; + /// - /// Contains methods to verify the request protocol version of an HTTP request. + /// HTTP protocol version 2. /// - public static class HttpProtocol + public static readonly string Http2 = "HTTP/2"; + + /// + /// HTTP protcol version 3. + /// + public static readonly string Http3 = "HTTP/3"; + + /// + /// Returns a value that indicates if the HTTP request protocol is HTTP/0.9. + /// + /// The HTTP request protocol. + /// + /// if the protocol is HTTP/0.9; otherwise, . + /// + public static bool IsHttp09(string protocol) { - // We are intentionally using 'static readonly' here instead of 'const'. - // 'const' values would be embedded into each assembly that used them - // and each consuming assembly would have a different 'string' instance. - // Using .'static readonly' means that all consumers get these exact same - // 'string' instance, which means the 'ReferenceEquals' checks below work - // and allow us to optimize comparisons when these constants are used. - - // Please do NOT change these to 'const' - - /// - /// HTTP protocol version 0.9. - /// - public static readonly string Http09 = "HTTP/0.9"; - - /// - /// HTTP protocol version 1.0. - /// - public static readonly string Http10 = "HTTP/1.0"; - - /// - /// HTTP protocol version 1.1. - /// - public static readonly string Http11 = "HTTP/1.1"; - - /// - /// HTTP protocol version 2. - /// - public static readonly string Http2 = "HTTP/2"; - - /// - /// HTTP protcol version 3. - /// - public static readonly string Http3 = "HTTP/3"; - - /// - /// Returns a value that indicates if the HTTP request protocol is HTTP/0.9. - /// - /// The HTTP request protocol. - /// - /// if the protocol is HTTP/0.9; otherwise, . - /// - public static bool IsHttp09(string protocol) - { - return object.ReferenceEquals(Http09, protocol) || StringComparer.OrdinalIgnoreCase.Equals(Http09, protocol); - } + return object.ReferenceEquals(Http09, protocol) || StringComparer.OrdinalIgnoreCase.Equals(Http09, protocol); + } - /// - /// Returns a value that indicates if the HTTP request protocol is HTTP/1.0. - /// - /// The HTTP request protocol. - /// - /// if the protocol is HTTP/1.0; otherwise, . - /// - public static bool IsHttp10(string protocol) - { - return object.ReferenceEquals(Http10, protocol) || StringComparer.OrdinalIgnoreCase.Equals(Http10, protocol); - } + /// + /// Returns a value that indicates if the HTTP request protocol is HTTP/1.0. + /// + /// The HTTP request protocol. + /// + /// if the protocol is HTTP/1.0; otherwise, . + /// + public static bool IsHttp10(string protocol) + { + return object.ReferenceEquals(Http10, protocol) || StringComparer.OrdinalIgnoreCase.Equals(Http10, protocol); + } - /// - /// Returns a value that indicates if the HTTP request protocol is HTTP/1.1. - /// - /// The HTTP request protocol. - /// - /// if the protocol is HTTP/1.1; otherwise, . - /// - public static bool IsHttp11(string protocol) - { - return object.ReferenceEquals(Http11, protocol) || StringComparer.OrdinalIgnoreCase.Equals(Http11, protocol); - } + /// + /// Returns a value that indicates if the HTTP request protocol is HTTP/1.1. + /// + /// The HTTP request protocol. + /// + /// if the protocol is HTTP/1.1; otherwise, . + /// + public static bool IsHttp11(string protocol) + { + return object.ReferenceEquals(Http11, protocol) || StringComparer.OrdinalIgnoreCase.Equals(Http11, protocol); + } - /// - /// Returns a value that indicates if the HTTP request protocol is HTTP/2. - /// - /// The HTTP request protocol. - /// - /// if the protocol is HTTP/2; otherwise, . - /// - public static bool IsHttp2(string protocol) - { - return object.ReferenceEquals(Http2, protocol) || StringComparer.OrdinalIgnoreCase.Equals(Http2, protocol); - } + /// + /// Returns a value that indicates if the HTTP request protocol is HTTP/2. + /// + /// The HTTP request protocol. + /// + /// if the protocol is HTTP/2; otherwise, . + /// + public static bool IsHttp2(string protocol) + { + return object.ReferenceEquals(Http2, protocol) || StringComparer.OrdinalIgnoreCase.Equals(Http2, protocol); + } + + /// + /// Returns a value that indicates if the HTTP request protocol is HTTP/3. + /// + /// The HTTP request protocol. + /// + /// if the protocol is HTTP/3; otherwise, . + /// + public static bool IsHttp3(string protocol) + { + return object.ReferenceEquals(Http3, protocol) || StringComparer.OrdinalIgnoreCase.Equals(Http3, protocol); + } - /// - /// Returns a value that indicates if the HTTP request protocol is HTTP/3. - /// - /// The HTTP request protocol. - /// - /// if the protocol is HTTP/3; otherwise, . - /// - public static bool IsHttp3(string protocol) + /// + /// Gets the HTTP request protocol for the specified . + /// + /// The version. + /// A HTTP request protocol. + public static string GetHttpProtocol(Version version) + { + if (version == null) { - return object.ReferenceEquals(Http3, protocol) || StringComparer.OrdinalIgnoreCase.Equals(Http3, protocol); + throw new ArgumentNullException(nameof(version)); } - /// - /// Gets the HTTP request protocol for the specified . - /// - /// The version. - /// A HTTP request protocol. - public static string GetHttpProtocol(Version version) + return version switch { - if (version == null) - { - throw new ArgumentNullException(nameof(version)); - } - - return version switch - { - { Major: 3, Minor: 0 } => Http3, - { Major: 2, Minor: 0 } => Http2, - { Major: 1, Minor: 1 } => Http11, - { Major: 1, Minor: 0 } => Http10, - { Major: 0, Minor: 9 } => Http09, - _ => throw new ArgumentOutOfRangeException(nameof(version), "Version doesn't map to a known HTTP protocol.") - }; - } + { Major: 3, Minor: 0 } => Http3, + { Major: 2, Minor: 0 } => Http2, + { Major: 1, Minor: 1 } => Http11, + { Major: 1, Minor: 0 } => Http10, + { Major: 0, Minor: 9 } => Http09, + _ => throw new ArgumentOutOfRangeException(nameof(version), "Version doesn't map to a known HTTP protocol.") + }; } } diff --git a/src/Http/Http.Abstractions/src/HttpRequest.cs b/src/Http/Http.Abstractions/src/HttpRequest.cs index c0f78134ea..12bcb6f0e4 100644 --- a/src/Http/Http.Abstractions/src/HttpRequest.cs +++ b/src/Http/Http.Abstractions/src/HttpRequest.cs @@ -8,129 +8,128 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Represents the incoming side of an individual HTTP request. +/// +public abstract class HttpRequest { /// - /// Represents the incoming side of an individual HTTP request. - /// - public abstract class HttpRequest - { - /// - /// Gets the for this request. - /// - public abstract HttpContext HttpContext { get; } - - /// - /// Gets or sets the HTTP method. - /// - /// The HTTP method. - public abstract string Method { get; set; } - - /// - /// Gets or sets the HTTP request scheme. - /// - /// The HTTP request scheme. - public abstract string Scheme { get; set; } - - /// - /// Returns true if the RequestScheme is https. - /// - /// true if this request is using https; otherwise, false. - public abstract bool IsHttps { get; set; } - - /// - /// Gets or sets the Host header. May include the port. - /// - /// The Host header. - public abstract HostString Host { get; set; } - - /// - /// Gets or sets the base path for the request. The path base should not end with a trailing slash. - /// - /// The base path for the request. - public abstract PathString PathBase { get; set; } - - /// - /// Gets or sets the request path from RequestPath. - /// - /// The request path from RequestPath. - public abstract PathString Path { get; set; } - - /// - /// Gets or sets the raw query string used to create the query collection in Request.Query. - /// - /// The raw query string. - public abstract QueryString QueryString { get; set; } - - /// - /// Gets the query value collection parsed from Request.QueryString. - /// - /// The query value collection parsed from Request.QueryString. - public abstract IQueryCollection Query { get; set; } - - /// - /// Gets or sets the request protocol (e.g. HTTP/1.1). - /// - /// The request protocol. - public abstract string Protocol { get; set; } - - /// - /// Gets the request headers. - /// - /// The request headers. - public abstract IHeaderDictionary Headers { get; } - - /// - /// Gets the collection of Cookies for this request. - /// - /// The collection of Cookies for this request. - public abstract IRequestCookieCollection Cookies { get; set; } - - /// - /// Gets or sets the Content-Length header. - /// - /// The value of the Content-Length header, if any. - public abstract long? ContentLength { get; set; } - - /// - /// Gets or sets the Content-Type header. - /// - /// The Content-Type header. - public abstract string? ContentType { get; set; } - - /// - /// Gets or sets the request body . - /// - /// The request body . - public abstract Stream Body { get; set; } - - /// - /// Gets the request body . - /// - /// The request body . - public virtual PipeReader BodyReader { get => throw new NotImplementedException(); } - - /// - /// Checks the Content-Type header for form types. - /// - /// true if the Content-Type header represents a form content type; otherwise, false. - public abstract bool HasFormContentType { get; } - - /// - /// Gets or sets the request body as a form. - /// - public abstract IFormCollection Form { get; set; } - - /// - /// Reads the request body if it is a form. - /// - /// - public abstract Task ReadFormAsync(CancellationToken cancellationToken = new CancellationToken()); - - /// - /// Gets the collection of route values for this request. - /// - /// The collection of route values for this request. - public virtual RouteValueDictionary RouteValues { get; set; } = null!; - } + /// Gets the for this request. + /// + public abstract HttpContext HttpContext { get; } + + /// + /// Gets or sets the HTTP method. + /// + /// The HTTP method. + public abstract string Method { get; set; } + + /// + /// Gets or sets the HTTP request scheme. + /// + /// The HTTP request scheme. + public abstract string Scheme { get; set; } + + /// + /// Returns true if the RequestScheme is https. + /// + /// true if this request is using https; otherwise, false. + public abstract bool IsHttps { get; set; } + + /// + /// Gets or sets the Host header. May include the port. + /// + /// The Host header. + public abstract HostString Host { get; set; } + + /// + /// Gets or sets the base path for the request. The path base should not end with a trailing slash. + /// + /// The base path for the request. + public abstract PathString PathBase { get; set; } + + /// + /// Gets or sets the request path from RequestPath. + /// + /// The request path from RequestPath. + public abstract PathString Path { get; set; } + + /// + /// Gets or sets the raw query string used to create the query collection in Request.Query. + /// + /// The raw query string. + public abstract QueryString QueryString { get; set; } + + /// + /// Gets the query value collection parsed from Request.QueryString. + /// + /// The query value collection parsed from Request.QueryString. + public abstract IQueryCollection Query { get; set; } + + /// + /// Gets or sets the request protocol (e.g. HTTP/1.1). + /// + /// The request protocol. + public abstract string Protocol { get; set; } + + /// + /// Gets the request headers. + /// + /// The request headers. + public abstract IHeaderDictionary Headers { get; } + + /// + /// Gets the collection of Cookies for this request. + /// + /// The collection of Cookies for this request. + public abstract IRequestCookieCollection Cookies { get; set; } + + /// + /// Gets or sets the Content-Length header. + /// + /// The value of the Content-Length header, if any. + public abstract long? ContentLength { get; set; } + + /// + /// Gets or sets the Content-Type header. + /// + /// The Content-Type header. + public abstract string? ContentType { get; set; } + + /// + /// Gets or sets the request body . + /// + /// The request body . + public abstract Stream Body { get; set; } + + /// + /// Gets the request body . + /// + /// The request body . + public virtual PipeReader BodyReader { get => throw new NotImplementedException(); } + + /// + /// Checks the Content-Type header for form types. + /// + /// true if the Content-Type header represents a form content type; otherwise, false. + public abstract bool HasFormContentType { get; } + + /// + /// Gets or sets the request body as a form. + /// + public abstract IFormCollection Form { get; set; } + + /// + /// Reads the request body if it is a form. + /// + /// + public abstract Task ReadFormAsync(CancellationToken cancellationToken = new CancellationToken()); + + /// + /// Gets the collection of route values for this request. + /// + /// The collection of route values for this request. + public virtual RouteValueDictionary RouteValues { get; set; } = null!; } diff --git a/src/Http/Http.Abstractions/src/HttpResponse.cs b/src/Http/Http.Abstractions/src/HttpResponse.cs index 4fbac26f51..8b2599cf81 100644 --- a/src/Http/Http.Abstractions/src/HttpResponse.cs +++ b/src/Http/Http.Abstractions/src/HttpResponse.cs @@ -7,150 +7,149 @@ using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Represents the outgoing side of an individual HTTP request. +/// +public abstract class HttpResponse { - /// - /// Represents the outgoing side of an individual HTTP request. - /// - public abstract class HttpResponse + private static readonly Func _callbackDelegate = callback => ((Func)callback)(); + private static readonly Func _disposeDelegate = state => { - private static readonly Func _callbackDelegate = callback => ((Func)callback)(); - private static readonly Func _disposeDelegate = state => - { // Prefer async dispose over dispose if (state is IAsyncDisposable asyncDisposable) - { - return asyncDisposable.DisposeAsync().AsTask(); - } - else if (state is IDisposable disposable) - { - disposable.Dispose(); - } - return Task.CompletedTask; - }; - - /// - /// Gets the for this response. - /// - public abstract HttpContext HttpContext { get; } - - /// - /// Gets or sets the HTTP response code. - /// - public abstract int StatusCode { get; set; } - - /// - /// Gets the response headers. - /// - public abstract IHeaderDictionary Headers { get; } - - /// - /// Gets or sets the response body . - /// - public abstract Stream Body { get; set; } - - /// - /// Gets the response body - /// - /// The response body . - public virtual PipeWriter BodyWriter { get => throw new NotImplementedException(); } - - /// - /// Gets or sets the value for the Content-Length response header. - /// - public abstract long? ContentLength { get; set; } - - /// - /// Gets or sets the value for the Content-Type response header. - /// - public abstract string? ContentType { get; set; } - - /// - /// Gets an object that can be used to manage cookies for this response. - /// - public abstract IResponseCookies Cookies { get; } - - /// - /// Gets a value indicating whether response headers have been sent to the client. - /// - public abstract bool HasStarted { get; } - - /// - /// Adds a delegate to be invoked just before response headers will be sent to the client. - /// Callbacks registered here run in reverse order. - /// - /// - /// Callbacks registered here run in reverse order. The last one registered is invoked first. - /// The reverse order is done to replicate the way middleware works, with the inner-most middleware looking at the - /// response first. - /// - /// The delegate to execute. - /// A state object to capture and pass back to the delegate. - public abstract void OnStarting(Func callback, object state); - - /// - /// Adds a delegate to be invoked just before response headers will be sent to the client. - /// Callbacks registered here run in reverse order. - /// - /// - /// Callbacks registered here run in reverse order. The last one registered is invoked first. - /// The reverse order is done to replicate the way middleware works, with the inner-most middleware looking at the - /// response first. - /// - /// The delegate to execute. - public virtual void OnStarting(Func callback) => OnStarting(_callbackDelegate, callback); - - /// - /// Adds a delegate to be invoked after the response has finished being sent to the client. - /// - /// The delegate to invoke. - /// A state object to capture and pass back to the delegate. - public abstract void OnCompleted(Func callback, object state); - - /// - /// Registers an object for disposal by the host once the request has finished processing. - /// - /// The object to be disposed. - public virtual void RegisterForDispose(IDisposable disposable) => OnCompleted(_disposeDelegate, disposable); - - /// - /// Registers an object for asynchronous disposal by the host once the request has finished processing. - /// - /// The object to be disposed asynchronously. - public virtual void RegisterForDisposeAsync(IAsyncDisposable disposable) => OnCompleted(_disposeDelegate, disposable); - - /// - /// Adds a delegate to be invoked after the response has finished being sent to the client. - /// - /// The delegate to invoke. - public virtual void OnCompleted(Func callback) => OnCompleted(_callbackDelegate, callback); - - /// - /// Returns a temporary redirect response (HTTP 302) to the client. - /// - /// The URL to redirect the client to. This must be properly encoded for use in http headers - /// where only ASCII characters are allowed. - public virtual void Redirect(string location) => Redirect(location, permanent: false); - - /// - /// Returns a redirect response (HTTP 301 or HTTP 302) to the client. - /// - /// The URL to redirect the client to. This must be properly encoded for use in http headers - /// where only ASCII characters are allowed. - /// True if the redirect is permanent (301), otherwise false (302). - public abstract void Redirect(string location, bool permanent); - - /// - /// Starts the response by calling OnStarting() and making headers unmodifiable. - /// - /// - public virtual Task StartAsync(CancellationToken cancellationToken = default) { throw new NotImplementedException(); } - - /// - /// Flush any remaining response headers, data, or trailers. - /// This may throw if the response is in an invalid state such as a Content-Length mismatch. - /// - /// - public virtual Task CompleteAsync() { throw new NotImplementedException(); } - } + { + return asyncDisposable.DisposeAsync().AsTask(); + } + else if (state is IDisposable disposable) + { + disposable.Dispose(); + } + return Task.CompletedTask; + }; + + /// + /// Gets the for this response. + /// + public abstract HttpContext HttpContext { get; } + + /// + /// Gets or sets the HTTP response code. + /// + public abstract int StatusCode { get; set; } + + /// + /// Gets the response headers. + /// + public abstract IHeaderDictionary Headers { get; } + + /// + /// Gets or sets the response body . + /// + public abstract Stream Body { get; set; } + + /// + /// Gets the response body + /// + /// The response body . + public virtual PipeWriter BodyWriter { get => throw new NotImplementedException(); } + + /// + /// Gets or sets the value for the Content-Length response header. + /// + public abstract long? ContentLength { get; set; } + + /// + /// Gets or sets the value for the Content-Type response header. + /// + public abstract string? ContentType { get; set; } + + /// + /// Gets an object that can be used to manage cookies for this response. + /// + public abstract IResponseCookies Cookies { get; } + + /// + /// Gets a value indicating whether response headers have been sent to the client. + /// + public abstract bool HasStarted { get; } + + /// + /// Adds a delegate to be invoked just before response headers will be sent to the client. + /// Callbacks registered here run in reverse order. + /// + /// + /// Callbacks registered here run in reverse order. The last one registered is invoked first. + /// The reverse order is done to replicate the way middleware works, with the inner-most middleware looking at the + /// response first. + /// + /// The delegate to execute. + /// A state object to capture and pass back to the delegate. + public abstract void OnStarting(Func callback, object state); + + /// + /// Adds a delegate to be invoked just before response headers will be sent to the client. + /// Callbacks registered here run in reverse order. + /// + /// + /// Callbacks registered here run in reverse order. The last one registered is invoked first. + /// The reverse order is done to replicate the way middleware works, with the inner-most middleware looking at the + /// response first. + /// + /// The delegate to execute. + public virtual void OnStarting(Func callback) => OnStarting(_callbackDelegate, callback); + + /// + /// Adds a delegate to be invoked after the response has finished being sent to the client. + /// + /// The delegate to invoke. + /// A state object to capture and pass back to the delegate. + public abstract void OnCompleted(Func callback, object state); + + /// + /// Registers an object for disposal by the host once the request has finished processing. + /// + /// The object to be disposed. + public virtual void RegisterForDispose(IDisposable disposable) => OnCompleted(_disposeDelegate, disposable); + + /// + /// Registers an object for asynchronous disposal by the host once the request has finished processing. + /// + /// The object to be disposed asynchronously. + public virtual void RegisterForDisposeAsync(IAsyncDisposable disposable) => OnCompleted(_disposeDelegate, disposable); + + /// + /// Adds a delegate to be invoked after the response has finished being sent to the client. + /// + /// The delegate to invoke. + public virtual void OnCompleted(Func callback) => OnCompleted(_callbackDelegate, callback); + + /// + /// Returns a temporary redirect response (HTTP 302) to the client. + /// + /// The URL to redirect the client to. This must be properly encoded for use in http headers + /// where only ASCII characters are allowed. + public virtual void Redirect(string location) => Redirect(location, permanent: false); + + /// + /// Returns a redirect response (HTTP 301 or HTTP 302) to the client. + /// + /// The URL to redirect the client to. This must be properly encoded for use in http headers + /// where only ASCII characters are allowed. + /// True if the redirect is permanent (301), otherwise false (302). + public abstract void Redirect(string location, bool permanent); + + /// + /// Starts the response by calling OnStarting() and making headers unmodifiable. + /// + /// + public virtual Task StartAsync(CancellationToken cancellationToken = default) { throw new NotImplementedException(); } + + /// + /// Flush any remaining response headers, data, or trailers. + /// This may throw if the response is in an invalid state such as a Content-Length mismatch. + /// + /// + public virtual Task CompleteAsync() { throw new NotImplementedException(); } } diff --git a/src/Http/Http.Abstractions/src/IApplicationBuilder.cs b/src/Http/Http.Abstractions/src/IApplicationBuilder.cs index 0b57f15720..6b7f2a3020 100644 --- a/src/Http/Http.Abstractions/src/IApplicationBuilder.cs +++ b/src/Http/Http.Abstractions/src/IApplicationBuilder.cs @@ -6,46 +6,45 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Defines a class that provides the mechanisms to configure an application's request pipeline. +/// +public interface IApplicationBuilder { /// - /// Defines a class that provides the mechanisms to configure an application's request pipeline. + /// Gets or sets the that provides access to the application's service container. + /// + IServiceProvider ApplicationServices { get; set; } + + /// + /// Gets the set of HTTP features the application's server provides. + /// + IFeatureCollection ServerFeatures { get; } + + /// + /// Gets a key/value collection that can be used to share data between middleware. + /// + IDictionary Properties { get; } + + /// + /// Adds a middleware delegate to the application's request pipeline. + /// + /// The middleware delegate. + /// The . + IApplicationBuilder Use(Func middleware); + + /// + /// Creates a new that shares the of this + /// . + /// + /// The new . + IApplicationBuilder New(); + + /// + /// Builds the delegate used by this application to process HTTP requests. /// - public interface IApplicationBuilder - { - /// - /// Gets or sets the that provides access to the application's service container. - /// - IServiceProvider ApplicationServices { get; set; } - - /// - /// Gets the set of HTTP features the application's server provides. - /// - IFeatureCollection ServerFeatures { get; } - - /// - /// Gets a key/value collection that can be used to share data between middleware. - /// - IDictionary Properties { get; } - - /// - /// Adds a middleware delegate to the application's request pipeline. - /// - /// The middleware delegate. - /// The . - IApplicationBuilder Use(Func middleware); - - /// - /// Creates a new that shares the of this - /// . - /// - /// The new . - IApplicationBuilder New(); - - /// - /// Builds the delegate used by this application to process HTTP requests. - /// - /// The request handling delegate. - RequestDelegate Build(); - } + /// The request handling delegate. + RequestDelegate Build(); } diff --git a/src/Http/Http.Abstractions/src/ICorsMetadata.cs b/src/Http/Http.Abstractions/src/ICorsMetadata.cs index 727561663f..cc6ec493cb 100644 --- a/src/Http/Http.Abstractions/src/ICorsMetadata.cs +++ b/src/Http/Http.Abstractions/src/ICorsMetadata.cs @@ -1,12 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Cors.Infrastructure +namespace Microsoft.AspNetCore.Cors.Infrastructure; + +/// +/// A marker interface which can be used to identify CORS metadata. +/// +public interface ICorsMetadata { - /// - /// A marker interface which can be used to identify CORS metadata. - /// - public interface ICorsMetadata - { - } } diff --git a/src/Http/Http.Abstractions/src/IHttpContextAccessor.cs b/src/Http/Http.Abstractions/src/IHttpContextAccessor.cs index 171146ccaf..33e6e23330 100644 --- a/src/Http/Http.Abstractions/src/IHttpContextAccessor.cs +++ b/src/Http/Http.Abstractions/src/IHttpContextAccessor.cs @@ -1,20 +1,19 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Provides access to the current , if one is available. +/// +/// +/// This interface should be used with caution. It relies on which can have a negative performance impact on async calls. +/// It also creates a dependency on "ambient state" which can make testing more difficult. +/// +public interface IHttpContextAccessor { /// - /// Provides access to the current , if one is available. + /// Gets or sets the current . Returns if there is no active . /// - /// - /// This interface should be used with caution. It relies on which can have a negative performance impact on async calls. - /// It also creates a dependency on "ambient state" which can make testing more difficult. - /// - public interface IHttpContextAccessor - { - /// - /// Gets or sets the current . Returns if there is no active . - /// - HttpContext? HttpContext { get; set; } - } + HttpContext? HttpContext { get; set; } } diff --git a/src/Http/Http.Abstractions/src/IHttpContextFactory.cs b/src/Http/Http.Abstractions/src/IHttpContextFactory.cs index 4bc4ab7a17..625883d513 100644 --- a/src/Http/Http.Abstractions/src/IHttpContextFactory.cs +++ b/src/Http/Http.Abstractions/src/IHttpContextFactory.cs @@ -3,24 +3,23 @@ using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Provides methods to create and dispose of instances. +/// +public interface IHttpContextFactory { /// - /// Provides methods to create and dispose of instances. + /// Creates an instance for the specified set of HTTP features. /// - public interface IHttpContextFactory - { - /// - /// Creates an instance for the specified set of HTTP features. - /// - /// The collection of HTTP features to set on the created instance. - /// The instance. - HttpContext Create(IFeatureCollection featureCollection); + /// The collection of HTTP features to set on the created instance. + /// The instance. + HttpContext Create(IFeatureCollection featureCollection); - /// - /// Releases resources held by the . - /// - /// The to dispose. - void Dispose(HttpContext httpContext); - } + /// + /// Releases resources held by the . + /// + /// The to dispose. + void Dispose(HttpContext httpContext); } diff --git a/src/Http/Http.Abstractions/src/IMiddleware.cs b/src/Http/Http.Abstractions/src/IMiddleware.cs index 9320c5e782..f5c51ff937 100644 --- a/src/Http/Http.Abstractions/src/IMiddleware.cs +++ b/src/Http/Http.Abstractions/src/IMiddleware.cs @@ -3,19 +3,18 @@ using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Defines middleware that can be added to the application's request pipeline. +/// +public interface IMiddleware { /// - /// Defines middleware that can be added to the application's request pipeline. + /// Request handling method. /// - public interface IMiddleware - { - /// - /// Request handling method. - /// - /// The for the current request. - /// The delegate representing the remaining middleware in the request pipeline. - /// A that represents the execution of this middleware. - Task InvokeAsync(HttpContext context, RequestDelegate next); - } + /// The for the current request. + /// The delegate representing the remaining middleware in the request pipeline. + /// A that represents the execution of this middleware. + Task InvokeAsync(HttpContext context, RequestDelegate next); } diff --git a/src/Http/Http.Abstractions/src/IMiddlewareFactory.cs b/src/Http/Http.Abstractions/src/IMiddlewareFactory.cs index 82a0dc0203..0b7c7b1710 100644 --- a/src/Http/Http.Abstractions/src/IMiddlewareFactory.cs +++ b/src/Http/Http.Abstractions/src/IMiddlewareFactory.cs @@ -3,24 +3,23 @@ using System; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Provides methods to create middleware. +/// +public interface IMiddlewareFactory { /// - /// Provides methods to create middleware. + /// Creates a middleware instance for each request. /// - public interface IMiddlewareFactory - { - /// - /// Creates a middleware instance for each request. - /// - /// The concrete of the . - /// The instance. - IMiddleware? Create(Type middlewareType); + /// The concrete of the . + /// The instance. + IMiddleware? Create(Type middlewareType); - /// - /// Releases a instance at the end of each request. - /// - /// The instance to release. - void Release(IMiddleware middleware); - } + /// + /// Releases a instance at the end of each request. + /// + /// The instance to release. + void Release(IMiddleware middleware); } diff --git a/src/Http/Http.Abstractions/src/IResult.cs b/src/Http/Http.Abstractions/src/IResult.cs index ce83c591b4..8e5b5bf0fe 100644 --- a/src/Http/Http.Abstractions/src/IResult.cs +++ b/src/Http/Http.Abstractions/src/IResult.cs @@ -3,18 +3,17 @@ using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Defines a contract that represents the result of an HTTP endpoint. +/// +public interface IResult { /// - /// Defines a contract that represents the result of an HTTP endpoint. + /// Write an HTTP response reflecting the result. /// - public interface IResult - { - /// - /// Write an HTTP response reflecting the result. - /// - /// The for the current request. - /// A task that represents the asynchronous execute operation. - Task ExecuteAsync(HttpContext httpContext); - } + /// The for the current request. + /// A task that represents the asynchronous execute operation. + Task ExecuteAsync(HttpContext httpContext); } diff --git a/src/Http/Http.Abstractions/src/Internal/HeaderSegment.cs b/src/Http/Http.Abstractions/src/Internal/HeaderSegment.cs index b81d26a620..cbd82c9440 100644 --- a/src/Http/Http.Abstractions/src/Internal/HeaderSegment.cs +++ b/src/Http/Http.Abstractions/src/Internal/HeaderSegment.cs @@ -4,63 +4,62 @@ using System; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +internal readonly struct HeaderSegment : IEquatable { - internal readonly struct HeaderSegment : IEquatable + private readonly StringSegment _formatting; + private readonly StringSegment _data; + + // + // Initializes a new instance of the structure. + // + public HeaderSegment(StringSegment formatting, StringSegment data) { - private readonly StringSegment _formatting; - private readonly StringSegment _data; + _formatting = formatting; + _data = data; + } - // - // Initializes a new instance of the structure. - // - public HeaderSegment(StringSegment formatting, StringSegment data) - { - _formatting = formatting; - _data = data; - } + public StringSegment Formatting + { + get { return _formatting; } + } - public StringSegment Formatting - { - get { return _formatting; } - } + public StringSegment Data + { + get { return _data; } + } - public StringSegment Data - { - get { return _data; } - } + public bool Equals(HeaderSegment other) + { + return _formatting.Equals(other._formatting) && _data.Equals(other._data); + } - public bool Equals(HeaderSegment other) + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - return _formatting.Equals(other._formatting) && _data.Equals(other._data); + return false; } - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } - - return obj is HeaderSegment && Equals((HeaderSegment)obj); - } + return obj is HeaderSegment && Equals((HeaderSegment)obj); + } - public override int GetHashCode() + public override int GetHashCode() + { + unchecked { - unchecked - { - return (_formatting.GetHashCode() * 397) ^ _data.GetHashCode(); - } + return (_formatting.GetHashCode() * 397) ^ _data.GetHashCode(); } + } - public static bool operator ==(HeaderSegment left, HeaderSegment right) - { - return left.Equals(right); - } + public static bool operator ==(HeaderSegment left, HeaderSegment right) + { + return left.Equals(right); + } - public static bool operator !=(HeaderSegment left, HeaderSegment right) - { - return !left.Equals(right); - } + public static bool operator !=(HeaderSegment left, HeaderSegment right) + { + return !left.Equals(right); } } diff --git a/src/Http/Http.Abstractions/src/Internal/HeaderSegmentCollection.cs b/src/Http/Http.Abstractions/src/Internal/HeaderSegmentCollection.cs index 433d7d35e2..114e3384a7 100644 --- a/src/Http/Http.Abstractions/src/Internal/HeaderSegmentCollection.cs +++ b/src/Http/Http.Abstractions/src/Internal/HeaderSegmentCollection.cs @@ -6,297 +6,295 @@ using System.Collections; using System.Collections.Generic; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +internal readonly struct HeaderSegmentCollection : IEnumerable, IEquatable { - internal readonly struct HeaderSegmentCollection : IEnumerable, IEquatable + private readonly StringValues _headers; + + public HeaderSegmentCollection(StringValues headers) { - private readonly StringValues _headers; + _headers = headers; + } - public HeaderSegmentCollection(StringValues headers) - { - _headers = headers; - } + public bool Equals(HeaderSegmentCollection other) + { + return StringValues.Equals(_headers, other._headers); + } - public bool Equals(HeaderSegmentCollection other) + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - return StringValues.Equals(_headers, other._headers); + return false; } - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) - { - return false; - } + return obj is HeaderSegmentCollection && Equals((HeaderSegmentCollection)obj); + } - return obj is HeaderSegmentCollection && Equals((HeaderSegmentCollection)obj); - } + public override int GetHashCode() + { + return (!StringValues.IsNullOrEmpty(_headers) ? _headers.GetHashCode() : 0); + } - public override int GetHashCode() + public static bool operator ==(HeaderSegmentCollection left, HeaderSegmentCollection right) + { + return left.Equals(right); + } + + public static bool operator !=(HeaderSegmentCollection left, HeaderSegmentCollection right) + { + return !left.Equals(right); + } + + public Enumerator GetEnumerator() + { + return new Enumerator(_headers); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator + { + private readonly StringValues _headers; + private int _index; + + private string _header; + private int _headerLength; + private int _offset; + + private int _leadingStart; + private int _leadingEnd; + private int _valueStart; + private int _valueEnd; + private int _trailingStart; + + private Mode _mode; + + public Enumerator(StringValues headers) { - return (!StringValues.IsNullOrEmpty(_headers) ? _headers.GetHashCode() : 0); + _headers = headers; + _header = string.Empty; + _headerLength = -1; + _index = -1; + _offset = -1; + _leadingStart = -1; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; + _mode = Mode.Leading; } - public static bool operator ==(HeaderSegmentCollection left, HeaderSegmentCollection right) + private enum Mode { - return left.Equals(right); + Leading, + Value, + ValueQuoted, + Trailing, + Produce, } - public static bool operator !=(HeaderSegmentCollection left, HeaderSegmentCollection right) + private enum Attr { - return !left.Equals(right); + Value, + Quote, + Delimiter, + Whitespace } - public Enumerator GetEnumerator() + public HeaderSegment Current { - return new Enumerator(_headers); + get + { + return new HeaderSegment( + new StringSegment(_header, _leadingStart, _leadingEnd - _leadingStart), + new StringSegment(_header, _valueStart, _valueEnd - _valueStart)); + } } - IEnumerator IEnumerable.GetEnumerator() + object IEnumerator.Current { - return GetEnumerator(); + get { return Current; } } - IEnumerator IEnumerable.GetEnumerator() + public void Dispose() { - return GetEnumerator(); } - public struct Enumerator : IEnumerator + public bool MoveNext() { - private readonly StringValues _headers; - private int _index; - - private string _header; - private int _headerLength; - private int _offset; - - private int _leadingStart; - private int _leadingEnd; - private int _valueStart; - private int _valueEnd; - private int _trailingStart; - - private Mode _mode; - - public Enumerator(StringValues headers) - { - _headers = headers; - _header = string.Empty; - _headerLength = -1; - _index = -1; - _offset = -1; - _leadingStart = -1; - _leadingEnd = -1; - _valueStart = -1; - _valueEnd = -1; - _trailingStart = -1; - _mode = Mode.Leading; - } - - private enum Mode - { - Leading, - Value, - ValueQuoted, - Trailing, - Produce, - } - - private enum Attr - { - Value, - Quote, - Delimiter, - Whitespace - } - - public HeaderSegment Current + while (true) { - get + if (_mode == Mode.Produce) { - return new HeaderSegment( - new StringSegment(_header, _leadingStart, _leadingEnd - _leadingStart), - new StringSegment(_header, _valueStart, _valueEnd - _valueStart)); - } - } - - object IEnumerator.Current - { - get { return Current; } - } + _leadingStart = _trailingStart; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; - public void Dispose() - { - } - - public bool MoveNext() - { - while (true) - { - if (_mode == Mode.Produce) + if (_offset == _headerLength && + _leadingStart != -1 && + _leadingStart != _offset) { - _leadingStart = _trailingStart; - _leadingEnd = -1; - _valueStart = -1; - _valueEnd = -1; - _trailingStart = -1; - - if (_offset == _headerLength && - _leadingStart != -1 && - _leadingStart != _offset) - { - // Also produce trailing whitespace - _leadingEnd = _offset; - return true; - } - _mode = Mode.Leading; + // Also produce trailing whitespace + _leadingEnd = _offset; + return true; } + _mode = Mode.Leading; + } - // if end of a string - if (_offset == _headerLength) + // if end of a string + if (_offset == _headerLength) + { + ++_index; + _offset = -1; + _leadingStart = 0; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; + + // if that was the last string + if (_index == _headers.Count) { - ++_index; - _offset = -1; - _leadingStart = 0; - _leadingEnd = -1; - _valueStart = -1; - _valueEnd = -1; - _trailingStart = -1; + // no more move nexts + return false; + } - // if that was the last string - if (_index == _headers.Count) - { - // no more move nexts - return false; - } + // grab the next string + _header = _headers[_index] ?? string.Empty; + _headerLength = _header.Length; + } + while (true) + { + ++_offset; + char ch = _offset == _headerLength ? (char)0 : _header[_offset]; + // todo - array of attrs + Attr attr = char.IsWhiteSpace(ch) ? Attr.Whitespace : ch == '\"' ? Attr.Quote : (ch == ',' || ch == (char)0) ? Attr.Delimiter : Attr.Value; - // grab the next string - _header = _headers[_index] ?? string.Empty; - _headerLength = _header.Length; - } - while (true) + switch (_mode) { - ++_offset; - char ch = _offset == _headerLength ? (char)0 : _header[_offset]; - // todo - array of attrs - Attr attr = char.IsWhiteSpace(ch) ? Attr.Whitespace : ch == '\"' ? Attr.Quote : (ch == ',' || ch == (char)0) ? Attr.Delimiter : Attr.Value; - - switch (_mode) - { - case Mode.Leading: - switch (attr) - { - case Attr.Delimiter: - _valueStart = _valueStart == -1 ? _offset : _valueStart; - _valueEnd = _valueEnd == -1 ? _offset : _valueEnd; - _trailingStart = _trailingStart == -1 ? _offset : _trailingStart; - _leadingEnd = _offset; - _mode = Mode.Produce; - break; - case Attr.Quote: - _leadingEnd = _offset; - _valueStart = _offset; - _mode = Mode.ValueQuoted; - break; - case Attr.Value: - _leadingEnd = _offset; - _valueStart = _offset; - _mode = Mode.Value; - break; - case Attr.Whitespace: - // more - break; - } - break; - case Mode.Value: - switch (attr) - { - case Attr.Quote: - _mode = Mode.ValueQuoted; - break; - case Attr.Delimiter: + case Mode.Leading: + switch (attr) + { + case Attr.Delimiter: + _valueStart = _valueStart == -1 ? _offset : _valueStart; + _valueEnd = _valueEnd == -1 ? _offset : _valueEnd; + _trailingStart = _trailingStart == -1 ? _offset : _trailingStart; + _leadingEnd = _offset; + _mode = Mode.Produce; + break; + case Attr.Quote: + _leadingEnd = _offset; + _valueStart = _offset; + _mode = Mode.ValueQuoted; + break; + case Attr.Value: + _leadingEnd = _offset; + _valueStart = _offset; + _mode = Mode.Value; + break; + case Attr.Whitespace: + // more + break; + } + break; + case Mode.Value: + switch (attr) + { + case Attr.Quote: + _mode = Mode.ValueQuoted; + break; + case Attr.Delimiter: + _valueEnd = _offset; + _trailingStart = _offset; + _mode = Mode.Produce; + break; + case Attr.Value: + // more + break; + case Attr.Whitespace: + _valueEnd = _offset; + _trailingStart = _offset; + _mode = Mode.Trailing; + break; + } + break; + case Mode.ValueQuoted: + switch (attr) + { + case Attr.Quote: + _mode = Mode.Value; + break; + case Attr.Delimiter: + if (ch == (char)0) + { _valueEnd = _offset; _trailingStart = _offset; _mode = Mode.Produce; - break; - case Attr.Value: - // more - break; - case Attr.Whitespace: + } + break; + case Attr.Value: + case Attr.Whitespace: + // more + break; + } + break; + case Mode.Trailing: + switch (attr) + { + case Attr.Delimiter: + if (ch == (char)0) + { _valueEnd = _offset; _trailingStart = _offset; - _mode = Mode.Trailing; - break; - } - break; - case Mode.ValueQuoted: - switch (attr) - { - case Attr.Quote: - _mode = Mode.Value; - break; - case Attr.Delimiter: - if (ch == (char)0) - { - _valueEnd = _offset; - _trailingStart = _offset; - _mode = Mode.Produce; - } - break; - case Attr.Value: - case Attr.Whitespace: - // more - break; - } - break; - case Mode.Trailing: - switch (attr) - { - case Attr.Delimiter: - if (ch == (char)0) - { - _valueEnd = _offset; - _trailingStart = _offset; - } - _mode = Mode.Produce; - break; - case Attr.Quote: - // back into value - _trailingStart = -1; - _valueEnd = -1; - _mode = Mode.ValueQuoted; - break; - case Attr.Value: - // back into value - _trailingStart = -1; - _valueEnd = -1; - _mode = Mode.Value; - break; - case Attr.Whitespace: - // more - break; - } - break; - } - if (_mode == Mode.Produce) - { - return true; - } + } + _mode = Mode.Produce; + break; + case Attr.Quote: + // back into value + _trailingStart = -1; + _valueEnd = -1; + _mode = Mode.ValueQuoted; + break; + case Attr.Value: + // back into value + _trailingStart = -1; + _valueEnd = -1; + _mode = Mode.Value; + break; + case Attr.Whitespace: + // more + break; + } + break; + } + if (_mode == Mode.Produce) + { + return true; } } } + } - public void Reset() - { - _index = 0; - _offset = 0; - _leadingStart = 0; - _leadingEnd = 0; - _valueStart = 0; - _valueEnd = 0; - } + public void Reset() + { + _index = 0; + _offset = 0; + _leadingStart = 0; + _leadingEnd = 0; + _valueStart = 0; + _valueEnd = 0; } } - } diff --git a/src/Http/Http.Abstractions/src/Internal/HostStringHelper.cs b/src/Http/Http.Abstractions/src/Internal/HostStringHelper.cs index 55e1cfee72..4148b939db 100644 --- a/src/Http/Http.Abstractions/src/Internal/HostStringHelper.cs +++ b/src/Http/Http.Abstractions/src/Internal/HostStringHelper.cs @@ -1,15 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +internal class HostStringHelper { - internal class HostStringHelper - { - // Allowed Characters: - // A-Z, a-z, 0-9, ., - // -, %, [, ], : - // Above for IPV6 - private static readonly bool[] SafeHostStringChars = { + // Allowed Characters: + // A-Z, a-z, 0-9, ., + // -, %, [, ], : + // Above for IPV6 + private static readonly bool[] SafeHostStringChars = { false, false, false, false, false, false, false, false, // 0x00 - 0x07 false, false, false, false, false, false, false, false, // 0x08 - 0x0F false, false, false, false, false, false, false, false, // 0x10 - 0x17 @@ -28,9 +28,8 @@ namespace Microsoft.AspNetCore.Http true, true, true, false, false, false, false, false, // 0x78 - 0x7F }; - public static bool IsSafeHostStringChar(char c) - { - return c < SafeHostStringChars.Length && SafeHostStringChars[c]; - } + public static bool IsSafeHostStringChar(char c) + { + return c < SafeHostStringChars.Length && SafeHostStringChars[c]; } } diff --git a/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs b/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs index 150a460edf..361bf2a51c 100644 --- a/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs +++ b/src/Http/Http.Abstractions/src/Internal/ParsingHelpers.cs @@ -5,160 +5,159 @@ using System; using System.Linq; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +internal static class ParsingHelpers { - internal static class ParsingHelpers + public static StringValues GetHeader(IHeaderDictionary headers, string key) { - public static StringValues GetHeader(IHeaderDictionary headers, string key) - { - StringValues value; - return headers.TryGetValue(key, out value) ? value : StringValues.Empty; - } + StringValues value; + return headers.TryGetValue(key, out value) ? value : StringValues.Empty; + } - public static StringValues GetHeaderSplit(IHeaderDictionary headers, string key) - { - var values = GetHeaderUnmodified(headers, key); + public static StringValues GetHeaderSplit(IHeaderDictionary headers, string key) + { + var values = GetHeaderUnmodified(headers, key); - StringValues result = default; + StringValues result = default; - foreach (var segment in new HeaderSegmentCollection(values)) + foreach (var segment in new HeaderSegmentCollection(values)) + { + if (!StringSegment.IsNullOrEmpty(segment.Data)) { - if (!StringSegment.IsNullOrEmpty(segment.Data)) + var value = DeQuote(segment.Data.Value); + if (!string.IsNullOrEmpty(value)) { - var value = DeQuote(segment.Data.Value); - if (!string.IsNullOrEmpty(value)) - { - result = StringValues.Concat(in result, value); - } + result = StringValues.Concat(in result, value); } } - - return result; } - public static StringValues GetHeaderUnmodified(IHeaderDictionary headers, string key) - { - if (headers == null) - { - throw new ArgumentNullException(nameof(headers)); - } + return result; + } - StringValues values; - return headers.TryGetValue(key, out values) ? values : StringValues.Empty; + public static StringValues GetHeaderUnmodified(IHeaderDictionary headers, string key) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); } - public static void SetHeaderJoined(IHeaderDictionary headers, string key, StringValues value) - { - if (headers == null) - { - throw new ArgumentNullException(nameof(headers)); - } + StringValues values; + return headers.TryGetValue(key, out values) ? values : StringValues.Empty; + } - if (string.IsNullOrEmpty(key)) - { - throw new ArgumentNullException(nameof(key)); - } - if (StringValues.IsNullOrEmpty(value)) - { - headers.Remove(key); - } - else - { - headers[key] = string.Join(",", value.Select((s) => QuoteIfNeeded(s))); - } + public static void SetHeaderJoined(IHeaderDictionary headers, string key, StringValues value) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); } - // Quote items that contain commas and are not already quoted. - private static string? QuoteIfNeeded(string? value) + if (string.IsNullOrEmpty(key)) { - if (!string.IsNullOrEmpty(value) && - value.Contains(',') && - (value[0] != '"' || value[value.Length - 1] != '"')) - { - return $"\"{value}\""; - } - return value; + throw new ArgumentNullException(nameof(key)); } - - private static string? DeQuote(string? value) + if (StringValues.IsNullOrEmpty(value)) { - if (!string.IsNullOrEmpty(value) && - (value.Length > 1 && value[0] == '"' && value[value.Length - 1] == '"')) - { - value = value.Substring(1, value.Length - 2); - } + headers.Remove(key); + } + else + { + headers[key] = string.Join(",", value.Select((s) => QuoteIfNeeded(s))); + } + } - return value; + // Quote items that contain commas and are not already quoted. + private static string? QuoteIfNeeded(string? value) + { + if (!string.IsNullOrEmpty(value) && + value.Contains(',') && + (value[0] != '"' || value[value.Length - 1] != '"')) + { + return $"\"{value}\""; } + return value; + } - public static void SetHeaderUnmodified(IHeaderDictionary headers, string key, StringValues? values) + private static string? DeQuote(string? value) + { + if (!string.IsNullOrEmpty(value) && + (value.Length > 1 && value[0] == '"' && value[value.Length - 1] == '"')) { - if (headers == null) - { - throw new ArgumentNullException(nameof(headers)); - } + value = value.Substring(1, value.Length - 2); + } - if (string.IsNullOrEmpty(key)) - { - throw new ArgumentNullException(nameof(key)); - } - if (!values.HasValue || StringValues.IsNullOrEmpty(values.GetValueOrDefault())) - { - headers.Remove(key); - } - else - { - headers[key] = values.GetValueOrDefault(); - } + return value; + } + + public static void SetHeaderUnmodified(IHeaderDictionary headers, string key, StringValues? values) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); } - public static void AppendHeaderJoined(IHeaderDictionary headers, string key, params string[] values) + if (string.IsNullOrEmpty(key)) { - if (headers == null) - { - throw new ArgumentNullException(nameof(headers)); - } + throw new ArgumentNullException(nameof(key)); + } + if (!values.HasValue || StringValues.IsNullOrEmpty(values.GetValueOrDefault())) + { + headers.Remove(key); + } + else + { + headers[key] = values.GetValueOrDefault(); + } + } - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } + public static void AppendHeaderJoined(IHeaderDictionary headers, string key, params string[] values) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } - if (values == null || values.Length == 0) - { - return; - } + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } - string? existing = GetHeader(headers, key); - if (existing == null) - { - SetHeaderJoined(headers, key, values); - } - else - { - headers[key] = existing + "," + string.Join(",", values.Select(value => QuoteIfNeeded(value))); - } + if (values == null || values.Length == 0) + { + return; } - public static void AppendHeaderUnmodified(IHeaderDictionary headers, string key, StringValues values) + string? existing = GetHeader(headers, key); + if (existing == null) { - if (headers == null) - { - throw new ArgumentNullException(nameof(headers)); - } + SetHeaderJoined(headers, key, values); + } + else + { + headers[key] = existing + "," + string.Join(",", values.Select(value => QuoteIfNeeded(value))); + } + } - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } + public static void AppendHeaderUnmodified(IHeaderDictionary headers, string key, StringValues values) + { + if (headers == null) + { + throw new ArgumentNullException(nameof(headers)); + } - if (values.Count == 0) - { - return; - } + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } - var existing = GetHeaderUnmodified(headers, key); - SetHeaderUnmodified(headers, key, StringValues.Concat(existing, values)); + if (values.Count == 0) + { + return; } + + var existing = GetHeaderUnmodified(headers, key); + SetHeaderUnmodified(headers, key, StringValues.Concat(existing, values)); } } diff --git a/src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs b/src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs index 02fca45dda..7ef353255b 100644 --- a/src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs +++ b/src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs @@ -4,64 +4,63 @@ using System.Diagnostics; using System.Runtime.CompilerServices; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +internal static class PathStringHelper { - internal static class PathStringHelper - { - // uint[] bits uses 1 cache line (Array info + 16 bytes) - // bool[] would use 3 cache lines (Array info + 128 bytes) - // So we use 128 bits rather than 128 bytes/bools - private static readonly uint[] ValidPathChars = { + // uint[] bits uses 1 cache line (Array info + 16 bytes) + // bool[] would use 3 cache lines (Array info + 128 bytes) + // So we use 128 bits rather than 128 bytes/bools + private static readonly uint[] ValidPathChars = { 0b_0000_0000__0000_0000__0000_0000__0000_0000, // 0x00 - 0x1F 0b_0010_1111__1111_1111__1111_1111__1101_0010, // 0x20 - 0x3F 0b_1000_0111__1111_1111__1111_1111__1111_1111, // 0x40 - 0x5F 0b_0100_0111__1111_1111__1111_1111__1111_1110, // 0x60 - 0x7F }; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsValidPathChar(char c) - { - // Use local array and uint .Length compare to elide the bounds check on array access - var validChars = ValidPathChars; - var i = (int)c; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsValidPathChar(char c) + { + // Use local array and uint .Length compare to elide the bounds check on array access + var validChars = ValidPathChars; + var i = (int)c; - // Array is in chunks of 32 bits, so get offset by dividing by 32 - var offset = i >> 5; // i / 32; - // Significant bit position is the remainder of the above calc; i % 32 => i & 31 - var significantBit = 1u << (i & 31); + // Array is in chunks of 32 bits, so get offset by dividing by 32 + var offset = i >> 5; // i / 32; + // Significant bit position is the remainder of the above calc; i % 32 => i & 31 + var significantBit = 1u << (i & 31); - // Check offset in bounds and check if significant bit set - return (uint)offset < (uint)validChars.Length && - ((validChars[offset] & significantBit) != 0); - } + // Check offset in bounds and check if significant bit set + return (uint)offset < (uint)validChars.Length && + ((validChars[offset] & significantBit) != 0); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsPercentEncodedChar(string str, int index) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsPercentEncodedChar(string str, int index) + { + var len = (uint)str.Length; + if (str[index] == '%' && index < len - 2) { - var len = (uint)str.Length; - if (str[index] == '%' && index < len - 2) - { - return AreFollowingTwoCharsHex(str, index); - } - - return false; + return AreFollowingTwoCharsHex(str, index); } - [MethodImpl(MethodImplOptions.NoInlining)] - private static bool AreFollowingTwoCharsHex(string str, int index) - { - Debug.Assert(index < str.Length - 2); + return false; + } - var c1 = str[index + 1]; - var c2 = str[index + 2]; - return IsHexadecimalChar(c1) && IsHexadecimalChar(c2); - } + [MethodImpl(MethodImplOptions.NoInlining)] + private static bool AreFollowingTwoCharsHex(string str, int index) + { + Debug.Assert(index < str.Length - 2); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsHexadecimalChar(char c) - { - // Between 0 - 9 or uppercased between A - F - return (uint)(c - '0') <= 9 || (uint)((c & ~0x20) - 'A') <= ('F' - 'A'); - } + var c1 = str[index + 1]; + var c2 = str[index + 2]; + return IsHexadecimalChar(c1) && IsHexadecimalChar(c2); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsHexadecimalChar(char c) + { + // Between 0 - 9 or uppercased between A - F + return (uint)(c - '0') <= 9 || (uint)((c & ~0x20) - 'A') <= ('F' - 'A'); } } diff --git a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs index ce20bb9fdc..482cabf76e 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IAcceptsMetadata.cs @@ -4,27 +4,26 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.Http.Metadata +namespace Microsoft.AspNetCore.Http.Metadata; + +/// +/// Interface for accepting request media types. +/// +public interface IAcceptsMetadata { /// - /// Interface for accepting request media types. + /// Gets a list of the allowed request content types. + /// If the incoming request does not have a Content-Type with one of these values, the request will be rejected with a 415 response. /// - public interface IAcceptsMetadata - { - /// - /// Gets a list of the allowed request content types. - /// If the incoming request does not have a Content-Type with one of these values, the request will be rejected with a 415 response. - /// - IReadOnlyList ContentTypes { get; } + IReadOnlyList ContentTypes { get; } - /// - /// Gets the type being read from the request. - /// - Type? RequestType { get; } + /// + /// Gets the type being read from the request. + /// + Type? RequestType { get; } - /// - /// Gets a value that determines if the request body is optional. - /// - bool IsOptional { get; } - } + /// + /// Gets a value that determines if the request body is optional. + /// + bool IsOptional { get; } } diff --git a/src/Http/Http.Abstractions/src/Metadata/IFromBodyMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IFromBodyMetadata.cs index 8654c25d94..70b92a8926 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IFromBodyMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IFromBodyMetadata.cs @@ -1,16 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Metadata +namespace Microsoft.AspNetCore.Http.Metadata; + +/// +/// Interface marking attributes that specify a parameter should be bound using the request body. +/// +public interface IFromBodyMetadata { /// - /// Interface marking attributes that specify a parameter should be bound using the request body. + /// Gets whether empty input should be rejected or treated as valid. /// - public interface IFromBodyMetadata - { - /// - /// Gets whether empty input should be rejected or treated as valid. - /// - bool AllowEmpty => false; - } + bool AllowEmpty => false; } diff --git a/src/Http/Http.Abstractions/src/Metadata/IFromHeaderMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IFromHeaderMetadata.cs index 2efde28809..21045907cf 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IFromHeaderMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IFromHeaderMetadata.cs @@ -1,16 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Metadata +namespace Microsoft.AspNetCore.Http.Metadata; + +/// +/// Interface marking attributes that specify a parameter should be bound using the request headers. +/// +public interface IFromHeaderMetadata { /// - /// Interface marking attributes that specify a parameter should be bound using the request headers. + /// The request header name. /// - public interface IFromHeaderMetadata - { - /// - /// The request header name. - /// - string? Name { get; } - } + string? Name { get; } } diff --git a/src/Http/Http.Abstractions/src/Metadata/IFromQueryMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IFromQueryMetadata.cs index 0afaa0b746..df9ce62040 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IFromQueryMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IFromQueryMetadata.cs @@ -1,16 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Metadata +namespace Microsoft.AspNetCore.Http.Metadata; + +/// +/// Interface marking attributes that specify a parameter should be bound using the request query string. +/// +public interface IFromQueryMetadata { /// - /// Interface marking attributes that specify a parameter should be bound using the request query string. + /// The name of the query string field. /// - public interface IFromQueryMetadata - { - /// - /// The name of the query string field. - /// - string? Name { get; } - } + string? Name { get; } } diff --git a/src/Http/Http.Abstractions/src/Metadata/IFromRouteMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IFromRouteMetadata.cs index 4f3b45ba24..ab53cbd77c 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IFromRouteMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IFromRouteMetadata.cs @@ -1,16 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Metadata +namespace Microsoft.AspNetCore.Http.Metadata; + +/// +/// Interface marking attributes that specify a parameter should be bound using route-data from the current request. +/// +public interface IFromRouteMetadata { /// - /// Interface marking attributes that specify a parameter should be bound using route-data from the current request. + /// The name. /// - public interface IFromRouteMetadata - { - /// - /// The name. - /// - string? Name { get; } - } + string? Name { get; } } diff --git a/src/Http/Http.Abstractions/src/Metadata/IFromServiceMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IFromServiceMetadata.cs index 6a995dec2e..c74fcd5997 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IFromServiceMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IFromServiceMetadata.cs @@ -1,12 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Metadata +namespace Microsoft.AspNetCore.Http.Metadata; + +/// +/// Interface marking attributes that specify a parameter should be bound using request services. +/// +public interface IFromServiceMetadata { - /// - /// Interface marking attributes that specify a parameter should be bound using request services. - /// - public interface IFromServiceMetadata - { - } } diff --git a/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs index 7bbd8546d4..92bc264271 100644 --- a/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/IProducesResponseTypeMetadata.cs @@ -1,26 +1,25 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Metadata +namespace Microsoft.AspNetCore.Http.Metadata; + +/// +/// Defines a contract for outline the response type returned from an endpoint. +/// +public interface IProducesResponseTypeMetadata { /// - /// Defines a contract for outline the response type returned from an endpoint. + /// Gets the optimistic return type of the action. /// - public interface IProducesResponseTypeMetadata - { - /// - /// Gets the optimistic return type of the action. - /// - Type? Type { get; } + Type? Type { get; } - /// - /// Gets the HTTP status code of the response. - /// - int StatusCode { get; } + /// + /// Gets the HTTP status code of the response. + /// + int StatusCode { get; } - /// - /// Gets the content types supported by the metadata. - /// - IEnumerable ContentTypes { get; } - } -} \ No newline at end of file + /// + /// Gets the content types supported by the metadata. + /// + IEnumerable ContentTypes { get; } +} diff --git a/src/Http/Http.Abstractions/src/Metadata/ITagsMetadata.cs b/src/Http/Http.Abstractions/src/Metadata/ITagsMetadata.cs index 47d119cc20..0fcd9ba31c 100644 --- a/src/Http/Http.Abstractions/src/Metadata/ITagsMetadata.cs +++ b/src/Http/Http.Abstractions/src/Metadata/ITagsMetadata.cs @@ -1,16 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Metadata +namespace Microsoft.AspNetCore.Http.Metadata; + +/// +/// Defines a contract used to specify a collection of tags in . +/// +public interface ITagsMetadata { /// - /// Defines a contract used to specify a collection of tags in . + /// Gets the collection of tags associated with the endpoint. /// - public interface ITagsMetadata - { - /// - /// Gets the collection of tags associated with the endpoint. - /// - IReadOnlyList Tags { get; } - } + IReadOnlyList Tags { get; } } diff --git a/src/Http/Http.Abstractions/src/PathString.cs b/src/Http/Http.Abstractions/src/PathString.cs index fbcaa2bb87..1b5077cf99 100644 --- a/src/Http/Http.Abstractions/src/PathString.cs +++ b/src/Http/Http.Abstractions/src/PathString.cs @@ -9,490 +9,489 @@ using System.Text; using Microsoft.AspNetCore.Http.Abstractions; using Microsoft.AspNetCore.Internal; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Provides correct escaping for Path and PathBase values when needed to reconstruct a request or redirect URI string +/// +[TypeConverter(typeof(PathStringConverter))] +public readonly struct PathString : IEquatable { + internal const int StackAllocThreshold = 128; + + /// + /// Represents the empty path. This field is read-only. + /// + public static readonly PathString Empty = new(string.Empty); + /// - /// Provides correct escaping for Path and PathBase values when needed to reconstruct a request or redirect URI string + /// Initialize the path string with a given value. This value must be in unescaped format. Use + /// PathString.FromUriComponent(value) if you have a path value which is in an escaped format. /// - [TypeConverter(typeof(PathStringConverter))] - public readonly struct PathString : IEquatable + /// The unescaped path to be assigned to the Value property. + public PathString(string? value) { - internal const int StackAllocThreshold = 128; - - /// - /// Represents the empty path. This field is read-only. - /// - public static readonly PathString Empty = new(string.Empty); - - /// - /// Initialize the path string with a given value. This value must be in unescaped format. Use - /// PathString.FromUriComponent(value) if you have a path value which is in an escaped format. - /// - /// The unescaped path to be assigned to the Value property. - public PathString(string? value) + if (!string.IsNullOrEmpty(value) && value[0] != '/') { - if (!string.IsNullOrEmpty(value) && value[0] != '/') - { - throw new ArgumentException(Resources.FormatException_PathMustStartWithSlash(nameof(value)), nameof(value)); - } - Value = value; + throw new ArgumentException(Resources.FormatException_PathMustStartWithSlash(nameof(value)), nameof(value)); } + Value = value; + } - /// - /// The unescaped path value - /// - public string? Value { get; } + /// + /// The unescaped path value + /// + public string? Value { get; } - /// - /// True if the path is not empty - /// - [MemberNotNullWhen(true, nameof(Value))] - public bool HasValue - { - get { return !string.IsNullOrEmpty(Value); } - } + /// + /// True if the path is not empty + /// + [MemberNotNullWhen(true, nameof(Value))] + public bool HasValue + { + get { return !string.IsNullOrEmpty(Value); } + } + + /// + /// Provides the path string escaped in a way which is correct for combining into the URI representation. + /// + /// The escaped path value + public override string ToString() + { + return ToUriComponent(); + } - /// - /// Provides the path string escaped in a way which is correct for combining into the URI representation. - /// - /// The escaped path value - public override string ToString() + /// + /// Provides the path string escaped in a way which is correct for combining into the URI representation. + /// + /// The escaped path value + public string ToUriComponent() + { + if (!HasValue) { - return ToUriComponent(); + return string.Empty; } - /// - /// Provides the path string escaped in a way which is correct for combining into the URI representation. - /// - /// The escaped path value - public string ToUriComponent() + var value = Value; + var i = 0; + for (; i < value.Length; i++) { - if (!HasValue) + if (!PathStringHelper.IsValidPathChar(value[i]) || PathStringHelper.IsPercentEncodedChar(value, i)) { - return string.Empty; + break; } + } - var value = Value; - var i = 0; - for (; i < value.Length; i++) - { - if (!PathStringHelper.IsValidPathChar(value[i]) || PathStringHelper.IsPercentEncodedChar(value, i)) - { - break; - } - } + if (i < value.Length) + { + return ToEscapedUriComponent(value, i); + } - if (i < value.Length) - { - return ToEscapedUriComponent(value, i); - } + return value; + } - return value; - } + private static string ToEscapedUriComponent(string value, int i) + { + StringBuilder? buffer = null; + + var start = 0; + var count = i; + var requiresEscaping = false; - private static string ToEscapedUriComponent(string value, int i) + while (i < value.Length) { - StringBuilder? buffer = null; + var isPercentEncodedChar = PathStringHelper.IsPercentEncodedChar(value, i); + if (PathStringHelper.IsValidPathChar(value[i]) || isPercentEncodedChar) + { + if (requiresEscaping) + { + // the current segment requires escape + buffer ??= new StringBuilder(value.Length * 3); + buffer.Append(Uri.EscapeDataString(value.Substring(start, count))); - var start = 0; - var count = i; - var requiresEscaping = false; + requiresEscaping = false; + start = i; + count = 0; + } - while (i < value.Length) - { - var isPercentEncodedChar = PathStringHelper.IsPercentEncodedChar(value, i); - if (PathStringHelper.IsValidPathChar(value[i]) || isPercentEncodedChar) + if (isPercentEncodedChar) { - if (requiresEscaping) - { - // the current segment requires escape - buffer ??= new StringBuilder(value.Length * 3); - buffer.Append(Uri.EscapeDataString(value.Substring(start, count))); - - requiresEscaping = false; - start = i; - count = 0; - } - - if (isPercentEncodedChar) - { - count += 3; - i += 3; - } - else - { - count++; - i++; - } + count += 3; + i += 3; } else { - if (!requiresEscaping) - { - // the current segment doesn't require escape - buffer ??= new StringBuilder(value.Length * 3); - buffer.Append(value, start, count); - - requiresEscaping = true; - start = i; - count = 0; - } - count++; i++; } } - - if (count == value.Length && !requiresEscaping) - { - return value; - } else { - if (count > 0) + if (!requiresEscaping) { + // the current segment doesn't require escape buffer ??= new StringBuilder(value.Length * 3); + buffer.Append(value, start, count); - if (requiresEscaping) - { - buffer.Append(Uri.EscapeDataString(value.Substring(start, count))); - } - else - { - buffer.Append(value, start, count); - } + requiresEscaping = true; + start = i; + count = 0; } - return buffer?.ToString() ?? string.Empty; + count++; + i++; } } - /// - /// Returns an PathString given the path as it is escaped in the URI format. The string MUST NOT contain any - /// value that is not a path. - /// - /// The escaped path as it appears in the URI format. - /// The resulting PathString - public static PathString FromUriComponent(string uriComponent) + if (count == value.Length && !requiresEscaping) { - int position = uriComponent.IndexOf('%'); - if (position == -1) - { - return new PathString(uriComponent); - } - Span pathBuffer = uriComponent.Length <= StackAllocThreshold ? stackalloc char[StackAllocThreshold] : new char[uriComponent.Length]; - uriComponent.CopyTo(pathBuffer); - var length = UrlDecoder.DecodeInPlace(pathBuffer.Slice(position, uriComponent.Length - position)); - pathBuffer = pathBuffer.Slice(0, position + length); - return new PathString(pathBuffer.ToString()); - } - - /// - /// Returns an PathString given the path as from a Uri object. Relative Uri objects are not supported. - /// - /// The Uri object - /// The resulting PathString - public static PathString FromUriComponent(Uri uri) - { - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } - var uriComponent = uri.GetComponents(UriComponents.Path, UriFormat.UriEscaped); - Span pathBuffer = uriComponent.Length < StackAllocThreshold ? stackalloc char[StackAllocThreshold] : new char[uriComponent.Length + 1]; - pathBuffer[0] = '/'; - var length = UrlDecoder.DecodeRequestLine(uriComponent.AsSpan(), pathBuffer.Slice(1)); - pathBuffer = pathBuffer.Slice(0, length + 1); - return new PathString(pathBuffer.ToString()); - } - - /// - /// Determines whether the beginning of this instance matches the specified . - /// - /// The to compare. - /// true if value matches the beginning of this string; otherwise, false. - public bool StartsWithSegments(PathString other) - { - return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase); + return value; } - - /// - /// Determines whether the beginning of this instance matches the specified when compared - /// using the specified comparison option. - /// - /// The to compare. - /// One of the enumeration values that determines how this and value are compared. - /// true if value matches the beginning of this string; otherwise, false. - public bool StartsWithSegments(PathString other, StringComparison comparisonType) + else { - var value1 = Value ?? string.Empty; - var value2 = other.Value ?? string.Empty; - if (value1.StartsWith(value2, comparisonType)) + if (count > 0) { - return value1.Length == value2.Length || value1[value2.Length] == '/'; - } - return false; - } - - /// - /// Determines whether the beginning of this instance matches the specified and returns - /// the remaining segments. - /// - /// The to compare. - /// The remaining segments after the match. - /// true if value matches the beginning of this string; otherwise, false. - public bool StartsWithSegments(PathString other, out PathString remaining) - { - return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase, out remaining); - } + buffer ??= new StringBuilder(value.Length * 3); - /// - /// Determines whether the beginning of this instance matches the specified when compared - /// using the specified comparison option and returns the remaining segments. - /// - /// The to compare. - /// One of the enumeration values that determines how this and value are compared. - /// The remaining segments after the match. - /// true if value matches the beginning of this string; otherwise, false. - public bool StartsWithSegments(PathString other, StringComparison comparisonType, out PathString remaining) - { - var value1 = Value ?? string.Empty; - var value2 = other.Value ?? string.Empty; - if (value1.StartsWith(value2, comparisonType)) - { - if (value1.Length == value2.Length || value1[value2.Length] == '/') + if (requiresEscaping) { - remaining = new PathString(value1[value2.Length..]); - return true; + buffer.Append(Uri.EscapeDataString(value.Substring(start, count))); } - } - remaining = Empty; - return false; - } - - /// - /// Determines whether the beginning of this instance matches the specified and returns - /// the matched and remaining segments. - /// - /// The to compare. - /// The matched segments with the original casing in the source value. - /// The remaining segments after the match. - /// true if value matches the beginning of this string; otherwise, false. - public bool StartsWithSegments(PathString other, out PathString matched, out PathString remaining) - { - return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase, out matched, out remaining); - } - - /// - /// Determines whether the beginning of this instance matches the specified when compared - /// using the specified comparison option and returns the matched and remaining segments. - /// - /// The to compare. - /// One of the enumeration values that determines how this and value are compared. - /// The matched segments with the original casing in the source value. - /// The remaining segments after the match. - /// true if value matches the beginning of this string; otherwise, false. - public bool StartsWithSegments(PathString other, StringComparison comparisonType, out PathString matched, out PathString remaining) - { - var value1 = Value ?? string.Empty; - var value2 = other.Value ?? string.Empty; - if (value1.StartsWith(value2, comparisonType)) - { - if (value1.Length == value2.Length || value1[value2.Length] == '/') + else { - matched = new PathString(value1.Substring(0, value2.Length)); - remaining = new PathString(value1[value2.Length..]); - return true; + buffer.Append(value, start, count); } } - remaining = Empty; - matched = Empty; - return false; + + return buffer?.ToString() ?? string.Empty; } + } - /// - /// Adds two PathString instances into a combined PathString value. - /// - /// The combined PathString value - public PathString Add(PathString other) + /// + /// Returns an PathString given the path as it is escaped in the URI format. The string MUST NOT contain any + /// value that is not a path. + /// + /// The escaped path as it appears in the URI format. + /// The resulting PathString + public static PathString FromUriComponent(string uriComponent) + { + int position = uriComponent.IndexOf('%'); + if (position == -1) { - if (HasValue && - other.HasValue && - Value[^1] == '/') - { - // If the path string has a trailing slash and the other string has a leading slash, we need - // to trim one of them. - var combined = string.Concat(Value.AsSpan(), other.Value.AsSpan(1)); - return new PathString(combined); - } - - return new PathString(Value + other.Value); + return new PathString(uriComponent); } + Span pathBuffer = uriComponent.Length <= StackAllocThreshold ? stackalloc char[StackAllocThreshold] : new char[uriComponent.Length]; + uriComponent.CopyTo(pathBuffer); + var length = UrlDecoder.DecodeInPlace(pathBuffer.Slice(position, uriComponent.Length - position)); + pathBuffer = pathBuffer.Slice(0, position + length); + return new PathString(pathBuffer.ToString()); + } - /// - /// Combines a PathString and QueryString into the joined URI formatted string value. - /// - /// The joined URI formatted string value - public string Add(QueryString other) + /// + /// Returns an PathString given the path as from a Uri object. Relative Uri objects are not supported. + /// + /// The Uri object + /// The resulting PathString + public static PathString FromUriComponent(Uri uri) + { + if (uri == null) { - return ToUriComponent() + other.ToUriComponent(); + throw new ArgumentNullException(nameof(uri)); } + var uriComponent = uri.GetComponents(UriComponents.Path, UriFormat.UriEscaped); + Span pathBuffer = uriComponent.Length < StackAllocThreshold ? stackalloc char[StackAllocThreshold] : new char[uriComponent.Length + 1]; + pathBuffer[0] = '/'; + var length = UrlDecoder.DecodeRequestLine(uriComponent.AsSpan(), pathBuffer.Slice(1)); + pathBuffer = pathBuffer.Slice(0, length + 1); + return new PathString(pathBuffer.ToString()); + } - /// - /// Compares this PathString value to another value. The default comparison is StringComparison.OrdinalIgnoreCase. - /// - /// The second PathString for comparison. - /// True if both PathString values are equal - public bool Equals(PathString other) + /// + /// Determines whether the beginning of this instance matches the specified . + /// + /// The to compare. + /// true if value matches the beginning of this string; otherwise, false. + public bool StartsWithSegments(PathString other) + { + return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Determines whether the beginning of this instance matches the specified when compared + /// using the specified comparison option. + /// + /// The to compare. + /// One of the enumeration values that determines how this and value are compared. + /// true if value matches the beginning of this string; otherwise, false. + public bool StartsWithSegments(PathString other, StringComparison comparisonType) + { + var value1 = Value ?? string.Empty; + var value2 = other.Value ?? string.Empty; + if (value1.StartsWith(value2, comparisonType)) { - return Equals(other, StringComparison.OrdinalIgnoreCase); + return value1.Length == value2.Length || value1[value2.Length] == '/'; } + return false; + } + + /// + /// Determines whether the beginning of this instance matches the specified and returns + /// the remaining segments. + /// + /// The to compare. + /// The remaining segments after the match. + /// true if value matches the beginning of this string; otherwise, false. + public bool StartsWithSegments(PathString other, out PathString remaining) + { + return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase, out remaining); + } - /// - /// Compares this PathString value to another value using a specific StringComparison type - /// - /// The second PathString for comparison - /// The StringComparison type to use - /// True if both PathString values are equal - public bool Equals(PathString other, StringComparison comparisonType) + /// + /// Determines whether the beginning of this instance matches the specified when compared + /// using the specified comparison option and returns the remaining segments. + /// + /// The to compare. + /// One of the enumeration values that determines how this and value are compared. + /// The remaining segments after the match. + /// true if value matches the beginning of this string; otherwise, false. + public bool StartsWithSegments(PathString other, StringComparison comparisonType, out PathString remaining) + { + var value1 = Value ?? string.Empty; + var value2 = other.Value ?? string.Empty; + if (value1.StartsWith(value2, comparisonType)) { - if (!HasValue && !other.HasValue) + if (value1.Length == value2.Length || value1[value2.Length] == '/') { + remaining = new PathString(value1[value2.Length..]); return true; } - return string.Equals(Value, other.Value, comparisonType); } + remaining = Empty; + return false; + } - /// - /// Compares this PathString value to another value. The default comparison is StringComparison.OrdinalIgnoreCase. - /// - /// The second PathString for comparison. - /// True if both PathString values are equal - public override bool Equals(object? obj) + /// + /// Determines whether the beginning of this instance matches the specified and returns + /// the matched and remaining segments. + /// + /// The to compare. + /// The matched segments with the original casing in the source value. + /// The remaining segments after the match. + /// true if value matches the beginning of this string; otherwise, false. + public bool StartsWithSegments(PathString other, out PathString matched, out PathString remaining) + { + return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase, out matched, out remaining); + } + + /// + /// Determines whether the beginning of this instance matches the specified when compared + /// using the specified comparison option and returns the matched and remaining segments. + /// + /// The to compare. + /// One of the enumeration values that determines how this and value are compared. + /// The matched segments with the original casing in the source value. + /// The remaining segments after the match. + /// true if value matches the beginning of this string; otherwise, false. + public bool StartsWithSegments(PathString other, StringComparison comparisonType, out PathString matched, out PathString remaining) + { + var value1 = Value ?? string.Empty; + var value2 = other.Value ?? string.Empty; + if (value1.StartsWith(value2, comparisonType)) { - if (obj is null) + if (value1.Length == value2.Length || value1[value2.Length] == '/') { - return !HasValue; + matched = new PathString(value1.Substring(0, value2.Length)); + remaining = new PathString(value1[value2.Length..]); + return true; } - return obj is PathString pathString && Equals(pathString); } + remaining = Empty; + matched = Empty; + return false; + } - /// - /// Returns the hash code for the PathString value. The hash code is provided by the OrdinalIgnoreCase implementation. - /// - /// The hash code - public override int GetHashCode() + /// + /// Adds two PathString instances into a combined PathString value. + /// + /// The combined PathString value + public PathString Add(PathString other) + { + if (HasValue && + other.HasValue && + Value[^1] == '/') { - return (HasValue ? StringComparer.OrdinalIgnoreCase.GetHashCode(Value) : 0); + // If the path string has a trailing slash and the other string has a leading slash, we need + // to trim one of them. + var combined = string.Concat(Value.AsSpan(), other.Value.AsSpan(1)); + return new PathString(combined); } - /// - /// Operator call through to Equals - /// - /// The left parameter - /// The right parameter - /// True if both PathString values are equal - public static bool operator ==(PathString left, PathString right) - { - return left.Equals(right); - } + return new PathString(Value + other.Value); + } - /// - /// Operator call through to Equals - /// - /// The left parameter - /// The right parameter - /// True if both PathString values are not equal - public static bool operator !=(PathString left, PathString right) - { - return !left.Equals(right); - } + /// + /// Combines a PathString and QueryString into the joined URI formatted string value. + /// + /// The joined URI formatted string value + public string Add(QueryString other) + { + return ToUriComponent() + other.ToUriComponent(); + } - /// - /// - /// The left parameter - /// The right parameter - /// The ToString combination of both values - public static string operator +(string left, PathString right) - { - // This overload exists to prevent the implicit string<->PathString converter from - // trying to call the PathString+PathString operator for things that are not path strings. - return string.Concat(left, right.ToString()); - } + /// + /// Compares this PathString value to another value. The default comparison is StringComparison.OrdinalIgnoreCase. + /// + /// The second PathString for comparison. + /// True if both PathString values are equal + public bool Equals(PathString other) + { + return Equals(other, StringComparison.OrdinalIgnoreCase); + } - /// - /// - /// The left parameter - /// The right parameter - /// The ToString combination of both values - public static string operator +(PathString left, string? right) + /// + /// Compares this PathString value to another value using a specific StringComparison type + /// + /// The second PathString for comparison + /// The StringComparison type to use + /// True if both PathString values are equal + public bool Equals(PathString other, StringComparison comparisonType) + { + if (!HasValue && !other.HasValue) { - // This overload exists to prevent the implicit string<->PathString converter from - // trying to call the PathString+PathString operator for things that are not path strings. - return string.Concat(left.ToString(), right); + return true; } + return string.Equals(Value, other.Value, comparisonType); + } - /// - /// Operator call through to Add - /// - /// The left parameter - /// The right parameter - /// The PathString combination of both values - public static PathString operator +(PathString left, PathString right) + /// + /// Compares this PathString value to another value. The default comparison is StringComparison.OrdinalIgnoreCase. + /// + /// The second PathString for comparison. + /// True if both PathString values are equal + public override bool Equals(object? obj) + { + if (obj is null) { - return left.Add(right); + return !HasValue; } + return obj is PathString pathString && Equals(pathString); + } - /// - /// Operator call through to Add - /// - /// The left parameter - /// The right parameter - /// The PathString combination of both values - public static string operator +(PathString left, QueryString right) - { - return left.Add(right); - } + /// + /// Returns the hash code for the PathString value. The hash code is provided by the OrdinalIgnoreCase implementation. + /// + /// The hash code + public override int GetHashCode() + { + return (HasValue ? StringComparer.OrdinalIgnoreCase.GetHashCode(Value) : 0); + } - /// - /// Implicitly creates a new PathString from the given string. - /// - /// - public static implicit operator PathString(string? s) - => ConvertFromString(s); - - /// - /// Implicitly calls ToString(). - /// - /// - public static implicit operator string(PathString path) - => path.ToString(); - - internal static PathString ConvertFromString(string? s) - => string.IsNullOrEmpty(s) ? new PathString(s) : FromUriComponent(s); + /// + /// Operator call through to Equals + /// + /// The left parameter + /// The right parameter + /// True if both PathString values are equal + public static bool operator ==(PathString left, PathString right) + { + return left.Equals(right); } - internal sealed class PathStringConverter : TypeConverter + /// + /// Operator call through to Equals + /// + /// The left parameter + /// The right parameter + /// True if both PathString values are not equal + public static bool operator !=(PathString left, PathString right) { - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) - => sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + return !left.Equals(right); + } - public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) - => value is string @string - ? PathString.ConvertFromString(@string) - : base.ConvertFrom(context, culture, value); + /// + /// + /// The left parameter + /// The right parameter + /// The ToString combination of both values + public static string operator +(string left, PathString right) + { + // This overload exists to prevent the implicit string<->PathString converter from + // trying to call the PathString+PathString operator for things that are not path strings. + return string.Concat(left, right.ToString()); + } - public override object? ConvertTo(ITypeDescriptorContext? context, - CultureInfo? culture, object? value, Type destinationType) - { - if (destinationType == null) - { - throw new ArgumentNullException(nameof(destinationType)); - } + /// + /// + /// The left parameter + /// The right parameter + /// The ToString combination of both values + public static string operator +(PathString left, string? right) + { + // This overload exists to prevent the implicit string<->PathString converter from + // trying to call the PathString+PathString operator for things that are not path strings. + return string.Concat(left.ToString(), right); + } - return destinationType == typeof(string) - ? value?.ToString() ?? string.Empty - : base.ConvertTo(context, culture, value, destinationType); + /// + /// Operator call through to Add + /// + /// The left parameter + /// The right parameter + /// The PathString combination of both values + public static PathString operator +(PathString left, PathString right) + { + return left.Add(right); + } + + /// + /// Operator call through to Add + /// + /// The left parameter + /// The right parameter + /// The PathString combination of both values + public static string operator +(PathString left, QueryString right) + { + return left.Add(right); + } + + /// + /// Implicitly creates a new PathString from the given string. + /// + /// + public static implicit operator PathString(string? s) + => ConvertFromString(s); + + /// + /// Implicitly calls ToString(). + /// + /// + public static implicit operator string(PathString path) + => path.ToString(); + + internal static PathString ConvertFromString(string? s) + => string.IsNullOrEmpty(s) ? new PathString(s) : FromUriComponent(s); +} + +internal sealed class PathStringConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + => sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + => value is string @string + ? PathString.ConvertFromString(@string) + : base.ConvertFrom(context, culture, value); + + public override object? ConvertTo(ITypeDescriptorContext? context, + CultureInfo? culture, object? value, Type destinationType) + { + if (destinationType == null) + { + throw new ArgumentNullException(nameof(destinationType)); } + + return destinationType == typeof(string) + ? value?.ToString() ?? string.Empty + : base.ConvertTo(context, culture, value, destinationType); } } diff --git a/src/Http/Http.Abstractions/src/QueryString.cs b/src/Http/Http.Abstractions/src/QueryString.cs index 5f03accd77..646f2756d4 100644 --- a/src/Http/Http.Abstractions/src/QueryString.cs +++ b/src/Http/Http.Abstractions/src/QueryString.cs @@ -8,291 +8,290 @@ using System.Text; using System.Text.Encodings.Web; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Provides correct handling for QueryString value when needed to reconstruct a request or redirect URI string +/// +public readonly struct QueryString : IEquatable { /// - /// Provides correct handling for QueryString value when needed to reconstruct a request or redirect URI string + /// Represents the empty query string. This field is read-only. /// - public readonly struct QueryString : IEquatable - { - /// - /// Represents the empty query string. This field is read-only. - /// - public static readonly QueryString Empty = new QueryString(string.Empty); + public static readonly QueryString Empty = new QueryString(string.Empty); - /// - /// Initialize the query string with a given value. This value must be in escaped and delimited format with - /// a leading '?' character. - /// - /// The query string to be assigned to the Value property. - public QueryString(string? value) + /// + /// Initialize the query string with a given value. This value must be in escaped and delimited format with + /// a leading '?' character. + /// + /// The query string to be assigned to the Value property. + public QueryString(string? value) + { + if (!string.IsNullOrEmpty(value) && value[0] != '?') { - if (!string.IsNullOrEmpty(value) && value[0] != '?') - { - throw new ArgumentException("The leading '?' must be included for a non-empty query.", nameof(value)); - } - Value = value; + throw new ArgumentException("The leading '?' must be included for a non-empty query.", nameof(value)); } + Value = value; + } - /// - /// The escaped query string with the leading '?' character - /// - public string? Value { get; } + /// + /// The escaped query string with the leading '?' character + /// + public string? Value { get; } - /// - /// True if the query string is not empty - /// - public bool HasValue => !string.IsNullOrEmpty(Value); + /// + /// True if the query string is not empty + /// + public bool HasValue => !string.IsNullOrEmpty(Value); - /// - /// Provides the query string escaped in a way which is correct for combining into the URI representation. - /// A leading '?' character will be included unless the Value is null or empty. Characters which are potentially - /// dangerous are escaped. - /// - /// The query string value - public override string ToString() - { - return ToUriComponent(); - } + /// + /// Provides the query string escaped in a way which is correct for combining into the URI representation. + /// A leading '?' character will be included unless the Value is null or empty. Characters which are potentially + /// dangerous are escaped. + /// + /// The query string value + public override string ToString() + { + return ToUriComponent(); + } + + /// + /// Provides the query string escaped in a way which is correct for combining into the URI representation. + /// A leading '?' character will be included unless the Value is null or empty. Characters which are potentially + /// dangerous are escaped. + /// + /// The query string value + public string ToUriComponent() + { + // Escape things properly so System.Uri doesn't mis-interpret the data. + return !string.IsNullOrEmpty(Value) ? Value!.Replace("#", "%23") : string.Empty; + } - /// - /// Provides the query string escaped in a way which is correct for combining into the URI representation. - /// A leading '?' character will be included unless the Value is null or empty. Characters which are potentially - /// dangerous are escaped. - /// - /// The query string value - public string ToUriComponent() + /// + /// Returns an QueryString given the query as it is escaped in the URI format. The string MUST NOT contain any + /// value that is not a query. + /// + /// The escaped query as it appears in the URI format. + /// The resulting QueryString + public static QueryString FromUriComponent(string uriComponent) + { + if (string.IsNullOrEmpty(uriComponent)) { - // Escape things properly so System.Uri doesn't mis-interpret the data. - return !string.IsNullOrEmpty(Value) ? Value!.Replace("#", "%23") : string.Empty; + return new QueryString(string.Empty); } + return new QueryString(uriComponent); + } - /// - /// Returns an QueryString given the query as it is escaped in the URI format. The string MUST NOT contain any - /// value that is not a query. - /// - /// The escaped query as it appears in the URI format. - /// The resulting QueryString - public static QueryString FromUriComponent(string uriComponent) + /// + /// Returns an QueryString given the query as from a Uri object. Relative Uri objects are not supported. + /// + /// The Uri object + /// The resulting QueryString + public static QueryString FromUriComponent(Uri uri) + { + if (uri == null) { - if (string.IsNullOrEmpty(uriComponent)) - { - return new QueryString(string.Empty); - } - return new QueryString(uriComponent); + throw new ArgumentNullException(nameof(uri)); } - /// - /// Returns an QueryString given the query as from a Uri object. Relative Uri objects are not supported. - /// - /// The Uri object - /// The resulting QueryString - public static QueryString FromUriComponent(Uri uri) + string queryValue = uri.GetComponents(UriComponents.Query, UriFormat.UriEscaped); + if (!string.IsNullOrEmpty(queryValue)) { - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } - - string queryValue = uri.GetComponents(UriComponents.Query, UriFormat.UriEscaped); - if (!string.IsNullOrEmpty(queryValue)) - { - queryValue = "?" + queryValue; - } - return new QueryString(queryValue); + queryValue = "?" + queryValue; } + return new QueryString(queryValue); + } - /// - /// Create a query string with a single given parameter name and value. - /// - /// The un-encoded parameter name - /// The un-encoded parameter value - /// The resulting QueryString - public static QueryString Create(string name, string value) + /// + /// Create a query string with a single given parameter name and value. + /// + /// The un-encoded parameter name + /// The un-encoded parameter value + /// The resulting QueryString + public static QueryString Create(string name, string value) + { + if (name == null) { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - if (!string.IsNullOrEmpty(value)) - { - value = UrlEncoder.Default.Encode(value); - } - return new QueryString($"?{UrlEncoder.Default.Encode(name)}={value}"); + throw new ArgumentNullException(nameof(name)); } - /// - /// Creates a query string composed from the given name value pairs. - /// - /// - /// The resulting QueryString - public static QueryString Create(IEnumerable> parameters) + if (!string.IsNullOrEmpty(value)) { - var builder = new StringBuilder(); - var first = true; - foreach (var pair in parameters) - { - AppendKeyValuePair(builder, pair.Key, pair.Value, first); - first = false; - } - - return new QueryString(builder.ToString()); + value = UrlEncoder.Default.Encode(value); } + return new QueryString($"?{UrlEncoder.Default.Encode(name)}={value}"); + } - /// - /// Creates a query string composed from the given name value pairs. - /// - /// - /// The resulting QueryString - public static QueryString Create(IEnumerable> parameters) + /// + /// Creates a query string composed from the given name value pairs. + /// + /// + /// The resulting QueryString + public static QueryString Create(IEnumerable> parameters) + { + var builder = new StringBuilder(); + var first = true; + foreach (var pair in parameters) { - var builder = new StringBuilder(); - var first = true; - - foreach (var pair in parameters) - { - // If nothing in this pair.Values, append null value and continue - if (StringValues.IsNullOrEmpty(pair.Value)) - { - AppendKeyValuePair(builder, pair.Key, null, first); - first = false; - continue; - } - // Otherwise, loop through values in pair.Value - foreach (var value in pair.Value) - { - AppendKeyValuePair(builder, pair.Key, value, first); - first = false; - } - } - - return new QueryString(builder.ToString()); + AppendKeyValuePair(builder, pair.Key, pair.Value, first); + first = false; } - /// - /// Concatenates to the current query string. - /// - /// The to concatenate. - /// The concatenated . - public QueryString Add(QueryString other) - { - if (!HasValue || Value!.Equals("?", StringComparison.Ordinal)) - { - return other; - } - if (!other.HasValue || other.Value!.Equals("?", StringComparison.Ordinal)) - { - return this; - } + return new QueryString(builder.ToString()); + } - // ?name1=value1 Add ?name2=value2 returns ?name1=value1&name2=value2 - return new QueryString(string.Concat(Value, "&", other.Value.AsSpan(1))); - } + /// + /// Creates a query string composed from the given name value pairs. + /// + /// + /// The resulting QueryString + public static QueryString Create(IEnumerable> parameters) + { + var builder = new StringBuilder(); + var first = true; - /// - /// Concatenates a query string with and - /// to the current query string. - /// - /// The name of the query string to concatenate. - /// The value of the query string to concatenate. - /// The concatenated . - public QueryString Add(string name, string value) + foreach (var pair in parameters) { - if (name == null) + // If nothing in this pair.Values, append null value and continue + if (StringValues.IsNullOrEmpty(pair.Value)) { - throw new ArgumentNullException(nameof(name)); + AppendKeyValuePair(builder, pair.Key, null, first); + first = false; + continue; } - - if (!HasValue || Value!.Equals("?", StringComparison.Ordinal)) + // Otherwise, loop through values in pair.Value + foreach (var value in pair.Value) { - return Create(name, value); + AppendKeyValuePair(builder, pair.Key, value, first); + first = false; } - - var builder = new StringBuilder(Value); - AppendKeyValuePair(builder, name, value, first: false); - return new QueryString(builder.ToString()); } - /// - /// Evalutes if the current query string is equal to . - /// - /// The to compare. - /// if the query strings are equal. - public bool Equals(QueryString other) + return new QueryString(builder.ToString()); + } + + /// + /// Concatenates to the current query string. + /// + /// The to concatenate. + /// The concatenated . + public QueryString Add(QueryString other) + { + if (!HasValue || Value!.Equals("?", StringComparison.Ordinal)) { - if (!HasValue && !other.HasValue) - { - return true; - } - return string.Equals(Value, other.Value, StringComparison.Ordinal); + return other; } - - /// - /// Evaluates if the current query string is equal to an object . - /// - /// An object to compare. - /// if the query strings are equal. - public override bool Equals(object? obj) + if (!other.HasValue || other.Value!.Equals("?", StringComparison.Ordinal)) { - if (ReferenceEquals(null, obj)) - { - return !HasValue; - } - return obj is QueryString && Equals((QueryString)obj); + return this; } - /// - /// Gets a hash code for the value. - /// - /// The hash code as an . - public override int GetHashCode() + // ?name1=value1 Add ?name2=value2 returns ?name1=value1&name2=value2 + return new QueryString(string.Concat(Value, "&", other.Value.AsSpan(1))); + } + + /// + /// Concatenates a query string with and + /// to the current query string. + /// + /// The name of the query string to concatenate. + /// The value of the query string to concatenate. + /// The concatenated . + public QueryString Add(string name, string value) + { + if (name == null) { - return (HasValue ? Value!.GetHashCode() : 0); + throw new ArgumentNullException(nameof(name)); } - /// - /// Evaluates if one query string is equal to another. - /// - /// A instance. - /// A instance. - /// if the query strings are equal. - public static bool operator ==(QueryString left, QueryString right) + if (!HasValue || Value!.Equals("?", StringComparison.Ordinal)) { - return left.Equals(right); + return Create(name, value); } - /// - /// Evaluates if one query string is not equal to another. - /// - /// A instance. - /// A instance. - /// if the query strings are not equal. - public static bool operator !=(QueryString left, QueryString right) + var builder = new StringBuilder(Value); + AppendKeyValuePair(builder, name, value, first: false); + return new QueryString(builder.ToString()); + } + + /// + /// Evalutes if the current query string is equal to . + /// + /// The to compare. + /// if the query strings are equal. + public bool Equals(QueryString other) + { + if (!HasValue && !other.HasValue) { - return !left.Equals(right); + return true; } + return string.Equals(Value, other.Value, StringComparison.Ordinal); + } - /// - /// Concatenates and into a single query string. - /// - /// A instance. - /// A instance. - /// The concatenated . - public static QueryString operator +(QueryString left, QueryString right) + /// + /// Evaluates if the current query string is equal to an object . + /// + /// An object to compare. + /// if the query strings are equal. + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) { - return left.Add(right); + return !HasValue; } + return obj is QueryString && Equals((QueryString)obj); + } + + /// + /// Gets a hash code for the value. + /// + /// The hash code as an . + public override int GetHashCode() + { + return (HasValue ? Value!.GetHashCode() : 0); + } + + /// + /// Evaluates if one query string is equal to another. + /// + /// A instance. + /// A instance. + /// if the query strings are equal. + public static bool operator ==(QueryString left, QueryString right) + { + return left.Equals(right); + } + + /// + /// Evaluates if one query string is not equal to another. + /// + /// A instance. + /// A instance. + /// if the query strings are not equal. + public static bool operator !=(QueryString left, QueryString right) + { + return !left.Equals(right); + } - private static void AppendKeyValuePair(StringBuilder builder, string key, string? value, bool first) + /// + /// Concatenates and into a single query string. + /// + /// A instance. + /// A instance. + /// The concatenated . + public static QueryString operator +(QueryString left, QueryString right) + { + return left.Add(right); + } + + private static void AppendKeyValuePair(StringBuilder builder, string key, string? value, bool first) + { + builder.Append(first ? '?' : '&'); + builder.Append(UrlEncoder.Default.Encode(key)); + builder.Append('='); + if (!string.IsNullOrEmpty(value)) { - builder.Append(first ? '?' : '&'); - builder.Append(UrlEncoder.Default.Encode(key)); - builder.Append('='); - if (!string.IsNullOrEmpty(value)) - { - builder.Append(UrlEncoder.Default.Encode(value)); - } + builder.Append(UrlEncoder.Default.Encode(value)); } } } diff --git a/src/Http/Http.Abstractions/src/RequestDelegate.cs b/src/Http/Http.Abstractions/src/RequestDelegate.cs index ad4254bd1b..6fb2e5a432 100644 --- a/src/Http/Http.Abstractions/src/RequestDelegate.cs +++ b/src/Http/Http.Abstractions/src/RequestDelegate.cs @@ -3,12 +3,11 @@ using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http -{ - /// - /// A function that can process an HTTP request. - /// - /// The for the request. - /// A task that represents the completion of request processing. - public delegate Task RequestDelegate(HttpContext context); -} \ No newline at end of file +namespace Microsoft.AspNetCore.Http; + +/// +/// A function that can process an HTTP request. +/// +/// The for the request. +/// A task that represents the completion of request processing. +public delegate Task RequestDelegate(HttpContext context); diff --git a/src/Http/Http.Abstractions/src/RequestDelegateResult.cs b/src/Http/Http.Abstractions/src/RequestDelegateResult.cs index 88ddd28a41..5ea60fe338 100644 --- a/src/Http/Http.Abstractions/src/RequestDelegateResult.cs +++ b/src/Http/Http.Abstractions/src/RequestDelegateResult.cs @@ -3,31 +3,29 @@ using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// The result of creating a from a +/// +public sealed class RequestDelegateResult { /// - /// The result of creating a from a + /// Creates a new instance of . /// - public sealed class RequestDelegateResult + public RequestDelegateResult(RequestDelegate requestDelegate, IReadOnlyList metadata) { - /// - /// Creates a new instance of . - /// - public RequestDelegateResult(RequestDelegate requestDelegate, IReadOnlyList metadata) - { - RequestDelegate = requestDelegate; - EndpointMetadata = metadata; - } - - /// - /// Gets the - /// - public RequestDelegate RequestDelegate { get;} - - /// - /// Gets endpoint metadata inferred from creating the - /// - public IReadOnlyList EndpointMetadata { get;} + RequestDelegate = requestDelegate; + EndpointMetadata = metadata; } + /// + /// Gets the + /// + public RequestDelegate RequestDelegate { get; } + + /// + /// Gets endpoint metadata inferred from creating the + /// + public IReadOnlyList EndpointMetadata { get; } } diff --git a/src/Http/Http.Abstractions/src/Routing/Endpoint.cs b/src/Http/Http.Abstractions/src/Routing/Endpoint.cs index bf1e2fa076..a3b1718687 100644 --- a/src/Http/Http.Abstractions/src/Routing/Endpoint.cs +++ b/src/Http/Http.Abstractions/src/Routing/Endpoint.cs @@ -1,52 +1,51 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Represents a logical endpoint in an application. +/// +public class Endpoint { /// - /// Represents a logical endpoint in an application. + /// Creates a new instance of . /// - public class Endpoint + /// The delegate used to process requests for the endpoint. + /// + /// The endpoint . May be null. + /// + /// + /// The informational display name of the endpoint. May be null. + /// + public Endpoint( + RequestDelegate? requestDelegate, + EndpointMetadataCollection? metadata, + string? displayName) { - /// - /// Creates a new instance of . - /// - /// The delegate used to process requests for the endpoint. - /// - /// The endpoint . May be null. - /// - /// - /// The informational display name of the endpoint. May be null. - /// - public Endpoint( - RequestDelegate? requestDelegate, - EndpointMetadataCollection? metadata, - string? displayName) - { - // All are allowed to be null - RequestDelegate = requestDelegate; - Metadata = metadata ?? EndpointMetadataCollection.Empty; - DisplayName = displayName; - } + // All are allowed to be null + RequestDelegate = requestDelegate; + Metadata = metadata ?? EndpointMetadataCollection.Empty; + DisplayName = displayName; + } - /// - /// Gets the informational display name of this endpoint. - /// - public string? DisplayName { get; } + /// + /// Gets the informational display name of this endpoint. + /// + public string? DisplayName { get; } - /// - /// Gets the collection of metadata associated with this endpoint. - /// - public EndpointMetadataCollection Metadata { get; } + /// + /// Gets the collection of metadata associated with this endpoint. + /// + public EndpointMetadataCollection Metadata { get; } - /// - /// Gets the delegate used to process requests for the endpoint. - /// - public RequestDelegate? RequestDelegate { get; } + /// + /// Gets the delegate used to process requests for the endpoint. + /// + public RequestDelegate? RequestDelegate { get; } - /// - /// Returns a string representation of the endpoint. - /// - public override string? ToString() => DisplayName ?? base.ToString(); - } + /// + /// Returns a string representation of the endpoint. + /// + public override string? ToString() => DisplayName ?? base.ToString(); } diff --git a/src/Http/Http.Abstractions/src/Routing/EndpointHttpContextExtensions.cs b/src/Http/Http.Abstractions/src/Routing/EndpointHttpContextExtensions.cs index 2c03bde3d3..77aa9af3bf 100644 --- a/src/Http/Http.Abstractions/src/Routing/EndpointHttpContextExtensions.cs +++ b/src/Http/Http.Abstractions/src/Routing/EndpointHttpContextExtensions.cs @@ -4,67 +4,66 @@ using System; using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Extension methods to expose Endpoint on HttpContext. +/// +public static class EndpointHttpContextExtensions { /// - /// Extension methods to expose Endpoint on HttpContext. + /// Extension method for getting the for the current request. /// - public static class EndpointHttpContextExtensions + /// The context. + /// The . + public static Endpoint? GetEndpoint(this HttpContext context) { - /// - /// Extension method for getting the for the current request. - /// - /// The context. - /// The . - public static Endpoint? GetEndpoint(this HttpContext context) + if (context == null) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } + throw new ArgumentNullException(nameof(context)); + } + + return context.Features.Get()?.Endpoint; + } - return context.Features.Get()?.Endpoint; + /// + /// Extension method for setting the for the current request. + /// + /// The context. + /// The . + public static void SetEndpoint(this HttpContext context, Endpoint? endpoint) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); } - /// - /// Extension method for setting the for the current request. - /// - /// The context. - /// The . - public static void SetEndpoint(this HttpContext context, Endpoint? endpoint) + var feature = context.Features.Get(); + + if (endpoint != null) { - if (context == null) + if (feature == null) { - throw new ArgumentNullException(nameof(context)); + feature = new EndpointFeature(); + context.Features.Set(feature); } - var feature = context.Features.Get(); - - if (endpoint != null) + feature.Endpoint = endpoint; + } + else + { + if (feature == null) { - if (feature == null) - { - feature = new EndpointFeature(); - context.Features.Set(feature); - } - - feature.Endpoint = endpoint; + // No endpoint to set and no feature on context. Do nothing + return; } - else - { - if (feature == null) - { - // No endpoint to set and no feature on context. Do nothing - return; - } - feature.Endpoint = null; - } + feature.Endpoint = null; } + } - private class EndpointFeature : IEndpointFeature - { - public Endpoint? Endpoint { get; set; } - } + private class EndpointFeature : IEndpointFeature + { + public Endpoint? Endpoint { get; set; } } } diff --git a/src/Http/Http.Abstractions/src/Routing/EndpointMetadataCollection.cs b/src/Http/Http.Abstractions/src/Routing/EndpointMetadataCollection.cs index bab598ec6d..70bfff4508 100644 --- a/src/Http/Http.Abstractions/src/Routing/EndpointMetadataCollection.cs +++ b/src/Http/Http.Abstractions/src/Routing/EndpointMetadataCollection.cs @@ -8,201 +8,200 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// A collection of arbitrary metadata associated with an endpoint. +/// +/// +/// instances contain a list of metadata items +/// of arbitrary types. The metadata items are stored as an ordered collection with +/// items arranged in ascending order of precedence. +/// +public sealed class EndpointMetadataCollection : IReadOnlyList { /// - /// A collection of arbitrary metadata associated with an endpoint. + /// An empty . /// - /// - /// instances contain a list of metadata items - /// of arbitrary types. The metadata items are stored as an ordered collection with - /// items arranged in ascending order of precedence. - /// - public sealed class EndpointMetadataCollection : IReadOnlyList - { - /// - /// An empty . - /// - public static readonly EndpointMetadataCollection Empty = new EndpointMetadataCollection(Array.Empty()); + public static readonly EndpointMetadataCollection Empty = new EndpointMetadataCollection(Array.Empty()); - private readonly object[] _items; - private readonly ConcurrentDictionary _cache; + private readonly object[] _items; + private readonly ConcurrentDictionary _cache; - /// - /// Creates a new . - /// - /// The metadata items. - public EndpointMetadataCollection(IEnumerable items) - { - if (items == null) - { - throw new ArgumentNullException(nameof(items)); - } - - _items = items.ToArray(); - _cache = new ConcurrentDictionary(); - } - - /// - /// Creates a new . - /// - /// The metadata items. - public EndpointMetadataCollection(params object[] items) - : this((IEnumerable)items) + /// + /// Creates a new . + /// + /// The metadata items. + public EndpointMetadataCollection(IEnumerable items) + { + if (items == null) { + throw new ArgumentNullException(nameof(items)); } - /// - /// Gets the item at . - /// - /// The index of the item to retrieve. - /// The item at . - public object this[int index] => _items[index]; + _items = items.ToArray(); + _cache = new ConcurrentDictionary(); + } - /// - /// Gets the count of metadata items. - /// - public int Count => _items.Length; + /// + /// Creates a new . + /// + /// The metadata items. + public EndpointMetadataCollection(params object[] items) + : this((IEnumerable)items) + { + } - /// - /// Gets the most significant metadata item of type . - /// - /// The type of metadata to retrieve. - /// - /// The most significant metadata of type or null. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T? GetMetadata() where T : class - { - if (_cache.TryGetValue(typeof(T), out var obj)) - { - var result = (T[])obj; - var length = result.Length; - return length > 0 ? result[length - 1] : default; - } + /// + /// Gets the item at . + /// + /// The index of the item to retrieve. + /// The item at . + public object this[int index] => _items[index]; - return GetMetadataSlow(); - } + /// + /// Gets the count of metadata items. + /// + public int Count => _items.Length; - private T? GetMetadataSlow() where T : class + /// + /// Gets the most significant metadata item of type . + /// + /// The type of metadata to retrieve. + /// + /// The most significant metadata of type or null. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T? GetMetadata() where T : class + { + if (_cache.TryGetValue(typeof(T), out var obj)) { - var result = GetOrderedMetadataSlow(); + var result = (T[])obj; var length = result.Length; return length > 0 ? result[length - 1] : default; } - /// - /// Gets the metadata items of type in ascending - /// order of precedence. - /// - /// The type of metadata. - /// A sequence of metadata items of . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public IReadOnlyList GetOrderedMetadata() where T : class - { - if (_cache.TryGetValue(typeof(T), out var result)) - { - return (T[])result; - } + return GetMetadataSlow(); + } - return GetOrderedMetadataSlow(); - } + private T? GetMetadataSlow() where T : class + { + var result = GetOrderedMetadataSlow(); + var length = result.Length; + return length > 0 ? result[length - 1] : default; + } - private T[] GetOrderedMetadataSlow() where T : class + /// + /// Gets the metadata items of type in ascending + /// order of precedence. + /// + /// The type of metadata. + /// A sequence of metadata items of . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public IReadOnlyList GetOrderedMetadata() where T : class + { + if (_cache.TryGetValue(typeof(T), out var result)) { - // Perf: avoid allocations totally for the common case where there are no matching metadata. - List? matches = null; + return (T[])result; + } + + return GetOrderedMetadataSlow(); + } + + private T[] GetOrderedMetadataSlow() where T : class + { + // Perf: avoid allocations totally for the common case where there are no matching metadata. + List? matches = null; - var items = _items; - for (var i = 0; i < items.Length; i++) + var items = _items; + for (var i = 0; i < items.Length; i++) + { + if (items[i] is T item) { - if (items[i] is T item) - { - matches ??= new List(); - matches.Add(item); - } + matches ??= new List(); + matches.Add(item); } - - var results = matches == null ? Array.Empty() : matches.ToArray(); - _cache.TryAdd(typeof(T), results); - return results; } - /// - /// Gets an of all metadata items. - /// - /// An of all metadata items. - public Enumerator GetEnumerator() => new Enumerator(this); + var results = matches == null ? Array.Empty() : matches.ToArray(); + _cache.TryAdd(typeof(T), results); + return results; + } + + /// + /// Gets an of all metadata items. + /// + /// An of all metadata items. + public Enumerator GetEnumerator() => new Enumerator(this); + + /// + /// Gets an of all metadata items. + /// + /// An of all metadata items. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Gets an of all metadata items. + /// + /// An of all metadata items. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Enumerates the elements of an . + /// + public struct Enumerator : IEnumerator + { +#pragma warning disable IDE0044 + // Intentionally not readonly to prevent defensive struct copies + private object[] _items; +#pragma warning restore IDE0044 + private int _index; + + internal Enumerator(EndpointMetadataCollection collection) + { + _items = collection._items; + _index = 0; + Current = null; + } /// - /// Gets an of all metadata items. + /// Gets the element at the current position of the enumerator /// - /// An of all metadata items. - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public object? Current { get; private set; } /// - /// Gets an of all metadata items. + /// Releases all resources used by the . /// - /// An of all metadata items. - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + public void Dispose() + { + } /// - /// Enumerates the elements of an . + /// Advances the enumerator to the next element of the . /// - public struct Enumerator : IEnumerator + /// + /// true if the enumerator was successfully advanced to the next element; + /// false if the enumerator has passed the end of the collection. + /// + public bool MoveNext() { -#pragma warning disable IDE0044 - // Intentionally not readonly to prevent defensive struct copies - private object[] _items; -#pragma warning restore IDE0044 - private int _index; - - internal Enumerator(EndpointMetadataCollection collection) + if (_index < _items.Length) { - _items = collection._items; - _index = 0; - Current = null; + Current = _items[_index++]; + return true; } - /// - /// Gets the element at the current position of the enumerator - /// - public object? Current { get; private set; } - - /// - /// Releases all resources used by the . - /// - public void Dispose() - { - } - - /// - /// Advances the enumerator to the next element of the . - /// - /// - /// true if the enumerator was successfully advanced to the next element; - /// false if the enumerator has passed the end of the collection. - /// - public bool MoveNext() - { - if (_index < _items.Length) - { - Current = _items[_index++]; - return true; - } - - Current = null; - return false; - } + Current = null; + return false; + } - /// - /// Sets the enumerator to its initial position, which is before the first element in the collection. - /// - public void Reset() - { - _index = 0; - Current = null; - } + /// + /// Sets the enumerator to its initial position, which is before the first element in the collection. + /// + public void Reset() + { + _index = 0; + Current = null; } } } diff --git a/src/Http/Http.Abstractions/src/Routing/IEndpointFeature.cs b/src/Http/Http.Abstractions/src/Routing/IEndpointFeature.cs index c7e0da5019..64bd861f62 100644 --- a/src/Http/Http.Abstractions/src/Routing/IEndpointFeature.cs +++ b/src/Http/Http.Abstractions/src/Routing/IEndpointFeature.cs @@ -1,18 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// A feature interface for endpoint routing. Use +/// to access an instance associated with the current request. +/// +public interface IEndpointFeature { /// - /// A feature interface for endpoint routing. Use - /// to access an instance associated with the current request. + /// Gets or sets the selected for the current + /// request. /// - public interface IEndpointFeature - { - /// - /// Gets or sets the selected for the current - /// request. - /// - Endpoint? Endpoint { get; set; } - } + Endpoint? Endpoint { get; set; } } diff --git a/src/Http/Http.Abstractions/src/Routing/IRouteValuesFeature.cs b/src/Http/Http.Abstractions/src/Routing/IRouteValuesFeature.cs index 686c3b56c5..f03b818489 100644 --- a/src/Http/Http.Abstractions/src/Routing/IRouteValuesFeature.cs +++ b/src/Http/Http.Abstractions/src/Routing/IRouteValuesFeature.cs @@ -3,18 +3,17 @@ using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// A feature interface for routing values. Use +/// to access the values associated with the current request. +/// +public interface IRouteValuesFeature { /// - /// A feature interface for routing values. Use - /// to access the values associated with the current request. + /// Gets or sets the associated with the current + /// request. /// - public interface IRouteValuesFeature - { - /// - /// Gets or sets the associated with the current - /// request. - /// - RouteValueDictionary RouteValues { get; set; } - } + RouteValueDictionary RouteValues { get; set; } } diff --git a/src/Http/Http.Abstractions/src/Routing/RouteValueDictionary.cs b/src/Http/Http.Abstractions/src/Routing/RouteValueDictionary.cs index 37b92913e5..9ee9ab5548 100644 --- a/src/Http/Http.Abstractions/src/Routing/RouteValueDictionary.cs +++ b/src/Http/Http.Abstractions/src/Routing/RouteValueDictionary.cs @@ -11,775 +11,774 @@ using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Http.Abstractions; using Microsoft.Extensions.Internal; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// An type for route values. +/// +public class RouteValueDictionary : IDictionary, IReadOnlyDictionary { + // 4 is a good default capacity here because that leaves enough space for area/controller/action/id + private readonly int DefaultCapacity = 4; + + internal KeyValuePair[] _arrayStorage; + internal PropertyStorage? _propertyStorage; + private int _count; + /// - /// An type for route values. + /// Creates a new from the provided array. + /// The new instance will take ownership of the array, and may mutate it. /// - public class RouteValueDictionary : IDictionary, IReadOnlyDictionary + /// The items array. + /// A new . + public static RouteValueDictionary FromArray(KeyValuePair[] items) { - // 4 is a good default capacity here because that leaves enough space for area/controller/action/id - private readonly int DefaultCapacity = 4; + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } - internal KeyValuePair[] _arrayStorage; - internal PropertyStorage? _propertyStorage; - private int _count; + // We need to compress the array by removing non-contiguous items. We + // typically have a very small number of items to process. We don't need + // to preserve order. + var start = 0; + var end = items.Length - 1; - /// - /// Creates a new from the provided array. - /// The new instance will take ownership of the array, and may mutate it. - /// - /// The items array. - /// A new . - public static RouteValueDictionary FromArray(KeyValuePair[] items) + // We walk forwards from the beginning of the array and fill in 'null' slots. + // We walk backwards from the end of the array end move items in non-null' slots + // into whatever start is pointing to. O(n) + while (start <= end) { - if (items == null) + if (items[start].Key != null) { - throw new ArgumentNullException(nameof(items)); + start++; } - - // We need to compress the array by removing non-contiguous items. We - // typically have a very small number of items to process. We don't need - // to preserve order. - var start = 0; - var end = items.Length - 1; - - // We walk forwards from the beginning of the array and fill in 'null' slots. - // We walk backwards from the end of the array end move items in non-null' slots - // into whatever start is pointing to. O(n) - while (start <= end) + else if (items[end].Key != null) { - if (items[start].Key != null) - { - start++; - } - else if (items[end].Key != null) - { - // Swap this item into start and advance - items[start] = items[end]; - items[end] = default; - start++; - end--; - } - else - { - // Both null, we need to hold on 'start' since we - // still need to fill it with something. - end--; - } + // Swap this item into start and advance + items[start] = items[end]; + items[end] = default; + start++; + end--; } - - return new RouteValueDictionary() + else { - _arrayStorage = items!, - _count = start, - }; + // Both null, we need to hold on 'start' since we + // still need to fill it with something. + end--; + } } - /// - /// Creates an empty . - /// - public RouteValueDictionary() + return new RouteValueDictionary() { - _arrayStorage = Array.Empty>(); - } - - /// - /// Creates a initialized with the specified . - /// - /// An object to initialize the dictionary. The value can be of type - /// or - /// or an object with public properties as key-value pairs. - /// - /// - /// If the value is a dictionary or other of , - /// then its entries are copied. Otherwise the object is interpreted as a set of key-value pairs where the - /// property names are keys, and property values are the values, and copied into the dictionary. - /// Only public instance non-index properties are considered. - /// - public RouteValueDictionary(object? values) - { - if (values is RouteValueDictionary dictionary) - { - if (dictionary._propertyStorage != null) - { - // PropertyStorage is immutable so we can just copy it. - _propertyStorage = dictionary._propertyStorage; - _count = dictionary._count; - _arrayStorage = Array.Empty>(); - return; - } - - var count = dictionary._count; - if (count > 0) - { - var other = dictionary._arrayStorage; - var storage = new KeyValuePair[count]; - Array.Copy(other, 0, storage, 0, count); - _arrayStorage = storage; - _count = count; - } - else - { - _arrayStorage = Array.Empty>(); - } - - return; - } - - if (values is IEnumerable> keyValueEnumerable) - { - _arrayStorage = Array.Empty>(); - - foreach (var kvp in keyValueEnumerable) - { - Add(kvp.Key, kvp.Value); - } + _arrayStorage = items!, + _count = start, + }; + } - return; - } + /// + /// Creates an empty . + /// + public RouteValueDictionary() + { + _arrayStorage = Array.Empty>(); + } - if (values is IEnumerable> stringValueEnumerable) + /// + /// Creates a initialized with the specified . + /// + /// An object to initialize the dictionary. The value can be of type + /// or + /// or an object with public properties as key-value pairs. + /// + /// + /// If the value is a dictionary or other of , + /// then its entries are copied. Otherwise the object is interpreted as a set of key-value pairs where the + /// property names are keys, and property values are the values, and copied into the dictionary. + /// Only public instance non-index properties are considered. + /// + public RouteValueDictionary(object? values) + { + if (values is RouteValueDictionary dictionary) + { + if (dictionary._propertyStorage != null) { + // PropertyStorage is immutable so we can just copy it. + _propertyStorage = dictionary._propertyStorage; + _count = dictionary._count; _arrayStorage = Array.Empty>(); - - foreach (var kvp in stringValueEnumerable) - { - Add(kvp.Key, kvp.Value); - } - return; } - if (values != null) + var count = dictionary._count; + if (count > 0) { - var storage = new PropertyStorage(values); - _propertyStorage = storage; - _count = storage.Properties.Length; - _arrayStorage = Array.Empty>(); + var other = dictionary._arrayStorage; + var storage = new KeyValuePair[count]; + Array.Copy(other, 0, storage, 0, count); + _arrayStorage = storage; + _count = count; } else { _arrayStorage = Array.Empty>(); } + + return; } - /// - public object? this[string key] + if (values is IEnumerable> keyValueEnumerable) { - get - { - if (key == null) - { - ThrowArgumentNullExceptionForKey(); - } - - TryGetValue(key, out var value); - return value; - } + _arrayStorage = Array.Empty>(); - set + foreach (var kvp in keyValueEnumerable) { - if (key == null) - { - ThrowArgumentNullExceptionForKey(); - } - - // We're calling this here for the side-effect of converting from properties - // to array. We need to create the array even if we just set an existing value since - // property storage is immutable. - EnsureCapacity(_count); - - var index = FindIndex(key); - if (index < 0) - { - EnsureCapacity(_count + 1); - _arrayStorage[_count++] = new KeyValuePair(key, value); - } - else - { - _arrayStorage[index] = new KeyValuePair(key, value); - } + Add(kvp.Key, kvp.Value); } - } - - /// - /// Gets the comparer for this dictionary. - /// - /// - /// This will always be a reference to - /// - public IEqualityComparer Comparer => StringComparer.OrdinalIgnoreCase; - - /// - public int Count => _count; - /// - bool ICollection>.IsReadOnly => false; + return; + } - /// - public ICollection Keys + if (values is IEnumerable> stringValueEnumerable) { - get - { - EnsureCapacity(_count); - - var array = _arrayStorage; - var keys = new string[_count]; - for (var i = 0; i < keys.Length; i++) - { - keys[i] = array[i].Key; - } + _arrayStorage = Array.Empty>(); - return keys; + foreach (var kvp in stringValueEnumerable) + { + Add(kvp.Key, kvp.Value); } + + return; } - IEnumerable IReadOnlyDictionary.Keys => Keys; + if (values != null) + { + var storage = new PropertyStorage(values); + _propertyStorage = storage; + _count = storage.Properties.Length; + _arrayStorage = Array.Empty>(); + } + else + { + _arrayStorage = Array.Empty>(); + } + } - /// - public ICollection Values + /// + public object? this[string key] + { + get { - get + if (key == null) { - EnsureCapacity(_count); - - var array = _arrayStorage; - var values = new object?[_count]; - for (var i = 0; i < values.Length; i++) - { - values[i] = array[i].Value; - } - - return values; + ThrowArgumentNullExceptionForKey(); } - } - - IEnumerable IReadOnlyDictionary.Values => Values; - /// - void ICollection>.Add(KeyValuePair item) - { - Add(item.Key, item.Value); + TryGetValue(key, out var value); + return value; } - /// - public void Add(string key, object? value) + set { if (key == null) { ThrowArgumentNullExceptionForKey(); } - EnsureCapacity(_count + 1); + // We're calling this here for the side-effect of converting from properties + // to array. We need to create the array even if we just set an existing value since + // property storage is immutable. + EnsureCapacity(_count); - if (ContainsKeyArray(key)) + var index = FindIndex(key); + if (index < 0) { - var message = Resources.FormatRouteValueDictionary_DuplicateKey(key, nameof(RouteValueDictionary)); - throw new ArgumentException(message, nameof(key)); + EnsureCapacity(_count + 1); + _arrayStorage[_count++] = new KeyValuePair(key, value); + } + else + { + _arrayStorage[index] = new KeyValuePair(key, value); } - - _arrayStorage[_count] = new KeyValuePair(key, value); - _count++; } + } - /// - public void Clear() + /// + /// Gets the comparer for this dictionary. + /// + /// + /// This will always be a reference to + /// + public IEqualityComparer Comparer => StringComparer.OrdinalIgnoreCase; + + /// + public int Count => _count; + + /// + bool ICollection>.IsReadOnly => false; + + /// + public ICollection Keys + { + get { - if (_count == 0) - { - return; - } + EnsureCapacity(_count); - if (_propertyStorage != null) + var array = _arrayStorage; + var keys = new string[_count]; + for (var i = 0; i < keys.Length; i++) { - _arrayStorage = Array.Empty>(); - _propertyStorage = null; - _count = 0; - return; + keys[i] = array[i].Key; } - Array.Clear(_arrayStorage, 0, _count); - _count = 0; + return keys; } + } - /// - bool ICollection>.Contains(KeyValuePair item) - { - return TryGetValue(item.Key, out var value) && EqualityComparer.Default.Equals(value, item.Value); - } + IEnumerable IReadOnlyDictionary.Keys => Keys; - /// - public bool ContainsKey(string key) + /// + public ICollection Values + { + get { - if (key == null) + EnsureCapacity(_count); + + var array = _arrayStorage; + var values = new object?[_count]; + for (var i = 0; i < values.Length; i++) { - ThrowArgumentNullExceptionForKey(); + values[i] = array[i].Value; } - return ContainsKeyCore(key); + return values; } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool ContainsKeyCore(string key) + IEnumerable IReadOnlyDictionary.Values => Values; + + /// + void ICollection>.Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + /// + public void Add(string key, object? value) + { + if (key == null) { - if (_propertyStorage == null) - { - return ContainsKeyArray(key); - } + ThrowArgumentNullExceptionForKey(); + } + + EnsureCapacity(_count + 1); - return ContainsKeyProperties(key); + if (ContainsKeyArray(key)) + { + var message = Resources.FormatRouteValueDictionary_DuplicateKey(key, nameof(RouteValueDictionary)); + throw new ArgumentException(message, nameof(key)); } - /// - void ICollection>.CopyTo( - KeyValuePair[] array, - int arrayIndex) + _arrayStorage[_count] = new KeyValuePair(key, value); + _count++; + } + + /// + public void Clear() + { + if (_count == 0) { - if (array == null) - { - throw new ArgumentNullException(nameof(array)); - } + return; + } - if (arrayIndex < 0 || arrayIndex > array.Length || array.Length - arrayIndex < this.Count) - { - throw new ArgumentOutOfRangeException(nameof(arrayIndex)); - } + if (_propertyStorage != null) + { + _arrayStorage = Array.Empty>(); + _propertyStorage = null; + _count = 0; + return; + } - if (Count == 0) - { - return; - } + Array.Clear(_arrayStorage, 0, _count); + _count = 0; + } - EnsureCapacity(Count); + /// + bool ICollection>.Contains(KeyValuePair item) + { + return TryGetValue(item.Key, out var value) && EqualityComparer.Default.Equals(value, item.Value); + } - var storage = _arrayStorage; - Array.Copy(storage, 0, array, arrayIndex, _count); + /// + public bool ContainsKey(string key) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); } - /// - public Enumerator GetEnumerator() + return ContainsKeyCore(key); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ContainsKeyCore(string key) + { + if (_propertyStorage == null) { - return new Enumerator(this); + return ContainsKeyArray(key); } - /// - IEnumerator> IEnumerable>.GetEnumerator() + return ContainsKeyProperties(key); + } + + /// + void ICollection>.CopyTo( + KeyValuePair[] array, + int arrayIndex) + { + if (array == null) { - return GetEnumerator(); + throw new ArgumentNullException(nameof(array)); } - /// - IEnumerator IEnumerable.GetEnumerator() + if (arrayIndex < 0 || arrayIndex > array.Length || array.Length - arrayIndex < this.Count) { - return GetEnumerator(); + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); } - /// - bool ICollection>.Remove(KeyValuePair item) + if (Count == 0) { - if (Count == 0) - { - return false; - } + return; + } - Debug.Assert(_arrayStorage != null); + EnsureCapacity(Count); - EnsureCapacity(Count); + var storage = _arrayStorage; + Array.Copy(storage, 0, array, arrayIndex, _count); + } - var index = FindIndex(item.Key); - var array = _arrayStorage; - if (index >= 0 && EqualityComparer.Default.Equals(array[index].Value, item.Value)) - { - Array.Copy(array, index + 1, array, index, _count - index); - _count--; - array[_count] = default; - return true; - } + /// + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + /// + IEnumerator> IEnumerable>.GetEnumerator() + { + return GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + /// + bool ICollection>.Remove(KeyValuePair item) + { + if (Count == 0) + { return false; } - /// - public bool Remove(string key) - { - if (key == null) - { - ThrowArgumentNullExceptionForKey(); - } + Debug.Assert(_arrayStorage != null); - if (Count == 0) - { - return false; - } + EnsureCapacity(Count); - // Ensure property storage is converted to array storage as we'll be - // applying the lookup and removal on the array - EnsureCapacity(_count); + var index = FindIndex(item.Key); + var array = _arrayStorage; + if (index >= 0 && EqualityComparer.Default.Equals(array[index].Value, item.Value)) + { + Array.Copy(array, index + 1, array, index, _count - index); + _count--; + array[_count] = default; + return true; + } - var index = FindIndex(key); - if (index >= 0) - { - _count--; - var array = _arrayStorage; - Array.Copy(array, index + 1, array, index, _count - index); - array[_count] = default; + return false; + } - return true; - } + /// + public bool Remove(string key) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + if (Count == 0) + { return false; } - /// - /// Attempts to remove and return the value that has the specified key from the . - /// - /// The key of the element to remove and return. - /// When this method returns, contains the object removed from the , or null if key does not exist. - /// - /// true if the object was removed successfully; otherwise, false. - /// - public bool Remove(string key, out object? value) - { - if (key == null) - { - ThrowArgumentNullExceptionForKey(); - } + // Ensure property storage is converted to array storage as we'll be + // applying the lookup and removal on the array + EnsureCapacity(_count); - if (_count == 0) - { - value = default; - return false; - } + var index = FindIndex(key); + if (index >= 0) + { + _count--; + var array = _arrayStorage; + Array.Copy(array, index + 1, array, index, _count - index); + array[_count] = default; - // Ensure property storage is converted to array storage as we'll be - // applying the lookup and removal on the array - EnsureCapacity(_count); + return true; + } - var index = FindIndex(key); - if (index >= 0) - { - _count--; - var array = _arrayStorage; - value = array[index].Value; - Array.Copy(array, index + 1, array, index, _count - index); - array[_count] = default; + return false; + } - return true; - } + /// + /// Attempts to remove and return the value that has the specified key from the . + /// + /// The key of the element to remove and return. + /// When this method returns, contains the object removed from the , or null if key does not exist. + /// + /// true if the object was removed successfully; otherwise, false. + /// + public bool Remove(string key, out object? value) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); + } + if (_count == 0) + { value = default; return false; } - /// - /// Attempts to the add the provided and to the dictionary. - /// - /// The key. - /// The value. - /// Returns true if the value was added. Returns false if the key was already present. - public bool TryAdd(string key, object? value) - { - if (key == null) - { - ThrowArgumentNullExceptionForKey(); - } + // Ensure property storage is converted to array storage as we'll be + // applying the lookup and removal on the array + EnsureCapacity(_count); - if (ContainsKeyCore(key)) - { - return false; - } + var index = FindIndex(key); + if (index >= 0) + { + _count--; + var array = _arrayStorage; + value = array[index].Value; + Array.Copy(array, index + 1, array, index, _count - index); + array[_count] = default; - EnsureCapacity(Count + 1); - _arrayStorage[Count] = new KeyValuePair(key, value); - _count++; return true; } - /// - public bool TryGetValue(string key, out object? value) + value = default; + return false; + } + + /// + /// Attempts to the add the provided and to the dictionary. + /// + /// The key. + /// The value. + /// Returns true if the value was added. Returns false if the key was already present. + public bool TryAdd(string key, object? value) + { + if (key == null) { - if (key == null) - { - ThrowArgumentNullExceptionForKey(); - } + ThrowArgumentNullExceptionForKey(); + } - if (_propertyStorage == null) - { - return TryFindItem(key, out value); - } + if (ContainsKeyCore(key)) + { + return false; + } + + EnsureCapacity(Count + 1); + _arrayStorage[Count] = new KeyValuePair(key, value); + _count++; + return true; + } - return TryGetValueSlow(key, out value); + /// + public bool TryGetValue(string key, out object? value) + { + if (key == null) + { + ThrowArgumentNullExceptionForKey(); } - private bool TryGetValueSlow(string key, out object? value) + if (_propertyStorage == null) { - if (_propertyStorage != null) + return TryFindItem(key, out value); + } + + return TryGetValueSlow(key, out value); + } + + private bool TryGetValueSlow(string key, out object? value) + { + if (_propertyStorage != null) + { + var storage = _propertyStorage; + for (var i = 0; i < storage.Properties.Length; i++) { - var storage = _propertyStorage; - for (var i = 0; i < storage.Properties.Length; i++) + if (string.Equals(storage.Properties[i].Name, key, StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(storage.Properties[i].Name, key, StringComparison.OrdinalIgnoreCase)) - { - value = storage.Properties[i].GetValue(storage.Value); - return true; - } + value = storage.Properties[i].GetValue(storage.Value); + return true; } } - - value = default; - return false; } - [DoesNotReturn] - private static void ThrowArgumentNullExceptionForKey() + value = default; + return false; + } + + [DoesNotReturn] + private static void ThrowArgumentNullExceptionForKey() + { + throw new ArgumentNullException("key"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacity(int capacity) + { + if (_propertyStorage != null || _arrayStorage.Length < capacity) { - throw new ArgumentNullException("key"); + EnsureCapacitySlow(capacity); } + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void EnsureCapacity(int capacity) + private void EnsureCapacitySlow(int capacity) + { + if (_propertyStorage != null) { - if (_propertyStorage != null || _arrayStorage.Length < capacity) + var storage = _propertyStorage; + + // If we're converting from properties, it's likely due to an 'add' to make sure we have at least + // the default amount of space. + capacity = Math.Max(DefaultCapacity, Math.Max(storage.Properties.Length, capacity)); + var array = new KeyValuePair[capacity]; + + for (var i = 0; i < storage.Properties.Length; i++) { - EnsureCapacitySlow(capacity); + var property = storage.Properties[i]; + array[i] = new KeyValuePair(property.Name, property.GetValue(storage.Value)); } + + _arrayStorage = array; + _propertyStorage = null; + return; } - private void EnsureCapacitySlow(int capacity) + if (_arrayStorage.Length < capacity) { - if (_propertyStorage != null) + capacity = _arrayStorage.Length == 0 ? DefaultCapacity : _arrayStorage.Length * 2; + var array = new KeyValuePair[capacity]; + if (_count > 0) { - var storage = _propertyStorage; + Array.Copy(_arrayStorage, 0, array, 0, _count); + } - // If we're converting from properties, it's likely due to an 'add' to make sure we have at least - // the default amount of space. - capacity = Math.Max(DefaultCapacity, Math.Max(storage.Properties.Length, capacity)); - var array = new KeyValuePair[capacity]; + _arrayStorage = array; + } + } - for (var i = 0; i < storage.Properties.Length; i++) - { - var property = storage.Properties[i]; - array[i] = new KeyValuePair(property.Name, property.GetValue(storage.Value)); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int FindIndex(string key) + { + // Generally the bounds checking here will be elided by the JIT because this will be called + // on the same code path as EnsureCapacity. + var array = _arrayStorage; + var count = _count; - _arrayStorage = array; - _propertyStorage = null; - return; + for (var i = 0; i < count; i++) + { + if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase)) + { + return i; } + } + + return -1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool TryFindItem(string key, out object? value) + { + var array = _arrayStorage; + var count = _count; - if (_arrayStorage.Length < capacity) + // Elide bounds check for indexing. + if ((uint)count <= (uint)array.Length) + { + for (var i = 0; i < count; i++) { - capacity = _arrayStorage.Length == 0 ? DefaultCapacity : _arrayStorage.Length * 2; - var array = new KeyValuePair[capacity]; - if (_count > 0) + if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase)) { - Array.Copy(_arrayStorage, 0, array, 0, _count); + value = array[i].Value; + return true; } - - _arrayStorage = array; } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int FindIndex(string key) - { - // Generally the bounds checking here will be elided by the JIT because this will be called - // on the same code path as EnsureCapacity. - var array = _arrayStorage; - var count = _count; + value = null; + return false; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ContainsKeyArray(string key) + { + var array = _arrayStorage; + var count = _count; + + // Elide bounds check for indexing. + if ((uint)count <= (uint)array.Length) + { for (var i = 0; i < count; i++) { if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase)) { - return i; + return true; } } - - return -1; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool TryFindItem(string key, out object? value) - { - var array = _arrayStorage; - var count = _count; + return false; + } - // Elide bounds check for indexing. - if ((uint)count <= (uint)array.Length) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ContainsKeyProperties(string key) + { + Debug.Assert(_propertyStorage != null); + + var properties = _propertyStorage.Properties; + for (var i = 0; i < properties.Length; i++) + { + if (string.Equals(properties[i].Name, key, StringComparison.OrdinalIgnoreCase)) { - for (var i = 0; i < count; i++) - { - if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase)) - { - value = array[i].Value; - return true; - } - } + return true; } - - value = null; - return false; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool ContainsKeyArray(string key) - { - var array = _arrayStorage; - var count = _count; + return false; + } + + /// + public struct Enumerator : IEnumerator> + { + private readonly RouteValueDictionary _dictionary; + private int _index; - // Elide bounds check for indexing. - if ((uint)count <= (uint)array.Length) + /// + /// Instantiates a new enumerator with the values provided in . + /// + /// A . + public Enumerator(RouteValueDictionary dictionary) + { + if (dictionary == null) { - for (var i = 0; i < count; i++) - { - if (string.Equals(array[i].Key, key, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } + throw new ArgumentNullException(nameof(dictionary)); } - return false; + _dictionary = dictionary; + + Current = default; + _index = 0; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool ContainsKeyProperties(string key) - { - Debug.Assert(_propertyStorage != null); + /// + public KeyValuePair Current { get; private set; } - var properties = _propertyStorage.Properties; - for (var i = 0; i < properties.Length; i++) - { - if (string.Equals(properties[i].Name, key, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } + object IEnumerator.Current => Current; - return false; + /// + /// Releases resources used by the . + /// + public void Dispose() + { } + // Similar to the design of List.Enumerator - Split into fast path and slow path for inlining friendliness /// - public struct Enumerator : IEnumerator> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool MoveNext() { - private readonly RouteValueDictionary _dictionary; - private int _index; + var dictionary = _dictionary; - /// - /// Instantiates a new enumerator with the values provided in . - /// - /// A . - public Enumerator(RouteValueDictionary dictionary) + // The uncommon case is that the propertyStorage is in use + if (dictionary._propertyStorage == null && ((uint)_index < (uint)dictionary._count)) { - if (dictionary == null) - { - throw new ArgumentNullException(nameof(dictionary)); - } - - _dictionary = dictionary; - - Current = default; - _index = 0; + Current = dictionary._arrayStorage[_index]; + _index++; + return true; } - /// - public KeyValuePair Current { get; private set; } - - object IEnumerator.Current => Current; + return MoveNextRare(); + } - /// - /// Releases resources used by the . - /// - public void Dispose() + private bool MoveNextRare() + { + var dictionary = _dictionary; + if (dictionary._propertyStorage != null && ((uint)_index < (uint)dictionary._count)) { + var storage = dictionary._propertyStorage; + var property = storage.Properties[_index]; + Current = new KeyValuePair(property.Name, property.GetValue(storage.Value)); + _index++; + return true; } - // Similar to the design of List.Enumerator - Split into fast path and slow path for inlining friendliness - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool MoveNext() - { - var dictionary = _dictionary; + _index = dictionary._count; + Current = default; + return false; + } - // The uncommon case is that the propertyStorage is in use - if (dictionary._propertyStorage == null && ((uint)_index < (uint)dictionary._count)) - { - Current = dictionary._arrayStorage[_index]; - _index++; - return true; - } + /// + public void Reset() + { + Current = default; + _index = 0; + } + } - return MoveNextRare(); - } + internal class PropertyStorage + { + private static readonly ConcurrentDictionary _propertyCache = new ConcurrentDictionary(); - private bool MoveNextRare() - { - var dictionary = _dictionary; - if (dictionary._propertyStorage != null && ((uint)_index < (uint)dictionary._count)) - { - var storage = dictionary._propertyStorage; - var property = storage.Properties[_index]; - Current = new KeyValuePair(property.Name, property.GetValue(storage.Value)); - _index++; - return true; - } + public readonly object Value; + public readonly PropertyHelper[] Properties; - _index = dictionary._count; - Current = default; - return false; - } + public PropertyStorage(object value) + { + Debug.Assert(value != null); + Value = value; - /// - public void Reset() + // Cache the properties so we can know if we've already validated them for duplicates. + var type = Value.GetType(); + if (!_propertyCache.TryGetValue(type, out Properties!)) { - Current = default; - _index = 0; + Properties = PropertyHelper.GetVisibleProperties(type); + ValidatePropertyNames(type, Properties); + _propertyCache.TryAdd(type, Properties); } } - internal class PropertyStorage + private static void ValidatePropertyNames(Type type, PropertyHelper[] properties) { - private static readonly ConcurrentDictionary _propertyCache = new ConcurrentDictionary(); - - public readonly object Value; - public readonly PropertyHelper[] Properties; - - public PropertyStorage(object value) + var names = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < properties.Length; i++) { - Debug.Assert(value != null); - Value = value; + var property = properties[i]; - // Cache the properties so we can know if we've already validated them for duplicates. - var type = Value.GetType(); - if (!_propertyCache.TryGetValue(type, out Properties!)) + if (names.TryGetValue(property.Name, out var duplicate)) { - Properties = PropertyHelper.GetVisibleProperties(type); - ValidatePropertyNames(type, Properties); - _propertyCache.TryAdd(type, Properties); + var message = Resources.FormatRouteValueDictionary_DuplicatePropertyName( + type.FullName, + property.Name, + duplicate.Name, + nameof(RouteValueDictionary)); + throw new InvalidOperationException(message); } - } - private static void ValidatePropertyNames(Type type, PropertyHelper[] properties) - { - var names = new Dictionary(StringComparer.OrdinalIgnoreCase); - for (var i = 0; i < properties.Length; i++) - { - var property = properties[i]; - - if (names.TryGetValue(property.Name, out var duplicate)) - { - var message = Resources.FormatRouteValueDictionary_DuplicatePropertyName( - type.FullName, - property.Name, - duplicate.Name, - nameof(RouteValueDictionary)); - throw new InvalidOperationException(message); - } - - names.Add(property.Name, property); - } + names.Add(property.Name, property); } } } diff --git a/src/Http/Http.Abstractions/src/StatusCodes.cs b/src/Http/Http.Abstractions/src/StatusCodes.cs index 99a47f9f22..d24da21a4b 100644 --- a/src/Http/Http.Abstractions/src/StatusCodes.cs +++ b/src/Http/Http.Abstractions/src/StatusCodes.cs @@ -1,340 +1,339 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// A collection of constants for HTTP status codes. +/// +/// Status Codes listed at http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml +/// +public static class StatusCodes { /// - /// A collection of constants for HTTP status codes. - /// - /// Status Codes listed at http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml - /// - public static class StatusCodes - { - /// - /// HTTP status code 100. - /// - public const int Status100Continue = 100; - - /// - /// HTTP status code 101. - /// - public const int Status101SwitchingProtocols = 101; - - /// - /// HTTP status code 102. - /// - public const int Status102Processing = 102; - - /// - /// HTTP status code 200. - /// - public const int Status200OK = 200; - - /// - /// HTTP status code 201. - /// - public const int Status201Created = 201; - - /// - /// HTTP status code 202. - /// - public const int Status202Accepted = 202; - - /// - /// HTTP status code 203. - /// - public const int Status203NonAuthoritative = 203; - - /// - /// HTTP status code 204. - /// - public const int Status204NoContent = 204; - - /// - /// HTTP status code 205. - /// - public const int Status205ResetContent = 205; - - /// - /// HTTP status code 206. - /// - public const int Status206PartialContent = 206; - - /// - /// HTTP status code 207. - /// - public const int Status207MultiStatus = 207; - - /// - /// HTTP status code 208. - /// - public const int Status208AlreadyReported = 208; - - /// - /// HTTP status code 226. - /// - public const int Status226IMUsed = 226; - - /// - /// HTTP status code 300. - /// - public const int Status300MultipleChoices = 300; - - /// - /// HTTP status code 301. - /// - public const int Status301MovedPermanently = 301; - - /// - /// HTTP status code 302. - /// - public const int Status302Found = 302; - - /// - /// HTTP status code 303. - /// - public const int Status303SeeOther = 303; - - /// - /// HTTP status code 304. - /// - public const int Status304NotModified = 304; - - /// - /// HTTP status code 305. - /// - public const int Status305UseProxy = 305; - - /// - /// HTTP status code 306. - /// - public const int Status306SwitchProxy = 306; // RFC 2616, removed - - /// - /// HTTP status code 307. - /// - public const int Status307TemporaryRedirect = 307; - - /// - /// HTTP status code 308. - /// - public const int Status308PermanentRedirect = 308; - - /// - /// HTTP status code 400. - /// - - public const int Status400BadRequest = 400; - - /// - /// HTTP status code 401. - /// - public const int Status401Unauthorized = 401; - - /// - /// HTTP status code 402. - /// - public const int Status402PaymentRequired = 402; - - /// - /// HTTP status code 403. - /// - public const int Status403Forbidden = 403; - - /// - /// HTTP status code 404. - /// - public const int Status404NotFound = 404; - - /// - /// HTTP status code 405. - /// - public const int Status405MethodNotAllowed = 405; - - /// - /// HTTP status code 406. - /// - public const int Status406NotAcceptable = 406; - - /// - /// HTTP status code 407. - /// - public const int Status407ProxyAuthenticationRequired = 407; - - /// - /// HTTP status code 408. - /// - public const int Status408RequestTimeout = 408; - - /// - /// HTTP status code 409. - /// - public const int Status409Conflict = 409; - - /// - /// HTTP status code 410. - /// - public const int Status410Gone = 410; - - /// - /// HTTP status code 411. - /// - public const int Status411LengthRequired = 411; - - /// - /// HTTP status code 412. - /// - public const int Status412PreconditionFailed = 412; - - /// - /// HTTP status code 413. - /// - public const int Status413RequestEntityTooLarge = 413; // RFC 2616, renamed - - /// - /// HTTP status code 413. - /// - public const int Status413PayloadTooLarge = 413; // RFC 7231 - - /// - /// HTTP status code 414. - /// - public const int Status414RequestUriTooLong = 414; // RFC 2616, renamed - - /// - /// HTTP status code 414. - /// - public const int Status414UriTooLong = 414; // RFC 7231 - - /// - /// HTTP status code 415. - /// - public const int Status415UnsupportedMediaType = 415; - - /// - /// HTTP status code 416. - /// - public const int Status416RequestedRangeNotSatisfiable = 416; // RFC 2616, renamed - - /// - /// HTTP status code 416. - /// - public const int Status416RangeNotSatisfiable = 416; // RFC 7233 - - /// - /// HTTP status code 417. - /// - public const int Status417ExpectationFailed = 417; - - /// - /// HTTP status code 418. - /// - public const int Status418ImATeapot = 418; - - /// - /// HTTP status code 419. - /// - public const int Status419AuthenticationTimeout = 419; // Not defined in any RFC - - /// - /// HTTP status code 422. - /// - public const int Status421MisdirectedRequest = 421; - - /// - /// HTTP status code 422. - /// - public const int Status422UnprocessableEntity = 422; - - /// - /// HTTP status code 423. - /// - public const int Status423Locked = 423; - - /// - /// HTTP status code 424. - /// - public const int Status424FailedDependency = 424; - - /// - /// HTTP status code 426. - /// - public const int Status426UpgradeRequired = 426; - - /// - /// HTTP status code 428. - /// - public const int Status428PreconditionRequired = 428; - - /// - /// HTTP status code 429. - /// - public const int Status429TooManyRequests = 429; - - /// - /// HTTP status code 431. - /// - public const int Status431RequestHeaderFieldsTooLarge = 431; - - /// - /// HTTP status code 451. - /// - public const int Status451UnavailableForLegalReasons = 451; - - /// - /// HTTP status code 500. - /// - - public const int Status500InternalServerError = 500; - - /// - /// HTTP status code 501. - /// - public const int Status501NotImplemented = 501; - - /// - /// HTTP status code 502. - /// - public const int Status502BadGateway = 502; - - /// - /// HTTP status code 503. - /// - public const int Status503ServiceUnavailable = 503; - - /// - /// HTTP status code 504. - /// - public const int Status504GatewayTimeout = 504; - - /// - /// HTTP status code 505. - /// - public const int Status505HttpVersionNotsupported = 505; - - /// - /// HTTP status code 506. - /// - public const int Status506VariantAlsoNegotiates = 506; - - /// - /// HTTP status code 507. - /// - public const int Status507InsufficientStorage = 507; - - /// - /// HTTP status code 508. - /// - public const int Status508LoopDetected = 508; - - /// - /// HTTP status code 510. - /// - public const int Status510NotExtended = 510; - - /// - /// HTTP status code 511. - /// - public const int Status511NetworkAuthenticationRequired = 511; - } + /// HTTP status code 100. + /// + public const int Status100Continue = 100; + + /// + /// HTTP status code 101. + /// + public const int Status101SwitchingProtocols = 101; + + /// + /// HTTP status code 102. + /// + public const int Status102Processing = 102; + + /// + /// HTTP status code 200. + /// + public const int Status200OK = 200; + + /// + /// HTTP status code 201. + /// + public const int Status201Created = 201; + + /// + /// HTTP status code 202. + /// + public const int Status202Accepted = 202; + + /// + /// HTTP status code 203. + /// + public const int Status203NonAuthoritative = 203; + + /// + /// HTTP status code 204. + /// + public const int Status204NoContent = 204; + + /// + /// HTTP status code 205. + /// + public const int Status205ResetContent = 205; + + /// + /// HTTP status code 206. + /// + public const int Status206PartialContent = 206; + + /// + /// HTTP status code 207. + /// + public const int Status207MultiStatus = 207; + + /// + /// HTTP status code 208. + /// + public const int Status208AlreadyReported = 208; + + /// + /// HTTP status code 226. + /// + public const int Status226IMUsed = 226; + + /// + /// HTTP status code 300. + /// + public const int Status300MultipleChoices = 300; + + /// + /// HTTP status code 301. + /// + public const int Status301MovedPermanently = 301; + + /// + /// HTTP status code 302. + /// + public const int Status302Found = 302; + + /// + /// HTTP status code 303. + /// + public const int Status303SeeOther = 303; + + /// + /// HTTP status code 304. + /// + public const int Status304NotModified = 304; + + /// + /// HTTP status code 305. + /// + public const int Status305UseProxy = 305; + + /// + /// HTTP status code 306. + /// + public const int Status306SwitchProxy = 306; // RFC 2616, removed + + /// + /// HTTP status code 307. + /// + public const int Status307TemporaryRedirect = 307; + + /// + /// HTTP status code 308. + /// + public const int Status308PermanentRedirect = 308; + + /// + /// HTTP status code 400. + /// + + public const int Status400BadRequest = 400; + + /// + /// HTTP status code 401. + /// + public const int Status401Unauthorized = 401; + + /// + /// HTTP status code 402. + /// + public const int Status402PaymentRequired = 402; + + /// + /// HTTP status code 403. + /// + public const int Status403Forbidden = 403; + + /// + /// HTTP status code 404. + /// + public const int Status404NotFound = 404; + + /// + /// HTTP status code 405. + /// + public const int Status405MethodNotAllowed = 405; + + /// + /// HTTP status code 406. + /// + public const int Status406NotAcceptable = 406; + + /// + /// HTTP status code 407. + /// + public const int Status407ProxyAuthenticationRequired = 407; + + /// + /// HTTP status code 408. + /// + public const int Status408RequestTimeout = 408; + + /// + /// HTTP status code 409. + /// + public const int Status409Conflict = 409; + + /// + /// HTTP status code 410. + /// + public const int Status410Gone = 410; + + /// + /// HTTP status code 411. + /// + public const int Status411LengthRequired = 411; + + /// + /// HTTP status code 412. + /// + public const int Status412PreconditionFailed = 412; + + /// + /// HTTP status code 413. + /// + public const int Status413RequestEntityTooLarge = 413; // RFC 2616, renamed + + /// + /// HTTP status code 413. + /// + public const int Status413PayloadTooLarge = 413; // RFC 7231 + + /// + /// HTTP status code 414. + /// + public const int Status414RequestUriTooLong = 414; // RFC 2616, renamed + + /// + /// HTTP status code 414. + /// + public const int Status414UriTooLong = 414; // RFC 7231 + + /// + /// HTTP status code 415. + /// + public const int Status415UnsupportedMediaType = 415; + + /// + /// HTTP status code 416. + /// + public const int Status416RequestedRangeNotSatisfiable = 416; // RFC 2616, renamed + + /// + /// HTTP status code 416. + /// + public const int Status416RangeNotSatisfiable = 416; // RFC 7233 + + /// + /// HTTP status code 417. + /// + public const int Status417ExpectationFailed = 417; + + /// + /// HTTP status code 418. + /// + public const int Status418ImATeapot = 418; + + /// + /// HTTP status code 419. + /// + public const int Status419AuthenticationTimeout = 419; // Not defined in any RFC + + /// + /// HTTP status code 422. + /// + public const int Status421MisdirectedRequest = 421; + + /// + /// HTTP status code 422. + /// + public const int Status422UnprocessableEntity = 422; + + /// + /// HTTP status code 423. + /// + public const int Status423Locked = 423; + + /// + /// HTTP status code 424. + /// + public const int Status424FailedDependency = 424; + + /// + /// HTTP status code 426. + /// + public const int Status426UpgradeRequired = 426; + + /// + /// HTTP status code 428. + /// + public const int Status428PreconditionRequired = 428; + + /// + /// HTTP status code 429. + /// + public const int Status429TooManyRequests = 429; + + /// + /// HTTP status code 431. + /// + public const int Status431RequestHeaderFieldsTooLarge = 431; + + /// + /// HTTP status code 451. + /// + public const int Status451UnavailableForLegalReasons = 451; + + /// + /// HTTP status code 500. + /// + + public const int Status500InternalServerError = 500; + + /// + /// HTTP status code 501. + /// + public const int Status501NotImplemented = 501; + + /// + /// HTTP status code 502. + /// + public const int Status502BadGateway = 502; + + /// + /// HTTP status code 503. + /// + public const int Status503ServiceUnavailable = 503; + + /// + /// HTTP status code 504. + /// + public const int Status504GatewayTimeout = 504; + + /// + /// HTTP status code 505. + /// + public const int Status505HttpVersionNotsupported = 505; + + /// + /// HTTP status code 506. + /// + public const int Status506VariantAlsoNegotiates = 506; + + /// + /// HTTP status code 507. + /// + public const int Status507InsufficientStorage = 507; + + /// + /// HTTP status code 508. + /// + public const int Status508LoopDetected = 508; + + /// + /// HTTP status code 510. + /// + public const int Status510NotExtended = 510; + + /// + /// HTTP status code 511. + /// + public const int Status511NetworkAuthenticationRequired = 511; } diff --git a/src/Http/Http.Abstractions/src/WebSocketManager.cs b/src/Http/Http.Abstractions/src/WebSocketManager.cs index 14725d698d..6f1f76a467 100644 --- a/src/Http/Http.Abstractions/src/WebSocketManager.cs +++ b/src/Http/Http.Abstractions/src/WebSocketManager.cs @@ -6,44 +6,43 @@ using System.Collections.Generic; using System.Net.WebSockets; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Manages the establishment of WebSocket connections for a specific HTTP request. +/// +public abstract class WebSocketManager { /// - /// Manages the establishment of WebSocket connections for a specific HTTP request. + /// Gets a value indicating whether the request is a WebSocket establishment request. /// - public abstract class WebSocketManager - { - /// - /// Gets a value indicating whether the request is a WebSocket establishment request. - /// - public abstract bool IsWebSocketRequest { get; } + public abstract bool IsWebSocketRequest { get; } - /// - /// Gets the list of requested WebSocket sub-protocols. - /// - public abstract IList WebSocketRequestedProtocols { get; } + /// + /// Gets the list of requested WebSocket sub-protocols. + /// + public abstract IList WebSocketRequestedProtocols { get; } - /// - /// Transitions the request to a WebSocket connection. - /// - /// A task representing the completion of the transition. - public virtual Task AcceptWebSocketAsync() - { - return AcceptWebSocketAsync(subProtocol: null); - } + /// + /// Transitions the request to a WebSocket connection. + /// + /// A task representing the completion of the transition. + public virtual Task AcceptWebSocketAsync() + { + return AcceptWebSocketAsync(subProtocol: null); + } - /// - /// Transitions the request to a WebSocket connection using the specified sub-protocol. - /// - /// The sub-protocol to use. - /// A task representing the completion of the transition. - public abstract Task AcceptWebSocketAsync(string? subProtocol); + /// + /// Transitions the request to a WebSocket connection using the specified sub-protocol. + /// + /// The sub-protocol to use. + /// A task representing the completion of the transition. + public abstract Task AcceptWebSocketAsync(string? subProtocol); - /// - /// - /// - /// - /// - public virtual Task AcceptWebSocketAsync(WebSocketAcceptContext acceptContext) => throw new NotImplementedException(); - } + /// + /// + /// + /// + /// + public virtual Task AcceptWebSocketAsync(WebSocketAcceptContext acceptContext) => throw new NotImplementedException(); } diff --git a/src/Http/Http.Abstractions/test/CookieBuilderTests.cs b/src/Http/Http.Abstractions/test/CookieBuilderTests.cs index 6b545c18dd..fa5c8b5f9d 100644 --- a/src/Http/Http.Abstractions/test/CookieBuilderTests.cs +++ b/src/Http/Http.Abstractions/test/CookieBuilderTests.cs @@ -4,54 +4,53 @@ using System; using Xunit; -namespace Microsoft.AspNetCore.Http.Abstractions.Tests +namespace Microsoft.AspNetCore.Http.Abstractions.Tests; + +public class CookieBuilderTests { - public class CookieBuilderTests + [Theory] + [InlineData(CookieSecurePolicy.Always, false, true)] + [InlineData(CookieSecurePolicy.Always, true, true)] + [InlineData(CookieSecurePolicy.SameAsRequest, true, true)] + [InlineData(CookieSecurePolicy.SameAsRequest, false, false)] + [InlineData(CookieSecurePolicy.None, true, false)] + [InlineData(CookieSecurePolicy.None, false, false)] + public void ConfiguresSecurePolicy(CookieSecurePolicy policy, bool requestIsHttps, bool secure) { - [Theory] - [InlineData(CookieSecurePolicy.Always, false, true)] - [InlineData(CookieSecurePolicy.Always, true, true)] - [InlineData(CookieSecurePolicy.SameAsRequest, true, true)] - [InlineData(CookieSecurePolicy.SameAsRequest, false, false)] - [InlineData(CookieSecurePolicy.None, true, false)] - [InlineData(CookieSecurePolicy.None, false, false)] - public void ConfiguresSecurePolicy(CookieSecurePolicy policy, bool requestIsHttps, bool secure) - { - var builder = new CookieBuilder - { - SecurePolicy = policy - }; - var context = new DefaultHttpContext(); - context.Request.IsHttps = requestIsHttps; - var options = builder.Build(context); - - Assert.Equal(secure, options.Secure); - } - - [Fact] - public void ComputesExpiration() + var builder = new CookieBuilder { - Assert.Null(new CookieBuilder().Build(new DefaultHttpContext()).Expires); + SecurePolicy = policy + }; + var context = new DefaultHttpContext(); + context.Request.IsHttps = requestIsHttps; + var options = builder.Build(context); + + Assert.Equal(secure, options.Secure); + } - var now = DateTimeOffset.Now; - var options = new CookieBuilder { Expiration = TimeSpan.FromHours(1) }.Build(new DefaultHttpContext(), now); - Assert.Equal(now.AddHours(1), options.Expires); - } + [Fact] + public void ComputesExpiration() + { + Assert.Null(new CookieBuilder().Build(new DefaultHttpContext()).Expires); - [Fact] - public void ComputesMaxAge() - { - Assert.Null(new CookieBuilder().Build(new DefaultHttpContext()).MaxAge); + var now = DateTimeOffset.Now; + var options = new CookieBuilder { Expiration = TimeSpan.FromHours(1) }.Build(new DefaultHttpContext(), now); + Assert.Equal(now.AddHours(1), options.Expires); + } - var now = TimeSpan.FromHours(1); - var options = new CookieBuilder { MaxAge = now }.Build(new DefaultHttpContext()); - Assert.Equal(now, options.MaxAge); - } + [Fact] + public void ComputesMaxAge() + { + Assert.Null(new CookieBuilder().Build(new DefaultHttpContext()).MaxAge); - [Fact] - public void CookieBuilderPreservesDefaultPath() - { - Assert.Equal(new CookieOptions().Path, new CookieBuilder().Build(new DefaultHttpContext()).Path); - } + var now = TimeSpan.FromHours(1); + var options = new CookieBuilder { MaxAge = now }.Build(new DefaultHttpContext()); + Assert.Equal(now, options.MaxAge); + } + + [Fact] + public void CookieBuilderPreservesDefaultPath() + { + Assert.Equal(new CookieOptions().Path, new CookieBuilder().Build(new DefaultHttpContext()).Path); } } diff --git a/src/Http/Http.Abstractions/test/EndpointHttpContextExtensionsTests.cs b/src/Http/Http.Abstractions/test/EndpointHttpContextExtensionsTests.cs index 247a9e880b..fd742aa6e0 100644 --- a/src/Http/Http.Abstractions/test/EndpointHttpContextExtensionsTests.cs +++ b/src/Http/Http.Abstractions/test/EndpointHttpContextExtensionsTests.cs @@ -8,148 +8,147 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; using Xunit; -namespace Microsoft.AspNetCore.Http.Abstractions.Tests +namespace Microsoft.AspNetCore.Http.Abstractions.Tests; + +public class EndpointHttpContextExtensionsTests { - public class EndpointHttpContextExtensionsTests + [Fact] + public void GetEndpoint_ContextWithoutFeature_ReturnsNull() { - [Fact] - public void GetEndpoint_ContextWithoutFeature_ReturnsNull() - { - // Arrange - var context = new DefaultHttpContext(); + // Arrange + var context = new DefaultHttpContext(); - // Act - var endpoint = context.GetEndpoint(); + // Act + var endpoint = context.GetEndpoint(); - // Assert - Assert.Null(endpoint); - } + // Assert + Assert.Null(endpoint); + } - [Fact] - public void GetEndpoint_ContextWithFeatureAndNullEndpoint_ReturnsNull() - { - // Arrange - var context = new DefaultHttpContext(); - context.Features.Set(new EndpointFeature - { - Endpoint = null - }); - - // Act - var endpoint = context.GetEndpoint(); - - // Assert - Assert.Null(endpoint); - } - - [Fact] - public void GetEndpoint_ContextWithFeatureAndEndpoint_ReturnsEndpoint() - { - // Arrange - var context = new DefaultHttpContext(); - var initial = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); - context.Features.Set(new EndpointFeature - { - Endpoint = initial - }); - - // Act - var endpoint = context.GetEndpoint(); - - // Assert - Assert.Equal(initial, endpoint); - } - - [Fact] - public void SetEndpoint_NullOnContextWithoutFeature_NoFeatureSet() + [Fact] + public void GetEndpoint_ContextWithFeatureAndNullEndpoint_ReturnsNull() + { + // Arrange + var context = new DefaultHttpContext(); + context.Features.Set(new EndpointFeature { - // Arrange - var context = new DefaultHttpContext(); + Endpoint = null + }); - // Act - context.SetEndpoint(null); + // Act + var endpoint = context.GetEndpoint(); - // Assert - Assert.Null(context.Features.Get()); - } + // Assert + Assert.Null(endpoint); + } - [Fact] - public void SetEndpoint_EndpointOnContextWithoutFeature_FeatureWithEndpointSet() + [Fact] + public void GetEndpoint_ContextWithFeatureAndEndpoint_ReturnsEndpoint() + { + // Arrange + var context = new DefaultHttpContext(); + var initial = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + context.Features.Set(new EndpointFeature { - // Arrange - var context = new DefaultHttpContext(); + Endpoint = initial + }); - // Act - var endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); - context.SetEndpoint(endpoint); + // Act + var endpoint = context.GetEndpoint(); - // Assert - var feature = context.Features.Get(); - Assert.NotNull(feature); - Assert.Equal(endpoint, context.GetEndpoint()); - } + // Assert + Assert.Equal(initial, endpoint); + } - [Fact] - public void SetEndpoint_EndpointOnContextWithFeature_EndpointSetOnExistingFeature() - { - // Arrange - var context = new DefaultHttpContext(); - var initialEndpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); - var initialFeature = new EndpointFeature - { - Endpoint = initialEndpoint - }; - context.Features.Set(initialFeature); - - // Act - var endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); - context.SetEndpoint(endpoint); - - // Assert - var feature = context.Features.Get(); - Assert.Equal(initialFeature, feature); - Assert.Equal(endpoint, context.GetEndpoint()); - } - - [Fact] - public void SetEndpoint_NullOnContextWithFeature_NullSetOnExistingFeature() + [Fact] + public void SetEndpoint_NullOnContextWithoutFeature_NoFeatureSet() + { + // Arrange + var context = new DefaultHttpContext(); + + // Act + context.SetEndpoint(null); + + // Assert + Assert.Null(context.Features.Get()); + } + + [Fact] + public void SetEndpoint_EndpointOnContextWithoutFeature_FeatureWithEndpointSet() + { + // Arrange + var context = new DefaultHttpContext(); + + // Act + var endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + context.SetEndpoint(endpoint); + + // Assert + var feature = context.Features.Get(); + Assert.NotNull(feature); + Assert.Equal(endpoint, context.GetEndpoint()); + } + + [Fact] + public void SetEndpoint_EndpointOnContextWithFeature_EndpointSetOnExistingFeature() + { + // Arrange + var context = new DefaultHttpContext(); + var initialEndpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + var initialFeature = new EndpointFeature { - // Arrange - var context = new DefaultHttpContext(); - var initialEndpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); - var initialFeature = new EndpointFeature - { - Endpoint = initialEndpoint - }; - context.Features.Set(initialFeature); - - // Act - context.SetEndpoint(null); - - // Assert - var feature = context.Features.Get(); - Assert.Equal(initialFeature, feature); - Assert.Null(context.GetEndpoint()); - } - - [Fact] - public void SetAndGetEndpoint_Roundtrip_EndpointIsRoundtrip() + Endpoint = initialEndpoint + }; + context.Features.Set(initialFeature); + + // Act + var endpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + context.SetEndpoint(endpoint); + + // Assert + var feature = context.Features.Get(); + Assert.Equal(initialFeature, feature); + Assert.Equal(endpoint, context.GetEndpoint()); + } + + [Fact] + public void SetEndpoint_NullOnContextWithFeature_NullSetOnExistingFeature() + { + // Arrange + var context = new DefaultHttpContext(); + var initialEndpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + var initialFeature = new EndpointFeature { - // Arrange - var context = new DefaultHttpContext(); - var initialEndpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + Endpoint = initialEndpoint + }; + context.Features.Set(initialFeature); - // Act - context.SetEndpoint(initialEndpoint); - var endpoint = context.GetEndpoint(); + // Act + context.SetEndpoint(null); - // Assert - Assert.Equal(initialEndpoint, endpoint); - } + // Assert + var feature = context.Features.Get(); + Assert.Equal(initialFeature, feature); + Assert.Null(context.GetEndpoint()); + } - private class EndpointFeature : IEndpointFeature - { - public Endpoint? Endpoint { get; set; } - } + [Fact] + public void SetAndGetEndpoint_Roundtrip_EndpointIsRoundtrip() + { + // Arrange + var context = new DefaultHttpContext(); + var initialEndpoint = new Endpoint(c => Task.CompletedTask, EndpointMetadataCollection.Empty, "Test endpoint"); + + // Act + context.SetEndpoint(initialEndpoint); + var endpoint = context.GetEndpoint(); + + // Assert + Assert.Equal(initialEndpoint, endpoint); + } + + private class EndpointFeature : IEndpointFeature + { + public Endpoint? Endpoint { get; set; } } } diff --git a/src/Http/Http.Abstractions/test/EndpointMetadataCollectionTests.cs b/src/Http/Http.Abstractions/test/EndpointMetadataCollectionTests.cs index 1cc65f79e4..37a2e6d025 100644 --- a/src/Http/Http.Abstractions/test/EndpointMetadataCollectionTests.cs +++ b/src/Http/Http.Abstractions/test/EndpointMetadataCollectionTests.cs @@ -7,69 +7,68 @@ using System.Text; using Microsoft.AspNetCore.Http; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class EndpointMetadataCollectionTests { - public class EndpointMetadataCollectionTests + [Fact] + public void Constructor_Enumeration_ContainsValues() { - [Fact] - public void Constructor_Enumeration_ContainsValues() - { - // Arrange & Act - var metadata = new EndpointMetadataCollection(new List + // Arrange & Act + var metadata = new EndpointMetadataCollection(new List { 1, 2, 3, }); - // Assert - Assert.Equal(3, metadata.Count); + // Assert + Assert.Equal(3, metadata.Count); - Assert.Collection(metadata, - value => Assert.Equal(1, value), - value => Assert.Equal(2, value), - value => Assert.Equal(3, value)); - } + Assert.Collection(metadata, + value => Assert.Equal(1, value), + value => Assert.Equal(2, value), + value => Assert.Equal(3, value)); + } - [Fact] - public void Constructor_ParamsArray_ContainsValues() - { - // Arrange & Act - var metadata = new EndpointMetadataCollection(1, 2, 3); + [Fact] + public void Constructor_ParamsArray_ContainsValues() + { + // Arrange & Act + var metadata = new EndpointMetadataCollection(1, 2, 3); - // Assert - Assert.Equal(3, metadata.Count); + // Assert + Assert.Equal(3, metadata.Count); - Assert.Collection(metadata, - value => Assert.Equal(1, value), - value => Assert.Equal(2, value), - value => Assert.Equal(3, value)); - } + Assert.Collection(metadata, + value => Assert.Equal(1, value), + value => Assert.Equal(2, value), + value => Assert.Equal(3, value)); + } - [Fact] - public void GetOrderedMetadata_CanReturnEmptyCollection() - { - // Arrange - var metadata = new EndpointMetadataCollection(1, 2, 3); + [Fact] + public void GetOrderedMetadata_CanReturnEmptyCollection() + { + // Arrange + var metadata = new EndpointMetadataCollection(1, 2, 3); - // Act - var ordered = metadata.GetOrderedMetadata(); + // Act + var ordered = metadata.GetOrderedMetadata(); - Assert.Same(Array.Empty(), ordered); - } + Assert.Same(Array.Empty(), ordered); + } - [Fact] - public void GetOrderedMetadata_CanReturnNonEmptyCollection() - { - // Arrange - var metadata = new EndpointMetadataCollection("1", "2"); + [Fact] + public void GetOrderedMetadata_CanReturnNonEmptyCollection() + { + // Arrange + var metadata = new EndpointMetadataCollection("1", "2"); - // Act - var ordered1 = metadata.GetOrderedMetadata(); - var ordered2 = metadata.GetOrderedMetadata(); + // Act + var ordered1 = metadata.GetOrderedMetadata(); + var ordered2 = metadata.GetOrderedMetadata(); - Assert.Same(ordered1, ordered2); - Assert.Equal(new string[] { "1", "2" }, ordered1); - } + Assert.Same(ordered1, ordered2); + Assert.Equal(new string[] { "1", "2" }, ordered1); } } diff --git a/src/Http/Http.Abstractions/test/FragmentStringTests.cs b/src/Http/Http.Abstractions/test/FragmentStringTests.cs index 370f5b92f9..7eb68cc1a2 100644 --- a/src/Http/Http.Abstractions/test/FragmentStringTests.cs +++ b/src/Http/Http.Abstractions/test/FragmentStringTests.cs @@ -3,39 +3,38 @@ using Xunit; -namespace Microsoft.AspNetCore.Http.Abstractions.Tests +namespace Microsoft.AspNetCore.Http.Abstractions.Tests; + +public class FragmentStringTests { - public class FragmentStringTests + [Fact] + public void Equals_EmptyFragmentStringAndDefaultFragmentString() { - [Fact] - public void Equals_EmptyFragmentStringAndDefaultFragmentString() - { - // Act and Assert - Assert.Equal(default(FragmentString), FragmentString.Empty); - Assert.Equal(default(FragmentString), FragmentString.Empty); - // explicitly checking == operator - Assert.True(FragmentString.Empty == default(FragmentString)); - Assert.True(default(FragmentString) == FragmentString.Empty); - } + // Act and Assert + Assert.Equal(default(FragmentString), FragmentString.Empty); + Assert.Equal(default(FragmentString), FragmentString.Empty); + // explicitly checking == operator + Assert.True(FragmentString.Empty == default(FragmentString)); + Assert.True(default(FragmentString) == FragmentString.Empty); + } - [Fact] - public void NotEquals_DefaultFragmentStringAndNonNullFragmentString() - { - // Arrange - var fragmentString = new FragmentString("#col=1"); + [Fact] + public void NotEquals_DefaultFragmentStringAndNonNullFragmentString() + { + // Arrange + var fragmentString = new FragmentString("#col=1"); - // Act and Assert - Assert.NotEqual(default(FragmentString), fragmentString); - } + // Act and Assert + Assert.NotEqual(default(FragmentString), fragmentString); + } - [Fact] - public void NotEquals_EmptyFragmentStringAndNonNullFragmentString() - { - // Arrange - var fragmentString = new FragmentString("#col=1"); + [Fact] + public void NotEquals_EmptyFragmentStringAndNonNullFragmentString() + { + // Arrange + var fragmentString = new FragmentString("#col=1"); - // Act and Assert - Assert.NotEqual(fragmentString, FragmentString.Empty); - } + // Act and Assert + Assert.NotEqual(fragmentString, FragmentString.Empty); } } diff --git a/src/Http/Http.Abstractions/test/HostStringTest.cs b/src/Http/Http.Abstractions/test/HostStringTest.cs index c672ccfc80..443e382c13 100644 --- a/src/Http/Http.Abstractions/test/HostStringTest.cs +++ b/src/Http/Http.Abstractions/test/HostStringTest.cs @@ -6,170 +6,169 @@ using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Primitives; using Xunit; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +public class HostStringTests { - public class HostStringTests + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void CtorThrows_IfPortIsNotGreaterThanZero(int port) + { + // Act and Assert + ExceptionAssert.ThrowsArgumentOutOfRange(() => new HostString("localhost", port), "port", "The value must be greater than zero."); + } + + [Theory] + [InlineData("localhost", "localhost")] + [InlineData("1.2.3.4", "1.2.3.4")] + [InlineData("[2001:db8:a0b:12f0::1]", "[2001:db8:a0b:12f0::1]")] + [InlineData("本地主機", "本地主機")] + [InlineData("localhost:5000", "localhost")] + [InlineData("1.2.3.4:5000", "1.2.3.4")] + [InlineData("[2001:db8:a0b:12f0::1]:5000", "[2001:db8:a0b:12f0::1]")] + [InlineData("本地主機:5000", "本地主機")] + public void Domain_ExtractsHostFromValue(string sourceValue, string expectedDomain) + { + // Arrange + var hostString = new HostString(sourceValue); + + // Act + var result = hostString.Host; + + // Assert + Assert.Equal(expectedDomain, result); + } + + [Theory] + [InlineData("localhost", null)] + [InlineData("1.2.3.4", null)] + [InlineData("[2001:db8:a0b:12f0::1]", null)] + [InlineData("本地主機", null)] + [InlineData("localhost:5000", 5000)] + [InlineData("1.2.3.4:5000", 5000)] + [InlineData("[2001:db8:a0b:12f0::1]:5000", 5000)] + [InlineData("本地主機:5000", 5000)] + public void Port_ExtractsPortFromValue(string sourceValue, int? expectedPort) + { + // Arrange + var hostString = new HostString(sourceValue); + + // Act + var result = hostString.Port; + + // Assert + Assert.Equal(expectedPort, result); + } + + [Theory] + [InlineData("localhost:BLAH")] + public void Port_ExtractsInvalidPortFromValue(string sourceValue) + { + // Arrange + var hostString = new HostString(sourceValue); + + // Act + var result = hostString.Port; + + // Assert + Assert.Null(result); + } + + [Theory] + [InlineData("localhost", 5000, "localhost", 5000)] + [InlineData("1.2.3.4", 5000, "1.2.3.4", 5000)] + [InlineData("[2001:db8:a0b:12f0::1]", 5000, "[2001:db8:a0b:12f0::1]", 5000)] + [InlineData("2001:db8:a0b:12f0::1", 5000, "[2001:db8:a0b:12f0::1]", 5000)] + [InlineData("本地主機", 5000, "本地主機", 5000)] + public void Ctor_CreatesFromHostAndPort(string sourceHost, int sourcePort, string expectedHost, int expectedPort) + { + // Arrange + var hostString = new HostString(sourceHost, sourcePort); + + // Act + var host = hostString.Host; + var port = hostString.Port; + + // Assert + Assert.Equal(expectedHost, host); + Assert.Equal(expectedPort, port); + } + + [Fact] + public void Equals_EmptyHostStringAndDefaultHostString() + { + // Act and Assert + Assert.Equal(default(HostString), new HostString(string.Empty)); + Assert.Equal(default(HostString), new HostString(string.Empty)); + // explicitly checking == operator + Assert.True(new HostString(string.Empty) == default(HostString)); + Assert.True(default(HostString) == new HostString(string.Empty)); + } + + [Fact] + public void NotEquals_DefaultHostStringAndNonNullHostString() + { + // Arrange + var hostString = new HostString("example.com"); + + // Act and Assert + Assert.NotEqual(default(HostString), hostString); + } + + [Fact] + public void NotEquals_EmptyHostStringAndNonNullHostString() + { + // Arrange + var hostString = new HostString("example.com"); + + // Act and Assert + Assert.NotEqual(hostString, new HostString(string.Empty)); + } + + [Theory] + [InlineData("localHost", "localhost")] + [InlineData("localHost", "*")] // Any - Used by HttpSys + [InlineData("localhost:9090", "localHost")] + [InlineData("example.com:443", "example.com")] + [InlineData("foo.eXample.com:443", "*.exampLe.com")] + [InlineData("f.eXample.com:443", "*.exampLe.com")] + [InlineData("a.b.c.eXample.com:443", "*.exampLe.com")] + [InlineData("127.0.0.1", "127.0.0.1")] + [InlineData("127.0.0.1:443", "127.0.0.1")] + [InlineData("xn--c1yn36f:443", "xn--c1yn36f")] + [InlineData("點看", "點看")] + [InlineData("[::ABC]", "[::aBc]")] + [InlineData("[::1]:80", "[::1]")] + [InlineData("[::1]:", "[::1]")] + [InlineData("::1", "[::1]")] + public void HostMatches(string host, string pattern) + { + Assert.True(HostString.MatchesAny(host, new StringSegment[] { pattern })); + } + + [Theory] + [InlineData("example.com", "localhost")] + [InlineData("localhost:9090", "example.com")] + [InlineData(":80", "localhost")] + [InlineData(":", "localhost")] + [InlineData("example.com:443", "*.example.com")] + [InlineData(".example.com:443", "*.example.com")] + [InlineData("foo.com:443", "*.example.com")] + [InlineData("foo.example.com.bar:443", "*.example.com")] + [InlineData(".com:443", "*.com")] + [InlineData("xn--c1yn36f:443", "點看")] + [InlineData("[::1", "[::1]")] + [InlineData("[::1:80", "[::1]")] + [InlineData("::1", "::1")] // Brackets are added to the host before the comparison + public void HostDoesntMatch(string host, string pattern) + { + Assert.False(HostString.MatchesAny(host, new StringSegment[] { pattern })); + } + + [Fact] + public void HostMatchThrowsForBadPort() { - [Theory] - [InlineData(0)] - [InlineData(-1)] - public void CtorThrows_IfPortIsNotGreaterThanZero(int port) - { - // Act and Assert - ExceptionAssert.ThrowsArgumentOutOfRange(() => new HostString("localhost", port), "port", "The value must be greater than zero."); - } - - [Theory] - [InlineData("localhost", "localhost")] - [InlineData("1.2.3.4", "1.2.3.4")] - [InlineData("[2001:db8:a0b:12f0::1]", "[2001:db8:a0b:12f0::1]")] - [InlineData("本地主機", "本地主機")] - [InlineData("localhost:5000", "localhost")] - [InlineData("1.2.3.4:5000", "1.2.3.4")] - [InlineData("[2001:db8:a0b:12f0::1]:5000", "[2001:db8:a0b:12f0::1]")] - [InlineData("本地主機:5000", "本地主機")] - public void Domain_ExtractsHostFromValue(string sourceValue, string expectedDomain) - { - // Arrange - var hostString = new HostString(sourceValue); - - // Act - var result = hostString.Host; - - // Assert - Assert.Equal(expectedDomain, result); - } - - [Theory] - [InlineData("localhost", null)] - [InlineData("1.2.3.4", null)] - [InlineData("[2001:db8:a0b:12f0::1]", null)] - [InlineData("本地主機", null)] - [InlineData("localhost:5000", 5000)] - [InlineData("1.2.3.4:5000", 5000)] - [InlineData("[2001:db8:a0b:12f0::1]:5000", 5000)] - [InlineData("本地主機:5000", 5000)] - public void Port_ExtractsPortFromValue(string sourceValue, int? expectedPort) - { - // Arrange - var hostString = new HostString(sourceValue); - - // Act - var result = hostString.Port; - - // Assert - Assert.Equal(expectedPort, result); - } - - [Theory] - [InlineData("localhost:BLAH")] - public void Port_ExtractsInvalidPortFromValue(string sourceValue) - { - // Arrange - var hostString = new HostString(sourceValue); - - // Act - var result = hostString.Port; - - // Assert - Assert.Null(result); - } - - [Theory] - [InlineData("localhost", 5000, "localhost", 5000)] - [InlineData("1.2.3.4", 5000, "1.2.3.4", 5000)] - [InlineData("[2001:db8:a0b:12f0::1]", 5000, "[2001:db8:a0b:12f0::1]", 5000)] - [InlineData("2001:db8:a0b:12f0::1", 5000, "[2001:db8:a0b:12f0::1]", 5000)] - [InlineData("本地主機", 5000, "本地主機", 5000)] - public void Ctor_CreatesFromHostAndPort(string sourceHost, int sourcePort, string expectedHost, int expectedPort) - { - // Arrange - var hostString = new HostString(sourceHost, sourcePort); - - // Act - var host = hostString.Host; - var port = hostString.Port; - - // Assert - Assert.Equal(expectedHost, host); - Assert.Equal(expectedPort, port); - } - - [Fact] - public void Equals_EmptyHostStringAndDefaultHostString() - { - // Act and Assert - Assert.Equal(default(HostString), new HostString(string.Empty)); - Assert.Equal(default(HostString), new HostString(string.Empty)); - // explicitly checking == operator - Assert.True(new HostString(string.Empty) == default(HostString)); - Assert.True(default(HostString) == new HostString(string.Empty)); - } - - [Fact] - public void NotEquals_DefaultHostStringAndNonNullHostString() - { - // Arrange - var hostString = new HostString("example.com"); - - // Act and Assert - Assert.NotEqual(default(HostString), hostString); - } - - [Fact] - public void NotEquals_EmptyHostStringAndNonNullHostString() - { - // Arrange - var hostString = new HostString("example.com"); - - // Act and Assert - Assert.NotEqual(hostString, new HostString(string.Empty)); - } - - [Theory] - [InlineData("localHost", "localhost")] - [InlineData("localHost", "*")] // Any - Used by HttpSys - [InlineData("localhost:9090", "localHost")] - [InlineData("example.com:443", "example.com")] - [InlineData("foo.eXample.com:443", "*.exampLe.com")] - [InlineData("f.eXample.com:443", "*.exampLe.com")] - [InlineData("a.b.c.eXample.com:443", "*.exampLe.com")] - [InlineData("127.0.0.1", "127.0.0.1")] - [InlineData("127.0.0.1:443", "127.0.0.1")] - [InlineData("xn--c1yn36f:443", "xn--c1yn36f")] - [InlineData("點看", "點看")] - [InlineData("[::ABC]", "[::aBc]")] - [InlineData("[::1]:80", "[::1]")] - [InlineData("[::1]:", "[::1]")] - [InlineData("::1", "[::1]")] - public void HostMatches(string host, string pattern) - { - Assert.True(HostString.MatchesAny(host, new StringSegment[] { pattern })); - } - - [Theory] - [InlineData("example.com", "localhost")] - [InlineData("localhost:9090", "example.com")] - [InlineData(":80", "localhost")] - [InlineData(":", "localhost")] - [InlineData("example.com:443", "*.example.com")] - [InlineData(".example.com:443", "*.example.com")] - [InlineData("foo.com:443", "*.example.com")] - [InlineData("foo.example.com.bar:443", "*.example.com")] - [InlineData(".com:443", "*.com")] - [InlineData("xn--c1yn36f:443", "點看")] - [InlineData("[::1", "[::1]")] - [InlineData("[::1:80", "[::1]")] - [InlineData("::1", "::1")] // Brackets are added to the host before the comparison - public void HostDoesntMatch(string host, string pattern) - { - Assert.False(HostString.MatchesAny(host, new StringSegment[] { pattern })); - } - - [Fact] - public void HostMatchThrowsForBadPort() - { - Assert.Throws(() => HostString.MatchesAny("example.com:1abc", new StringSegment[] { "example.com" })); - } + Assert.Throws(() => HostString.MatchesAny("example.com:1abc", new StringSegment[] { "example.com" })); } } diff --git a/src/Http/Http.Abstractions/test/HttpMethodslTests.cs b/src/Http/Http.Abstractions/test/HttpMethodslTests.cs index 10c8e32a99..b60b5468eb 100644 --- a/src/Http/Http.Abstractions/test/HttpMethodslTests.cs +++ b/src/Http/Http.Abstractions/test/HttpMethodslTests.cs @@ -6,14 +6,14 @@ using System.Collections.Generic; using System.Text; using Xunit; -namespace Microsoft.AspNetCore.Http.Abstractions +namespace Microsoft.AspNetCore.Http.Abstractions; + +public class HttpMethodslTests { - public class HttpMethodslTests + [Fact] + public void CanonicalizedValue_Success() { - [Fact] - public void CanonicalizedValue_Success() - { - var testCases = new List<(string[] methods, string expectedMethod)> + var testCases = new List<(string[] methods, string expectedMethod)> { (new string[] { "GET", "Get", "get" }, HttpMethods.Get), (new string[] { "POST", "Post", "post" }, HttpMethods.Post), @@ -26,28 +26,27 @@ namespace Microsoft.AspNetCore.Http.Abstractions (new string[] { "TRACE", "Trace", "trace" }, HttpMethods.Trace) }; - for (int i = 0; i < testCases.Count; i++) + for (int i = 0; i < testCases.Count; i++) + { + var testCase = testCases[i]; + for (int j = 0; j < testCase.methods.Length; j++) { - var testCase = testCases[i]; - for (int j = 0; j < testCase.methods.Length; j++) - { - CanonicalizedValueTest(testCase.methods[j], testCase.expectedMethod); - } + CanonicalizedValueTest(testCase.methods[j], testCase.expectedMethod); } } + } - private void CanonicalizedValueTest(string method, string expectedMethod) - { - string inputMethod = CreateStringAtRuntime(method); - var canonicalizedValue = HttpMethods.GetCanonicalizedValue(inputMethod); + private void CanonicalizedValueTest(string method, string expectedMethod) + { + string inputMethod = CreateStringAtRuntime(method); + var canonicalizedValue = HttpMethods.GetCanonicalizedValue(inputMethod); - Assert.Same(expectedMethod, canonicalizedValue); - } + Assert.Same(expectedMethod, canonicalizedValue); + } - private string CreateStringAtRuntime(string input) - { - return new StringBuilder(input).ToString(); - } + private string CreateStringAtRuntime(string input) + { + return new StringBuilder(input).ToString(); } } diff --git a/src/Http/Http.Abstractions/test/HttpProtocolTests.cs b/src/Http/Http.Abstractions/test/HttpProtocolTests.cs index a73ce04e7f..adcf24c5b5 100644 --- a/src/Http/Http.Abstractions/test/HttpProtocolTests.cs +++ b/src/Http/Http.Abstractions/test/HttpProtocolTests.cs @@ -4,106 +4,106 @@ using System; using Xunit; -namespace Microsoft.AspNetCore.Http.Abstractions +namespace Microsoft.AspNetCore.Http.Abstractions; + +public class HttpProtocolTests { - public class HttpProtocolTests + [Fact] + public void Http3_Success() { - [Fact] - public void Http3_Success() - { - Assert.Equal("HTTP/3", HttpProtocol.Http3); - } - - [Theory] - [InlineData("HTTP/3", true)] - [InlineData("http/3", true)] - [InlineData("HTTP/1.1", false)] - [InlineData("HTTP/3.0", false)] - [InlineData("HTTP/1", false)] - [InlineData(" HTTP/3", false)] - [InlineData("HTTP/3 ", false)] - public void IsHttp3_Success(string protocol, bool match) - { - Assert.Equal(match, HttpProtocol.IsHttp3(protocol)); - } + Assert.Equal("HTTP/3", HttpProtocol.Http3); + } - [Fact] - public void Http2_Success() - { - Assert.Equal("HTTP/2", HttpProtocol.Http2); - } - - [Theory] - [InlineData("HTTP/2", true)] - [InlineData("http/2", true)] - [InlineData("HTTP/1.1", false)] - [InlineData("HTTP/2.0", false)] - [InlineData("HTTP/1", false)] - [InlineData(" HTTP/2", false)] - [InlineData("HTTP/2 ", false)] - public void IsHttp2_Success(string protocol, bool match) - { - Assert.Equal(match, HttpProtocol.IsHttp2(protocol)); - } + [Theory] + [InlineData("HTTP/3", true)] + [InlineData("http/3", true)] + [InlineData("HTTP/1.1", false)] + [InlineData("HTTP/3.0", false)] + [InlineData("HTTP/1", false)] + [InlineData(" HTTP/3", false)] + [InlineData("HTTP/3 ", false)] + public void IsHttp3_Success(string protocol, bool match) + { + Assert.Equal(match, HttpProtocol.IsHttp3(protocol)); + } - [Fact] - public void Http11_Success() - { - Assert.Equal("HTTP/1.1", HttpProtocol.Http11); - } - - [Theory] - [InlineData("HTTP/1.1", true)] - [InlineData("http/1.1", true)] - [InlineData("HTTP/2", false)] - [InlineData("HTTP/1.0", false)] - [InlineData("HTTP/1", false)] - [InlineData(" HTTP/1.1", false)] - [InlineData("HTTP/1.1 ", false)] - public void IsHttp11_Success(string protocol, bool match) - { - Assert.Equal(match, HttpProtocol.IsHttp11(protocol)); - } + [Fact] + public void Http2_Success() + { + Assert.Equal("HTTP/2", HttpProtocol.Http2); + } - [Fact] - public void Http10_Success() - { - Assert.Equal("HTTP/1.0", HttpProtocol.Http10); - } - - [Theory] - [InlineData("HTTP/1.0", true)] - [InlineData("http/1.0", true)] - [InlineData("HTTP/2", false)] - [InlineData("HTTP/1.1", false)] - [InlineData("HTTP/1", false)] - [InlineData(" HTTP/1.0", false)] - [InlineData("HTTP/1.0 ", false)] - public void IsHttp10_Success(string protocol, bool match) - { - Assert.Equal(match, HttpProtocol.IsHttp10(protocol)); - } + [Theory] + [InlineData("HTTP/2", true)] + [InlineData("http/2", true)] + [InlineData("HTTP/1.1", false)] + [InlineData("HTTP/2.0", false)] + [InlineData("HTTP/1", false)] + [InlineData(" HTTP/2", false)] + [InlineData("HTTP/2 ", false)] + public void IsHttp2_Success(string protocol, bool match) + { + Assert.Equal(match, HttpProtocol.IsHttp2(protocol)); + } - [Fact] - public void Http09_Success() - { - Assert.Equal("HTTP/0.9", HttpProtocol.Http09); - } - - [Theory] - [InlineData("HTTP/0.9", true)] - [InlineData("http/0.9", true)] - [InlineData("HTTP/2", false)] - [InlineData("HTTP/1", false)] - [InlineData("HTTP/09", false)] - [InlineData(" HTTP/0.9", false)] - [InlineData("HTTP/0.9 ", false)] - public void IsHttp09_Success(string protocol, bool match) - { - Assert.Equal(match, HttpProtocol.IsHttp09(protocol)); - } + [Fact] + public void Http11_Success() + { + Assert.Equal("HTTP/1.1", HttpProtocol.Http11); + } - public static TheoryData s_ValidData = new TheoryData + [Theory] + [InlineData("HTTP/1.1", true)] + [InlineData("http/1.1", true)] + [InlineData("HTTP/2", false)] + [InlineData("HTTP/1.0", false)] + [InlineData("HTTP/1", false)] + [InlineData(" HTTP/1.1", false)] + [InlineData("HTTP/1.1 ", false)] + public void IsHttp11_Success(string protocol, bool match) + { + Assert.Equal(match, HttpProtocol.IsHttp11(protocol)); + } + + [Fact] + public void Http10_Success() + { + Assert.Equal("HTTP/1.0", HttpProtocol.Http10); + } + + [Theory] + [InlineData("HTTP/1.0", true)] + [InlineData("http/1.0", true)] + [InlineData("HTTP/2", false)] + [InlineData("HTTP/1.1", false)] + [InlineData("HTTP/1", false)] + [InlineData(" HTTP/1.0", false)] + [InlineData("HTTP/1.0 ", false)] + public void IsHttp10_Success(string protocol, bool match) + { + Assert.Equal(match, HttpProtocol.IsHttp10(protocol)); + } + + [Fact] + public void Http09_Success() + { + Assert.Equal("HTTP/0.9", HttpProtocol.Http09); + } + + [Theory] + [InlineData("HTTP/0.9", true)] + [InlineData("http/0.9", true)] + [InlineData("HTTP/2", false)] + [InlineData("HTTP/1", false)] + [InlineData("HTTP/09", false)] + [InlineData(" HTTP/0.9", false)] + [InlineData("HTTP/0.9 ", false)] + public void IsHttp09_Success(string protocol, bool match) + { + Assert.Equal(match, HttpProtocol.IsHttp09(protocol)); + } + + public static TheoryData s_ValidData = new TheoryData { { new Version(3, 0), "HTTP/3" }, { new Version(2, 0), "HTTP/2" }, @@ -112,26 +112,25 @@ namespace Microsoft.AspNetCore.Http.Abstractions { new Version(0, 9), "HTTP/0.9" } }; - [Theory] - [MemberData(nameof(s_ValidData))] - public void GetHttpProtocol_CorrectIETFVersion(Version version, string expected) - { - var actual = HttpProtocol.GetHttpProtocol(version); + [Theory] + [MemberData(nameof(s_ValidData))] + public void GetHttpProtocol_CorrectIETFVersion(Version version, string expected) + { + var actual = HttpProtocol.GetHttpProtocol(version); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - public static TheoryData s_InvalidData = new TheoryData + public static TheoryData s_InvalidData = new TheoryData { { new Version(0, 3) }, { new Version(2, 1) } }; - [Theory] - [MemberData(nameof(s_InvalidData))] - public void GetHttpProtocol_ThrowErrorForUnknownVersion(Version version) - { - Assert.Throws(() => HttpProtocol.GetHttpProtocol(version)); - } + [Theory] + [MemberData(nameof(s_InvalidData))] + public void GetHttpProtocol_ThrowErrorForUnknownVersion(Version version) + { + Assert.Throws(() => HttpProtocol.GetHttpProtocol(version)); } } diff --git a/src/Http/Http.Abstractions/test/HttpResponseWritingExtensionsTests.cs b/src/Http/Http.Abstractions/test/HttpResponseWritingExtensionsTests.cs index 6f47fa57d1..60db97e5ba 100644 --- a/src/Http/Http.Abstractions/test/HttpResponseWritingExtensionsTests.cs +++ b/src/Http/Http.Abstractions/test/HttpResponseWritingExtensionsTests.cs @@ -8,74 +8,74 @@ using System.Text; using System.Threading.Tasks; using Xunit; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +public class HttpResponseWritingExtensionsTests { - public class HttpResponseWritingExtensionsTests + [Fact] + public async Task WritingText_WriteText() { - [Fact] - public async Task WritingText_WriteText() - { - HttpContext context = CreateRequest(); - await context.Response.WriteAsync("Hello World"); + HttpContext context = CreateRequest(); + await context.Response.WriteAsync("Hello World"); - Assert.Equal(11, context.Response.Body.Length); - } + Assert.Equal(11, context.Response.Body.Length); + } - [Fact] - public async Task WritingText_MultipleWrites() - { - HttpContext context = CreateRequest(); - await context.Response.WriteAsync("Hello World"); - await context.Response.WriteAsync("Hello World"); + [Fact] + public async Task WritingText_MultipleWrites() + { + HttpContext context = CreateRequest(); + await context.Response.WriteAsync("Hello World"); + await context.Response.WriteAsync("Hello World"); - Assert.Equal(22, context.Response.Body.Length); - } + Assert.Equal(22, context.Response.Body.Length); + } - [Theory] - [MemberData(nameof(Encodings))] - public async Task WritingTextThatRequiresMultipleSegmentsWorks(Encoding encoding) - { - var outputStream = new MemoryStream(); + [Theory] + [MemberData(nameof(Encodings))] + public async Task WritingTextThatRequiresMultipleSegmentsWorks(Encoding encoding) + { + var outputStream = new MemoryStream(); - HttpContext context = new DefaultHttpContext(); - context.Response.Body = outputStream; + HttpContext context = new DefaultHttpContext(); + context.Response.Body = outputStream; - var inputString = string.Concat(Enumerable.Repeat("昨日すき焼きを食べました", 1000)); - var expected = encoding.GetBytes(inputString); - await context.Response.WriteAsync(inputString, encoding); + var inputString = string.Concat(Enumerable.Repeat("昨日すき焼きを食べました", 1000)); + var expected = encoding.GetBytes(inputString); + await context.Response.WriteAsync(inputString, encoding); - outputStream.Position = 0; - var actual = new byte[expected.Length]; - var length = outputStream.Read(actual); + outputStream.Position = 0; + var actual = new byte[expected.Length]; + var length = outputStream.Read(actual); - Assert.Equal(expected.Length, length); - Assert.Equal(expected, actual); - } + Assert.Equal(expected.Length, length); + Assert.Equal(expected, actual); + } - [Theory] - [MemberData(nameof(Encodings))] - public async Task WritingTextWithPassedInEncodingWorks(Encoding encoding) - { - HttpContext context = CreateRequest(); + [Theory] + [MemberData(nameof(Encodings))] + public async Task WritingTextWithPassedInEncodingWorks(Encoding encoding) + { + HttpContext context = CreateRequest(); - var inputString = "昨日すき焼きを食べました"; - var expected = encoding.GetBytes(inputString); - await context.Response.WriteAsync(inputString, encoding); + var inputString = "昨日すき焼きを食べました"; + var expected = encoding.GetBytes(inputString); + await context.Response.WriteAsync(inputString, encoding); - context.Response.Body.Position = 0; - var actual = new byte[expected.Length * 2]; - var length = context.Response.Body.Read(actual); + context.Response.Body.Position = 0; + var actual = new byte[expected.Length * 2]; + var length = context.Response.Body.Read(actual); - var actualShortened = new byte[length]; - Array.Copy(actual, actualShortened, length); + var actualShortened = new byte[length]; + Array.Copy(actual, actualShortened, length); - Assert.Equal(expected.Length, length); - Assert.Equal(expected, actualShortened); - } + Assert.Equal(expected.Length, length); + Assert.Equal(expected, actualShortened); + } - public static TheoryData Encodings => - new TheoryData - { + public static TheoryData Encodings => + new TheoryData + { { Encoding.ASCII }, { Encoding.BigEndianUnicode }, { Encoding.Unicode }, @@ -84,13 +84,12 @@ namespace Microsoft.AspNetCore.Http { Encoding.UTF7 }, #pragma warning restore CS0618, SYSLIB0001 // Type or member is obsolete { Encoding.UTF8 } - }; + }; - private HttpContext CreateRequest() - { - HttpContext context = new DefaultHttpContext(); - context.Response.Body = new MemoryStream(); - return context; - } + private HttpContext CreateRequest() + { + HttpContext context = new DefaultHttpContext(); + context.Response.Body = new MemoryStream(); + return context; } } diff --git a/src/Http/Http.Abstractions/test/MapPathMiddlewareTests.cs b/src/Http/Http.Abstractions/test/MapPathMiddlewareTests.cs index 48c718a47c..2882c92b63 100644 --- a/src/Http/Http.Abstractions/test/MapPathMiddlewareTests.cs +++ b/src/Http/Http.Abstractions/test/MapPathMiddlewareTests.cs @@ -5,269 +5,268 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.Builder.Extensions +namespace Microsoft.AspNetCore.Builder.Extensions; + +public class MapPathMiddlewareTests { - public class MapPathMiddlewareTests + private static Task Success(HttpContext context) { - private static Task Success(HttpContext context) - { - context.Response.StatusCode = 200; - context.Items["test.PathBase"] = context.Request.PathBase.Value; - context.Items["test.Path"] = context.Request.Path.Value; - return Task.FromResult(null); - } + context.Response.StatusCode = 200; + context.Items["test.PathBase"] = context.Request.PathBase.Value; + context.Items["test.Path"] = context.Request.Path.Value; + return Task.FromResult(null); + } - private static void UseSuccess(IApplicationBuilder app) - { - app.Run(Success); - } + private static void UseSuccess(IApplicationBuilder app) + { + app.Run(Success); + } - private static Task NotImplemented(HttpContext context) - { - throw new NotImplementedException(); - } + private static Task NotImplemented(HttpContext context) + { + throw new NotImplementedException(); + } - private static void UseNotImplemented(IApplicationBuilder app) - { - app.Run(NotImplemented); - } + private static void UseNotImplemented(IApplicationBuilder app) + { + app.Run(NotImplemented); + } - [Fact] - public void NullArguments_ArgumentNullException() - { - var builder = new ApplicationBuilder(serviceProvider: null!); - var noMiddleware = new ApplicationBuilder(serviceProvider: null!).Build(); - var noOptions = new MapOptions(); - Assert.Throws(() => builder.Map("/foo", configuration: null!)); - Assert.Throws(() => new MapMiddleware(noMiddleware, null!)); - } + [Fact] + public void NullArguments_ArgumentNullException() + { + var builder = new ApplicationBuilder(serviceProvider: null!); + var noMiddleware = new ApplicationBuilder(serviceProvider: null!).Build(); + var noOptions = new MapOptions(); + Assert.Throws(() => builder.Map("/foo", configuration: null!)); + Assert.Throws(() => new MapMiddleware(noMiddleware, null!)); + } - [Theory] - [InlineData("/foo", "", "/foo")] - [InlineData("/foo", "", "/foo/")] - [InlineData("/foo", "/Bar", "/foo")] - [InlineData("/foo", "/Bar", "/foo/cho")] - [InlineData("/foo", "/Bar", "/foo/cho/")] - [InlineData("/foo/cho", "/Bar", "/foo/cho")] - [InlineData("/foo/cho", "/Bar", "/foo/cho/do")] - public async Task PathMatchFunc_BranchTaken(string matchPath, string basePath, string requestPath) - { - HttpContext context = CreateRequest(basePath, requestPath); - var builder = new ApplicationBuilder(serviceProvider: null!); - builder.Map(matchPath, UseSuccess); - var app = builder.Build(); - await app.Invoke(context); - - Assert.Equal(200, context.Response.StatusCode); - Assert.Equal(basePath, context.Request.PathBase.Value); - Assert.Equal(requestPath, context.Request.Path.Value); - } + [Theory] + [InlineData("/foo", "", "/foo")] + [InlineData("/foo", "", "/foo/")] + [InlineData("/foo", "/Bar", "/foo")] + [InlineData("/foo", "/Bar", "/foo/cho")] + [InlineData("/foo", "/Bar", "/foo/cho/")] + [InlineData("/foo/cho", "/Bar", "/foo/cho")] + [InlineData("/foo/cho", "/Bar", "/foo/cho/do")] + public async Task PathMatchFunc_BranchTaken(string matchPath, string basePath, string requestPath) + { + HttpContext context = CreateRequest(basePath, requestPath); + var builder = new ApplicationBuilder(serviceProvider: null!); + builder.Map(matchPath, UseSuccess); + var app = builder.Build(); + await app.Invoke(context); + + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(basePath, context.Request.PathBase.Value); + Assert.Equal(requestPath, context.Request.Path.Value); + } - [Theory] - [InlineData("/foo", "", "/foo")] - [InlineData("/foo", "", "/foo/")] - [InlineData("/foo", "/Bar", "/foo")] - [InlineData("/foo", "/Bar", "/foo/cho")] - [InlineData("/foo", "/Bar", "/foo/cho/")] - [InlineData("/foo/cho", "/Bar", "/foo/cho")] - [InlineData("/foo/cho", "/Bar", "/foo/cho/do")] - [InlineData("/foo", "", "/Foo")] - [InlineData("/foo", "", "/Foo/")] - [InlineData("/foo", "/Bar", "/Foo")] - [InlineData("/foo", "/Bar", "/Foo/Cho")] - [InlineData("/foo", "/Bar", "/Foo/Cho/")] - [InlineData("/foo/cho", "/Bar", "/Foo/Cho")] - [InlineData("/foo/cho", "/Bar", "/Foo/Cho/do")] - public async Task PathMatchAction_BranchTaken(string matchPath, string basePath, string requestPath) - { - HttpContext context = CreateRequest(basePath, requestPath); - var builder = new ApplicationBuilder(serviceProvider: null!); - builder.Map(matchPath, subBuilder => subBuilder.Run(Success)); - var app = builder.Build(); - await app.Invoke(context); - - Assert.Equal(200, context.Response.StatusCode); - Assert.Equal(string.Concat(basePath, requestPath.AsSpan(0, matchPath.Length)), (string)context.Items["test.PathBase"]!); - Assert.Equal(requestPath.Substring(matchPath.Length), context.Items["test.Path"]); - } + [Theory] + [InlineData("/foo", "", "/foo")] + [InlineData("/foo", "", "/foo/")] + [InlineData("/foo", "/Bar", "/foo")] + [InlineData("/foo", "/Bar", "/foo/cho")] + [InlineData("/foo", "/Bar", "/foo/cho/")] + [InlineData("/foo/cho", "/Bar", "/foo/cho")] + [InlineData("/foo/cho", "/Bar", "/foo/cho/do")] + [InlineData("/foo", "", "/Foo")] + [InlineData("/foo", "", "/Foo/")] + [InlineData("/foo", "/Bar", "/Foo")] + [InlineData("/foo", "/Bar", "/Foo/Cho")] + [InlineData("/foo", "/Bar", "/Foo/Cho/")] + [InlineData("/foo/cho", "/Bar", "/Foo/Cho")] + [InlineData("/foo/cho", "/Bar", "/Foo/Cho/do")] + public async Task PathMatchAction_BranchTaken(string matchPath, string basePath, string requestPath) + { + HttpContext context = CreateRequest(basePath, requestPath); + var builder = new ApplicationBuilder(serviceProvider: null!); + builder.Map(matchPath, subBuilder => subBuilder.Run(Success)); + var app = builder.Build(); + await app.Invoke(context); + + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(string.Concat(basePath, requestPath.AsSpan(0, matchPath.Length)), (string)context.Items["test.PathBase"]!); + Assert.Equal(requestPath.Substring(matchPath.Length), context.Items["test.Path"]); + } - [Theory] - [InlineData("/foo", "", "/foo")] - [InlineData("/foo", "", "/foo/")] - [InlineData("/foo", "/Bar", "/foo")] - [InlineData("/foo", "/Bar", "/foo/cho")] - [InlineData("/foo", "/Bar", "/foo/cho/")] - [InlineData("/foo/cho", "/Bar", "/foo/cho")] - [InlineData("/foo/cho", "/Bar", "/foo/cho/do")] - [InlineData("/foo", "", "/Foo")] - [InlineData("/foo", "", "/Foo/")] - [InlineData("/foo", "/Bar", "/Foo")] - [InlineData("/foo", "/Bar", "/Foo/Cho")] - [InlineData("/foo", "/Bar", "/Foo/Cho/")] - [InlineData("/foo/cho", "/Bar", "/Foo/Cho")] - [InlineData("/foo/cho", "/Bar", "/Foo/Cho/do")] - public async Task PathMatchAction_BranchTaken_WithPreserveMatchedPathSegment(string matchPath, string basePath, string requestPath) - { - HttpContext context = CreateRequest(basePath, requestPath); - var builder = new ApplicationBuilder(serviceProvider: null!); - builder.Map(matchPath, true, subBuilder => subBuilder.Run(Success)); - var app = builder.Build(); - await app.Invoke(context); - - Assert.Equal(200, context.Response.StatusCode); - Assert.Equal(basePath, (string)context.Items["test.PathBase"]!); - Assert.Equal(requestPath, context.Items["test.Path"]); - } + [Theory] + [InlineData("/foo", "", "/foo")] + [InlineData("/foo", "", "/foo/")] + [InlineData("/foo", "/Bar", "/foo")] + [InlineData("/foo", "/Bar", "/foo/cho")] + [InlineData("/foo", "/Bar", "/foo/cho/")] + [InlineData("/foo/cho", "/Bar", "/foo/cho")] + [InlineData("/foo/cho", "/Bar", "/foo/cho/do")] + [InlineData("/foo", "", "/Foo")] + [InlineData("/foo", "", "/Foo/")] + [InlineData("/foo", "/Bar", "/Foo")] + [InlineData("/foo", "/Bar", "/Foo/Cho")] + [InlineData("/foo", "/Bar", "/Foo/Cho/")] + [InlineData("/foo/cho", "/Bar", "/Foo/Cho")] + [InlineData("/foo/cho", "/Bar", "/Foo/Cho/do")] + public async Task PathMatchAction_BranchTaken_WithPreserveMatchedPathSegment(string matchPath, string basePath, string requestPath) + { + HttpContext context = CreateRequest(basePath, requestPath); + var builder = new ApplicationBuilder(serviceProvider: null!); + builder.Map(matchPath, true, subBuilder => subBuilder.Run(Success)); + var app = builder.Build(); + await app.Invoke(context); + + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(basePath, (string)context.Items["test.PathBase"]!); + Assert.Equal(requestPath, context.Items["test.Path"]); + } - [Theory] - [InlineData("/")] - [InlineData("/foo/")] - [InlineData("/foo/cho/")] - public void MatchPathWithTrailingSlashThrowsException(string matchPath) - { - Assert.Throws(() => new ApplicationBuilder(serviceProvider: null!).Map(matchPath, map => { }).Build()); - } + [Theory] + [InlineData("/")] + [InlineData("/foo/")] + [InlineData("/foo/cho/")] + public void MatchPathWithTrailingSlashThrowsException(string matchPath) + { + Assert.Throws(() => new ApplicationBuilder(serviceProvider: null!).Map(matchPath, map => { }).Build()); + } - [Theory] - [InlineData("/foo", "", "")] - [InlineData("/foo", "/bar", "")] - [InlineData("/foo", "", "/bar")] - [InlineData("/foo", "/foo", "")] - [InlineData("/foo", "/foo", "/bar")] - [InlineData("/foo", "", "/bar/foo")] - [InlineData("/foo/bar", "/foo", "/bar")] - public async Task PathMismatchFunc_PassedThrough(string matchPath, string basePath, string requestPath) - { - HttpContext context = CreateRequest(basePath, requestPath); - var builder = new ApplicationBuilder(serviceProvider: null!); - builder.Map(matchPath, UseNotImplemented); - builder.Run(Success); - var app = builder.Build(); - await app.Invoke(context); - - Assert.Equal(200, context.Response.StatusCode); - Assert.Equal(basePath, context.Request.PathBase.Value); - Assert.Equal(requestPath, context.Request.Path.Value); - } + [Theory] + [InlineData("/foo", "", "")] + [InlineData("/foo", "/bar", "")] + [InlineData("/foo", "", "/bar")] + [InlineData("/foo", "/foo", "")] + [InlineData("/foo", "/foo", "/bar")] + [InlineData("/foo", "", "/bar/foo")] + [InlineData("/foo/bar", "/foo", "/bar")] + public async Task PathMismatchFunc_PassedThrough(string matchPath, string basePath, string requestPath) + { + HttpContext context = CreateRequest(basePath, requestPath); + var builder = new ApplicationBuilder(serviceProvider: null!); + builder.Map(matchPath, UseNotImplemented); + builder.Run(Success); + var app = builder.Build(); + await app.Invoke(context); + + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(basePath, context.Request.PathBase.Value); + Assert.Equal(requestPath, context.Request.Path.Value); + } - [Theory] - [InlineData("/foo", "", "")] - [InlineData("/foo", "/bar", "")] - [InlineData("/foo", "", "/bar")] - [InlineData("/foo", "/foo", "")] - [InlineData("/foo", "/foo", "/bar")] - [InlineData("/foo", "", "/bar/foo")] - [InlineData("/foo/bar", "/foo", "/bar")] - public async Task PathMismatchAction_PassedThrough(string matchPath, string basePath, string requestPath) - { - HttpContext context = CreateRequest(basePath, requestPath); - var builder = new ApplicationBuilder(serviceProvider: null!); - builder.Map(matchPath, UseNotImplemented); - builder.Run(Success); - var app = builder.Build(); - await app.Invoke(context); - - Assert.Equal(200, context.Response.StatusCode); - Assert.Equal(basePath, context.Request.PathBase.Value); - Assert.Equal(requestPath, context.Request.Path.Value); - } + [Theory] + [InlineData("/foo", "", "")] + [InlineData("/foo", "/bar", "")] + [InlineData("/foo", "", "/bar")] + [InlineData("/foo", "/foo", "")] + [InlineData("/foo", "/foo", "/bar")] + [InlineData("/foo", "", "/bar/foo")] + [InlineData("/foo/bar", "/foo", "/bar")] + public async Task PathMismatchAction_PassedThrough(string matchPath, string basePath, string requestPath) + { + HttpContext context = CreateRequest(basePath, requestPath); + var builder = new ApplicationBuilder(serviceProvider: null!); + builder.Map(matchPath, UseNotImplemented); + builder.Run(Success); + var app = builder.Build(); + await app.Invoke(context); + + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(basePath, context.Request.PathBase.Value); + Assert.Equal(requestPath, context.Request.Path.Value); + } - [Fact] - public async Task ChainedRoutes_Success() + [Fact] + public async Task ChainedRoutes_Success() + { + var builder = new ApplicationBuilder(serviceProvider: null!); + builder.Map("/route1", map => { - var builder = new ApplicationBuilder(serviceProvider: null!); - builder.Map("/route1", map => - { - map.Map("/subroute1", UseSuccess); - map.Run(NotImplemented); - }); - builder.Map("/route2/subroute2", UseSuccess); - var app = builder.Build(); - - HttpContext context = CreateRequest(string.Empty, "/route1"); - await Assert.ThrowsAsync(() => app.Invoke(context)); - - context = CreateRequest(string.Empty, "/route1/subroute1"); - await app.Invoke(context); - Assert.Equal(200, context.Response.StatusCode); - Assert.Equal(string.Empty, context.Request.PathBase.Value); - Assert.Equal("/route1/subroute1", context.Request.Path.Value); - - context = CreateRequest(string.Empty, "/route2"); - await app.Invoke(context); - Assert.Equal(404, context.Response.StatusCode); - Assert.Equal(string.Empty, context.Request.PathBase.Value); - Assert.Equal("/route2", context.Request.Path.Value); - - context = CreateRequest(string.Empty, "/route2/subroute2"); - await app.Invoke(context); - Assert.Equal(200, context.Response.StatusCode); - Assert.Equal(string.Empty, context.Request.PathBase.Value); - Assert.Equal("/route2/subroute2", context.Request.Path.Value); - - context = CreateRequest(string.Empty, "/route2/subroute2/subsub2"); - await app.Invoke(context); - Assert.Equal(200, context.Response.StatusCode); - Assert.Equal(string.Empty, context.Request.PathBase.Value); - Assert.Equal("/route2/subroute2/subsub2", context.Request.Path.Value); - } + map.Map("/subroute1", UseSuccess); + map.Run(NotImplemented); + }); + builder.Map("/route2/subroute2", UseSuccess); + var app = builder.Build(); + + HttpContext context = CreateRequest(string.Empty, "/route1"); + await Assert.ThrowsAsync(() => app.Invoke(context)); + + context = CreateRequest(string.Empty, "/route1/subroute1"); + await app.Invoke(context); + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(string.Empty, context.Request.PathBase.Value); + Assert.Equal("/route1/subroute1", context.Request.Path.Value); + + context = CreateRequest(string.Empty, "/route2"); + await app.Invoke(context); + Assert.Equal(404, context.Response.StatusCode); + Assert.Equal(string.Empty, context.Request.PathBase.Value); + Assert.Equal("/route2", context.Request.Path.Value); + + context = CreateRequest(string.Empty, "/route2/subroute2"); + await app.Invoke(context); + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(string.Empty, context.Request.PathBase.Value); + Assert.Equal("/route2/subroute2", context.Request.Path.Value); + + context = CreateRequest(string.Empty, "/route2/subroute2/subsub2"); + await app.Invoke(context); + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(string.Empty, context.Request.PathBase.Value); + Assert.Equal("/route2/subroute2/subsub2", context.Request.Path.Value); + } - [Fact] - public void ApplicationBuilderMapOverloadPreferredOverEndpointBuilderGivenStringPathAndImplicitLambdaParameterType() - { - var mockWebApplication = new MockWebApplication(); + [Fact] + public void ApplicationBuilderMapOverloadPreferredOverEndpointBuilderGivenStringPathAndImplicitLambdaParameterType() + { + var mockWebApplication = new MockWebApplication(); - mockWebApplication.Map("/foo", app => { }); + mockWebApplication.Map("/foo", app => { }); - Assert.True(mockWebApplication.UseCalled); - } + Assert.True(mockWebApplication.UseCalled); + } - [Fact] - public void ApplicationBuilderMapOverloadPreferredOverEndpointBuilderGivenStringPathAndExplicitLambdaParameterType() - { - var mockWebApplication = new MockWebApplication(); + [Fact] + public void ApplicationBuilderMapOverloadPreferredOverEndpointBuilderGivenStringPathAndExplicitLambdaParameterType() + { + var mockWebApplication = new MockWebApplication(); - mockWebApplication.Map("/foo", (IApplicationBuilder app) => { }); + mockWebApplication.Map("/foo", (IApplicationBuilder app) => { }); - Assert.True(mockWebApplication.UseCalled); - } + Assert.True(mockWebApplication.UseCalled); + } - private HttpContext CreateRequest(string basePath, string requestPath) - { - HttpContext context = new DefaultHttpContext(); - context.Request.PathBase = new PathString(basePath); - context.Request.Path = new PathString(requestPath); - return context; - } + private HttpContext CreateRequest(string basePath, string requestPath) + { + HttpContext context = new DefaultHttpContext(); + context.Request.PathBase = new PathString(basePath); + context.Request.Path = new PathString(requestPath); + return context; + } - private class MockWebApplication : IApplicationBuilder, IEndpointRouteBuilder - { - public bool UseCalled { get; set; } + private class MockWebApplication : IApplicationBuilder, IEndpointRouteBuilder + { + public bool UseCalled { get; set; } - public IServiceProvider ApplicationServices { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public IServiceProvider ApplicationServices { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public IFeatureCollection ServerFeatures => throw new NotImplementedException(); + public IFeatureCollection ServerFeatures => throw new NotImplementedException(); - public IDictionary Properties => throw new NotImplementedException(); + public IDictionary Properties => throw new NotImplementedException(); - public IServiceProvider ServiceProvider => throw new NotImplementedException(); + public IServiceProvider ServiceProvider => throw new NotImplementedException(); - public ICollection DataSources => throw new NotImplementedException(); + public ICollection DataSources => throw new NotImplementedException(); - public IApplicationBuilder CreateApplicationBuilder() => throw new NotImplementedException(); + public IApplicationBuilder CreateApplicationBuilder() => throw new NotImplementedException(); - public IApplicationBuilder New() => this; + public IApplicationBuilder New() => this; - public IApplicationBuilder Use(Func middleware) - { - UseCalled = true; - return this; - } + public IApplicationBuilder Use(Func middleware) + { + UseCalled = true; + return this; + } - public RequestDelegate Build() - { - return context => Task.CompletedTask; - } + public RequestDelegate Build() + { + return context => Task.CompletedTask; } } } diff --git a/src/Http/Http.Abstractions/test/MapPredicateMiddlewareTests.cs b/src/Http/Http.Abstractions/test/MapPredicateMiddlewareTests.cs index 5f1a189043..a543686e72 100644 --- a/src/Http/Http.Abstractions/test/MapPredicateMiddlewareTests.cs +++ b/src/Http/Http.Abstractions/test/MapPredicateMiddlewareTests.cs @@ -6,117 +6,116 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; -namespace Microsoft.AspNetCore.Builder.Extensions +namespace Microsoft.AspNetCore.Builder.Extensions; + +using Predicate = Func; + +public class MapPredicateMiddlewareTests { - using Predicate = Func; + private static readonly Predicate NotImplementedPredicate = new Predicate(environment => { throw new NotImplementedException(); }); - public class MapPredicateMiddlewareTests + private static Task Success(HttpContext context) { - private static readonly Predicate NotImplementedPredicate = new Predicate(environment => { throw new NotImplementedException(); }); + context.Response.StatusCode = 200; + return Task.FromResult(null!); + } - private static Task Success(HttpContext context) - { - context.Response.StatusCode = 200; - return Task.FromResult(null!); - } + private static void UseSuccess(IApplicationBuilder app) + { + app.Run(Success); + } - private static void UseSuccess(IApplicationBuilder app) - { - app.Run(Success); - } + private static Task NotImplemented(HttpContext context) + { + throw new NotImplementedException(); + } - private static Task NotImplemented(HttpContext context) - { - throw new NotImplementedException(); - } + private static void UseNotImplemented(IApplicationBuilder app) + { + app.Run(NotImplemented); + } - private static void UseNotImplemented(IApplicationBuilder app) - { - app.Run(NotImplemented); - } + private bool TruePredicate(HttpContext context) + { + return true; + } - private bool TruePredicate(HttpContext context) - { - return true; - } + private bool FalsePredicate(HttpContext context) + { + return false; + } - private bool FalsePredicate(HttpContext context) - { - return false; - } + [Fact] + public void NullArguments_ArgumentNullException() + { + var builder = new ApplicationBuilder(serviceProvider: null!); + var noMiddleware = new ApplicationBuilder(serviceProvider: null!).Build(); + var noOptions = new MapWhenOptions(); + Assert.Throws(() => builder.MapWhen(null!, UseNotImplemented)); + Assert.Throws(() => builder.MapWhen(NotImplementedPredicate, configuration: null!)); + Assert.Throws(() => new MapWhenMiddleware(null!, noOptions)); + Assert.Throws(() => new MapWhenMiddleware(noMiddleware, null!)); + Assert.Throws(() => new MapWhenMiddleware(null!, noOptions)); + Assert.Throws(() => new MapWhenMiddleware(noMiddleware, null!)); + } - [Fact] - public void NullArguments_ArgumentNullException() - { - var builder = new ApplicationBuilder(serviceProvider: null!); - var noMiddleware = new ApplicationBuilder(serviceProvider: null!).Build(); - var noOptions = new MapWhenOptions(); - Assert.Throws(() => builder.MapWhen(null!, UseNotImplemented)); - Assert.Throws(() => builder.MapWhen(NotImplementedPredicate, configuration: null!)); - Assert.Throws(() => new MapWhenMiddleware(null!, noOptions)); - Assert.Throws(() => new MapWhenMiddleware(noMiddleware, null!)); - Assert.Throws(() => new MapWhenMiddleware(null!, noOptions)); - Assert.Throws(() => new MapWhenMiddleware(noMiddleware, null!)); - } - - [Fact] - public async Task PredicateTrue_BranchTaken() - { - HttpContext context = CreateRequest(); - var builder = new ApplicationBuilder(serviceProvider: null!); - builder.MapWhen(TruePredicate, UseSuccess); - var app = builder.Build(); - await app.Invoke(context); + [Fact] + public async Task PredicateTrue_BranchTaken() + { + HttpContext context = CreateRequest(); + var builder = new ApplicationBuilder(serviceProvider: null!); + builder.MapWhen(TruePredicate, UseSuccess); + var app = builder.Build(); + await app.Invoke(context); - Assert.Equal(200, context.Response.StatusCode); - } + Assert.Equal(200, context.Response.StatusCode); + } - [Fact] - public async Task PredicateTrueAction_BranchTaken() - { - HttpContext context = CreateRequest(); - var builder = new ApplicationBuilder(serviceProvider: null!); - builder.MapWhen(TruePredicate, UseSuccess); - var app = builder.Build(); - await app.Invoke(context); + [Fact] + public async Task PredicateTrueAction_BranchTaken() + { + HttpContext context = CreateRequest(); + var builder = new ApplicationBuilder(serviceProvider: null!); + builder.MapWhen(TruePredicate, UseSuccess); + var app = builder.Build(); + await app.Invoke(context); + + Assert.Equal(200, context.Response.StatusCode); + } - Assert.Equal(200, context.Response.StatusCode); - } + [Fact] + public async Task PredicateFalseAction_PassThrough() + { + HttpContext context = CreateRequest(); + var builder = new ApplicationBuilder(serviceProvider: null!); + builder.MapWhen(FalsePredicate, UseNotImplemented); + builder.Run(Success); + var app = builder.Build(); + await app.Invoke(context); + + Assert.Equal(200, context.Response.StatusCode); + } - [Fact] - public async Task PredicateFalseAction_PassThrough() - { - HttpContext context = CreateRequest(); - var builder = new ApplicationBuilder(serviceProvider: null!); - builder.MapWhen(FalsePredicate, UseNotImplemented); - builder.Run(Success); - var app = builder.Build(); - await app.Invoke(context); - - Assert.Equal(200, context.Response.StatusCode); - } - - [Fact] - public async Task ChainedPredicates_Success() - { - var builder = new ApplicationBuilder(serviceProvider: null!); - builder.MapWhen(TruePredicate, map1 => - { - map1.MapWhen((Predicate)FalsePredicate, UseNotImplemented); - map1.MapWhen((Predicate)TruePredicate, map2 => map2.MapWhen((Predicate)TruePredicate, UseSuccess)); - map1.Run(NotImplemented); - }); - var app = builder.Build(); - - HttpContext context = CreateRequest(); - await app.Invoke(context); - Assert.Equal(200, context.Response.StatusCode); - } - - private HttpContext CreateRequest() + [Fact] + public async Task ChainedPredicates_Success() + { + var builder = new ApplicationBuilder(serviceProvider: null!); + builder.MapWhen(TruePredicate, map1 => { - HttpContext context = new DefaultHttpContext(); - return context; - } + map1.MapWhen((Predicate)FalsePredicate, UseNotImplemented); + map1.MapWhen((Predicate)TruePredicate, map2 => map2.MapWhen((Predicate)TruePredicate, UseSuccess)); + map1.Run(NotImplemented); + }); + var app = builder.Build(); + + HttpContext context = CreateRequest(); + await app.Invoke(context); + Assert.Equal(200, context.Response.StatusCode); + } + + private HttpContext CreateRequest() + { + HttpContext context = new DefaultHttpContext(); + return context; } } diff --git a/src/Http/Http.Abstractions/test/PathStringTests.cs b/src/Http/Http.Abstractions/test/PathStringTests.cs index ca53a3014e..9fe0b08138 100644 --- a/src/Http/Http.Abstractions/test/PathStringTests.cs +++ b/src/Http/Http.Abstractions/test/PathStringTests.cs @@ -9,342 +9,341 @@ using System.Linq; using Microsoft.AspNetCore.Testing; using Xunit; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +public class PathStringTests { - public class PathStringTests + [Fact] + public void CtorThrows_IfPathDoesNotHaveLeadingSlash() { - [Fact] - public void CtorThrows_IfPathDoesNotHaveLeadingSlash() - { - // Act and Assert - ExceptionAssert.ThrowsArgument(() => new PathString("hello"), "value", "The path in 'value' must start with '/'."); - } + // Act and Assert + ExceptionAssert.ThrowsArgument(() => new PathString("hello"), "value", "The path in 'value' must start with '/'."); + } - [Fact] - public void Equals_EmptyPathStringAndDefaultPathString() - { - // Act and Assert - Assert.Equal(default(PathString), PathString.Empty); - Assert.Equal(default(PathString), PathString.Empty); - Assert.True(PathString.Empty == default(PathString)); - Assert.True(default(PathString) == PathString.Empty); - Assert.True(PathString.Empty.Equals(default(PathString))); - Assert.True(default(PathString).Equals(PathString.Empty)); - } + [Fact] + public void Equals_EmptyPathStringAndDefaultPathString() + { + // Act and Assert + Assert.Equal(default(PathString), PathString.Empty); + Assert.Equal(default(PathString), PathString.Empty); + Assert.True(PathString.Empty == default(PathString)); + Assert.True(default(PathString) == PathString.Empty); + Assert.True(PathString.Empty.Equals(default(PathString))); + Assert.True(default(PathString).Equals(PathString.Empty)); + } - [Fact] - public void NotEquals_DefaultPathStringAndNonNullPathString() - { - // Arrange - var pathString = new PathString("/hello"); + [Fact] + public void NotEquals_DefaultPathStringAndNonNullPathString() + { + // Arrange + var pathString = new PathString("/hello"); - // Act and Assert - Assert.NotEqual(default(PathString), pathString); - } + // Act and Assert + Assert.NotEqual(default(PathString), pathString); + } - [Fact] - public void NotEquals_EmptyPathStringAndNonNullPathString() - { - // Arrange - var pathString = new PathString("/hello"); + [Fact] + public void NotEquals_EmptyPathStringAndNonNullPathString() + { + // Arrange + var pathString = new PathString("/hello"); - // Act and Assert - Assert.NotEqual(pathString, PathString.Empty); - } + // Act and Assert + Assert.NotEqual(pathString, PathString.Empty); + } - [Fact] - public void HashCode_CheckNullAndEmptyHaveSameHashcodes() - { - Assert.Equal(PathString.Empty.GetHashCode(), default(PathString).GetHashCode()); - } + [Fact] + public void HashCode_CheckNullAndEmptyHaveSameHashcodes() + { + Assert.Equal(PathString.Empty.GetHashCode(), default(PathString).GetHashCode()); + } - [Theory] - [InlineData(null, null)] - [InlineData("", null)] - public void AddPathString_HandlesNullAndEmptyStrings(string appString, string concatString) - { - // Arrange - var appPath = new PathString(appString); - var concatPath = new PathString(concatString); + [Theory] + [InlineData(null, null)] + [InlineData("", null)] + public void AddPathString_HandlesNullAndEmptyStrings(string appString, string concatString) + { + // Arrange + var appPath = new PathString(appString); + var concatPath = new PathString(concatString); - // Act - var result = appPath.Add(concatPath); + // Act + var result = appPath.Add(concatPath); - // Assert - Assert.False(result.HasValue); - } + // Assert + Assert.False(result.HasValue); + } - [Theory] - [InlineData("", "/", "/")] - [InlineData("/", null, "/")] - [InlineData("/", "", "/")] - [InlineData("/", "/test", "/test")] - [InlineData("/myapp/", "/test/bar", "/myapp/test/bar")] - [InlineData("/myapp/", "/test/bar/", "/myapp/test/bar/")] - public void AddPathString_HandlesLeadingAndTrailingSlashes(string appString, string concatString, string expected) - { - // Arrange - var appPath = new PathString(appString); - var concatPath = new PathString(concatString); + [Theory] + [InlineData("", "/", "/")] + [InlineData("/", null, "/")] + [InlineData("/", "", "/")] + [InlineData("/", "/test", "/test")] + [InlineData("/myapp/", "/test/bar", "/myapp/test/bar")] + [InlineData("/myapp/", "/test/bar/", "/myapp/test/bar/")] + public void AddPathString_HandlesLeadingAndTrailingSlashes(string appString, string concatString, string expected) + { + // Arrange + var appPath = new PathString(appString); + var concatPath = new PathString(concatString); - // Act - var result = appPath.Add(concatPath); + // Act + var result = appPath.Add(concatPath); - // Assert - Assert.Equal(expected, result.Value); - } + // Assert + Assert.Equal(expected, result.Value); + } - [Fact] - public void ImplicitStringConverters_WorksWithAdd() - { - var scheme = "http"; - var host = new HostString("localhost:80"); - var pathBase = new PathString("/base"); - var path = new PathString("/path"); - var query = new QueryString("?query"); - var fragment = new FragmentString("#frag"); + [Fact] + public void ImplicitStringConverters_WorksWithAdd() + { + var scheme = "http"; + var host = new HostString("localhost:80"); + var pathBase = new PathString("/base"); + var path = new PathString("/path"); + var query = new QueryString("?query"); + var fragment = new FragmentString("#frag"); - var result = scheme + "://" + host + pathBase + path + query + fragment; - Assert.Equal("http://localhost:80/base/path?query#frag", result); + var result = scheme + "://" + host + pathBase + path + query + fragment; + Assert.Equal("http://localhost:80/base/path?query#frag", result); - result = pathBase + path + query + fragment; - Assert.Equal("/base/path?query#frag", result); + result = pathBase + path + query + fragment; + Assert.Equal("/base/path?query#frag", result); - result = path + "text"; - Assert.Equal("/pathtext", result); - } + result = path + "text"; + Assert.Equal("/pathtext", result); + } - [Theory] - [InlineData("/test/path", "/TEST", true)] - [InlineData("/test/path", "/TEST/pa", false)] - [InlineData("/TEST/PATH", "/test", true)] - [InlineData("/TEST/path", "/test/pa", false)] - [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", true)] - public void StartsWithSegments_DoesACaseInsensitiveMatch(string sourcePath, string testPath, bool expectedResult) - { - var source = new PathString(sourcePath); - var test = new PathString(testPath); + [Theory] + [InlineData("/test/path", "/TEST", true)] + [InlineData("/test/path", "/TEST/pa", false)] + [InlineData("/TEST/PATH", "/test", true)] + [InlineData("/TEST/path", "/test/pa", false)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", true)] + public void StartsWithSegments_DoesACaseInsensitiveMatch(string sourcePath, string testPath, bool expectedResult) + { + var source = new PathString(sourcePath); + var test = new PathString(testPath); - var result = source.StartsWithSegments(test); + var result = source.StartsWithSegments(test); - Assert.Equal(expectedResult, result); - } + Assert.Equal(expectedResult, result); + } - [Theory] - [InlineData("/test/path", "/TEST", true)] - [InlineData("/test/path", "/TEST/pa", false)] - [InlineData("/TEST/PATH", "/test", true)] - [InlineData("/TEST/path", "/test/pa", false)] - [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", true)] - public void StartsWithSegmentsWithRemainder_DoesACaseInsensitiveMatch(string sourcePath, string testPath, bool expectedResult) - { - var source = new PathString(sourcePath); - var test = new PathString(testPath); + [Theory] + [InlineData("/test/path", "/TEST", true)] + [InlineData("/test/path", "/TEST/pa", false)] + [InlineData("/TEST/PATH", "/test", true)] + [InlineData("/TEST/path", "/test/pa", false)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", true)] + public void StartsWithSegmentsWithRemainder_DoesACaseInsensitiveMatch(string sourcePath, string testPath, bool expectedResult) + { + var source = new PathString(sourcePath); + var test = new PathString(testPath); - var result = source.StartsWithSegments(test, out var remaining); + var result = source.StartsWithSegments(test, out var remaining); - Assert.Equal(expectedResult, result); - } + Assert.Equal(expectedResult, result); + } - [Theory] - [InlineData("/test/path", "/TEST", StringComparison.OrdinalIgnoreCase, true)] - [InlineData("/test/path", "/TEST", StringComparison.Ordinal, false)] - [InlineData("/test/path", "/TEST/pa", StringComparison.OrdinalIgnoreCase, false)] - [InlineData("/test/path", "/TEST/pa", StringComparison.Ordinal, false)] - [InlineData("/TEST/PATH", "/test", StringComparison.OrdinalIgnoreCase, true)] - [InlineData("/TEST/PATH", "/test", StringComparison.Ordinal, false)] - [InlineData("/TEST/path", "/test/pa", StringComparison.OrdinalIgnoreCase, false)] - [InlineData("/TEST/path", "/test/pa", StringComparison.Ordinal, false)] - [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", StringComparison.OrdinalIgnoreCase, true)] - [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", StringComparison.Ordinal, false)] - public void StartsWithSegments_DoesMatchUsingSpecifiedComparison(string sourcePath, string testPath, StringComparison comparison, bool expectedResult) - { - var source = new PathString(sourcePath); - var test = new PathString(testPath); + [Theory] + [InlineData("/test/path", "/TEST", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/test/path", "/TEST", StringComparison.Ordinal, false)] + [InlineData("/test/path", "/TEST/pa", StringComparison.OrdinalIgnoreCase, false)] + [InlineData("/test/path", "/TEST/pa", StringComparison.Ordinal, false)] + [InlineData("/TEST/PATH", "/test", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/TEST/PATH", "/test", StringComparison.Ordinal, false)] + [InlineData("/TEST/path", "/test/pa", StringComparison.OrdinalIgnoreCase, false)] + [InlineData("/TEST/path", "/test/pa", StringComparison.Ordinal, false)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", StringComparison.Ordinal, false)] + public void StartsWithSegments_DoesMatchUsingSpecifiedComparison(string sourcePath, string testPath, StringComparison comparison, bool expectedResult) + { + var source = new PathString(sourcePath); + var test = new PathString(testPath); - var result = source.StartsWithSegments(test, comparison); + var result = source.StartsWithSegments(test, comparison); - Assert.Equal(expectedResult, result); - } + Assert.Equal(expectedResult, result); + } - [Theory] - [InlineData("/test/path", "/TEST", StringComparison.OrdinalIgnoreCase, true)] - [InlineData("/test/path", "/TEST", StringComparison.Ordinal, false)] - [InlineData("/test/path", "/TEST/pa", StringComparison.OrdinalIgnoreCase, false)] - [InlineData("/test/path", "/TEST/pa", StringComparison.Ordinal, false)] - [InlineData("/TEST/PATH", "/test", StringComparison.OrdinalIgnoreCase, true)] - [InlineData("/TEST/PATH", "/test", StringComparison.Ordinal, false)] - [InlineData("/TEST/path", "/test/pa", StringComparison.OrdinalIgnoreCase, false)] - [InlineData("/TEST/path", "/test/pa", StringComparison.Ordinal, false)] - [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", StringComparison.OrdinalIgnoreCase, true)] - [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", StringComparison.Ordinal, false)] - public void StartsWithSegmentsWithRemainder_DoesMatchUsingSpecifiedComparison(string sourcePath, string testPath, StringComparison comparison, bool expectedResult) - { - var source = new PathString(sourcePath); - var test = new PathString(testPath); + [Theory] + [InlineData("/test/path", "/TEST", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/test/path", "/TEST", StringComparison.Ordinal, false)] + [InlineData("/test/path", "/TEST/pa", StringComparison.OrdinalIgnoreCase, false)] + [InlineData("/test/path", "/TEST/pa", StringComparison.Ordinal, false)] + [InlineData("/TEST/PATH", "/test", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/TEST/PATH", "/test", StringComparison.Ordinal, false)] + [InlineData("/TEST/path", "/test/pa", StringComparison.OrdinalIgnoreCase, false)] + [InlineData("/TEST/path", "/test/pa", StringComparison.Ordinal, false)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", StringComparison.OrdinalIgnoreCase, true)] + [InlineData("/test/PATH/path/TEST", "/TEST/path/PATH", StringComparison.Ordinal, false)] + public void StartsWithSegmentsWithRemainder_DoesMatchUsingSpecifiedComparison(string sourcePath, string testPath, StringComparison comparison, bool expectedResult) + { + var source = new PathString(sourcePath); + var test = new PathString(testPath); - var result = source.StartsWithSegments(test, comparison, out var remaining); + var result = source.StartsWithSegments(test, comparison, out var remaining); - Assert.Equal(expectedResult, result); - } + Assert.Equal(expectedResult, result); + } - [Theory] - // unreserved - [InlineData("/abc123.-_~", "/abc123.-_~")] - // colon - [InlineData("/:", "/:")] - // at - [InlineData("/@", "/@")] - // sub-delims - [InlineData("/!$&'()*+,;=", "/!$&'()*+,;=")] - // reserved - [InlineData("/?#[]", "/%3F%23%5B%5D")] - // pct-encoding - [InlineData("/单行道", "/%E5%8D%95%E8%A1%8C%E9%81%93")] - // mixed - [InlineData("/index/单行道=(x*y)[abc]", "/index/%E5%8D%95%E8%A1%8C%E9%81%93=(x*y)%5Babc%5D")] - [InlineData("/index/单行道=(x*y)[abc]_", "/index/%E5%8D%95%E8%A1%8C%E9%81%93=(x*y)%5Babc%5D_")] - // encoded - [InlineData("/http%3a%2f%2f[foo]%3A5000/", "/http%3a%2f%2f%5Bfoo%5D%3A5000/")] - [InlineData("/http%3a%2f%2f[foo]%3A5000/%", "/http%3a%2f%2f%5Bfoo%5D%3A5000/%25")] - [InlineData("/http%3a%2f%2f[foo]%3A5000/%2", "/http%3a%2f%2f%5Bfoo%5D%3A5000/%252")] - [InlineData("/http%3a%2f%2f[foo]%3A5000/%2F", "/http%3a%2f%2f%5Bfoo%5D%3A5000/%2F")] - public void ToUriComponentEscapeCorrectly(string input, string expected) - { - var path = new PathString(input); + [Theory] + // unreserved + [InlineData("/abc123.-_~", "/abc123.-_~")] + // colon + [InlineData("/:", "/:")] + // at + [InlineData("/@", "/@")] + // sub-delims + [InlineData("/!$&'()*+,;=", "/!$&'()*+,;=")] + // reserved + [InlineData("/?#[]", "/%3F%23%5B%5D")] + // pct-encoding + [InlineData("/单行道", "/%E5%8D%95%E8%A1%8C%E9%81%93")] + // mixed + [InlineData("/index/单行道=(x*y)[abc]", "/index/%E5%8D%95%E8%A1%8C%E9%81%93=(x*y)%5Babc%5D")] + [InlineData("/index/单行道=(x*y)[abc]_", "/index/%E5%8D%95%E8%A1%8C%E9%81%93=(x*y)%5Babc%5D_")] + // encoded + [InlineData("/http%3a%2f%2f[foo]%3A5000/", "/http%3a%2f%2f%5Bfoo%5D%3A5000/")] + [InlineData("/http%3a%2f%2f[foo]%3A5000/%", "/http%3a%2f%2f%5Bfoo%5D%3A5000/%25")] + [InlineData("/http%3a%2f%2f[foo]%3A5000/%2", "/http%3a%2f%2f%5Bfoo%5D%3A5000/%252")] + [InlineData("/http%3a%2f%2f[foo]%3A5000/%2F", "/http%3a%2f%2f%5Bfoo%5D%3A5000/%2F")] + public void ToUriComponentEscapeCorrectly(string input, string expected) + { + var path = new PathString(input); - Assert.Equal(expected, path.ToUriComponent()); - } + Assert.Equal(expected, path.ToUriComponent()); + } - [Fact] - public void PathStringConvertsOnlyToAndFromString() - { - var converter = TypeDescriptor.GetConverter(typeof(PathString)); - PathString result = (PathString)converter.ConvertFromInvariantString("/foo")!; - Assert.Equal("/foo", result.ToString()); - Assert.Equal("/foo", converter.ConvertTo(result, typeof(string))); - Assert.True(converter.CanConvertFrom(typeof(string))); - Assert.False(converter.CanConvertFrom(typeof(int))); - Assert.False(converter.CanConvertFrom(typeof(bool))); - Assert.True(converter.CanConvertTo(typeof(string))); - Assert.False(converter.CanConvertTo(typeof(int))); - Assert.False(converter.CanConvertTo(typeof(bool))); - } + [Fact] + public void PathStringConvertsOnlyToAndFromString() + { + var converter = TypeDescriptor.GetConverter(typeof(PathString)); + PathString result = (PathString)converter.ConvertFromInvariantString("/foo")!; + Assert.Equal("/foo", result.ToString()); + Assert.Equal("/foo", converter.ConvertTo(result, typeof(string))); + Assert.True(converter.CanConvertFrom(typeof(string))); + Assert.False(converter.CanConvertFrom(typeof(int))); + Assert.False(converter.CanConvertFrom(typeof(bool))); + Assert.True(converter.CanConvertTo(typeof(string))); + Assert.False(converter.CanConvertTo(typeof(int))); + Assert.False(converter.CanConvertTo(typeof(bool))); + } - [Fact] - public void PathStringStaysEqualAfterAssignments() - { - PathString p1 = "/?"; - string s1 = p1; - PathString p2 = s1; - Assert.Equal(p1, p2); - } + [Fact] + public void PathStringStaysEqualAfterAssignments() + { + PathString p1 = "/?"; + string s1 = p1; + PathString p2 = s1; + Assert.Equal(p1, p2); + } - [Theory] - [InlineData("/a%2Fb")] - [InlineData("/a%2F")] - [InlineData("/%2fb")] - [InlineData("/a%2Fb/c%2Fd/e")] - public void StringFromUriComponentLeavesForwardSlashEscaped(string input) - { - var sut = PathString.FromUriComponent(input); - Assert.Equal(input, sut.Value); - } + [Theory] + [InlineData("/a%2Fb")] + [InlineData("/a%2F")] + [InlineData("/%2fb")] + [InlineData("/a%2Fb/c%2Fd/e")] + public void StringFromUriComponentLeavesForwardSlashEscaped(string input) + { + var sut = PathString.FromUriComponent(input); + Assert.Equal(input, sut.Value); + } - [Theory] - [InlineData("/a%2Fb")] - [InlineData("/a%2F")] - [InlineData("/%2fb")] - [InlineData("/a%2Fb/c%2Fd/e")] - public void UriFromUriComponentLeavesForwardSlashEscaped(string input) - { - var uri = new Uri($"https://localhost:5001{input}"); - var sut = PathString.FromUriComponent(uri); - Assert.Equal(input, sut.Value); - } + [Theory] + [InlineData("/a%2Fb")] + [InlineData("/a%2F")] + [InlineData("/%2fb")] + [InlineData("/a%2Fb/c%2Fd/e")] + public void UriFromUriComponentLeavesForwardSlashEscaped(string input) + { + var uri = new Uri($"https://localhost:5001{input}"); + var sut = PathString.FromUriComponent(uri); + Assert.Equal(input, sut.Value); + } - [Theory] - [InlineData("/a%20b", "/a b")] - [InlineData("/thisMustBeAVeryLongPath/SoLongThatItCouldActuallyBeLargerToTheStackAllocThresholdValue/PathsShorterToThisAllocateLessOnHeapByUsingStackAllocation/api/a%20b", - "/thisMustBeAVeryLongPath/SoLongThatItCouldActuallyBeLargerToTheStackAllocThresholdValue/PathsShorterToThisAllocateLessOnHeapByUsingStackAllocation/api/a b")] - public void StringFromUriComponentUnescapes(string input, string expected) - { - var sut = PathString.FromUriComponent(input); - Assert.Equal(expected, sut.Value); - } + [Theory] + [InlineData("/a%20b", "/a b")] + [InlineData("/thisMustBeAVeryLongPath/SoLongThatItCouldActuallyBeLargerToTheStackAllocThresholdValue/PathsShorterToThisAllocateLessOnHeapByUsingStackAllocation/api/a%20b", + "/thisMustBeAVeryLongPath/SoLongThatItCouldActuallyBeLargerToTheStackAllocThresholdValue/PathsShorterToThisAllocateLessOnHeapByUsingStackAllocation/api/a b")] + public void StringFromUriComponentUnescapes(string input, string expected) + { + var sut = PathString.FromUriComponent(input); + Assert.Equal(expected, sut.Value); + } - [Theory] - [InlineData("/a%20b", "/a b")] - [InlineData("/thisMustBeAVeryLongPath/SoLongThatItCouldActuallyBeLargerToTheStackAllocThresholdValue/PathsShorterToThisAllocateLessOnHeapByUsingStackAllocation/api/a%20b", - "/thisMustBeAVeryLongPath/SoLongThatItCouldActuallyBeLargerToTheStackAllocThresholdValue/PathsShorterToThisAllocateLessOnHeapByUsingStackAllocation/api/a b")] - public void UriFromUriComponentUnescapes(string input, string expected) - { - var uri = new Uri($"https://localhost:5001{input}"); - var sut = PathString.FromUriComponent(uri); - Assert.Equal(expected, sut.Value); - } + [Theory] + [InlineData("/a%20b", "/a b")] + [InlineData("/thisMustBeAVeryLongPath/SoLongThatItCouldActuallyBeLargerToTheStackAllocThresholdValue/PathsShorterToThisAllocateLessOnHeapByUsingStackAllocation/api/a%20b", +"/thisMustBeAVeryLongPath/SoLongThatItCouldActuallyBeLargerToTheStackAllocThresholdValue/PathsShorterToThisAllocateLessOnHeapByUsingStackAllocation/api/a b")] + public void UriFromUriComponentUnescapes(string input, string expected) + { + var uri = new Uri($"https://localhost:5001{input}"); + var sut = PathString.FromUriComponent(uri); + Assert.Equal(expected, sut.Value); + } - [Theory] - [InlineData("/a%2Fb")] - [InlineData("/a%2F")] - [InlineData("/%2fb")] - [InlineData("/%2Fb%20c")] - [InlineData("/a%2Fb%20c")] - [InlineData("/a%20b")] - [InlineData("/a%2Fb/c%2Fd/e%20f")] - [InlineData("/%E4%BD%A0%E5%A5%BD")] - public void FromUriComponentToUriComponent(string input) - { - var sut = PathString.FromUriComponent(input); - Assert.Equal(input, sut.ToUriComponent()); - } + [Theory] + [InlineData("/a%2Fb")] + [InlineData("/a%2F")] + [InlineData("/%2fb")] + [InlineData("/%2Fb%20c")] + [InlineData("/a%2Fb%20c")] + [InlineData("/a%20b")] + [InlineData("/a%2Fb/c%2Fd/e%20f")] + [InlineData("/%E4%BD%A0%E5%A5%BD")] + public void FromUriComponentToUriComponent(string input) + { + var sut = PathString.FromUriComponent(input); + Assert.Equal(input, sut.ToUriComponent()); + } - [Theory] - [MemberData(nameof(CharsToUnescape))] - [InlineData("/%E4%BD%A0%E5%A5%BD", "/你好")] - public void FromUriComponentUnescapesAllExceptForwardSlash(string input, string expected) - { - var sut = PathString.FromUriComponent(input); - Assert.Equal(expected, sut.Value); - } + [Theory] + [MemberData(nameof(CharsToUnescape))] + [InlineData("/%E4%BD%A0%E5%A5%BD", "/你好")] + public void FromUriComponentUnescapesAllExceptForwardSlash(string input, string expected) + { + var sut = PathString.FromUriComponent(input); + Assert.Equal(expected, sut.Value); + } - [Theory] - [InlineData(-1)] - [InlineData(0)] - [InlineData(1)] - public void ExercisingStringFromUriComponentOnStackAllocLimit(int offset) - { - var path = "/"; - var testString = new string('a', PathString.StackAllocThreshold + offset - path.Length); - var sut = PathString.FromUriComponent(path + testString); - Assert.Equal(PathString.StackAllocThreshold + offset, sut.Value!.Length); - } + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + public void ExercisingStringFromUriComponentOnStackAllocLimit(int offset) + { + var path = "/"; + var testString = new string('a', PathString.StackAllocThreshold + offset - path.Length); + var sut = PathString.FromUriComponent(path + testString); + Assert.Equal(PathString.StackAllocThreshold + offset, sut.Value!.Length); + } - [Theory] - [InlineData(-1)] - [InlineData(0)] - [InlineData(1)] - public void ExercisingUriFromUriComponentOnStackAllocLimit(int offset) - { - var localhost = "https://localhost:5001/"; - var testString = new string('a', PathString.StackAllocThreshold + offset); - var sut = PathString.FromUriComponent(new Uri(localhost + testString)); - Assert.Equal(PathString.StackAllocThreshold + offset + 1, sut.Value!.Length); - } + [Theory] + [InlineData(-1)] + [InlineData(0)] + [InlineData(1)] + public void ExercisingUriFromUriComponentOnStackAllocLimit(int offset) + { + var localhost = "https://localhost:5001/"; + var testString = new string('a', PathString.StackAllocThreshold + offset); + var sut = PathString.FromUriComponent(new Uri(localhost + testString)); + Assert.Equal(PathString.StackAllocThreshold + offset + 1, sut.Value!.Length); + } - public static IEnumerable CharsToUnescape + public static IEnumerable CharsToUnescape + { + get { - get + foreach (var item in Enumerable.Range(1, 127)) { - foreach (var item in Enumerable.Range(1, 127)) + // %2F is '/' not escaped for paths + if (item != 0x2f) { - // %2F is '/' not escaped for paths - if (item != 0x2f) - { - var hexEscapedValue = "%" + item.ToString("x2", CultureInfo.InvariantCulture); - var expected = Uri.UnescapeDataString(hexEscapedValue); - yield return new object[] { "/a" + hexEscapedValue, "/a" + expected }; - } + var hexEscapedValue = "%" + item.ToString("x2", CultureInfo.InvariantCulture); + var expected = Uri.UnescapeDataString(hexEscapedValue); + yield return new object[] { "/a" + hexEscapedValue, "/a" + expected }; } } } diff --git a/src/Http/Http.Abstractions/test/QueryStringTests.cs b/src/Http/Http.Abstractions/test/QueryStringTests.cs index 1025f7a7d9..4152de93ec 100644 --- a/src/Http/Http.Abstractions/test/QueryStringTests.cs +++ b/src/Http/Http.Abstractions/test/QueryStringTests.cs @@ -9,158 +9,157 @@ using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Primitives; using Xunit; -namespace Microsoft.AspNetCore.Http.Abstractions +namespace Microsoft.AspNetCore.Http.Abstractions; + +public class QueryStringTests { - public class QueryStringTests + [Fact] + public void CtorThrows_IfQueryDoesNotHaveLeadingQuestionMark() { - [Fact] - public void CtorThrows_IfQueryDoesNotHaveLeadingQuestionMark() - { - // Act and Assert - ExceptionAssert.ThrowsArgument(() => new QueryString("hello"), "value", "The leading '?' must be included for a non-empty query."); - } + // Act and Assert + ExceptionAssert.ThrowsArgument(() => new QueryString("hello"), "value", "The leading '?' must be included for a non-empty query."); + } - [Fact] - public void CtorNullOrEmpty_Success() - { - var query = new QueryString(); - Assert.False(query.HasValue); - Assert.Null(query.Value); + [Fact] + public void CtorNullOrEmpty_Success() + { + var query = new QueryString(); + Assert.False(query.HasValue); + Assert.Null(query.Value); - query = new QueryString(null); - Assert.False(query.HasValue); - Assert.Null(query.Value); + query = new QueryString(null); + Assert.False(query.HasValue); + Assert.Null(query.Value); - query = new QueryString(string.Empty); - Assert.False(query.HasValue); - Assert.Equal(string.Empty, query.Value); - } + query = new QueryString(string.Empty); + Assert.False(query.HasValue); + Assert.Equal(string.Empty, query.Value); + } - [Fact] - public void CtorJustAQuestionMark_Success() - { - var query = new QueryString("?"); - Assert.True(query.HasValue); - Assert.Equal("?", query.Value); - } + [Fact] + public void CtorJustAQuestionMark_Success() + { + var query = new QueryString("?"); + Assert.True(query.HasValue); + Assert.Equal("?", query.Value); + } - [Fact] - public void ToString_EncodesHash() - { - var query = new QueryString("?Hello=Wor#ld"); - Assert.Equal("?Hello=Wor%23ld", query.ToString()); - } - - [Theory] - [InlineData("name", "value", "?name=value")] - [InlineData("na me", "val ue", "?na%20me=val%20ue")] - [InlineData("name", "", "?name=")] - [InlineData("name", null, "?name=")] - [InlineData("", "value", "?=value")] - [InlineData("", "", "?=")] - [InlineData("", null, "?=")] - public void CreateNameValue_Success(string name, string value, string expected) - { - var query = QueryString.Create(name, value); - Assert.Equal(expected, query.Value); - } + [Fact] + public void ToString_EncodesHash() + { + var query = new QueryString("?Hello=Wor#ld"); + Assert.Equal("?Hello=Wor%23ld", query.ToString()); + } - [Fact] - public void CreateFromList_Success() + [Theory] + [InlineData("name", "value", "?name=value")] + [InlineData("na me", "val ue", "?na%20me=val%20ue")] + [InlineData("name", "", "?name=")] + [InlineData("name", null, "?name=")] + [InlineData("", "value", "?=value")] + [InlineData("", "", "?=")] + [InlineData("", null, "?=")] + public void CreateNameValue_Success(string name, string value, string expected) + { + var query = QueryString.Create(name, value); + Assert.Equal(expected, query.Value); + } + + [Fact] + public void CreateFromList_Success() + { + var query = QueryString.Create(new[] { - var query = QueryString.Create(new[] - { new KeyValuePair("key1", "value1"), new KeyValuePair("key2", "value2"), new KeyValuePair("key3", "value3"), new KeyValuePair("key4", null), new KeyValuePair("key5", "") }); - Assert.Equal("?key1=value1&key2=value2&key3=value3&key4=&key5=", query.Value); - } + Assert.Equal("?key1=value1&key2=value2&key3=value3&key4=&key5=", query.Value); + } - [Fact] - public void CreateFromListStringValues_Success() + [Fact] + public void CreateFromListStringValues_Success() + { + var query = QueryString.Create(new[] { - var query = QueryString.Create(new[] - { new KeyValuePair("key1", new StringValues("value1")), new KeyValuePair("key2", new StringValues("value2")), new KeyValuePair("key3", new StringValues("value3")), new KeyValuePair("key4", new StringValues()), new KeyValuePair("key5", new StringValues("")), }); - Assert.Equal("?key1=value1&key2=value2&key3=value3&key4=&key5=", query.Value); - } - - [Theory] - [InlineData(null, null, null)] - [InlineData("", "", "")] - [InlineData(null, "?name2=value2", "?name2=value2")] - [InlineData("", "?name2=value2", "?name2=value2")] - [InlineData("?", "?name2=value2", "?name2=value2")] - [InlineData("?name1=value1", null, "?name1=value1")] - [InlineData("?name1=value1", "", "?name1=value1")] - [InlineData("?name1=value1", "?", "?name1=value1")] - [InlineData("?name1=value1", "?name2=value2", "?name1=value1&name2=value2")] - public void AddQueryString_Success(string query1, string query2, string expected) - { - var q1 = new QueryString(query1); - var q2 = new QueryString(query2); - Assert.Equal(expected, q1.Add(q2).Value); - Assert.Equal(expected, (q1 + q2).Value); - } - - [Theory] - [InlineData("", "", "", "?=")] - [InlineData("", "", null, "?=")] - [InlineData("?", "", "", "?=")] - [InlineData("?", "", null, "?=")] - [InlineData("?", "name2", "value2", "?name2=value2")] - [InlineData("?", "name2", "", "?name2=")] - [InlineData("?", "name2", null, "?name2=")] - [InlineData("?name1=value1", "name2", "value2", "?name1=value1&name2=value2")] - [InlineData("?name1=value1", "na me2", "val ue2", "?name1=value1&na%20me2=val%20ue2")] - [InlineData("?name1=value1", "", "", "?name1=value1&=")] - [InlineData("?name1=value1", "", null, "?name1=value1&=")] - [InlineData("?name1=value1", "name2", "", "?name1=value1&name2=")] - [InlineData("?name1=value1", "name2", null, "?name1=value1&name2=")] - public void AddNameValue_Success(string query1, string name2, string value2, string expected) - { - var q1 = new QueryString(query1); - var q2 = q1.Add(name2, value2); - Assert.Equal(expected, q2.Value); - } + Assert.Equal("?key1=value1&key2=value2&key3=value3&key4=&key5=", query.Value); + } - [Fact] - public void Equals_EmptyQueryStringAndDefaultQueryString() - { - // Act and Assert - Assert.Equal(default(QueryString), QueryString.Empty); - Assert.Equal(default(QueryString), QueryString.Empty); - // explicitly checking == operator - Assert.True(QueryString.Empty == default(QueryString)); - Assert.True(default(QueryString) == QueryString.Empty); - } - - [Fact] - public void NotEquals_DefaultQueryStringAndNonNullQueryString() - { - // Arrange - var queryString = new QueryString("?foo=1"); + [Theory] + [InlineData(null, null, null)] + [InlineData("", "", "")] + [InlineData(null, "?name2=value2", "?name2=value2")] + [InlineData("", "?name2=value2", "?name2=value2")] + [InlineData("?", "?name2=value2", "?name2=value2")] + [InlineData("?name1=value1", null, "?name1=value1")] + [InlineData("?name1=value1", "", "?name1=value1")] + [InlineData("?name1=value1", "?", "?name1=value1")] + [InlineData("?name1=value1", "?name2=value2", "?name1=value1&name2=value2")] + public void AddQueryString_Success(string query1, string query2, string expected) + { + var q1 = new QueryString(query1); + var q2 = new QueryString(query2); + Assert.Equal(expected, q1.Add(q2).Value); + Assert.Equal(expected, (q1 + q2).Value); + } + + [Theory] + [InlineData("", "", "", "?=")] + [InlineData("", "", null, "?=")] + [InlineData("?", "", "", "?=")] + [InlineData("?", "", null, "?=")] + [InlineData("?", "name2", "value2", "?name2=value2")] + [InlineData("?", "name2", "", "?name2=")] + [InlineData("?", "name2", null, "?name2=")] + [InlineData("?name1=value1", "name2", "value2", "?name1=value1&name2=value2")] + [InlineData("?name1=value1", "na me2", "val ue2", "?name1=value1&na%20me2=val%20ue2")] + [InlineData("?name1=value1", "", "", "?name1=value1&=")] + [InlineData("?name1=value1", "", null, "?name1=value1&=")] + [InlineData("?name1=value1", "name2", "", "?name1=value1&name2=")] + [InlineData("?name1=value1", "name2", null, "?name1=value1&name2=")] + public void AddNameValue_Success(string query1, string name2, string value2, string expected) + { + var q1 = new QueryString(query1); + var q2 = q1.Add(name2, value2); + Assert.Equal(expected, q2.Value); + } - // Act and Assert - Assert.NotEqual(default(QueryString), queryString); - } + [Fact] + public void Equals_EmptyQueryStringAndDefaultQueryString() + { + // Act and Assert + Assert.Equal(default(QueryString), QueryString.Empty); + Assert.Equal(default(QueryString), QueryString.Empty); + // explicitly checking == operator + Assert.True(QueryString.Empty == default(QueryString)); + Assert.True(default(QueryString) == QueryString.Empty); + } - [Fact] - public void NotEquals_EmptyQueryStringAndNonNullQueryString() - { - // Arrange - var queryString = new QueryString("?foo=1"); + [Fact] + public void NotEquals_DefaultQueryStringAndNonNullQueryString() + { + // Arrange + var queryString = new QueryString("?foo=1"); + + // Act and Assert + Assert.NotEqual(default(QueryString), queryString); + } + + [Fact] + public void NotEquals_EmptyQueryStringAndNonNullQueryString() + { + // Arrange + var queryString = new QueryString("?foo=1"); - // Act and Assert - Assert.NotEqual(queryString, QueryString.Empty); - } + // Act and Assert + Assert.NotEqual(queryString, QueryString.Empty); } } diff --git a/src/Http/Http.Abstractions/test/RouteValueDictionaryTests.cs b/src/Http/Http.Abstractions/test/RouteValueDictionaryTests.cs index a9cff69dca..9e56c70cab 100644 --- a/src/Http/Http.Abstractions/test/RouteValueDictionaryTests.cs +++ b/src/Http/Http.Abstractions/test/RouteValueDictionaryTests.cs @@ -7,1813 +7,1813 @@ using System.Linq; using Microsoft.AspNetCore.Testing; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class RouteValueDictionaryTests { - public class RouteValueDictionaryTests + [Fact] + public void DefaultCtor_UsesEmptyStorage() { - [Fact] - public void DefaultCtor_UsesEmptyStorage() - { - // Arrange - // Act - var dict = new RouteValueDictionary(); - - // Assert - Assert.Empty(dict); - Assert.Empty(dict._arrayStorage); - Assert.Null(dict._propertyStorage); - } + // Arrange + // Act + var dict = new RouteValueDictionary(); + + // Assert + Assert.Empty(dict); + Assert.Empty(dict._arrayStorage); + Assert.Null(dict._propertyStorage); + } - [Fact] - public void CreateFromNull_UsesEmptyStorage() - { - // Arrange - // Act - var dict = new RouteValueDictionary(null); - - // Assert - Assert.Empty(dict); - Assert.Empty(dict._arrayStorage); - Assert.Null(dict._propertyStorage); - } + [Fact] + public void CreateFromNull_UsesEmptyStorage() + { + // Arrange + // Act + var dict = new RouteValueDictionary(null); + + // Assert + Assert.Empty(dict); + Assert.Empty(dict._arrayStorage); + Assert.Null(dict._propertyStorage); + } - [Fact] - public void CreateFromRouteValueDictionary_WithArrayStorage_CopiesStorage() - { - // Arrange - var other = new RouteValueDictionary() + [Fact] + public void CreateFromRouteValueDictionary_WithArrayStorage_CopiesStorage() + { + // Arrange + var other = new RouteValueDictionary() { { "1", 1 } }; - // Act - var dict = new RouteValueDictionary(other); + // Act + var dict = new RouteValueDictionary(other); - // Assert - Assert.Equal(other, dict); - Assert.Single(dict._arrayStorage); - Assert.Null(dict._propertyStorage); + // Assert + Assert.Equal(other, dict); + Assert.Single(dict._arrayStorage); + Assert.Null(dict._propertyStorage); - var storage = Assert.IsType[]>(dict._arrayStorage); - var otherStorage = Assert.IsType[]>(other._arrayStorage); - Assert.NotSame(otherStorage, storage); - } + var storage = Assert.IsType[]>(dict._arrayStorage); + var otherStorage = Assert.IsType[]>(other._arrayStorage); + Assert.NotSame(otherStorage, storage); + } - [Fact] - public void CreateFromRouteValueDictionary_WithPropertyStorage_CopiesStorage() - { - // Arrange - var other = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void CreateFromRouteValueDictionary_WithPropertyStorage_CopiesStorage() + { + // Arrange + var other = new RouteValueDictionary(new { key = "value" }); - // Act - var dict = new RouteValueDictionary(other); + // Act + var dict = new RouteValueDictionary(other); - // Assert - Assert.Equal(other, dict); - AssertEmptyArrayStorage(dict); + // Assert + Assert.Equal(other, dict); + AssertEmptyArrayStorage(dict); - var storage = dict._propertyStorage; - var otherStorage = other._propertyStorage; - Assert.Same(otherStorage, storage); - } + var storage = dict._propertyStorage; + var otherStorage = other._propertyStorage; + Assert.Same(otherStorage, storage); + } - public static IEnumerable IEnumerableKeyValuePairData + public static IEnumerable IEnumerableKeyValuePairData + { + get { - get + var routeValues = new[] { - var routeValues = new[] - { new KeyValuePair("Name", "James"), new KeyValuePair("Age", 30), new KeyValuePair("Address", new Address() { City = "Redmond", State = "WA" }) }; - yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) }; + yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) }; - yield return new object[] { routeValues.ToList() }; + yield return new object[] { routeValues.ToList() }; - yield return new object[] { routeValues }; - } + yield return new object[] { routeValues }; } + } - public static IEnumerable IEnumerableStringValuePairData + public static IEnumerable IEnumerableStringValuePairData + { + get { - get + var routeValues = new[] { - var routeValues = new[] - { new KeyValuePair("First Name", "James"), new KeyValuePair("Last Name", "Henrik"), new KeyValuePair("Middle Name", "Bob") }; - yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) }; + yield return new object[] { routeValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) }; - yield return new object[] { routeValues.ToList() }; + yield return new object[] { routeValues.ToList() }; - yield return new object[] { routeValues }; - } + yield return new object[] { routeValues }; } + } - [Theory] - [MemberData(nameof(IEnumerableKeyValuePairData))] - public void CreateFromIEnumerableKeyValuePair_CopiesValues(object values) - { - // Arrange & Act - var dict = new RouteValueDictionary(values); - - // Assert - Assert.IsType[]>(dict._arrayStorage); - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => - { - Assert.Equal("Address", kvp.Key); - var address = Assert.IsType
(kvp.Value); - Assert.Equal("Redmond", address.City); - Assert.Equal("WA", address.State); - }, - kvp => { Assert.Equal("Age", kvp.Key); Assert.Equal(30, kvp.Value); }, - kvp => { Assert.Equal("Name", kvp.Key); Assert.Equal("James", kvp.Value); }); - } + [Theory] + [MemberData(nameof(IEnumerableKeyValuePairData))] + public void CreateFromIEnumerableKeyValuePair_CopiesValues(object values) + { + // Arrange & Act + var dict = new RouteValueDictionary(values); + + // Assert + Assert.IsType[]>(dict._arrayStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("Address", kvp.Key); + var address = Assert.IsType
(kvp.Value); + Assert.Equal("Redmond", address.City); + Assert.Equal("WA", address.State); + }, + kvp => { Assert.Equal("Age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("Name", kvp.Key); Assert.Equal("James", kvp.Value); }); + } - [Theory] - [MemberData(nameof(IEnumerableStringValuePairData))] - public void CreateFromIEnumerableStringValuePair_CopiesValues(object values) - { - // Arrange & Act - var dict = new RouteValueDictionary(values); - - // Assert - Assert.IsType[]>(dict._arrayStorage); - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("First Name", kvp.Key); Assert.Equal("James", kvp.Value); }, - kvp => { Assert.Equal("Last Name", kvp.Key); Assert.Equal("Henrik", kvp.Value); }, - kvp => { Assert.Equal("Middle Name", kvp.Key); Assert.Equal("Bob", kvp.Value); }); - } + [Theory] + [MemberData(nameof(IEnumerableStringValuePairData))] + public void CreateFromIEnumerableStringValuePair_CopiesValues(object values) + { + // Arrange & Act + var dict = new RouteValueDictionary(values); + + // Assert + Assert.IsType[]>(dict._arrayStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("First Name", kvp.Key); Assert.Equal("James", kvp.Value); }, + kvp => { Assert.Equal("Last Name", kvp.Key); Assert.Equal("Henrik", kvp.Value); }, + kvp => { Assert.Equal("Middle Name", kvp.Key); Assert.Equal("Bob", kvp.Value); }); + } - [Fact] - public void CreateFromIEnumerableKeyValuePair_ThrowsExceptionForDuplicateKey() - { - // Arrange - var values = new List>() + [Fact] + public void CreateFromIEnumerableKeyValuePair_ThrowsExceptionForDuplicateKey() + { + // Arrange + var values = new List>() { new KeyValuePair("name", "Billy"), new KeyValuePair("Name", "Joey"), }; - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => new RouteValueDictionary(values), - "key", - $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}."); - } + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => new RouteValueDictionary(values), + "key", + $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}."); + } - [Fact] - public void CreateFromIEnumerableStringValuePair_ThrowsExceptionForDuplicateKey() - { - // Arrange - var values = new List>() + [Fact] + public void CreateFromIEnumerableStringValuePair_ThrowsExceptionForDuplicateKey() + { + // Arrange + var values = new List>() { new KeyValuePair("name", "Billy"), new KeyValuePair("Name", "Joey"), }; - // Act & Assert - ExceptionAssert.ThrowsArgument( - () => new RouteValueDictionary(values), - "key", - $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}."); - } + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => new RouteValueDictionary(values), + "key", + $"An element with the key 'Name' already exists in the {nameof(RouteValueDictionary)}."); + } - [Fact] - public void CreateFromObject_CopiesPropertiesFromAnonymousType() - { - // Arrange - var obj = new { cool = "beans", awesome = 123 }; - - // Act - var dict = new RouteValueDictionary(obj); - - // Assert - Assert.NotNull(dict._propertyStorage); - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("awesome", kvp.Key); Assert.Equal(123, kvp.Value); }, - kvp => { Assert.Equal("cool", kvp.Key); Assert.Equal("beans", kvp.Value); }); - } + [Fact] + public void CreateFromObject_CopiesPropertiesFromAnonymousType() + { + // Arrange + var obj = new { cool = "beans", awesome = 123 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("awesome", kvp.Key); Assert.Equal(123, kvp.Value); }, + kvp => { Assert.Equal("cool", kvp.Key); Assert.Equal("beans", kvp.Value); }); + } - [Fact] - public void CreateFromObject_CopiesPropertiesFromRegularType() - { - // Arrange - var obj = new RegularType() { CoolnessFactor = 73 }; - - // Act - var dict = new RouteValueDictionary(obj); - - // Assert - Assert.NotNull(dict._propertyStorage); - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => - { - Assert.Equal("CoolnessFactor", kvp.Key); - Assert.Equal(73, kvp.Value); - }, - kvp => - { - Assert.Equal("IsAwesome", kvp.Key); - var value = Assert.IsType(kvp.Value); - Assert.False(value); - }); - } + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType() + { + // Arrange + var obj = new RegularType() { CoolnessFactor = 73 }; - [Fact] - public void CreateFromObject_CopiesPropertiesFromRegularType_PublicOnly() - { - // Arrange - var obj = new Visibility() { IsPublic = true, ItsInternalDealWithIt = 5 }; - - // Act - var dict = new RouteValueDictionary(obj); - - // Assert - Assert.NotNull(dict._propertyStorage); - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => - { - Assert.Equal("IsPublic", kvp.Key); - var value = Assert.IsType(kvp.Value); - Assert.True(value); - }); - } + // Act + var dict = new RouteValueDictionary(obj); - [Fact] - public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresStatic() - { - // Arrange - var obj = new StaticProperty(); + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("CoolnessFactor", kvp.Key); + Assert.Equal(73, kvp.Value); + }, + kvp => + { + Assert.Equal("IsAwesome", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.False(value); + }); + } - // Act - var dict = new RouteValueDictionary(obj); + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_PublicOnly() + { + // Arrange + var obj = new Visibility() { IsPublic = true, ItsInternalDealWithIt = 5 }; - // Assert - Assert.NotNull(dict._propertyStorage); - AssertEmptyArrayStorage(dict); - Assert.Empty(dict); - } + // Act + var dict = new RouteValueDictionary(obj); - [Fact] - public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresSetOnly() - { - // Arrange - var obj = new SetterOnly() { CoolSetOnly = false }; + // Assert + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("IsPublic", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.True(value); + }); + } - // Act - var dict = new RouteValueDictionary(obj); + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresStatic() + { + // Arrange + var obj = new StaticProperty(); - // Assert - Assert.NotNull(dict._propertyStorage); - AssertEmptyArrayStorage(dict); - Assert.Empty(dict); - } + // Act + var dict = new RouteValueDictionary(obj); - [Fact] - public void CreateFromObject_CopiesPropertiesFromRegularType_IncludesInherited() - { - // Arrange - var obj = new Derived() { TotallySweetProperty = true, DerivedProperty = false }; - - // Act - var dict = new RouteValueDictionary(obj); - - // Assert - Assert.NotNull(dict._propertyStorage); - AssertEmptyArrayStorage(dict); - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => - { - Assert.Equal("DerivedProperty", kvp.Key); - var value = Assert.IsType(kvp.Value); - Assert.False(value); - }, - kvp => - { - Assert.Equal("TotallySweetProperty", kvp.Key); - var value = Assert.IsType(kvp.Value); - Assert.True(value); - }); - } + // Assert + Assert.NotNull(dict._propertyStorage); + AssertEmptyArrayStorage(dict); + Assert.Empty(dict); + } - [Fact] - public void CreateFromObject_CopiesPropertiesFromRegularType_WithHiddenProperty() - { - // Arrange - var obj = new DerivedHiddenProperty() { DerivedProperty = 5 }; - - // Act - var dict = new RouteValueDictionary(obj); - - // Assert - Assert.NotNull(dict._propertyStorage); - AssertEmptyArrayStorage(dict); - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("DerivedProperty", kvp.Key); Assert.Equal(5, kvp.Value); }); - } + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_IgnoresSetOnly() + { + // Arrange + var obj = new SetterOnly() { CoolSetOnly = false }; - [Fact] - public void CreateFromObject_CopiesPropertiesFromRegularType_WithIndexerProperty() - { - // Arrange - var obj = new IndexerProperty(); + // Act + var dict = new RouteValueDictionary(obj); - // Act - var dict = new RouteValueDictionary(obj); + // Assert + Assert.NotNull(dict._propertyStorage); + AssertEmptyArrayStorage(dict); + Assert.Empty(dict); + } - // Assert - Assert.NotNull(dict._propertyStorage); - AssertEmptyArrayStorage(dict); - Assert.Empty(dict); - } + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_IncludesInherited() + { + // Arrange + var obj = new Derived() { TotallySweetProperty = true, DerivedProperty = false }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + AssertEmptyArrayStorage(dict); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("DerivedProperty", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.False(value); + }, + kvp => + { + Assert.Equal("TotallySweetProperty", kvp.Key); + var value = Assert.IsType(kvp.Value); + Assert.True(value); + }); + } - [Fact] - public void CreateFromObject_MixedCaseThrows() - { - // Arrange - var obj = new { controller = "Home", Controller = "Home" }; + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_WithHiddenProperty() + { + // Arrange + var obj = new DerivedHiddenProperty() { DerivedProperty = 5 }; + + // Act + var dict = new RouteValueDictionary(obj); + + // Assert + Assert.NotNull(dict._propertyStorage); + AssertEmptyArrayStorage(dict); + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("DerivedProperty", kvp.Key); Assert.Equal(5, kvp.Value); }); + } - var message = - $"The type '{obj.GetType().FullName}' defines properties 'controller' and 'Controller' which differ " + - $"only by casing. This is not supported by {nameof(RouteValueDictionary)} which uses " + - $"case-insensitive comparisons."; + [Fact] + public void CreateFromObject_CopiesPropertiesFromRegularType_WithIndexerProperty() + { + // Arrange + var obj = new IndexerProperty(); - // Act & Assert - var exception = Assert.Throws(() => - { - var dictionary = new RouteValueDictionary(obj); - }); + // Act + var dict = new RouteValueDictionary(obj); - // Ignoring case to make sure we're not testing reflection's ordering. - Assert.Equal(message, exception.Message, ignoreCase: true); - } + // Assert + Assert.NotNull(dict._propertyStorage); + AssertEmptyArrayStorage(dict); + Assert.Empty(dict); + } - // Our comparer is hardcoded to be OrdinalIgnoreCase no matter what. - [Fact] - public void Comparer_IsOrdinalIgnoreCase() - { - // Arrange - // Act - var dict = new RouteValueDictionary(); + [Fact] + public void CreateFromObject_MixedCaseThrows() + { + // Arrange + var obj = new { controller = "Home", Controller = "Home" }; - // Assert - Assert.Same(StringComparer.OrdinalIgnoreCase, dict.Comparer); - } + var message = + $"The type '{obj.GetType().FullName}' defines properties 'controller' and 'Controller' which differ " + + $"only by casing. This is not supported by {nameof(RouteValueDictionary)} which uses " + + $"case-insensitive comparisons."; - // Our comparer is hardcoded to be IsReadOnly==false no matter what. - [Fact] - public void IsReadOnly_False() + // Act & Assert + var exception = Assert.Throws(() => { - // Arrange - var dict = new RouteValueDictionary(); + var dictionary = new RouteValueDictionary(obj); + }); + + // Ignoring case to make sure we're not testing reflection's ordering. + Assert.Equal(message, exception.Message, ignoreCase: true); + } - // Act - var result = ((ICollection>)dict).IsReadOnly; + // Our comparer is hardcoded to be OrdinalIgnoreCase no matter what. + [Fact] + public void Comparer_IsOrdinalIgnoreCase() + { + // Arrange + // Act + var dict = new RouteValueDictionary(); - // Assert - Assert.False(result); - } + // Assert + Assert.Same(StringComparer.OrdinalIgnoreCase, dict.Comparer); + } - [Fact] - public void IndexGet_EmptyStringIsAllowed() - { - // Arrange - var dict = new RouteValueDictionary(); + // Our comparer is hardcoded to be IsReadOnly==false no matter what. + [Fact] + public void IsReadOnly_False() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - var value = dict[""]; + // Act + var result = ((ICollection>)dict).IsReadOnly; - // Assert - Assert.Null(value); - } + // Assert + Assert.False(result); + } - [Fact] - public void IndexGet_EmptyStorage_ReturnsNull() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void IndexGet_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - var value = dict["key"]; + // Act + var value = dict[""]; - // Assert - Assert.Null(value); - } + // Assert + Assert.Null(value); + } - [Fact] - public void IndexGet_PropertyStorage_NoMatch_ReturnsNull() - { - // Arrange - var dict = new RouteValueDictionary(new { age = 30 }); + [Fact] + public void IndexGet_EmptyStorage_ReturnsNull() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - var value = dict["key"]; + // Act + var value = dict["key"]; - // Assert - Assert.Null(value); - Assert.NotNull(dict._propertyStorage); - } + // Assert + Assert.Null(value); + } - [Fact] - public void IndexGet_PropertyStorage_Match_ReturnsValue() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void IndexGet_PropertyStorage_NoMatch_ReturnsNull() + { + // Arrange + var dict = new RouteValueDictionary(new { age = 30 }); - // Act - var value = dict["key"]; + // Act + var value = dict["key"]; - // Assert - Assert.Equal("value", value); - Assert.NotNull(dict._propertyStorage); - } + // Assert + Assert.Null(value); + Assert.NotNull(dict._propertyStorage); + } - [Fact] - public void IndexGet_PropertyStorage_MatchIgnoreCase_ReturnsValue() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void IndexGet_PropertyStorage_Match_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - // Act - var value = dict["kEy"]; + // Act + var value = dict["key"]; - // Assert - Assert.Equal("value", value); - Assert.NotNull(dict._propertyStorage); - } + // Assert + Assert.Equal("value", value); + Assert.NotNull(dict._propertyStorage); + } - [Fact] - public void IndexGet_ArrayStorage_NoMatch_ReturnsNull() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void IndexGet_PropertyStorage_MatchIgnoreCase_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); + + // Act + var value = dict["kEy"]; + + // Assert + Assert.Equal("value", value); + Assert.NotNull(dict._propertyStorage); + } + + [Fact] + public void IndexGet_ArrayStorage_NoMatch_ReturnsNull() + { + // Arrange + var dict = new RouteValueDictionary() { { "age", 30 }, }; - // Act - var value = dict["key"]; + // Act + var value = dict["key"]; - // Assert - Assert.Null(value); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Null(value); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void IndexGet_ListStorage_Match_ReturnsValue() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void IndexGet_ListStorage_Match_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - var value = dict["key"]; + // Act + var value = dict["key"]; - // Assert - Assert.Equal("value", value); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void IndexGet_ListStorage_MatchIgnoreCase_ReturnsValue() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void IndexGet_ListStorage_MatchIgnoreCase_ReturnsValue() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - var value = dict["kEy"]; + // Act + var value = dict["kEy"]; - // Assert - Assert.Equal("value", value); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void IndexSet_EmptyStringIsAllowed() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void IndexSet_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - dict[""] = "foo"; + // Act + dict[""] = "foo"; - // Assert - Assert.Equal("foo", dict[""]); - } + // Assert + Assert.Equal("foo", dict[""]); + } - [Fact] - public void IndexSet_EmptyStorage_UpgradesToList() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void IndexSet_EmptyStorage_UpgradesToList() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - dict["key"] = "value"; + // Act + dict["key"] = "value"; - // Assert - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void IndexSet_PropertyStorage_NoMatch_AddsValue() - { - // Arrange - var dict = new RouteValueDictionary(new { age = 30 }); - - // Act - dict["key"] = "value"; - - // Assert - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, - kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + [Fact] + public void IndexSet_PropertyStorage_NoMatch_AddsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { age = 30 }); + + // Act + dict["key"] = "value"; + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void IndexSet_PropertyStorage_Match_SetsValue() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void IndexSet_PropertyStorage_Match_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - // Act - dict["key"] = "value"; + // Act + dict["key"] = "value"; - // Assert - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void IndexSet_PropertyStorage_MatchIgnoreCase_SetsValue() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void IndexSet_PropertyStorage_MatchIgnoreCase_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - // Act - dict["kEy"] = "value"; + // Act + dict["kEy"] = "value"; - // Assert - Assert.Collection(dict, kvp => { Assert.Equal("kEy", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("kEy", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void IndexSet_ListStorage_NoMatch_AddsValue() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void IndexSet_ListStorage_NoMatch_AddsValue() + { + // Arrange + var dict = new RouteValueDictionary() { { "age", 30 }, }; - // Act - dict["key"] = "value"; + // Act + dict["key"] = "value"; - // Assert - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, - kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void IndexSet_ListStorage_Match_SetsValue() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void IndexSet_ListStorage_Match_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - dict["key"] = "value"; + // Act + dict["key"] = "value"; - // Assert - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void IndexSet_ListStorage_MatchIgnoreCase_SetsValue() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void IndexSet_ListStorage_MatchIgnoreCase_SetsValue() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - dict["key"] = "value"; + // Act + dict["key"] = "value"; - // Assert - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Count_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void Count_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - var count = dict.Count; + // Act + var count = dict.Count; - // Assert - Assert.Equal(0, count); - } + // Assert + Assert.Equal(0, count); + } - [Fact] - public void Count_PropertyStorage() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value", }); + [Fact] + public void Count_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); - // Act - var count = dict.Count; + // Act + var count = dict.Count; - // Assert - Assert.Equal(1, count); - Assert.NotNull(dict._propertyStorage); - } + // Assert + Assert.Equal(1, count); + Assert.NotNull(dict._propertyStorage); + } - [Fact] - public void Count_ListStorage() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Count_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - var count = dict.Count; + // Act + var count = dict.Count; - // Assert - Assert.Equal(1, count); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Equal(1, count); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Keys_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void Keys_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - var keys = dict.Keys; + // Act + var keys = dict.Keys; - // Assert - Assert.Empty(keys); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Empty(keys); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Keys_PropertyStorage() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value", }); + [Fact] + public void Keys_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); - // Act - var keys = dict.Keys; + // Act + var keys = dict.Keys; - // Assert - Assert.Equal(new[] { "key" }, keys); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Equal(new[] { "key" }, keys); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Keys_ListStorage() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Keys_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - var keys = dict.Keys; + // Act + var keys = dict.Keys; - // Assert - Assert.Equal(new[] { "key" }, keys); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Equal(new[] { "key" }, keys); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Values_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void Values_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - var values = dict.Values; + // Act + var values = dict.Values; - // Assert - Assert.Empty(values); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Empty(values); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Values_PropertyStorage() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value", }); + [Fact] + public void Values_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); - // Act - var values = dict.Values; + // Act + var values = dict.Values; - // Assert - Assert.Equal(new object[] { "value" }, values); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Equal(new object[] { "value" }, values); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Values_ListStorage() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Values_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - var values = dict.Values; + // Act + var values = dict.Values; - // Assert - Assert.Equal(new object[] { "value" }, values); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Equal(new object[] { "value" }, values); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Add_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void Add_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - dict.Add("key", "value"); + // Act + dict.Add("key", "value"); - // Assert - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Add_EmptyStringIsAllowed() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void Add_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - dict.Add("", "foo"); + // Act + dict.Add("", "foo"); - // Assert - Assert.Equal("foo", dict[""]); - } + // Assert + Assert.Equal("foo", dict[""]); + } - [Fact] - public void Add_PropertyStorage() - { - // Arrange - var dict = new RouteValueDictionary(new { age = 30 }); - - // Act - dict.Add("key", "value"); - - // Assert - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, - kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - - // The upgrade from property -> array should make space for at least 4 entries - Assert.Collection( - dict._arrayStorage, - kvp => Assert.Equal(new KeyValuePair("age", 30), kvp), - kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp)); - } + [Fact] + public void Add_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { age = 30 }); + + // Act + dict.Add("key", "value"); + + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + + // The upgrade from property -> array should make space for at least 4 entries + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("age", 30), kvp), + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } - [Fact] - public void Add_ListStorage() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Add_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() { { "age", 30 }, }; - // Act - dict.Add("key", "value"); + // Act + dict.Add("key", "value"); - // Assert - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, - kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Add_DuplicateKey() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Add_DuplicateKey() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - var message = $"An element with the key 'key' already exists in the {nameof(RouteValueDictionary)}"; + var message = $"An element with the key 'key' already exists in the {nameof(RouteValueDictionary)}"; - // Act & Assert - ExceptionAssert.ThrowsArgument(() => dict.Add("key", "value2"), "key", message); + // Act & Assert + ExceptionAssert.ThrowsArgument(() => dict.Add("key", "value2"), "key", message); - // Assert - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Add_DuplicateKey_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Add_DuplicateKey_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - var message = $"An element with the key 'kEy' already exists in the {nameof(RouteValueDictionary)}"; + var message = $"An element with the key 'kEy' already exists in the {nameof(RouteValueDictionary)}"; - // Act & Assert - ExceptionAssert.ThrowsArgument(() => dict.Add("kEy", "value2"), "key", message); + // Act & Assert + ExceptionAssert.ThrowsArgument(() => dict.Add("kEy", "value2"), "key", message); - // Assert - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Add_KeyValuePair() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Add_KeyValuePair() + { + // Arrange + var dict = new RouteValueDictionary() { { "age", 30 }, }; - // Act - ((ICollection>)dict).Add(new KeyValuePair("key", "value")); + // Act + ((ICollection>)dict).Add(new KeyValuePair("key", "value")); - // Assert - Assert.Collection( - dict.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, - kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.Collection( + dict.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("age", kvp.Key); Assert.Equal(30, kvp.Value); }, + kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Clear_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void Clear_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - dict.Clear(); + // Act + dict.Clear(); - // Assert - Assert.Empty(dict); - } + // Assert + Assert.Empty(dict); + } - [Fact] - public void Clear_PropertyStorage_AlreadyEmpty() - { - // Arrange - var dict = new RouteValueDictionary(new { }); + [Fact] + public void Clear_PropertyStorage_AlreadyEmpty() + { + // Arrange + var dict = new RouteValueDictionary(new { }); - // Act - dict.Clear(); + // Act + dict.Clear(); - // Assert - Assert.Empty(dict); - Assert.NotNull(dict._propertyStorage); - AssertEmptyArrayStorage(dict); - } + // Assert + Assert.Empty(dict); + Assert.NotNull(dict._propertyStorage); + AssertEmptyArrayStorage(dict); + } - [Fact] - public void Clear_PropertyStorage() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void Clear_PropertyStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - // Act - dict.Clear(); + // Act + dict.Clear(); - // Assert - Assert.Empty(dict); - Assert.Null(dict._propertyStorage); - Assert.Empty(dict._arrayStorage); - } + // Assert + Assert.Empty(dict); + Assert.Null(dict._propertyStorage); + Assert.Empty(dict._arrayStorage); + } - [Fact] - public void Clear_ListStorage() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Clear_ListStorage() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - dict.Clear(); + // Act + dict.Clear(); - // Assert - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - Assert.Null(dict._propertyStorage); - } + // Assert + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + Assert.Null(dict._propertyStorage); + } - [Fact] - public void Contains_ListStorage_KeyValuePair_True() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Contains_ListStorage_KeyValuePair_True() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - var input = new KeyValuePair("key", "value"); + var input = new KeyValuePair("key", "value"); - // Act - var result = ((ICollection>)dict).Contains(input); + // Act + var result = ((ICollection>)dict).Contains(input); - // Assert - Assert.True(result); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.True(result); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Contains_ListStory_KeyValuePair_True_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Contains_ListStory_KeyValuePair_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - var input = new KeyValuePair("KEY", "value"); + var input = new KeyValuePair("KEY", "value"); - // Act - var result = ((ICollection>)dict).Contains(input); + // Act + var result = ((ICollection>)dict).Contains(input); - // Assert - Assert.True(result); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.True(result); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Contains_ListStorage_KeyValuePair_False() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Contains_ListStorage_KeyValuePair_False() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - var input = new KeyValuePair("other", "value"); + var input = new KeyValuePair("other", "value"); - // Act - var result = ((ICollection>)dict).Contains(input); + // Act + var result = ((ICollection>)dict).Contains(input); - // Assert - Assert.False(result); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.False(result); + Assert.IsType[]>(dict._arrayStorage); + } - // Value comparisons use the default equality comparer. - [Fact] - public void Contains_ListStorage_KeyValuePair_False_ValueComparisonIsDefault() - { - // Arrange - var dict = new RouteValueDictionary() + // Value comparisons use the default equality comparer. + [Fact] + public void Contains_ListStorage_KeyValuePair_False_ValueComparisonIsDefault() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - var input = new KeyValuePair("key", "valUE"); + var input = new KeyValuePair("key", "valUE"); - // Act - var result = ((ICollection>)dict).Contains(input); + // Act + var result = ((ICollection>)dict).Contains(input); - // Assert - Assert.False(result); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.False(result); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Contains_PropertyStorage_KeyValuePair_True() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void Contains_PropertyStorage_KeyValuePair_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - var input = new KeyValuePair("key", "value"); + var input = new KeyValuePair("key", "value"); - // Act - var result = ((ICollection>)dict).Contains(input); + // Act + var result = ((ICollection>)dict).Contains(input); - // Assert - Assert.True(result); - Assert.NotNull(dict._propertyStorage); - AssertEmptyArrayStorage(dict); - Assert.Collection( - dict, - kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); - } + // Assert + Assert.True(result); + Assert.NotNull(dict._propertyStorage); + AssertEmptyArrayStorage(dict); + Assert.Collection( + dict, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); + } - [Fact] - public void Contains_PropertyStory_KeyValuePair_True_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void Contains_PropertyStory_KeyValuePair_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - var input = new KeyValuePair("KEY", "value"); + var input = new KeyValuePair("KEY", "value"); - // Act - var result = ((ICollection>)dict).Contains(input); + // Act + var result = ((ICollection>)dict).Contains(input); - // Assert - Assert.True(result); - Assert.NotNull(dict._propertyStorage); - AssertEmptyArrayStorage(dict); - Assert.Collection( - dict, - kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); - } + // Assert + Assert.True(result); + Assert.NotNull(dict._propertyStorage); + AssertEmptyArrayStorage(dict); + Assert.Collection( + dict, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); + } - [Fact] - public void Contains_PropertyStorage_KeyValuePair_False() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void Contains_PropertyStorage_KeyValuePair_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - var input = new KeyValuePair("other", "value"); + var input = new KeyValuePair("other", "value"); - // Act - var result = ((ICollection>)dict).Contains(input); + // Act + var result = ((ICollection>)dict).Contains(input); - // Assert - Assert.False(result); - Assert.NotNull(dict._propertyStorage); - AssertEmptyArrayStorage(dict); - Assert.Collection( - dict, - kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); - } + // Assert + Assert.False(result); + Assert.NotNull(dict._propertyStorage); + AssertEmptyArrayStorage(dict); + Assert.Collection( + dict, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); + } - // Value comparisons use the default equality comparer. - [Fact] - public void Contains_PropertyStorage_KeyValuePair_False_ValueComparisonIsDefault() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + // Value comparisons use the default equality comparer. + [Fact] + public void Contains_PropertyStorage_KeyValuePair_False_ValueComparisonIsDefault() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - var input = new KeyValuePair("key", "valUE"); + var input = new KeyValuePair("key", "valUE"); - // Act - var result = ((ICollection>)dict).Contains(input); + // Act + var result = ((ICollection>)dict).Contains(input); - // Assert - Assert.False(result); - Assert.NotNull(dict._propertyStorage); - AssertEmptyArrayStorage(dict); - Assert.Collection( - dict, - kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); - } + // Assert + Assert.False(result); + Assert.NotNull(dict._propertyStorage); + AssertEmptyArrayStorage(dict); + Assert.Collection( + dict, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); + } - [Fact] - public void ContainsKey_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void ContainsKey_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - var result = dict.ContainsKey("key"); + // Act + var result = dict.ContainsKey("key"); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Fact] - public void ContainsKey_EmptyStringIsAllowed() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void ContainsKey_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - var result = dict.ContainsKey(""); + // Act + var result = dict.ContainsKey(""); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Fact] - public void ContainsKey_PropertyStorage_False() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void ContainsKey_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - // Act - var result = dict.ContainsKey("other"); + // Act + var result = dict.ContainsKey("other"); - // Assert - Assert.False(result); - Assert.NotNull(dict._propertyStorage); - AssertEmptyArrayStorage(dict); - } + // Assert + Assert.False(result); + Assert.NotNull(dict._propertyStorage); + AssertEmptyArrayStorage(dict); + } - [Fact] - public void ContainsKey_PropertyStorage_True() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void ContainsKey_PropertyStorage_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - // Act - var result = dict.ContainsKey("key"); + // Act + var result = dict.ContainsKey("key"); - // Assert - Assert.True(result); - Assert.NotNull(dict._propertyStorage); - AssertEmptyArrayStorage(dict); - } + // Assert + Assert.True(result); + Assert.NotNull(dict._propertyStorage); + AssertEmptyArrayStorage(dict); + } - [Fact] - public void ContainsKey_PropertyStorage_True_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void ContainsKey_PropertyStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - // Act - var result = dict.ContainsKey("kEy"); + // Act + var result = dict.ContainsKey("kEy"); - // Assert - Assert.True(result); - Assert.NotNull(dict._propertyStorage); - AssertEmptyArrayStorage(dict); - } + // Assert + Assert.True(result); + Assert.NotNull(dict._propertyStorage); + AssertEmptyArrayStorage(dict); + } - [Fact] - public void ContainsKey_ListStorage_False() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void ContainsKey_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - var result = dict.ContainsKey("other"); + // Act + var result = dict.ContainsKey("other"); - // Assert - Assert.False(result); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.False(result); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void ContainsKey_ListStorage_True() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void ContainsKey_ListStorage_True() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - var result = dict.ContainsKey("key"); + // Act + var result = dict.ContainsKey("key"); - // Assert - Assert.True(result); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.True(result); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void ContainsKey_ListStorage_True_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void ContainsKey_ListStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - var result = dict.ContainsKey("kEy"); + // Act + var result = dict.ContainsKey("kEy"); - // Assert - Assert.True(result); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.True(result); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void CopyTo() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void CopyTo() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - var array = new KeyValuePair[2]; + var array = new KeyValuePair[2]; - // Act - ((ICollection>)dict).CopyTo(array, 1); + // Act + ((ICollection>)dict).CopyTo(array, 1); - // Assert - Assert.Equal( - new KeyValuePair[] - { + // Assert + Assert.Equal( + new KeyValuePair[] + { default(KeyValuePair), new KeyValuePair("key", "value") - }, - array); - Assert.IsType[]>(dict._arrayStorage); - } + }, + array); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_KeyValuePair_True() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Remove_KeyValuePair_True() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - var input = new KeyValuePair("key", "value"); + var input = new KeyValuePair("key", "value"); - // Act - var result = ((ICollection>)dict).Remove(input); + // Act + var result = ((ICollection>)dict).Remove(input); - // Assert - Assert.True(result); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_KeyValuePair_True_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Remove_KeyValuePair_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - var input = new KeyValuePair("KEY", "value"); + var input = new KeyValuePair("KEY", "value"); - // Act - var result = ((ICollection>)dict).Remove(input); + // Act + var result = ((ICollection>)dict).Remove(input); - // Assert - Assert.True(result); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_KeyValuePair_False() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Remove_KeyValuePair_False() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - var input = new KeyValuePair("other", "value"); + var input = new KeyValuePair("other", "value"); - // Act - var result = ((ICollection>)dict).Remove(input); + // Act + var result = ((ICollection>)dict).Remove(input); - // Assert - Assert.False(result); - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - // Value comparisons use the default equality comparer. - [Fact] - public void Remove_KeyValuePair_False_ValueComparisonIsDefault() - { - // Arrange - var dict = new RouteValueDictionary() + // Value comparisons use the default equality comparer. + [Fact] + public void Remove_KeyValuePair_False_ValueComparisonIsDefault() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - var input = new KeyValuePair("key", "valUE"); + var input = new KeyValuePair("key", "valUE"); - // Act - var result = ((ICollection>)dict).Remove(input); + // Act + var result = ((ICollection>)dict).Remove(input); - // Assert - Assert.False(result); - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void Remove_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - var result = dict.Remove("key"); + // Act + var result = dict.Remove("key"); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Fact] - public void Remove_EmptyStringIsAllowed() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void Remove_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - var result = dict.Remove(""); + // Act + var result = dict.Remove(""); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Fact] - public void Remove_PropertyStorage_Empty() - { - // Arrange - var dict = new RouteValueDictionary(new { }); + [Fact] + public void Remove_PropertyStorage_Empty() + { + // Arrange + var dict = new RouteValueDictionary(new { }); - // Act - var result = dict.Remove("other"); + // Act + var result = dict.Remove("other"); - // Assert - Assert.False(result); - Assert.Empty(dict); - Assert.NotNull(dict._propertyStorage); - } + // Assert + Assert.False(result); + Assert.Empty(dict); + Assert.NotNull(dict._propertyStorage); + } - [Fact] - public void Remove_PropertyStorage_False() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void Remove_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - // Act - var result = dict.Remove("other"); + // Act + var result = dict.Remove("other"); - // Assert - Assert.False(result); - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_PropertyStorage_True() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void Remove_PropertyStorage_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - // Act - var result = dict.Remove("key"); + // Act + var result = dict.Remove("key"); - // Assert - Assert.True(result); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_PropertyStorage_True_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void Remove_PropertyStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - // Act - var result = dict.Remove("kEy"); + // Act + var result = dict.Remove("kEy"); - // Assert - Assert.True(result); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_ListStorage_False() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Remove_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - var result = dict.Remove("other"); + // Act + var result = dict.Remove("other"); - // Assert - Assert.False(result); - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.False(result); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_ListStorage_True() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Remove_ListStorage_True() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - var result = dict.Remove("key"); + // Act + var result = dict.Remove("key"); - // Assert - Assert.True(result); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_ListStorage_True_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Remove_ListStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - var result = dict.Remove("kEy"); + // Act + var result = dict.Remove("kEy"); - // Assert - Assert.True(result); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.True(result); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_KeyAndOutValue_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void Remove_KeyAndOutValue_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - var result = dict.Remove("key", out var removedValue); + // Act + var result = dict.Remove("key", out var removedValue); - // Assert - Assert.False(result); - Assert.Null(removedValue); - } + // Assert + Assert.False(result); + Assert.Null(removedValue); + } - [Fact] - public void Remove_KeyAndOutValue_EmptyStringIsAllowed() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void Remove_KeyAndOutValue_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - var result = dict.Remove("", out var removedValue); + // Act + var result = dict.Remove("", out var removedValue); - // Assert - Assert.False(result); - Assert.Null(removedValue); - } + // Assert + Assert.False(result); + Assert.Null(removedValue); + } - [Fact] - public void Remove_KeyAndOutValue_PropertyStorage_Empty() - { - // Arrange - var dict = new RouteValueDictionary(new { }); + [Fact] + public void Remove_KeyAndOutValue_PropertyStorage_Empty() + { + // Arrange + var dict = new RouteValueDictionary(new { }); - // Act - var result = dict.Remove("other", out var removedValue); + // Act + var result = dict.Remove("other", out var removedValue); - // Assert - Assert.False(result); - Assert.Null(removedValue); - Assert.Empty(dict); - Assert.NotNull(dict._propertyStorage); - } + // Assert + Assert.False(result); + Assert.Null(removedValue); + Assert.Empty(dict); + Assert.NotNull(dict._propertyStorage); + } - [Fact] - public void Remove_KeyAndOutValue_PropertyStorage_False() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void Remove_KeyAndOutValue_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - // Act - var result = dict.Remove("other", out var removedValue); + // Act + var result = dict.Remove("other", out var removedValue); - // Assert - Assert.False(result); - Assert.Null(removedValue); - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.False(result); + Assert.Null(removedValue); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_KeyAndOutValue_PropertyStorage_True() - { - // Arrange - object value = "value"; - var dict = new RouteValueDictionary(new { key = value }); - - // Act - var result = dict.Remove("key", out var removedValue); - - // Assert - Assert.True(result); - Assert.Same(value, removedValue); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } + [Fact] + public void Remove_KeyAndOutValue_PropertyStorage_True() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary(new { key = value }); + + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_KeyAndOutValue_PropertyStorage_True_CaseInsensitive() - { - // Arrange - object value = "value"; - var dict = new RouteValueDictionary(new { key = value }); - - // Act - var result = dict.Remove("kEy", out var removedValue); - - // Assert - Assert.True(result); - Assert.Same(value, removedValue); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } + [Fact] + public void Remove_KeyAndOutValue_PropertyStorage_True_CaseInsensitive() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary(new { key = value }); + + // Act + var result = dict.Remove("kEy", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_KeyAndOutValue_ListStorage_False() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void Remove_KeyAndOutValue_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - var result = dict.Remove("other", out var removedValue); + // Act + var result = dict.Remove("other", out var removedValue); - // Assert - Assert.False(result); - Assert.Null(removedValue); - Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.False(result); + Assert.Null(removedValue); + Assert.Collection(dict, kvp => { Assert.Equal("key", kvp.Key); Assert.Equal("value", kvp.Value); }); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_KeyAndOutValue_ListStorage_True() - { - // Arrange - object value = "value"; - var dict = new RouteValueDictionary() + [Fact] + public void Remove_KeyAndOutValue_ListStorage_True() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary() { { "key", value } }; - // Act - var result = dict.Remove("key", out var removedValue); + // Act + var result = dict.Remove("key", out var removedValue); - // Assert - Assert.True(result); - Assert.Same(value, removedValue); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_KeyAndOutValue_ListStorage_True_CaseInsensitive() - { - // Arrange - object value = "value"; - var dict = new RouteValueDictionary() + [Fact] + public void Remove_KeyAndOutValue_ListStorage_True_CaseInsensitive() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary() { { "key", value } }; - // Act - var result = dict.Remove("kEy", out var removedValue); + // Act + var result = dict.Remove("kEy", out var removedValue); - // Assert - Assert.True(result); - Assert.Same(value, removedValue); - Assert.Empty(dict); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Empty(dict); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_KeyAndOutValue_ListStorage_KeyExists_First() - { - // Arrange - object value = "value"; - var dict = new RouteValueDictionary() + [Fact] + public void Remove_KeyAndOutValue_ListStorage_KeyExists_First() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary() { { "key", value }, { "other", 5 }, { "dotnet", "rocks" } }; - // Act - var result = dict.Remove("key", out var removedValue); - - // Assert - Assert.True(result); - Assert.Same(value, removedValue); - Assert.Equal(2, dict.Count); - Assert.False(dict.ContainsKey("key")); - Assert.True(dict.ContainsKey("other")); - Assert.True(dict.ContainsKey("dotnet")); - Assert.IsType[]>(dict._arrayStorage); - } + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Equal(2, dict.Count); + Assert.False(dict.ContainsKey("key")); + Assert.True(dict.ContainsKey("other")); + Assert.True(dict.ContainsKey("dotnet")); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_KeyAndOutValue_ListStorage_KeyExists_Middle() - { - // Arrange - object value = "value"; - var dict = new RouteValueDictionary() + [Fact] + public void Remove_KeyAndOutValue_ListStorage_KeyExists_Middle() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary() { { "other", 5 }, { "key", value }, { "dotnet", "rocks" } }; - // Act - var result = dict.Remove("key", out var removedValue); - - // Assert - Assert.True(result); - Assert.Same(value, removedValue); - Assert.Equal(2, dict.Count); - Assert.False(dict.ContainsKey("key")); - Assert.True(dict.ContainsKey("other")); - Assert.True(dict.ContainsKey("dotnet")); - Assert.IsType[]>(dict._arrayStorage); - } + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Equal(2, dict.Count); + Assert.False(dict.ContainsKey("key")); + Assert.True(dict.ContainsKey("other")); + Assert.True(dict.ContainsKey("dotnet")); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void Remove_KeyAndOutValue_ListStorage_KeyExists_Last() - { - // Arrange - object value = "value"; - var dict = new RouteValueDictionary() + [Fact] + public void Remove_KeyAndOutValue_ListStorage_KeyExists_Last() + { + // Arrange + object value = "value"; + var dict = new RouteValueDictionary() { { "other", 5 }, { "dotnet", "rocks" }, { "key", value } }; - // Act - var result = dict.Remove("key", out var removedValue); - - // Assert - Assert.True(result); - Assert.Same(value, removedValue); - Assert.Equal(2, dict.Count); - Assert.False(dict.ContainsKey("key")); - Assert.True(dict.ContainsKey("other")); - Assert.True(dict.ContainsKey("dotnet")); - Assert.IsType[]>(dict._arrayStorage); - } + // Act + var result = dict.Remove("key", out var removedValue); + + // Assert + Assert.True(result); + Assert.Same(value, removedValue); + Assert.Equal(2, dict.Count); + Assert.False(dict.ContainsKey("key")); + Assert.True(dict.ContainsKey("other")); + Assert.True(dict.ContainsKey("dotnet")); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void TryAdd_EmptyStringIsAllowed() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void TryAdd_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - var result = dict.TryAdd("", "foo"); + // Act + var result = dict.TryAdd("", "foo"); - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void TryAdd_PropertyStorage_KeyDoesNotExist_ConvertsPropertyStorageToArrayStorage() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value", }); - - // Act - var result = dict.TryAdd("otherKey", "value"); - - // Assert - Assert.True(result); - Assert.Null(dict._propertyStorage); - Assert.Collection( - dict._arrayStorage, - kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), - kvp => Assert.Equal(new KeyValuePair("otherKey", "value"), kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp)); - } + [Fact] + public void TryAdd_PropertyStorage_KeyDoesNotExist_ConvertsPropertyStorageToArrayStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var result = dict.TryAdd("otherKey", "value"); + + // Assert + Assert.True(result); + Assert.Null(dict._propertyStorage); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), + kvp => Assert.Equal(new KeyValuePair("otherKey", "value"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } - [Fact] - public void TryAdd_PropertyStory_KeyExist_DoesNotConvertPropertyStorageToArrayStorage() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value", }); - - // Act - var result = dict.TryAdd("key", "value"); - - // Assert - Assert.False(result); - AssertEmptyArrayStorage(dict); - Assert.NotNull(dict._propertyStorage); - Assert.Collection( - dict, - kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); - } + [Fact] + public void TryAdd_PropertyStory_KeyExist_DoesNotConvertPropertyStorageToArrayStorage() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value", }); + + // Act + var result = dict.TryAdd("key", "value"); + + // Assert + Assert.False(result); + AssertEmptyArrayStorage(dict); + Assert.NotNull(dict._propertyStorage); + Assert.Collection( + dict, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp)); + } - [Fact] - public void TryAdd_EmptyStorage_CanAdd() - { - // Arrange - var dict = new RouteValueDictionary(); - - // Act - var result = dict.TryAdd("key", "value"); - - // Assert - Assert.True(result); - Assert.Collection( - dict._arrayStorage, - kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp)); - } + [Fact] + public void TryAdd_EmptyStorage_CanAdd() + { + // Arrange + var dict = new RouteValueDictionary(); + + // Act + var result = dict.TryAdd("key", "value"); + + // Assert + Assert.True(result); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key", "value"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } - [Fact] - public void TryAdd_ArrayStorage_CanAdd() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void TryAdd_ArrayStorage_CanAdd() + { + // Arrange + var dict = new RouteValueDictionary() { { "key0", "value0" }, }; - // Act - var result = dict.TryAdd("key1", "value1"); - - // Assert - Assert.True(result); - Assert.Collection( - dict._arrayStorage, - kvp => Assert.Equal(new KeyValuePair("key0", "value0"), kvp), - kvp => Assert.Equal(new KeyValuePair("key1", "value1"), kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp)); - } + // Act + var result = dict.TryAdd("key1", "value1"); + + // Assert + Assert.True(result); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key0", "value0"), kvp), + kvp => Assert.Equal(new KeyValuePair("key1", "value1"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } - [Fact] - public void TryAdd_ArrayStorage_CanAddWithResize() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void TryAdd_ArrayStorage_CanAddWithResize() + { + // Arrange + var dict = new RouteValueDictionary() { { "key0", "value0" }, { "key1", "value1" }, @@ -1821,261 +1821,261 @@ namespace Microsoft.AspNetCore.Routing.Tests { "key3", "value3" }, }; - // Act - var result = dict.TryAdd("key4", "value4"); - - // Assert - Assert.True(result); - Assert.Collection( - dict._arrayStorage, - kvp => Assert.Equal(new KeyValuePair("key0", "value0"), kvp), - kvp => Assert.Equal(new KeyValuePair("key1", "value1"), kvp), - kvp => Assert.Equal(new KeyValuePair("key2", "value2"), kvp), - kvp => Assert.Equal(new KeyValuePair("key3", "value3"), kvp), - kvp => Assert.Equal(new KeyValuePair("key4", "value4"), kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp)); - } + // Act + var result = dict.TryAdd("key4", "value4"); + + // Assert + Assert.True(result); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key0", "value0"), kvp), + kvp => Assert.Equal(new KeyValuePair("key1", "value1"), kvp), + kvp => Assert.Equal(new KeyValuePair("key2", "value2"), kvp), + kvp => Assert.Equal(new KeyValuePair("key3", "value3"), kvp), + kvp => Assert.Equal(new KeyValuePair("key4", "value4"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } - [Fact] - public void TryAdd_ArrayStorage_DoesNotAddWhenKeyIsPresent() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void TryAdd_ArrayStorage_DoesNotAddWhenKeyIsPresent() + { + // Arrange + var dict = new RouteValueDictionary() { { "key0", "value0" }, }; - // Act - var result = dict.TryAdd("key0", "value1"); - - // Assert - Assert.False(result); - Assert.Collection( - dict._arrayStorage, - kvp => Assert.Equal(new KeyValuePair("key0", "value0"), kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp), - kvp => Assert.Equal(default, kvp)); - } + // Act + var result = dict.TryAdd("key0", "value1"); + + // Assert + Assert.False(result); + Assert.Collection( + dict._arrayStorage, + kvp => Assert.Equal(new KeyValuePair("key0", "value0"), kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp), + kvp => Assert.Equal(default, kvp)); + } - [Fact] - public void TryGetValue_EmptyStorage() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void TryGetValue_EmptyStorage() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - var result = dict.TryGetValue("key", out var value); + // Act + var result = dict.TryGetValue("key", out var value); - // Assert - Assert.False(result); - Assert.Null(value); - } + // Assert + Assert.False(result); + Assert.Null(value); + } - [Fact] - public void TryGetValue_EmptyStringIsAllowed() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void TryGetValue_EmptyStringIsAllowed() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act - var result = dict.TryGetValue("", out var value); + // Act + var result = dict.TryGetValue("", out var value); - // Assert - Assert.False(result); - Assert.Null(value); - } + // Assert + Assert.False(result); + Assert.Null(value); + } - [Fact] - public void TryGetValue_PropertyStorage_False() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void TryGetValue_PropertyStorage_False() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - // Act - var result = dict.TryGetValue("other", out var value); + // Act + var result = dict.TryGetValue("other", out var value); - // Assert - Assert.False(result); - Assert.Null(value); - Assert.NotNull(dict._propertyStorage); - } + // Assert + Assert.False(result); + Assert.Null(value); + Assert.NotNull(dict._propertyStorage); + } - [Fact] - public void TryGetValue_PropertyStorage_True() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void TryGetValue_PropertyStorage_True() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - // Act - var result = dict.TryGetValue("key", out var value); + // Act + var result = dict.TryGetValue("key", out var value); - // Assert - Assert.True(result); - Assert.Equal("value", value); - Assert.NotNull(dict._propertyStorage); - } + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.NotNull(dict._propertyStorage); + } - [Fact] - public void TryGetValue_PropertyStorage_True_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary(new { key = "value" }); + [Fact] + public void TryGetValue_PropertyStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary(new { key = "value" }); - // Act - var result = dict.TryGetValue("kEy", out var value); + // Act + var result = dict.TryGetValue("kEy", out var value); - // Assert - Assert.True(result); - Assert.Equal("value", value); - Assert.NotNull(dict._propertyStorage); - } + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.NotNull(dict._propertyStorage); + } - [Fact] - public void TryGetValue_ListStorage_False() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void TryGetValue_ListStorage_False() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - var result = dict.TryGetValue("other", out var value); + // Act + var result = dict.TryGetValue("other", out var value); - // Assert - Assert.False(result); - Assert.Null(value); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.False(result); + Assert.Null(value); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void TryGetValue_ListStorage_True() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void TryGetValue_ListStorage_True() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - var result = dict.TryGetValue("key", out var value); + // Act + var result = dict.TryGetValue("key", out var value); - // Assert - Assert.True(result); - Assert.Equal("value", value); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void TryGetValue_ListStorage_True_CaseInsensitive() - { - // Arrange - var dict = new RouteValueDictionary() + [Fact] + public void TryGetValue_ListStorage_True_CaseInsensitive() + { + // Arrange + var dict = new RouteValueDictionary() { { "key", "value" }, }; - // Act - var result = dict.TryGetValue("kEy", out var value); + // Act + var result = dict.TryGetValue("kEy", out var value); - // Assert - Assert.True(result); - Assert.Equal("value", value); - Assert.IsType[]>(dict._arrayStorage); - } + // Assert + Assert.True(result); + Assert.Equal("value", value); + Assert.IsType[]>(dict._arrayStorage); + } - [Fact] - public void ListStorage_DynamicallyAdjustsCapacity() - { - // Arrange - var dict = new RouteValueDictionary(); + [Fact] + public void ListStorage_DynamicallyAdjustsCapacity() + { + // Arrange + var dict = new RouteValueDictionary(); - // Act 1 - dict.Add("key", "value"); + // Act 1 + dict.Add("key", "value"); - // Assert 1 - var storage = Assert.IsType[]>(dict._arrayStorage); - Assert.Equal(4, storage.Length); + // Assert 1 + var storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(4, storage.Length); - // Act 2 - dict.Add("key2", "value2"); - dict.Add("key3", "value3"); - dict.Add("key4", "value4"); - dict.Add("key5", "value5"); + // Act 2 + dict.Add("key2", "value2"); + dict.Add("key3", "value3"); + dict.Add("key4", "value4"); + dict.Add("key5", "value5"); - // Assert 2 - storage = Assert.IsType[]>(dict._arrayStorage); - Assert.Equal(8, storage.Length); - } + // Assert 2 + storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(8, storage.Length); + } - [Fact] - public void ListStorage_RemoveAt_RearrangesInnerArray() - { - // Arrange - var dict = new RouteValueDictionary(); - dict.Add("key", "value"); - dict.Add("key2", "value2"); - dict.Add("key3", "value3"); - - // Assert 1 - var storage = Assert.IsType[]>(dict._arrayStorage); - Assert.Equal(3, dict.Count); - - // Act - dict.Remove("key2"); - - // Assert 2 - storage = Assert.IsType[]>(dict._arrayStorage); - Assert.Equal(2, dict.Count); - Assert.Equal("key", storage[0].Key); - Assert.Equal("value", storage[0].Value); - Assert.Equal("key3", storage[1].Key); - Assert.Equal("value3", storage[1].Value); - } + [Fact] + public void ListStorage_RemoveAt_RearrangesInnerArray() + { + // Arrange + var dict = new RouteValueDictionary(); + dict.Add("key", "value"); + dict.Add("key2", "value2"); + dict.Add("key3", "value3"); + + // Assert 1 + var storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(3, dict.Count); + + // Act + dict.Remove("key2"); + + // Assert 2 + storage = Assert.IsType[]>(dict._arrayStorage); + Assert.Equal(2, dict.Count); + Assert.Equal("key", storage[0].Key); + Assert.Equal("value", storage[0].Value); + Assert.Equal("key3", storage[1].Key); + Assert.Equal("value3", storage[1].Value); + } - [Fact] - public void FromArray_TakesOwnershipOfArray() + [Fact] + public void FromArray_TakesOwnershipOfArray() + { + // Arrange + var array = new KeyValuePair[] { - // Arrange - var array = new KeyValuePair[] - { new KeyValuePair("a", 0), new KeyValuePair("b", 1), new KeyValuePair("c", 2), - }; + }; - var dictionary = RouteValueDictionary.FromArray(array); + var dictionary = RouteValueDictionary.FromArray(array); - // Act - modifying the array should modify the dictionary - array[0] = new KeyValuePair("aa", 10); + // Act - modifying the array should modify the dictionary + array[0] = new KeyValuePair("aa", 10); - // Assert - Assert.Equal(3, dictionary.Count); - Assert.Equal(10, dictionary["aa"]); - } + // Assert + Assert.Equal(3, dictionary.Count); + Assert.Equal(10, dictionary["aa"]); + } - [Fact] - public void FromArray_EmptyArray() - { - // Arrange - var array = Array.Empty>(); + [Fact] + public void FromArray_EmptyArray() + { + // Arrange + var array = Array.Empty>(); - // Act - var dictionary = RouteValueDictionary.FromArray(array); + // Act + var dictionary = RouteValueDictionary.FromArray(array); - // Assert - Assert.Empty(dictionary); - } + // Assert + Assert.Empty(dictionary); + } - [Fact] - public void FromArray_RemovesGapsInArray() + [Fact] + public void FromArray_RemovesGapsInArray() + { + // Arrange + var array = new KeyValuePair[] { - // Arrange - var array = new KeyValuePair[] - { new KeyValuePair(null!, null), new KeyValuePair("a", 0), new KeyValuePair(null!, null), @@ -2084,16 +2084,16 @@ namespace Microsoft.AspNetCore.Routing.Tests new KeyValuePair("c", 2), new KeyValuePair("d", 3), new KeyValuePair(null!, null), - }; + }; - // Act - calling From should modify the array - var dictionary = RouteValueDictionary.FromArray(array); + // Act - calling From should modify the array + var dictionary = RouteValueDictionary.FromArray(array); - // Assert - Assert.Equal(4, dictionary.Count); - Assert.Equal( - new KeyValuePair[] - { + // Assert + Assert.Equal(4, dictionary.Count); + Assert.Equal( + new KeyValuePair[] + { new KeyValuePair("d", 3), new KeyValuePair("a", 0), new KeyValuePair("c", 2), @@ -2102,72 +2102,71 @@ namespace Microsoft.AspNetCore.Routing.Tests new KeyValuePair(null!, null), new KeyValuePair(null!, null), new KeyValuePair(null!, null), - }, - array); - } + }, + array); + } - private void AssertEmptyArrayStorage(RouteValueDictionary value) - { - Assert.Same(Array.Empty>(), value._arrayStorage); - } + private void AssertEmptyArrayStorage(RouteValueDictionary value) + { + Assert.Same(Array.Empty>(), value._arrayStorage); + } - private class RegularType - { - public bool IsAwesome { get; set; } + private class RegularType + { + public bool IsAwesome { get; set; } - public int CoolnessFactor { get; set; } - } + public int CoolnessFactor { get; set; } + } - private class Visibility - { - private string? PrivateYo { get; set; } + private class Visibility + { + private string? PrivateYo { get; set; } - internal int ItsInternalDealWithIt { get; set; } + internal int ItsInternalDealWithIt { get; set; } - public bool IsPublic { get; set; } - } + public bool IsPublic { get; set; } + } - private class StaticProperty - { - public static bool IsStatic { get; set; } - } + private class StaticProperty + { + public static bool IsStatic { get; set; } + } - private class SetterOnly - { - private bool _coolSetOnly; + private class SetterOnly + { + private bool _coolSetOnly; - public bool CoolSetOnly { set { _coolSetOnly = value; } } - } + public bool CoolSetOnly { set { _coolSetOnly = value; } } + } - private class Base - { - public bool DerivedProperty { get; set; } - } + private class Base + { + public bool DerivedProperty { get; set; } + } - private class Derived : Base - { - public bool TotallySweetProperty { get; set; } - } + private class Derived : Base + { + public bool TotallySweetProperty { get; set; } + } - private class DerivedHiddenProperty : Base - { - public new int DerivedProperty { get; set; } - } + private class DerivedHiddenProperty : Base + { + public new int DerivedProperty { get; set; } + } - private class IndexerProperty + private class IndexerProperty + { + public bool this[string key] { - public bool this[string key] - { - get { return false; } - set { } - } + get { return false; } + set { } } + } - private class Address - { - public string? City { get; set; } + private class Address + { + public string? City { get; set; } - public string? State { get; set; } - } + public string? State { get; set; } } } diff --git a/src/Http/Http.Abstractions/test/UseExtensionsTests.cs b/src/Http/Http.Abstractions/test/UseExtensionsTests.cs index 2fe3563e1f..3fc3aed617 100644 --- a/src/Http/Http.Abstractions/test/UseExtensionsTests.cs +++ b/src/Http/Http.Abstractions/test/UseExtensionsTests.cs @@ -6,75 +6,74 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; -namespace Microsoft.AspNetCore.Builder.Extensions +namespace Microsoft.AspNetCore.Builder.Extensions; + +public class UseExtensionsTests { - public class UseExtensionsTests + [Fact] + public async Task UseCallsNextMiddleware() { - [Fact] - public async Task UseCallsNextMiddleware() + // Arrange + var builder = new ApplicationBuilder(serviceProvider: null!); + var context = new DefaultHttpContext(); + var firstCalled = false; + var secondCalled = false; + var lastCalled = false; + + builder.Use((context, next) => + { + firstCalled = true; + return next(); + }); + builder.Use((context, next) => + { + Assert.True(firstCalled); + secondCalled = true; + return next(context); + }); + builder.Run(context => { - // Arrange - var builder = new ApplicationBuilder(serviceProvider: null!); - var context = new DefaultHttpContext(); - var firstCalled = false; - var secondCalled = false; - var lastCalled = false; + Assert.True(secondCalled); + lastCalled = true; + return Task.CompletedTask; + }); - builder.Use((context, next) => - { - firstCalled = true; - return next(); - }); - builder.Use((context, next) => - { - Assert.True(firstCalled); - secondCalled = true; - return next(context); - }); - builder.Run(context => - { - Assert.True(secondCalled); - lastCalled = true; - return Task.CompletedTask; - }); + // Act + await builder.Build().Invoke(context); - // Act - await builder.Build().Invoke(context); + // Assert + Assert.True(firstCalled); + Assert.True(secondCalled); + Assert.True(lastCalled); + } - // Assert - Assert.True(firstCalled); - Assert.True(secondCalled); - Assert.True(lastCalled); - } + [Fact] + public async Task ThrowFromMiddlewareFlowsBackToInvoke() + { + // Arrange + var builder = new ApplicationBuilder(serviceProvider: null!); + var context = new DefaultHttpContext(); + var shouldThrow = true; - [Fact] - public async Task ThrowFromMiddlewareFlowsBackToInvoke() + builder.Use(async (context, next) => { - // Arrange - var builder = new ApplicationBuilder(serviceProvider: null!); - var context = new DefaultHttpContext(); - var shouldThrow = true; - - builder.Use(async (context, next) => - { - throw await Assert.ThrowsAsync(() => next()); - }); - builder.Use(async (context, next) => - { - throw await Assert.ThrowsAsync(() => next(context)); - }); - builder.Run(context => + throw await Assert.ThrowsAsync(() => next()); + }); + builder.Use(async (context, next) => + { + throw await Assert.ThrowsAsync(() => next(context)); + }); + builder.Run(context => + { + if (shouldThrow) { - if (shouldThrow) - { - throw new Exception("From Use"); - } - return Task.CompletedTask; - }); + throw new Exception("From Use"); + } + return Task.CompletedTask; + }); - // Act & Assert - var ex = await Assert.ThrowsAsync(() => builder.Build().Invoke(context)); - Assert.Equal("From Use", ex.Message); - } + // Act & Assert + var ex = await Assert.ThrowsAsync(() => builder.Build().Invoke(context)); + Assert.Equal("From Use", ex.Message); } } diff --git a/src/Http/Http.Abstractions/test/UseMiddlewareTest.cs b/src/Http/Http.Abstractions/test/UseMiddlewareTest.cs index 0c89b00e32..5c2dab0b6e 100644 --- a/src/Http/Http.Abstractions/test/UseMiddlewareTest.cs +++ b/src/Http/Http.Abstractions/test/UseMiddlewareTest.cs @@ -9,368 +9,367 @@ using Microsoft.AspNetCore.Builder.Internal; using Microsoft.AspNetCore.Http.Abstractions; using Xunit; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +public class UseMiddlewareTest { - public class UseMiddlewareTest + [Fact] + public void UseMiddleware_WithNoParameters_ThrowsException() { - [Fact] - public void UseMiddleware_WithNoParameters_ThrowsException() - { - var builder = new ApplicationBuilder(new DummyServiceProvider()); - builder.UseMiddleware(typeof(MiddlewareNoParametersStub)); - var exception = Assert.Throws(() => builder.Build()); - - Assert.Equal( - Resources.FormatException_UseMiddlewareNoParameters( - UseMiddlewareExtensions.InvokeMethodName, - UseMiddlewareExtensions.InvokeAsyncMethodName, - nameof(HttpContext)), - exception.Message); - } - - [Fact] - public void UseMiddleware_AsyncWithNoParameters_ThrowsException() - { - var builder = new ApplicationBuilder(new DummyServiceProvider()); - builder.UseMiddleware(typeof(MiddlewareAsyncNoParametersStub)); - var exception = Assert.Throws(() => builder.Build()); - - Assert.Equal( - Resources.FormatException_UseMiddlewareNoParameters( - UseMiddlewareExtensions.InvokeMethodName, - UseMiddlewareExtensions.InvokeAsyncMethodName, - nameof(HttpContext)), - exception.Message); - } + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareNoParametersStub)); + var exception = Assert.Throws(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddlewareNoParameters( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName, + nameof(HttpContext)), + exception.Message); + } - [Fact] - public void UseMiddleware_NonTaskReturnType_ThrowsException() - { - var builder = new ApplicationBuilder(new DummyServiceProvider()); - builder.UseMiddleware(typeof(MiddlewareNonTaskReturnStub)); - var exception = Assert.Throws(() => builder.Build()); - - Assert.Equal( - Resources.FormatException_UseMiddlewareNonTaskReturnType( - UseMiddlewareExtensions.InvokeMethodName, - UseMiddlewareExtensions.InvokeAsyncMethodName, - nameof(Task)), - exception.Message); - } + [Fact] + public void UseMiddleware_AsyncWithNoParameters_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareAsyncNoParametersStub)); + var exception = Assert.Throws(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddlewareNoParameters( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName, + nameof(HttpContext)), + exception.Message); + } - [Fact] - public void UseMiddleware_AsyncNonTaskReturnType_ThrowsException() - { - var builder = new ApplicationBuilder(new DummyServiceProvider()); - builder.UseMiddleware(typeof(MiddlewareAsyncNonTaskReturnStub)); - var exception = Assert.Throws(() => builder.Build()); - - Assert.Equal( - Resources.FormatException_UseMiddlewareNonTaskReturnType( - UseMiddlewareExtensions.InvokeMethodName, - UseMiddlewareExtensions.InvokeAsyncMethodName, - nameof(Task)), - exception.Message); - } + [Fact] + public void UseMiddleware_NonTaskReturnType_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareNonTaskReturnStub)); + var exception = Assert.Throws(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddlewareNonTaskReturnType( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName, + nameof(Task)), + exception.Message); + } - [Fact] - public void UseMiddleware_NoInvokeOrInvokeAsyncMethod_ThrowsException() - { - var builder = new ApplicationBuilder(new DummyServiceProvider()); - builder.UseMiddleware(typeof(MiddlewareNoInvokeStub)); - var exception = Assert.Throws(() => builder.Build()); - - Assert.Equal( - Resources.FormatException_UseMiddlewareNoInvokeMethod( - UseMiddlewareExtensions.InvokeMethodName, - UseMiddlewareExtensions.InvokeAsyncMethodName, typeof(MiddlewareNoInvokeStub)), - exception.Message); - } + [Fact] + public void UseMiddleware_AsyncNonTaskReturnType_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareAsyncNonTaskReturnStub)); + var exception = Assert.Throws(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddlewareNonTaskReturnType( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName, + nameof(Task)), + exception.Message); + } - [Fact] - public void UseMiddleware_MultipleInvokeMethods_ThrowsException() - { - var builder = new ApplicationBuilder(new DummyServiceProvider()); - builder.UseMiddleware(typeof(MiddlewareMultipleInvokesStub)); - var exception = Assert.Throws(() => builder.Build()); - - Assert.Equal( - Resources.FormatException_UseMiddleMutlipleInvokes( - UseMiddlewareExtensions.InvokeMethodName, - UseMiddlewareExtensions.InvokeAsyncMethodName), - exception.Message); - } + [Fact] + public void UseMiddleware_NoInvokeOrInvokeAsyncMethod_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareNoInvokeStub)); + var exception = Assert.Throws(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddlewareNoInvokeMethod( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName, typeof(MiddlewareNoInvokeStub)), + exception.Message); + } - [Fact] - public void UseMiddleware_MultipleInvokeAsyncMethods_ThrowsException() - { - var builder = new ApplicationBuilder(new DummyServiceProvider()); - builder.UseMiddleware(typeof(MiddlewareMultipleInvokeAsyncStub)); - var exception = Assert.Throws(() => builder.Build()); - - Assert.Equal( - Resources.FormatException_UseMiddleMutlipleInvokes( - UseMiddlewareExtensions.InvokeMethodName, - UseMiddlewareExtensions.InvokeAsyncMethodName), - exception.Message); - } + [Fact] + public void UseMiddleware_MultipleInvokeMethods_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareMultipleInvokesStub)); + var exception = Assert.Throws(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddleMutlipleInvokes( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName), + exception.Message); + } - [Fact] - public void UseMiddleware_MultipleInvokeAndInvokeAsyncMethods_ThrowsException() - { - var builder = new ApplicationBuilder(new DummyServiceProvider()); - builder.UseMiddleware(typeof(MiddlewareMultipleInvokeAndInvokeAsyncStub)); - var exception = Assert.Throws(() => builder.Build()); - - Assert.Equal( - Resources.FormatException_UseMiddleMutlipleInvokes( - UseMiddlewareExtensions.InvokeMethodName, - UseMiddlewareExtensions.InvokeAsyncMethodName), - exception.Message); - } + [Fact] + public void UseMiddleware_MultipleInvokeAsyncMethods_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareMultipleInvokeAsyncStub)); + var exception = Assert.Throws(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddleMutlipleInvokes( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName), + exception.Message); + } - [Fact] - public async Task UseMiddleware_ThrowsIfArgCantBeResolvedFromContainer() - { - var builder = new ApplicationBuilder(new DummyServiceProvider()); - builder.UseMiddleware(typeof(MiddlewareInjectInvokeNoService)); - var app = builder.Build(); - var exception = await Assert.ThrowsAsync(() => app(new DefaultHttpContext())); - Assert.Equal( - Resources.FormatException_InvokeMiddlewareNoService( - typeof(object), - typeof(MiddlewareInjectInvokeNoService)), - exception.Message); - } + [Fact] + public void UseMiddleware_MultipleInvokeAndInvokeAsyncMethods_ThrowsException() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareMultipleInvokeAndInvokeAsyncStub)); + var exception = Assert.Throws(() => builder.Build()); + + Assert.Equal( + Resources.FormatException_UseMiddleMutlipleInvokes( + UseMiddlewareExtensions.InvokeMethodName, + UseMiddlewareExtensions.InvokeAsyncMethodName), + exception.Message); + } - [Fact] - public void UseMiddlewareWithInvokeArg() - { - var builder = new ApplicationBuilder(new DummyServiceProvider()); - builder.UseMiddleware(typeof(MiddlewareInjectInvoke)); - var app = builder.Build(); - app(new DefaultHttpContext()); - } + [Fact] + public async Task UseMiddleware_ThrowsIfArgCantBeResolvedFromContainer() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareInjectInvokeNoService)); + var app = builder.Build(); + var exception = await Assert.ThrowsAsync(() => app(new DefaultHttpContext())); + Assert.Equal( + Resources.FormatException_InvokeMiddlewareNoService( + typeof(object), + typeof(MiddlewareInjectInvokeNoService)), + exception.Message); + } - [Fact] - public void UseMiddlewareWithInvokeWithOutAndRefThrows() - { - var mockServiceProvider = new DummyServiceProvider(); - var builder = new ApplicationBuilder(mockServiceProvider); - builder.UseMiddleware(typeof(MiddlewareInjectWithOutAndRefParams)); - var exception = Assert.Throws(() => builder.Build()); - } + [Fact] + public void UseMiddlewareWithInvokeArg() + { + var builder = new ApplicationBuilder(new DummyServiceProvider()); + builder.UseMiddleware(typeof(MiddlewareInjectInvoke)); + var app = builder.Build(); + app(new DefaultHttpContext()); + } - [Fact] - public void UseMiddlewareWithIMiddlewareThrowsIfParametersSpecified() - { - var mockServiceProvider = new DummyServiceProvider(); - var builder = new ApplicationBuilder(mockServiceProvider); - var exception = Assert.Throws(() => builder.UseMiddleware(typeof(Middleware), "arg")); - Assert.Equal(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)), exception.Message); - } + [Fact] + public void UseMiddlewareWithInvokeWithOutAndRefThrows() + { + var mockServiceProvider = new DummyServiceProvider(); + var builder = new ApplicationBuilder(mockServiceProvider); + builder.UseMiddleware(typeof(MiddlewareInjectWithOutAndRefParams)); + var exception = Assert.Throws(() => builder.Build()); + } - [Fact] - public async Task UseMiddlewareWithIMiddlewareThrowsIfNoIMiddlewareFactoryRegistered() - { - var mockServiceProvider = new DummyServiceProvider(); - var builder = new ApplicationBuilder(mockServiceProvider); - builder.UseMiddleware(typeof(Middleware)); - var app = builder.Build(); - var exception = await Assert.ThrowsAsync(async () => - { - var context = new DefaultHttpContext(); - var sp = new DummyServiceProvider(); - context.RequestServices = sp; - await app(context); - }); - Assert.Equal(Resources.FormatException_UseMiddlewareNoMiddlewareFactory(typeof(IMiddlewareFactory)), exception.Message); - } + [Fact] + public void UseMiddlewareWithIMiddlewareThrowsIfParametersSpecified() + { + var mockServiceProvider = new DummyServiceProvider(); + var builder = new ApplicationBuilder(mockServiceProvider); + var exception = Assert.Throws(() => builder.UseMiddleware(typeof(Middleware), "arg")); + Assert.Equal(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)), exception.Message); + } - [Fact] - public async Task UseMiddlewareWithIMiddlewareThrowsIfMiddlewareFactoryCreateReturnsNull() + [Fact] + public async Task UseMiddlewareWithIMiddlewareThrowsIfNoIMiddlewareFactoryRegistered() + { + var mockServiceProvider = new DummyServiceProvider(); + var builder = new ApplicationBuilder(mockServiceProvider); + builder.UseMiddleware(typeof(Middleware)); + var app = builder.Build(); + var exception = await Assert.ThrowsAsync(async () => { - var mockServiceProvider = new DummyServiceProvider(); - var builder = new ApplicationBuilder(mockServiceProvider); - builder.UseMiddleware(typeof(Middleware)); - var app = builder.Build(); - var exception = await Assert.ThrowsAsync(async () => - { - var context = new DefaultHttpContext(); - var sp = new DummyServiceProvider(); - sp.AddService(typeof(IMiddlewareFactory), new BadMiddlewareFactory()); - context.RequestServices = sp; - await app(context); - }); - - Assert.Equal( - Resources.FormatException_UseMiddlewareUnableToCreateMiddleware( - typeof(BadMiddlewareFactory), - typeof(Middleware)), - exception.Message); - } + var context = new DefaultHttpContext(); + var sp = new DummyServiceProvider(); + context.RequestServices = sp; + await app(context); + }); + Assert.Equal(Resources.FormatException_UseMiddlewareNoMiddlewareFactory(typeof(IMiddlewareFactory)), exception.Message); + } - [Fact] - public async Task UseMiddlewareWithIMiddlewareWorks() + [Fact] + public async Task UseMiddlewareWithIMiddlewareThrowsIfMiddlewareFactoryCreateReturnsNull() + { + var mockServiceProvider = new DummyServiceProvider(); + var builder = new ApplicationBuilder(mockServiceProvider); + builder.UseMiddleware(typeof(Middleware)); + var app = builder.Build(); + var exception = await Assert.ThrowsAsync(async () => { - var mockServiceProvider = new DummyServiceProvider(); - var builder = new ApplicationBuilder(mockServiceProvider); - builder.UseMiddleware(typeof(Middleware)); - var app = builder.Build(); var context = new DefaultHttpContext(); var sp = new DummyServiceProvider(); - var middlewareFactory = new BasicMiddlewareFactory(); - sp.AddService(typeof(IMiddlewareFactory), middlewareFactory); + sp.AddService(typeof(IMiddlewareFactory), new BadMiddlewareFactory()); context.RequestServices = sp; await app(context); - Assert.True(Assert.IsType(context.Items["before"])); - Assert.True(Assert.IsType(context.Items["after"])); - Assert.NotNull(middlewareFactory.Created); - Assert.NotNull(middlewareFactory.Released); - Assert.IsType(middlewareFactory.Created); - Assert.IsType(middlewareFactory.Released); - Assert.Same(middlewareFactory.Created, middlewareFactory.Released); - } + }); - public class Middleware : IMiddleware - { - public async Task InvokeAsync(HttpContext context, RequestDelegate next) - { - context.Items["before"] = true; - await next(context); - context.Items["after"] = true; - } - } + Assert.Equal( + Resources.FormatException_UseMiddlewareUnableToCreateMiddleware( + typeof(BadMiddlewareFactory), + typeof(Middleware)), + exception.Message); + } - public class BasicMiddlewareFactory : IMiddlewareFactory + [Fact] + public async Task UseMiddlewareWithIMiddlewareWorks() + { + var mockServiceProvider = new DummyServiceProvider(); + var builder = new ApplicationBuilder(mockServiceProvider); + builder.UseMiddleware(typeof(Middleware)); + var app = builder.Build(); + var context = new DefaultHttpContext(); + var sp = new DummyServiceProvider(); + var middlewareFactory = new BasicMiddlewareFactory(); + sp.AddService(typeof(IMiddlewareFactory), middlewareFactory); + context.RequestServices = sp; + await app(context); + Assert.True(Assert.IsType(context.Items["before"])); + Assert.True(Assert.IsType(context.Items["after"])); + Assert.NotNull(middlewareFactory.Created); + Assert.NotNull(middlewareFactory.Released); + Assert.IsType(middlewareFactory.Created); + Assert.IsType(middlewareFactory.Released); + Assert.Same(middlewareFactory.Created, middlewareFactory.Released); + } + + public class Middleware : IMiddleware + { + public async Task InvokeAsync(HttpContext context, RequestDelegate next) { - public IMiddleware? Created { get; private set; } - public IMiddleware? Released { get; private set; } + context.Items["before"] = true; + await next(context); + context.Items["after"] = true; + } + } - public IMiddleware? Create(Type middlewareType) - { - Created = Activator.CreateInstance(middlewareType) as IMiddleware; - return Created; - } + public class BasicMiddlewareFactory : IMiddlewareFactory + { + public IMiddleware? Created { get; private set; } + public IMiddleware? Released { get; private set; } - public void Release(IMiddleware middleware) - { - Released = middleware; - } + public IMiddleware? Create(Type middlewareType) + { + Created = Activator.CreateInstance(middlewareType) as IMiddleware; + return Created; } - public class BadMiddlewareFactory : IMiddlewareFactory + public void Release(IMiddleware middleware) { - public IMiddleware? Create(Type middlewareType) => null; - - public void Release(IMiddleware middleware) { } + Released = middleware; } + } - private class DummyServiceProvider : IServiceProvider - { - private readonly Dictionary _services = new Dictionary(); + public class BadMiddlewareFactory : IMiddlewareFactory + { + public IMiddleware? Create(Type middlewareType) => null; - public void AddService(Type type, object value) => _services[type] = value; + public void Release(IMiddleware middleware) { } + } - public object? GetService(Type serviceType) - { - if (serviceType == typeof(IServiceProvider)) - { - return this; - } - - if (_services.TryGetValue(serviceType, out var value)) - { - return value; - } - return null; - } - } + private class DummyServiceProvider : IServiceProvider + { + private readonly Dictionary _services = new Dictionary(); - public class MiddlewareInjectWithOutAndRefParams + public void AddService(Type type, object value) => _services[type] = value; + + public object? GetService(Type serviceType) { - public MiddlewareInjectWithOutAndRefParams(RequestDelegate next) { } + if (serviceType == typeof(IServiceProvider)) + { + return this; + } - public Task Invoke(HttpContext context, ref IServiceProvider? sp1, out IServiceProvider? sp2) + if (_services.TryGetValue(serviceType, out var value)) { - sp1 = null; - sp2 = null; - return Task.FromResult(0); + return value; } + return null; } + } - private class MiddlewareInjectInvokeNoService - { - public MiddlewareInjectInvokeNoService(RequestDelegate next) { } + public class MiddlewareInjectWithOutAndRefParams + { + public MiddlewareInjectWithOutAndRefParams(RequestDelegate next) { } - public Task Invoke(HttpContext context, object value) => Task.CompletedTask; + public Task Invoke(HttpContext context, ref IServiceProvider? sp1, out IServiceProvider? sp2) + { + sp1 = null; + sp2 = null; + return Task.FromResult(0); } + } - private class MiddlewareInjectInvoke - { - public MiddlewareInjectInvoke(RequestDelegate next) { } + private class MiddlewareInjectInvokeNoService + { + public MiddlewareInjectInvokeNoService(RequestDelegate next) { } - public Task Invoke(HttpContext context, IServiceProvider provider) => Task.CompletedTask; - } + public Task Invoke(HttpContext context, object value) => Task.CompletedTask; + } - private class MiddlewareNoParametersStub - { - public MiddlewareNoParametersStub(RequestDelegate next) { } + private class MiddlewareInjectInvoke + { + public MiddlewareInjectInvoke(RequestDelegate next) { } - public Task Invoke() => Task.CompletedTask; - } + public Task Invoke(HttpContext context, IServiceProvider provider) => Task.CompletedTask; + } - private class MiddlewareAsyncNoParametersStub - { - public MiddlewareAsyncNoParametersStub(RequestDelegate next) { } + private class MiddlewareNoParametersStub + { + public MiddlewareNoParametersStub(RequestDelegate next) { } - public Task InvokeAsync() => Task.CompletedTask; - } + public Task Invoke() => Task.CompletedTask; + } - private class MiddlewareNonTaskReturnStub - { - public MiddlewareNonTaskReturnStub(RequestDelegate next) { } + private class MiddlewareAsyncNoParametersStub + { + public MiddlewareAsyncNoParametersStub(RequestDelegate next) { } - public int Invoke() => 0; - } + public Task InvokeAsync() => Task.CompletedTask; + } - private class MiddlewareAsyncNonTaskReturnStub - { - public MiddlewareAsyncNonTaskReturnStub(RequestDelegate next) { } + private class MiddlewareNonTaskReturnStub + { + public MiddlewareNonTaskReturnStub(RequestDelegate next) { } - public int InvokeAsync() => 0; - } + public int Invoke() => 0; + } - private class MiddlewareNoInvokeStub - { - public MiddlewareNoInvokeStub(RequestDelegate next) { } - } + private class MiddlewareAsyncNonTaskReturnStub + { + public MiddlewareAsyncNonTaskReturnStub(RequestDelegate next) { } - private class MiddlewareMultipleInvokesStub - { - public MiddlewareMultipleInvokesStub(RequestDelegate next) { } + public int InvokeAsync() => 0; + } + + private class MiddlewareNoInvokeStub + { + public MiddlewareNoInvokeStub(RequestDelegate next) { } + } - public Task Invoke(HttpContext context) => Task.CompletedTask; + private class MiddlewareMultipleInvokesStub + { + public MiddlewareMultipleInvokesStub(RequestDelegate next) { } - public Task Invoke(HttpContext context, int i) => Task.CompletedTask; - } + public Task Invoke(HttpContext context) => Task.CompletedTask; - private class MiddlewareMultipleInvokeAsyncStub - { - public MiddlewareMultipleInvokeAsyncStub(RequestDelegate next) { } + public Task Invoke(HttpContext context, int i) => Task.CompletedTask; + } - public Task InvokeAsync(HttpContext context) => Task.CompletedTask; + private class MiddlewareMultipleInvokeAsyncStub + { + public MiddlewareMultipleInvokeAsyncStub(RequestDelegate next) { } - public Task InvokeAsync(HttpContext context, int i) => Task.CompletedTask; - } + public Task InvokeAsync(HttpContext context) => Task.CompletedTask; - private class MiddlewareMultipleInvokeAndInvokeAsyncStub - { - public MiddlewareMultipleInvokeAndInvokeAsyncStub(RequestDelegate next) { } + public Task InvokeAsync(HttpContext context, int i) => Task.CompletedTask; + } + + private class MiddlewareMultipleInvokeAndInvokeAsyncStub + { + public MiddlewareMultipleInvokeAndInvokeAsyncStub(RequestDelegate next) { } - public Task Invoke(HttpContext context) => Task.CompletedTask; + public Task Invoke(HttpContext context) => Task.CompletedTask; - public Task InvokeAsync(HttpContext context) => Task.CompletedTask; - } + public Task InvokeAsync(HttpContext context) => Task.CompletedTask; } } diff --git a/src/Http/Http.Abstractions/test/UsePathBaseExtensionsTests.cs b/src/Http/Http.Abstractions/test/UsePathBaseExtensionsTests.cs index a5511ef0c1..26a91a6777 100644 --- a/src/Http/Http.Abstractions/test/UsePathBaseExtensionsTests.cs +++ b/src/Http/Http.Abstractions/test/UsePathBaseExtensionsTests.cs @@ -9,161 +9,160 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Xunit; -namespace Microsoft.AspNetCore.Builder.Extensions +namespace Microsoft.AspNetCore.Builder.Extensions; + +public class UsePathBaseExtensionsTests { - public class UsePathBaseExtensionsTests + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("/")] + public void EmptyOrNullPathBase_DoNotAddMiddleware(string pathBase) { - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData("/")] - public void EmptyOrNullPathBase_DoNotAddMiddleware(string pathBase) - { - // Arrange - var useCalled = false; - var builder = new ApplicationBuilderWrapper(CreateBuilder(), () => useCalled = true) - .UsePathBase(pathBase); + // Arrange + var useCalled = false; + var builder = new ApplicationBuilderWrapper(CreateBuilder(), () => useCalled = true) + .UsePathBase(pathBase); - // Act - builder.Build(); + // Act + builder.Build(); - // Assert - Assert.False(useCalled); - } - - private class ApplicationBuilderWrapper : IApplicationBuilder - { - private readonly IApplicationBuilder _wrappedBuilder; - private readonly Action _useCallback; - - public ApplicationBuilderWrapper(IApplicationBuilder applicationBuilder, Action useCallback) - { - _wrappedBuilder = applicationBuilder; - _useCallback = useCallback; - } - - public IApplicationBuilder Use(Func middleware) - { - _useCallback(); - return _wrappedBuilder.Use(middleware); - } - - public IServiceProvider ApplicationServices - { - get { return _wrappedBuilder.ApplicationServices; } - set { _wrappedBuilder.ApplicationServices = value; } - } - - public IDictionary Properties => _wrappedBuilder.Properties; - public IFeatureCollection ServerFeatures => _wrappedBuilder.ServerFeatures; - public RequestDelegate Build() => _wrappedBuilder.Build(); - public IApplicationBuilder New() => _wrappedBuilder.New(); + // Assert + Assert.False(useCalled); + } - } + private class ApplicationBuilderWrapper : IApplicationBuilder + { + private readonly IApplicationBuilder _wrappedBuilder; + private readonly Action _useCallback; - [Theory] - [InlineData("/base", "", "/base", "/base", "")] - [InlineData("/base", "", "/base/", "/base", "/")] - [InlineData("/base", "", "/base/something", "/base", "/something")] - [InlineData("/base", "", "/base/something/", "/base", "/something/")] - [InlineData("/base/more", "", "/base/more", "/base/more", "")] - [InlineData("/base/more", "", "/base/more/something", "/base/more", "/something")] - [InlineData("/base/more", "", "/base/more/something/", "/base/more", "/something/")] - [InlineData("/base", "/oldbase", "/base", "/oldbase/base", "")] - [InlineData("/base", "/oldbase", "/base/", "/oldbase/base", "/")] - [InlineData("/base", "/oldbase", "/base/something", "/oldbase/base", "/something")] - [InlineData("/base", "/oldbase", "/base/something/", "/oldbase/base", "/something/")] - [InlineData("/base/more", "/oldbase", "/base/more", "/oldbase/base/more", "")] - [InlineData("/base/more", "/oldbase", "/base/more/something", "/oldbase/base/more", "/something")] - [InlineData("/base/more", "/oldbase", "/base/more/something/", "/oldbase/base/more", "/something/")] - public Task RequestPathBaseContainingPathBase_IsSplit(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + public ApplicationBuilderWrapper(IApplicationBuilder applicationBuilder, Action useCallback) { - return TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + _wrappedBuilder = applicationBuilder; + _useCallback = useCallback; } - [Theory] - [InlineData("/base", "", "/something", "", "/something")] - [InlineData("/base", "", "/baseandsomething", "", "/baseandsomething")] - [InlineData("/base", "", "/ba", "", "/ba")] - [InlineData("/base", "", "/ba/se", "", "/ba/se")] - [InlineData("/base", "/oldbase", "/something", "/oldbase", "/something")] - [InlineData("/base", "/oldbase", "/baseandsomething", "/oldbase", "/baseandsomething")] - [InlineData("/base", "/oldbase", "/ba", "/oldbase", "/ba")] - [InlineData("/base", "/oldbase", "/ba/se", "/oldbase", "/ba/se")] - public Task RequestPathBaseNotContainingPathBase_IsNotSplit(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + public IApplicationBuilder Use(Func middleware) { - return TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + _useCallback(); + return _wrappedBuilder.Use(middleware); } - [Theory] - [InlineData("", "", "/", "", "/")] - [InlineData("/", "", "/", "", "/")] - [InlineData("/base", "", "/base/", "/base", "/")] - [InlineData("/base/", "", "/base", "/base", "")] - [InlineData("/base/", "", "/base/", "/base", "/")] - [InlineData("", "/oldbase", "/", "/oldbase", "/")] - [InlineData("/", "/oldbase", "/", "/oldbase", "/")] - [InlineData("/base", "/oldbase", "/base/", "/oldbase/base", "/")] - [InlineData("/base/", "/oldbase", "/base", "/oldbase/base", "")] - [InlineData("/base/", "/oldbase", "/base/", "/oldbase/base", "/")] - public Task PathBaseNeverEndsWithSlash(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + public IServiceProvider ApplicationServices { - return TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + get { return _wrappedBuilder.ApplicationServices; } + set { _wrappedBuilder.ApplicationServices = value; } } - [Theory] - [InlineData("/base", "", "/Base/Something", "/Base", "/Something")] - [InlineData("/base", "/OldBase", "/Base/Something", "/OldBase/Base", "/Something")] - public Task PathBaseAndPathPreserveRequestCasing(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) - { - return TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); - } + public IDictionary Properties => _wrappedBuilder.Properties; + public IFeatureCollection ServerFeatures => _wrappedBuilder.ServerFeatures; + public RequestDelegate Build() => _wrappedBuilder.Build(); + public IApplicationBuilder New() => _wrappedBuilder.New(); - [Theory] - [InlineData("/b♫se", "", "/b♫se/something", "/b♫se", "/something")] - [InlineData("/b♫se", "", "/B♫se/something", "/B♫se", "/something")] - [InlineData("/b♫se", "", "/b♫se/Something", "/b♫se", "/Something")] - [InlineData("/b♫se", "/oldb♫se", "/b♫se/something", "/oldb♫se/b♫se", "/something")] - [InlineData("/b♫se", "/oldb♫se", "/b♫se/Something", "/oldb♫se/b♫se", "/Something")] - [InlineData("/b♫se", "/oldb♫se", "/B♫se/something", "/oldb♫se/B♫se", "/something")] - public Task PathBaseCanHaveUnicodeCharacters(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) - { - return TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); - } + } - private static async Task TestPathBase(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) - { - HttpContext requestContext = CreateRequest(pathBase, requestPath); - var builder = CreateBuilder() - .UsePathBase(registeredPathBase); - builder.Run(context => - { - context.Items["test.Path"] = context.Request.Path; - context.Items["test.PathBase"] = context.Request.PathBase; - return Task.FromResult(0); - }); - await builder.Build().Invoke(requestContext); - - // Assert path and pathBase are split after middleware - Assert.Equal(expectedPath, ((PathString?)requestContext.Items["test.Path"])!.Value.Value); - Assert.Equal(expectedPathBase, ((PathString?)requestContext.Items["test.PathBase"])!.Value.Value); - - // Assert path and pathBase are reset after request - Assert.Equal(pathBase, requestContext.Request.PathBase.Value); - Assert.Equal(requestPath, requestContext.Request.Path.Value); - } + [Theory] + [InlineData("/base", "", "/base", "/base", "")] + [InlineData("/base", "", "/base/", "/base", "/")] + [InlineData("/base", "", "/base/something", "/base", "/something")] + [InlineData("/base", "", "/base/something/", "/base", "/something/")] + [InlineData("/base/more", "", "/base/more", "/base/more", "")] + [InlineData("/base/more", "", "/base/more/something", "/base/more", "/something")] + [InlineData("/base/more", "", "/base/more/something/", "/base/more", "/something/")] + [InlineData("/base", "/oldbase", "/base", "/oldbase/base", "")] + [InlineData("/base", "/oldbase", "/base/", "/oldbase/base", "/")] + [InlineData("/base", "/oldbase", "/base/something", "/oldbase/base", "/something")] + [InlineData("/base", "/oldbase", "/base/something/", "/oldbase/base", "/something/")] + [InlineData("/base/more", "/oldbase", "/base/more", "/oldbase/base/more", "")] + [InlineData("/base/more", "/oldbase", "/base/more/something", "/oldbase/base/more", "/something")] + [InlineData("/base/more", "/oldbase", "/base/more/something/", "/oldbase/base/more", "/something/")] + public Task RequestPathBaseContainingPathBase_IsSplit(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + return TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } - private static HttpContext CreateRequest(string pathBase, string requestPath) - { - HttpContext context = new DefaultHttpContext(); - context.Request.PathBase = new PathString(pathBase); - context.Request.Path = new PathString(requestPath); - return context; - } + [Theory] + [InlineData("/base", "", "/something", "", "/something")] + [InlineData("/base", "", "/baseandsomething", "", "/baseandsomething")] + [InlineData("/base", "", "/ba", "", "/ba")] + [InlineData("/base", "", "/ba/se", "", "/ba/se")] + [InlineData("/base", "/oldbase", "/something", "/oldbase", "/something")] + [InlineData("/base", "/oldbase", "/baseandsomething", "/oldbase", "/baseandsomething")] + [InlineData("/base", "/oldbase", "/ba", "/oldbase", "/ba")] + [InlineData("/base", "/oldbase", "/ba/se", "/oldbase", "/ba/se")] + public Task RequestPathBaseNotContainingPathBase_IsNotSplit(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + return TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + [Theory] + [InlineData("", "", "/", "", "/")] + [InlineData("/", "", "/", "", "/")] + [InlineData("/base", "", "/base/", "/base", "/")] + [InlineData("/base/", "", "/base", "/base", "")] + [InlineData("/base/", "", "/base/", "/base", "/")] + [InlineData("", "/oldbase", "/", "/oldbase", "/")] + [InlineData("/", "/oldbase", "/", "/oldbase", "/")] + [InlineData("/base", "/oldbase", "/base/", "/oldbase/base", "/")] + [InlineData("/base/", "/oldbase", "/base", "/oldbase/base", "")] + [InlineData("/base/", "/oldbase", "/base/", "/oldbase/base", "/")] + public Task PathBaseNeverEndsWithSlash(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + return TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } - private static ApplicationBuilder CreateBuilder() + [Theory] + [InlineData("/base", "", "/Base/Something", "/Base", "/Something")] + [InlineData("/base", "/OldBase", "/Base/Something", "/OldBase/Base", "/Something")] + public Task PathBaseAndPathPreserveRequestCasing(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + return TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + [Theory] + [InlineData("/b♫se", "", "/b♫se/something", "/b♫se", "/something")] + [InlineData("/b♫se", "", "/B♫se/something", "/B♫se", "/something")] + [InlineData("/b♫se", "", "/b♫se/Something", "/b♫se", "/Something")] + [InlineData("/b♫se", "/oldb♫se", "/b♫se/something", "/oldb♫se/b♫se", "/something")] + [InlineData("/b♫se", "/oldb♫se", "/b♫se/Something", "/oldb♫se/b♫se", "/Something")] + [InlineData("/b♫se", "/oldb♫se", "/B♫se/something", "/oldb♫se/B♫se", "/something")] + public Task PathBaseCanHaveUnicodeCharacters(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + return TestPathBase(registeredPathBase, pathBase, requestPath, expectedPathBase, expectedPath); + } + + private static async Task TestPathBase(string registeredPathBase, string pathBase, string requestPath, string expectedPathBase, string expectedPath) + { + HttpContext requestContext = CreateRequest(pathBase, requestPath); + var builder = CreateBuilder() + .UsePathBase(registeredPathBase); + builder.Run(context => { - return new ApplicationBuilder(serviceProvider: null!); - } + context.Items["test.Path"] = context.Request.Path; + context.Items["test.PathBase"] = context.Request.PathBase; + return Task.FromResult(0); + }); + await builder.Build().Invoke(requestContext); + + // Assert path and pathBase are split after middleware + Assert.Equal(expectedPath, ((PathString?)requestContext.Items["test.Path"])!.Value.Value); + Assert.Equal(expectedPathBase, ((PathString?)requestContext.Items["test.PathBase"])!.Value.Value); + + // Assert path and pathBase are reset after request + Assert.Equal(pathBase, requestContext.Request.PathBase.Value); + Assert.Equal(requestPath, requestContext.Request.Path.Value); + } + + private static HttpContext CreateRequest(string pathBase, string requestPath) + { + HttpContext context = new DefaultHttpContext(); + context.Request.PathBase = new PathString(pathBase); + context.Request.Path = new PathString(requestPath); + return context; + } + + private static ApplicationBuilder CreateBuilder() + { + return new ApplicationBuilder(serviceProvider: null!); } } diff --git a/src/Http/Http.Abstractions/test/UseWhenExtensionsTests.cs b/src/Http/Http.Abstractions/test/UseWhenExtensionsTests.cs index 8b46e07b83..934f3dcbe9 100644 --- a/src/Http/Http.Abstractions/test/UseWhenExtensionsTests.cs +++ b/src/Http/Http.Abstractions/test/UseWhenExtensionsTests.cs @@ -7,164 +7,163 @@ using Microsoft.AspNetCore.Builder.Internal; using Microsoft.AspNetCore.Http; using Xunit; -namespace Microsoft.AspNetCore.Builder.Extensions +namespace Microsoft.AspNetCore.Builder.Extensions; + +public class UseWhenExtensionsTests { - public class UseWhenExtensionsTests + [Fact] + public void NullArguments_ArgumentNullException() { - [Fact] - public void NullArguments_ArgumentNullException() - { - // Arrange - var builder = CreateBuilder(); + // Arrange + var builder = CreateBuilder(); - // Act - Action nullPredicate = () => builder.UseWhen(null!, app => { }); - Action nullConfiguration = () => builder.UseWhen(TruePredicate, null!); + // Act + Action nullPredicate = () => builder.UseWhen(null!, app => { }); + Action nullConfiguration = () => builder.UseWhen(TruePredicate, null!); - // Assert - Assert.Throws(nullPredicate); - Assert.Throws(nullConfiguration); - } + // Assert + Assert.Throws(nullPredicate); + Assert.Throws(nullConfiguration); + } - [Fact] - public async Task PredicateTrue_BranchTaken_WillRejoin() - { - // Arrange - var context = CreateContext(); - var parent = CreateBuilder(); + [Fact] + public async Task PredicateTrue_BranchTaken_WillRejoin() + { + // Arrange + var context = CreateContext(); + var parent = CreateBuilder(); - parent.UseWhen(TruePredicate, child => + parent.UseWhen(TruePredicate, child => + { + child.UseWhen(TruePredicate, grandchild => { - child.UseWhen(TruePredicate, grandchild => - { - grandchild.Use(Increment("grandchild")); - }); - - child.Use(Increment("child")); + grandchild.Use(Increment("grandchild")); }); - parent.Use(Increment("parent")); + child.Use(Increment("child")); + }); - // Act - await parent.Build().Invoke(context); + parent.Use(Increment("parent")); - // Assert - Assert.Equal(1, Count(context, "parent")); - Assert.Equal(1, Count(context, "child")); - Assert.Equal(1, Count(context, "grandchild")); - } + // Act + await parent.Build().Invoke(context); - [Fact] - public async Task PredicateTrue_BranchTaken_CanTerminate() - { - // Arrange - var context = CreateContext(); - var parent = CreateBuilder(); + // Assert + Assert.Equal(1, Count(context, "parent")); + Assert.Equal(1, Count(context, "child")); + Assert.Equal(1, Count(context, "grandchild")); + } - parent.UseWhen(TruePredicate, child => - { - child.UseWhen(TruePredicate, grandchild => - { - grandchild.Use(Increment("grandchild", terminate: true)); - }); + [Fact] + public async Task PredicateTrue_BranchTaken_CanTerminate() + { + // Arrange + var context = CreateContext(); + var parent = CreateBuilder(); - child.Use(Increment("child")); + parent.UseWhen(TruePredicate, child => + { + child.UseWhen(TruePredicate, grandchild => + { + grandchild.Use(Increment("grandchild", terminate: true)); }); - parent.Use(Increment("parent")); + child.Use(Increment("child")); + }); - // Act - await parent.Build().Invoke(context); + parent.Use(Increment("parent")); - // Assert - Assert.Equal(0, Count(context, "parent")); - Assert.Equal(0, Count(context, "child")); - Assert.Equal(1, Count(context, "grandchild")); - } + // Act + await parent.Build().Invoke(context); - [Fact] - public async Task PredicateFalse_PassThrough() - { - // Arrange - var context = CreateContext(); - var parent = CreateBuilder(); + // Assert + Assert.Equal(0, Count(context, "parent")); + Assert.Equal(0, Count(context, "child")); + Assert.Equal(1, Count(context, "grandchild")); + } - parent.UseWhen(FalsePredicate, child => - { - child.Use(Increment("child")); - }); + [Fact] + public async Task PredicateFalse_PassThrough() + { + // Arrange + var context = CreateContext(); + var parent = CreateBuilder(); - parent.Use(Increment("parent")); + parent.UseWhen(FalsePredicate, child => + { + child.Use(Increment("child")); + }); - // Act - await parent.Build().Invoke(context); + parent.Use(Increment("parent")); - // Assert - Assert.Equal(1, Count(context, "parent")); - Assert.Equal(0, Count(context, "child")); - } + // Act + await parent.Build().Invoke(context); - private static HttpContext CreateContext() - { - return new DefaultHttpContext(); - } + // Assert + Assert.Equal(1, Count(context, "parent")); + Assert.Equal(0, Count(context, "child")); + } - private static ApplicationBuilder CreateBuilder() - { - return new ApplicationBuilder(serviceProvider: null!); - } + private static HttpContext CreateContext() + { + return new DefaultHttpContext(); + } - private static bool TruePredicate(HttpContext context) - { - return true; - } + private static ApplicationBuilder CreateBuilder() + { + return new ApplicationBuilder(serviceProvider: null!); + } - private static bool FalsePredicate(HttpContext context) - { - return false; - } + private static bool TruePredicate(HttpContext context) + { + return true; + } + + private static bool FalsePredicate(HttpContext context) + { + return false; + } - private static Func, Task> Increment(string key, bool terminate = false) + private static Func, Task> Increment(string key, bool terminate = false) + { + return (context, next) => { - return (context, next) => + if (!context.Items.ContainsKey(key)) { - if (!context.Items.ContainsKey(key)) + context.Items[key] = 1; + } + else + { + var item = context.Items[key]; + + if (item is int) { - context.Items[key] = 1; + context.Items[key] = 1 + (int)item; } else { - var item = context.Items[key]; - - if (item is int) - { - context.Items[key] = 1 + (int)item; - } - else - { - context.Items[key] = 1; - } + context.Items[key] = 1; } + } - return terminate ? Task.FromResult(null) : next(); - }; - } + return terminate ? Task.FromResult(null) : next(); + }; + } - private static int Count(HttpContext context, string key) + private static int Count(HttpContext context, string key) + { + if (!context.Items.ContainsKey(key)) { - if (!context.Items.ContainsKey(key)) - { - return 0; - } - - var item = context.Items[key]; + return 0; + } - if (item is int) - { - return (int)item; - } + var item = context.Items[key]; - return 0; + if (item is int) + { + return (int)item; } + + return 0; } } diff --git a/src/Http/Http.Extensions/src/HeaderDictionaryTypeExtensions.cs b/src/Http/Http.Extensions/src/HeaderDictionaryTypeExtensions.cs index 783afd3229..87268945ed 100644 --- a/src/Http/Http.Extensions/src/HeaderDictionaryTypeExtensions.cs +++ b/src/Http/Http.Extensions/src/HeaderDictionaryTypeExtensions.cs @@ -10,165 +10,165 @@ using Microsoft.AspNetCore.Http.Headers; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Extension methods for accessing strongly typed HTTP request and response +/// headers. +/// +public static class HeaderDictionaryTypeExtensions { /// - /// Extension methods for accessing strongly typed HTTP request and response - /// headers. + /// Gets strongly typed HTTP request headers. /// - public static class HeaderDictionaryTypeExtensions + /// The . + /// The . + public static RequestHeaders GetTypedHeaders(this HttpRequest request) { - /// - /// Gets strongly typed HTTP request headers. - /// - /// The . - /// The . - public static RequestHeaders GetTypedHeaders(this HttpRequest request) + return new RequestHeaders(request.Headers); + } + + /// + /// Gets strongly typed HTTP response headers. + /// + /// The . + /// The . + public static ResponseHeaders GetTypedHeaders(this HttpResponse response) + { + return new ResponseHeaders(response.Headers); + } + + // These are all shared helpers used by both RequestHeaders and ResponseHeaders + + internal static DateTimeOffset? GetDate(this IHeaderDictionary headers, string name) + { + if (headers == null) { - return new RequestHeaders(request.Headers); + throw new ArgumentNullException(nameof(headers)); } - /// - /// Gets strongly typed HTTP response headers. - /// - /// The . - /// The . - public static ResponseHeaders GetTypedHeaders(this HttpResponse response) + if (name == null) { - return new ResponseHeaders(response.Headers); + throw new ArgumentNullException(nameof(name)); } - // These are all shared helpers used by both RequestHeaders and ResponseHeaders + return headers.Get(name); + } - internal static DateTimeOffset? GetDate(this IHeaderDictionary headers, string name) + internal static void Set(this IHeaderDictionary headers, string name, object? value) + { + if (headers == null) { - if (headers == null) - { - throw new ArgumentNullException(nameof(headers)); - } + throw new ArgumentNullException(nameof(headers)); + } - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } - return headers.Get(name); + if (value == null) + { + headers.Remove(name); + } + else + { + headers[name] = value.ToString(); } + } - internal static void Set(this IHeaderDictionary headers, string name, object? value) + internal static void SetList(this IHeaderDictionary headers, string name, IList? values) + { + if (headers == null) { - if (headers == null) - { - throw new ArgumentNullException(nameof(headers)); - } + throw new ArgumentNullException(nameof(headers)); + } - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } - if (value == null) - { - headers.Remove(name); - } - else + if (values == null || values.Count == 0) + { + headers.Remove(name); + } + else if (values.Count == 1) + { + headers[name] = new StringValues(values[0]!.ToString()); + } + else + { + var newValues = new string[values.Count]; + for (var i = 0; i < values.Count; i++) { - headers[name] = value.ToString(); + newValues[i] = values[i]!.ToString()!; } + headers[name] = new StringValues(newValues); } + } - internal static void SetList(this IHeaderDictionary headers, string name, IList? values) + /// + /// Appends a sequence of values to . + /// + /// The type of header value. + /// The . + /// The header name. + /// The values to append. + public static void AppendList(this IHeaderDictionary Headers, string name, IList values) + { + if (name == null) { - if (headers == null) - { - throw new ArgumentNullException(nameof(headers)); - } + throw new ArgumentNullException(nameof(name)); + } - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } - if (values == null || values.Count == 0) - { - headers.Remove(name); - } - else if (values.Count == 1) - { - headers[name] = new StringValues(values[0]!.ToString()); - } - else - { + switch (values.Count) + { + case 0: + Headers.Append(name, StringValues.Empty); + break; + case 1: + Headers.Append(name, new StringValues(values[0]!.ToString())); + break; + default: var newValues = new string[values.Count]; for (var i = 0; i < values.Count; i++) { newValues[i] = values[i]!.ToString()!; } - headers[name] = new StringValues(newValues); - } + Headers.Append(name, new StringValues(newValues)); + break; } + } - /// - /// Appends a sequence of values to . - /// - /// The type of header value. - /// The . - /// The header name. - /// The values to append. - public static void AppendList(this IHeaderDictionary Headers, string name, IList values) + internal static void SetDate(this IHeaderDictionary headers, string name, DateTimeOffset? value) + { + if (headers == null) { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - switch (values.Count) - { - case 0: - Headers.Append(name, StringValues.Empty); - break; - case 1: - Headers.Append(name, new StringValues(values[0]!.ToString())); - break; - default: - var newValues = new string[values.Count]; - for (var i = 0; i < values.Count; i++) - { - newValues[i] = values[i]!.ToString()!; - } - Headers.Append(name, new StringValues(newValues)); - break; - } + throw new ArgumentNullException(nameof(headers)); } - internal static void SetDate(this IHeaderDictionary headers, string name, DateTimeOffset? value) + if (name == null) { - if (headers == null) - { - throw new ArgumentNullException(nameof(headers)); - } - - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } + throw new ArgumentNullException(nameof(name)); + } - if (value.HasValue) - { - headers[name] = HeaderUtilities.FormatDate(value.GetValueOrDefault()); - } - else - { - headers.Remove(name); - } + if (value.HasValue) + { + headers[name] = HeaderUtilities.FormatDate(value.GetValueOrDefault()); + } + else + { + headers.Remove(name); } + } - private static readonly IDictionary KnownParsers = new Dictionary() + private static readonly IDictionary KnownParsers = new Dictionary() { { typeof(CacheControlHeaderValue), new Func(value => { return CacheControlHeaderValue.TryParse(value, out var result) ? result : null; }) }, { typeof(ContentDispositionHeaderValue), new Func(value => { return ContentDispositionHeaderValue.TryParse(value, out var result) ? result : null; }) }, @@ -181,7 +181,7 @@ namespace Microsoft.AspNetCore.Http { typeof(long?), new Func(value => { return HeaderUtilities.TryParseNonNegativeInt64(value, out var result) ? result : null; }) }, }; - private static readonly IDictionary KnownListParsers = new Dictionary() + private static readonly IDictionary KnownListParsers = new Dictionary() { { typeof(MediaTypeHeaderValue), new Func, IList>(value => { return MediaTypeHeaderValue.TryParseList(value, out var result) ? result : Array.Empty(); }) }, { typeof(StringWithQualityHeaderValue), new Func, IList>(value => { return StringWithQualityHeaderValue.TryParseList(value, out var result) ? result : Array.Empty(); }) }, @@ -190,127 +190,126 @@ namespace Microsoft.AspNetCore.Http { typeof(SetCookieHeaderValue), new Func, IList>(value => { return SetCookieHeaderValue.TryParseList(value, out var result) ? result : Array.Empty(); }) }, }; - internal static T? Get(this IHeaderDictionary headers, string name) + internal static T? Get(this IHeaderDictionary headers, string name) + { + if (headers == null) { - if (headers == null) - { - throw new ArgumentNullException(nameof(headers)); - } - - var value = headers[name]; + throw new ArgumentNullException(nameof(headers)); + } - if (StringValues.IsNullOrEmpty(value)) - { - return default(T); - } + var value = headers[name]; - if (KnownParsers.TryGetValue(typeof(T), out var temp)) - { - var func = (Func)temp; - return func(value.ToString()); - } + if (StringValues.IsNullOrEmpty(value)) + { + return default(T); + } - return GetViaReflection(value.ToString()); + if (KnownParsers.TryGetValue(typeof(T), out var temp)) + { + var func = (Func)temp; + return func(value.ToString()); } - internal static IList GetList(this IHeaderDictionary headers, string name) + return GetViaReflection(value.ToString()); + } + + internal static IList GetList(this IHeaderDictionary headers, string name) + { + if (headers == null) { - if (headers == null) - { - throw new ArgumentNullException(nameof(headers)); - } + throw new ArgumentNullException(nameof(headers)); + } + + var values = headers[name]; - var values = headers[name]; + return GetList(values); + } - return GetList(values); + internal static IList GetList(this StringValues values) + { + if (StringValues.IsNullOrEmpty(values)) + { + return Array.Empty(); } - internal static IList GetList(this StringValues values) + if (KnownListParsers.TryGetValue(typeof(T), out var temp)) { - if (StringValues.IsNullOrEmpty(values)) - { - return Array.Empty(); - } + var func = (Func, IList>)temp; + return func(values); + } + + return GetListViaReflection(values); + } - if (KnownListParsers.TryGetValue(typeof(T), out var temp)) + private static T? GetViaReflection(string value) + { + // TODO: Cache the reflected type for later? Only if success? + var type = typeof(T); + var method = type.GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(methodInfo => { - var func = (Func, IList>)temp; - return func(values); - } + if (string.Equals("TryParse", methodInfo.Name, StringComparison.Ordinal) + && methodInfo.ReturnParameter.ParameterType.Equals(typeof(bool))) + { + var methodParams = methodInfo.GetParameters(); + return methodParams.Length == 2 + && methodParams[0].ParameterType.Equals(typeof(string)) + && methodParams[1].IsOut + && methodParams[1].ParameterType.Equals(type.MakeByRefType()); + } + return false; + }); - return GetListViaReflection(values); + if (method == null) + { + throw new NotSupportedException(string.Format( + CultureInfo.CurrentCulture, + "The given type '{0}' does not have a TryParse method with the required signature 'public static bool TryParse(string, out {0}).", + nameof(T))); } - private static T? GetViaReflection(string value) + var parameters = new object?[] { value, null }; + var success = (bool)method.Invoke(null, parameters)!; + if (success) { - // TODO: Cache the reflected type for later? Only if success? - var type = typeof(T); - var method = type.GetMethods(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(methodInfo => - { - if (string.Equals("TryParse", methodInfo.Name, StringComparison.Ordinal) - && methodInfo.ReturnParameter.ParameterType.Equals(typeof(bool))) - { - var methodParams = methodInfo.GetParameters(); - return methodParams.Length == 2 - && methodParams[0].ParameterType.Equals(typeof(string)) - && methodParams[1].IsOut - && methodParams[1].ParameterType.Equals(type.MakeByRefType()); - } - return false; - }); - - if (method == null) - { - throw new NotSupportedException(string.Format( - CultureInfo.CurrentCulture, - "The given type '{0}' does not have a TryParse method with the required signature 'public static bool TryParse(string, out {0}).", - nameof(T))); - } + return (T?)parameters[1]; + } + return default(T); + } - var parameters = new object?[] { value, null }; - var success = (bool)method.Invoke(null, parameters)!; - if (success) + private static IList GetListViaReflection(StringValues values) + { + // TODO: Cache the reflected type for later? Only if success? + var type = typeof(T); + var method = type.GetMethods(BindingFlags.Public | BindingFlags.Static) + .FirstOrDefault(methodInfo => { - return (T?)parameters[1]; - } - return default(T); - } + if (string.Equals("TryParseList", methodInfo.Name, StringComparison.Ordinal) + && methodInfo.ReturnParameter.ParameterType.Equals(typeof(Boolean))) + { + var methodParams = methodInfo.GetParameters(); + return methodParams.Length == 2 + && methodParams[0].ParameterType.Equals(typeof(IList)) + && methodParams[1].IsOut + && methodParams[1].ParameterType.Equals(typeof(IList).MakeByRefType()); + } + return false; + }); - private static IList GetListViaReflection(StringValues values) + if (method == null) { - // TODO: Cache the reflected type for later? Only if success? - var type = typeof(T); - var method = type.GetMethods(BindingFlags.Public | BindingFlags.Static) - .FirstOrDefault(methodInfo => - { - if (string.Equals("TryParseList", methodInfo.Name, StringComparison.Ordinal) - && methodInfo.ReturnParameter.ParameterType.Equals(typeof(Boolean))) - { - var methodParams = methodInfo.GetParameters(); - return methodParams.Length == 2 - && methodParams[0].ParameterType.Equals(typeof(IList)) - && methodParams[1].IsOut - && methodParams[1].ParameterType.Equals(typeof(IList).MakeByRefType()); - } - return false; - }); - - if (method == null) - { - throw new NotSupportedException(string.Format( - CultureInfo.CurrentCulture, - "The given type '{0}' does not have a TryParseList method with the required signature 'public static bool TryParseList(IList, out IList<{0}>).", - nameof(T))); - } + throw new NotSupportedException(string.Format( + CultureInfo.CurrentCulture, + "The given type '{0}' does not have a TryParseList method with the required signature 'public static bool TryParseList(IList, out IList<{0}>).", + nameof(T))); + } - var parameters = new object?[] { values, null }; - var success = (bool)method.Invoke(null, parameters)!; - if (success) - { - return (IList)parameters[1]!; - } - return Array.Empty(); + var parameters = new object?[] { values, null }; + var success = (bool)method.Invoke(null, parameters)!; + if (success) + { + return (IList)parameters[1]!; } + return Array.Empty(); } } diff --git a/src/Http/Http.Extensions/src/HttpContextServerVariableExtensions.cs b/src/Http/Http.Extensions/src/HttpContextServerVariableExtensions.cs index cefd005da2..2e7bdd2090 100644 --- a/src/Http/Http.Extensions/src/HttpContextServerVariableExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpContextServerVariableExtensions.cs @@ -3,32 +3,31 @@ using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Extensions for reading HTTP server variables. +/// +public static class HttpContextServerVariableExtensions { /// - /// Extensions for reading HTTP server variables. + /// Gets the value of a server variable for the current request. /// - public static class HttpContextServerVariableExtensions + /// The http context for the request. + /// The name of the variable. + /// + /// null if the server does not support the feature. + /// May return null or empty if the variable does not exist or is not set. + /// + public static string? GetServerVariable(this HttpContext context, string variableName) { - /// - /// Gets the value of a server variable for the current request. - /// - /// The http context for the request. - /// The name of the variable. - /// - /// null if the server does not support the feature. - /// May return null or empty if the variable does not exist or is not set. - /// - public static string? GetServerVariable(this HttpContext context, string variableName) - { - var feature = context.Features.Get(); - - if (feature == null) - { - return null; - } + var feature = context.Features.Get(); - return feature[variableName]; + if (feature == null) + { + return null; } + + return feature[variableName]; } } diff --git a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs index e2adc83d71..cc2b51dfc3 100644 --- a/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpRequestJsonExtensions.cs @@ -14,218 +14,217 @@ using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Extension methods to read the request body as JSON. +/// +public static class HttpRequestJsonExtensions { /// - /// Extension methods to read the request body as JSON. + /// Read JSON from the request and deserialize to the specified type. + /// If the request's content-type is not a known JSON type then an error will be thrown. /// - public static class HttpRequestJsonExtensions + /// The type of object to read. + /// The request to read from. + /// A used to cancel the operation. + /// The task object representing the asynchronous operation. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static ValueTask ReadFromJsonAsync( + this HttpRequest request, + CancellationToken cancellationToken = default) { - /// - /// Read JSON from the request and deserialize to the specified type. - /// If the request's content-type is not a known JSON type then an error will be thrown. - /// - /// The type of object to read. - /// The request to read from. - /// A used to cancel the operation. - /// The task object representing the asynchronous operation. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static ValueTask ReadFromJsonAsync( - this HttpRequest request, - CancellationToken cancellationToken = default) - { - return request.ReadFromJsonAsync(options: null, cancellationToken); - } - - /// - /// Read JSON from the request and deserialize to the specified type. - /// If the request's content-type is not a known JSON type then an error will be thrown. - /// - /// The type of object to read. - /// The request to read from. - /// The serializer options use when deserializing the content. - /// A used to cancel the operation. - /// The task object representing the asynchronous operation. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static async ValueTask ReadFromJsonAsync( - this HttpRequest request, - JsonSerializerOptions? options, - CancellationToken cancellationToken = default) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + return request.ReadFromJsonAsync(options: null, cancellationToken); + } - if (!request.HasJsonContentType(out var charset)) - { - throw CreateContentTypeError(request); - } + /// + /// Read JSON from the request and deserialize to the specified type. + /// If the request's content-type is not a known JSON type then an error will be thrown. + /// + /// The type of object to read. + /// The request to read from. + /// The serializer options use when deserializing the content. + /// A used to cancel the operation. + /// The task object representing the asynchronous operation. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static async ValueTask ReadFromJsonAsync( + this HttpRequest request, + JsonSerializerOptions? options, + CancellationToken cancellationToken = default) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } - options ??= ResolveSerializerOptions(request.HttpContext); + if (!request.HasJsonContentType(out var charset)) + { + throw CreateContentTypeError(request); + } - var encoding = GetEncodingFromCharset(charset); - var (inputStream, usesTranscodingStream) = GetInputStream(request.HttpContext, encoding); + options ??= ResolveSerializerOptions(request.HttpContext); - try - { - return await JsonSerializer.DeserializeAsync(inputStream, options, cancellationToken); - } - finally + var encoding = GetEncodingFromCharset(charset); + var (inputStream, usesTranscodingStream) = GetInputStream(request.HttpContext, encoding); + + try + { + return await JsonSerializer.DeserializeAsync(inputStream, options, cancellationToken); + } + finally + { + if (usesTranscodingStream) { - if (usesTranscodingStream) - { - await inputStream.DisposeAsync(); - } + await inputStream.DisposeAsync(); } } + } - /// - /// Read JSON from the request and deserialize to the specified type. - /// If the request's content-type is not a known JSON type then an error will be thrown. - /// - /// The request to read from. - /// The type of object to read. - /// A used to cancel the operation. - /// The task object representing the asynchronous operation. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static ValueTask ReadFromJsonAsync( - this HttpRequest request, - Type type, - CancellationToken cancellationToken = default) - { - return request.ReadFromJsonAsync(type, options: null, cancellationToken); - } - - /// - /// Read JSON from the request and deserialize to the specified type. - /// If the request's content-type is not a known JSON type then an error will be thrown. - /// - /// The request to read from. - /// The type of object to read. - /// The serializer options use when deserializing the content. - /// A used to cancel the operation. - /// The task object representing the asynchronous operation. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static async ValueTask ReadFromJsonAsync( - this HttpRequest request, - Type type, - JsonSerializerOptions? options, - CancellationToken cancellationToken = default) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } + /// + /// Read JSON from the request and deserialize to the specified type. + /// If the request's content-type is not a known JSON type then an error will be thrown. + /// + /// The request to read from. + /// The type of object to read. + /// A used to cancel the operation. + /// The task object representing the asynchronous operation. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static ValueTask ReadFromJsonAsync( + this HttpRequest request, + Type type, + CancellationToken cancellationToken = default) + { + return request.ReadFromJsonAsync(type, options: null, cancellationToken); + } - if (!request.HasJsonContentType(out var charset)) - { - throw CreateContentTypeError(request); - } + /// + /// Read JSON from the request and deserialize to the specified type. + /// If the request's content-type is not a known JSON type then an error will be thrown. + /// + /// The request to read from. + /// The type of object to read. + /// The serializer options use when deserializing the content. + /// A used to cancel the operation. + /// The task object representing the asynchronous operation. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static async ValueTask ReadFromJsonAsync( + this HttpRequest request, + Type type, + JsonSerializerOptions? options, + CancellationToken cancellationToken = default) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } - options ??= ResolveSerializerOptions(request.HttpContext); + if (!request.HasJsonContentType(out var charset)) + { + throw CreateContentTypeError(request); + } - var encoding = GetEncodingFromCharset(charset); - var (inputStream, usesTranscodingStream) = GetInputStream(request.HttpContext, encoding); + options ??= ResolveSerializerOptions(request.HttpContext); - try - { - return await JsonSerializer.DeserializeAsync(inputStream, type, options, cancellationToken); - } - finally - { - if (usesTranscodingStream) - { - await inputStream.DisposeAsync(); - } - } - } + var encoding = GetEncodingFromCharset(charset); + var (inputStream, usesTranscodingStream) = GetInputStream(request.HttpContext, encoding); - /// - /// Checks the Content-Type header for JSON types. - /// - /// true if the Content-Type header represents a JSON content type; otherwise, false. - public static bool HasJsonContentType(this HttpRequest request) + try { - return request.HasJsonContentType(out _); + return await JsonSerializer.DeserializeAsync(inputStream, type, options, cancellationToken); } - - private static bool HasJsonContentType(this HttpRequest request, out StringSegment charset) + finally { - if (request == null) + if (usesTranscodingStream) { - throw new ArgumentNullException(nameof(request)); - } - - if (!MediaTypeHeaderValue.TryParse(request.ContentType, out var mt)) - { - charset = StringSegment.Empty; - return false; + await inputStream.DisposeAsync(); } + } + } - // Matches application/json - if (mt.MediaType.Equals(JsonConstants.JsonContentType, StringComparison.OrdinalIgnoreCase)) - { - charset = mt.Charset; - return true; - } + /// + /// Checks the Content-Type header for JSON types. + /// + /// true if the Content-Type header represents a JSON content type; otherwise, false. + public static bool HasJsonContentType(this HttpRequest request) + { + return request.HasJsonContentType(out _); + } - // Matches +json, e.g. application/ld+json - if (mt.Suffix.Equals("json", StringComparison.OrdinalIgnoreCase)) - { - charset = mt.Charset; - return true; - } + private static bool HasJsonContentType(this HttpRequest request, out StringSegment charset) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + if (!MediaTypeHeaderValue.TryParse(request.ContentType, out var mt)) + { charset = StringSegment.Empty; return false; } - - private static JsonSerializerOptions ResolveSerializerOptions(HttpContext httpContext) + // Matches application/json + if (mt.MediaType.Equals(JsonConstants.JsonContentType, StringComparison.OrdinalIgnoreCase)) { - // Attempt to resolve options from DI then fallback to default options - return httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; + charset = mt.Charset; + return true; } - private static InvalidOperationException CreateContentTypeError(HttpRequest request) + // Matches +json, e.g. application/ld+json + if (mt.Suffix.Equals("json", StringComparison.OrdinalIgnoreCase)) { - return new InvalidOperationException($"Unable to read the request as JSON because the request content type '{request.ContentType}' is not a known JSON content type."); + charset = mt.Charset; + return true; } - private static (Stream inputStream, bool usesTranscodingStream) GetInputStream(HttpContext httpContext, Encoding? encoding) - { - if (encoding == null || encoding.CodePage == Encoding.UTF8.CodePage) - { - return (httpContext.Request.Body, false); - } + charset = StringSegment.Empty; + return false; + } - var inputStream = Encoding.CreateTranscodingStream(httpContext.Request.Body, encoding, Encoding.UTF8, leaveOpen: true); - return (inputStream, true); + + private static JsonSerializerOptions ResolveSerializerOptions(HttpContext httpContext) + { + // Attempt to resolve options from DI then fallback to default options + return httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; + } + + private static InvalidOperationException CreateContentTypeError(HttpRequest request) + { + return new InvalidOperationException($"Unable to read the request as JSON because the request content type '{request.ContentType}' is not a known JSON content type."); + } + + private static (Stream inputStream, bool usesTranscodingStream) GetInputStream(HttpContext httpContext, Encoding? encoding) + { + if (encoding == null || encoding.CodePage == Encoding.UTF8.CodePage) + { + return (httpContext.Request.Body, false); } - private static Encoding? GetEncodingFromCharset(StringSegment charset) + var inputStream = Encoding.CreateTranscodingStream(httpContext.Request.Body, encoding, Encoding.UTF8, leaveOpen: true); + return (inputStream, true); + } + + private static Encoding? GetEncodingFromCharset(StringSegment charset) + { + if (charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase)) { - if (charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase)) - { - // This is an optimization for utf-8 that prevents the Substring caused by - // charset.Value - return Encoding.UTF8; - } + // This is an optimization for utf-8 that prevents the Substring caused by + // charset.Value + return Encoding.UTF8; + } - try - { - // charset.Value might be an invalid encoding name as in charset=invalid. - return charset.HasValue ? Encoding.GetEncoding(charset.Value) : null; - } - catch (Exception ex) - { - throw new InvalidOperationException($"Unable to read the request as JSON because the request content type charset '{charset}' is not a known encoding.", ex); - } + try + { + // charset.Value might be an invalid encoding name as in charset=invalid. + return charset.HasValue ? Encoding.GetEncoding(charset.Value) : null; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Unable to read the request as JSON because the request content type charset '{charset}' is not a known encoding.", ex); } } } diff --git a/src/Http/Http.Extensions/src/HttpRequestMultipartExtensions.cs b/src/Http/Http.Extensions/src/HttpRequestMultipartExtensions.cs index 17236850fc..87fa8e3c88 100644 --- a/src/Http/Http.Extensions/src/HttpRequestMultipartExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpRequestMultipartExtensions.cs @@ -4,30 +4,29 @@ using System; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http.Extensions +namespace Microsoft.AspNetCore.Http.Extensions; + +/// +/// Extension methods for working with multipart form requests. +/// +public static class HttpRequestMultipartExtensions { /// - /// Extension methods for working with multipart form requests. + /// Gets the mutipart boundary from the Content-Type header. /// - public static class HttpRequestMultipartExtensions + /// The . + /// The multipart boundary. + public static string GetMultipartBoundary(this HttpRequest request) { - /// - /// Gets the mutipart boundary from the Content-Type header. - /// - /// The . - /// The multipart boundary. - public static string GetMultipartBoundary(this HttpRequest request) + if (request == null) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + throw new ArgumentNullException(nameof(request)); + } - if (!MediaTypeHeaderValue.TryParse(request.ContentType, out var mediaType)) - { - return string.Empty; - } - return HeaderUtilities.RemoveQuotes(mediaType.Boundary).ToString(); + if (!MediaTypeHeaderValue.TryParse(request.ContentType, out var mediaType)) + { + return string.Empty; } + return HeaderUtilities.RemoveQuotes(mediaType.Boundary).ToString(); } } diff --git a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs index 1336cc8f1b..d9eea4aad9 100644 --- a/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs +++ b/src/Http/Http.Extensions/src/HttpResponseJsonExtensions.cs @@ -10,200 +10,199 @@ using Microsoft.AspNetCore.Http.Json; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Provides extension methods for writing a JSON serialized value to the HTTP response. +/// +public static partial class HttpResponseJsonExtensions { /// - /// Provides extension methods for writing a JSON serialized value to the HTTP response. + /// Write the specified value as JSON to the response body. The response content-type will be set to + /// application/json; charset=utf-8. + /// + /// The type of object to write. + /// The response to write JSON to. + /// The value to write as JSON. + /// A used to cancel the operation. + /// The task object representing the asynchronous operation. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static Task WriteAsJsonAsync( + this HttpResponse response, + TValue value, + CancellationToken cancellationToken = default) + { + return response.WriteAsJsonAsync(value, options: null, contentType: null, cancellationToken); + } + + /// + /// Write the specified value as JSON to the response body. The response content-type will be set to + /// application/json; charset=utf-8. + /// + /// The type of object to write. + /// The response to write JSON to. + /// The value to write as JSON. + /// The serializer options use when serializing the value. + /// A used to cancel the operation. + /// The task object representing the asynchronous operation. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static Task WriteAsJsonAsync( + this HttpResponse response, + TValue value, + JsonSerializerOptions? options, + CancellationToken cancellationToken = default) + { + return response.WriteAsJsonAsync(value, options, contentType: null, cancellationToken); + } + + /// + /// Write the specified value as JSON to the response body. The response content-type will be set to + /// the specified content-type. /// - public static partial class HttpResponseJsonExtensions + /// The type of object to write. + /// The response to write JSON to. + /// The value to write as JSON. + /// The serializer options use when serializing the value. + /// The content-type to set on the response. + /// A used to cancel the operation. + /// The task object representing the asynchronous operation. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static Task WriteAsJsonAsync( + this HttpResponse response, + TValue value, + JsonSerializerOptions? options, + string? contentType, + CancellationToken cancellationToken = default) { - /// - /// Write the specified value as JSON to the response body. The response content-type will be set to - /// application/json; charset=utf-8. - /// - /// The type of object to write. - /// The response to write JSON to. - /// The value to write as JSON. - /// A used to cancel the operation. - /// The task object representing the asynchronous operation. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static Task WriteAsJsonAsync( - this HttpResponse response, - TValue value, - CancellationToken cancellationToken = default) + if (response == null) { - return response.WriteAsJsonAsync(value, options: null, contentType: null, cancellationToken); + throw new ArgumentNullException(nameof(response)); } - /// - /// Write the specified value as JSON to the response body. The response content-type will be set to - /// application/json; charset=utf-8. - /// - /// The type of object to write. - /// The response to write JSON to. - /// The value to write as JSON. - /// The serializer options use when serializing the value. - /// A used to cancel the operation. - /// The task object representing the asynchronous operation. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static Task WriteAsJsonAsync( - this HttpResponse response, - TValue value, - JsonSerializerOptions? options, - CancellationToken cancellationToken = default) + options ??= ResolveSerializerOptions(response.HttpContext); + + response.ContentType = contentType ?? JsonConstants.JsonContentTypeWithCharset; + // if no user provided token, pass the RequestAborted token and ignore OperationCanceledException + if (!cancellationToken.CanBeCanceled) { - return response.WriteAsJsonAsync(value, options, contentType: null, cancellationToken); + return WriteAsJsonAsyncSlow(response.Body, value, options, response.HttpContext.RequestAborted); } - /// - /// Write the specified value as JSON to the response body. The response content-type will be set to - /// the specified content-type. - /// - /// The type of object to write. - /// The response to write JSON to. - /// The value to write as JSON. - /// The serializer options use when serializing the value. - /// The content-type to set on the response. - /// A used to cancel the operation. - /// The task object representing the asynchronous operation. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static Task WriteAsJsonAsync( - this HttpResponse response, - TValue value, - JsonSerializerOptions? options, - string? contentType, - CancellationToken cancellationToken = default) - { - if (response == null) - { - throw new ArgumentNullException(nameof(response)); - } + return JsonSerializer.SerializeAsync(response.Body, value, options, cancellationToken); + } - options ??= ResolveSerializerOptions(response.HttpContext); + private static async Task WriteAsJsonAsyncSlow( + Stream body, + TValue value, + JsonSerializerOptions? options, + CancellationToken cancellationToken) + { + try + { + await JsonSerializer.SerializeAsync(body, value, options, cancellationToken); + } + catch (OperationCanceledException) { } + } - response.ContentType = contentType ?? JsonConstants.JsonContentTypeWithCharset; - // if no user provided token, pass the RequestAborted token and ignore OperationCanceledException - if (!cancellationToken.CanBeCanceled) - { - return WriteAsJsonAsyncSlow(response.Body, value, options, response.HttpContext.RequestAborted); - } + /// + /// Write the specified value as JSON to the response body. The response content-type will be set to + /// application/json; charset=utf-8. + /// + /// The response to write JSON to. + /// The value to write as JSON. + /// The type of object to write. + /// A used to cancel the operation. + /// The task object representing the asynchronous operation. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static Task WriteAsJsonAsync( + this HttpResponse response, + object? value, + Type type, + CancellationToken cancellationToken = default) + { + return response.WriteAsJsonAsync(value, type, options: null, contentType: null, cancellationToken); + } - return JsonSerializer.SerializeAsync(response.Body, value, options, cancellationToken); - } + /// + /// Write the specified value as JSON to the response body. The response content-type will be set to + /// application/json; charset=utf-8. + /// + /// The response to write JSON to. + /// The value to write as JSON. + /// The type of object to write. + /// The serializer options use when serializing the value. + /// A used to cancel the operation. + /// The task object representing the asynchronous operation. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static Task WriteAsJsonAsync( + this HttpResponse response, + object? value, + Type type, + JsonSerializerOptions? options, + CancellationToken cancellationToken = default) + { + return response.WriteAsJsonAsync(value, type, options, contentType: null, cancellationToken); + } - private static async Task WriteAsJsonAsyncSlow( - Stream body, - TValue value, - JsonSerializerOptions? options, - CancellationToken cancellationToken) + /// + /// Write the specified value as JSON to the response body. The response content-type will be set to + /// the specified content-type. + /// + /// The response to write JSON to. + /// The value to write as JSON. + /// The type of object to write. + /// The serializer options use when serializing the value. + /// The content-type to set on the response. + /// A used to cancel the operation. + /// The task object representing the asynchronous operation. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static Task WriteAsJsonAsync( + this HttpResponse response, + object? value, + Type type, + JsonSerializerOptions? options, + string? contentType, + CancellationToken cancellationToken = default) + { + if (response == null) { - try - { - await JsonSerializer.SerializeAsync(body, value, options, cancellationToken); - } - catch (OperationCanceledException) { } + throw new ArgumentNullException(nameof(response)); } - - /// - /// Write the specified value as JSON to the response body. The response content-type will be set to - /// application/json; charset=utf-8. - /// - /// The response to write JSON to. - /// The value to write as JSON. - /// The type of object to write. - /// A used to cancel the operation. - /// The task object representing the asynchronous operation. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static Task WriteAsJsonAsync( - this HttpResponse response, - object? value, - Type type, - CancellationToken cancellationToken = default) + if (type == null) { - return response.WriteAsJsonAsync(value, type, options: null, contentType: null, cancellationToken); + throw new ArgumentNullException(nameof(type)); } - /// - /// Write the specified value as JSON to the response body. The response content-type will be set to - /// application/json; charset=utf-8. - /// - /// The response to write JSON to. - /// The value to write as JSON. - /// The type of object to write. - /// The serializer options use when serializing the value. - /// A used to cancel the operation. - /// The task object representing the asynchronous operation. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static Task WriteAsJsonAsync( - this HttpResponse response, - object? value, - Type type, - JsonSerializerOptions? options, - CancellationToken cancellationToken = default) - { - return response.WriteAsJsonAsync(value, type, options, contentType: null, cancellationToken); - } + options ??= ResolveSerializerOptions(response.HttpContext); - /// - /// Write the specified value as JSON to the response body. The response content-type will be set to - /// the specified content-type. - /// - /// The response to write JSON to. - /// The value to write as JSON. - /// The type of object to write. - /// The serializer options use when serializing the value. - /// The content-type to set on the response. - /// A used to cancel the operation. - /// The task object representing the asynchronous operation. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static Task WriteAsJsonAsync( - this HttpResponse response, - object? value, - Type type, - JsonSerializerOptions? options, - string? contentType, - CancellationToken cancellationToken = default) - { - if (response == null) - { - throw new ArgumentNullException(nameof(response)); - } - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - options ??= ResolveSerializerOptions(response.HttpContext); - - response.ContentType = contentType ?? JsonConstants.JsonContentTypeWithCharset; - - // if no user provided token, pass the RequestAborted token and ignore OperationCanceledException - if (!cancellationToken.CanBeCanceled) - { - return WriteAsJsonAsyncSlow(response.Body, value, type, options, response.HttpContext.RequestAborted); - } - - return JsonSerializer.SerializeAsync(response.Body, value, type, options, cancellationToken); - } + response.ContentType = contentType ?? JsonConstants.JsonContentTypeWithCharset; - private static async Task WriteAsJsonAsyncSlow( - Stream body, - object? value, - Type type, - JsonSerializerOptions? options, - CancellationToken cancellationToken) + // if no user provided token, pass the RequestAborted token and ignore OperationCanceledException + if (!cancellationToken.CanBeCanceled) { - try - { - await JsonSerializer.SerializeAsync(body, value, type, options, cancellationToken); - } - catch (OperationCanceledException) { } + return WriteAsJsonAsyncSlow(response.Body, value, type, options, response.HttpContext.RequestAborted); } - private static JsonSerializerOptions ResolveSerializerOptions(HttpContext httpContext) + return JsonSerializer.SerializeAsync(response.Body, value, type, options, cancellationToken); + } + + private static async Task WriteAsJsonAsyncSlow( + Stream body, + object? value, + Type type, + JsonSerializerOptions? options, + CancellationToken cancellationToken) + { + try { - // Attempt to resolve options from DI then fallback to default options - return httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; + await JsonSerializer.SerializeAsync(body, value, type, options, cancellationToken); } + catch (OperationCanceledException) { } + } + + private static JsonSerializerOptions ResolveSerializerOptions(HttpContext httpContext) + { + // Attempt to resolve options from DI then fallback to default options + return httpContext.RequestServices?.GetService>()?.Value?.SerializerOptions ?? JsonOptions.DefaultSerializerOptions; } } diff --git a/src/Http/Http.Extensions/src/HttpValidationProblemDetails.cs b/src/Http/Http.Extensions/src/HttpValidationProblemDetails.cs index be1543d916..5bab6886c7 100644 --- a/src/Http/Http.Extensions/src/HttpValidationProblemDetails.cs +++ b/src/Http/Http.Extensions/src/HttpValidationProblemDetails.cs @@ -4,40 +4,39 @@ using System.Text.Json.Serialization; using Microsoft.AspNetCore.Mvc; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// A for validation errors. +/// +[JsonConverter(typeof(HttpValidationProblemDetailsJsonConverter))] +public class HttpValidationProblemDetails : ProblemDetails { /// - /// A for validation errors. + /// Initializes a new instance of . /// - [JsonConverter(typeof(HttpValidationProblemDetailsJsonConverter))] - public class HttpValidationProblemDetails : ProblemDetails + public HttpValidationProblemDetails() + : this(new Dictionary(StringComparer.Ordinal)) { - /// - /// Initializes a new instance of . - /// - public HttpValidationProblemDetails() - : this(new Dictionary(StringComparer.Ordinal)) - { - } - - /// - /// Initializes a new instance of using the specified . - /// - /// The validation errors. - public HttpValidationProblemDetails(IDictionary errors) - : this(new Dictionary(errors, StringComparer.Ordinal)) - { - } + } - private HttpValidationProblemDetails(Dictionary errors) - { - Title = "One or more validation errors occurred."; - Errors = errors; - } + /// + /// Initializes a new instance of using the specified . + /// + /// The validation errors. + public HttpValidationProblemDetails(IDictionary errors) + : this(new Dictionary(errors, StringComparer.Ordinal)) + { + } - /// - /// Gets the validation errors associated with this instance of . - /// - public IDictionary Errors { get; } = new Dictionary(StringComparer.Ordinal); + private HttpValidationProblemDetails(Dictionary errors) + { + Title = "One or more validation errors occurred."; + Errors = errors; } + + /// + /// Gets the validation errors associated with this instance of . + /// + public IDictionary Errors { get; } = new Dictionary(StringComparer.Ordinal); } diff --git a/src/Http/Http.Extensions/src/JsonConstants.cs b/src/Http/Http.Extensions/src/JsonConstants.cs index 4f66d5228e..4e8411b180 100644 --- a/src/Http/Http.Extensions/src/JsonConstants.cs +++ b/src/Http/Http.Extensions/src/JsonConstants.cs @@ -1,11 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +internal static class JsonConstants { - internal static class JsonConstants - { - public const string JsonContentType = "application/json"; - public const string JsonContentTypeWithCharset = "application/json; charset=utf-8"; - } + public const string JsonContentType = "application/json"; + public const string JsonContentTypeWithCharset = "application/json; charset=utf-8"; } diff --git a/src/Http/Http.Extensions/src/JsonOptions.cs b/src/Http/Http.Extensions/src/JsonOptions.cs index 2fa2cc87c5..220ad7938f 100644 --- a/src/Http/Http.Extensions/src/JsonOptions.cs +++ b/src/Http/Http.Extensions/src/JsonOptions.cs @@ -6,27 +6,26 @@ using System.Text.Json; #nullable enable -namespace Microsoft.AspNetCore.Http.Json +namespace Microsoft.AspNetCore.Http.Json; + +/// +/// Options to configure JSON serialization settings for +/// and . +/// +public class JsonOptions { - /// - /// Options to configure JSON serialization settings for - /// and . - /// - public class JsonOptions + internal static readonly JsonSerializerOptions DefaultSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) { - internal static readonly JsonSerializerOptions DefaultSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) - { - // Web defaults don't use the relex JSON escaping encoder. - // - // Because these options are for producing content that is written directly to the request - // (and not embedded in an HTML page for example), we can use UnsafeRelaxedJsonEscaping. - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, - }; + // Web defaults don't use the relex JSON escaping encoder. + // + // Because these options are for producing content that is written directly to the request + // (and not embedded in an HTML page for example), we can use UnsafeRelaxedJsonEscaping. + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + }; - // Use a copy so the defaults are not modified. - /// - /// Gets the . - /// - public JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions(DefaultSerializerOptions); - } + // Use a copy so the defaults are not modified. + /// + /// Gets the . + /// + public JsonSerializerOptions SerializerOptions { get; } = new JsonSerializerOptions(DefaultSerializerOptions); } diff --git a/src/Http/Http.Extensions/src/ProblemDetails.cs b/src/Http/Http.Extensions/src/ProblemDetails.cs index d04ea9f429..9776f1205f 100644 --- a/src/Http/Http.Extensions/src/ProblemDetails.cs +++ b/src/Http/Http.Extensions/src/ProblemDetails.cs @@ -4,61 +4,60 @@ using System.Text.Json.Serialization; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Mvc +namespace Microsoft.AspNetCore.Mvc; + +/// +/// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807. +/// +[JsonConverter(typeof(ProblemDetailsJsonConverter))] +public class ProblemDetails { /// - /// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807. + /// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when + /// dereferenced, it provide human-readable documentation for the problem type + /// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be + /// "about:blank". /// - [JsonConverter(typeof(ProblemDetailsJsonConverter))] - public class ProblemDetails - { - /// - /// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when - /// dereferenced, it provide human-readable documentation for the problem type - /// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be - /// "about:blank". - /// - [JsonPropertyName("type")] - public string? Type { get; set; } + [JsonPropertyName("type")] + public string? Type { get; set; } - /// - /// A short, human-readable summary of the problem type.It SHOULD NOT change from occurrence to occurrence - /// of the problem, except for purposes of localization(e.g., using proactive content negotiation; - /// see[RFC7231], Section 3.4). - /// - [JsonPropertyName("title")] - public string? Title { get; set; } + /// + /// A short, human-readable summary of the problem type.It SHOULD NOT change from occurrence to occurrence + /// of the problem, except for purposes of localization(e.g., using proactive content negotiation; + /// see[RFC7231], Section 3.4). + /// + [JsonPropertyName("title")] + public string? Title { get; set; } - /// - /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. - /// - [JsonPropertyName("status")] - public int? Status { get; set; } + /// + /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. + /// + [JsonPropertyName("status")] + public int? Status { get; set; } - /// - /// A human-readable explanation specific to this occurrence of the problem. - /// - [JsonPropertyName("detail")] - public string? Detail { get; set; } + /// + /// A human-readable explanation specific to this occurrence of the problem. + /// + [JsonPropertyName("detail")] + public string? Detail { get; set; } - /// - /// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced. - /// - [JsonPropertyName("instance")] - public string? Instance { get; set; } + /// + /// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced. + /// + [JsonPropertyName("instance")] + public string? Instance { get; set; } - /// - /// Gets the for extension members. - /// - /// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as - /// other members of a problem type. - /// - /// - /// - /// The round-tripping behavior for is determined by the implementation of the Input \ Output formatters. - /// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters. - /// - [JsonExtensionData] - public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); - } + /// + /// Gets the for extension members. + /// + /// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as + /// other members of a problem type. + /// + /// + /// + /// The round-tripping behavior for is determined by the implementation of the Input \ Output formatters. + /// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters. + /// + [JsonExtensionData] + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); } diff --git a/src/Http/Http.Extensions/src/QueryBuilder.cs b/src/Http/Http.Extensions/src/QueryBuilder.cs index 930c0dd9ec..7175bd16ea 100644 --- a/src/Http/Http.Extensions/src/QueryBuilder.cs +++ b/src/Http/Http.Extensions/src/QueryBuilder.cs @@ -8,114 +8,113 @@ using System.Text; using System.Text.Encodings.Web; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Http.Extensions +namespace Microsoft.AspNetCore.Http.Extensions; + +// The IEnumerable interface is required for the collection initialization syntax: new QueryBuilder() { { "key", "value" } }; +/// +/// Allows constructing a query string. +/// +public class QueryBuilder : IEnumerable> { - // The IEnumerable interface is required for the collection initialization syntax: new QueryBuilder() { { "key", "value" } }; + private readonly IList> _params; + /// - /// Allows constructing a query string. + /// Initializes a new instance of . /// - public class QueryBuilder : IEnumerable> + public QueryBuilder() { - private readonly IList> _params; - - /// - /// Initializes a new instance of . - /// - public QueryBuilder() - { - _params = new List>(); - } - - /// - /// Initializes a new instance of . - /// - /// The parameters to initialize the instance with. - public QueryBuilder(IEnumerable> parameters) - { - _params = new List>(parameters); - } + _params = new List>(); + } - /// - /// Initializes a new instance of . - /// - /// The parameters to initialize the instance with. - public QueryBuilder(IEnumerable> parameters) - : this(parameters.SelectMany(kvp => kvp.Value, (kvp, v) => KeyValuePair.Create(kvp.Key, v ?? string.Empty))) - { + /// + /// Initializes a new instance of . + /// + /// The parameters to initialize the instance with. + public QueryBuilder(IEnumerable> parameters) + { + _params = new List>(parameters); + } - } + /// + /// Initializes a new instance of . + /// + /// The parameters to initialize the instance with. + public QueryBuilder(IEnumerable> parameters) + : this(parameters.SelectMany(kvp => kvp.Value, (kvp, v) => KeyValuePair.Create(kvp.Key, v ?? string.Empty))) + { - /// - /// Adds a query string token to the instance. - /// - /// The query key. - /// The sequence of query values. - public void Add(string key, IEnumerable values) - { - foreach (var value in values) - { - _params.Add(new KeyValuePair(key, value)); - } - } + } - /// - /// Adds a query string token to the instance. - /// - /// The query key. - /// The query value. - public void Add(string key, string value) + /// + /// Adds a query string token to the instance. + /// + /// The query key. + /// The sequence of query values. + public void Add(string key, IEnumerable values) + { + foreach (var value in values) { _params.Add(new KeyValuePair(key, value)); } + } - /// - public override string ToString() - { - var builder = new StringBuilder(); - bool first = true; - for (var i = 0; i < _params.Count; i++) - { - var pair = _params[i]; - builder.Append(first ? '?' : '&'); - first = false; - builder.Append(UrlEncoder.Default.Encode(pair.Key)); - builder.Append('='); - builder.Append(UrlEncoder.Default.Encode(pair.Value)); - } - - return builder.ToString(); - } + /// + /// Adds a query string token to the instance. + /// + /// The query key. + /// The query value. + public void Add(string key, string value) + { + _params.Add(new KeyValuePair(key, value)); + } - /// - /// Constructs a from this . - /// - /// The . - public QueryString ToQueryString() + /// + public override string ToString() + { + var builder = new StringBuilder(); + bool first = true; + for (var i = 0; i < _params.Count; i++) { - return new QueryString(ToString()); + var pair = _params[i]; + builder.Append(first ? '?' : '&'); + first = false; + builder.Append(UrlEncoder.Default.Encode(pair.Key)); + builder.Append('='); + builder.Append(UrlEncoder.Default.Encode(pair.Value)); } - /// - public override int GetHashCode() - { - return ToQueryString().GetHashCode(); - } + return builder.ToString(); + } - /// - public override bool Equals(object? obj) - { - return ToQueryString().Equals(obj); - } + /// + /// Constructs a from this . + /// + /// The . + public QueryString ToQueryString() + { + return new QueryString(ToString()); + } - /// - public IEnumerator> GetEnumerator() - { - return _params.GetEnumerator(); - } + /// + public override int GetHashCode() + { + return ToQueryString().GetHashCode(); + } - IEnumerator IEnumerable.GetEnumerator() - { - return _params.GetEnumerator(); - } + /// + public override bool Equals(object? obj) + { + return ToQueryString().Equals(obj); + } + + /// + public IEnumerator> GetEnumerator() + { + return _params.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _params.GetEnumerator(); } } diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs index 227be2f576..5864b8bd27 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactory.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactory.cs @@ -15,549 +15,509 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Creates implementations from request handlers. +/// +public static partial class RequestDelegateFactory { + private static readonly ParameterBindingMethodCache ParameterBindingMethodCache = new(); + + private static readonly MethodInfo ExecuteTaskOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTask), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo ExecuteTaskOfStringMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskOfString), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo ExecuteValueTaskOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskOfT), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo ExecuteValueTaskMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTask), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo ExecuteValueTaskOfStringMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskOfString), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo ExecuteTaskResultOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo ExecuteValueResultTaskOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo ExecuteObjectReturnMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteObjectReturn), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo GetRequiredServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetRequiredService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!; + private static readonly MethodInfo GetServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!; + private static readonly MethodInfo ResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteResultWriteResponse), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo StringResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteWriteStringResponseAsync), BindingFlags.NonPublic | BindingFlags.Static)!; + private static readonly MethodInfo JsonResultWriteResponseAsyncMethod = GetMethodInfo>((response, value) => HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, default)); + + private static readonly MethodInfo LogParameterBindingFailedMethod = GetMethodInfo>((httpContext, parameterType, parameterName, sourceValue, shouldThrow) => + Log.ParameterBindingFailed(httpContext, parameterType, parameterName, sourceValue, shouldThrow)); + private static readonly MethodInfo LogRequiredParameterNotProvidedMethod = GetMethodInfo>((httpContext, parameterType, parameterName, source, shouldThrow) => + Log.RequiredParameterNotProvided(httpContext, parameterType, parameterName, source, shouldThrow)); + private static readonly MethodInfo LogImplicitBodyNotProvidedMethod = GetMethodInfo>((httpContext, parameterName, shouldThrow) => + Log.ImplicitBodyNotProvided(httpContext, parameterName, shouldThrow)); + + private static readonly ParameterExpression TargetExpr = Expression.Parameter(typeof(object), "target"); + private static readonly ParameterExpression BodyValueExpr = Expression.Parameter(typeof(object), "bodyValue"); + private static readonly ParameterExpression WasParamCheckFailureExpr = Expression.Variable(typeof(bool), "wasParamCheckFailure"); + private static readonly ParameterExpression BoundValuesArrayExpr = Expression.Parameter(typeof(object[]), "boundValues"); + + private static readonly ParameterExpression HttpContextExpr = ParameterBindingMethodCache.HttpContextExpr; + private static readonly MemberExpression RequestServicesExpr = Expression.Property(HttpContextExpr, typeof(HttpContext).GetProperty(nameof(HttpContext.RequestServices))!); + private static readonly MemberExpression HttpRequestExpr = Expression.Property(HttpContextExpr, typeof(HttpContext).GetProperty(nameof(HttpContext.Request))!); + private static readonly MemberExpression HttpResponseExpr = Expression.Property(HttpContextExpr, typeof(HttpContext).GetProperty(nameof(HttpContext.Response))!); + private static readonly MemberExpression RequestAbortedExpr = Expression.Property(HttpContextExpr, typeof(HttpContext).GetProperty(nameof(HttpContext.RequestAborted))!); + private static readonly MemberExpression UserExpr = Expression.Property(HttpContextExpr, typeof(HttpContext).GetProperty(nameof(HttpContext.User))!); + private static readonly MemberExpression RouteValuesExpr = Expression.Property(HttpRequestExpr, typeof(HttpRequest).GetProperty(nameof(HttpRequest.RouteValues))!); + private static readonly MemberExpression QueryExpr = Expression.Property(HttpRequestExpr, typeof(HttpRequest).GetProperty(nameof(HttpRequest.Query))!); + private static readonly MemberExpression HeadersExpr = Expression.Property(HttpRequestExpr, typeof(HttpRequest).GetProperty(nameof(HttpRequest.Headers))!); + private static readonly MemberExpression StatusCodeExpr = Expression.Property(HttpResponseExpr, typeof(HttpResponse).GetProperty(nameof(HttpResponse.StatusCode))!); + private static readonly MemberExpression CompletedTaskExpr = Expression.Property(null, (PropertyInfo)GetMemberInfo>(() => Task.CompletedTask)); + + private static readonly ParameterExpression TempSourceStringExpr = ParameterBindingMethodCache.TempSourceStringExpr; + private static readonly BinaryExpression TempSourceStringNotNullExpr = Expression.NotEqual(TempSourceStringExpr, Expression.Constant(null)); + private static readonly BinaryExpression TempSourceStringNullExpr = Expression.Equal(TempSourceStringExpr, Expression.Constant(null)); + private static readonly string[] DefaultAcceptsContentType = new[] { "application/json" }; + /// - /// Creates implementations from request handlers. + /// Creates a implementation for . /// - public static partial class RequestDelegateFactory - { - private static readonly ParameterBindingMethodCache ParameterBindingMethodCache = new(); - - private static readonly MethodInfo ExecuteTaskOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTask), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo ExecuteTaskOfStringMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskOfString), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo ExecuteValueTaskOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskOfT), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo ExecuteValueTaskMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTask), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo ExecuteValueTaskOfStringMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskOfString), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo ExecuteTaskResultOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo ExecuteValueResultTaskOfTMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteValueTaskResult), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo ExecuteObjectReturnMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteObjectReturn), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo GetRequiredServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetRequiredService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!; - private static readonly MethodInfo GetServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod(nameof(ServiceProviderServiceExtensions.GetService), BindingFlags.Public | BindingFlags.Static, new Type[] { typeof(IServiceProvider) })!; - private static readonly MethodInfo ResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteResultWriteResponse), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo StringResultWriteResponseAsyncMethod = typeof(RequestDelegateFactory).GetMethod(nameof(ExecuteWriteStringResponseAsync), BindingFlags.NonPublic | BindingFlags.Static)!; - private static readonly MethodInfo JsonResultWriteResponseAsyncMethod = GetMethodInfo>((response, value) => HttpResponseJsonExtensions.WriteAsJsonAsync(response, value, default)); - - private static readonly MethodInfo LogParameterBindingFailedMethod = GetMethodInfo>((httpContext, parameterType, parameterName, sourceValue, shouldThrow) => - Log.ParameterBindingFailed(httpContext, parameterType, parameterName, sourceValue, shouldThrow)); - private static readonly MethodInfo LogRequiredParameterNotProvidedMethod = GetMethodInfo>((httpContext, parameterType, parameterName, source, shouldThrow) => - Log.RequiredParameterNotProvided(httpContext, parameterType, parameterName, source, shouldThrow)); - private static readonly MethodInfo LogImplicitBodyNotProvidedMethod = GetMethodInfo>((httpContext, parameterName, shouldThrow) => - Log.ImplicitBodyNotProvided(httpContext, parameterName, shouldThrow)); - - private static readonly ParameterExpression TargetExpr = Expression.Parameter(typeof(object), "target"); - private static readonly ParameterExpression BodyValueExpr = Expression.Parameter(typeof(object), "bodyValue"); - private static readonly ParameterExpression WasParamCheckFailureExpr = Expression.Variable(typeof(bool), "wasParamCheckFailure"); - private static readonly ParameterExpression BoundValuesArrayExpr = Expression.Parameter(typeof(object[]), "boundValues"); - - private static readonly ParameterExpression HttpContextExpr = ParameterBindingMethodCache.HttpContextExpr; - private static readonly MemberExpression RequestServicesExpr = Expression.Property(HttpContextExpr, typeof(HttpContext).GetProperty(nameof(HttpContext.RequestServices))!); - private static readonly MemberExpression HttpRequestExpr = Expression.Property(HttpContextExpr, typeof(HttpContext).GetProperty(nameof(HttpContext.Request))!); - private static readonly MemberExpression HttpResponseExpr = Expression.Property(HttpContextExpr, typeof(HttpContext).GetProperty(nameof(HttpContext.Response))!); - private static readonly MemberExpression RequestAbortedExpr = Expression.Property(HttpContextExpr, typeof(HttpContext).GetProperty(nameof(HttpContext.RequestAborted))!); - private static readonly MemberExpression UserExpr = Expression.Property(HttpContextExpr, typeof(HttpContext).GetProperty(nameof(HttpContext.User))!); - private static readonly MemberExpression RouteValuesExpr = Expression.Property(HttpRequestExpr, typeof(HttpRequest).GetProperty(nameof(HttpRequest.RouteValues))!); - private static readonly MemberExpression QueryExpr = Expression.Property(HttpRequestExpr, typeof(HttpRequest).GetProperty(nameof(HttpRequest.Query))!); - private static readonly MemberExpression HeadersExpr = Expression.Property(HttpRequestExpr, typeof(HttpRequest).GetProperty(nameof(HttpRequest.Headers))!); - private static readonly MemberExpression StatusCodeExpr = Expression.Property(HttpResponseExpr, typeof(HttpResponse).GetProperty(nameof(HttpResponse.StatusCode))!); - private static readonly MemberExpression CompletedTaskExpr = Expression.Property(null, (PropertyInfo)GetMemberInfo>(() => Task.CompletedTask)); - - private static readonly ParameterExpression TempSourceStringExpr = ParameterBindingMethodCache.TempSourceStringExpr; - private static readonly BinaryExpression TempSourceStringNotNullExpr = Expression.NotEqual(TempSourceStringExpr, Expression.Constant(null)); - private static readonly BinaryExpression TempSourceStringNullExpr = Expression.Equal(TempSourceStringExpr, Expression.Constant(null)); - private static readonly string[] DefaultAcceptsContentType = new[] { "application/json" }; - - /// - /// Creates a implementation for . - /// - /// A request handler with any number of custom parameters that often produces a response with its return value. - /// The used to configure the behavior of the handler. - /// The . + /// A request handler with any number of custom parameters that often produces a response with its return value. + /// The used to configure the behavior of the handler. + /// The . #pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters - public static RequestDelegateResult Create(Delegate handler, RequestDelegateFactoryOptions? options = null) + public static RequestDelegateResult Create(Delegate handler, RequestDelegateFactoryOptions? options = null) #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + { + if (handler is null) { - if (handler is null) - { - throw new ArgumentNullException(nameof(handler)); - } + throw new ArgumentNullException(nameof(handler)); + } - var targetExpression = handler.Target switch - { - object => Expression.Convert(TargetExpr, handler.Target.GetType()), - null => null, - }; + var targetExpression = handler.Target switch + { + object => Expression.Convert(TargetExpr, handler.Target.GetType()), + null => null, + }; - var factoryContext = CreateFactoryContext(options); - var targetableRequestDelegate = CreateTargetableRequestDelegate(handler.Method, targetExpression, factoryContext); + var factoryContext = CreateFactoryContext(options); + var targetableRequestDelegate = CreateTargetableRequestDelegate(handler.Method, targetExpression, factoryContext); - return new RequestDelegateResult(httpContext => targetableRequestDelegate(handler.Target, httpContext), factoryContext.Metadata); - } + return new RequestDelegateResult(httpContext => targetableRequestDelegate(handler.Target, httpContext), factoryContext.Metadata); + } - /// - /// Creates a implementation for . - /// - /// A request handler with any number of custom parameters that often produces a response with its return value. - /// Creates the for the non-static method. - /// The used to configure the behavior of the handler. - /// The . + /// + /// Creates a implementation for . + /// + /// A request handler with any number of custom parameters that often produces a response with its return value. + /// Creates the for the non-static method. + /// The used to configure the behavior of the handler. + /// The . #pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters - public static RequestDelegateResult Create(MethodInfo methodInfo, Func? targetFactory = null, RequestDelegateFactoryOptions? options = null) + public static RequestDelegateResult Create(MethodInfo methodInfo, Func? targetFactory = null, RequestDelegateFactoryOptions? options = null) #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + { + if (methodInfo is null) { - if (methodInfo is null) - { - throw new ArgumentNullException(nameof(methodInfo)); - } + throw new ArgumentNullException(nameof(methodInfo)); + } - if (methodInfo.DeclaringType is null) - { - throw new ArgumentException($"{nameof(methodInfo)} does not have a declaring type."); - } + if (methodInfo.DeclaringType is null) + { + throw new ArgumentException($"{nameof(methodInfo)} does not have a declaring type."); + } - var factoryContext = CreateFactoryContext(options); + var factoryContext = CreateFactoryContext(options); - if (targetFactory is null) + if (targetFactory is null) + { + if (methodInfo.IsStatic) { - if (methodInfo.IsStatic) - { - var untargetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, targetExpression: null, factoryContext); - - return new RequestDelegateResult(httpContext => untargetableRequestDelegate(null, httpContext), factoryContext.Metadata); - } + var untargetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, targetExpression: null, factoryContext); - targetFactory = context => Activator.CreateInstance(methodInfo.DeclaringType)!; + return new RequestDelegateResult(httpContext => untargetableRequestDelegate(null, httpContext), factoryContext.Metadata); } - var targetExpression = Expression.Convert(TargetExpr, methodInfo.DeclaringType); - var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, targetExpression, factoryContext); - - return new RequestDelegateResult(httpContext => targetableRequestDelegate(targetFactory(httpContext), httpContext), factoryContext.Metadata); + targetFactory = context => Activator.CreateInstance(methodInfo.DeclaringType)!; } - private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions? options) => - new() - { - ServiceProviderIsService = options?.ServiceProvider?.GetService(), - RouteParameters = options?.RouteParameterNames?.ToList(), - ThrowOnBadRequest = options?.ThrowOnBadRequest ?? false, - DisableInferredFromBody = options?.DisableInferBodyFromParameters ?? false, - }; + var targetExpression = Expression.Convert(TargetExpr, methodInfo.DeclaringType); + var targetableRequestDelegate = CreateTargetableRequestDelegate(methodInfo, targetExpression, factoryContext); - private static Func CreateTargetableRequestDelegate(MethodInfo methodInfo, Expression? targetExpression, FactoryContext factoryContext) + return new RequestDelegateResult(httpContext => targetableRequestDelegate(targetFactory(httpContext), httpContext), factoryContext.Metadata); + } + + private static FactoryContext CreateFactoryContext(RequestDelegateFactoryOptions? options) => + new() { - // Non void return type + ServiceProviderIsService = options?.ServiceProvider?.GetService(), + RouteParameters = options?.RouteParameterNames?.ToList(), + ThrowOnBadRequest = options?.ThrowOnBadRequest ?? false, + DisableInferredFromBody = options?.DisableInferBodyFromParameters ?? false, + }; - // Task Invoke(HttpContext httpContext) - // { - // // Action parameters are bound from the request, services, etc... based on attribute and type information. - // return ExecuteTask(handler(...), httpContext); - // } + private static Func CreateTargetableRequestDelegate(MethodInfo methodInfo, Expression? targetExpression, FactoryContext factoryContext) + { + // Non void return type - // void return type + // Task Invoke(HttpContext httpContext) + // { + // // Action parameters are bound from the request, services, etc... based on attribute and type information. + // return ExecuteTask(handler(...), httpContext); + // } - // Task Invoke(HttpContext httpContext) - // { - // handler(...); - // return default; - // } + // void return type - var arguments = CreateArguments(methodInfo.GetParameters(), factoryContext); + // Task Invoke(HttpContext httpContext) + // { + // handler(...); + // return default; + // } - var responseWritingMethodCall = factoryContext.ParamCheckExpressions.Count > 0 ? - CreateParamCheckingResponseWritingMethodCall(methodInfo, targetExpression, arguments, factoryContext) : - CreateResponseWritingMethodCall(methodInfo, targetExpression, arguments); + var arguments = CreateArguments(methodInfo.GetParameters(), factoryContext); - if (factoryContext.UsingTempSourceString) - { - responseWritingMethodCall = Expression.Block(new[] { TempSourceStringExpr }, responseWritingMethodCall); - } + var responseWritingMethodCall = factoryContext.ParamCheckExpressions.Count > 0 ? + CreateParamCheckingResponseWritingMethodCall(methodInfo, targetExpression, arguments, factoryContext) : + CreateResponseWritingMethodCall(methodInfo, targetExpression, arguments); - return HandleRequestBodyAndCompileRequestDelegate(responseWritingMethodCall, factoryContext); + if (factoryContext.UsingTempSourceString) + { + responseWritingMethodCall = Expression.Block(new[] { TempSourceStringExpr }, responseWritingMethodCall); } - private static Expression[] CreateArguments(ParameterInfo[]? parameters, FactoryContext factoryContext) + return HandleRequestBodyAndCompileRequestDelegate(responseWritingMethodCall, factoryContext); + } + + private static Expression[] CreateArguments(ParameterInfo[]? parameters, FactoryContext factoryContext) + { + if (parameters is null || parameters.Length == 0) { - if (parameters is null || parameters.Length == 0) - { - return Array.Empty(); - } + return Array.Empty(); + } - var args = new Expression[parameters.Length]; + var args = new Expression[parameters.Length]; - for (var i = 0; i < parameters.Length; i++) - { - args[i] = CreateArgument(parameters[i], factoryContext); - } + for (var i = 0; i < parameters.Length; i++) + { + args[i] = CreateArgument(parameters[i], factoryContext); + } - if (factoryContext.HasInferredBody && factoryContext.DisableInferredFromBody) - { - var errorMessage = BuildErrorMessageForInferredBodyParameter(factoryContext); - throw new InvalidOperationException(errorMessage); - } - if (factoryContext.HasMultipleBodyParameters) - { - var errorMessage = BuildErrorMessageForMultipleBodyParameters(factoryContext); - throw new InvalidOperationException(errorMessage); - } + if (factoryContext.HasInferredBody && factoryContext.DisableInferredFromBody) + { + var errorMessage = BuildErrorMessageForInferredBodyParameter(factoryContext); + throw new InvalidOperationException(errorMessage); + } + if (factoryContext.HasMultipleBodyParameters) + { + var errorMessage = BuildErrorMessageForMultipleBodyParameters(factoryContext); + throw new InvalidOperationException(errorMessage); + } + + return args; + } - return args; + private static Expression CreateArgument(ParameterInfo parameter, FactoryContext factoryContext) + { + if (parameter.Name is null) + { + throw new InvalidOperationException($"Encountered a parameter of type '{parameter.ParameterType}' without a name. Parameters must have a name."); } - private static Expression CreateArgument(ParameterInfo parameter, FactoryContext factoryContext) + var parameterCustomAttributes = parameter.GetCustomAttributes(); + + if (parameterCustomAttributes.OfType().FirstOrDefault() is { } routeAttribute) { - if (parameter.Name is null) + var routeName = routeAttribute.Name ?? parameter.Name; + factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.RouteAttribute); + if (factoryContext.RouteParameters is { } routeParams && !routeParams.Contains(routeName, StringComparer.OrdinalIgnoreCase)) { - throw new InvalidOperationException($"Encountered a parameter of type '{parameter.ParameterType}' without a name. Parameters must have a name."); + throw new InvalidOperationException($"'{routeName}' is not a route parameter."); } - var parameterCustomAttributes = parameter.GetCustomAttributes(); - - if (parameterCustomAttributes.OfType().FirstOrDefault() is { } routeAttribute) + return BindParameterFromProperty(parameter, RouteValuesExpr, routeName, factoryContext, "route"); + } + else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } queryAttribute) + { + factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.QueryAttribute); + return BindParameterFromProperty(parameter, QueryExpr, queryAttribute.Name ?? parameter.Name, factoryContext, "query string"); + } + else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } headerAttribute) + { + factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.HeaderAttribute); + return BindParameterFromProperty(parameter, HeadersExpr, headerAttribute.Name ?? parameter.Name, factoryContext, "header"); + } + else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } bodyAttribute) + { + factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.BodyAttribute); + return BindParameterFromBody(parameter, bodyAttribute.AllowEmpty, factoryContext); + } + else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType))) + { + factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.ServiceAttribute); + return BindParameterFromService(parameter, factoryContext); + } + else if (parameter.ParameterType == typeof(HttpContext)) + { + return HttpContextExpr; + } + else if (parameter.ParameterType == typeof(HttpRequest)) + { + return HttpRequestExpr; + } + else if (parameter.ParameterType == typeof(HttpResponse)) + { + return HttpResponseExpr; + } + else if (parameter.ParameterType == typeof(ClaimsPrincipal)) + { + return UserExpr; + } + else if (parameter.ParameterType == typeof(CancellationToken)) + { + return RequestAbortedExpr; + } + else if (ParameterBindingMethodCache.HasBindAsyncMethod(parameter)) + { + return BindParameterFromBindAsync(parameter, factoryContext); + } + else if (parameter.ParameterType == typeof(string) || ParameterBindingMethodCache.HasTryParseMethod(parameter)) + { + // 1. We bind from route values only, if route parameters are non-null and the parameter name is in that set. + // 2. We bind from query only, if route parameters are non-null and the parameter name is NOT in that set. + // 3. Otherwise, we fallback to route or query if route parameters is null (it means we don't know what route parameters are defined). This case only happens + // when RDF.Create is manually invoked. + if (factoryContext.RouteParameters is { } routeParams) { - var routeName = routeAttribute.Name ?? parameter.Name; - factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.RouteAttribute); - if (factoryContext.RouteParameters is { } routeParams && !routeParams.Contains(routeName, StringComparer.OrdinalIgnoreCase)) + if (routeParams.Contains(parameter.Name, StringComparer.OrdinalIgnoreCase)) { - throw new InvalidOperationException($"'{routeName}' is not a route parameter."); + // We're in the fallback case and we have a parameter and route parameter match so don't fallback + // to query string in this case + factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.RouteParameter); + return BindParameterFromProperty(parameter, RouteValuesExpr, parameter.Name, factoryContext, "route"); } - - return BindParameterFromProperty(parameter, RouteValuesExpr, routeName, factoryContext, "route"); - } - else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } queryAttribute) - { - factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.QueryAttribute); - return BindParameterFromProperty(parameter, QueryExpr, queryAttribute.Name ?? parameter.Name, factoryContext, "query string"); - } - else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } headerAttribute) - { - factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.HeaderAttribute); - return BindParameterFromProperty(parameter, HeadersExpr, headerAttribute.Name ?? parameter.Name, factoryContext, "header"); - } - else if (parameterCustomAttributes.OfType().FirstOrDefault() is { } bodyAttribute) - { - factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.BodyAttribute); - return BindParameterFromBody(parameter, bodyAttribute.AllowEmpty, factoryContext); - } - else if (parameter.CustomAttributes.Any(a => typeof(IFromServiceMetadata).IsAssignableFrom(a.AttributeType))) - { - factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.ServiceAttribute); - return BindParameterFromService(parameter, factoryContext); - } - else if (parameter.ParameterType == typeof(HttpContext)) - { - return HttpContextExpr; - } - else if (parameter.ParameterType == typeof(HttpRequest)) - { - return HttpRequestExpr; - } - else if (parameter.ParameterType == typeof(HttpResponse)) - { - return HttpResponseExpr; - } - else if (parameter.ParameterType == typeof(ClaimsPrincipal)) - { - return UserExpr; - } - else if (parameter.ParameterType == typeof(CancellationToken)) - { - return RequestAbortedExpr; - } - else if (ParameterBindingMethodCache.HasBindAsyncMethod(parameter)) - { - return BindParameterFromBindAsync(parameter, factoryContext); - } - else if (parameter.ParameterType == typeof(string) || ParameterBindingMethodCache.HasTryParseMethod(parameter)) - { - // 1. We bind from route values only, if route parameters are non-null and the parameter name is in that set. - // 2. We bind from query only, if route parameters are non-null and the parameter name is NOT in that set. - // 3. Otherwise, we fallback to route or query if route parameters is null (it means we don't know what route parameters are defined). This case only happens - // when RDF.Create is manually invoked. - if (factoryContext.RouteParameters is { } routeParams) + else { - if (routeParams.Contains(parameter.Name, StringComparer.OrdinalIgnoreCase)) - { - // We're in the fallback case and we have a parameter and route parameter match so don't fallback - // to query string in this case - factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.RouteParameter); - return BindParameterFromProperty(parameter, RouteValuesExpr, parameter.Name, factoryContext, "route"); - } - else - { - factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.QueryStringParameter); - return BindParameterFromProperty(parameter, QueryExpr, parameter.Name, factoryContext, "query string"); - } + factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.QueryStringParameter); + return BindParameterFromProperty(parameter, QueryExpr, parameter.Name, factoryContext, "query string"); } - - factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.RouteOrQueryStringParameter); - return BindParameterFromRouteValueOrQueryString(parameter, parameter.Name, factoryContext); } - else + + factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.RouteOrQueryStringParameter); + return BindParameterFromRouteValueOrQueryString(parameter, parameter.Name, factoryContext); + } + else + { + if (factoryContext.ServiceProviderIsService is IServiceProviderIsService serviceProviderIsService) { - if (factoryContext.ServiceProviderIsService is IServiceProviderIsService serviceProviderIsService) + if (serviceProviderIsService.IsService(parameter.ParameterType)) { - if (serviceProviderIsService.IsService(parameter.ParameterType)) - { - factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.ServiceParameter); - return Expression.Call(GetRequiredServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr); - } + factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.ServiceParameter); + return Expression.Call(GetRequiredServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr); } - - factoryContext.HasInferredBody = true; - factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.BodyParameter); - return BindParameterFromBody(parameter, allowEmpty: false, factoryContext); } + + factoryContext.HasInferredBody = true; + factoryContext.TrackedParameters.Add(parameter.Name, RequestDelegateFactoryConstants.BodyParameter); + return BindParameterFromBody(parameter, allowEmpty: false, factoryContext); } + } + + private static Expression CreateMethodCall(MethodInfo methodInfo, Expression? target, Expression[] arguments) => + target is null ? + Expression.Call(methodInfo, arguments) : + Expression.Call(target, methodInfo, arguments); - private static Expression CreateMethodCall(MethodInfo methodInfo, Expression? target, Expression[] arguments) => - target is null ? - Expression.Call(methodInfo, arguments) : - Expression.Call(target, methodInfo, arguments); + private static Expression CreateResponseWritingMethodCall(MethodInfo methodInfo, Expression? target, Expression[] arguments) + { + var callMethod = CreateMethodCall(methodInfo, target, arguments); + return AddResponseWritingToMethodCall(callMethod, methodInfo.ReturnType); + } - private static Expression CreateResponseWritingMethodCall(MethodInfo methodInfo, Expression? target, Expression[] arguments) + // If we're calling TryParse or validating parameter optionality and + // wasParamCheckFailure indicates it failed, set a 400 StatusCode instead of calling the method. + private static Expression CreateParamCheckingResponseWritingMethodCall( + MethodInfo methodInfo, Expression? target, Expression[] arguments, FactoryContext factoryContext) + { + // { + // string tempSourceString; + // bool wasParamCheckFailure = false; + // + // // Assume "int param1" is the first parameter, "[FromRoute] int? param2 = 42" is the second parameter ... + // int param1_local; + // int? param2_local; + // // ... + // + // tempSourceString = httpContext.RouteValue["param1"] ?? httpContext.Query["param1"]; + // + // if (tempSourceString != null) + // { + // if (!int.TryParse(tempSourceString, out param1_local)) + // { + // wasParamCheckFailure = true; + // Log.ParameterBindingFailed(httpContext, "Int32", "id", tempSourceString) + // } + // } + // + // tempSourceString = httpContext.RouteValue["param2"]; + // // ... + // + // return wasParamCheckFailure ? + // { + // httpContext.Response.StatusCode = 400; + // return Task.CompletedTask; + // } : + // { + // // Logic generated by AddResponseWritingToMethodCall() that calls handler(param1_local, param2_local, ...) + // }; + // } + + var localVariables = new ParameterExpression[factoryContext.ExtraLocals.Count + 1]; + var checkParamAndCallMethod = new Expression[factoryContext.ParamCheckExpressions.Count + 1]; + + for (var i = 0; i < factoryContext.ExtraLocals.Count; i++) { - var callMethod = CreateMethodCall(methodInfo, target, arguments); - return AddResponseWritingToMethodCall(callMethod, methodInfo.ReturnType); + localVariables[i] = factoryContext.ExtraLocals[i]; } - // If we're calling TryParse or validating parameter optionality and - // wasParamCheckFailure indicates it failed, set a 400 StatusCode instead of calling the method. - private static Expression CreateParamCheckingResponseWritingMethodCall( - MethodInfo methodInfo, Expression? target, Expression[] arguments, FactoryContext factoryContext) + for (var i = 0; i < factoryContext.ParamCheckExpressions.Count; i++) { - // { - // string tempSourceString; - // bool wasParamCheckFailure = false; - // - // // Assume "int param1" is the first parameter, "[FromRoute] int? param2 = 42" is the second parameter ... - // int param1_local; - // int? param2_local; - // // ... - // - // tempSourceString = httpContext.RouteValue["param1"] ?? httpContext.Query["param1"]; - // - // if (tempSourceString != null) - // { - // if (!int.TryParse(tempSourceString, out param1_local)) - // { - // wasParamCheckFailure = true; - // Log.ParameterBindingFailed(httpContext, "Int32", "id", tempSourceString) - // } - // } - // - // tempSourceString = httpContext.RouteValue["param2"]; - // // ... - // - // return wasParamCheckFailure ? - // { - // httpContext.Response.StatusCode = 400; - // return Task.CompletedTask; - // } : - // { - // // Logic generated by AddResponseWritingToMethodCall() that calls handler(param1_local, param2_local, ...) - // }; - // } - - var localVariables = new ParameterExpression[factoryContext.ExtraLocals.Count + 1]; - var checkParamAndCallMethod = new Expression[factoryContext.ParamCheckExpressions.Count + 1]; - - for (var i = 0; i < factoryContext.ExtraLocals.Count; i++) - { - localVariables[i] = factoryContext.ExtraLocals[i]; - } + checkParamAndCallMethod[i] = factoryContext.ParamCheckExpressions[i]; + } - for (var i = 0; i < factoryContext.ParamCheckExpressions.Count; i++) - { - checkParamAndCallMethod[i] = factoryContext.ParamCheckExpressions[i]; - } + localVariables[factoryContext.ExtraLocals.Count] = WasParamCheckFailureExpr; - localVariables[factoryContext.ExtraLocals.Count] = WasParamCheckFailureExpr; + var set400StatusAndReturnCompletedTask = Expression.Block( + Expression.Assign(StatusCodeExpr, Expression.Constant(400)), + CompletedTaskExpr); - var set400StatusAndReturnCompletedTask = Expression.Block( - Expression.Assign(StatusCodeExpr, Expression.Constant(400)), - CompletedTaskExpr); + var methodCall = CreateMethodCall(methodInfo, target, arguments); - var methodCall = CreateMethodCall(methodInfo, target, arguments); + var checkWasParamCheckFailure = Expression.Condition(WasParamCheckFailureExpr, + set400StatusAndReturnCompletedTask, + AddResponseWritingToMethodCall(methodCall, methodInfo.ReturnType)); - var checkWasParamCheckFailure = Expression.Condition(WasParamCheckFailureExpr, - set400StatusAndReturnCompletedTask, - AddResponseWritingToMethodCall(methodCall, methodInfo.ReturnType)); + checkParamAndCallMethod[factoryContext.ParamCheckExpressions.Count] = checkWasParamCheckFailure; - checkParamAndCallMethod[factoryContext.ParamCheckExpressions.Count] = checkWasParamCheckFailure; + return Expression.Block(localVariables, checkParamAndCallMethod); + } - return Expression.Block(localVariables, checkParamAndCallMethod); + private static Expression AddResponseWritingToMethodCall(Expression methodCall, Type returnType) + { + // Exact request delegate match + if (returnType == typeof(void)) + { + return Expression.Block(methodCall, CompletedTaskExpr); } - - private static Expression AddResponseWritingToMethodCall(Expression methodCall, Type returnType) + else if (returnType == typeof(object)) { - // Exact request delegate match - if (returnType == typeof(void)) - { - return Expression.Block(methodCall, CompletedTaskExpr); - } - else if (returnType == typeof(object)) - { - return Expression.Call(ExecuteObjectReturnMethod, methodCall, HttpContextExpr); - } - else if (returnType == typeof(ValueTask)) + return Expression.Call(ExecuteObjectReturnMethod, methodCall, HttpContextExpr); + } + else if (returnType == typeof(ValueTask)) + { + // REVIEW: We can avoid this box if it becomes a performance issue + var box = Expression.TypeAs(methodCall, typeof(object)); + return Expression.Call(ExecuteObjectReturnMethod, box, HttpContextExpr); + } + else if (returnType == typeof(Task)) + { + var convert = Expression.Convert(methodCall, typeof(object)); + return Expression.Call(ExecuteObjectReturnMethod, convert, HttpContextExpr); + } + else if (AwaitableInfo.IsTypeAwaitable(returnType, out _)) + { + if (returnType == typeof(Task)) { - // REVIEW: We can avoid this box if it becomes a performance issue - var box = Expression.TypeAs(methodCall, typeof(object)); - return Expression.Call(ExecuteObjectReturnMethod, box, HttpContextExpr); + return methodCall; } - else if (returnType == typeof(Task)) + else if (returnType == typeof(ValueTask)) { - var convert = Expression.Convert(methodCall, typeof(object)); - return Expression.Call(ExecuteObjectReturnMethod, convert, HttpContextExpr); + return Expression.Call( + ExecuteValueTaskMethod, + methodCall); } - else if (AwaitableInfo.IsTypeAwaitable(returnType, out _)) + else if (returnType.IsGenericType && + returnType.GetGenericTypeDefinition() == typeof(Task<>)) { - if (returnType == typeof(Task)) + var typeArg = returnType.GetGenericArguments()[0]; + + if (typeof(IResult).IsAssignableFrom(typeArg)) { - return methodCall; + return Expression.Call( + ExecuteTaskResultOfTMethod.MakeGenericMethod(typeArg), + methodCall, + HttpContextExpr); } - else if (returnType == typeof(ValueTask)) + // ExecuteTask(handler(..), httpContext); + else if (typeArg == typeof(string)) { return Expression.Call( - ExecuteValueTaskMethod, - methodCall); + ExecuteTaskOfStringMethod, + methodCall, + HttpContextExpr); } - else if (returnType.IsGenericType && - returnType.GetGenericTypeDefinition() == typeof(Task<>)) + else { - var typeArg = returnType.GetGenericArguments()[0]; - - if (typeof(IResult).IsAssignableFrom(typeArg)) - { - return Expression.Call( - ExecuteTaskResultOfTMethod.MakeGenericMethod(typeArg), - methodCall, - HttpContextExpr); - } - // ExecuteTask(handler(..), httpContext); - else if (typeArg == typeof(string)) - { - return Expression.Call( - ExecuteTaskOfStringMethod, - methodCall, - HttpContextExpr); - } - else - { - return Expression.Call( - ExecuteTaskOfTMethod.MakeGenericMethod(typeArg), - methodCall, - HttpContextExpr); - } + return Expression.Call( + ExecuteTaskOfTMethod.MakeGenericMethod(typeArg), + methodCall, + HttpContextExpr); } - else if (returnType.IsGenericType && - returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) - { - var typeArg = returnType.GetGenericArguments()[0]; + } + else if (returnType.IsGenericType && + returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) + { + var typeArg = returnType.GetGenericArguments()[0]; - if (typeof(IResult).IsAssignableFrom(typeArg)) - { - return Expression.Call( - ExecuteValueResultTaskOfTMethod.MakeGenericMethod(typeArg), - methodCall, - HttpContextExpr); - } - // ExecuteTask(handler(..), httpContext); - else if (typeArg == typeof(string)) - { - return Expression.Call( - ExecuteValueTaskOfStringMethod, - methodCall, - HttpContextExpr); - } - else - { - return Expression.Call( - ExecuteValueTaskOfTMethod.MakeGenericMethod(typeArg), - methodCall, - HttpContextExpr); - } + if (typeof(IResult).IsAssignableFrom(typeArg)) + { + return Expression.Call( + ExecuteValueResultTaskOfTMethod.MakeGenericMethod(typeArg), + methodCall, + HttpContextExpr); } - else + // ExecuteTask(handler(..), httpContext); + else if (typeArg == typeof(string)) { - // TODO: Handle custom awaitables - throw new NotSupportedException($"Unsupported return type: {returnType}"); + return Expression.Call( + ExecuteValueTaskOfStringMethod, + methodCall, + HttpContextExpr); } - } - else if (typeof(IResult).IsAssignableFrom(returnType)) - { - if (returnType.IsValueType) + else { - var box = Expression.TypeAs(methodCall, typeof(IResult)); - return Expression.Call(ResultWriteResponseAsyncMethod, box, HttpContextExpr); + return Expression.Call( + ExecuteValueTaskOfTMethod.MakeGenericMethod(typeArg), + methodCall, + HttpContextExpr); } - return Expression.Call(ResultWriteResponseAsyncMethod, methodCall, HttpContextExpr); - } - else if (returnType == typeof(string)) - { - return Expression.Call(StringResultWriteResponseAsyncMethod, HttpContextExpr, methodCall); - } - else if (returnType.IsValueType) - { - var box = Expression.TypeAs(methodCall, typeof(object)); - return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, box, Expression.Constant(CancellationToken.None)); } else { - return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None)); + // TODO: Handle custom awaitables + throw new NotSupportedException($"Unsupported return type: {returnType}"); } } - - private static Func HandleRequestBodyAndCompileRequestDelegate(Expression responseWritingMethodCall, FactoryContext factoryContext) + else if (typeof(IResult).IsAssignableFrom(returnType)) { - if (factoryContext.JsonRequestBodyParameter is null) + if (returnType.IsValueType) { - if (factoryContext.ParameterBinders.Count > 0) - { - // We need to generate the code for reading from the custom binders calling into the delegate - var continuation = Expression.Lambda>( - responseWritingMethodCall, TargetExpr, HttpContextExpr, BoundValuesArrayExpr).Compile(); - - // Looping over arrays is faster - var binders = factoryContext.ParameterBinders.ToArray(); - var count = binders.Length; - - return async (target, httpContext) => - { - var boundValues = new object?[count]; - - for (var i = 0; i < count; i++) - { - boundValues[i] = await binders[i](httpContext); - } - - await continuation(target, httpContext, boundValues); - }; - } - - return Expression.Lambda>( - responseWritingMethodCall, TargetExpr, HttpContextExpr).Compile(); - } - - var bodyType = factoryContext.JsonRequestBodyParameter.ParameterType; - var parameterTypeName = TypeNameHelper.GetTypeDisplayName(factoryContext.JsonRequestBodyParameter.ParameterType, fullName: false); - var parameterName = factoryContext.JsonRequestBodyParameter.Name; - - Debug.Assert(parameterName is not null, "CreateArgument() should throw if parameter.Name is null."); - - object? defaultBodyValue = null; - - if (factoryContext.AllowEmptyRequestBody && bodyType.IsValueType) - { - defaultBodyValue = Activator.CreateInstance(bodyType); + var box = Expression.TypeAs(methodCall, typeof(IResult)); + return Expression.Call(ResultWriteResponseAsyncMethod, box, HttpContextExpr); } + return Expression.Call(ResultWriteResponseAsyncMethod, methodCall, HttpContextExpr); + } + else if (returnType == typeof(string)) + { + return Expression.Call(StringResultWriteResponseAsyncMethod, HttpContextExpr, methodCall); + } + else if (returnType.IsValueType) + { + var box = Expression.TypeAs(methodCall, typeof(object)); + return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, box, Expression.Constant(CancellationToken.None)); + } + else + { + return Expression.Call(JsonResultWriteResponseAsyncMethod, HttpResponseExpr, methodCall, Expression.Constant(CancellationToken.None)); + } + } + private static Func HandleRequestBodyAndCompileRequestDelegate(Expression responseWritingMethodCall, FactoryContext factoryContext) + { + if (factoryContext.JsonRequestBodyParameter is null) + { if (factoryContext.ParameterBinders.Count > 0) { - // We need to generate the code for reading from the body before calling into the delegate - var continuation = Expression.Lambda>( - responseWritingMethodCall, TargetExpr, HttpContextExpr, BodyValueExpr, BoundValuesArrayExpr).Compile(); + // We need to generate the code for reading from the custom binders calling into the delegate + var continuation = Expression.Lambda>( + responseWritingMethodCall, TargetExpr, HttpContextExpr, BoundValuesArrayExpr).Compile(); // Looping over arrays is faster var binders = factoryContext.ParameterBinders.ToArray(); @@ -565,7 +525,6 @@ namespace Microsoft.AspNetCore.Http return async (target, httpContext) => { - // Run these first so that they can potentially read and rewind the body var boundValues = new object?[count]; for (var i = 0; i < count; i++) @@ -573,812 +532,852 @@ namespace Microsoft.AspNetCore.Http boundValues[i] = await binders[i](httpContext); } - var bodyValue = defaultBodyValue; - var feature = httpContext.Features.Get(); - if (feature?.CanHaveBody == true) - { - if (!httpContext.Request.HasJsonContentType()) - { - Log.UnexpectedContentType(httpContext, httpContext.Request.ContentType, factoryContext.ThrowOnBadRequest); - httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType; - return; - } - try - { - bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType); - } - catch (IOException ex) - { - Log.RequestBodyIOException(httpContext, ex); - return; - } - catch (JsonException ex) - { - Log.InvalidJsonRequestBody(httpContext, parameterTypeName, parameterName, ex, factoryContext.ThrowOnBadRequest); - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return; - } - } - - await continuation(target, httpContext, bodyValue, boundValues); + await continuation(target, httpContext, boundValues); }; } - else + + return Expression.Lambda>( + responseWritingMethodCall, TargetExpr, HttpContextExpr).Compile(); + } + + var bodyType = factoryContext.JsonRequestBodyParameter.ParameterType; + var parameterTypeName = TypeNameHelper.GetTypeDisplayName(factoryContext.JsonRequestBodyParameter.ParameterType, fullName: false); + var parameterName = factoryContext.JsonRequestBodyParameter.Name; + + Debug.Assert(parameterName is not null, "CreateArgument() should throw if parameter.Name is null."); + + object? defaultBodyValue = null; + + if (factoryContext.AllowEmptyRequestBody && bodyType.IsValueType) + { + defaultBodyValue = Activator.CreateInstance(bodyType); + } + + if (factoryContext.ParameterBinders.Count > 0) + { + // We need to generate the code for reading from the body before calling into the delegate + var continuation = Expression.Lambda>( + responseWritingMethodCall, TargetExpr, HttpContextExpr, BodyValueExpr, BoundValuesArrayExpr).Compile(); + + // Looping over arrays is faster + var binders = factoryContext.ParameterBinders.ToArray(); + var count = binders.Length; + + return async (target, httpContext) => { - // We need to generate the code for reading from the body before calling into the delegate - var continuation = Expression.Lambda>( - responseWritingMethodCall, TargetExpr, HttpContextExpr, BodyValueExpr).Compile(); + // Run these first so that they can potentially read and rewind the body + var boundValues = new object?[count]; - return async (target, httpContext) => + for (var i = 0; i < count; i++) + { + boundValues[i] = await binders[i](httpContext); + } + + var bodyValue = defaultBodyValue; + var feature = httpContext.Features.Get(); + if (feature?.CanHaveBody == true) { - var bodyValue = defaultBodyValue; - var feature = httpContext.Features.Get(); - if (feature?.CanHaveBody == true) + if (!httpContext.Request.HasJsonContentType()) { - if (!httpContext.Request.HasJsonContentType()) - { - Log.UnexpectedContentType(httpContext, httpContext.Request.ContentType, factoryContext.ThrowOnBadRequest); - httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType; - return; - } - try - { - bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType); - } - catch (IOException ex) - { - Log.RequestBodyIOException(httpContext, ex); - return; - } - catch (JsonException ex) - { - - Log.InvalidJsonRequestBody(httpContext, parameterTypeName, parameterName, ex, factoryContext.ThrowOnBadRequest); - httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; - return; - } + Log.UnexpectedContentType(httpContext, httpContext.Request.ContentType, factoryContext.ThrowOnBadRequest); + httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType; + return; } - await continuation(target, httpContext, bodyValue); - }; - } - } + try + { + bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType); + } + catch (IOException ex) + { + Log.RequestBodyIOException(httpContext, ex); + return; + } + catch (JsonException ex) + { + Log.InvalidJsonRequestBody(httpContext, parameterTypeName, parameterName, ex, factoryContext.ThrowOnBadRequest); + httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + } - private static Expression GetValueFromProperty(Expression sourceExpression, string key) - { - var itemProperty = sourceExpression.Type.GetProperty("Item"); - var indexArguments = new[] { Expression.Constant(key) }; - var indexExpression = Expression.MakeIndex(sourceExpression, itemProperty, indexArguments); - return Expression.Convert(indexExpression, typeof(string)); + await continuation(target, httpContext, bodyValue, boundValues); + }; } - - private static Expression BindParameterFromService(ParameterInfo parameter, FactoryContext factoryContext) + else { - var isOptional = IsOptionalParameter(parameter, factoryContext); + // We need to generate the code for reading from the body before calling into the delegate + var continuation = Expression.Lambda>( + responseWritingMethodCall, TargetExpr, HttpContextExpr, BodyValueExpr).Compile(); - if (isOptional) + return async (target, httpContext) => { - return Expression.Call(GetServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr); - } - return Expression.Call(GetRequiredServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr); + var bodyValue = defaultBodyValue; + var feature = httpContext.Features.Get(); + if (feature?.CanHaveBody == true) + { + if (!httpContext.Request.HasJsonContentType()) + { + Log.UnexpectedContentType(httpContext, httpContext.Request.ContentType, factoryContext.ThrowOnBadRequest); + httpContext.Response.StatusCode = StatusCodes.Status415UnsupportedMediaType; + return; + } + try + { + bodyValue = await httpContext.Request.ReadFromJsonAsync(bodyType); + } + catch (IOException ex) + { + Log.RequestBodyIOException(httpContext, ex); + return; + } + catch (JsonException ex) + { + + Log.InvalidJsonRequestBody(httpContext, parameterTypeName, parameterName, ex, factoryContext.ThrowOnBadRequest); + httpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + } + await continuation(target, httpContext, bodyValue); + }; } + } - private static Expression BindParameterFromValue(ParameterInfo parameter, Expression valueExpression, FactoryContext factoryContext, string source) + private static Expression GetValueFromProperty(Expression sourceExpression, string key) + { + var itemProperty = sourceExpression.Type.GetProperty("Item"); + var indexArguments = new[] { Expression.Constant(key) }; + var indexExpression = Expression.MakeIndex(sourceExpression, itemProperty, indexArguments); + return Expression.Convert(indexExpression, typeof(string)); + } + + private static Expression BindParameterFromService(ParameterInfo parameter, FactoryContext factoryContext) + { + var isOptional = IsOptionalParameter(parameter, factoryContext); + + if (isOptional) { - var isOptional = IsOptionalParameter(parameter, factoryContext); + return Expression.Call(GetServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr); + } + return Expression.Call(GetRequiredServiceMethod.MakeGenericMethod(parameter.ParameterType), RequestServicesExpr); + } + + private static Expression BindParameterFromValue(ParameterInfo parameter, Expression valueExpression, FactoryContext factoryContext, string source) + { + var isOptional = IsOptionalParameter(parameter, factoryContext); - var argument = Expression.Variable(parameter.ParameterType, $"{parameter.Name}_local"); + var argument = Expression.Variable(parameter.ParameterType, $"{parameter.Name}_local"); - var parameterTypeNameConstant = Expression.Constant(TypeNameHelper.GetTypeDisplayName(parameter.ParameterType, fullName: false)); - var parameterNameConstant = Expression.Constant(parameter.Name); - var sourceConstant = Expression.Constant(source); + var parameterTypeNameConstant = Expression.Constant(TypeNameHelper.GetTypeDisplayName(parameter.ParameterType, fullName: false)); + var parameterNameConstant = Expression.Constant(parameter.Name); + var sourceConstant = Expression.Constant(source); - if (parameter.ParameterType == typeof(string)) + if (parameter.ParameterType == typeof(string)) + { + if (!isOptional) { - if (!isOptional) - { - // The following is produced if the parameter is required: - // - // tempSourceString = httpContext.RouteValue["param1"] ?? httpContext.Query["param1"]; - // if (tempSourceString == null) - // { - // wasParamCheckFailure = true; - // Log.RequiredParameterNotProvided(httpContext, "Int32", "param1"); - // } - var checkRequiredStringParameterBlock = Expression.Block( - Expression.Assign(argument, valueExpression), - Expression.IfThen(Expression.Equal(argument, Expression.Constant(null)), - Expression.Block( - Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), - Expression.Call(LogRequiredParameterNotProvidedMethod, - HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, sourceConstant, - Expression.Constant(factoryContext.ThrowOnBadRequest)) - ) + // The following is produced if the parameter is required: + // + // tempSourceString = httpContext.RouteValue["param1"] ?? httpContext.Query["param1"]; + // if (tempSourceString == null) + // { + // wasParamCheckFailure = true; + // Log.RequiredParameterNotProvided(httpContext, "Int32", "param1"); + // } + var checkRequiredStringParameterBlock = Expression.Block( + Expression.Assign(argument, valueExpression), + Expression.IfThen(Expression.Equal(argument, Expression.Constant(null)), + Expression.Block( + Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), + Expression.Call(LogRequiredParameterNotProvidedMethod, + HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, sourceConstant, + Expression.Constant(factoryContext.ThrowOnBadRequest)) ) - ); - - factoryContext.ExtraLocals.Add(argument); - factoryContext.ParamCheckExpressions.Add(checkRequiredStringParameterBlock); - return argument; - } - - // Allow nullable parameters that don't have a default value - var nullability = factoryContext.NullabilityContext.Create(parameter); - if (nullability.ReadState != NullabilityState.NotNull && !parameter.HasDefaultValue) - { - return valueExpression; - } + ) + ); - // The following is produced if the parameter is optional. Note that we convert the - // default value to the target ParameterType to address scenarios where the user is - // is setting null as the default value in a context where nullability is disabled. - // - // param1_local = httpContext.RouteValue["param1"] ?? httpContext.Query["param1"]; - // param1_local != null ? param1_local : Convert(null, Int32) - return Expression.Block( - Expression.Condition(Expression.NotEqual(valueExpression, Expression.Constant(null)), - valueExpression, - Expression.Convert(Expression.Constant(parameter.DefaultValue), parameter.ParameterType))); + factoryContext.ExtraLocals.Add(argument); + factoryContext.ParamCheckExpressions.Add(checkRequiredStringParameterBlock); + return argument; } - factoryContext.UsingTempSourceString = true; - - var underlyingNullableType = Nullable.GetUnderlyingType(parameter.ParameterType); - var isNotNullable = underlyingNullableType is null; - - var nonNullableParameterType = underlyingNullableType ?? parameter.ParameterType; - var tryParseMethodCall = ParameterBindingMethodCache.FindTryParseMethod(nonNullableParameterType); - - if (tryParseMethodCall is null) + // Allow nullable parameters that don't have a default value + var nullability = factoryContext.NullabilityContext.Create(parameter); + if (nullability.ReadState != NullabilityState.NotNull && !parameter.HasDefaultValue) { - var typeName = TypeNameHelper.GetTypeDisplayName(parameter.ParameterType, fullName: false); - throw new InvalidOperationException($"No public static bool {typeName}.TryParse(string, out {typeName}) method found for {parameter.Name}."); + return valueExpression; } - // string tempSourceString; - // bool wasParamCheckFailure = false; - // - // // Assume "int param1" is the first parameter and "[FromRoute] int? param2 = 42" is the second parameter. - // int param1_local; - // int? param2_local; - // - // tempSourceString = httpContext.RouteValue["param1"] ?? httpContext.Query["param1"]; + // The following is produced if the parameter is optional. Note that we convert the + // default value to the target ParameterType to address scenarios where the user is + // is setting null as the default value in a context where nullability is disabled. // - // if (tempSourceString != null) - // { - // if (!int.TryParse(tempSourceString, out param1_local)) - // { - // wasParamCheckFailure = true; - // Log.ParameterBindingFailed(httpContext, "Int32", "id", tempSourceString) - // } - // } - // - // tempSourceString = httpContext.RouteValue["param2"]; - // - // if (tempSourceString != null) - // { - // if (int.TryParse(tempSourceString, out int parsedValue)) - // { - // param2_local = parsedValue; - // } - // else - // { - // wasParamCheckFailure = true; - // Log.ParameterBindingFailed(httpContext, "Int32", "id", tempSourceString) - // } - // } - // else - // { - // param2_local = 42; - // } - - // If the parameter is nullable, create a "parsedValue" local to TryParse into since we cannot use the parameter directly. - var parsedValue = isNotNullable ? argument : Expression.Variable(nonNullableParameterType, "parsedValue"); - - var failBlock = Expression.Block( - Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), - Expression.Call(LogParameterBindingFailedMethod, - HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, - TempSourceStringExpr, Expression.Constant(factoryContext.ThrowOnBadRequest))); - - var tryParseCall = tryParseMethodCall(parsedValue); - - // The following code is generated if the parameter is required and - // the method should not be matched. - // - // if (tempSourceString == null) - // { - // wasParamCheckFailure = true; - // Log.RequiredParameterNotProvided(httpContext, "Int32", "param1"); - // } - var checkRequiredParaseableParameterBlock = Expression.Block( - Expression.IfThen(TempSourceStringNullExpr, - Expression.Block( - Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), - Expression.Call(LogRequiredParameterNotProvidedMethod, - HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, sourceConstant, - Expression.Constant(factoryContext.ThrowOnBadRequest)) - ) - ) - ); - - // If the parameter is nullable, we need to assign the "parsedValue" local to the nullable parameter on success. - Expression tryParseExpression = isNotNullable ? - Expression.IfThen(Expression.Not(tryParseCall), failBlock) : - Expression.Block(new[] { parsedValue }, - Expression.IfThenElse(tryParseCall, - Expression.Assign(argument, Expression.Convert(parsedValue, parameter.ParameterType)), - failBlock)); + // param1_local = httpContext.RouteValue["param1"] ?? httpContext.Query["param1"]; + // param1_local != null ? param1_local : Convert(null, Int32) + return Expression.Block( + Expression.Condition(Expression.NotEqual(valueExpression, Expression.Constant(null)), + valueExpression, + Expression.Convert(Expression.Constant(parameter.DefaultValue), parameter.ParameterType))); + } - var ifNotNullTryParse = !parameter.HasDefaultValue ? - Expression.IfThen(TempSourceStringNotNullExpr, tryParseExpression) : - Expression.IfThenElse(TempSourceStringNotNullExpr, - tryParseExpression, - Expression.Assign(argument, Expression.Constant(parameter.DefaultValue))); + factoryContext.UsingTempSourceString = true; - var fullParamCheckBlock = !isOptional - ? Expression.Block( - // tempSourceString = httpContext.RequestValue["id"]; - Expression.Assign(TempSourceStringExpr, valueExpression), - // if (tempSourceString == null) { ... } only produced when parameter is required - checkRequiredParaseableParameterBlock, - // if (tempSourceString != null) { ... } - ifNotNullTryParse) - : Expression.Block( - // tempSourceString = httpContext.RequestValue["id"]; - Expression.Assign(TempSourceStringExpr, valueExpression), - // if (tempSourceString != null) { ... } - ifNotNullTryParse); + var underlyingNullableType = Nullable.GetUnderlyingType(parameter.ParameterType); + var isNotNullable = underlyingNullableType is null; - factoryContext.ExtraLocals.Add(argument); - factoryContext.ParamCheckExpressions.Add(fullParamCheckBlock); + var nonNullableParameterType = underlyingNullableType ?? parameter.ParameterType; + var tryParseMethodCall = ParameterBindingMethodCache.FindTryParseMethod(nonNullableParameterType); - return argument; + if (tryParseMethodCall is null) + { + var typeName = TypeNameHelper.GetTypeDisplayName(parameter.ParameterType, fullName: false); + throw new InvalidOperationException($"No public static bool {typeName}.TryParse(string, out {typeName}) method found for {parameter.Name}."); } - private static Expression BindParameterFromProperty(ParameterInfo parameter, MemberExpression property, string key, FactoryContext factoryContext, string source) => - BindParameterFromValue(parameter, GetValueFromProperty(property, key), factoryContext, source); + // string tempSourceString; + // bool wasParamCheckFailure = false; + // + // // Assume "int param1" is the first parameter and "[FromRoute] int? param2 = 42" is the second parameter. + // int param1_local; + // int? param2_local; + // + // tempSourceString = httpContext.RouteValue["param1"] ?? httpContext.Query["param1"]; + // + // if (tempSourceString != null) + // { + // if (!int.TryParse(tempSourceString, out param1_local)) + // { + // wasParamCheckFailure = true; + // Log.ParameterBindingFailed(httpContext, "Int32", "id", tempSourceString) + // } + // } + // + // tempSourceString = httpContext.RouteValue["param2"]; + // + // if (tempSourceString != null) + // { + // if (int.TryParse(tempSourceString, out int parsedValue)) + // { + // param2_local = parsedValue; + // } + // else + // { + // wasParamCheckFailure = true; + // Log.ParameterBindingFailed(httpContext, "Int32", "id", tempSourceString) + // } + // } + // else + // { + // param2_local = 42; + // } + + // If the parameter is nullable, create a "parsedValue" local to TryParse into since we cannot use the parameter directly. + var parsedValue = isNotNullable ? argument : Expression.Variable(nonNullableParameterType, "parsedValue"); + + var failBlock = Expression.Block( + Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), + Expression.Call(LogParameterBindingFailedMethod, + HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, + TempSourceStringExpr, Expression.Constant(factoryContext.ThrowOnBadRequest))); + + var tryParseCall = tryParseMethodCall(parsedValue); + + // The following code is generated if the parameter is required and + // the method should not be matched. + // + // if (tempSourceString == null) + // { + // wasParamCheckFailure = true; + // Log.RequiredParameterNotProvided(httpContext, "Int32", "param1"); + // } + var checkRequiredParaseableParameterBlock = Expression.Block( + Expression.IfThen(TempSourceStringNullExpr, + Expression.Block( + Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), + Expression.Call(LogRequiredParameterNotProvidedMethod, + HttpContextExpr, parameterTypeNameConstant, parameterNameConstant, sourceConstant, + Expression.Constant(factoryContext.ThrowOnBadRequest)) + ) + ) + ); + + // If the parameter is nullable, we need to assign the "parsedValue" local to the nullable parameter on success. + Expression tryParseExpression = isNotNullable ? + Expression.IfThen(Expression.Not(tryParseCall), failBlock) : + Expression.Block(new[] { parsedValue }, + Expression.IfThenElse(tryParseCall, + Expression.Assign(argument, Expression.Convert(parsedValue, parameter.ParameterType)), + failBlock)); + + var ifNotNullTryParse = !parameter.HasDefaultValue ? + Expression.IfThen(TempSourceStringNotNullExpr, tryParseExpression) : + Expression.IfThenElse(TempSourceStringNotNullExpr, + tryParseExpression, + Expression.Assign(argument, Expression.Constant(parameter.DefaultValue))); + + var fullParamCheckBlock = !isOptional + ? Expression.Block( + // tempSourceString = httpContext.RequestValue["id"]; + Expression.Assign(TempSourceStringExpr, valueExpression), + // if (tempSourceString == null) { ... } only produced when parameter is required + checkRequiredParaseableParameterBlock, + // if (tempSourceString != null) { ... } + ifNotNullTryParse) + : Expression.Block( + // tempSourceString = httpContext.RequestValue["id"]; + Expression.Assign(TempSourceStringExpr, valueExpression), + // if (tempSourceString != null) { ... } + ifNotNullTryParse); + + factoryContext.ExtraLocals.Add(argument); + factoryContext.ParamCheckExpressions.Add(fullParamCheckBlock); + + return argument; + } - private static Expression BindParameterFromRouteValueOrQueryString(ParameterInfo parameter, string key, FactoryContext factoryContext) - { - var routeValue = GetValueFromProperty(RouteValuesExpr, key); - var queryValue = GetValueFromProperty(QueryExpr, key); - return BindParameterFromValue(parameter, Expression.Coalesce(routeValue, queryValue), factoryContext, "route or query string"); - } + private static Expression BindParameterFromProperty(ParameterInfo parameter, MemberExpression property, string key, FactoryContext factoryContext, string source) => + BindParameterFromValue(parameter, GetValueFromProperty(property, key), factoryContext, source); - private static Expression BindParameterFromBindAsync(ParameterInfo parameter, FactoryContext factoryContext) - { - // We reference the boundValues array by parameter index here - var nullability = factoryContext.NullabilityContext.Create(parameter); - var isOptional = IsOptionalParameter(parameter, factoryContext); + private static Expression BindParameterFromRouteValueOrQueryString(ParameterInfo parameter, string key, FactoryContext factoryContext) + { + var routeValue = GetValueFromProperty(RouteValuesExpr, key); + var queryValue = GetValueFromProperty(QueryExpr, key); + return BindParameterFromValue(parameter, Expression.Coalesce(routeValue, queryValue), factoryContext, "route or query string"); + } - // Get the BindAsync method for the type. - var bindAsyncMethod = ParameterBindingMethodCache.FindBindAsyncMethod(parameter); - // We know BindAsync exists because there's no way to opt-in without defining the method on the type. - Debug.Assert(bindAsyncMethod.Expression is not null); + private static Expression BindParameterFromBindAsync(ParameterInfo parameter, FactoryContext factoryContext) + { + // We reference the boundValues array by parameter index here + var nullability = factoryContext.NullabilityContext.Create(parameter); + var isOptional = IsOptionalParameter(parameter, factoryContext); - // Compile the delegate to the BindAsync method for this parameter index - var bindAsyncDelegate = Expression.Lambda>>(bindAsyncMethod.Expression, HttpContextExpr).Compile(); - factoryContext.ParameterBinders.Add(bindAsyncDelegate); + // Get the BindAsync method for the type. + var bindAsyncMethod = ParameterBindingMethodCache.FindBindAsyncMethod(parameter); + // We know BindAsync exists because there's no way to opt-in without defining the method on the type. + Debug.Assert(bindAsyncMethod.Expression is not null); - // boundValues[index] - var boundValueExpr = Expression.ArrayIndex(BoundValuesArrayExpr, Expression.Constant(factoryContext.ParameterBinders.Count - 1)); + // Compile the delegate to the BindAsync method for this parameter index + var bindAsyncDelegate = Expression.Lambda>>(bindAsyncMethod.Expression, HttpContextExpr).Compile(); + factoryContext.ParameterBinders.Add(bindAsyncDelegate); - if (!isOptional) - { - var typeName = TypeNameHelper.GetTypeDisplayName(parameter.ParameterType, fullName: false); - var message = bindAsyncMethod.ParamCount == 2 ? $"{typeName}.BindAsync(HttpContext, ParameterInfo)" : $"{typeName}.BindAsync(HttpContext)"; - var checkRequiredBodyBlock = Expression.Block( - Expression.IfThen( - Expression.Equal(boundValueExpr, Expression.Constant(null)), - Expression.Block( - Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), - Expression.Call(LogRequiredParameterNotProvidedMethod, - HttpContextExpr, - Expression.Constant(typeName), - Expression.Constant(parameter.Name), - Expression.Constant(message), - Expression.Constant(factoryContext.ThrowOnBadRequest)) - ) - ) - ); + // boundValues[index] + var boundValueExpr = Expression.ArrayIndex(BoundValuesArrayExpr, Expression.Constant(factoryContext.ParameterBinders.Count - 1)); - factoryContext.ParamCheckExpressions.Add(checkRequiredBodyBlock); - } + if (!isOptional) + { + var typeName = TypeNameHelper.GetTypeDisplayName(parameter.ParameterType, fullName: false); + var message = bindAsyncMethod.ParamCount == 2 ? $"{typeName}.BindAsync(HttpContext, ParameterInfo)" : $"{typeName}.BindAsync(HttpContext)"; + var checkRequiredBodyBlock = Expression.Block( + Expression.IfThen( + Expression.Equal(boundValueExpr, Expression.Constant(null)), + Expression.Block( + Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), + Expression.Call(LogRequiredParameterNotProvidedMethod, + HttpContextExpr, + Expression.Constant(typeName), + Expression.Constant(parameter.Name), + Expression.Constant(message), + Expression.Constant(factoryContext.ThrowOnBadRequest)) + ) + ) + ); - // (ParameterType)boundValues[i] - return Expression.Convert(boundValueExpr, parameter.ParameterType); + factoryContext.ParamCheckExpressions.Add(checkRequiredBodyBlock); } - private static Expression BindParameterFromBody(ParameterInfo parameter, bool allowEmpty, FactoryContext factoryContext) + // (ParameterType)boundValues[i] + return Expression.Convert(boundValueExpr, parameter.ParameterType); + } + + private static Expression BindParameterFromBody(ParameterInfo parameter, bool allowEmpty, FactoryContext factoryContext) + { + if (factoryContext.JsonRequestBodyParameter is not null) { - if (factoryContext.JsonRequestBodyParameter is not null) - { - factoryContext.HasMultipleBodyParameters = true; - var parameterName = parameter.Name; + factoryContext.HasMultipleBodyParameters = true; + var parameterName = parameter.Name; - Debug.Assert(parameterName is not null, "CreateArgument() should throw if parameter.Name is null."); + Debug.Assert(parameterName is not null, "CreateArgument() should throw if parameter.Name is null."); - if (factoryContext.TrackedParameters.ContainsKey(parameterName)) - { - factoryContext.TrackedParameters.Remove(parameterName); - factoryContext.TrackedParameters.Add(parameterName, "UNKNOWN"); - } + if (factoryContext.TrackedParameters.ContainsKey(parameterName)) + { + factoryContext.TrackedParameters.Remove(parameterName); + factoryContext.TrackedParameters.Add(parameterName, "UNKNOWN"); } + } - var isOptional = IsOptionalParameter(parameter, factoryContext); + var isOptional = IsOptionalParameter(parameter, factoryContext); - factoryContext.JsonRequestBodyParameter = parameter; - factoryContext.AllowEmptyRequestBody = allowEmpty || isOptional; - factoryContext.Metadata.Add(new AcceptsMetadata(parameter.ParameterType, factoryContext.AllowEmptyRequestBody, DefaultAcceptsContentType)); + factoryContext.JsonRequestBodyParameter = parameter; + factoryContext.AllowEmptyRequestBody = allowEmpty || isOptional; + factoryContext.Metadata.Add(new AcceptsMetadata(parameter.ParameterType, factoryContext.AllowEmptyRequestBody, DefaultAcceptsContentType)); - if (!factoryContext.AllowEmptyRequestBody) - { - if (factoryContext.HasInferredBody) - { - // if (bodyValue == null) - // { - // wasParamCheckFailure = true; - // Log.ImplicitBodyNotProvided(httpContext, "todo", ThrowOnBadRequest); - // } - factoryContext.ParamCheckExpressions.Add(Expression.Block( - Expression.IfThen( - Expression.Equal(BodyValueExpr, Expression.Constant(null)), - Expression.Block( - Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), - Expression.Call(LogImplicitBodyNotProvidedMethod, - HttpContextExpr, - Expression.Constant(parameter.Name), - Expression.Constant(factoryContext.ThrowOnBadRequest) - ) - ) - ) - )); - } - else - { - // If the parameter is required or the user has not explicitly - // set allowBody to be empty then validate that it is required. - // - // if (bodyValue == null) - // { - // wasParamCheckFailure = true; - // Log.RequiredParameterNotProvided(httpContext, "Todo", "todo", "body", ThrowOnBadRequest); - // } - var checkRequiredBodyBlock = Expression.Block( - Expression.IfThen( + if (!factoryContext.AllowEmptyRequestBody) + { + if (factoryContext.HasInferredBody) + { + // if (bodyValue == null) + // { + // wasParamCheckFailure = true; + // Log.ImplicitBodyNotProvided(httpContext, "todo", ThrowOnBadRequest); + // } + factoryContext.ParamCheckExpressions.Add(Expression.Block( + Expression.IfThen( Expression.Equal(BodyValueExpr, Expression.Constant(null)), - Expression.Block( - Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), - Expression.Call(LogRequiredParameterNotProvidedMethod, - HttpContextExpr, - Expression.Constant(TypeNameHelper.GetTypeDisplayName(parameter.ParameterType, fullName: false)), - Expression.Constant(parameter.Name), - Expression.Constant("body"), - Expression.Constant(factoryContext.ThrowOnBadRequest)) + Expression.Block( + Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), + Expression.Call(LogImplicitBodyNotProvidedMethod, + HttpContextExpr, + Expression.Constant(parameter.Name), + Expression.Constant(factoryContext.ThrowOnBadRequest) ) ) - ); - factoryContext.ParamCheckExpressions.Add(checkRequiredBodyBlock); - } - + ) + )); } - else if (parameter.HasDefaultValue) + else { - // Convert(bodyValue ?? SomeDefault, Todo) - return Expression.Convert( - Expression.Coalesce(BodyValueExpr, Expression.Constant(parameter.DefaultValue)), - parameter.ParameterType); + // If the parameter is required or the user has not explicitly + // set allowBody to be empty then validate that it is required. + // + // if (bodyValue == null) + // { + // wasParamCheckFailure = true; + // Log.RequiredParameterNotProvided(httpContext, "Todo", "todo", "body", ThrowOnBadRequest); + // } + var checkRequiredBodyBlock = Expression.Block( + Expression.IfThen( + Expression.Equal(BodyValueExpr, Expression.Constant(null)), + Expression.Block( + Expression.Assign(WasParamCheckFailureExpr, Expression.Constant(true)), + Expression.Call(LogRequiredParameterNotProvidedMethod, + HttpContextExpr, + Expression.Constant(TypeNameHelper.GetTypeDisplayName(parameter.ParameterType, fullName: false)), + Expression.Constant(parameter.Name), + Expression.Constant("body"), + Expression.Constant(factoryContext.ThrowOnBadRequest)) + ) + ) + ); + factoryContext.ParamCheckExpressions.Add(checkRequiredBodyBlock); } - // Convert(bodyValue, Todo) - return Expression.Convert(BodyValueExpr, parameter.ParameterType); } - - private static bool IsOptionalParameter(ParameterInfo parameter, FactoryContext factoryContext) + else if (parameter.HasDefaultValue) { - // - Parameters representing value or reference types with a default value - // under any nullability context are treated as optional. - // - Value type parameters without a default value in an oblivious - // nullability context are required. - // - Reference type parameters without a default value in an oblivious - // nullability context are optional. - var nullability = factoryContext.NullabilityContext.Create(parameter); - return parameter.HasDefaultValue - || nullability.ReadState != NullabilityState.NotNull; + // Convert(bodyValue ?? SomeDefault, Todo) + return Expression.Convert( + Expression.Coalesce(BodyValueExpr, Expression.Constant(parameter.DefaultValue)), + parameter.ParameterType); } - private static MethodInfo GetMethodInfo(Expression expr) + // Convert(bodyValue, Todo) + return Expression.Convert(BodyValueExpr, parameter.ParameterType); + } + + private static bool IsOptionalParameter(ParameterInfo parameter, FactoryContext factoryContext) + { + // - Parameters representing value or reference types with a default value + // under any nullability context are treated as optional. + // - Value type parameters without a default value in an oblivious + // nullability context are required. + // - Reference type parameters without a default value in an oblivious + // nullability context are optional. + var nullability = factoryContext.NullabilityContext.Create(parameter); + return parameter.HasDefaultValue + || nullability.ReadState != NullabilityState.NotNull; + } + + private static MethodInfo GetMethodInfo(Expression expr) + { + var mc = (MethodCallExpression)expr.Body; + return mc.Method; + } + + private static MemberInfo GetMemberInfo(Expression expr) + { + var mc = (MemberExpression)expr.Body; + return mc.Member; + } + + // The result of the method is null so we fallback to some runtime logic. + // First we check if the result is IResult, Task or ValueTask. If + // it is, we await if necessary then execute the result. + // Then we check to see if it's Task or ValueTask. If it is, we await + // if necessary and restart the cycle until we've reached a terminal state (unknown type). + // We currently don't handle Task or ValueTask. We can support this later if this + // ends up being a common scenario. + private static async Task ExecuteObjectReturn(object? obj, HttpContext httpContext) + { + // See if we need to unwrap Task or ValueTask + if (obj is Task taskObj) { - var mc = (MethodCallExpression)expr.Body; - return mc.Method; + obj = await taskObj; } - - private static MemberInfo GetMemberInfo(Expression expr) + else if (obj is ValueTask valueTaskObj) { - var mc = (MemberExpression)expr.Body; - return mc.Member; + obj = await valueTaskObj; } - - // The result of the method is null so we fallback to some runtime logic. - // First we check if the result is IResult, Task or ValueTask. If - // it is, we await if necessary then execute the result. - // Then we check to see if it's Task or ValueTask. If it is, we await - // if necessary and restart the cycle until we've reached a terminal state (unknown type). - // We currently don't handle Task or ValueTask. We can support this later if this - // ends up being a common scenario. - private static async Task ExecuteObjectReturn(object? obj, HttpContext httpContext) + else if (obj is Task task) { - // See if we need to unwrap Task or ValueTask - if (obj is Task taskObj) - { - obj = await taskObj; - } - else if (obj is ValueTask valueTaskObj) - { - obj = await valueTaskObj; - } - else if (obj is Task task) - { - await ExecuteTaskResult(task, httpContext); - return; - } - else if (obj is ValueTask valueTask) - { - await ExecuteValueTaskResult(valueTask, httpContext); - return; - } - else if (obj is Task taskString) - { - await ExecuteTaskOfString(taskString, httpContext); - return; - } - else if (obj is ValueTask valueTaskString) - { - await ExecuteValueTaskOfString(valueTaskString, httpContext); - return; - } - - // Terminal built ins - if (obj is IResult result) - { - await ExecuteResultWriteResponse(result, httpContext); - } - else if (obj is string stringValue) - { - SetPlaintextContentType(httpContext); - await httpContext.Response.WriteAsync(stringValue); - } - else - { - // Otherwise, we JSON serialize when we reach the terminal state - await httpContext.Response.WriteAsJsonAsync(obj); - } + await ExecuteTaskResult(task, httpContext); + return; } - - private static Task ExecuteTask(Task task, HttpContext httpContext) + else if (obj is ValueTask valueTask) { - EnsureRequestTaskNotNull(task); - - static async Task ExecuteAwaited(Task task, HttpContext httpContext) - { - await httpContext.Response.WriteAsJsonAsync(await task); - } - - if (task.IsCompletedSuccessfully) - { - return httpContext.Response.WriteAsJsonAsync(task.GetAwaiter().GetResult()); - } - - return ExecuteAwaited(task, httpContext); + await ExecuteValueTaskResult(valueTask, httpContext); + return; } - - private static Task ExecuteTaskOfString(Task task, HttpContext httpContext) + else if (obj is Task taskString) { - SetPlaintextContentType(httpContext); - EnsureRequestTaskNotNull(task); - - static async Task ExecuteAwaited(Task task, HttpContext httpContext) - { - await httpContext.Response.WriteAsync(await task); - } - - if (task.IsCompletedSuccessfully) - { - return httpContext.Response.WriteAsync(task.GetAwaiter().GetResult()!); - } - - return ExecuteAwaited(task!, httpContext); + await ExecuteTaskOfString(taskString, httpContext); + return; } - - private static Task ExecuteWriteStringResponseAsync(HttpContext httpContext, string text) + else if (obj is ValueTask valueTaskString) { - SetPlaintextContentType(httpContext); - return httpContext.Response.WriteAsync(text); + await ExecuteValueTaskOfString(valueTaskString, httpContext); + return; } - private static Task ExecuteValueTask(ValueTask task) + // Terminal built ins + if (obj is IResult result) { - static async Task ExecuteAwaited(ValueTask task) - { - await task; - } - - if (task.IsCompletedSuccessfully) - { - task.GetAwaiter().GetResult(); - } - - return ExecuteAwaited(task); + await ExecuteResultWriteResponse(result, httpContext); } - - private static Task ExecuteValueTaskOfT(ValueTask task, HttpContext httpContext) + else if (obj is string stringValue) { - static async Task ExecuteAwaited(ValueTask task, HttpContext httpContext) - { - await httpContext.Response.WriteAsJsonAsync(await task); - } + SetPlaintextContentType(httpContext); + await httpContext.Response.WriteAsync(stringValue); + } + else + { + // Otherwise, we JSON serialize when we reach the terminal state + await httpContext.Response.WriteAsJsonAsync(obj); + } + } - if (task.IsCompletedSuccessfully) - { - return httpContext.Response.WriteAsJsonAsync(task.GetAwaiter().GetResult()); - } + private static Task ExecuteTask(Task task, HttpContext httpContext) + { + EnsureRequestTaskNotNull(task); - return ExecuteAwaited(task, httpContext); + static async Task ExecuteAwaited(Task task, HttpContext httpContext) + { + await httpContext.Response.WriteAsJsonAsync(await task); } - private static Task ExecuteValueTaskOfString(ValueTask task, HttpContext httpContext) + if (task.IsCompletedSuccessfully) { - SetPlaintextContentType(httpContext); + return httpContext.Response.WriteAsJsonAsync(task.GetAwaiter().GetResult()); + } - static async Task ExecuteAwaited(ValueTask task, HttpContext httpContext) - { - await httpContext.Response.WriteAsync(await task); - } + return ExecuteAwaited(task, httpContext); + } - if (task.IsCompletedSuccessfully) - { - return httpContext.Response.WriteAsync(task.GetAwaiter().GetResult()!); - } + private static Task ExecuteTaskOfString(Task task, HttpContext httpContext) + { + SetPlaintextContentType(httpContext); + EnsureRequestTaskNotNull(task); - return ExecuteAwaited(task!, httpContext); + static async Task ExecuteAwaited(Task task, HttpContext httpContext) + { + await httpContext.Response.WriteAsync(await task); } - private static Task ExecuteValueTaskResult(ValueTask task, HttpContext httpContext) where T : IResult + if (task.IsCompletedSuccessfully) { - static async Task ExecuteAwaited(ValueTask task, HttpContext httpContext) - { - await EnsureRequestResultNotNull(await task).ExecuteAsync(httpContext); - } + return httpContext.Response.WriteAsync(task.GetAwaiter().GetResult()!); + } - if (task.IsCompletedSuccessfully) - { - return EnsureRequestResultNotNull(task.GetAwaiter().GetResult()).ExecuteAsync(httpContext); - } + return ExecuteAwaited(task!, httpContext); + } - return ExecuteAwaited(task!, httpContext); - } + private static Task ExecuteWriteStringResponseAsync(HttpContext httpContext, string text) + { + SetPlaintextContentType(httpContext); + return httpContext.Response.WriteAsync(text); + } - private static async Task ExecuteTaskResult(Task task, HttpContext httpContext) where T : IResult + private static Task ExecuteValueTask(ValueTask task) + { + static async Task ExecuteAwaited(ValueTask task) { - EnsureRequestTaskOfNotNull(task); - - await EnsureRequestResultNotNull(await task).ExecuteAsync(httpContext); + await task; } - private static async Task ExecuteResultWriteResponse(IResult? result, HttpContext httpContext) + if (task.IsCompletedSuccessfully) { - await EnsureRequestResultNotNull(result).ExecuteAsync(httpContext); + task.GetAwaiter().GetResult(); } - private class FactoryContext + return ExecuteAwaited(task); + } + + private static Task ExecuteValueTaskOfT(ValueTask task, HttpContext httpContext) + { + static async Task ExecuteAwaited(ValueTask task, HttpContext httpContext) { - // Options - public IServiceProviderIsService? ServiceProviderIsService { get; init; } - public List? RouteParameters { get; init; } - public bool ThrowOnBadRequest { get; init; } - public bool DisableInferredFromBody { get; init; } + await httpContext.Response.WriteAsJsonAsync(await task); + } - // Temporary State - public ParameterInfo? JsonRequestBodyParameter { get; set; } - public bool AllowEmptyRequestBody { get; set; } + if (task.IsCompletedSuccessfully) + { + return httpContext.Response.WriteAsJsonAsync(task.GetAwaiter().GetResult()); + } - public bool UsingTempSourceString { get; set; } - public List ExtraLocals { get; } = new(); - public List ParamCheckExpressions { get; } = new(); - public List>> ParameterBinders { get; } = new(); + return ExecuteAwaited(task, httpContext); + } - public Dictionary TrackedParameters { get; } = new(); - public bool HasMultipleBodyParameters { get; set; } - public bool HasInferredBody { get; set; } + private static Task ExecuteValueTaskOfString(ValueTask task, HttpContext httpContext) + { + SetPlaintextContentType(httpContext); - public List Metadata { get; } = new(); + static async Task ExecuteAwaited(ValueTask task, HttpContext httpContext) + { + await httpContext.Response.WriteAsync(await task); + } - public NullabilityInfoContext NullabilityContext { get; } = new(); + if (task.IsCompletedSuccessfully) + { + return httpContext.Response.WriteAsync(task.GetAwaiter().GetResult()!); } - private static class RequestDelegateFactoryConstants + return ExecuteAwaited(task!, httpContext); + } + + private static Task ExecuteValueTaskResult(ValueTask task, HttpContext httpContext) where T : IResult + { + static async Task ExecuteAwaited(ValueTask task, HttpContext httpContext) { - public const string RouteAttribute = "Route (Attribute)"; - public const string QueryAttribute = "Query (Attribute)"; - public const string HeaderAttribute = "Header (Attribute)"; - public const string BodyAttribute = "Body (Attribute)"; - public const string ServiceAttribute = "Service (Attribute)"; - public const string RouteParameter = "Route (Inferred)"; - public const string QueryStringParameter = "Query String (Inferred)"; - public const string ServiceParameter = "Services (Inferred)"; - public const string BodyParameter = "Body (Inferred)"; - public const string RouteOrQueryStringParameter = "Route or Query String (Inferred)"; + await EnsureRequestResultNotNull(await task).ExecuteAsync(httpContext); } - private static partial class Log + if (task.IsCompletedSuccessfully) { - private const string InvalidJsonRequestBodyMessage = @"Failed to read parameter ""{ParameterType} {ParameterName}"" from the request body as JSON."; - private const string InvalidJsonRequestBodyExceptionMessage = @"Failed to read parameter ""{0} {1}"" from the request body as JSON."; + return EnsureRequestResultNotNull(task.GetAwaiter().GetResult()).ExecuteAsync(httpContext); + } - private const string ParameterBindingFailedLogMessage = @"Failed to bind parameter ""{ParameterType} {ParameterName}"" from ""{SourceValue}""."; - private const string ParameterBindingFailedExceptionMessage = @"Failed to bind parameter ""{0} {1}"" from ""{2}""."; + return ExecuteAwaited(task!, httpContext); + } - private const string RequiredParameterNotProvidedLogMessage = @"Required parameter ""{ParameterType} {ParameterName}"" was not provided from {Source}."; - private const string RequiredParameterNotProvidedExceptionMessage = @"Required parameter ""{0} {1}"" was not provided from {2}."; + private static async Task ExecuteTaskResult(Task task, HttpContext httpContext) where T : IResult + { + EnsureRequestTaskOfNotNull(task); - private const string UnexpectedContentTypeLogMessage = @"Expected a supported JSON media type but got ""{ContentType}""."; - private const string UnexpectedContentTypeExceptionMessage = @"Expected a supported JSON media type but got ""{0}""."; + await EnsureRequestResultNotNull(await task).ExecuteAsync(httpContext); + } - private const string ImplicitBodyNotProvidedLogMessage = @"Implicit body inferred for parameter ""{ParameterName}"" but no body was provided. Did you mean to use a Service instead?"; - private const string ImplicitBodyNotProvidedExceptionMessage = @"Implicit body inferred for parameter ""{0}"" but no body was provided. Did you mean to use a Service instead?"; + private static async Task ExecuteResultWriteResponse(IResult? result, HttpContext httpContext) + { + await EnsureRequestResultNotNull(result).ExecuteAsync(httpContext); + } - // This doesn't take a shouldThrow parameter because an IOException indicates an aborted request rather than a "bad" request so - // a BadHttpRequestException feels wrong. The client shouldn't be able to read the Developer Exception Page at any rate. - public static void RequestBodyIOException(HttpContext httpContext, IOException exception) - => RequestBodyIOException(GetLogger(httpContext), exception); + private class FactoryContext + { + // Options + public IServiceProviderIsService? ServiceProviderIsService { get; init; } + public List? RouteParameters { get; init; } + public bool ThrowOnBadRequest { get; init; } + public bool DisableInferredFromBody { get; init; } - [LoggerMessage(1, LogLevel.Debug, "Reading the request body failed with an IOException.", EventName = "RequestBodyIOException")] - private static partial void RequestBodyIOException(ILogger logger, IOException exception); + // Temporary State + public ParameterInfo? JsonRequestBodyParameter { get; set; } + public bool AllowEmptyRequestBody { get; set; } - public static void InvalidJsonRequestBody(HttpContext httpContext, string parameterTypeName, string parameterName, Exception exception, bool shouldThrow) - { - if (shouldThrow) - { - var message = string.Format(CultureInfo.InvariantCulture, InvalidJsonRequestBodyExceptionMessage, parameterTypeName, parameterName); - throw new BadHttpRequestException(message, exception); - } + public bool UsingTempSourceString { get; set; } + public List ExtraLocals { get; } = new(); + public List ParamCheckExpressions { get; } = new(); + public List>> ParameterBinders { get; } = new(); - InvalidJsonRequestBody(GetLogger(httpContext), parameterTypeName, parameterName, exception); - } + public Dictionary TrackedParameters { get; } = new(); + public bool HasMultipleBodyParameters { get; set; } + public bool HasInferredBody { get; set; } - [LoggerMessage(2, LogLevel.Debug, InvalidJsonRequestBodyMessage, EventName = "InvalidJsonRequestBody")] - private static partial void InvalidJsonRequestBody(ILogger logger, string parameterType, string parameterName, Exception exception); + public List Metadata { get; } = new(); - public static void ParameterBindingFailed(HttpContext httpContext, string parameterTypeName, string parameterName, string sourceValue, bool shouldThrow) - { - if (shouldThrow) - { - var message = string.Format(CultureInfo.InvariantCulture, ParameterBindingFailedExceptionMessage, parameterTypeName, parameterName, sourceValue); - throw new BadHttpRequestException(message); - } + public NullabilityInfoContext NullabilityContext { get; } = new(); + } - ParameterBindingFailed(GetLogger(httpContext), parameterTypeName, parameterName, sourceValue); - } + private static class RequestDelegateFactoryConstants + { + public const string RouteAttribute = "Route (Attribute)"; + public const string QueryAttribute = "Query (Attribute)"; + public const string HeaderAttribute = "Header (Attribute)"; + public const string BodyAttribute = "Body (Attribute)"; + public const string ServiceAttribute = "Service (Attribute)"; + public const string RouteParameter = "Route (Inferred)"; + public const string QueryStringParameter = "Query String (Inferred)"; + public const string ServiceParameter = "Services (Inferred)"; + public const string BodyParameter = "Body (Inferred)"; + public const string RouteOrQueryStringParameter = "Route or Query String (Inferred)"; + } - [LoggerMessage(3, LogLevel.Debug, ParameterBindingFailedLogMessage, EventName = "ParameterBindingFailed")] - private static partial void ParameterBindingFailed(ILogger logger, string parameterType, string parameterName, string sourceValue); + private static partial class Log + { + private const string InvalidJsonRequestBodyMessage = @"Failed to read parameter ""{ParameterType} {ParameterName}"" from the request body as JSON."; + private const string InvalidJsonRequestBodyExceptionMessage = @"Failed to read parameter ""{0} {1}"" from the request body as JSON."; - public static void RequiredParameterNotProvided(HttpContext httpContext, string parameterTypeName, string parameterName, string source, bool shouldThrow) - { - if (shouldThrow) - { - var message = string.Format(CultureInfo.InvariantCulture, RequiredParameterNotProvidedExceptionMessage, parameterTypeName, parameterName, source); - throw new BadHttpRequestException(message); - } + private const string ParameterBindingFailedLogMessage = @"Failed to bind parameter ""{ParameterType} {ParameterName}"" from ""{SourceValue}""."; + private const string ParameterBindingFailedExceptionMessage = @"Failed to bind parameter ""{0} {1}"" from ""{2}""."; - RequiredParameterNotProvided(GetLogger(httpContext), parameterTypeName, parameterName, source); - } + private const string RequiredParameterNotProvidedLogMessage = @"Required parameter ""{ParameterType} {ParameterName}"" was not provided from {Source}."; + private const string RequiredParameterNotProvidedExceptionMessage = @"Required parameter ""{0} {1}"" was not provided from {2}."; - [LoggerMessage(4, LogLevel.Debug, RequiredParameterNotProvidedLogMessage, EventName = "RequiredParameterNotProvided")] - private static partial void RequiredParameterNotProvided(ILogger logger, string parameterType, string parameterName, string source); + private const string UnexpectedContentTypeLogMessage = @"Expected a supported JSON media type but got ""{ContentType}""."; + private const string UnexpectedContentTypeExceptionMessage = @"Expected a supported JSON media type but got ""{0}""."; - public static void ImplicitBodyNotProvided(HttpContext httpContext, string parameterName, bool shouldThrow) - { - if (shouldThrow) - { - var message = string.Format(CultureInfo.InvariantCulture, ImplicitBodyNotProvidedExceptionMessage, parameterName); - throw new BadHttpRequestException(message); - } + private const string ImplicitBodyNotProvidedLogMessage = @"Implicit body inferred for parameter ""{ParameterName}"" but no body was provided. Did you mean to use a Service instead?"; + private const string ImplicitBodyNotProvidedExceptionMessage = @"Implicit body inferred for parameter ""{0}"" but no body was provided. Did you mean to use a Service instead?"; - ImplicitBodyNotProvided(GetLogger(httpContext), parameterName); - } + // This doesn't take a shouldThrow parameter because an IOException indicates an aborted request rather than a "bad" request so + // a BadHttpRequestException feels wrong. The client shouldn't be able to read the Developer Exception Page at any rate. + public static void RequestBodyIOException(HttpContext httpContext, IOException exception) + => RequestBodyIOException(GetLogger(httpContext), exception); - [LoggerMessage(5, LogLevel.Debug, ImplicitBodyNotProvidedLogMessage, EventName = "ImplicitBodyNotProvided")] - private static partial void ImplicitBodyNotProvided(ILogger logger, string parameterName); + [LoggerMessage(1, LogLevel.Debug, "Reading the request body failed with an IOException.", EventName = "RequestBodyIOException")] + private static partial void RequestBodyIOException(ILogger logger, IOException exception); - public static void UnexpectedContentType(HttpContext httpContext, string? contentType, bool shouldThrow) + public static void InvalidJsonRequestBody(HttpContext httpContext, string parameterTypeName, string parameterName, Exception exception, bool shouldThrow) + { + if (shouldThrow) { - if (shouldThrow) - { - var message = string.Format(CultureInfo.InvariantCulture, UnexpectedContentTypeExceptionMessage, contentType); - throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); - } - - UnexpectedContentType(GetLogger(httpContext), contentType ?? "(none)"); + var message = string.Format(CultureInfo.InvariantCulture, InvalidJsonRequestBodyExceptionMessage, parameterTypeName, parameterName); + throw new BadHttpRequestException(message, exception); } - [LoggerMessage(6, LogLevel.Debug, UnexpectedContentTypeLogMessage, EventName = "UnexpectedContentType")] - private static partial void UnexpectedContentType(ILogger logger, string contentType); + InvalidJsonRequestBody(GetLogger(httpContext), parameterTypeName, parameterName, exception); + } + + [LoggerMessage(2, LogLevel.Debug, InvalidJsonRequestBodyMessage, EventName = "InvalidJsonRequestBody")] + private static partial void InvalidJsonRequestBody(ILogger logger, string parameterType, string parameterName, Exception exception); - private static ILogger GetLogger(HttpContext httpContext) + public static void ParameterBindingFailed(HttpContext httpContext, string parameterTypeName, string parameterName, string sourceValue, bool shouldThrow) + { + if (shouldThrow) { - var loggerFactory = httpContext.RequestServices.GetRequiredService(); - return loggerFactory.CreateLogger(typeof(RequestDelegateFactory)); + var message = string.Format(CultureInfo.InvariantCulture, ParameterBindingFailedExceptionMessage, parameterTypeName, parameterName, sourceValue); + throw new BadHttpRequestException(message); } + + ParameterBindingFailed(GetLogger(httpContext), parameterTypeName, parameterName, sourceValue); } - private static void EnsureRequestTaskOfNotNull(Task task) where T : IResult + [LoggerMessage(3, LogLevel.Debug, ParameterBindingFailedLogMessage, EventName = "ParameterBindingFailed")] + private static partial void ParameterBindingFailed(ILogger logger, string parameterType, string parameterName, string sourceValue); + + public static void RequiredParameterNotProvided(HttpContext httpContext, string parameterTypeName, string parameterName, string source, bool shouldThrow) { - if (task is null) + if (shouldThrow) { - throw new InvalidOperationException("The IResult in Task response must not be null."); + var message = string.Format(CultureInfo.InvariantCulture, RequiredParameterNotProvidedExceptionMessage, parameterTypeName, parameterName, source); + throw new BadHttpRequestException(message); } + + RequiredParameterNotProvided(GetLogger(httpContext), parameterTypeName, parameterName, source); } - private static void EnsureRequestTaskNotNull(Task? task) + [LoggerMessage(4, LogLevel.Debug, RequiredParameterNotProvidedLogMessage, EventName = "RequiredParameterNotProvided")] + private static partial void RequiredParameterNotProvided(ILogger logger, string parameterType, string parameterName, string source); + + public static void ImplicitBodyNotProvided(HttpContext httpContext, string parameterName, bool shouldThrow) { - if (task is null) + if (shouldThrow) { - throw new InvalidOperationException("The Task returned by the Delegate must not be null."); + var message = string.Format(CultureInfo.InvariantCulture, ImplicitBodyNotProvidedExceptionMessage, parameterName); + throw new BadHttpRequestException(message); } + + ImplicitBodyNotProvided(GetLogger(httpContext), parameterName); } - private static IResult EnsureRequestResultNotNull(IResult? result) + [LoggerMessage(5, LogLevel.Debug, ImplicitBodyNotProvidedLogMessage, EventName = "ImplicitBodyNotProvided")] + private static partial void ImplicitBodyNotProvided(ILogger logger, string parameterName); + + public static void UnexpectedContentType(HttpContext httpContext, string? contentType, bool shouldThrow) { - if (result is null) + if (shouldThrow) { - throw new InvalidOperationException("The IResult returned by the Delegate must not be null."); + var message = string.Format(CultureInfo.InvariantCulture, UnexpectedContentTypeExceptionMessage, contentType); + throw new BadHttpRequestException(message, StatusCodes.Status415UnsupportedMediaType); } - return result; + UnexpectedContentType(GetLogger(httpContext), contentType ?? "(none)"); } - private static void SetPlaintextContentType(HttpContext httpContext) + [LoggerMessage(6, LogLevel.Debug, UnexpectedContentTypeLogMessage, EventName = "UnexpectedContentType")] + private static partial void UnexpectedContentType(ILogger logger, string contentType); + + private static ILogger GetLogger(HttpContext httpContext) { - httpContext.Response.ContentType ??= "text/plain; charset=utf-8"; + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + return loggerFactory.CreateLogger(typeof(RequestDelegateFactory)); } + } - private static string BuildErrorMessageForMultipleBodyParameters(FactoryContext factoryContext) + private static void EnsureRequestTaskOfNotNull(Task task) where T : IResult + { + if (task is null) { - var errorMessage = new StringBuilder(); - errorMessage.AppendLine("Failure to infer one or more parameters."); - errorMessage.AppendLine("Below is the list of parameters that we found: "); - errorMessage.AppendLine(); - errorMessage.AppendLine(FormattableString.Invariant($"{"Parameter",-20}| {"Source",-30}")); - errorMessage.AppendLine("---------------------------------------------------------------------------------"); + throw new InvalidOperationException("The IResult in Task response must not be null."); + } + } - foreach (var kv in factoryContext.TrackedParameters) - { - errorMessage.AppendLine(FormattableString.Invariant($"{kv.Key,-19} | {kv.Value,-15}")); - } - errorMessage.AppendLine().AppendLine(); - errorMessage.AppendLine("Did you mean to register the \"UNKNOWN\" parameters as a Service?") - .AppendLine(); - return errorMessage.ToString(); + private static void EnsureRequestTaskNotNull(Task? task) + { + if (task is null) + { + throw new InvalidOperationException("The Task returned by the Delegate must not be null."); + } + } + + private static IResult EnsureRequestResultNotNull(IResult? result) + { + if (result is null) + { + throw new InvalidOperationException("The IResult returned by the Delegate must not be null."); } - private static string BuildErrorMessageForInferredBodyParameter(FactoryContext factoryContext) + return result; + } + + private static void SetPlaintextContentType(HttpContext httpContext) + { + httpContext.Response.ContentType ??= "text/plain; charset=utf-8"; + } + + private static string BuildErrorMessageForMultipleBodyParameters(FactoryContext factoryContext) + { + var errorMessage = new StringBuilder(); + errorMessage.AppendLine("Failure to infer one or more parameters."); + errorMessage.AppendLine("Below is the list of parameters that we found: "); + errorMessage.AppendLine(); + errorMessage.AppendLine(FormattableString.Invariant($"{"Parameter",-20}| {"Source",-30}")); + errorMessage.AppendLine("---------------------------------------------------------------------------------"); + + foreach (var kv in factoryContext.TrackedParameters) { - var errorMessage = new StringBuilder(); - errorMessage.AppendLine("Body was inferred but the method does not allow inferred body parameters."); - errorMessage.AppendLine("Below is the list of parameters that we found: "); - errorMessage.AppendLine(); - errorMessage.AppendLine(FormattableString.Invariant($"{"Parameter",-20}| {"Source",-30}")); - errorMessage.AppendLine("---------------------------------------------------------------------------------"); + errorMessage.AppendLine(FormattableString.Invariant($"{kv.Key,-19} | {kv.Value,-15}")); + } + errorMessage.AppendLine().AppendLine(); + errorMessage.AppendLine("Did you mean to register the \"UNKNOWN\" parameters as a Service?") + .AppendLine(); + return errorMessage.ToString(); + } - foreach (var kv in factoryContext.TrackedParameters) - { - errorMessage.AppendLine(FormattableString.Invariant($"{kv.Key,-19} | {kv.Value,-15}")); - } - errorMessage.AppendLine().AppendLine(); - errorMessage.AppendLine("Did you mean to register the \"Body (Inferred)\" parameter(s) as a Service or apply the [FromService] or [FromBody] attribute?") - .AppendLine(); - return errorMessage.ToString(); + private static string BuildErrorMessageForInferredBodyParameter(FactoryContext factoryContext) + { + var errorMessage = new StringBuilder(); + errorMessage.AppendLine("Body was inferred but the method does not allow inferred body parameters."); + errorMessage.AppendLine("Below is the list of parameters that we found: "); + errorMessage.AppendLine(); + errorMessage.AppendLine(FormattableString.Invariant($"{"Parameter",-20}| {"Source",-30}")); + errorMessage.AppendLine("---------------------------------------------------------------------------------"); + + foreach (var kv in factoryContext.TrackedParameters) + { + errorMessage.AppendLine(FormattableString.Invariant($"{kv.Key,-19} | {kv.Value,-15}")); } + errorMessage.AppendLine().AppendLine(); + errorMessage.AppendLine("Did you mean to register the \"Body (Inferred)\" parameter(s) as a Service or apply the [FromService] or [FromBody] attribute?") + .AppendLine(); + return errorMessage.ToString(); } } diff --git a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs index 6d41ece481..892cbd2c7e 100644 --- a/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs +++ b/src/Http/Http.Extensions/src/RequestDelegateFactoryOptions.cs @@ -4,32 +4,31 @@ using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Options for controlling the behavior of the when created using . +/// +public sealed class RequestDelegateFactoryOptions { /// - /// Options for controlling the behavior of the when created using . + /// The instance used to detect if handler parameters are services. /// - public sealed class RequestDelegateFactoryOptions - { - /// - /// The instance used to detect if handler parameters are services. - /// - public IServiceProvider? ServiceProvider { get; init; } + public IServiceProvider? ServiceProvider { get; init; } - /// - /// The list of route parameter names that are specified for this handler. - /// - public IEnumerable? RouteParameterNames { get; init; } + /// + /// The list of route parameter names that are specified for this handler. + /// + public IEnumerable? RouteParameterNames { get; init; } - /// - /// Controls whether the should throw a in addition to - /// writing a log when handling invalid requests. - /// - public bool ThrowOnBadRequest { get; init; } + /// + /// Controls whether the should throw a in addition to + /// writing a log when handling invalid requests. + /// + public bool ThrowOnBadRequest { get; init; } - /// - /// Prevent the from inferring a parameter should be bound from the request body without an attribute that implements . - /// - public bool DisableInferBodyFromParameters { get; init; } - } + /// + /// Prevent the from inferring a parameter should be bound from the request body without an attribute that implements . + /// + public bool DisableInferBodyFromParameters { get; init; } } diff --git a/src/Http/Http.Extensions/src/RequestHeaders.cs b/src/Http/Http.Extensions/src/RequestHeaders.cs index dcccc0231f..a26757c8d7 100644 --- a/src/Http/Http.Extensions/src/RequestHeaders.cs +++ b/src/Http/Http.Extensions/src/RequestHeaders.cs @@ -6,436 +6,435 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http.Headers +namespace Microsoft.AspNetCore.Http.Headers; + +/// +/// Strongly typed HTTP request headers. +/// +public class RequestHeaders { /// - /// Strongly typed HTTP request headers. + /// Initializes a new instance of . /// - public class RequestHeaders + /// The request headers. + public RequestHeaders(IHeaderDictionary headers) { - /// - /// Initializes a new instance of . - /// - /// The request headers. - public RequestHeaders(IHeaderDictionary headers) + if (headers == null) { - if (headers == null) - { - throw new ArgumentNullException(nameof(headers)); - } - - Headers = headers; + throw new ArgumentNullException(nameof(headers)); } - /// - /// Gets the backing request header dictionary. - /// - public IHeaderDictionary Headers { get; } + Headers = headers; + } + + /// + /// Gets the backing request header dictionary. + /// + public IHeaderDictionary Headers { get; } - /// - /// Gets or sets the Accept header for an HTTP request. - /// - public IList Accept + /// + /// Gets or sets the Accept header for an HTTP request. + /// + public IList Accept + { + get { - get - { - return Headers.Accept.GetList(); - } - set - { - Headers.SetList(HeaderNames.Accept, value); - } + return Headers.Accept.GetList(); } - - /// - /// Gets or sets the Accept-Charset header for an HTTP request. - /// - public IList AcceptCharset + set { - get - { - return Headers.AcceptCharset.GetList(); - } - set - { - Headers.SetList(HeaderNames.AcceptCharset, value); - } + Headers.SetList(HeaderNames.Accept, value); } + } - /// - /// Gets or sets the Accept-Encoding header for an HTTP request. - /// - public IList AcceptEncoding + /// + /// Gets or sets the Accept-Charset header for an HTTP request. + /// + public IList AcceptCharset + { + get { - get - { - return Headers.AcceptEncoding.GetList(); - } - set - { - Headers.SetList(HeaderNames.AcceptEncoding, value); - } + return Headers.AcceptCharset.GetList(); } - - /// - /// Gets or sets the Accept-Language header for an HTTP request. - /// - public IList AcceptLanguage + set { - get - { - return Headers.AcceptLanguage.GetList(); - } - set - { - Headers.SetList(HeaderNames.AcceptLanguage, value); - } + Headers.SetList(HeaderNames.AcceptCharset, value); } + } - /// - /// Gets or sets the Cache-Control header for an HTTP request. - /// - public CacheControlHeaderValue? CacheControl + /// + /// Gets or sets the Accept-Encoding header for an HTTP request. + /// + public IList AcceptEncoding + { + get { - get - { - return Headers.Get(HeaderNames.CacheControl); - } - set - { - Headers.Set(HeaderNames.CacheControl, value); - } + return Headers.AcceptEncoding.GetList(); } - - /// - /// Gets or sets the Content-Disposition header for an HTTP request. - /// - public ContentDispositionHeaderValue? ContentDisposition + set { - get - { - return Headers.Get(HeaderNames.ContentDisposition); - } - set - { - Headers.Set(HeaderNames.ContentDisposition, value); - } + Headers.SetList(HeaderNames.AcceptEncoding, value); } + } - /// - /// Gets or sets the Content-Length header for an HTTP request. - /// - public long? ContentLength + /// + /// Gets or sets the Accept-Language header for an HTTP request. + /// + public IList AcceptLanguage + { + get { - get - { - return Headers.ContentLength; - } - set - { - Headers.ContentLength = value; - } + return Headers.AcceptLanguage.GetList(); } - - /// - /// Gets or sets the Content-Range header for an HTTP request. - /// - public ContentRangeHeaderValue? ContentRange + set { - get - { - return Headers.Get(HeaderNames.ContentRange); - } - set - { - Headers.Set(HeaderNames.ContentRange, value); - } + Headers.SetList(HeaderNames.AcceptLanguage, value); } + } - /// - /// Gets or sets the Content-Type header for an HTTP request. - /// - public MediaTypeHeaderValue? ContentType + /// + /// Gets or sets the Cache-Control header for an HTTP request. + /// + public CacheControlHeaderValue? CacheControl + { + get { - get - { - return Headers.Get(HeaderNames.ContentType); - } - set - { - Headers.Set(HeaderNames.ContentType, value); - } + return Headers.Get(HeaderNames.CacheControl); } - - /// - /// Gets or sets the Cookie header for an HTTP request. - /// - public IList Cookie + set { - get - { - return Headers.Cookie.GetList(); - } - set - { - Headers.SetList(HeaderNames.Cookie, value); - } + Headers.Set(HeaderNames.CacheControl, value); } + } - /// - /// Gets or sets the Date header for an HTTP request. - /// - public DateTimeOffset? Date + /// + /// Gets or sets the Content-Disposition header for an HTTP request. + /// + public ContentDispositionHeaderValue? ContentDisposition + { + get { - get - { - return Headers.GetDate(HeaderNames.Date); - } - set - { - Headers.SetDate(HeaderNames.Date, value); - } + return Headers.Get(HeaderNames.ContentDisposition); } + set + { + Headers.Set(HeaderNames.ContentDisposition, value); + } + } - /// - /// Gets or sets the Expires header for an HTTP request. - /// - public DateTimeOffset? Expires + /// + /// Gets or sets the Content-Length header for an HTTP request. + /// + public long? ContentLength + { + get { - get - { - return Headers.GetDate(HeaderNames.Expires); - } - set - { - Headers.SetDate(HeaderNames.Expires, value); - } + return Headers.ContentLength; + } + set + { + Headers.ContentLength = value; } + } - /// - /// Gets or sets the Host header for an HTTP request. - /// - public HostString Host + /// + /// Gets or sets the Content-Range header for an HTTP request. + /// + public ContentRangeHeaderValue? ContentRange + { + get { - get - { - return HostString.FromUriComponent(Headers.Host.ToString()); - } - set - { - Headers.Host = value.ToUriComponent(); - } + return Headers.Get(HeaderNames.ContentRange); } + set + { + Headers.Set(HeaderNames.ContentRange, value); + } + } - /// - /// Gets or sets the If-Match header for an HTTP request. - /// - public IList IfMatch + /// + /// Gets or sets the Content-Type header for an HTTP request. + /// + public MediaTypeHeaderValue? ContentType + { + get { - get - { - return Headers.IfMatch.GetList(); - } - set - { - Headers.SetList(HeaderNames.IfMatch, value); - } + return Headers.Get(HeaderNames.ContentType); + } + set + { + Headers.Set(HeaderNames.ContentType, value); } + } - /// - /// Gets or sets the If-Modified-Since header for an HTTP request. - /// - public DateTimeOffset? IfModifiedSince + /// + /// Gets or sets the Cookie header for an HTTP request. + /// + public IList Cookie + { + get { - get - { - return Headers.GetDate(HeaderNames.IfModifiedSince); - } - set - { - Headers.SetDate(HeaderNames.IfModifiedSince, value); - } + return Headers.Cookie.GetList(); } + set + { + Headers.SetList(HeaderNames.Cookie, value); + } + } - /// - /// Gets or sets the If-None-Match header for an HTTP request. - /// - public IList IfNoneMatch + /// + /// Gets or sets the Date header for an HTTP request. + /// + public DateTimeOffset? Date + { + get { - get - { - return Headers.IfNoneMatch.GetList(); - } - set - { - Headers.SetList(HeaderNames.IfNoneMatch, value); - } + return Headers.GetDate(HeaderNames.Date); + } + set + { + Headers.SetDate(HeaderNames.Date, value); } + } - /// - /// Gets or sets the If-Range header for an HTTP request. - /// - public RangeConditionHeaderValue? IfRange + /// + /// Gets or sets the Expires header for an HTTP request. + /// + public DateTimeOffset? Expires + { + get { - get - { - return Headers.Get(HeaderNames.IfRange); - } - set - { - Headers.Set(HeaderNames.IfRange, value); - } + return Headers.GetDate(HeaderNames.Expires); + } + set + { + Headers.SetDate(HeaderNames.Expires, value); } + } - /// - /// Gets or sets the If-Unmodified-Since header for an HTTP request. - /// - public DateTimeOffset? IfUnmodifiedSince + /// + /// Gets or sets the Host header for an HTTP request. + /// + public HostString Host + { + get { - get - { - return Headers.GetDate(HeaderNames.IfUnmodifiedSince); - } - set - { - Headers.SetDate(HeaderNames.IfUnmodifiedSince, value); - } + return HostString.FromUriComponent(Headers.Host.ToString()); + } + set + { + Headers.Host = value.ToUriComponent(); } + } - /// - /// Gets or sets the Last-Modified header for an HTTP request. - /// - public DateTimeOffset? LastModified + /// + /// Gets or sets the If-Match header for an HTTP request. + /// + public IList IfMatch + { + get { - get - { - return Headers.GetDate(HeaderNames.LastModified); - } - set - { - Headers.SetDate(HeaderNames.LastModified, value); - } + return Headers.IfMatch.GetList(); + } + set + { + Headers.SetList(HeaderNames.IfMatch, value); } + } - /// - /// Gets or sets the Range header for an HTTP request. - /// - public RangeHeaderValue? Range + /// + /// Gets or sets the If-Modified-Since header for an HTTP request. + /// + public DateTimeOffset? IfModifiedSince + { + get { - get - { - return Headers.Get(HeaderNames.Range); - } - set - { - Headers.Set(HeaderNames.Range, value); - } + return Headers.GetDate(HeaderNames.IfModifiedSince); + } + set + { + Headers.SetDate(HeaderNames.IfModifiedSince, value); } + } - /// - /// Gets or sets the Referer header for an HTTP request. - /// - public Uri? Referer + /// + /// Gets or sets the If-None-Match header for an HTTP request. + /// + public IList IfNoneMatch + { + get { - get - { - if (Uri.TryCreate(Headers.Referer, UriKind.RelativeOrAbsolute, out var uri)) - { - return uri; - } - return null; - } - set - { - Headers.Set(HeaderNames.Referer, value == null ? null : UriHelper.Encode(value)); - } + return Headers.IfNoneMatch.GetList(); + } + set + { + Headers.SetList(HeaderNames.IfNoneMatch, value); } + } - /// - /// Gets the value of header with . - /// - /// must contain a TryParse method with the signature public static bool TryParse(string, out T). - /// The type of the header. - /// The given type must have a static TryParse method. - /// The name of the header to retrieve. - /// The value of the header. - public T? Get(string name) + /// + /// Gets or sets the If-Range header for an HTTP request. + /// + public RangeConditionHeaderValue? IfRange + { + get { - return Headers.Get(name); + return Headers.Get(HeaderNames.IfRange); } + set + { + Headers.Set(HeaderNames.IfRange, value); + } + } - /// - /// Gets the values of header with . - /// - /// must contain a TryParseList method with the signature public static bool TryParseList(IList<string>, out IList<T>). - /// The type of the header. - /// The given type must have a static TryParseList method. - /// The name of the header to retrieve. - /// List of values of the header. - public IList GetList(string name) + /// + /// Gets or sets the If-Unmodified-Since header for an HTTP request. + /// + public DateTimeOffset? IfUnmodifiedSince + { + get + { + return Headers.GetDate(HeaderNames.IfUnmodifiedSince); + } + set { - return Headers.GetList(name); + Headers.SetDate(HeaderNames.IfUnmodifiedSince, value); } + } - /// - /// Sets the header value. - /// - /// The header name. - /// The header value. - public void Set(string name, object? value) + /// + /// Gets or sets the Last-Modified header for an HTTP request. + /// + public DateTimeOffset? LastModified + { + get { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } + return Headers.GetDate(HeaderNames.LastModified); + } + set + { + Headers.SetDate(HeaderNames.LastModified, value); + } + } - Headers.Set(name, value); + /// + /// Gets or sets the Range header for an HTTP request. + /// + public RangeHeaderValue? Range + { + get + { + return Headers.Get(HeaderNames.Range); + } + set + { + Headers.Set(HeaderNames.Range, value); } + } - /// - /// Sets the specified header and it's values. - /// - /// The type of the value. - /// The header name. - /// The sequence of header values. - public void SetList(string name, IList? values) + /// + /// Gets or sets the Referer header for an HTTP request. + /// + public Uri? Referer + { + get { - if (name == null) + if (Uri.TryCreate(Headers.Referer, UriKind.RelativeOrAbsolute, out var uri)) { - throw new ArgumentNullException(nameof(name)); + return uri; } + return null; + } + set + { + Headers.Set(HeaderNames.Referer, value == null ? null : UriHelper.Encode(value)); + } + } - Headers.SetList(name, values); + /// + /// Gets the value of header with . + /// + /// must contain a TryParse method with the signature public static bool TryParse(string, out T). + /// The type of the header. + /// The given type must have a static TryParse method. + /// The name of the header to retrieve. + /// The value of the header. + public T? Get(string name) + { + return Headers.Get(name); + } + + /// + /// Gets the values of header with . + /// + /// must contain a TryParseList method with the signature public static bool TryParseList(IList<string>, out IList<T>). + /// The type of the header. + /// The given type must have a static TryParseList method. + /// The name of the header to retrieve. + /// List of values of the header. + public IList GetList(string name) + { + return Headers.GetList(name); + } + + /// + /// Sets the header value. + /// + /// The header name. + /// The header value. + public void Set(string name, object? value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); } - /// - /// Appends the header name and value. - /// - /// The header name. - /// The header value. - public void Append(string name, object value) + Headers.Set(name, value); + } + + /// + /// Sets the specified header and it's values. + /// + /// The type of the value. + /// The header name. + /// The sequence of header values. + public void SetList(string name, IList? values) + { + if (name == null) { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } + throw new ArgumentNullException(nameof(name)); + } - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } + Headers.SetList(name, values); + } - Headers.Append(name, value.ToString()); + /// + /// Appends the header name and value. + /// + /// The header name. + /// The header value. + public void Append(string name, object value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); } - /// - /// Appends the header name and it's values. - /// - /// The header name. - /// The header values. - public void AppendList(string name, IList values) + if (value == null) { - Headers.AppendList(name, values); + throw new ArgumentNullException(nameof(value)); } + + Headers.Append(name, value.ToString()); + } + + /// + /// Appends the header name and it's values. + /// + /// The header name. + /// The header values. + public void AppendList(string name, IList values) + { + Headers.AppendList(name, values); } } diff --git a/src/Http/Http.Extensions/src/ResponseExtensions.cs b/src/Http/Http.Extensions/src/ResponseExtensions.cs index 4a3607625c..992fa29214 100644 --- a/src/Http/Http.Extensions/src/ResponseExtensions.cs +++ b/src/Http/Http.Extensions/src/ResponseExtensions.cs @@ -5,54 +5,53 @@ using System; using Microsoft.AspNetCore.Http.Features; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Extension methods for . +/// +public static class ResponseExtensions { /// - /// Extension methods for . + /// Clears the HTTP response. + /// + /// This invocation resets the response headers, response status code, and response body. + /// /// - public static class ResponseExtensions + /// The to clear. + public static void Clear(this HttpResponse response) { - /// - /// Clears the HTTP response. - /// - /// This invocation resets the response headers, response status code, and response body. - /// - /// - /// The to clear. - public static void Clear(this HttpResponse response) + if (response.HasStarted) { - if (response.HasStarted) - { - throw new InvalidOperationException("The response cannot be cleared, it has already started sending."); - } - response.StatusCode = 200; - response.HttpContext.Features.Get()!.ReasonPhrase = null; - response.Headers.Clear(); - if (response.Body.CanSeek) - { - response.Body.SetLength(0); - } + throw new InvalidOperationException("The response cannot be cleared, it has already started sending."); } - - /// - /// Returns a redirect response (HTTP 301, HTTP 302, HTTP 307 or HTTP 308) to the client. - /// - /// The to redirect. - /// The URL to redirect the client to. This must be properly encoded for use in http headers where only ASCII characters are allowed. - /// True if the redirect is permanent (301 or 308), otherwise false (302 or 307). - /// True if the redirect needs to reuse the method and body (308 or 307), otherwise false (301 or 302). - public static void Redirect(this HttpResponse response, string location, bool permanent, bool preserveMethod) + response.StatusCode = 200; + response.HttpContext.Features.Get()!.ReasonPhrase = null; + response.Headers.Clear(); + if (response.Body.CanSeek) { - if (preserveMethod) - { - response.StatusCode = permanent ? StatusCodes.Status308PermanentRedirect : StatusCodes.Status307TemporaryRedirect; - } - else - { - response.StatusCode = permanent ? StatusCodes.Status301MovedPermanently : StatusCodes.Status302Found; - } + response.Body.SetLength(0); + } + } - response.Headers.Location = location; + /// + /// Returns a redirect response (HTTP 301, HTTP 302, HTTP 307 or HTTP 308) to the client. + /// + /// The to redirect. + /// The URL to redirect the client to. This must be properly encoded for use in http headers where only ASCII characters are allowed. + /// True if the redirect is permanent (301 or 308), otherwise false (302 or 307). + /// True if the redirect needs to reuse the method and body (308 or 307), otherwise false (301 or 302). + public static void Redirect(this HttpResponse response, string location, bool permanent, bool preserveMethod) + { + if (preserveMethod) + { + response.StatusCode = permanent ? StatusCodes.Status308PermanentRedirect : StatusCodes.Status307TemporaryRedirect; } + else + { + response.StatusCode = permanent ? StatusCodes.Status301MovedPermanently : StatusCodes.Status302Found; + } + + response.Headers.Location = location; } } diff --git a/src/Http/Http.Extensions/src/ResponseHeaders.cs b/src/Http/Http.Extensions/src/ResponseHeaders.cs index d5f72dcf6a..f4212e3232 100644 --- a/src/Http/Http.Extensions/src/ResponseHeaders.cs +++ b/src/Http/Http.Extensions/src/ResponseHeaders.cs @@ -6,286 +6,285 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http.Headers +namespace Microsoft.AspNetCore.Http.Headers; + +/// +/// Strongly typed HTTP response headers. +/// +public class ResponseHeaders { /// - /// Strongly typed HTTP response headers. + /// Initializes a new instance of . /// - public class ResponseHeaders + /// The request headers. + public ResponseHeaders(IHeaderDictionary headers) { - /// - /// Initializes a new instance of . - /// - /// The request headers. - public ResponseHeaders(IHeaderDictionary headers) + if (headers == null) { - if (headers == null) - { - throw new ArgumentNullException(nameof(headers)); - } - - Headers = headers; + throw new ArgumentNullException(nameof(headers)); } - /// - /// Gets the backing response header dictionary. - /// - public IHeaderDictionary Headers { get; } + Headers = headers; + } + + /// + /// Gets the backing response header dictionary. + /// + public IHeaderDictionary Headers { get; } - /// - /// Gets or sets the Cache-Control header for an HTTP response. - /// - public CacheControlHeaderValue? CacheControl + /// + /// Gets or sets the Cache-Control header for an HTTP response. + /// + public CacheControlHeaderValue? CacheControl + { + get { - get - { - return Headers.Get(HeaderNames.CacheControl); - } - set - { - Headers.Set(HeaderNames.CacheControl, value); - } + return Headers.Get(HeaderNames.CacheControl); } - - /// - /// Gets or sets the Content-Disposition header for an HTTP response. - /// - public ContentDispositionHeaderValue? ContentDisposition + set { - get - { - return Headers.Get(HeaderNames.ContentDisposition); - } - set - { - Headers.Set(HeaderNames.ContentDisposition, value); - } + Headers.Set(HeaderNames.CacheControl, value); } + } - /// - /// Gets or sets the Content-Length header for an HTTP response. - /// - public long? ContentLength + /// + /// Gets or sets the Content-Disposition header for an HTTP response. + /// + public ContentDispositionHeaderValue? ContentDisposition + { + get { - get - { - return Headers.ContentLength; - } - set - { - Headers.ContentLength = value; - } + return Headers.Get(HeaderNames.ContentDisposition); } - - /// - /// Gets or sets the Content-Range header for an HTTP response. - /// - public ContentRangeHeaderValue? ContentRange + set { - get - { - return Headers.Get(HeaderNames.ContentRange); - } - set - { - Headers.Set(HeaderNames.ContentRange, value); - } + Headers.Set(HeaderNames.ContentDisposition, value); } + } - /// - /// Gets or sets the Content-Type header for an HTTP response. - /// - public MediaTypeHeaderValue? ContentType + /// + /// Gets or sets the Content-Length header for an HTTP response. + /// + public long? ContentLength + { + get { - get - { - return Headers.Get(HeaderNames.ContentType); - } - set - { - Headers.Set(HeaderNames.ContentType, value); - } + return Headers.ContentLength; } - - /// - /// Gets or sets the Date header for an HTTP response. - /// - public DateTimeOffset? Date + set { - get - { - return Headers.GetDate(HeaderNames.Date); - } - set - { - Headers.SetDate(HeaderNames.Date, value); - } + Headers.ContentLength = value; } + } - /// - /// Gets or sets the ETag header for an HTTP response. - /// - public EntityTagHeaderValue? ETag + /// + /// Gets or sets the Content-Range header for an HTTP response. + /// + public ContentRangeHeaderValue? ContentRange + { + get { - get - { - return Headers.Get(HeaderNames.ETag); - } - set - { - Headers.Set(HeaderNames.ETag, value); - } + return Headers.Get(HeaderNames.ContentRange); } - - /// - /// Gets or sets the Expires header for an HTTP response. - /// - public DateTimeOffset? Expires + set { - get - { - return Headers.GetDate(HeaderNames.Expires); - } - set - { - Headers.SetDate(HeaderNames.Expires, value); - } + Headers.Set(HeaderNames.ContentRange, value); } + } - /// - /// Gets or sets the Last-Modified header for an HTTP response. - /// - public DateTimeOffset? LastModified + /// + /// Gets or sets the Content-Type header for an HTTP response. + /// + public MediaTypeHeaderValue? ContentType + { + get { - get - { - return Headers.GetDate(HeaderNames.LastModified); - } - set - { - Headers.SetDate(HeaderNames.LastModified, value); - } + return Headers.Get(HeaderNames.ContentType); } + set + { + Headers.Set(HeaderNames.ContentType, value); + } + } - /// - /// Gets or sets the Location header for an HTTP response. - /// - public Uri? Location + /// + /// Gets or sets the Date header for an HTTP response. + /// + public DateTimeOffset? Date + { + get { - get - { - if (Uri.TryCreate(Headers.Location, UriKind.RelativeOrAbsolute, out var uri)) - { - return uri; - } - return null; - } - set - { - Headers.Set(HeaderNames.Location, value == null ? null : UriHelper.Encode(value)); - } + return Headers.GetDate(HeaderNames.Date); } + set + { + Headers.SetDate(HeaderNames.Date, value); + } + } - /// - /// Gets or sets the Set-Cookie header for an HTTP response. - /// - public IList SetCookie + /// + /// Gets or sets the ETag header for an HTTP response. + /// + public EntityTagHeaderValue? ETag + { + get { - get - { - return Headers.SetCookie.GetList(); - } - set - { - Headers.SetList(HeaderNames.SetCookie, value); - } + return Headers.Get(HeaderNames.ETag); } + set + { + Headers.Set(HeaderNames.ETag, value); + } + } - /// - /// Gets the value of header with . - /// - /// must contain a TryParse method with the signature public static bool TryParse(string, out T). - /// The type of the header. - /// The given type must have a static TryParse method. - /// The name of the header to retrieve. - /// The value of the header. - public T? Get(string name) + /// + /// Gets or sets the Expires header for an HTTP response. + /// + public DateTimeOffset? Expires + { + get + { + return Headers.GetDate(HeaderNames.Expires); + } + set { - return Headers.Get(name); + Headers.SetDate(HeaderNames.Expires, value); } + } - /// - /// Gets the values of header with . - /// - /// must contain a TryParseList method with the signature public static bool TryParseList(IList<string>, out IList<T>). - /// The type of the header. - /// The given type must have a static TryParseList method. - /// The name of the header to retrieve. - /// List of values of the header. - public IList GetList(string name) + /// + /// Gets or sets the Last-Modified header for an HTTP response. + /// + public DateTimeOffset? LastModified + { + get + { + return Headers.GetDate(HeaderNames.LastModified); + } + set { - return Headers.GetList(name); + Headers.SetDate(HeaderNames.LastModified, value); } + } - /// - /// Sets the header value. - /// - /// The header name. - /// The header value. - public void Set(string name, object? value) + /// + /// Gets or sets the Location header for an HTTP response. + /// + public Uri? Location + { + get { - if (name == null) + if (Uri.TryCreate(Headers.Location, UriKind.RelativeOrAbsolute, out var uri)) { - throw new ArgumentNullException(nameof(name)); + return uri; } - - Headers.Set(name, value); + return null; } + set + { + Headers.Set(HeaderNames.Location, value == null ? null : UriHelper.Encode(value)); + } + } - /// - /// Sets the specified header and it's values. - /// - /// The type of the value. - /// The header name. - /// The sequence of header values. - public void SetList(string name, IList? values) + /// + /// Gets or sets the Set-Cookie header for an HTTP response. + /// + public IList SetCookie + { + get { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } + return Headers.SetCookie.GetList(); + } + set + { + Headers.SetList(HeaderNames.SetCookie, value); + } + } + + /// + /// Gets the value of header with . + /// + /// must contain a TryParse method with the signature public static bool TryParse(string, out T). + /// The type of the header. + /// The given type must have a static TryParse method. + /// The name of the header to retrieve. + /// The value of the header. + public T? Get(string name) + { + return Headers.Get(name); + } - Headers.SetList(name, values); + /// + /// Gets the values of header with . + /// + /// must contain a TryParseList method with the signature public static bool TryParseList(IList<string>, out IList<T>). + /// The type of the header. + /// The given type must have a static TryParseList method. + /// The name of the header to retrieve. + /// List of values of the header. + public IList GetList(string name) + { + return Headers.GetList(name); + } + + /// + /// Sets the header value. + /// + /// The header name. + /// The header value. + public void Set(string name, object? value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); } - /// - /// Appends the header name and value. - /// - /// The header name. - /// The header value. - public void Append(string name, object value) + Headers.Set(name, value); + } + + /// + /// Sets the specified header and it's values. + /// + /// The type of the value. + /// The header name. + /// The sequence of header values. + public void SetList(string name, IList? values) + { + if (name == null) { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } + throw new ArgumentNullException(nameof(name)); + } - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } + Headers.SetList(name, values); + } - Headers.Append(name, value.ToString()); + /// + /// Appends the header name and value. + /// + /// The header name. + /// The header value. + public void Append(string name, object value) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); } - /// - /// Appends the header name and it's values. - /// - /// The header name. - /// The header values. - public void AppendList(string name, IList values) + if (value == null) { - Headers.AppendList(name, values); + throw new ArgumentNullException(nameof(value)); } + + Headers.Append(name, value.ToString()); + } + + /// + /// Appends the header name and it's values. + /// + /// The header name. + /// The header values. + public void AppendList(string name, IList values) + { + Headers.AppendList(name, values); } } diff --git a/src/Http/Http.Extensions/src/SendFileResponseExtensions.cs b/src/Http/Http.Extensions/src/SendFileResponseExtensions.cs index 65e16a7feb..fcdb50aa40 100644 --- a/src/Http/Http.Extensions/src/SendFileResponseExtensions.cs +++ b/src/Http/Http.Extensions/src/SendFileResponseExtensions.cs @@ -10,159 +10,158 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.FileProviders; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Provides extensions for HttpResponse exposing the SendFile extension. +/// +public static class SendFileResponseExtensions { + private const int StreamCopyBufferSize = 64 * 1024; + /// - /// Provides extensions for HttpResponse exposing the SendFile extension. + /// Sends the given file using the SendFile extension. /// - public static class SendFileResponseExtensions + /// + /// The file. + /// The . + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static Task SendFileAsync(this HttpResponse response, IFileInfo file, CancellationToken cancellationToken = default) { - private const int StreamCopyBufferSize = 64 * 1024; - - /// - /// Sends the given file using the SendFile extension. - /// - /// - /// The file. - /// The . - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static Task SendFileAsync(this HttpResponse response, IFileInfo file, CancellationToken cancellationToken = default) + if (response == null) { - if (response == null) - { - throw new ArgumentNullException(nameof(response)); - } - if (file == null) - { - throw new ArgumentNullException(nameof(file)); - } - - return SendFileAsyncCore(response, file, 0, null, cancellationToken); + throw new ArgumentNullException(nameof(response)); } - - /// - /// Sends the given file using the SendFile extension. - /// - /// - /// The file. - /// The offset in the file. - /// The number of bytes to send, or null to send the remainder of the file. - /// - /// - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static Task SendFileAsync(this HttpResponse response, IFileInfo file, long offset, long? count, CancellationToken cancellationToken = default) + if (file == null) { - if (response == null) - { - throw new ArgumentNullException(nameof(response)); - } - if (file == null) - { - throw new ArgumentNullException(nameof(file)); - } - - return SendFileAsyncCore(response, file, offset, count, cancellationToken); + throw new ArgumentNullException(nameof(file)); } - /// - /// Sends the given file using the SendFile extension. - /// - /// - /// The full path to the file. - /// The . - /// - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static Task SendFileAsync(this HttpResponse response, string fileName, CancellationToken cancellationToken = default) + return SendFileAsyncCore(response, file, 0, null, cancellationToken); + } + + /// + /// Sends the given file using the SendFile extension. + /// + /// + /// The file. + /// The offset in the file. + /// The number of bytes to send, or null to send the remainder of the file. + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static Task SendFileAsync(this HttpResponse response, IFileInfo file, long offset, long? count, CancellationToken cancellationToken = default) + { + if (response == null) { - if (response == null) - { - throw new ArgumentNullException(nameof(response)); - } + throw new ArgumentNullException(nameof(response)); + } + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } - if (fileName == null) - { - throw new ArgumentNullException(nameof(fileName)); - } + return SendFileAsyncCore(response, file, offset, count, cancellationToken); + } - return SendFileAsyncCore(response, fileName, 0, null, cancellationToken); + /// + /// Sends the given file using the SendFile extension. + /// + /// + /// The full path to the file. + /// The . + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static Task SendFileAsync(this HttpResponse response, string fileName, CancellationToken cancellationToken = default) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); } - /// - /// Sends the given file using the SendFile extension. - /// - /// - /// The full path to the file. - /// The offset in the file. - /// The number of bytes to send, or null to send the remainder of the file. - /// - /// - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static Task SendFileAsync(this HttpResponse response, string fileName, long offset, long? count, CancellationToken cancellationToken = default) + if (fileName == null) { - if (response == null) - { - throw new ArgumentNullException(nameof(response)); - } + throw new ArgumentNullException(nameof(fileName)); + } - if (fileName == null) - { - throw new ArgumentNullException(nameof(fileName)); - } + return SendFileAsyncCore(response, fileName, 0, null, cancellationToken); + } - return SendFileAsyncCore(response, fileName, offset, count, cancellationToken); + /// + /// Sends the given file using the SendFile extension. + /// + /// + /// The full path to the file. + /// The offset in the file. + /// The number of bytes to send, or null to send the remainder of the file. + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static Task SendFileAsync(this HttpResponse response, string fileName, long offset, long? count, CancellationToken cancellationToken = default) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); } - private static async Task SendFileAsyncCore(HttpResponse response, IFileInfo file, long offset, long? count, CancellationToken cancellationToken) + if (fileName == null) { - if (string.IsNullOrEmpty(file.PhysicalPath)) - { - CheckRange(offset, count, file.Length); - await using var fileContent = file.CreateReadStream(); - - var useRequestAborted = !cancellationToken.CanBeCanceled; - var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken; - - try - { - localCancel.ThrowIfCancellationRequested(); - if (offset > 0) - { - fileContent.Seek(offset, SeekOrigin.Begin); - } - await StreamCopyOperation.CopyToAsync(fileContent, response.Body, count, StreamCopyBufferSize, localCancel); - } - catch (OperationCanceledException) when (useRequestAborted) { } - } - else - { - await response.SendFileAsync(file.PhysicalPath, offset, count, cancellationToken); - } + throw new ArgumentNullException(nameof(fileName)); } - private static async Task SendFileAsyncCore(HttpResponse response, string fileName, long offset, long? count, CancellationToken cancellationToken = default) + return SendFileAsyncCore(response, fileName, offset, count, cancellationToken); + } + + private static async Task SendFileAsyncCore(HttpResponse response, IFileInfo file, long offset, long? count, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(file.PhysicalPath)) { + CheckRange(offset, count, file.Length); + await using var fileContent = file.CreateReadStream(); + var useRequestAborted = !cancellationToken.CanBeCanceled; var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken; - var sendFile = response.HttpContext.Features.Get()!; try { - await sendFile.SendFileAsync(fileName, offset, count, localCancel); + localCancel.ThrowIfCancellationRequested(); + if (offset > 0) + { + fileContent.Seek(offset, SeekOrigin.Begin); + } + await StreamCopyOperation.CopyToAsync(fileContent, response.Body, count, StreamCopyBufferSize, localCancel); } catch (OperationCanceledException) when (useRequestAborted) { } } + else + { + await response.SendFileAsync(file.PhysicalPath, offset, count, cancellationToken); + } + } - private static void CheckRange(long offset, long? count, long fileLength) + private static async Task SendFileAsyncCore(HttpResponse response, string fileName, long offset, long? count, CancellationToken cancellationToken = default) + { + var useRequestAborted = !cancellationToken.CanBeCanceled; + var localCancel = useRequestAborted ? response.HttpContext.RequestAborted : cancellationToken; + var sendFile = response.HttpContext.Features.Get()!; + + try { - if (offset < 0 || offset > fileLength) - { - throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty); - } - if (count.HasValue && - (count.GetValueOrDefault() < 0 || count.GetValueOrDefault() > fileLength - offset)) - { - throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty); - } + await sendFile.SendFileAsync(fileName, offset, count, localCancel); + } + catch (OperationCanceledException) when (useRequestAborted) { } + } + + private static void CheckRange(long offset, long? count, long fileLength) + { + if (offset < 0 || offset > fileLength) + { + throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty); + } + if (count.HasValue && + (count.GetValueOrDefault() < 0 || count.GetValueOrDefault() > fileLength - offset)) + { + throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty); } } } diff --git a/src/Http/Http.Extensions/src/SessionExtensions.cs b/src/Http/Http.Extensions/src/SessionExtensions.cs index f9ecc61a97..81c4d07d69 100644 --- a/src/Http/Http.Extensions/src/SessionExtensions.cs +++ b/src/Http/Http.Extensions/src/SessionExtensions.cs @@ -3,81 +3,80 @@ using System.Text; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Extension methods for . +/// +public static class SessionExtensions { /// - /// Extension methods for . + /// Sets an int value in the . /// - public static class SessionExtensions + /// The . + /// The key to assign. + /// The value to assign. + public static void SetInt32(this ISession session, string key, int value) { - /// - /// Sets an int value in the . - /// - /// The . - /// The key to assign. - /// The value to assign. - public static void SetInt32(this ISession session, string key, int value) + var bytes = new byte[] { - var bytes = new byte[] - { (byte)(value >> 24), (byte)(0xFF & (value >> 16)), (byte)(0xFF & (value >> 8)), (byte)(0xFF & value) - }; - session.Set(key, bytes); - } + }; + session.Set(key, bytes); + } - /// - /// Gets an int value from . - /// - /// The . - /// The key to read. - public static int? GetInt32(this ISession session, string key) + /// + /// Gets an int value from . + /// + /// The . + /// The key to read. + public static int? GetInt32(this ISession session, string key) + { + var data = session.Get(key); + if (data == null || data.Length < 4) { - var data = session.Get(key); - if (data == null || data.Length < 4) - { - return null; - } - return data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]; + return null; } + return data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]; + } - /// - /// Sets a value in the . - /// - /// The . - /// The key to assign. - /// The value to assign. - public static void SetString(this ISession session, string key, string value) - { - session.Set(key, Encoding.UTF8.GetBytes(value)); - } + /// + /// Sets a value in the . + /// + /// The . + /// The key to assign. + /// The value to assign. + public static void SetString(this ISession session, string key, string value) + { + session.Set(key, Encoding.UTF8.GetBytes(value)); + } - /// - /// Gets a string value from . - /// - /// The . - /// The key to read. - public static string? GetString(this ISession session, string key) + /// + /// Gets a string value from . + /// + /// The . + /// The key to read. + public static string? GetString(this ISession session, string key) + { + var data = session.Get(key); + if (data == null) { - var data = session.Get(key); - if (data == null) - { - return null; - } - return Encoding.UTF8.GetString(data); + return null; } + return Encoding.UTF8.GetString(data); + } - /// - /// Gets a byte-array value from . - /// - /// The . - /// The key to read. - public static byte[]? Get(this ISession session, string key) - { - session.TryGetValue(key, out var value); - return value; - } + /// + /// Gets a byte-array value from . + /// + /// The . + /// The key to read. + public static byte[]? Get(this ISession session, string key) + { + session.TryGetValue(key, out var value); + return value; } } diff --git a/src/Http/Http.Extensions/src/StreamCopyOperation.cs b/src/Http/Http.Extensions/src/StreamCopyOperation.cs index b681489471..402c96f8de 100644 --- a/src/Http/Http.Extensions/src/StreamCopyOperation.cs +++ b/src/Http/Http.Extensions/src/StreamCopyOperation.cs @@ -5,31 +5,30 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http.Extensions +namespace Microsoft.AspNetCore.Http.Extensions; + +// FYI: In most cases the source will be a FileStream and the destination will be to the network. +/// +/// Provides APIs to copy a range of bytes from a source to a destination . +/// +public static class StreamCopyOperation { - // FYI: In most cases the source will be a FileStream and the destination will be to the network. - /// - /// Provides APIs to copy a range of bytes from a source to a destination . - /// - public static class StreamCopyOperation - { - /// Asynchronously reads the given number of bytes from the source stream and writes them to another stream. - /// A task that represents the asynchronous copy operation. - /// The stream from which the contents will be copied. - /// The stream to which the contents of the current stream will be copied. - /// The count of bytes to be copied. - /// The token to monitor for cancellation requests. The default value is . - public static Task CopyToAsync(Stream source, Stream destination, long? count, CancellationToken cancel) - => StreamCopyOperationInternal.CopyToAsync(source, destination, count, cancel); + /// Asynchronously reads the given number of bytes from the source stream and writes them to another stream. + /// A task that represents the asynchronous copy operation. + /// The stream from which the contents will be copied. + /// The stream to which the contents of the current stream will be copied. + /// The count of bytes to be copied. + /// The token to monitor for cancellation requests. The default value is . + public static Task CopyToAsync(Stream source, Stream destination, long? count, CancellationToken cancel) + => StreamCopyOperationInternal.CopyToAsync(source, destination, count, cancel); - /// Asynchronously reads the given number of bytes from the source stream and writes them to another stream, using a specified buffer size. - /// A task that represents the asynchronous copy operation. - /// The stream from which the contents will be copied. - /// The stream to which the contents of the current stream will be copied. - /// The count of bytes to be copied. - /// The size, in bytes, of the buffer. This value must be greater than zero. The default size is 4096. - /// The token to monitor for cancellation requests. The default value is . - public static Task CopyToAsync(Stream source, Stream destination, long? count, int bufferSize, CancellationToken cancel) - => StreamCopyOperationInternal.CopyToAsync(source, destination, count, bufferSize, cancel); - } + /// Asynchronously reads the given number of bytes from the source stream and writes them to another stream, using a specified buffer size. + /// A task that represents the asynchronous copy operation. + /// The stream from which the contents will be copied. + /// The stream to which the contents of the current stream will be copied. + /// The count of bytes to be copied. + /// The size, in bytes, of the buffer. This value must be greater than zero. The default size is 4096. + /// The token to monitor for cancellation requests. The default value is . + public static Task CopyToAsync(Stream source, Stream destination, long? count, int bufferSize, CancellationToken cancel) + => StreamCopyOperationInternal.CopyToAsync(source, destination, count, bufferSize, cancel); } diff --git a/src/Http/Http.Extensions/src/TagsAttribute.cs b/src/Http/Http.Extensions/src/TagsAttribute.cs index 891bf67084..f4d823dd14 100644 --- a/src/Http/Http.Extensions/src/TagsAttribute.cs +++ b/src/Http/Http.Extensions/src/TagsAttribute.cs @@ -4,31 +4,30 @@ using System; using Microsoft.AspNetCore.Http.Metadata; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Specifies a collection of tags in . +/// +/// +/// The OpenAPI specification supports a tags classification to categorize operations +/// into related groups. These tags are typically included in the generated specification +/// and are typically used to group operations by tags in the UI. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate | AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public sealed class TagsAttribute : Attribute, ITagsMetadata { /// - /// Specifies a collection of tags in . + /// Initializes an instance of the . /// - /// - /// The OpenAPI specification supports a tags classification to categorize operations - /// into related groups. These tags are typically included in the generated specification - /// and are typically used to group operations by tags in the UI. - /// - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate | AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class TagsAttribute : Attribute, ITagsMetadata + /// The tags associated with the endpoint. + public TagsAttribute(params string[] tags) { - /// - /// Initializes an instance of the . - /// - /// The tags associated with the endpoint. - public TagsAttribute(params string[] tags) - { - Tags = new List(tags); - } - - /// - /// Gets the collection of tags associated with the endpoint. - /// - public IReadOnlyList Tags { get; } + Tags = new List(tags); } + + /// + /// Gets the collection of tags associated with the endpoint. + /// + public IReadOnlyList Tags { get; } } diff --git a/src/Http/Http.Extensions/src/UriHelper.cs b/src/Http/Http.Extensions/src/UriHelper.cs index 16a921dfec..5a9bf46631 100644 --- a/src/Http/Http.Extensions/src/UriHelper.cs +++ b/src/Http/Http.Extensions/src/UriHelper.cs @@ -6,273 +6,272 @@ using System.Buffers; using System.Runtime.CompilerServices; using System.Text; -namespace Microsoft.AspNetCore.Http.Extensions +namespace Microsoft.AspNetCore.Http.Extensions; + +/// +/// A helper class for constructing encoded Uris for use in headers and other Uris. +/// +public static class UriHelper { + private const char ForwardSlash = '/'; + private const char Hash = '#'; + private const char QuestionMark = '?'; + private static readonly string SchemeDelimiter = Uri.SchemeDelimiter; + private static readonly SpanAction InitializeAbsoluteUriStringSpanAction = new(InitializeAbsoluteUriString); + /// - /// A helper class for constructing encoded Uris for use in headers and other Uris. + /// Combines the given URI components into a string that is properly encoded for use in HTTP headers. /// - public static class UriHelper + /// The first portion of the request path associated with application root. + /// The portion of the request path that identifies the requested resource. + /// The query, if any. + /// The fragment, if any. + /// The combined URI components, properly encoded for use in HTTP headers. + public static string BuildRelative( + PathString pathBase = new PathString(), + PathString path = new PathString(), + QueryString query = new QueryString(), + FragmentString fragment = new FragmentString()) { - private const char ForwardSlash = '/'; - private const char Hash = '#'; - private const char QuestionMark = '?'; - private static readonly string SchemeDelimiter = Uri.SchemeDelimiter; - private static readonly SpanAction InitializeAbsoluteUriStringSpanAction = new(InitializeAbsoluteUriString); + string combinePath = (pathBase.HasValue || path.HasValue) ? (pathBase + path).ToString() : "/"; + return combinePath + query.ToString() + fragment.ToString(); + } - /// - /// Combines the given URI components into a string that is properly encoded for use in HTTP headers. - /// - /// The first portion of the request path associated with application root. - /// The portion of the request path that identifies the requested resource. - /// The query, if any. - /// The fragment, if any. - /// The combined URI components, properly encoded for use in HTTP headers. - public static string BuildRelative( - PathString pathBase = new PathString(), - PathString path = new PathString(), - QueryString query = new QueryString(), - FragmentString fragment = new FragmentString()) + /// + /// Combines the given URI components into a string that is properly encoded for use in HTTP headers. + /// Note that unicode in the HostString will be encoded as punycode. + /// + /// http, https, etc. + /// The host portion of the uri normally included in the Host header. This may include the port. + /// The first portion of the request path associated with application root. + /// The portion of the request path that identifies the requested resource. + /// The query, if any. + /// The fragment, if any. + /// The combined URI components, properly encoded for use in HTTP headers. + public static string BuildAbsolute( + string scheme, + HostString host, + PathString pathBase = new PathString(), + PathString path = new PathString(), + QueryString query = new QueryString(), + FragmentString fragment = new FragmentString()) + { + if (scheme == null) { - string combinePath = (pathBase.HasValue || path.HasValue) ? (pathBase + path).ToString() : "/"; - return combinePath + query.ToString() + fragment.ToString(); + throw new ArgumentNullException(nameof(scheme)); } - /// - /// Combines the given URI components into a string that is properly encoded for use in HTTP headers. - /// Note that unicode in the HostString will be encoded as punycode. - /// - /// http, https, etc. - /// The host portion of the uri normally included in the Host header. This may include the port. - /// The first portion of the request path associated with application root. - /// The portion of the request path that identifies the requested resource. - /// The query, if any. - /// The fragment, if any. - /// The combined URI components, properly encoded for use in HTTP headers. - public static string BuildAbsolute( - string scheme, - HostString host, - PathString pathBase = new PathString(), - PathString path = new PathString(), - QueryString query = new QueryString(), - FragmentString fragment = new FragmentString()) - { - if (scheme == null) - { - throw new ArgumentNullException(nameof(scheme)); - } - - var hostText = host.ToUriComponent(); - var pathBaseText = pathBase.ToUriComponent(); - var pathText = path.ToUriComponent(); - var queryText = query.ToUriComponent(); - var fragmentText = fragment.ToUriComponent(); + var hostText = host.ToUriComponent(); + var pathBaseText = pathBase.ToUriComponent(); + var pathText = path.ToUriComponent(); + var queryText = query.ToUriComponent(); + var fragmentText = fragment.ToUriComponent(); - // PERF: Calculate string length to allocate correct buffer size for string.Create. - var length = - scheme.Length + - Uri.SchemeDelimiter.Length + - hostText.Length + - pathBaseText.Length + - pathText.Length + - queryText.Length + - fragmentText.Length; + // PERF: Calculate string length to allocate correct buffer size for string.Create. + var length = + scheme.Length + + Uri.SchemeDelimiter.Length + + hostText.Length + + pathBaseText.Length + + pathText.Length + + queryText.Length + + fragmentText.Length; - if (string.IsNullOrEmpty(pathText)) - { - if (string.IsNullOrEmpty(pathBaseText)) - { - pathText = "/"; - length++; - } - } - else if (pathBaseText.EndsWith('/')) + if (string.IsNullOrEmpty(pathText)) + { + if (string.IsNullOrEmpty(pathBaseText)) { - // If the path string has a trailing slash and the other string has a leading slash, we need - // to trim one of them. - // Just decrement the total length, for now. - length--; + pathText = "/"; + length++; } - - return string.Create(length, (scheme, hostText, pathBaseText, pathText, queryText, fragmentText), InitializeAbsoluteUriStringSpanAction); } - - /// - /// Separates the given absolute URI string into components. Assumes no PathBase. - /// - /// A string representation of the uri. - /// http, https, etc. - /// The host portion of the uri normally included in the Host header. This may include the port. - /// The portion of the request path that identifies the requested resource. - /// The query, if any. - /// The fragment, if any. - public static void FromAbsolute( - string uri, - out string scheme, - out HostString host, - out PathString path, - out QueryString query, - out FragmentString fragment) + else if (pathBaseText.EndsWith('/')) { - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } + // If the path string has a trailing slash and the other string has a leading slash, we need + // to trim one of them. + // Just decrement the total length, for now. + length--; + } - path = new PathString(); - query = new QueryString(); - fragment = new FragmentString(); - var startIndex = uri.IndexOf(SchemeDelimiter, StringComparison.Ordinal); + return string.Create(length, (scheme, hostText, pathBaseText, pathText, queryText, fragmentText), InitializeAbsoluteUriStringSpanAction); + } - if (startIndex < 0) - { - throw new FormatException("No scheme delimiter in uri."); - } + /// + /// Separates the given absolute URI string into components. Assumes no PathBase. + /// + /// A string representation of the uri. + /// http, https, etc. + /// The host portion of the uri normally included in the Host header. This may include the port. + /// The portion of the request path that identifies the requested resource. + /// The query, if any. + /// The fragment, if any. + public static void FromAbsolute( + string uri, + out string scheme, + out HostString host, + out PathString path, + out QueryString query, + out FragmentString fragment) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); + } - scheme = uri.Substring(0, startIndex); + path = new PathString(); + query = new QueryString(); + fragment = new FragmentString(); + var startIndex = uri.IndexOf(SchemeDelimiter, StringComparison.Ordinal); - // PERF: Calculate the end of the scheme for next IndexOf - startIndex += SchemeDelimiter.Length; + if (startIndex < 0) + { + throw new FormatException("No scheme delimiter in uri."); + } - int searchIndex; - var limit = uri.Length; - if ((searchIndex = uri.IndexOf(Hash, startIndex)) >= 0 && searchIndex < limit) - { - fragment = FragmentString.FromUriComponent(uri.Substring(searchIndex)); - limit = searchIndex; - } + scheme = uri.Substring(0, startIndex); - if ((searchIndex = uri.IndexOf(QuestionMark, startIndex)) >= 0 && searchIndex < limit) - { - query = QueryString.FromUriComponent(uri.Substring(searchIndex, limit - searchIndex)); - limit = searchIndex; - } + // PERF: Calculate the end of the scheme for next IndexOf + startIndex += SchemeDelimiter.Length; - if ((searchIndex = uri.IndexOf(ForwardSlash, startIndex)) >= 0 && searchIndex < limit) - { - path = PathString.FromUriComponent(uri.Substring(searchIndex, limit - searchIndex)); - limit = searchIndex; - } + int searchIndex; + var limit = uri.Length; + if ((searchIndex = uri.IndexOf(Hash, startIndex)) >= 0 && searchIndex < limit) + { + fragment = FragmentString.FromUriComponent(uri.Substring(searchIndex)); + limit = searchIndex; + } - host = HostString.FromUriComponent(uri.Substring(startIndex, limit - startIndex)); + if ((searchIndex = uri.IndexOf(QuestionMark, startIndex)) >= 0 && searchIndex < limit) + { + query = QueryString.FromUriComponent(uri.Substring(searchIndex, limit - searchIndex)); + limit = searchIndex; } - /// - /// Generates a string from the given absolute or relative Uri that is appropriately encoded for use in - /// HTTP headers. Note that a unicode host name will be encoded as punycode. - /// - /// The Uri to encode. - /// The encoded string version of . - public static string Encode(Uri uri) + if ((searchIndex = uri.IndexOf(ForwardSlash, startIndex)) >= 0 && searchIndex < limit) { - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } + path = PathString.FromUriComponent(uri.Substring(searchIndex, limit - searchIndex)); + limit = searchIndex; + } - if (uri.IsAbsoluteUri) - { - return BuildAbsolute( - scheme: uri.Scheme, - host: HostString.FromUriComponent(uri), - pathBase: PathString.FromUriComponent(uri), - query: QueryString.FromUriComponent(uri), - fragment: FragmentString.FromUriComponent(uri)); - } - else - { - return uri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); - } + host = HostString.FromUriComponent(uri.Substring(startIndex, limit - startIndex)); + } + + /// + /// Generates a string from the given absolute or relative Uri that is appropriately encoded for use in + /// HTTP headers. Note that a unicode host name will be encoded as punycode. + /// + /// The Uri to encode. + /// The encoded string version of . + public static string Encode(Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); } - /// - /// Returns the combined components of the request URL in a fully escaped form suitable for use in HTTP headers - /// and other HTTP operations. - /// - /// The request to assemble the uri pieces from. - /// The encoded string version of the URL from . - public static string GetEncodedUrl(this HttpRequest request) + if (uri.IsAbsoluteUri) { - return BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path, request.QueryString); + return BuildAbsolute( + scheme: uri.Scheme, + host: HostString.FromUriComponent(uri), + pathBase: PathString.FromUriComponent(uri), + query: QueryString.FromUriComponent(uri), + fragment: FragmentString.FromUriComponent(uri)); } - /// - /// Returns the relative URI. - /// - /// The request to assemble the uri pieces from. - /// The path and query off of . - public static string GetEncodedPathAndQuery(this HttpRequest request) + else { - return BuildRelative(request.PathBase, request.Path, request.QueryString); + return uri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); } + } - /// - /// Returns the combined components of the request URL in a fully un-escaped form (except for the QueryString) - /// suitable only for display. This format should not be used in HTTP headers or other HTTP operations. - /// - /// The request to assemble the uri pieces from. - /// The combined components of the request URL in a fully un-escaped form (except for the QueryString) - /// suitable only for display. - public static string GetDisplayUrl(this HttpRequest request) - { - var scheme = request.Scheme ?? string.Empty; - var host = request.Host.Value ?? string.Empty; - var pathBase = request.PathBase.Value ?? string.Empty; - var path = request.Path.Value ?? string.Empty; - var queryString = request.QueryString.Value ?? string.Empty; + /// + /// Returns the combined components of the request URL in a fully escaped form suitable for use in HTTP headers + /// and other HTTP operations. + /// + /// The request to assemble the uri pieces from. + /// The encoded string version of the URL from . + public static string GetEncodedUrl(this HttpRequest request) + { + return BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path, request.QueryString); + } + /// + /// Returns the relative URI. + /// + /// The request to assemble the uri pieces from. + /// The path and query off of . + public static string GetEncodedPathAndQuery(this HttpRequest request) + { + return BuildRelative(request.PathBase, request.Path, request.QueryString); + } - // PERF: Calculate string length to allocate correct buffer size for StringBuilder. - var length = scheme.Length + SchemeDelimiter.Length + host.Length - + pathBase.Length + path.Length + queryString.Length; + /// + /// Returns the combined components of the request URL in a fully un-escaped form (except for the QueryString) + /// suitable only for display. This format should not be used in HTTP headers or other HTTP operations. + /// + /// The request to assemble the uri pieces from. + /// The combined components of the request URL in a fully un-escaped form (except for the QueryString) + /// suitable only for display. + public static string GetDisplayUrl(this HttpRequest request) + { + var scheme = request.Scheme ?? string.Empty; + var host = request.Host.Value ?? string.Empty; + var pathBase = request.PathBase.Value ?? string.Empty; + var path = request.Path.Value ?? string.Empty; + var queryString = request.QueryString.Value ?? string.Empty; - return new StringBuilder(length) - .Append(scheme) - .Append(SchemeDelimiter) - .Append(host) - .Append(pathBase) - .Append(path) - .Append(queryString) - .ToString(); - } + // PERF: Calculate string length to allocate correct buffer size for StringBuilder. + var length = scheme.Length + SchemeDelimiter.Length + host.Length + + pathBase.Length + path.Length + queryString.Length; - /// - /// Copies the specified to the specified starting at the specified . - /// - /// The buffer to copy text to. - /// The buffer start index. - /// The text to copy. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int CopyTextToBuffer(Span buffer, int index, ReadOnlySpan text) - { - text.CopyTo(buffer.Slice(index, text.Length)); - return index + text.Length; - } + return new StringBuilder(length) + .Append(scheme) + .Append(SchemeDelimiter) + .Append(host) + .Append(pathBase) + .Append(path) + .Append(queryString) + .ToString(); + } - /// - /// Initializes the URI for . - /// - /// The URI 's buffer. - /// The URI parts. - private static void InitializeAbsoluteUriString(Span buffer, (string scheme, string host, string pathBase, string path, string query, string fragment) uriParts) - { - var index = 0; + /// + /// Copies the specified to the specified starting at the specified . + /// + /// The buffer to copy text to. + /// The buffer start index. + /// The text to copy. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CopyTextToBuffer(Span buffer, int index, ReadOnlySpan text) + { + text.CopyTo(buffer.Slice(index, text.Length)); + return index + text.Length; + } - var pathBaseSpan = uriParts.pathBase.AsSpan(); + /// + /// Initializes the URI for . + /// + /// The URI 's buffer. + /// The URI parts. + private static void InitializeAbsoluteUriString(Span buffer, (string scheme, string host, string pathBase, string path, string query, string fragment) uriParts) + { + var index = 0; - if (uriParts.path.Length > 0 && pathBaseSpan.Length > 0 && pathBaseSpan[^1] == '/') - { - // If the path string has a trailing slash and the other string has a leading slash, we need - // to trim one of them. - // Trim the last slahs from pathBase. The total length was decremented before the call to string.Create. - pathBaseSpan = pathBaseSpan[..^1]; - } + var pathBaseSpan = uriParts.pathBase.AsSpan(); - index = CopyTextToBuffer(buffer, index, uriParts.scheme.AsSpan()); - index = CopyTextToBuffer(buffer, index, Uri.SchemeDelimiter.AsSpan()); - index = CopyTextToBuffer(buffer, index, uriParts.host.AsSpan()); - index = CopyTextToBuffer(buffer, index, pathBaseSpan); - index = CopyTextToBuffer(buffer, index, uriParts.path.AsSpan()); - index = CopyTextToBuffer(buffer, index, uriParts.query.AsSpan()); - _ = CopyTextToBuffer(buffer, index, uriParts.fragment.AsSpan()); + if (uriParts.path.Length > 0 && pathBaseSpan.Length > 0 && pathBaseSpan[^1] == '/') + { + // If the path string has a trailing slash and the other string has a leading slash, we need + // to trim one of them. + // Trim the last slahs from pathBase. The total length was decremented before the call to string.Create. + pathBaseSpan = pathBaseSpan[..^1]; } + + index = CopyTextToBuffer(buffer, index, uriParts.scheme.AsSpan()); + index = CopyTextToBuffer(buffer, index, Uri.SchemeDelimiter.AsSpan()); + index = CopyTextToBuffer(buffer, index, uriParts.host.AsSpan()); + index = CopyTextToBuffer(buffer, index, pathBaseSpan); + index = CopyTextToBuffer(buffer, index, uriParts.path.AsSpan()); + index = CopyTextToBuffer(buffer, index, uriParts.query.AsSpan()); + _ = CopyTextToBuffer(buffer, index, uriParts.fragment.AsSpan()); } } diff --git a/src/Http/Http.Extensions/test/HeaderDictionaryTypeExtensionsTest.cs b/src/Http/Http.Extensions/test/HeaderDictionaryTypeExtensionsTest.cs index e413a49d21..7019546926 100644 --- a/src/Http/Http.Extensions/test/HeaderDictionaryTypeExtensionsTest.cs +++ b/src/Http/Http.Extensions/test/HeaderDictionaryTypeExtensionsTest.cs @@ -7,199 +7,198 @@ using System.Linq; using Microsoft.Net.Http.Headers; using Xunit; -namespace Microsoft.AspNetCore.Http.Headers +namespace Microsoft.AspNetCore.Http.Headers; + +public class HeaderDictionaryTypeExtensionsTest { - public class HeaderDictionaryTypeExtensionsTest + [Fact] + public void GetT_KnownTypeWithValidValue_Success() { - [Fact] - public void GetT_KnownTypeWithValidValue_Success() - { - var context = new DefaultHttpContext(); - context.Request.Headers.ContentType = "text/plain"; + var context = new DefaultHttpContext(); + context.Request.Headers.ContentType = "text/plain"; - var result = context.Request.GetTypedHeaders().Get(HeaderNames.ContentType); + var result = context.Request.GetTypedHeaders().Get(HeaderNames.ContentType); - var expected = new MediaTypeHeaderValue("text/plain"); - Assert.Equal(expected, result); - } + var expected = new MediaTypeHeaderValue("text/plain"); + Assert.Equal(expected, result); + } - [Fact] - public void GetT_KnownTypeWithMissingValue_Null() - { - var context = new DefaultHttpContext(); + [Fact] + public void GetT_KnownTypeWithMissingValue_Null() + { + var context = new DefaultHttpContext(); - var result = context.Request.GetTypedHeaders().Get(HeaderNames.ContentType); + var result = context.Request.GetTypedHeaders().Get(HeaderNames.ContentType); - Assert.Null(result); - } + Assert.Null(result); + } - [Fact] - public void GetT_KnownTypeWithInvalidValue_Null() - { - var context = new DefaultHttpContext(); - context.Request.Headers.ContentType = "invalid"; + [Fact] + public void GetT_KnownTypeWithInvalidValue_Null() + { + var context = new DefaultHttpContext(); + context.Request.Headers.ContentType = "invalid"; - var result = context.Request.GetTypedHeaders().Get(HeaderNames.ContentType); + var result = context.Request.GetTypedHeaders().Get(HeaderNames.ContentType); - Assert.Null(result); - } + Assert.Null(result); + } - [Fact] - public void GetT_UnknownTypeWithTryParseAndValidValue_Success() - { - var context = new DefaultHttpContext(); - context.Request.Headers["custom"] = "valid"; + [Fact] + public void GetT_UnknownTypeWithTryParseAndValidValue_Success() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "valid"; - var result = context.Request.GetTypedHeaders().Get("custom"); - Assert.NotNull(result); - } + var result = context.Request.GetTypedHeaders().Get("custom"); + Assert.NotNull(result); + } - [Fact] - public void GetT_UnknownTypeWithTryParseAndInvalidValue_Null() - { - var context = new DefaultHttpContext(); - context.Request.Headers["custom"] = "invalid"; + [Fact] + public void GetT_UnknownTypeWithTryParseAndInvalidValue_Null() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "invalid"; - var result = context.Request.GetTypedHeaders().Get("custom"); - Assert.Null(result); - } + var result = context.Request.GetTypedHeaders().Get("custom"); + Assert.Null(result); + } - [Fact] - public void GetT_UnknownTypeWithTryParseAndMissingValue_Null() - { - var context = new DefaultHttpContext(); + [Fact] + public void GetT_UnknownTypeWithTryParseAndMissingValue_Null() + { + var context = new DefaultHttpContext(); - var result = context.Request.GetTypedHeaders().Get("custom"); - Assert.Null(result); - } + var result = context.Request.GetTypedHeaders().Get("custom"); + Assert.Null(result); + } - [Fact] - public void GetT_UnknownTypeWithoutTryParse_Throws() - { - var context = new DefaultHttpContext(); - context.Request.Headers["custom"] = "valid"; + [Fact] + public void GetT_UnknownTypeWithoutTryParse_Throws() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "valid"; - Assert.Throws(() => context.Request.GetTypedHeaders().Get("custom")); - } + Assert.Throws(() => context.Request.GetTypedHeaders().Get("custom")); + } - [Fact] - public void GetListT_KnownTypeWithValidValue_Success() - { - var context = new DefaultHttpContext(); - context.Request.Headers.Accept = "text/plain; q=0.9, text/other, */*"; + [Fact] + public void GetListT_KnownTypeWithValidValue_Success() + { + var context = new DefaultHttpContext(); + context.Request.Headers.Accept = "text/plain; q=0.9, text/other, */*"; - var result = context.Request.GetTypedHeaders().GetList(HeaderNames.Accept); + var result = context.Request.GetTypedHeaders().GetList(HeaderNames.Accept); - var expected = new[] { + var expected = new[] { new MediaTypeHeaderValue("text/plain", 0.9), new MediaTypeHeaderValue("text/other"), new MediaTypeHeaderValue("*/*"), }.ToList(); - Assert.Equal(expected, result); - } + Assert.Equal(expected, result); + } - [Fact] - public void GetListT_KnownTypeWithMissingValue_EmptyList() - { - var context = new DefaultHttpContext(); + [Fact] + public void GetListT_KnownTypeWithMissingValue_EmptyList() + { + var context = new DefaultHttpContext(); - var result = context.Request.GetTypedHeaders().GetList(HeaderNames.Accept); + var result = context.Request.GetTypedHeaders().GetList(HeaderNames.Accept); - Assert.Empty(result); - } + Assert.Empty(result); + } - [Fact] - public void GetListT_KnownTypeWithInvalidValue_EmptyList() - { - var context = new DefaultHttpContext(); - context.Request.Headers.Accept = "invalid"; + [Fact] + public void GetListT_KnownTypeWithInvalidValue_EmptyList() + { + var context = new DefaultHttpContext(); + context.Request.Headers.Accept = "invalid"; - var result = context.Request.GetTypedHeaders().GetList(HeaderNames.Accept); + var result = context.Request.GetTypedHeaders().GetList(HeaderNames.Accept); - Assert.Empty(result); - } + Assert.Empty(result); + } - [Fact] - public void GetListT_UnknownTypeWithTryParseListAndValidValue_Success() - { - var context = new DefaultHttpContext(); - context.Request.Headers["custom"] = "valid"; + [Fact] + public void GetListT_UnknownTypeWithTryParseListAndValidValue_Success() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "valid"; - var results = context.Request.GetTypedHeaders().GetList("custom"); - Assert.NotNull(results); - Assert.Equal(new[] { new TestHeaderValue() }.ToList(), results); - } + var results = context.Request.GetTypedHeaders().GetList("custom"); + Assert.NotNull(results); + Assert.Equal(new[] { new TestHeaderValue() }.ToList(), results); + } - [Fact] - public void GetListT_UnknownTypeWithTryParseListAndInvalidValue_EmptyList() - { - var context = new DefaultHttpContext(); - context.Request.Headers["custom"] = "invalid"; + [Fact] + public void GetListT_UnknownTypeWithTryParseListAndInvalidValue_EmptyList() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "invalid"; - var results = context.Request.GetTypedHeaders().GetList("custom"); - Assert.Empty(results); - } + var results = context.Request.GetTypedHeaders().GetList("custom"); + Assert.Empty(results); + } - [Fact] - public void GetListT_UnknownTypeWithTryParseListAndMissingValue_EmptyList() - { - var context = new DefaultHttpContext(); + [Fact] + public void GetListT_UnknownTypeWithTryParseListAndMissingValue_EmptyList() + { + var context = new DefaultHttpContext(); - var results = context.Request.GetTypedHeaders().GetList("custom"); - Assert.Empty(results); - } + var results = context.Request.GetTypedHeaders().GetList("custom"); + Assert.Empty(results); + } - [Fact] - public void GetListT_UnknownTypeWithoutTryParseList_Throws() - { - var context = new DefaultHttpContext(); - context.Request.Headers["custom"] = "valid"; + [Fact] + public void GetListT_UnknownTypeWithoutTryParseList_Throws() + { + var context = new DefaultHttpContext(); + context.Request.Headers["custom"] = "valid"; - Assert.Throws(() => context.Request.GetTypedHeaders().GetList("custom")); - } + Assert.Throws(() => context.Request.GetTypedHeaders().GetList("custom")); + } - public class TestHeaderValue + public class TestHeaderValue + { + public static bool TryParse(string value, out TestHeaderValue result) { - public static bool TryParse(string value, out TestHeaderValue result) + if (string.Equals("valid", value, StringComparison.Ordinal)) { - if (string.Equals("valid", value, StringComparison.Ordinal)) - { - result = new TestHeaderValue(); - return true; - } - result = null; - return false; + result = new TestHeaderValue(); + return true; } + result = null; + return false; + } - public static bool TryParseList(IList values, out IList result) + public static bool TryParseList(IList values, out IList result) + { + var results = new List(); + foreach (var value in values) { - var results = new List(); - foreach (var value in values) - { - if (string.Equals("valid", value, StringComparison.Ordinal)) - { - results.Add(new TestHeaderValue()); - } - } - if (results.Count > 0) + if (string.Equals("valid", value, StringComparison.Ordinal)) { - result = results; - return true; + results.Add(new TestHeaderValue()); } - result = null; - return false; } - - public override bool Equals(object obj) + if (results.Count > 0) { - var other = obj as TestHeaderValue; - return other != null; + result = results; + return true; } + result = null; + return false; + } - public override int GetHashCode() - { - return 0; - } + public override bool Equals(object obj) + { + var other = obj as TestHeaderValue; + return other != null; + } + + public override int GetHashCode() + { + return 0; } } } diff --git a/src/Http/Http.Extensions/test/HttpRequestExtensionsTests.cs b/src/Http/Http.Extensions/test/HttpRequestExtensionsTests.cs index deb8d80456..021d0b7277 100644 --- a/src/Http/Http.Extensions/test/HttpRequestExtensionsTests.cs +++ b/src/Http/Http.Extensions/test/HttpRequestExtensionsTests.cs @@ -5,28 +5,27 @@ using Xunit; #nullable enable -namespace Microsoft.AspNetCore.Http.Extensions.Tests +namespace Microsoft.AspNetCore.Http.Extensions.Tests; + +public class HttpRequestExtensionsTests { - public class HttpRequestExtensionsTests + [Theory] + [InlineData(null, false)] + [InlineData("", false)] + [InlineData("application/xml", false)] + [InlineData("text/json", false)] + [InlineData("text/json; charset=utf-8", false)] + [InlineData("application/json", true)] + [InlineData("application/json; charset=utf-8", true)] + [InlineData("application/ld+json", true)] + [InlineData("APPLICATION/JSON", true)] + [InlineData("APPLICATION/JSON; CHARSET=UTF-8", true)] + [InlineData("APPLICATION/LD+JSON", true)] + public void HasJsonContentType(string contentType, bool hasJsonContentType) { - [Theory] - [InlineData(null, false)] - [InlineData("", false)] - [InlineData("application/xml", false)] - [InlineData("text/json", false)] - [InlineData("text/json; charset=utf-8", false)] - [InlineData("application/json", true)] - [InlineData("application/json; charset=utf-8", true)] - [InlineData("application/ld+json", true)] - [InlineData("APPLICATION/JSON", true)] - [InlineData("APPLICATION/JSON; CHARSET=UTF-8", true)] - [InlineData("APPLICATION/LD+JSON", true)] - public void HasJsonContentType(string contentType, bool hasJsonContentType) - { - var request = new DefaultHttpContext().Request; - request.ContentType = contentType; + var request = new DefaultHttpContext().Request; + request.ContentType = contentType; - Assert.Equal(hasJsonContentType, request.HasJsonContentType()); - } + Assert.Equal(hasJsonContentType, request.HasJsonContentType()); } } diff --git a/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs b/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs index af97e40412..c5041928f9 100644 --- a/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs +++ b/src/Http/Http.Extensions/test/HttpRequestJsonExtensionsTests.cs @@ -12,203 +12,202 @@ using Xunit; #nullable enable -namespace Microsoft.AspNetCore.Http.Extensions.Tests +namespace Microsoft.AspNetCore.Http.Extensions.Tests; + +public class HttpRequestJsonExtensionsTests { - public class HttpRequestJsonExtensionsTests + [Fact] + public async Task ReadFromJsonAsyncGeneric_NonJsonContentType_ThrowError() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.ContentType = "text/json"; + + // Act + var ex = await Assert.ThrowsAsync(async () => await context.Request.ReadFromJsonAsync()); + + // Assert + var exceptedMessage = $"Unable to read the request as JSON because the request content type 'text/json' is not a known JSON content type."; + Assert.Equal(exceptedMessage, ex.Message); + } + + [Fact] + public async Task ReadFromJsonAsyncGeneric_NoBodyContent_ThrowError() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.ContentType = "application/json"; + + // Act + var ex = await Assert.ThrowsAsync(async () => await context.Request.ReadFromJsonAsync()); + + // Assert + var exceptedMessage = $"The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0."; + Assert.Equal(exceptedMessage, ex.Message); + } + + [Fact] + public async Task ReadFromJsonAsyncGeneric_ValidBodyContent_ReturnValue() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.ContentType = "application/json"; + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("1")); + + // Act + var result = await context.Request.ReadFromJsonAsync(); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public async Task ReadFromJsonAsyncGeneric_WithOptions_ReturnValue() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.ContentType = "application/json"; + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2,]")); + + var options = new JsonSerializerOptions(); + options.AllowTrailingCommas = true; + + // Act + var result = await context.Request.ReadFromJsonAsync>(options); + + // Assert + Assert.Collection(result, + i => Assert.Equal(1, i), + i => Assert.Equal(2, i)); + } + + [Fact] + public async Task ReadFromJsonAsyncGeneric_Utf8Encoding_ReturnValue() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.ContentType = "application/json; charset=utf-8"; + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2]")); + + // Act + var result = await context.Request.ReadFromJsonAsync>(); + + // Assert + Assert.Collection(result, + i => Assert.Equal(1, i), + i => Assert.Equal(2, i)); + } + + [Fact] + public async Task ReadFromJsonAsyncGeneric_Utf16Encoding_ReturnValue() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.ContentType = "application/json; charset=utf-16"; + context.Request.Body = new MemoryStream(Encoding.Unicode.GetBytes(@"{""name"": ""激光這兩個字是甚麼意思""}")); + + // Act + var result = await context.Request.ReadFromJsonAsync>(); + + // Assert + Assert.Equal("激光這兩個字是甚麼意思", result!["name"]); + } + + [Fact] + public async Task ReadFromJsonAsyncGeneric_WithCancellationToken_CancellationRaised() { - [Fact] - public async Task ReadFromJsonAsyncGeneric_NonJsonContentType_ThrowError() - { - // Arrange - var context = new DefaultHttpContext(); - context.Request.ContentType = "text/json"; - - // Act - var ex = await Assert.ThrowsAsync(async () => await context.Request.ReadFromJsonAsync()); - - // Assert - var exceptedMessage = $"Unable to read the request as JSON because the request content type 'text/json' is not a known JSON content type."; - Assert.Equal(exceptedMessage, ex.Message); - } - - [Fact] - public async Task ReadFromJsonAsyncGeneric_NoBodyContent_ThrowError() - { - // Arrange - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/json"; - - // Act - var ex = await Assert.ThrowsAsync(async () => await context.Request.ReadFromJsonAsync()); - - // Assert - var exceptedMessage = $"The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0."; - Assert.Equal(exceptedMessage, ex.Message); - } - - [Fact] - public async Task ReadFromJsonAsyncGeneric_ValidBodyContent_ReturnValue() - { - // Arrange - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/json"; - context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("1")); - - // Act - var result = await context.Request.ReadFromJsonAsync(); - - // Assert - Assert.Equal(1, result); - } - - [Fact] - public async Task ReadFromJsonAsyncGeneric_WithOptions_ReturnValue() - { - // Arrange - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/json"; - context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2,]")); - - var options = new JsonSerializerOptions(); - options.AllowTrailingCommas = true; - - // Act - var result = await context.Request.ReadFromJsonAsync>(options); - - // Assert - Assert.Collection(result, - i => Assert.Equal(1, i), - i => Assert.Equal(2, i)); - } - - [Fact] - public async Task ReadFromJsonAsyncGeneric_Utf8Encoding_ReturnValue() - { - // Arrange - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/json; charset=utf-8"; - context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2]")); - - // Act - var result = await context.Request.ReadFromJsonAsync>(); - - // Assert - Assert.Collection(result, - i => Assert.Equal(1, i), - i => Assert.Equal(2, i)); - } - - [Fact] - public async Task ReadFromJsonAsyncGeneric_Utf16Encoding_ReturnValue() - { - // Arrange - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/json; charset=utf-16"; - context.Request.Body = new MemoryStream(Encoding.Unicode.GetBytes(@"{""name"": ""激光這兩個字是甚麼意思""}")); - - // Act - var result = await context.Request.ReadFromJsonAsync>(); - - // Assert - Assert.Equal("激光這兩個字是甚麼意思", result!["name"]); - } - - [Fact] - public async Task ReadFromJsonAsyncGeneric_WithCancellationToken_CancellationRaised() - { - // Arrange - var context = new DefaultHttpContext(); - context.Request.ContentType = "application /json"; - context.Request.Body = new TestStream(); - - var cts = new CancellationTokenSource(); - - // Act - var readTask = context.Request.ReadFromJsonAsync>(cts.Token); - Assert.False(readTask.IsCompleted); - - cts.Cancel(); - - // Assert - await Assert.ThrowsAsync(async () => await readTask); - } - - [Fact] - public async Task ReadFromJsonAsyncGeneric_InvalidEncoding_ThrowError() - { - // Arrange - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/json; charset=invalid"; - - // Act - var ex = await Assert.ThrowsAsync(async () => await context.Request.ReadFromJsonAsync()); - - // Assert - Assert.Equal("Unable to read the request as JSON because the request content type charset 'invalid' is not a known encoding.", ex.Message); - } - - [Fact] - public async Task ReadFromJsonAsync_ValidBodyContent_ReturnValue() - { - // Arrange - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/json"; - context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("1")); - - // Act - var result = (int?)await context.Request.ReadFromJsonAsync(typeof(int)); - - // Assert - Assert.Equal(1, result); - } - - [Fact] - public async Task ReadFromJsonAsync_Utf16Encoding_ReturnValue() - { - // Arrange - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/json; charset=utf-16"; - context.Request.Body = new MemoryStream(Encoding.Unicode.GetBytes(@"{""name"": ""激光這兩個字是甚麼意思""}")); - - // Act - var result = (Dictionary?)await context.Request.ReadFromJsonAsync(typeof(Dictionary)); - - // Assert - Assert.Equal("激光這兩個字是甚麼意思", result!["name"]); - } - - [Fact] - public async Task ReadFromJsonAsync_InvalidEncoding_ThrowError() - { - // Arrange - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/json; charset=invalid"; - - // Act - var ex = await Assert.ThrowsAsync(async () => await context.Request.ReadFromJsonAsync(typeof(object))); - - // Assert - Assert.Equal("Unable to read the request as JSON because the request content type charset 'invalid' is not a known encoding.", ex.Message); - } - - [Fact] - public async Task ReadFromJsonAsync_WithOptions_ReturnValue() - { - // Arrange - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/json"; - context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2,]")); - - var options = new JsonSerializerOptions(); - options.AllowTrailingCommas = true; - - // Act - var result = (List?)await context.Request.ReadFromJsonAsync(typeof(List), options); - - // Assert - Assert.Collection(result, - i => Assert.Equal(1, i), - i => Assert.Equal(2, i)); - } + // Arrange + var context = new DefaultHttpContext(); + context.Request.ContentType = "application /json"; + context.Request.Body = new TestStream(); + + var cts = new CancellationTokenSource(); + + // Act + var readTask = context.Request.ReadFromJsonAsync>(cts.Token); + Assert.False(readTask.IsCompleted); + + cts.Cancel(); + + // Assert + await Assert.ThrowsAsync(async () => await readTask); + } + + [Fact] + public async Task ReadFromJsonAsyncGeneric_InvalidEncoding_ThrowError() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.ContentType = "application/json; charset=invalid"; + + // Act + var ex = await Assert.ThrowsAsync(async () => await context.Request.ReadFromJsonAsync()); + + // Assert + Assert.Equal("Unable to read the request as JSON because the request content type charset 'invalid' is not a known encoding.", ex.Message); + } + + [Fact] + public async Task ReadFromJsonAsync_ValidBodyContent_ReturnValue() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.ContentType = "application/json"; + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("1")); + + // Act + var result = (int?)await context.Request.ReadFromJsonAsync(typeof(int)); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public async Task ReadFromJsonAsync_Utf16Encoding_ReturnValue() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.ContentType = "application/json; charset=utf-16"; + context.Request.Body = new MemoryStream(Encoding.Unicode.GetBytes(@"{""name"": ""激光這兩個字是甚麼意思""}")); + + // Act + var result = (Dictionary?)await context.Request.ReadFromJsonAsync(typeof(Dictionary)); + + // Assert + Assert.Equal("激光這兩個字是甚麼意思", result!["name"]); + } + + [Fact] + public async Task ReadFromJsonAsync_InvalidEncoding_ThrowError() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.ContentType = "application/json; charset=invalid"; + + // Act + var ex = await Assert.ThrowsAsync(async () => await context.Request.ReadFromJsonAsync(typeof(object))); + + // Assert + Assert.Equal("Unable to read the request as JSON because the request content type charset 'invalid' is not a known encoding.", ex.Message); + } + + [Fact] + public async Task ReadFromJsonAsync_WithOptions_ReturnValue() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.ContentType = "application/json"; + context.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("[1,2,]")); + + var options = new JsonSerializerOptions(); + options.AllowTrailingCommas = true; + + // Act + var result = (List?)await context.Request.ReadFromJsonAsync(typeof(List), options); + + // Assert + Assert.Collection(result, + i => Assert.Equal(1, i), + i => Assert.Equal(2, i)); } } diff --git a/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs b/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs index 5addfa413b..148ef1f846 100644 --- a/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs +++ b/src/Http/Http.Extensions/test/HttpResponseJsonExtensionsTests.cs @@ -13,486 +13,485 @@ using Xunit; #nullable enable -namespace Microsoft.AspNetCore.Http.Extensions.Tests +namespace Microsoft.AspNetCore.Http.Extensions.Tests; + +public class HttpResponseJsonExtensionsTests { - public class HttpResponseJsonExtensionsTests + [Fact] + public async Task WriteAsJsonAsyncGeneric_SimpleValue_JsonResponse() { - [Fact] - public async Task WriteAsJsonAsyncGeneric_SimpleValue_JsonResponse() - { - // Arrange - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; - // Act - await context.Response.WriteAsJsonAsync(1); + // Act + await context.Response.WriteAsJsonAsync(1); - // Assert - Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); - Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + // Assert + Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); - var data = body.ToArray(); - Assert.Collection(data, b => Assert.Equal((byte)'1', b)); - } + var data = body.ToArray(); + Assert.Collection(data, b => Assert.Equal((byte)'1', b)); + } - [Fact] - public async Task WriteAsJsonAsyncGeneric_NullValue_JsonResponse() - { - // Arrange - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; + [Fact] + public async Task WriteAsJsonAsyncGeneric_NullValue_JsonResponse() + { + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; - // Act - await context.Response.WriteAsJsonAsync(value: null); + // Act + await context.Response.WriteAsJsonAsync(value: null); - // Assert - Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); + // Assert + Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); - var data = Encoding.UTF8.GetString(body.ToArray()); - Assert.Equal("null", data); - } + var data = Encoding.UTF8.GetString(body.ToArray()); + Assert.Equal("null", data); + } - [Fact] - public async Task WriteAsJsonAsyncGeneric_WithOptions_JsonResponse() - { - // Arrange - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; + [Fact] + public async Task WriteAsJsonAsyncGeneric_WithOptions_JsonResponse() + { + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; - // Act - var options = new JsonSerializerOptions(); - options.Converters.Add(new IntegerConverter()); - await context.Response.WriteAsJsonAsync(new int[] { 1, 2, 3 }, options); + // Act + var options = new JsonSerializerOptions(); + options.Converters.Add(new IntegerConverter()); + await context.Response.WriteAsJsonAsync(new int[] { 1, 2, 3 }, options); - // Assert - Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); + // Assert + Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); - var data = Encoding.UTF8.GetString(body.ToArray()); - Assert.Equal("[false,true,false]", data); - } + var data = Encoding.UTF8.GetString(body.ToArray()); + Assert.Equal("[false,true,false]", data); + } - private class IntegerConverter : JsonConverter + private class IntegerConverter : JsonConverter + { + public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - throw new NotImplementedException(); - } - - public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) - { - writer.WriteBooleanValue(value % 2 == 0); - } + throw new NotImplementedException(); } - [Fact] - public async Task WriteAsJsonAsyncGeneric_CustomStatusCode_StatusCodeUnchanged() + public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) { - // Arrange - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; - - // Act - context.Response.StatusCode = StatusCodes.Status418ImATeapot; - await context.Response.WriteAsJsonAsync(1); - - // Assert - Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); - Assert.Equal(StatusCodes.Status418ImATeapot, context.Response.StatusCode); + writer.WriteBooleanValue(value % 2 == 0); } + } - [Fact] - public async Task WriteAsJsonAsyncGeneric_WithContentType_JsonResponseWithCustomContentType() - { - // Arrange - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; + [Fact] + public async Task WriteAsJsonAsyncGeneric_CustomStatusCode_StatusCodeUnchanged() + { + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; + + // Act + context.Response.StatusCode = StatusCodes.Status418ImATeapot; + await context.Response.WriteAsJsonAsync(1); + + // Assert + Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); + Assert.Equal(StatusCodes.Status418ImATeapot, context.Response.StatusCode); + } - // Act - await context.Response.WriteAsJsonAsync(1, options: null, contentType: "application/custom-type"); + [Fact] + public async Task WriteAsJsonAsyncGeneric_WithContentType_JsonResponseWithCustomContentType() + { + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; - // Assert - Assert.Equal("application/custom-type", context.Response.ContentType); - } + // Act + await context.Response.WriteAsJsonAsync(1, options: null, contentType: "application/custom-type"); - [Fact] - public async Task WriteAsJsonAsyncGeneric_WithCancellationToken_CancellationRaised() - { - // Arrange - var context = new DefaultHttpContext(); - context.Response.Body = new TestStream(); + // Assert + Assert.Equal("application/custom-type", context.Response.ContentType); + } - var cts = new CancellationTokenSource(); + [Fact] + public async Task WriteAsJsonAsyncGeneric_WithCancellationToken_CancellationRaised() + { + // Arrange + var context = new DefaultHttpContext(); + context.Response.Body = new TestStream(); - // Act - var writeTask = context.Response.WriteAsJsonAsync(1, cts.Token); - Assert.False(writeTask.IsCompleted); + var cts = new CancellationTokenSource(); - cts.Cancel(); + // Act + var writeTask = context.Response.WriteAsJsonAsync(1, cts.Token); + Assert.False(writeTask.IsCompleted); - // Assert - await Assert.ThrowsAsync(async () => await writeTask); - } + cts.Cancel(); - [Fact] - public async Task WriteAsJsonAsyncGeneric_ObjectWithStrings_CamcelCaseAndNotEscaped() + // Assert + await Assert.ThrowsAsync(async () => await writeTask); + } + + [Fact] + public async Task WriteAsJsonAsyncGeneric_ObjectWithStrings_CamcelCaseAndNotEscaped() + { + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; + var value = new TestObject { - // Arrange - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; - var value = new TestObject - { - StringProperty = "激光這兩個字是甚麼意思" - }; + StringProperty = "激光這兩個字是甚麼意思" + }; - // Act - await context.Response.WriteAsJsonAsync(value); + // Act + await context.Response.WriteAsJsonAsync(value); - // Assert - var data = Encoding.UTF8.GetString(body.ToArray()); - Assert.Equal(@"{""stringProperty"":""激光這兩個字是甚麼意思""}", data); - } + // Assert + var data = Encoding.UTF8.GetString(body.ToArray()); + Assert.Equal(@"{""stringProperty"":""激光這兩個字是甚麼意思""}", data); + } - [Fact] - public async Task WriteAsJsonAsync_SimpleValue_JsonResponse() - { - // Arrange - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; + [Fact] + public async Task WriteAsJsonAsync_SimpleValue_JsonResponse() + { + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; - // Act - await context.Response.WriteAsJsonAsync(1, typeof(int)); + // Act + await context.Response.WriteAsJsonAsync(1, typeof(int)); - // Assert - Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); - Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + // Assert + Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); - var data = body.ToArray(); - Assert.Collection(data, b => Assert.Equal((byte)'1', b)); - } + var data = body.ToArray(); + Assert.Collection(data, b => Assert.Equal((byte)'1', b)); + } - [Fact] - public async Task WriteAsJsonAsync_NullValue_JsonResponse() - { - // Arrange - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; + [Fact] + public async Task WriteAsJsonAsync_NullValue_JsonResponse() + { + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; - // Act - await context.Response.WriteAsJsonAsync(value: null, typeof(int?)); + // Act + await context.Response.WriteAsJsonAsync(value: null, typeof(int?)); - // Assert - Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); + // Assert + Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); - var data = Encoding.UTF8.GetString(body.ToArray()); - Assert.Equal("null", data); - } + var data = Encoding.UTF8.GetString(body.ToArray()); + Assert.Equal("null", data); + } - [Fact] - public async Task WriteAsJsonAsync_NullType_ThrowsArgumentNullException() - { - // Arrange - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; + [Fact] + public async Task WriteAsJsonAsync_NullType_ThrowsArgumentNullException() + { + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; - // Act & Assert - await Assert.ThrowsAsync(async () => await context.Response.WriteAsJsonAsync(value: null, type: null!)); - } + // Act & Assert + await Assert.ThrowsAsync(async () => await context.Response.WriteAsJsonAsync(value: null, type: null!)); + } - [Fact] - public async Task WriteAsJsonAsync_NullResponse_ThrowsArgumentNullException() - { - // Arrange - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; + [Fact] + public async Task WriteAsJsonAsync_NullResponse_ThrowsArgumentNullException() + { + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; - // Act & Assert - await Assert.ThrowsAsync(async () => await HttpResponseJsonExtensions.WriteAsJsonAsync(response: null!, value: null, typeof(int?))); - } + // Act & Assert + await Assert.ThrowsAsync(async () => await HttpResponseJsonExtensions.WriteAsJsonAsync(response: null!, value: null, typeof(int?))); + } - [Fact] - public async Task WriteAsJsonAsync_ObjectWithStrings_CamcelCaseAndNotEscaped() + [Fact] + public async Task WriteAsJsonAsync_ObjectWithStrings_CamcelCaseAndNotEscaped() + { + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; + var value = new TestObject { - // Arrange - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; - var value = new TestObject - { - StringProperty = "激光這兩個字是甚麼意思" - }; + StringProperty = "激光這兩個字是甚麼意思" + }; - // Act - await context.Response.WriteAsJsonAsync(value, typeof(TestObject)); + // Act + await context.Response.WriteAsJsonAsync(value, typeof(TestObject)); - // Assert - var data = Encoding.UTF8.GetString(body.ToArray()); - Assert.Equal(@"{""stringProperty"":""激光這兩個字是甚麼意思""}", data); - } + // Assert + var data = Encoding.UTF8.GetString(body.ToArray()); + Assert.Equal(@"{""stringProperty"":""激光這兩個字是甚麼意思""}", data); + } - [Fact] - public async Task WriteAsJsonAsync_CustomStatusCode_StatusCodeUnchanged() - { - // Arrange - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; - - // Act - context.Response.StatusCode = StatusCodes.Status418ImATeapot; - await context.Response.WriteAsJsonAsync(1, typeof(int)); - - // Assert - Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); - Assert.Equal(StatusCodes.Status418ImATeapot, context.Response.StatusCode); - } + [Fact] + public async Task WriteAsJsonAsync_CustomStatusCode_StatusCodeUnchanged() + { + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; + + // Act + context.Response.StatusCode = StatusCodes.Status418ImATeapot; + await context.Response.WriteAsJsonAsync(1, typeof(int)); + + // Assert + Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); + Assert.Equal(StatusCodes.Status418ImATeapot, context.Response.StatusCode); + } - [Fact] - public async Task WriteAsJsonAsyncGeneric_AsyncEnumerable() - { - // Arrange - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; + [Fact] + public async Task WriteAsJsonAsyncGeneric_AsyncEnumerable() + { + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; - // Act - await context.Response.WriteAsJsonAsync(AsyncEnumerable()); + // Act + await context.Response.WriteAsJsonAsync(AsyncEnumerable()); - // Assert - Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); - Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + // Assert + Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); - Assert.Equal("[1,2]", Encoding.UTF8.GetString(body.ToArray())); + Assert.Equal("[1,2]", Encoding.UTF8.GetString(body.ToArray())); - async IAsyncEnumerable AsyncEnumerable() - { - await Task.Yield(); - yield return 1; - yield return 2; - } + async IAsyncEnumerable AsyncEnumerable() + { + await Task.Yield(); + yield return 1; + yield return 2; } + } - [Fact] - public async Task WriteAsJsonAsync_AsyncEnumerable() - { - // Arrange - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; + [Fact] + public async Task WriteAsJsonAsync_AsyncEnumerable() + { + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; - // Act - await context.Response.WriteAsJsonAsync(AsyncEnumerable(), typeof(IAsyncEnumerable)); + // Act + await context.Response.WriteAsJsonAsync(AsyncEnumerable(), typeof(IAsyncEnumerable)); - // Assert - Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); - Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + // Assert + Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); - Assert.Equal("[1,2]", Encoding.UTF8.GetString(body.ToArray())); + Assert.Equal("[1,2]", Encoding.UTF8.GetString(body.ToArray())); - async IAsyncEnumerable AsyncEnumerable() - { - await Task.Yield(); - yield return 1; - yield return 2; - } + async IAsyncEnumerable AsyncEnumerable() + { + await Task.Yield(); + yield return 1; + yield return 2; } + } - [Fact] - public async Task WriteAsJsonAsyncGeneric_AsyncEnumerable_ClosedConnecton() + [Fact] + public async Task WriteAsJsonAsyncGeneric_AsyncEnumerable_ClosedConnecton() + { + // Arrange + var cts = new CancellationTokenSource(); + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; + context.RequestAborted = cts.Token; + var iterated = false; + + // Act + await context.Response.WriteAsJsonAsync(AsyncEnumerable()); + + // Assert + Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + + // System.Text.Json might write the '[' before cancellation is observed + Assert.InRange(body.ToArray().Length, 0, 1); + Assert.False(iterated); + + async IAsyncEnumerable AsyncEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default) { - // Arrange - var cts = new CancellationTokenSource(); - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; - context.RequestAborted = cts.Token; - var iterated = false; - - // Act - await context.Response.WriteAsJsonAsync(AsyncEnumerable()); - - // Assert - Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); - Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); - - // System.Text.Json might write the '[' before cancellation is observed - Assert.InRange(body.ToArray().Length, 0, 1); - Assert.False(iterated); - - async IAsyncEnumerable AsyncEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default) + await Task.Yield(); + cts.Cancel(); + for (var i = 0; i < 100 && !cancellationToken.IsCancellationRequested; i++) { - await Task.Yield(); - cts.Cancel(); - for (var i = 0; i < 100 && !cancellationToken.IsCancellationRequested; i++) - { - iterated = true; - yield return i; - } + iterated = true; + yield return i; } } + } - [Fact] - public async Task WriteAsJsonAsync_AsyncEnumerable_ClosedConnecton() + [Fact] + public async Task WriteAsJsonAsync_AsyncEnumerable_ClosedConnecton() + { + // Arrange + var cts = new CancellationTokenSource(); + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; + context.RequestAborted = cts.Token; + var iterated = false; + + // Act + await context.Response.WriteAsJsonAsync(AsyncEnumerable(), typeof(IAsyncEnumerable)); + + // Assert + Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + + // System.Text.Json might write the '[' before cancellation is observed + Assert.InRange(body.ToArray().Length, 0, 1); + Assert.False(iterated); + + async IAsyncEnumerable AsyncEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default) { - // Arrange - var cts = new CancellationTokenSource(); - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; - context.RequestAborted = cts.Token; - var iterated = false; - - // Act - await context.Response.WriteAsJsonAsync(AsyncEnumerable(), typeof(IAsyncEnumerable)); - - // Assert - Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); - Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); - - // System.Text.Json might write the '[' before cancellation is observed - Assert.InRange(body.ToArray().Length, 0, 1); - Assert.False(iterated); - - async IAsyncEnumerable AsyncEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default) + await Task.Yield(); + cts.Cancel(); + for (var i = 0; i < 100 && !cancellationToken.IsCancellationRequested; i++) { - await Task.Yield(); - cts.Cancel(); - for (var i = 0; i < 100 && !cancellationToken.IsCancellationRequested; i++) - { - iterated = true; - yield return i; - } + iterated = true; + yield return i; } } + } - [Fact] - public async Task WriteAsJsonAsync_AsyncEnumerable_UserPassedTokenThrows() + [Fact] + public async Task WriteAsJsonAsync_AsyncEnumerable_UserPassedTokenThrows() + { + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; + context.RequestAborted = new CancellationToken(canceled: true); + var cts = new CancellationTokenSource(); + var iterated = false; + + // Act + await Assert.ThrowsAnyAsync(() => context.Response.WriteAsJsonAsync(AsyncEnumerable(), typeof(IAsyncEnumerable), cts.Token)); + + // Assert + Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + + // System.Text.Json might write the '[' before cancellation is observed + Assert.InRange(body.ToArray().Length, 0, 1); + Assert.False(iterated); + + async IAsyncEnumerable AsyncEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default) { - // Arrange - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; - context.RequestAborted = new CancellationToken(canceled: true); - var cts = new CancellationTokenSource(); - var iterated = false; - - // Act - await Assert.ThrowsAnyAsync(() => context.Response.WriteAsJsonAsync(AsyncEnumerable(), typeof(IAsyncEnumerable), cts.Token)); - - // Assert - Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); - Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); - - // System.Text.Json might write the '[' before cancellation is observed - Assert.InRange(body.ToArray().Length, 0, 1); - Assert.False(iterated); - - async IAsyncEnumerable AsyncEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default) + await Task.Yield(); + cts.Cancel(); + for (var i = 0; i < 100 && !cancellationToken.IsCancellationRequested; i++) { - await Task.Yield(); - cts.Cancel(); - for (var i = 0; i < 100 && !cancellationToken.IsCancellationRequested; i++) - { - iterated = true; - yield return i; - } + iterated = true; + yield return i; } } + } - [Fact] - public async Task WriteAsJsonAsyncGeneric_AsyncEnumerableG_UserPassedTokenThrows() + [Fact] + public async Task WriteAsJsonAsyncGeneric_AsyncEnumerableG_UserPassedTokenThrows() + { + // Arrange + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.Response.Body = body; + context.RequestAborted = new CancellationToken(canceled: true); + var cts = new CancellationTokenSource(); + var iterated = false; + + // Act + await Assert.ThrowsAnyAsync(() => context.Response.WriteAsJsonAsync(AsyncEnumerable(), cts.Token)); + + // Assert + Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); + Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + + // System.Text.Json might write the '[' before cancellation is observed + Assert.InRange(body.ToArray().Length, 0, 1); + Assert.False(iterated); + + async IAsyncEnumerable AsyncEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default) { - // Arrange - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.Response.Body = body; - context.RequestAborted = new CancellationToken(canceled: true); - var cts = new CancellationTokenSource(); - var iterated = false; - - // Act - await Assert.ThrowsAnyAsync(() => context.Response.WriteAsJsonAsync(AsyncEnumerable(), cts.Token)); - - // Assert - Assert.Equal(JsonConstants.JsonContentTypeWithCharset, context.Response.ContentType); - Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); - - // System.Text.Json might write the '[' before cancellation is observed - Assert.InRange(body.ToArray().Length, 0, 1); - Assert.False(iterated); - - async IAsyncEnumerable AsyncEnumerable([EnumeratorCancellation] CancellationToken cancellationToken = default) + await Task.Yield(); + cts.Cancel(); + for (var i = 0; i < 100 && !cancellationToken.IsCancellationRequested; i++) { - await Task.Yield(); - cts.Cancel(); - for (var i = 0; i < 100 && !cancellationToken.IsCancellationRequested; i++) - { - iterated = true; - yield return i; - } + iterated = true; + yield return i; } } + } + + public class TestObject + { + public string? StringProperty { get; set; } + } - public class TestObject + private class TestStream : Stream + { + public override bool CanRead { get; } + public override bool CanSeek { get; } + public override bool CanWrite { get; } + public override long Length { get; } + public override long Position { get; set; } + + public override void Flush() { - public string? StringProperty { get; set; } + throw new NotImplementedException(); } - private class TestStream : Stream + public override int Read(byte[] buffer, int offset, int count) { - public override bool CanRead { get; } - public override bool CanSeek { get; } - public override bool CanWrite { get; } - public override long Length { get; } - public override long Position { get; set; } - - public override void Flush() - { - throw new NotImplementedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); + } - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } - public override void SetLength(long value) - { - throw new NotImplementedException(); - } + public override void SetLength(long value) + { + throw new NotImplementedException(); + } - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } - public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(); - cancellationToken.Register(s => ((TaskCompletionSource)s!).SetCanceled(), tcs); - return new ValueTask(tcs.Task); - } + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + cancellationToken.Register(s => ((TaskCompletionSource)s!).SetCanceled(), tcs); + return new ValueTask(tcs.Task); + } - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(); - cancellationToken.Register(s => ((TaskCompletionSource)s!).SetCanceled(), tcs); - return new ValueTask(tcs.Task); - } + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + cancellationToken.Register(s => ((TaskCompletionSource)s!).SetCanceled(), tcs); + return new ValueTask(tcs.Task); } } } diff --git a/src/Http/Http.Extensions/test/HttpValidationProblemDetailsJsonConverterTest.cs b/src/Http/Http.Extensions/test/HttpValidationProblemDetailsJsonConverterTest.cs index 3e987c3b2c..1c98c56fc5 100644 --- a/src/Http/Http.Extensions/test/HttpValidationProblemDetailsJsonConverterTest.cs +++ b/src/Http/Http.Extensions/test/HttpValidationProblemDetailsJsonConverterTest.cs @@ -7,134 +7,133 @@ using System.Text.Json; using Microsoft.AspNetCore.Http.Json; using Xunit; -namespace Microsoft.AspNetCore.Http.Extensions +namespace Microsoft.AspNetCore.Http.Extensions; + +public class HttpValidationProblemDetailsJsonConverterTest { - public class HttpValidationProblemDetailsJsonConverterTest - { - private static JsonSerializerOptions JsonSerializerOptions => new JsonOptions().SerializerOptions; + private static JsonSerializerOptions JsonSerializerOptions => new JsonOptions().SerializerOptions; - [Fact] - public void Read_Works() - { - // Arrange - var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4"; - var title = "Not found"; - var status = 404; - var detail = "Product not found"; - var instance = "http://example.com/products/14"; - var traceId = "|37dd3dd5-4a9619f953c40a16."; - var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"detail\":\"{detail}\", \"instance\":\"{instance}\",\"traceId\":\"{traceId}\"," + - "\"errors\":{\"key0\":[\"error0\"],\"key1\":[\"error1\",\"error2\"]}}"; - var converter = new HttpValidationProblemDetailsJsonConverter(); - var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); - reader.Read(); + [Fact] + public void Read_Works() + { + // Arrange + var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4"; + var title = "Not found"; + var status = 404; + var detail = "Product not found"; + var instance = "http://example.com/products/14"; + var traceId = "|37dd3dd5-4a9619f953c40a16."; + var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"detail\":\"{detail}\", \"instance\":\"{instance}\",\"traceId\":\"{traceId}\"," + + "\"errors\":{\"key0\":[\"error0\"],\"key1\":[\"error1\",\"error2\"]}}"; + var converter = new HttpValidationProblemDetailsJsonConverter(); + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); + reader.Read(); - // Act - var problemDetails = converter.Read(ref reader, typeof(HttpValidationProblemDetails), JsonSerializerOptions); + // Act + var problemDetails = converter.Read(ref reader, typeof(HttpValidationProblemDetails), JsonSerializerOptions); - Assert.Equal(type, problemDetails.Type); - Assert.Equal(title, problemDetails.Title); - Assert.Equal(status, problemDetails.Status); - Assert.Equal(instance, problemDetails.Instance); - Assert.Equal(detail, problemDetails.Detail); - Assert.Collection( - problemDetails.Extensions, - kvp => - { - Assert.Equal("traceId", kvp.Key); - Assert.Equal(traceId, kvp.Value.ToString()); - }); - Assert.Collection( - problemDetails.Errors.OrderBy(kvp => kvp.Key), - kvp => - { - Assert.Equal("key0", kvp.Key); - Assert.Equal(new[] { "error0" }, kvp.Value); - }, - kvp => - { - Assert.Equal("key1", kvp.Key); - Assert.Equal(new[] { "error1", "error2" }, kvp.Value); - }); - } + Assert.Equal(type, problemDetails.Type); + Assert.Equal(title, problemDetails.Title); + Assert.Equal(status, problemDetails.Status); + Assert.Equal(instance, problemDetails.Instance); + Assert.Equal(detail, problemDetails.Detail); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.Equal(traceId, kvp.Value.ToString()); + }); + Assert.Collection( + problemDetails.Errors.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("key0", kvp.Key); + Assert.Equal(new[] { "error0" }, kvp.Value); + }, + kvp => + { + Assert.Equal("key1", kvp.Key); + Assert.Equal(new[] { "error1", "error2" }, kvp.Value); + }); + } - [Fact] - public void Read_WithSomeMissingValues_Works() - { - // Arrange - var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4"; - var title = "Not found"; - var status = 404; - var traceId = "|37dd3dd5-4a9619f953c40a16."; - var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"traceId\":\"{traceId}\"," + - "\"errors\":{\"key0\":[\"error0\"],\"key1\":[\"error1\",\"error2\"]}}"; - var converter = new HttpValidationProblemDetailsJsonConverter(); - var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); - reader.Read(); + [Fact] + public void Read_WithSomeMissingValues_Works() + { + // Arrange + var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4"; + var title = "Not found"; + var status = 404; + var traceId = "|37dd3dd5-4a9619f953c40a16."; + var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"traceId\":\"{traceId}\"," + + "\"errors\":{\"key0\":[\"error0\"],\"key1\":[\"error1\",\"error2\"]}}"; + var converter = new HttpValidationProblemDetailsJsonConverter(); + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); + reader.Read(); - // Act - var problemDetails = converter.Read(ref reader, typeof(HttpValidationProblemDetails), JsonSerializerOptions); + // Act + var problemDetails = converter.Read(ref reader, typeof(HttpValidationProblemDetails), JsonSerializerOptions); - Assert.Equal(type, problemDetails.Type); - Assert.Equal(title, problemDetails.Title); - Assert.Equal(status, problemDetails.Status); - Assert.Collection( - problemDetails.Extensions, - kvp => - { - Assert.Equal("traceId", kvp.Key); - Assert.Equal(traceId, kvp.Value.ToString()); - }); - Assert.Collection( - problemDetails.Errors.OrderBy(kvp => kvp.Key), - kvp => - { - Assert.Equal("key0", kvp.Key); - Assert.Equal(new[] { "error0" }, kvp.Value); - }, - kvp => - { - Assert.Equal("key1", kvp.Key); - Assert.Equal(new[] { "error1", "error2" }, kvp.Value); - }); - } + Assert.Equal(type, problemDetails.Type); + Assert.Equal(title, problemDetails.Title); + Assert.Equal(status, problemDetails.Status); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.Equal(traceId, kvp.Value.ToString()); + }); + Assert.Collection( + problemDetails.Errors.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("key0", kvp.Key); + Assert.Equal(new[] { "error0" }, kvp.Value); + }, + kvp => + { + Assert.Equal("key1", kvp.Key); + Assert.Equal(new[] { "error1", "error2" }, kvp.Value); + }); + } - [Fact] - public void ReadUsingJsonSerializerWorks() - { - // Arrange - var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4"; - var title = "Not found"; - var status = 404; - var traceId = "|37dd3dd5-4a9619f953c40a16."; - var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"traceId\":\"{traceId}\"," + - "\"errors\":{\"key0\":[\"error0\"],\"key1\":[\"error1\",\"error2\"]}}"; + [Fact] + public void ReadUsingJsonSerializerWorks() + { + // Arrange + var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4"; + var title = "Not found"; + var status = 404; + var traceId = "|37dd3dd5-4a9619f953c40a16."; + var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"traceId\":\"{traceId}\"," + + "\"errors\":{\"key0\":[\"error0\"],\"key1\":[\"error1\",\"error2\"]}}"; - // Act - var problemDetails = JsonSerializer.Deserialize(json, JsonSerializerOptions); + // Act + var problemDetails = JsonSerializer.Deserialize(json, JsonSerializerOptions); - Assert.Equal(type, problemDetails.Type); - Assert.Equal(title, problemDetails.Title); - Assert.Equal(status, problemDetails.Status); - Assert.Collection( - problemDetails.Extensions, - kvp => - { - Assert.Equal("traceId", kvp.Key); - Assert.Equal(traceId, kvp.Value.ToString()); - }); - Assert.Collection( - problemDetails.Errors.OrderBy(kvp => kvp.Key), - kvp => - { - Assert.Equal("key0", kvp.Key); - Assert.Equal(new[] { "error0" }, kvp.Value); - }, - kvp => - { - Assert.Equal("key1", kvp.Key); - Assert.Equal(new[] { "error1", "error2" }, kvp.Value); - }); - } + Assert.Equal(type, problemDetails.Type); + Assert.Equal(title, problemDetails.Title); + Assert.Equal(status, problemDetails.Status); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.Equal(traceId, kvp.Value.ToString()); + }); + Assert.Collection( + problemDetails.Errors.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("key0", kvp.Key); + Assert.Equal(new[] { "error0" }, kvp.Value); + }, + kvp => + { + Assert.Equal("key1", kvp.Key); + Assert.Equal(new[] { "error1", "error2" }, kvp.Value); + }); } } diff --git a/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs b/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs index e64496b207..dad9ad3375 100644 --- a/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs +++ b/src/Http/Http.Extensions/test/ParameterBindingMethodCacheTests.cs @@ -8,122 +8,122 @@ using System.Linq.Expressions; using System.Reflection; using Microsoft.Extensions.Internal; -namespace Microsoft.AspNetCore.Http.Extensions.Tests +namespace Microsoft.AspNetCore.Http.Extensions.Tests; + +public class ParameterBindingMethodCacheTests { - public class ParameterBindingMethodCacheTests - { - [Theory] - [InlineData(typeof(int))] - [InlineData(typeof(double))] - [InlineData(typeof(float))] - [InlineData(typeof(Half))] - [InlineData(typeof(short))] - [InlineData(typeof(long))] - [InlineData(typeof(IntPtr))] - [InlineData(typeof(sbyte))] - [InlineData(typeof(ushort))] - [InlineData(typeof(uint))] - [InlineData(typeof(ulong))] - public void FindTryParseStringMethod_ReturnsTheExpectedTryParseMethodWithInvariantCulture(Type type) - { - var methodFound = new ParameterBindingMethodCache().FindTryParseMethod(@type); + [Theory] + [InlineData(typeof(int))] + [InlineData(typeof(double))] + [InlineData(typeof(float))] + [InlineData(typeof(Half))] + [InlineData(typeof(short))] + [InlineData(typeof(long))] + [InlineData(typeof(IntPtr))] + [InlineData(typeof(sbyte))] + [InlineData(typeof(ushort))] + [InlineData(typeof(uint))] + [InlineData(typeof(ulong))] + public void FindTryParseStringMethod_ReturnsTheExpectedTryParseMethodWithInvariantCulture(Type type) + { + var methodFound = new ParameterBindingMethodCache().FindTryParseMethod(@type); - Assert.NotNull(methodFound); + Assert.NotNull(methodFound); - var call = methodFound!(Expression.Variable(type, "parsedValue")) as MethodCallExpression; - Assert.NotNull(call); - var parameters = call!.Method.GetParameters(); + var call = methodFound!(Expression.Variable(type, "parsedValue")) as MethodCallExpression; + Assert.NotNull(call); + var parameters = call!.Method.GetParameters(); - Assert.Equal(4, parameters.Length); - Assert.Equal(typeof(string), parameters[0].ParameterType); - Assert.Equal(typeof(NumberStyles), parameters[1].ParameterType); - Assert.Equal(typeof(IFormatProvider), parameters[2].ParameterType); - Assert.True(parameters[3].IsOut); - } + Assert.Equal(4, parameters.Length); + Assert.Equal(typeof(string), parameters[0].ParameterType); + Assert.Equal(typeof(NumberStyles), parameters[1].ParameterType); + Assert.Equal(typeof(IFormatProvider), parameters[2].ParameterType); + Assert.True(parameters[3].IsOut); + } - [Theory] - [InlineData(typeof(DateTime))] - [InlineData(typeof(DateOnly))] - [InlineData(typeof(DateTimeOffset))] - [InlineData(typeof(TimeOnly))] - [InlineData(typeof(TimeSpan))] - public void FindTryParseStringMethod_ReturnsTheExpectedTryParseMethodWithInvariantCultureDateType(Type type) - { - var methodFound = new ParameterBindingMethodCache().FindTryParseMethod(@type); + [Theory] + [InlineData(typeof(DateTime))] + [InlineData(typeof(DateOnly))] + [InlineData(typeof(DateTimeOffset))] + [InlineData(typeof(TimeOnly))] + [InlineData(typeof(TimeSpan))] + public void FindTryParseStringMethod_ReturnsTheExpectedTryParseMethodWithInvariantCultureDateType(Type type) + { + var methodFound = new ParameterBindingMethodCache().FindTryParseMethod(@type); - Assert.NotNull(methodFound); + Assert.NotNull(methodFound); - var call = methodFound!(Expression.Variable(type, "parsedValue")) as MethodCallExpression; - Assert.NotNull(call); - var parameters = call!.Method.GetParameters(); + var call = methodFound!(Expression.Variable(type, "parsedValue")) as MethodCallExpression; + Assert.NotNull(call); + var parameters = call!.Method.GetParameters(); - if (@type == typeof(TimeSpan)) - { - Assert.Equal(3, parameters.Length); - Assert.Equal(typeof(string), parameters[0].ParameterType); - Assert.Equal(typeof(IFormatProvider), parameters[1].ParameterType); - Assert.True(parameters[2].IsOut); - } - else - { - Assert.Equal(4, parameters.Length); - Assert.Equal(typeof(string), parameters[0].ParameterType); - Assert.Equal(typeof(IFormatProvider), parameters[1].ParameterType); - Assert.Equal(typeof(DateTimeStyles), parameters[2].ParameterType); - Assert.True(parameters[3].IsOut); - } - } - - [Theory] - [InlineData(typeof(TryParseStringRecord))] - [InlineData(typeof(TryParseStringStruct))] - [InlineData(typeof(TryParseInheritClassWithFormatProvider))] - [InlineData(typeof(TryParseFromInterfaceWithFormatProvider))] - public void FindTryParseStringMethod_ReturnsTheExpectedTryParseMethodWithInvariantCultureCustomType(Type type) + if (@type == typeof(TimeSpan)) { - var methodFound = new ParameterBindingMethodCache().FindTryParseMethod(@type); - - Assert.NotNull(methodFound); - - var call = methodFound!(Expression.Variable(type, "parsedValue")) as MethodCallExpression; - Assert.NotNull(call); - var parameters = call!.Method.GetParameters(); - Assert.Equal(3, parameters.Length); Assert.Equal(typeof(string), parameters[0].ParameterType); Assert.Equal(typeof(IFormatProvider), parameters[1].ParameterType); Assert.True(parameters[2].IsOut); - Assert.True(((call.Arguments[1] as ConstantExpression)!.Value as CultureInfo)!.Equals(CultureInfo.InvariantCulture)); } - - [Theory] - [InlineData(typeof(TryParseNoFormatProviderRecord))] - [InlineData(typeof(TryParseNoFormatProviderStruct))] - [InlineData(typeof(TryParseInheritClass))] - [InlineData(typeof(TryParseFromInterface))] - [InlineData(typeof(TryParseFromGrandparentInterface))] - [InlineData(typeof(TryParseDirectlyAndFromInterface))] - [InlineData(typeof(TryParseFromClassAndInterface))] - public void FindTryParseMethod_WithNoFormatProvider(Type type) + else { - var methodFound = new ParameterBindingMethodCache().FindTryParseMethod(@type); - Assert.NotNull(methodFound); - - var call = methodFound!(Expression.Variable(type, "parsedValue")) as MethodCallExpression; - Assert.NotNull(call); - var parameters = call!.Method.GetParameters(); - - Assert.Equal(2, parameters.Length); + Assert.Equal(4, parameters.Length); Assert.Equal(typeof(string), parameters[0].ParameterType); - Assert.True(parameters[1].IsOut); + Assert.Equal(typeof(IFormatProvider), parameters[1].ParameterType); + Assert.Equal(typeof(DateTimeStyles), parameters[2].ParameterType); + Assert.True(parameters[3].IsOut); } + } - public static IEnumerable TryParseStringParameterInfoData + [Theory] + [InlineData(typeof(TryParseStringRecord))] + [InlineData(typeof(TryParseStringStruct))] + [InlineData(typeof(TryParseInheritClassWithFormatProvider))] + [InlineData(typeof(TryParseFromInterfaceWithFormatProvider))] + public void FindTryParseStringMethod_ReturnsTheExpectedTryParseMethodWithInvariantCultureCustomType(Type type) + { + var methodFound = new ParameterBindingMethodCache().FindTryParseMethod(@type); + + Assert.NotNull(methodFound); + + var call = methodFound!(Expression.Variable(type, "parsedValue")) as MethodCallExpression; + Assert.NotNull(call); + var parameters = call!.Method.GetParameters(); + + Assert.Equal(3, parameters.Length); + Assert.Equal(typeof(string), parameters[0].ParameterType); + Assert.Equal(typeof(IFormatProvider), parameters[1].ParameterType); + Assert.True(parameters[2].IsOut); + Assert.True(((call.Arguments[1] as ConstantExpression)!.Value as CultureInfo)!.Equals(CultureInfo.InvariantCulture)); + } + + [Theory] + [InlineData(typeof(TryParseNoFormatProviderRecord))] + [InlineData(typeof(TryParseNoFormatProviderStruct))] + [InlineData(typeof(TryParseInheritClass))] + [InlineData(typeof(TryParseFromInterface))] + [InlineData(typeof(TryParseFromGrandparentInterface))] + [InlineData(typeof(TryParseDirectlyAndFromInterface))] + [InlineData(typeof(TryParseFromClassAndInterface))] + public void FindTryParseMethod_WithNoFormatProvider(Type type) + { + var methodFound = new ParameterBindingMethodCache().FindTryParseMethod(@type); + Assert.NotNull(methodFound); + + var call = methodFound!(Expression.Variable(type, "parsedValue")) as MethodCallExpression; + Assert.NotNull(call); + var parameters = call!.Method.GetParameters(); + + Assert.Equal(2, parameters.Length); + Assert.Equal(typeof(string), parameters[0].ParameterType); + Assert.True(parameters[1].IsOut); + } + + public static IEnumerable TryParseStringParameterInfoData + { + get { - get + return new[] { - return new[] - { new[] { GetFirstParameter((TryParseStringRecord arg) => TryParseStringRecordMethod(arg)), @@ -137,127 +137,127 @@ namespace Microsoft.AspNetCore.Http.Extensions.Tests GetFirstParameter((TryParseStringStruct? arg) => TryParseStringNullableStructMethod(arg)), }, }; - } } + } - [Theory] - [MemberData(nameof(TryParseStringParameterInfoData))] - public void HasTryParseStringMethod_ReturnsTrueWhenMethodExists(ParameterInfo parameterInfo) - { - Assert.True(new ParameterBindingMethodCache().HasTryParseMethod(parameterInfo)); - } + [Theory] + [MemberData(nameof(TryParseStringParameterInfoData))] + public void HasTryParseStringMethod_ReturnsTrueWhenMethodExists(ParameterInfo parameterInfo) + { + Assert.True(new ParameterBindingMethodCache().HasTryParseMethod(parameterInfo)); + } - [Fact] - public void FindTryParseStringMethod_WorksForEnums() - { - var type = typeof(Choice); - var methodFound = new ParameterBindingMethodCache().FindTryParseMethod(type); + [Fact] + public void FindTryParseStringMethod_WorksForEnums() + { + var type = typeof(Choice); + var methodFound = new ParameterBindingMethodCache().FindTryParseMethod(type); - Assert.NotNull(methodFound); + Assert.NotNull(methodFound); - var call = methodFound!(Expression.Variable(type, "parsedValue")) as MethodCallExpression; - Assert.NotNull(call); - var method = call!.Method; - var parameters = method.GetParameters(); + var call = methodFound!(Expression.Variable(type, "parsedValue")) as MethodCallExpression; + Assert.NotNull(call); + var method = call!.Method; + var parameters = method.GetParameters(); - // By default, we use Enum.TryParse - Assert.True(method.IsGenericMethod); - Assert.Equal(2, parameters.Length); - Assert.Equal(typeof(string), parameters[0].ParameterType); - Assert.True(parameters[1].IsOut); - } + // By default, we use Enum.TryParse + Assert.True(method.IsGenericMethod); + Assert.Equal(2, parameters.Length); + Assert.Equal(typeof(string), parameters[0].ParameterType); + Assert.True(parameters[1].IsOut); + } - [Fact] - public void FindTryParseStringMethod_WorksForEnumsWhenNonGenericEnumParseIsUsed() - { - var type = typeof(Choice); - var cache = new ParameterBindingMethodCache(preferNonGenericEnumParseOverload: true); - var methodFound = cache.FindTryParseMethod(type); + [Fact] + public void FindTryParseStringMethod_WorksForEnumsWhenNonGenericEnumParseIsUsed() + { + var type = typeof(Choice); + var cache = new ParameterBindingMethodCache(preferNonGenericEnumParseOverload: true); + var methodFound = cache.FindTryParseMethod(type); - Assert.NotNull(methodFound); + Assert.NotNull(methodFound); - var parsedValue = Expression.Variable(type, "parsedValue"); - var block = methodFound!(parsedValue) as BlockExpression; - Assert.NotNull(block); - Assert.Equal(typeof(bool), block!.Type); + var parsedValue = Expression.Variable(type, "parsedValue"); + var block = methodFound!(parsedValue) as BlockExpression; + Assert.NotNull(block); + Assert.Equal(typeof(bool), block!.Type); - var parseEnum = Expression.Lambda>(Expression.Block(new[] { parsedValue }, - block, - parsedValue), ParameterBindingMethodCache.TempSourceStringExpr).Compile(); + var parseEnum = Expression.Lambda>(Expression.Block(new[] { parsedValue }, + block, + parsedValue), ParameterBindingMethodCache.TempSourceStringExpr).Compile(); - Assert.Equal(Choice.One, parseEnum("One")); - Assert.Equal(Choice.Two, parseEnum("Two")); - Assert.Equal(Choice.Three, parseEnum("Three")); - } + Assert.Equal(Choice.One, parseEnum("One")); + Assert.Equal(Choice.Two, parseEnum("Two")); + Assert.Equal(Choice.Three, parseEnum("Three")); + } - [Fact] - public async Task FindBindAsyncMethod_FindsCorrectMethodOnClass() - { - var type = typeof(BindAsyncRecord); - var cache = new ParameterBindingMethodCache(); - var parameter = new MockParameterInfo(type, "bindAsyncRecord"); - var methodFound = cache.FindBindAsyncMethod(parameter); + [Fact] + public async Task FindBindAsyncMethod_FindsCorrectMethodOnClass() + { + var type = typeof(BindAsyncRecord); + var cache = new ParameterBindingMethodCache(); + var parameter = new MockParameterInfo(type, "bindAsyncRecord"); + var methodFound = cache.FindBindAsyncMethod(parameter); - Assert.NotNull(methodFound.Expression); - Assert.Equal(2, methodFound.ParamCount); + Assert.NotNull(methodFound.Expression); + Assert.Equal(2, methodFound.ParamCount); - var parsedValue = Expression.Variable(type, "parsedValue"); + var parsedValue = Expression.Variable(type, "parsedValue"); - var parseHttpContext = Expression.Lambda>>( - Expression.Block(new[] { parsedValue }, methodFound.Expression!), - ParameterBindingMethodCache.HttpContextExpr).Compile(); + var parseHttpContext = Expression.Lambda>>( + Expression.Block(new[] { parsedValue }, methodFound.Expression!), + ParameterBindingMethodCache.HttpContextExpr).Compile(); - var httpContext = new DefaultHttpContext - { - Request = + var httpContext = new DefaultHttpContext + { + Request = { Headers = { ["ETag"] = "42", }, }, - }; + }; - Assert.Equal(new BindAsyncRecord(42), await parseHttpContext(httpContext)); - } + Assert.Equal(new BindAsyncRecord(42), await parseHttpContext(httpContext)); + } - [Fact] - public async Task FindBindAsyncMethod_FindsSingleArgBindAsync() - { - var type = typeof(BindAsyncSingleArgStruct); - var cache = new ParameterBindingMethodCache(); - var parameter = new MockParameterInfo(type, "bindAsyncSingleArgStruct"); - var methodFound = cache.FindBindAsyncMethod(parameter); + [Fact] + public async Task FindBindAsyncMethod_FindsSingleArgBindAsync() + { + var type = typeof(BindAsyncSingleArgStruct); + var cache = new ParameterBindingMethodCache(); + var parameter = new MockParameterInfo(type, "bindAsyncSingleArgStruct"); + var methodFound = cache.FindBindAsyncMethod(parameter); - Assert.NotNull(methodFound.Expression); - Assert.Equal(1, methodFound.ParamCount); + Assert.NotNull(methodFound.Expression); + Assert.Equal(1, methodFound.ParamCount); - var parsedValue = Expression.Variable(type, "parsedValue"); + var parsedValue = Expression.Variable(type, "parsedValue"); - var parseHttpContext = Expression.Lambda>>( - Expression.Block(new[] { parsedValue }, methodFound.Expression!), - ParameterBindingMethodCache.HttpContextExpr).Compile(); + var parseHttpContext = Expression.Lambda>>( + Expression.Block(new[] { parsedValue }, methodFound.Expression!), + ParameterBindingMethodCache.HttpContextExpr).Compile(); - var httpContext = new DefaultHttpContext - { - Request = + var httpContext = new DefaultHttpContext + { + Request = { Headers = { ["ETag"] = "42", }, }, - }; + }; - Assert.Equal(new BindAsyncSingleArgStruct(42), await parseHttpContext(httpContext)); - } + Assert.Equal(new BindAsyncSingleArgStruct(42), await parseHttpContext(httpContext)); + } - public static IEnumerable BindAsyncParameterInfoData + public static IEnumerable BindAsyncParameterInfoData + { + get { - get + return new[] { - return new[] - { new[] { GetFirstParameter((BindAsyncRecord arg) => BindAsyncRecordMethod(arg)), @@ -303,725 +303,724 @@ namespace Microsoft.AspNetCore.Http.Extensions.Tests GetFirstParameter((BindAsyncFromInterfaceWithParameterInfo arg) => BindAsyncFromInterfaceWithParameterInfoMethod(arg)) }, }; - } } + } - [Theory] - [MemberData(nameof(BindAsyncParameterInfoData))] - public void HasBindAsyncMethod_ReturnsTrueWhenMethodExists(ParameterInfo parameterInfo) - { - Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); - } + [Theory] + [MemberData(nameof(BindAsyncParameterInfoData))] + public void HasBindAsyncMethod_ReturnsTrueWhenMethodExists(ParameterInfo parameterInfo) + { + Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); + } - [Fact] - public void HasBindAsyncMethod_ReturnsTrueForNullableReturningBindAsyncStructMethod() - { - var parameterInfo = GetFirstParameter((NullableReturningBindAsyncStruct arg) => NullableReturningBindAsyncStructMethod(arg)); - Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); - } + [Fact] + public void HasBindAsyncMethod_ReturnsTrueForNullableReturningBindAsyncStructMethod() + { + var parameterInfo = GetFirstParameter((NullableReturningBindAsyncStruct arg) => NullableReturningBindAsyncStructMethod(arg)); + Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); + } - [Fact] - public void FindBindAsyncMethod_FindsNonNullableReturningBindAsyncMethodGivenNullableType() - { - var parameterInfo = GetFirstParameter((BindAsyncStruct? arg) => BindAsyncNullableStructMethod(arg)); - Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); - } + [Fact] + public void FindBindAsyncMethod_FindsNonNullableReturningBindAsyncMethodGivenNullableType() + { + var parameterInfo = GetFirstParameter((BindAsyncStruct? arg) => BindAsyncNullableStructMethod(arg)); + Assert.True(new ParameterBindingMethodCache().HasBindAsyncMethod(parameterInfo)); + } - [Fact] - public async Task FindBindAsyncMethod_FindsFallbackMethodWhenPreferredMethodsReturnTypeIsWrong() - { - var parameterInfo = GetFirstParameter((BindAsyncFallsBack? arg) => BindAsyncFallbackMethod(arg)); - var cache = new ParameterBindingMethodCache(); - Assert.True(cache.HasBindAsyncMethod(parameterInfo)); - var methodFound = cache.FindBindAsyncMethod(parameterInfo); + [Fact] + public async Task FindBindAsyncMethod_FindsFallbackMethodWhenPreferredMethodsReturnTypeIsWrong() + { + var parameterInfo = GetFirstParameter((BindAsyncFallsBack? arg) => BindAsyncFallbackMethod(arg)); + var cache = new ParameterBindingMethodCache(); + Assert.True(cache.HasBindAsyncMethod(parameterInfo)); + var methodFound = cache.FindBindAsyncMethod(parameterInfo); - var parseHttpContext = Expression.Lambda>>(methodFound.Expression!, - ParameterBindingMethodCache.HttpContextExpr).Compile(); + var parseHttpContext = Expression.Lambda>>(methodFound.Expression!, + ParameterBindingMethodCache.HttpContextExpr).Compile(); - var httpContext = new DefaultHttpContext(); + var httpContext = new DefaultHttpContext(); - Assert.Null(await parseHttpContext(httpContext)); - } + Assert.Null(await parseHttpContext(httpContext)); + } - [Fact] - public async Task FindBindAsyncMethod_FindsFallbackMethodFromInheritedWhenPreferredMethodIsInvalid() - { - var parameterInfo = GetFirstParameter((BindAsyncBadMethod? arg) => BindAsyncBadMethodMethod(arg)); - var cache = new ParameterBindingMethodCache(); - Assert.True(cache.HasBindAsyncMethod(parameterInfo)); - var methodFound = cache.FindBindAsyncMethod(parameterInfo); + [Fact] + public async Task FindBindAsyncMethod_FindsFallbackMethodFromInheritedWhenPreferredMethodIsInvalid() + { + var parameterInfo = GetFirstParameter((BindAsyncBadMethod? arg) => BindAsyncBadMethodMethod(arg)); + var cache = new ParameterBindingMethodCache(); + Assert.True(cache.HasBindAsyncMethod(parameterInfo)); + var methodFound = cache.FindBindAsyncMethod(parameterInfo); - var parseHttpContext = Expression.Lambda>>(methodFound.Expression!, - ParameterBindingMethodCache.HttpContextExpr).Compile(); + var parseHttpContext = Expression.Lambda>>(methodFound.Expression!, + ParameterBindingMethodCache.HttpContextExpr).Compile(); - var httpContext = new DefaultHttpContext(); + var httpContext = new DefaultHttpContext(); - Assert.Null(await parseHttpContext(httpContext)); - } + Assert.Null(await parseHttpContext(httpContext)); + } - [Theory] - [InlineData(typeof(InvalidVoidReturnTryParseStruct))] - [InlineData(typeof(InvalidVoidReturnTryParseClass))] - [InlineData(typeof(InvalidWrongTypeTryParseStruct))] - [InlineData(typeof(InvalidWrongTypeTryParseClass))] - [InlineData(typeof(InvalidTryParseNullableStruct))] - [InlineData(typeof(InvalidTooFewArgsTryParseStruct))] - [InlineData(typeof(InvalidTooFewArgsTryParseClass))] - [InlineData(typeof(InvalidNonStaticTryParseStruct))] - [InlineData(typeof(InvalidNonStaticTryParseClass))] - [InlineData(typeof(TryParseWrongTypeInheritClass))] - [InlineData(typeof(TryParseWrongTypeFromInterface))] - public void FindTryParseMethod_ThrowsIfInvalidTryParseOnType(Type type) - { - var ex = Assert.Throws( - () => new ParameterBindingMethodCache().FindTryParseMethod(type)); - Assert.StartsWith($"TryParse method found on {TypeNameHelper.GetTypeDisplayName(type, fullName: false)} with incorrect format. Must be a static method with format", ex.Message); - Assert.Contains($"bool TryParse(string, IFormatProvider, out {TypeNameHelper.GetTypeDisplayName(type, fullName: false)})", ex.Message); - Assert.Contains($"bool TryParse(string, out {TypeNameHelper.GetTypeDisplayName(type, fullName: false)})", ex.Message); - } + [Theory] + [InlineData(typeof(InvalidVoidReturnTryParseStruct))] + [InlineData(typeof(InvalidVoidReturnTryParseClass))] + [InlineData(typeof(InvalidWrongTypeTryParseStruct))] + [InlineData(typeof(InvalidWrongTypeTryParseClass))] + [InlineData(typeof(InvalidTryParseNullableStruct))] + [InlineData(typeof(InvalidTooFewArgsTryParseStruct))] + [InlineData(typeof(InvalidTooFewArgsTryParseClass))] + [InlineData(typeof(InvalidNonStaticTryParseStruct))] + [InlineData(typeof(InvalidNonStaticTryParseClass))] + [InlineData(typeof(TryParseWrongTypeInheritClass))] + [InlineData(typeof(TryParseWrongTypeFromInterface))] + public void FindTryParseMethod_ThrowsIfInvalidTryParseOnType(Type type) + { + var ex = Assert.Throws( + () => new ParameterBindingMethodCache().FindTryParseMethod(type)); + Assert.StartsWith($"TryParse method found on {TypeNameHelper.GetTypeDisplayName(type, fullName: false)} with incorrect format. Must be a static method with format", ex.Message); + Assert.Contains($"bool TryParse(string, IFormatProvider, out {TypeNameHelper.GetTypeDisplayName(type, fullName: false)})", ex.Message); + Assert.Contains($"bool TryParse(string, out {TypeNameHelper.GetTypeDisplayName(type, fullName: false)})", ex.Message); + } - [Fact] - public void FindTryParseMethod_ThrowsIfMultipleInterfacesMatch() - { - var ex = Assert.Throws( - () => new ParameterBindingMethodCache().FindTryParseMethod(typeof(TryParseFromMultipleInterfaces))); - Assert.Equal("TryParseFromMultipleInterfaces implements multiple interfaces defining a static Boolean TryParse(System.String, TryParseFromMultipleInterfaces ByRef) method causing ambiguity.", ex.Message); - } + [Fact] + public void FindTryParseMethod_ThrowsIfMultipleInterfacesMatch() + { + var ex = Assert.Throws( + () => new ParameterBindingMethodCache().FindTryParseMethod(typeof(TryParseFromMultipleInterfaces))); + Assert.Equal("TryParseFromMultipleInterfaces implements multiple interfaces defining a static Boolean TryParse(System.String, TryParseFromMultipleInterfaces ByRef) method causing ambiguity.", ex.Message); + } - [Theory] - [InlineData(typeof(TryParseClassWithGoodAndBad))] - [InlineData(typeof(TryParseStructWithGoodAndBad))] - public void FindTryParseMethod_IgnoresInvalidTryParseIfGoodOneFound(Type type) - { - var method = new ParameterBindingMethodCache().FindTryParseMethod(type); - Assert.NotNull(method); - } + [Theory] + [InlineData(typeof(TryParseClassWithGoodAndBad))] + [InlineData(typeof(TryParseStructWithGoodAndBad))] + public void FindTryParseMethod_IgnoresInvalidTryParseIfGoodOneFound(Type type) + { + var method = new ParameterBindingMethodCache().FindTryParseMethod(type); + Assert.NotNull(method); + } - [Theory] - [InlineData(typeof(InvalidWrongReturnBindAsyncStruct))] - [InlineData(typeof(InvalidWrongReturnBindAsyncClass))] - [InlineData(typeof(InvalidWrongParamBindAsyncStruct))] - [InlineData(typeof(InvalidWrongParamBindAsyncClass))] - [InlineData(typeof(BindAsyncWrongTypeInherit))] - [InlineData(typeof(BindAsyncWithParameterInfoWrongTypeInherit))] - [InlineData(typeof(BindAsyncWrongTypeFromInterface))] - [InlineData(typeof(BindAsyncBothBadMethods))] - public void FindBindAsyncMethod_ThrowsIfInvalidBindAsyncOnType(Type type) - { - var cache = new ParameterBindingMethodCache(); - var parameter = new MockParameterInfo(type, "anything"); - var ex = Assert.Throws( - () => cache.FindBindAsyncMethod(parameter)); - Assert.StartsWith($"BindAsync method found on {TypeNameHelper.GetTypeDisplayName(type, fullName: false)} with incorrect format. Must be a static method with format", ex.Message); - Assert.Contains($"ValueTask<{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}> BindAsync(HttpContext context, ParameterInfo parameter)", ex.Message); - Assert.Contains($"ValueTask<{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}> BindAsync(HttpContext context)", ex.Message); - Assert.Contains($"ValueTask<{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}?> BindAsync(HttpContext context, ParameterInfo parameter)", ex.Message); - Assert.Contains($"ValueTask<{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}?> BindAsync(HttpContext context)", ex.Message); - } + [Theory] + [InlineData(typeof(InvalidWrongReturnBindAsyncStruct))] + [InlineData(typeof(InvalidWrongReturnBindAsyncClass))] + [InlineData(typeof(InvalidWrongParamBindAsyncStruct))] + [InlineData(typeof(InvalidWrongParamBindAsyncClass))] + [InlineData(typeof(BindAsyncWrongTypeInherit))] + [InlineData(typeof(BindAsyncWithParameterInfoWrongTypeInherit))] + [InlineData(typeof(BindAsyncWrongTypeFromInterface))] + [InlineData(typeof(BindAsyncBothBadMethods))] + public void FindBindAsyncMethod_ThrowsIfInvalidBindAsyncOnType(Type type) + { + var cache = new ParameterBindingMethodCache(); + var parameter = new MockParameterInfo(type, "anything"); + var ex = Assert.Throws( + () => cache.FindBindAsyncMethod(parameter)); + Assert.StartsWith($"BindAsync method found on {TypeNameHelper.GetTypeDisplayName(type, fullName: false)} with incorrect format. Must be a static method with format", ex.Message); + Assert.Contains($"ValueTask<{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}> BindAsync(HttpContext context, ParameterInfo parameter)", ex.Message); + Assert.Contains($"ValueTask<{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}> BindAsync(HttpContext context)", ex.Message); + Assert.Contains($"ValueTask<{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}?> BindAsync(HttpContext context, ParameterInfo parameter)", ex.Message); + Assert.Contains($"ValueTask<{TypeNameHelper.GetTypeDisplayName(type, fullName: false)}?> BindAsync(HttpContext context)", ex.Message); + } - [Fact] - public void FindBindAsyncMethod_ThrowsIfMultipleInterfacesMatch() - { - var cache = new ParameterBindingMethodCache(); - var parameter = new MockParameterInfo(typeof(BindAsyncFromMultipleInterfaces), "anything"); - var ex = Assert.Throws(() => cache.FindBindAsyncMethod(parameter)); - Assert.Equal("BindAsyncFromMultipleInterfaces implements multiple interfaces defining a static System.Threading.Tasks.ValueTask`1[Microsoft.AspNetCore.Http.Extensions.Tests.ParameterBindingMethodCacheTests+BindAsyncFromMultipleInterfaces] BindAsync(Microsoft.AspNetCore.Http.HttpContext) method causing ambiguity.", ex.Message); - } + [Fact] + public void FindBindAsyncMethod_ThrowsIfMultipleInterfacesMatch() + { + var cache = new ParameterBindingMethodCache(); + var parameter = new MockParameterInfo(typeof(BindAsyncFromMultipleInterfaces), "anything"); + var ex = Assert.Throws(() => cache.FindBindAsyncMethod(parameter)); + Assert.Equal("BindAsyncFromMultipleInterfaces implements multiple interfaces defining a static System.Threading.Tasks.ValueTask`1[Microsoft.AspNetCore.Http.Extensions.Tests.ParameterBindingMethodCacheTests+BindAsyncFromMultipleInterfaces] BindAsync(Microsoft.AspNetCore.Http.HttpContext) method causing ambiguity.", ex.Message); + } - [Theory] - [InlineData(typeof(BindAsyncStructWithGoodAndBad))] - [InlineData(typeof(BindAsyncClassWithGoodAndBad))] - public void FindBindAsyncMethod_IgnoresInvalidBindAsyncIfGoodOneFound(Type type) - { - var cache = new ParameterBindingMethodCache(); - var parameter = new MockParameterInfo(type, "anything"); - var (expression, _) = cache.FindBindAsyncMethod(parameter); - Assert.NotNull(expression); - } + [Theory] + [InlineData(typeof(BindAsyncStructWithGoodAndBad))] + [InlineData(typeof(BindAsyncClassWithGoodAndBad))] + public void FindBindAsyncMethod_IgnoresInvalidBindAsyncIfGoodOneFound(Type type) + { + var cache = new ParameterBindingMethodCache(); + var parameter = new MockParameterInfo(type, "anything"); + var (expression, _) = cache.FindBindAsyncMethod(parameter); + Assert.NotNull(expression); + } - enum Choice - { - One, - Two, - Three - } + enum Choice + { + One, + Two, + Three + } - private static void TryParseStringRecordMethod(TryParseStringRecord arg) { } - private static void TryParseStringStructMethod(TryParseStringStruct arg) { } - private static void TryParseStringNullableStructMethod(TryParseStringStruct? arg) { } - - private static void BindAsyncRecordMethod(BindAsyncRecord arg) { } - private static void BindAsyncStructMethod(BindAsyncStruct arg) { } - private static void BindAsyncNullableStructMethod(BindAsyncStruct? arg) { } - private static void NullableReturningBindAsyncStructMethod(NullableReturningBindAsyncStruct arg) { } - - private static void BindAsyncSingleArgRecordMethod(BindAsyncSingleArgRecord arg) { } - private static void BindAsyncSingleArgStructMethod(BindAsyncSingleArgStruct arg) { } - private static void InheritBindAsyncMethod(InheritBindAsync arg) { } - private static void InheritBindAsyncWithParameterInfoMethod(InheritBindAsyncWithParameterInfo args) { } - private static void BindAsyncFromInterfaceMethod(BindAsyncFromInterface arg) { } - private static void BindAsyncFromGrandparentInterfaceMethod(BindAsyncFromGrandparentInterface arg) { } - private static void BindAsyncDirectlyAndFromInterfaceMethod(BindAsyncDirectlyAndFromInterface arg) { } - private static void BindAsyncFromClassAndInterfaceMethod(BindAsyncFromClassAndInterface arg) { } - private static void BindAsyncFromInterfaceWithParameterInfoMethod(BindAsyncFromInterfaceWithParameterInfo args) { } - private static void BindAsyncFallbackMethod(BindAsyncFallsBack? arg) { } - private static void BindAsyncBadMethodMethod(BindAsyncBadMethod? arg) { } - - private static ParameterInfo GetFirstParameter(Expression> expr) - { - var mc = (MethodCallExpression)expr.Body; - return mc.Method.GetParameters()[0]; - } + private static void TryParseStringRecordMethod(TryParseStringRecord arg) { } + private static void TryParseStringStructMethod(TryParseStringStruct arg) { } + private static void TryParseStringNullableStructMethod(TryParseStringStruct? arg) { } + + private static void BindAsyncRecordMethod(BindAsyncRecord arg) { } + private static void BindAsyncStructMethod(BindAsyncStruct arg) { } + private static void BindAsyncNullableStructMethod(BindAsyncStruct? arg) { } + private static void NullableReturningBindAsyncStructMethod(NullableReturningBindAsyncStruct arg) { } + + private static void BindAsyncSingleArgRecordMethod(BindAsyncSingleArgRecord arg) { } + private static void BindAsyncSingleArgStructMethod(BindAsyncSingleArgStruct arg) { } + private static void InheritBindAsyncMethod(InheritBindAsync arg) { } + private static void InheritBindAsyncWithParameterInfoMethod(InheritBindAsyncWithParameterInfo args) { } + private static void BindAsyncFromInterfaceMethod(BindAsyncFromInterface arg) { } + private static void BindAsyncFromGrandparentInterfaceMethod(BindAsyncFromGrandparentInterface arg) { } + private static void BindAsyncDirectlyAndFromInterfaceMethod(BindAsyncDirectlyAndFromInterface arg) { } + private static void BindAsyncFromClassAndInterfaceMethod(BindAsyncFromClassAndInterface arg) { } + private static void BindAsyncFromInterfaceWithParameterInfoMethod(BindAsyncFromInterfaceWithParameterInfo args) { } + private static void BindAsyncFallbackMethod(BindAsyncFallsBack? arg) { } + private static void BindAsyncBadMethodMethod(BindAsyncBadMethod? arg) { } + + private static ParameterInfo GetFirstParameter(Expression> expr) + { + var mc = (MethodCallExpression)expr.Body; + return mc.Method.GetParameters()[0]; + } - private record TryParseStringRecord(int Value) + private record TryParseStringRecord(int Value) + { + public static bool TryParse(string? value, IFormatProvider formatProvider, out TryParseStringRecord? result) { - public static bool TryParse(string? value, IFormatProvider formatProvider, out TryParseStringRecord? result) + if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) { - if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) - { - result = null; - return false; - } - - result = new TryParseStringRecord(val); - return true; + result = null; + return false; } - } - - private record struct TryParseStringStruct(int Value) - { - public static bool TryParse(string? value, IFormatProvider formatProvider, out TryParseStringStruct result) - { - if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) - { - result = default; - return false; - } - result = new TryParseStringStruct(val); - return true; - } + result = new TryParseStringRecord(val); + return true; } + } - private record struct InvalidVoidReturnTryParseStruct(int Value) + private record struct TryParseStringStruct(int Value) + { + public static bool TryParse(string? value, IFormatProvider formatProvider, out TryParseStringStruct result) { - public static void TryParse(string? value, IFormatProvider formatProvider, out InvalidVoidReturnTryParseStruct result) + if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) { - if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) - { - result = default; - return; - } - - result = new InvalidVoidReturnTryParseStruct(val); - return; + result = default; + return false; } - } - - private record struct InvalidWrongTypeTryParseStruct(int Value) - { - public static bool TryParse(string? value, IFormatProvider formatProvider, out InvalidVoidReturnTryParseStruct result) - { - if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) - { - result = default; - return false; - } - result = new InvalidVoidReturnTryParseStruct(val); - return true; - } + result = new TryParseStringStruct(val); + return true; } + } - private record struct InvalidTryParseNullableStruct(int Value) + private record struct InvalidVoidReturnTryParseStruct(int Value) + { + public static void TryParse(string? value, IFormatProvider formatProvider, out InvalidVoidReturnTryParseStruct result) { - public static bool TryParse(string? value, IFormatProvider formatProvider, out InvalidTryParseNullableStruct? result) + if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) { - if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) - { - result = default; - return false; - } - - result = new InvalidTryParseNullableStruct(val); - return true; + result = default; + return; } + + result = new InvalidVoidReturnTryParseStruct(val); + return; } + } - private record struct InvalidTooFewArgsTryParseStruct(int Value) + private record struct InvalidWrongTypeTryParseStruct(int Value) + { + public static bool TryParse(string? value, IFormatProvider formatProvider, out InvalidVoidReturnTryParseStruct result) { - public static bool TryParse(out InvalidTooFewArgsTryParseStruct result) + if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) { result = default; return false; } + + result = new InvalidVoidReturnTryParseStruct(val); + return true; } + } - private struct TryParseStructWithGoodAndBad + private record struct InvalidTryParseNullableStruct(int Value) + { + public static bool TryParse(string? value, IFormatProvider formatProvider, out InvalidTryParseNullableStruct? result) { - public static bool TryParse(string? value, out TryParseStructWithGoodAndBad result) + if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) { - result = new(); + result = default; return false; } - public static void TryParse(out TryParseStructWithGoodAndBad result) - { - result = new(); - } + result = new InvalidTryParseNullableStruct(val); + return true; } + } - private record struct InvalidNonStaticTryParseStruct(int Value) + private record struct InvalidTooFewArgsTryParseStruct(int Value) + { + public static bool TryParse(out InvalidTooFewArgsTryParseStruct result) { - public bool TryParse(string? value, IFormatProvider formatProvider, out InvalidVoidReturnTryParseStruct result) - { - if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) - { - result = default; - return false; - } - - result = new InvalidVoidReturnTryParseStruct(val); - return true; - } + result = default; + return false; } + } - private class InvalidVoidReturnTryParseClass + private struct TryParseStructWithGoodAndBad + { + public static bool TryParse(string? value, out TryParseStructWithGoodAndBad result) { - public static void TryParse(string? value, IFormatProvider formatProvider, out InvalidVoidReturnTryParseClass result) - { - if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) - { - result = new(); - return; - } - - result = new(); - } + result = new(); + return false; } - private class InvalidWrongTypeTryParseClass + public static void TryParse(out TryParseStructWithGoodAndBad result) { - public static bool TryParse(string? value, IFormatProvider formatProvider, out InvalidVoidReturnTryParseClass result) - { - if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) - { - result = new(); - return false; - } - - result = new(); - return true; - } + result = new(); } + } - private class InvalidTooFewArgsTryParseClass + private record struct InvalidNonStaticTryParseStruct(int Value) + { + public bool TryParse(string? value, IFormatProvider formatProvider, out InvalidVoidReturnTryParseStruct result) { - public static bool TryParse(out InvalidTooFewArgsTryParseClass result) + if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) { - result = new(); + result = default; return false; } + + result = new InvalidVoidReturnTryParseStruct(val); + return true; } + } - private class TryParseClassWithGoodAndBad + private class InvalidVoidReturnTryParseClass + { + public static void TryParse(string? value, IFormatProvider formatProvider, out InvalidVoidReturnTryParseClass result) { - public static bool TryParse(string? value, out TryParseClassWithGoodAndBad result) + if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) { result = new(); - return false; + return; } - public static bool TryParse(out TryParseClassWithGoodAndBad result) - { - result = new(); - return false; - } + result = new(); } + } - private class InvalidNonStaticTryParseClass + private class InvalidWrongTypeTryParseClass + { + public static bool TryParse(string? value, IFormatProvider formatProvider, out InvalidVoidReturnTryParseClass result) { - public bool TryParse(string? value, IFormatProvider formatProvider, out InvalidNonStaticTryParseClass result) + if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) { - if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) - { - result = new(); - return false; - } - result = new(); - return true; - } - } - - private record TryParseNoFormatProviderRecord(int Value) - { - public static bool TryParse(string? value, out TryParseNoFormatProviderRecord? result) - { - if (!int.TryParse(value, out var val)) - { - result = null; - return false; - } - - result = new TryParseNoFormatProviderRecord(val); - return true; + return false; } - } - - private record struct TryParseNoFormatProviderStruct(int Value) - { - public static bool TryParse(string? value, out TryParseNoFormatProviderStruct result) - { - if (!int.TryParse(value, out var val)) - { - result = default; - return false; - } - result = new TryParseNoFormatProviderStruct(val); - return true; - } + result = new(); + return true; } + } - private class BaseTryParseClass + private class InvalidTooFewArgsTryParseClass + { + public static bool TryParse(out InvalidTooFewArgsTryParseClass result) { - public static bool TryParse(string? value, out T? result) - { - result = default(T); - return false; - } + result = new(); + return false; } + } - private class TryParseInheritClass : BaseTryParseClass + private class TryParseClassWithGoodAndBad + { + public static bool TryParse(string? value, out TryParseClassWithGoodAndBad result) { + result = new(); + return false; } - // using wrong T on purpose - private class TryParseWrongTypeInheritClass : BaseTryParseClass + public static bool TryParse(out TryParseClassWithGoodAndBad result) { + result = new(); + return false; } + } - private class BaseTryParseClassWithFormatProvider + private class InvalidNonStaticTryParseClass + { + public bool TryParse(string? value, IFormatProvider formatProvider, out InvalidNonStaticTryParseClass result) { - public static bool TryParse(string? value, IFormatProvider formatProvider, out T? result) + if (!int.TryParse(value, NumberStyles.Integer, formatProvider, out var val)) { - result = default(T); + result = new(); return false; } - } - private class TryParseInheritClassWithFormatProvider : BaseTryParseClassWithFormatProvider - { + result = new(); + return true; } + } - private interface ITryParse + private record TryParseNoFormatProviderRecord(int Value) + { + public static bool TryParse(string? value, out TryParseNoFormatProviderRecord? result) { - static bool TryParse(string? value, out T? result) + if (!int.TryParse(value, out var val)) { - result = default(T); + result = null; return false; } + + result = new TryParseNoFormatProviderRecord(val); + return true; } + } - private interface ITryParse2 + private record struct TryParseNoFormatProviderStruct(int Value) + { + public static bool TryParse(string? value, out TryParseNoFormatProviderStruct result) { - static bool TryParse(string? value, out T? result) + if (!int.TryParse(value, out var val)) { - result = default(T); + result = default; return false; } - } - private interface IImplementITryParse : ITryParse - { + result = new TryParseNoFormatProviderStruct(val); + return true; } + } - private class TryParseFromInterface : ITryParse + private class BaseTryParseClass + { + public static bool TryParse(string? value, out T? result) { + result = default(T); + return false; } + } - private class TryParseFromGrandparentInterface : IImplementITryParse - { - } + private class TryParseInheritClass : BaseTryParseClass + { + } - private class TryParseDirectlyAndFromInterface : ITryParse - { - static bool TryParse(string? value, out TryParseDirectlyAndFromInterface? result) - { - result = null; - return false; - } - } + // using wrong T on purpose + private class TryParseWrongTypeInheritClass : BaseTryParseClass + { + } - private class TryParseFromClassAndInterface - : BaseTryParseClass, - ITryParse + private class BaseTryParseClassWithFormatProvider + { + public static bool TryParse(string? value, IFormatProvider formatProvider, out T? result) { + result = default(T); + return false; } + } + + private class TryParseInheritClassWithFormatProvider : BaseTryParseClassWithFormatProvider + { + } - private class TryParseFromMultipleInterfaces - : ITryParse, - ITryParse2 + private interface ITryParse + { + static bool TryParse(string? value, out T? result) { + result = default(T); + return false; } + } - // using wrong T on purpose - private class TryParseWrongTypeFromInterface : ITryParse + private interface ITryParse2 + { + static bool TryParse(string? value, out T? result) { + result = default(T); + return false; } + } + + private interface IImplementITryParse : ITryParse + { + } + + private class TryParseFromInterface : ITryParse + { + } + + private class TryParseFromGrandparentInterface : IImplementITryParse + { + } - private interface ITryParseWithFormatProvider + private class TryParseDirectlyAndFromInterface : ITryParse + { + static bool TryParse(string? value, out TryParseDirectlyAndFromInterface? result) { - public static bool TryParse(string? value, IFormatProvider formatProvider, out T? result) - { - result = default(T); - return false; - } + result = null; + return false; } + } + + private class TryParseFromClassAndInterface + : BaseTryParseClass, + ITryParse + { + } - private class TryParseFromInterfaceWithFormatProvider : ITryParseWithFormatProvider + private class TryParseFromMultipleInterfaces + : ITryParse, + ITryParse2 + { + } + + // using wrong T on purpose + private class TryParseWrongTypeFromInterface : ITryParse + { + } + + private interface ITryParseWithFormatProvider + { + public static bool TryParse(string? value, IFormatProvider formatProvider, out T? result) { + result = default(T); + return false; } + } - private record BindAsyncRecord(int Value) - { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) - { - Assert.Equal(typeof(BindAsyncRecord), parameter.ParameterType); - Assert.Equal("bindAsyncRecord", parameter.Name); + private class TryParseFromInterfaceWithFormatProvider : ITryParseWithFormatProvider + { + } - if (!int.TryParse(context.Request.Headers.ETag, out var val)) - { - return new(result: null); - } + private record BindAsyncRecord(int Value) + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) + { + Assert.Equal(typeof(BindAsyncRecord), parameter.ParameterType); + Assert.Equal("bindAsyncRecord", parameter.Name); - return new(result: new(val)); + if (!int.TryParse(context.Request.Headers.ETag, out var val)) + { + return new(result: null); } + + return new(result: new(val)); } + } - private record struct BindAsyncStruct(int Value) + private record struct BindAsyncStruct(int Value) + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) - { - Assert.Equal(typeof(BindAsyncStruct), parameter.ParameterType); - Assert.Equal("bindAsyncStruct", parameter.Name); + Assert.Equal(typeof(BindAsyncStruct), parameter.ParameterType); + Assert.Equal("bindAsyncStruct", parameter.Name); - if (!int.TryParse(context.Request.Headers.ETag, out var val)) - { - throw new BadHttpRequestException("The request is missing the required ETag header."); - } - - return new(result: new(val)); + if (!int.TryParse(context.Request.Headers.ETag, out var val)) + { + throw new BadHttpRequestException("The request is missing the required ETag header."); } - } - private record struct NullableReturningBindAsyncStruct(int Value) - { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => - throw new NotImplementedException(); + return new(result: new(val)); } + } + + private record struct NullableReturningBindAsyncStruct(int Value) + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => + throw new NotImplementedException(); + } - private record BindAsyncSingleArgRecord(int Value) + private record BindAsyncSingleArgRecord(int Value) + { + public static ValueTask BindAsync(HttpContext context) { - public static ValueTask BindAsync(HttpContext context) + if (!int.TryParse(context.Request.Headers.ETag, out var val)) { - if (!int.TryParse(context.Request.Headers.ETag, out var val)) - { - return new(result: null); - } - - return new(result: new(val)); + return new(result: null); } + + return new(result: new(val)); } + } - private record struct BindAsyncSingleArgStruct(int Value) + private record struct BindAsyncSingleArgStruct(int Value) + { + public static ValueTask BindAsync(HttpContext context) { - public static ValueTask BindAsync(HttpContext context) + if (!int.TryParse(context.Request.Headers.ETag, out var val)) { - if (!int.TryParse(context.Request.Headers.ETag, out var val)) - { - throw new BadHttpRequestException("The request is missing the required ETag header."); - } - - return new(result: new(val)); + throw new BadHttpRequestException("The request is missing the required ETag header."); } - } - private record struct InvalidWrongReturnBindAsyncStruct(int Value) - { - public static Task BindAsync(HttpContext context, ParameterInfo parameter) => - throw new NotImplementedException(); + return new(result: new(val)); } + } - private class InvalidWrongReturnBindAsyncClass - { - public static Task BindAsync(HttpContext context, ParameterInfo parameter) => - throw new NotImplementedException(); - } + private record struct InvalidWrongReturnBindAsyncStruct(int Value) + { + public static Task BindAsync(HttpContext context, ParameterInfo parameter) => + throw new NotImplementedException(); + } - private record struct InvalidWrongParamBindAsyncStruct(int Value) - { - public static ValueTask BindAsync(ParameterInfo parameter) => - throw new NotImplementedException(); - } + private class InvalidWrongReturnBindAsyncClass + { + public static Task BindAsync(HttpContext context, ParameterInfo parameter) => + throw new NotImplementedException(); + } - private class InvalidWrongParamBindAsyncClass - { - public static Task BindAsync(ParameterInfo parameter) => - throw new NotImplementedException(); - } + private record struct InvalidWrongParamBindAsyncStruct(int Value) + { + public static ValueTask BindAsync(ParameterInfo parameter) => + throw new NotImplementedException(); + } - private record struct BindAsyncStructWithGoodAndBad(int Value) - { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => - throw new NotImplementedException(); + private class InvalidWrongParamBindAsyncClass + { + public static Task BindAsync(ParameterInfo parameter) => + throw new NotImplementedException(); + } - public static ValueTask BindAsync(ParameterInfo parameter) => - throw new NotImplementedException(); - } + private record struct BindAsyncStructWithGoodAndBad(int Value) + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => + throw new NotImplementedException(); - private class BindAsyncClassWithGoodAndBad - { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => - throw new NotImplementedException(); + public static ValueTask BindAsync(ParameterInfo parameter) => + throw new NotImplementedException(); + } - public static ValueTask BindAsync(ParameterInfo parameter) => - throw new NotImplementedException(); - } + private class BindAsyncClassWithGoodAndBad + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => + throw new NotImplementedException(); - private class BaseBindAsync - { - public static ValueTask BindAsync(HttpContext context) - { - return new(default(T)); - } - } + public static ValueTask BindAsync(ParameterInfo parameter) => + throw new NotImplementedException(); + } - private class InheritBindAsync : BaseBindAsync + private class BaseBindAsync + { + public static ValueTask BindAsync(HttpContext context) { + return new(default(T)); } + } - // Using wrong T on purpose - private class BindAsyncWrongTypeInherit : BaseBindAsync - { - } + private class InheritBindAsync : BaseBindAsync + { + } - private class BaseBindAsyncWithParameterInfo - { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) - { - return new(default(T)); - } - } + // Using wrong T on purpose + private class BindAsyncWrongTypeInherit : BaseBindAsync + { + } - private class InheritBindAsyncWithParameterInfo : BaseBindAsyncWithParameterInfo + private class BaseBindAsyncWithParameterInfo + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) { + return new(default(T)); } + } - // Using wrong T on purpose - private class BindAsyncWithParameterInfoWrongTypeInherit : BaseBindAsyncWithParameterInfo - { - } + private class InheritBindAsyncWithParameterInfo : BaseBindAsyncWithParameterInfo + { + } - private interface IBindAsync - { - static ValueTask BindAsync(HttpContext context) - { - return new(default(T)); - } - } + // Using wrong T on purpose + private class BindAsyncWithParameterInfoWrongTypeInherit : BaseBindAsyncWithParameterInfo + { + } - private interface IBindAsync2 + private interface IBindAsync + { + static ValueTask BindAsync(HttpContext context) { - static ValueTask BindAsync(HttpContext context) - { - return new(default(T)); - } + return new(default(T)); } + } - private interface IImeplmentIBindAsync : IBindAsync + private interface IBindAsync2 + { + static ValueTask BindAsync(HttpContext context) { + return new(default(T)); } + } - private class BindAsyncFromInterface : IBindAsync - { - } + private interface IImeplmentIBindAsync : IBindAsync + { + } - private class BindAsyncFromGrandparentInterface : IImeplmentIBindAsync - { - } + private class BindAsyncFromInterface : IBindAsync + { + } - private class BindAsyncDirectlyAndFromInterface : IBindAsync - { - static ValueTask BindAsync(HttpContext context) - { - return new(result: null); - } - } + private class BindAsyncFromGrandparentInterface : IImeplmentIBindAsync + { + } - private class BindAsyncFromClassAndInterface - : BaseBindAsync, - IBindAsync + private class BindAsyncDirectlyAndFromInterface : IBindAsync + { + static ValueTask BindAsync(HttpContext context) { + return new(result: null); } + } - private class BindAsyncFromMultipleInterfaces - : IBindAsync, - IBindAsync2 - { - } + private class BindAsyncFromClassAndInterface + : BaseBindAsync, + IBindAsync + { + } - // using wrong T on purpose - private class BindAsyncWrongTypeFromInterface : IBindAsync - { - } + private class BindAsyncFromMultipleInterfaces + : IBindAsync, + IBindAsync2 + { + } - private interface IBindAsyncWithParameterInfo - { - static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) - { - return new(default(T)); - } - } + // using wrong T on purpose + private class BindAsyncWrongTypeFromInterface : IBindAsync + { + } - private class BindAsyncFromInterfaceWithParameterInfo : IBindAsync + private interface IBindAsyncWithParameterInfo + { + static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) { + return new(default(T)); } + } - private class BindAsyncFallsBack - { - public static void BindAsync(HttpContext context, ParameterInfo parameter) - => throw new NotImplementedException(); + private class BindAsyncFromInterfaceWithParameterInfo : IBindAsync + { + } - public static ValueTask BindAsync(HttpContext context) - { - return new(result: null); - } - } + private class BindAsyncFallsBack + { + public static void BindAsync(HttpContext context, ParameterInfo parameter) + => throw new NotImplementedException(); - private class BindAsyncBadMethod : IBindAsyncWithParameterInfo + public static ValueTask BindAsync(HttpContext context) { - public static void BindAsync(HttpContext context, ParameterInfo parameter) - => throw new NotImplementedException(); + return new(result: null); } + } - private class BindAsyncBothBadMethods - { - public static void BindAsync(HttpContext context, ParameterInfo parameter) - => throw new NotImplementedException(); + private class BindAsyncBadMethod : IBindAsyncWithParameterInfo + { + public static void BindAsync(HttpContext context, ParameterInfo parameter) + => throw new NotImplementedException(); + } - public static void BindAsync(HttpContext context) - => throw new NotImplementedException(); - } + private class BindAsyncBothBadMethods + { + public static void BindAsync(HttpContext context, ParameterInfo parameter) + => throw new NotImplementedException(); + + public static void BindAsync(HttpContext context) + => throw new NotImplementedException(); + } - private class MockParameterInfo : ParameterInfo + private class MockParameterInfo : ParameterInfo + { + public MockParameterInfo(Type type, string name) { - public MockParameterInfo(Type type, string name) - { - ClassImpl = type; - NameImpl = name; - } + ClassImpl = type; + NameImpl = name; } } } diff --git a/src/Http/Http.Extensions/test/ProblemDetailsJsonConverterTest.cs b/src/Http/Http.Extensions/test/ProblemDetailsJsonConverterTest.cs index 94feff570e..76b0c99b85 100644 --- a/src/Http/Http.Extensions/test/ProblemDetailsJsonConverterTest.cs +++ b/src/Http/Http.Extensions/test/ProblemDetailsJsonConverterTest.cs @@ -8,173 +8,172 @@ using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Mvc; using Xunit; -namespace Microsoft.AspNetCore.Http.Extensions +namespace Microsoft.AspNetCore.Http.Extensions; + +public class ProblemDetailsJsonConverterTest { - public class ProblemDetailsJsonConverterTest + private static JsonSerializerOptions JsonSerializerOptions => new JsonOptions().SerializerOptions; + + [Fact] + public void Read_ThrowsIfJsonIsIncomplete() { - private static JsonSerializerOptions JsonSerializerOptions => new JsonOptions().SerializerOptions; + // Arrange + var json = "{"; + var converter = new ProblemDetailsJsonConverter(); - [Fact] - public void Read_ThrowsIfJsonIsIncomplete() + // Act & Assert + var ex = Record.Exception(() => { - // Arrange - var json = "{"; - var converter = new ProblemDetailsJsonConverter(); + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); + converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions); + }); + Assert.IsAssignableFrom(ex); + } - // Act & Assert - var ex = Record.Exception(() => + [Fact] + public void Read_Works() + { + // Arrange + var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4"; + var title = "Not found"; + var status = 404; + var detail = "Product not found"; + var instance = "http://example.com/products/14"; + var traceId = "|37dd3dd5-4a9619f953c40a16."; + var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"detail\":\"{detail}\", \"instance\":\"{instance}\",\"traceId\":\"{traceId}\"}}"; + var converter = new ProblemDetailsJsonConverter(); + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); + reader.Read(); + + // Act + var problemDetails = converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions); + + Assert.Equal(type, problemDetails.Type); + Assert.Equal(title, problemDetails.Title); + Assert.Equal(status, problemDetails.Status); + Assert.Equal(instance, problemDetails.Instance); + Assert.Equal(detail, problemDetails.Detail); + Assert.Collection( + problemDetails.Extensions, + kvp => { - var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); - converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions); + Assert.Equal("traceId", kvp.Key); + Assert.Equal(traceId, kvp.Value.ToString()); }); - Assert.IsAssignableFrom(ex); - } - - [Fact] - public void Read_Works() - { - // Arrange - var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4"; - var title = "Not found"; - var status = 404; - var detail = "Product not found"; - var instance = "http://example.com/products/14"; - var traceId = "|37dd3dd5-4a9619f953c40a16."; - var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"detail\":\"{detail}\", \"instance\":\"{instance}\",\"traceId\":\"{traceId}\"}}"; - var converter = new ProblemDetailsJsonConverter(); - var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); - reader.Read(); - - // Act - var problemDetails = converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions); - - Assert.Equal(type, problemDetails.Type); - Assert.Equal(title, problemDetails.Title); - Assert.Equal(status, problemDetails.Status); - Assert.Equal(instance, problemDetails.Instance); - Assert.Equal(detail, problemDetails.Detail); - Assert.Collection( - problemDetails.Extensions, - kvp => - { - Assert.Equal("traceId", kvp.Key); - Assert.Equal(traceId, kvp.Value.ToString()); - }); - } - - [Fact] - public void Read_UsingJsonSerializerWorks() - { - // Arrange - var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4"; - var title = "Not found"; - var status = 404; - var detail = "Product not found"; - var instance = "http://example.com/products/14"; - var traceId = "|37dd3dd5-4a9619f953c40a16."; - var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"detail\":\"{detail}\", \"instance\":\"{instance}\",\"traceId\":\"{traceId}\"}}"; - - // Act - var problemDetails = JsonSerializer.Deserialize(json, JsonSerializerOptions); - - Assert.Equal(type, problemDetails.Type); - Assert.Equal(title, problemDetails.Title); - Assert.Equal(status, problemDetails.Status); - Assert.Equal(instance, problemDetails.Instance); - Assert.Equal(detail, problemDetails.Detail); - Assert.Collection( - problemDetails.Extensions, - kvp => - { - Assert.Equal("traceId", kvp.Key); - Assert.Equal(traceId, kvp.Value.ToString()); - }); - } - - [Fact] - public void Read_WithSomeMissingValues_Works() - { - // Arrange - var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4"; - var title = "Not found"; - var status = 404; - var traceId = "|37dd3dd5-4a9619f953c40a16."; - var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"traceId\":\"{traceId}\"}}"; - var converter = new ProblemDetailsJsonConverter(); - var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); - reader.Read(); + } - // Act - var problemDetails = converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions); + [Fact] + public void Read_UsingJsonSerializerWorks() + { + // Arrange + var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4"; + var title = "Not found"; + var status = 404; + var detail = "Product not found"; + var instance = "http://example.com/products/14"; + var traceId = "|37dd3dd5-4a9619f953c40a16."; + var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"detail\":\"{detail}\", \"instance\":\"{instance}\",\"traceId\":\"{traceId}\"}}"; + + // Act + var problemDetails = JsonSerializer.Deserialize(json, JsonSerializerOptions); + + Assert.Equal(type, problemDetails.Type); + Assert.Equal(title, problemDetails.Title); + Assert.Equal(status, problemDetails.Status); + Assert.Equal(instance, problemDetails.Instance); + Assert.Equal(detail, problemDetails.Detail); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.Equal(traceId, kvp.Value.ToString()); + }); + } - Assert.Equal(type, problemDetails.Type); - Assert.Equal(title, problemDetails.Title); - Assert.Equal(status, problemDetails.Status); - Assert.Collection( - problemDetails.Extensions, - kvp => - { - Assert.Equal("traceId", kvp.Key); - Assert.Equal(traceId, kvp.Value.ToString()); - }); - } + [Fact] + public void Read_WithSomeMissingValues_Works() + { + // Arrange + var type = "https://tools.ietf.org/html/rfc7231#section-6.5.4"; + var title = "Not found"; + var status = 404; + var traceId = "|37dd3dd5-4a9619f953c40a16."; + var json = $"{{\"type\":\"{type}\",\"title\":\"{title}\",\"status\":{status},\"traceId\":\"{traceId}\"}}"; + var converter = new ProblemDetailsJsonConverter(); + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); + reader.Read(); + + // Act + var problemDetails = converter.Read(ref reader, typeof(ProblemDetails), JsonSerializerOptions); + + Assert.Equal(type, problemDetails.Type); + Assert.Equal(title, problemDetails.Title); + Assert.Equal(status, problemDetails.Status); + Assert.Collection( + problemDetails.Extensions, + kvp => + { + Assert.Equal("traceId", kvp.Key); + Assert.Equal(traceId, kvp.Value.ToString()); + }); + } - [Fact] - public void Write_Works() + [Fact] + public void Write_Works() + { + // Arrange + var traceId = "|37dd3dd5-4a9619f953c40a16."; + var value = new ProblemDetails { - // Arrange - var traceId = "|37dd3dd5-4a9619f953c40a16."; - var value = new ProblemDetails - { - Title = "Not found", - Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4", - Status = 404, - Detail = "Product not found", - Instance = "http://example.com/products/14", - Extensions = + Title = "Not found", + Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4", + Status = 404, + Detail = "Product not found", + Instance = "http://example.com/products/14", + Extensions = { { "traceId", traceId }, { "some-data", new[] { "value1", "value2" } } } - }; - var expected = $"{{\"type\":\"{JsonEncodedText.Encode(value.Type)}\",\"title\":\"{value.Title}\",\"status\":{value.Status},\"detail\":\"{value.Detail}\",\"instance\":\"{JsonEncodedText.Encode(value.Instance)}\",\"traceId\":\"{traceId}\",\"some-data\":[\"value1\",\"value2\"]}}"; - var converter = new ProblemDetailsJsonConverter(); - var stream = new MemoryStream(); + }; + var expected = $"{{\"type\":\"{JsonEncodedText.Encode(value.Type)}\",\"title\":\"{value.Title}\",\"status\":{value.Status},\"detail\":\"{value.Detail}\",\"instance\":\"{JsonEncodedText.Encode(value.Instance)}\",\"traceId\":\"{traceId}\",\"some-data\":[\"value1\",\"value2\"]}}"; + var converter = new ProblemDetailsJsonConverter(); + var stream = new MemoryStream(); - // Act - using (var writer = new Utf8JsonWriter(stream)) - { - converter.Write(writer, value, JsonSerializerOptions); - } - - // Assert - var actual = Encoding.UTF8.GetString(stream.ToArray()); - Assert.Equal(expected, actual); + // Act + using (var writer = new Utf8JsonWriter(stream)) + { + converter.Write(writer, value, JsonSerializerOptions); } - [Fact] - public void Write_WithSomeMissingContent_Works() - { - // Arrange - var value = new ProblemDetails - { - Title = "Not found", - Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4", - Status = 404, - }; - var expected = $"{{\"type\":\"{JsonEncodedText.Encode(value.Type)}\",\"title\":\"{value.Title}\",\"status\":{value.Status}}}"; - var converter = new ProblemDetailsJsonConverter(); - var stream = new MemoryStream(); - - // Act - using (var writer = new Utf8JsonWriter(stream)) - { - converter.Write(writer, value, JsonSerializerOptions); - } + // Assert + var actual = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(expected, actual); + } - // Assert - var actual = Encoding.UTF8.GetString(stream.ToArray()); - Assert.Equal(expected, actual); + [Fact] + public void Write_WithSomeMissingContent_Works() + { + // Arrange + var value = new ProblemDetails + { + Title = "Not found", + Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4", + Status = 404, + }; + var expected = $"{{\"type\":\"{JsonEncodedText.Encode(value.Type)}\",\"title\":\"{value.Title}\",\"status\":{value.Status}}}"; + var converter = new ProblemDetailsJsonConverter(); + var stream = new MemoryStream(); + + // Act + using (var writer = new Utf8JsonWriter(stream)) + { + converter.Write(writer, value, JsonSerializerOptions); } + + // Assert + var actual = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(expected, actual); } } diff --git a/src/Http/Http.Extensions/test/QueryBuilderTests.cs b/src/Http/Http.Extensions/test/QueryBuilderTests.cs index 3b00142adb..a12448283c 100644 --- a/src/Http/Http.Extensions/test/QueryBuilderTests.cs +++ b/src/Http/Http.Extensions/test/QueryBuilderTests.cs @@ -6,106 +6,105 @@ using System.Collections.Generic; using Microsoft.Extensions.Primitives; using Xunit; -namespace Microsoft.AspNetCore.Http.Extensions +namespace Microsoft.AspNetCore.Http.Extensions; + +public class QueryBuilderTests { - public class QueryBuilderTests + [Fact] + public void EmptyQuery_NoQuestionMark() { - [Fact] - public void EmptyQuery_NoQuestionMark() - { - var builder = new QueryBuilder(); - Assert.Equal(string.Empty, builder.ToString()); - } + var builder = new QueryBuilder(); + Assert.Equal(string.Empty, builder.ToString()); + } - [Fact] - public void AddSimple_NoEncoding() - { - var builder = new QueryBuilder(); - builder.Add("key", "value"); - Assert.Equal("?key=value", builder.ToString()); - } + [Fact] + public void AddSimple_NoEncoding() + { + var builder = new QueryBuilder(); + builder.Add("key", "value"); + Assert.Equal("?key=value", builder.ToString()); + } - [Fact] - public void AddSpace_PercentEncoded() - { - var builder = new QueryBuilder(); - builder.Add("key", "value 1"); - Assert.Equal("?key=value%201", builder.ToString()); - } + [Fact] + public void AddSpace_PercentEncoded() + { + var builder = new QueryBuilder(); + builder.Add("key", "value 1"); + Assert.Equal("?key=value%201", builder.ToString()); + } - [Fact] - public void AddReservedCharacters_PercentEncoded() - { - var builder = new QueryBuilder(); - builder.Add("key&", "value#"); - Assert.Equal("?key%26=value%23", builder.ToString()); - } + [Fact] + public void AddReservedCharacters_PercentEncoded() + { + var builder = new QueryBuilder(); + builder.Add("key&", "value#"); + Assert.Equal("?key%26=value%23", builder.ToString()); + } - [Fact] - public void AddMultipleValues_AddedInOrder() - { - var builder = new QueryBuilder(); - builder.Add("key1", "value1"); - builder.Add("key2", "value2"); - builder.Add("key3", "value3"); - Assert.Equal("?key1=value1&key2=value2&key3=value3", builder.ToString()); - } + [Fact] + public void AddMultipleValues_AddedInOrder() + { + var builder = new QueryBuilder(); + builder.Add("key1", "value1"); + builder.Add("key2", "value2"); + builder.Add("key3", "value3"); + Assert.Equal("?key1=value1&key2=value2&key3=value3", builder.ToString()); + } - [Fact] - public void AddIEnumerableValues_AddedInOrder() - { - var builder = new QueryBuilder(); - builder.Add("key", new[] { "value1", "value2", "value3" }); - Assert.Equal("?key=value1&key=value2&key=value3", builder.ToString()); - } + [Fact] + public void AddIEnumerableValues_AddedInOrder() + { + var builder = new QueryBuilder(); + builder.Add("key", new[] { "value1", "value2", "value3" }); + Assert.Equal("?key=value1&key=value2&key=value3", builder.ToString()); + } - [Fact] - public void AddMultipleValuesViaConstructor_AddedInOrder() + [Fact] + public void AddMultipleValuesViaConstructor_AddedInOrder() + { + var builder = new QueryBuilder(new[] { - var builder = new QueryBuilder(new[] - { new KeyValuePair("key1", "value1"), new KeyValuePair("key2", "value2"), new KeyValuePair("key3", "value3"), }); - Assert.Equal("?key1=value1&key2=value2&key3=value3", builder.ToString()); - } + Assert.Equal("?key1=value1&key2=value2&key3=value3", builder.ToString()); + } - [Fact] - public void AddMultipleValuesViaConstructor_WithStringValues() + [Fact] + public void AddMultipleValuesViaConstructor_WithStringValues() + { + var builder = new QueryBuilder(new[] { - var builder = new QueryBuilder(new[] - { new KeyValuePair("key1", new StringValues(new [] { "value1", string.Empty, "value3" })), new KeyValuePair("key2", string.Empty), new KeyValuePair("key3", StringValues.Empty) }); - Assert.Equal("?key1=value1&key1=&key1=value3&key2=", builder.ToString()); - } + Assert.Equal("?key1=value1&key1=&key1=value3&key2=", builder.ToString()); + } - [Fact] - public void AddMultipleValuesViaInitializer_AddedInOrder() - { - var builder = new QueryBuilder() + [Fact] + public void AddMultipleValuesViaInitializer_AddedInOrder() + { + var builder = new QueryBuilder() { { "key1", "value1" }, { "key2", "value2" }, { "key3", "value3" }, }; - Assert.Equal("?key1=value1&key2=value2&key3=value3", builder.ToString()); - } + Assert.Equal("?key1=value1&key2=value2&key3=value3", builder.ToString()); + } - [Fact] - public void CopyViaConstructor_AddedInOrder() - { - var builder = new QueryBuilder() + [Fact] + public void CopyViaConstructor_AddedInOrder() + { + var builder = new QueryBuilder() { { "key1", "value1" }, { "key2", "value2" }, { "key3", "value3" }, }; - var builder1 = new QueryBuilder(builder); - Assert.Equal("?key1=value1&key2=value2&key3=value3", builder1.ToString()); - } + var builder1 = new QueryBuilder(builder); + Assert.Equal("?key1=value1&key2=value2&key3=value3", builder1.ToString()); } } diff --git a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs index c3c9ec733e..7640eb1c55 100644 --- a/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs +++ b/src/Http/Http.Extensions/test/RequestDelegateFactoryTests.cs @@ -26,54 +26,54 @@ using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Moq; -namespace Microsoft.AspNetCore.Routing.Internal +namespace Microsoft.AspNetCore.Routing.Internal; + +public class RequestDelegateFactoryTests : LoggedTest { - public class RequestDelegateFactoryTests : LoggedTest + public static IEnumerable NoResult { - public static IEnumerable NoResult + get { - get + void TestAction(HttpContext httpContext) { - void TestAction(HttpContext httpContext) - { - MarkAsInvoked(httpContext); - } + MarkAsInvoked(httpContext); + } - Task TaskTestAction(HttpContext httpContext) - { - MarkAsInvoked(httpContext); - return Task.CompletedTask; - } + Task TaskTestAction(HttpContext httpContext) + { + MarkAsInvoked(httpContext); + return Task.CompletedTask; + } - ValueTask ValueTaskTestAction(HttpContext httpContext) - { - MarkAsInvoked(httpContext); - return ValueTask.CompletedTask; - } + ValueTask ValueTaskTestAction(HttpContext httpContext) + { + MarkAsInvoked(httpContext); + return ValueTask.CompletedTask; + } - void StaticTestAction(HttpContext httpContext) - { - MarkAsInvoked(httpContext); - } + void StaticTestAction(HttpContext httpContext) + { + MarkAsInvoked(httpContext); + } - Task StaticTaskTestAction(HttpContext httpContext) - { - MarkAsInvoked(httpContext); - return Task.CompletedTask; - } + Task StaticTaskTestAction(HttpContext httpContext) + { + MarkAsInvoked(httpContext); + return Task.CompletedTask; + } - ValueTask StaticValueTaskTestAction(HttpContext httpContext) - { - MarkAsInvoked(httpContext); - return ValueTask.CompletedTask; - } + ValueTask StaticValueTaskTestAction(HttpContext httpContext) + { + MarkAsInvoked(httpContext); + return ValueTask.CompletedTask; + } - void MarkAsInvoked(HttpContext httpContext) - { - httpContext.Items.Add("invoked", true); - } + void MarkAsInvoked(HttpContext httpContext) + { + httpContext.Items.Add("invoked", true); + } - return new List + return new List { new object[] { (Action)TestAction }, new object[] { (Func)TaskTestAction }, @@ -82,385 +82,385 @@ namespace Microsoft.AspNetCore.Routing.Internal new object[] { (Func)StaticTaskTestAction }, new object[] { (Func)StaticValueTaskTestAction }, }; - } } + } - [Theory] - [MemberData(nameof(NoResult))] - public async Task RequestDelegateInvokesAction(Delegate @delegate) - { - var httpContext = CreateHttpContext(); + [Theory] + [MemberData(nameof(NoResult))] + public async Task RequestDelegateInvokesAction(Delegate @delegate) + { + var httpContext = CreateHttpContext(); - var factoryResult = RequestDelegateFactory.Create(@delegate); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - Assert.True(httpContext.Items["invoked"] as bool?); - } + Assert.True(httpContext.Items["invoked"] as bool?); + } - private static void StaticTestActionBasicReflection(HttpContext httpContext) - { - httpContext.Items.Add("invoked", true); - } + private static void StaticTestActionBasicReflection(HttpContext httpContext) + { + httpContext.Items.Add("invoked", true); + } - [Fact] - public async Task StaticMethodInfoOverloadWorksWithBasicReflection() - { - var methodInfo = typeof(RequestDelegateFactoryTests).GetMethod( - nameof(StaticTestActionBasicReflection), - BindingFlags.NonPublic | BindingFlags.Static, - new[] { typeof(HttpContext) }); + [Fact] + public async Task StaticMethodInfoOverloadWorksWithBasicReflection() + { + var methodInfo = typeof(RequestDelegateFactoryTests).GetMethod( + nameof(StaticTestActionBasicReflection), + BindingFlags.NonPublic | BindingFlags.Static, + new[] { typeof(HttpContext) }); - var factoryResult = RequestDelegateFactory.Create(methodInfo!); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(methodInfo!); + var requestDelegate = factoryResult.RequestDelegate; - var httpContext = CreateHttpContext(); + var httpContext = CreateHttpContext(); - await requestDelegate(httpContext); + await requestDelegate(httpContext); + + Assert.True(httpContext.Items["invoked"] as bool?); + } - Assert.True(httpContext.Items["invoked"] as bool?); + private class TestNonStaticActionClass + { + private readonly object _invokedValue; + + public TestNonStaticActionClass(object invokedValue) + { + _invokedValue = invokedValue; } - private class TestNonStaticActionClass + public void NonStaticTestAction(HttpContext httpContext) { - private readonly object _invokedValue; + httpContext.Items.Add("invoked", _invokedValue); + } + } - public TestNonStaticActionClass(object invokedValue) - { - _invokedValue = invokedValue; - } + [Fact] + public async Task NonStaticMethodInfoOverloadWorksWithBasicReflection() + { + var methodInfo = typeof(TestNonStaticActionClass).GetMethod( + nameof(TestNonStaticActionClass.NonStaticTestAction), + BindingFlags.Public | BindingFlags.Instance, + new[] { typeof(HttpContext) }); - public void NonStaticTestAction(HttpContext httpContext) + var invoked = false; + + object GetTarget() + { + if (!invoked) { - httpContext.Items.Add("invoked", _invokedValue); + invoked = true; + return new TestNonStaticActionClass(1); } + + return new TestNonStaticActionClass(2); } - [Fact] - public async Task NonStaticMethodInfoOverloadWorksWithBasicReflection() - { - var methodInfo = typeof(TestNonStaticActionClass).GetMethod( - nameof(TestNonStaticActionClass.NonStaticTestAction), - BindingFlags.Public | BindingFlags.Instance, - new[] { typeof(HttpContext) }); + var factoryResult = RequestDelegateFactory.Create(methodInfo!, _ => GetTarget()); + var requestDelegate = factoryResult.RequestDelegate; - var invoked = false; + var httpContext = CreateHttpContext(); - object GetTarget() - { - if (!invoked) - { - invoked = true; - return new TestNonStaticActionClass(1); - } + await requestDelegate(httpContext); - return new TestNonStaticActionClass(2); - } + Assert.Equal(1, httpContext.Items["invoked"]); - var factoryResult = RequestDelegateFactory.Create(methodInfo!, _ => GetTarget()); - var requestDelegate = factoryResult.RequestDelegate; + httpContext = CreateHttpContext(); - var httpContext = CreateHttpContext(); + await requestDelegate(httpContext); - await requestDelegate(httpContext); + Assert.Equal(2, httpContext.Items["invoked"]); + } - Assert.Equal(1, httpContext.Items["invoked"]); + [Fact] + public void BuildRequestDelegateThrowsArgumentNullExceptions() + { + var methodInfo = typeof(RequestDelegateFactoryTests).GetMethod( + nameof(StaticTestActionBasicReflection), + BindingFlags.NonPublic | BindingFlags.Static, + new[] { typeof(HttpContext) }); - httpContext = CreateHttpContext(); + var serviceProvider = new EmptyServiceProvider(); - await requestDelegate(httpContext); + var exNullAction = Assert.Throws(() => RequestDelegateFactory.Create(handler: null!)); + var exNullMethodInfo1 = Assert.Throws(() => RequestDelegateFactory.Create(methodInfo: null!)); - Assert.Equal(2, httpContext.Items["invoked"]); - } + Assert.Equal("handler", exNullAction.ParamName); + Assert.Equal("methodInfo", exNullMethodInfo1.ParamName); + } - [Fact] - public void BuildRequestDelegateThrowsArgumentNullExceptions() - { - var methodInfo = typeof(RequestDelegateFactoryTests).GetMethod( - nameof(StaticTestActionBasicReflection), - BindingFlags.NonPublic | BindingFlags.Static, - new[] { typeof(HttpContext) }); + [Fact] + public async Task RequestDelegatePopulatesFromRouteParameterBasedOnParameterName() + { + const string paramName = "value"; + const int originalRouteParam = 42; - var serviceProvider = new EmptyServiceProvider(); + static void TestAction(HttpContext httpContext, [FromRoute] int value) + { + httpContext.Items.Add("input", value); + } - var exNullAction = Assert.Throws(() => RequestDelegateFactory.Create(handler: null!)); - var exNullMethodInfo1 = Assert.Throws(() => RequestDelegateFactory.Create(methodInfo: null!)); + var httpContext = CreateHttpContext(); + httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); - Assert.Equal("handler", exNullAction.ParamName); - Assert.Equal("methodInfo", exNullMethodInfo1.ParamName); - } + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; - [Fact] - public async Task RequestDelegatePopulatesFromRouteParameterBasedOnParameterName() - { - const string paramName = "value"; - const int originalRouteParam = 42; + await requestDelegate(httpContext); - static void TestAction(HttpContext httpContext, [FromRoute] int value) - { - httpContext.Items.Add("input", value); - } + Assert.Equal(originalRouteParam, httpContext.Items["input"]); + } - var httpContext = CreateHttpContext(); - httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); + private static void TestOptional(HttpContext httpContext, [FromRoute] int value = 42) + { + httpContext.Items.Add("input", value); + } - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; + private static void TestOptionalNullable(HttpContext httpContext, int? value = 42) + { + httpContext.Items.Add("input", value); + } - await requestDelegate(httpContext); + private static void TestOptionalString(HttpContext httpContext, string value = "default") + { + httpContext.Items.Add("input", value); + } - Assert.Equal(originalRouteParam, httpContext.Items["input"]); - } + [Fact] + public async Task SpecifiedRouteParametersDoNotFallbackToQueryString() + { + var httpContext = CreateHttpContext(); - private static void TestOptional(HttpContext httpContext, [FromRoute] int value = 42) + var factoryResult = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => { - httpContext.Items.Add("input", value); - } + if (id is not null) + { + httpContext.Items["input"] = id; + } + }, + new() { RouteParameterNames = new string[] { "id" } }); - private static void TestOptionalNullable(HttpContext httpContext, int? value = 42) - { - httpContext.Items.Add("input", value); - } + var requestDelegate = factoryResult.RequestDelegate; - private static void TestOptionalString(HttpContext httpContext, string value = "default") + httpContext.Request.Query = new QueryCollection(new Dictionary { - httpContext.Items.Add("input", value); - } + ["id"] = "42" + }); - [Fact] - public async Task SpecifiedRouteParametersDoNotFallbackToQueryString() - { - var httpContext = CreateHttpContext(); + await requestDelegate(httpContext); - var factoryResult = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => - { - if (id is not null) - { - httpContext.Items["input"] = id; - } - }, - new() { RouteParameterNames = new string[] { "id" } }); + Assert.Null(httpContext.Items["input"]); + } - var requestDelegate = factoryResult.RequestDelegate; + [Fact] + public async Task SpecifiedQueryParametersDoNotFallbackToRouteValues() + { + var httpContext = CreateHttpContext(); - httpContext.Request.Query = new QueryCollection(new Dictionary + var factoryResult = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => + { + if (id is not null) { - ["id"] = "42" - }); - - await requestDelegate(httpContext); - - Assert.Null(httpContext.Items["input"]); - } + httpContext.Items["input"] = id; + } + }, + new() { RouteParameterNames = new string[] { } }); - [Fact] - public async Task SpecifiedQueryParametersDoNotFallbackToRouteValues() + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["id"] = "41" + }); + httpContext.Request.RouteValues = new() { - var httpContext = CreateHttpContext(); + ["id"] = "42" + }; - var factoryResult = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => - { - if (id is not null) - { - httpContext.Items["input"] = id; - } - }, - new() { RouteParameterNames = new string[] { } }); + var requestDelegate = factoryResult.RequestDelegate; - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["id"] = "41" - }); - httpContext.Request.RouteValues = new() - { - ["id"] = "42" - }; + await requestDelegate(httpContext); - var requestDelegate = factoryResult.RequestDelegate; + Assert.Equal(41, httpContext.Items["input"]); + } - await requestDelegate(httpContext); + [Fact] + public async Task NullRouteParametersPrefersRouteOverQueryString() + { + var httpContext = CreateHttpContext(); - Assert.Equal(41, httpContext.Items["input"]); - } + var factoryResult = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => + { + if (id is not null) + { + httpContext.Items["input"] = id; + } + }, + new() { RouteParameterNames = null }); - [Fact] - public async Task NullRouteParametersPrefersRouteOverQueryString() + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["id"] = "41" + }); + httpContext.Request.RouteValues = new() { - var httpContext = CreateHttpContext(); + ["id"] = "42" + }; - var factoryResult = RequestDelegateFactory.Create((int? id, HttpContext httpContext) => - { - if (id is not null) - { - httpContext.Items["input"] = id; - } - }, - new() { RouteParameterNames = null }); + var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["id"] = "41" - }); - httpContext.Request.RouteValues = new() - { - ["id"] = "42" - }; + Assert.Equal(42, httpContext.Items["input"]); + } - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + [Fact] + public async Task CreatingDelegateWithInstanceMethodInfoCreatesInstancePerCall() + { + var methodInfo = typeof(HttpHandler).GetMethod(nameof(HttpHandler.Handle)); - Assert.Equal(42, httpContext.Items["input"]); - } + Assert.NotNull(methodInfo); - [Fact] - public async Task CreatingDelegateWithInstanceMethodInfoCreatesInstancePerCall() - { - var methodInfo = typeof(HttpHandler).GetMethod(nameof(HttpHandler.Handle)); + var factoryResult = RequestDelegateFactory.Create(methodInfo!); + var requestDelegate = factoryResult.RequestDelegate; - Assert.NotNull(methodInfo); + var context = CreateHttpContext(); - var factoryResult = RequestDelegateFactory.Create(methodInfo!); - var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(context); - var context = CreateHttpContext(); + Assert.Equal(1, context.Items["calls"]); - await requestDelegate(context); + await requestDelegate(context); - Assert.Equal(1, context.Items["calls"]); + Assert.Equal(1, context.Items["calls"]); + } - await requestDelegate(context); + [Fact] + public void SpecifiedEmptyRouteParametersThrowIfRouteParameterDoesNotExist() + { + var ex = Assert.Throws(() => + RequestDelegateFactory.Create(([FromRoute] int id) => { }, new() { RouteParameterNames = Array.Empty() })); - Assert.Equal(1, context.Items["calls"]); - } + Assert.Equal("'id' is not a route parameter.", ex.Message); + } - [Fact] - public void SpecifiedEmptyRouteParametersThrowIfRouteParameterDoesNotExist() - { - var ex = Assert.Throws(() => - RequestDelegateFactory.Create(([FromRoute] int id) => { }, new() { RouteParameterNames = Array.Empty() })); + [Fact] + public async Task RequestDelegatePopulatesFromRouteOptionalParameter() + { + var httpContext = CreateHttpContext(); - Assert.Equal("'id' is not a route parameter.", ex.Message); - } + var factoryResult = RequestDelegateFactory.Create(TestOptional); + var requestDelegate = factoryResult.RequestDelegate; - [Fact] - public async Task RequestDelegatePopulatesFromRouteOptionalParameter() - { - var httpContext = CreateHttpContext(); + await requestDelegate(httpContext); - var factoryResult = RequestDelegateFactory.Create(TestOptional); - var requestDelegate = factoryResult.RequestDelegate; + Assert.Equal(42, httpContext.Items["input"]); + } - await requestDelegate(httpContext); + [Fact] + public async Task RequestDelegatePopulatesFromNullableOptionalParameter() + { + var httpContext = CreateHttpContext(); - Assert.Equal(42, httpContext.Items["input"]); - } + var factoryResult = RequestDelegateFactory.Create(TestOptional); + var requestDelegate = factoryResult.RequestDelegate; - [Fact] - public async Task RequestDelegatePopulatesFromNullableOptionalParameter() - { - var httpContext = CreateHttpContext(); + await requestDelegate(httpContext); - var factoryResult = RequestDelegateFactory.Create(TestOptional); - var requestDelegate = factoryResult.RequestDelegate; + Assert.Equal(42, httpContext.Items["input"]); + } - await requestDelegate(httpContext); + [Fact] + public async Task RequestDelegatePopulatesFromOptionalStringParameter() + { + var httpContext = CreateHttpContext(); - Assert.Equal(42, httpContext.Items["input"]); - } + var factoryResult = RequestDelegateFactory.Create(TestOptionalString); + var requestDelegate = factoryResult.RequestDelegate; - [Fact] - public async Task RequestDelegatePopulatesFromOptionalStringParameter() - { - var httpContext = CreateHttpContext(); + await requestDelegate(httpContext); - var factoryResult = RequestDelegateFactory.Create(TestOptionalString); - var requestDelegate = factoryResult.RequestDelegate; + Assert.Equal("default", httpContext.Items["input"]); + } - await requestDelegate(httpContext); + [Fact] + public async Task RequestDelegatePopulatesFromRouteOptionalParameterBasedOnParameterName() + { + const string paramName = "value"; + const int originalRouteParam = 47; - Assert.Equal("default", httpContext.Items["input"]); - } + var httpContext = CreateHttpContext(); - [Fact] - public async Task RequestDelegatePopulatesFromRouteOptionalParameterBasedOnParameterName() - { - const string paramName = "value"; - const int originalRouteParam = 47; + httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var httpContext = CreateHttpContext(); + var factoryResult = RequestDelegateFactory.Create(TestOptional); + var requestDelegate = factoryResult.RequestDelegate; - httpContext.Request.RouteValues[paramName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); + await requestDelegate(httpContext); - var factoryResult = RequestDelegateFactory.Create(TestOptional); - var requestDelegate = factoryResult.RequestDelegate; + Assert.Equal(47, httpContext.Items["input"]); + } - await requestDelegate(httpContext); + [Fact] + public async Task RequestDelegatePopulatesFromRouteParameterBasedOnAttributeNameProperty() + { + const string specifiedName = "value"; + const int originalRouteParam = 42; - Assert.Equal(47, httpContext.Items["input"]); - } + int? deserializedRouteParam = null; - [Fact] - public async Task RequestDelegatePopulatesFromRouteParameterBasedOnAttributeNameProperty() + void TestAction([FromRoute(Name = specifiedName)] int foo) { - const string specifiedName = "value"; - const int originalRouteParam = 42; + deserializedRouteParam = foo; + } - int? deserializedRouteParam = null; + var httpContext = CreateHttpContext(); + httpContext.Request.RouteValues[specifiedName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); - void TestAction([FromRoute(Name = specifiedName)] int foo) - { - deserializedRouteParam = foo; - } + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; - var httpContext = CreateHttpContext(); - httpContext.Request.RouteValues[specifiedName] = originalRouteParam.ToString(NumberFormatInfo.InvariantInfo); + await requestDelegate(httpContext); - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; + Assert.Equal(originalRouteParam, deserializedRouteParam); + } - await requestDelegate(httpContext); + [Fact] + public async Task Returns400IfNoMatchingRouteValueForRequiredParam() + { + const string unmatchedName = "value"; + const int unmatchedRouteParam = 42; - Assert.Equal(originalRouteParam, deserializedRouteParam); - } + int? deserializedRouteParam = null; - [Fact] - public async Task Returns400IfNoMatchingRouteValueForRequiredParam() + void TestAction([FromRoute] int foo) { - const string unmatchedName = "value"; - const int unmatchedRouteParam = 42; - - int? deserializedRouteParam = null; - - void TestAction([FromRoute] int foo) - { - deserializedRouteParam = foo; - } + deserializedRouteParam = foo; + } - var httpContext = CreateHttpContext(); - httpContext.Request.RouteValues[unmatchedName] = unmatchedRouteParam.ToString(NumberFormatInfo.InvariantInfo); + var httpContext = CreateHttpContext(); + httpContext.Request.RouteValues[unmatchedName] = unmatchedRouteParam.ToString(NumberFormatInfo.InvariantInfo); - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - Assert.Equal(400, httpContext.Response.StatusCode); - } + Assert.Equal(400, httpContext.Response.StatusCode); + } - public static object?[][] TryParsableParameters + public static object?[][] TryParsableParameters + { + get { - get + static void Store(HttpContext httpContext, T tryParsable) { - static void Store(HttpContext httpContext, T tryParsable) - { - httpContext.Items["tryParsable"] = tryParsable; - } + httpContext.Items["tryParsable"] = tryParsable; + } - var now = DateTime.Now; + var now = DateTime.Now; - return new[] - { + return new[] + { // string is not technically "TryParsable", but it's the special case. new object[] { (Action)Store, "plain string", "plain string" }, new object[] { (Action)Store, "-42", -42 }, @@ -492,1453 +492,1453 @@ namespace Microsoft.AspNetCore.Routing.Internal new object[] { (Action)Store, "https://example.org", new MyTryParseRecord(new Uri("https://example.org")) }, new object?[] { (Action)Store, null, null }, }; - } } + } - private enum MyEnum { ValueA, ValueB, } + private enum MyEnum { ValueA, ValueB, } - private record MyTryParseRecord(Uri Uri) + private record MyTryParseRecord(Uri Uri) + { + public static bool TryParse(string? value, out MyTryParseRecord? result) { - public static bool TryParse(string? value, out MyTryParseRecord? result) + if (!Uri.TryCreate(value, UriKind.Absolute, out var uri)) { - if (!Uri.TryCreate(value, UriKind.Absolute, out var uri)) - { - result = null; - return false; - } - - result = new MyTryParseRecord(uri); - return true; + result = null; + return false; } - } - private class MyBindAsyncTypeThatThrows - { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => - throw new InvalidOperationException("BindAsync failed"); + result = new MyTryParseRecord(uri); + return true; } + } - private record MyBindAsyncRecord(Uri Uri) - { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) - { - Assert.Equal(typeof(MyBindAsyncRecord), parameter.ParameterType); - Assert.StartsWith("myBindAsyncRecord", parameter.Name); + private class MyBindAsyncTypeThatThrows + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) => + throw new InvalidOperationException("BindAsync failed"); + } - if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) - { - return new(result: null); - } + private record MyBindAsyncRecord(Uri Uri) + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) + { + Assert.Equal(typeof(MyBindAsyncRecord), parameter.ParameterType); + Assert.StartsWith("myBindAsyncRecord", parameter.Name); - return new(result: new(uri)); + if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) + { + return new(result: null); } - // BindAsync(HttpContext, ParameterInfo) should be preferred over TryParse(string, ...) if there's - // no [FromRoute] or [FromQuery] attributes. - public static bool TryParse(string? value, out MyBindAsyncRecord? result) => - throw new NotImplementedException(); + return new(result: new(uri)); } - private record struct MyNullableBindAsyncStruct(Uri Uri) - { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) - { - Assert.True(parameter.ParameterType == typeof(MyNullableBindAsyncStruct) || parameter.ParameterType == typeof(MyNullableBindAsyncStruct?)); - Assert.Equal("myNullableBindAsyncStruct", parameter.Name); + // BindAsync(HttpContext, ParameterInfo) should be preferred over TryParse(string, ...) if there's + // no [FromRoute] or [FromQuery] attributes. + public static bool TryParse(string? value, out MyBindAsyncRecord? result) => + throw new NotImplementedException(); + } - if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) - { - return new(result: null); - } + private record struct MyNullableBindAsyncStruct(Uri Uri) + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) + { + Assert.True(parameter.ParameterType == typeof(MyNullableBindAsyncStruct) || parameter.ParameterType == typeof(MyNullableBindAsyncStruct?)); + Assert.Equal("myNullableBindAsyncStruct", parameter.Name); - return new(result: new(uri)); + if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) + { + return new(result: null); } + + return new(result: new(uri)); } + } - private record struct MyBindAsyncStruct(Uri Uri) + private record struct MyBindAsyncStruct(Uri Uri) + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) - { - Assert.True(parameter.ParameterType == typeof(MyBindAsyncStruct) || parameter.ParameterType == typeof(MyBindAsyncStruct?)); - Assert.Equal("myBindAsyncStruct", parameter.Name); + Assert.True(parameter.ParameterType == typeof(MyBindAsyncStruct) || parameter.ParameterType == typeof(MyBindAsyncStruct?)); + Assert.Equal("myBindAsyncStruct", parameter.Name); - if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) - { - throw new BadHttpRequestException("The request is missing the required Referer header."); - } - - return new(result: new(uri)); + if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) + { + throw new BadHttpRequestException("The request is missing the required Referer header."); } - // BindAsync(HttpContext, ParameterInfo) should be preferred over TryParse(string, ...) if there's - // no [FromRoute] or [FromQuery] attributes. - public static bool TryParse(string? value, out MyBindAsyncStruct result) => - throw new NotImplementedException(); + return new(result: new(uri)); } - private record MyAwaitedBindAsyncRecord(Uri Uri) - { - public static async ValueTask BindAsync(HttpContext context, ParameterInfo parameter) - { - Assert.Equal(typeof(MyAwaitedBindAsyncRecord), parameter.ParameterType); - Assert.StartsWith("myAwaitedBindAsyncRecord", parameter.Name); + // BindAsync(HttpContext, ParameterInfo) should be preferred over TryParse(string, ...) if there's + // no [FromRoute] or [FromQuery] attributes. + public static bool TryParse(string? value, out MyBindAsyncStruct result) => + throw new NotImplementedException(); + } - await Task.Yield(); + private record MyAwaitedBindAsyncRecord(Uri Uri) + { + public static async ValueTask BindAsync(HttpContext context, ParameterInfo parameter) + { + Assert.Equal(typeof(MyAwaitedBindAsyncRecord), parameter.ParameterType); + Assert.StartsWith("myAwaitedBindAsyncRecord", parameter.Name); - if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) - { - return null; - } + await Task.Yield(); - return new(uri); + if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) + { + return null; } + + return new(uri); } + } - private record struct MyAwaitedBindAsyncStruct(Uri Uri) + private record struct MyAwaitedBindAsyncStruct(Uri Uri) + { + public static async ValueTask BindAsync(HttpContext context, ParameterInfo parameter) { - public static async ValueTask BindAsync(HttpContext context, ParameterInfo parameter) - { - Assert.Equal(typeof(MyAwaitedBindAsyncStruct), parameter.ParameterType); - Assert.Equal("myAwaitedBindAsyncStruct", parameter.Name); - - await Task.Yield(); + Assert.Equal(typeof(MyAwaitedBindAsyncStruct), parameter.ParameterType); + Assert.Equal("myAwaitedBindAsyncStruct", parameter.Name); - if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) - { - throw new BadHttpRequestException("The request is missing the required Referer header."); - } + await Task.Yield(); - return new(uri); + if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) + { + throw new BadHttpRequestException("The request is missing the required Referer header."); } + + return new(uri); } + } - private record struct MyBothBindAsyncStruct(Uri Uri) + private record struct MyBothBindAsyncStruct(Uri Uri) + { + public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) { - public static ValueTask BindAsync(HttpContext context, ParameterInfo parameter) - { - Assert.True(parameter.ParameterType == typeof(MyBothBindAsyncStruct) || parameter.ParameterType == typeof(MyBothBindAsyncStruct?)); - Assert.Equal("myBothBindAsyncStruct", parameter.Name); - - if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) - { - throw new BadHttpRequestException("The request is missing the required Referer header."); - } + Assert.True(parameter.ParameterType == typeof(MyBothBindAsyncStruct) || parameter.ParameterType == typeof(MyBothBindAsyncStruct?)); + Assert.Equal("myBothBindAsyncStruct", parameter.Name); - return new(result: new(uri)); - } - - // BindAsync with ParameterInfo is preferred - public static ValueTask BindAsync(HttpContext context) + if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) { - throw new NotImplementedException(); + throw new BadHttpRequestException("The request is missing the required Referer header."); } + + return new(result: new(uri)); } - private record struct MySimpleBindAsyncStruct(Uri Uri) + // BindAsync with ParameterInfo is preferred + public static ValueTask BindAsync(HttpContext context) { - public static ValueTask BindAsync(HttpContext context) - { - if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) - { - throw new BadHttpRequestException("The request is missing the required Referer header."); - } + throw new NotImplementedException(); + } + } - return new(result: new(uri)); + private record struct MySimpleBindAsyncStruct(Uri Uri) + { + public static ValueTask BindAsync(HttpContext context) + { + if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) + { + throw new BadHttpRequestException("The request is missing the required Referer header."); } + + return new(result: new(uri)); } + } - private record MySimpleBindAsyncRecord(Uri Uri) + private record MySimpleBindAsyncRecord(Uri Uri) + { + public static ValueTask BindAsync(HttpContext context) { - public static ValueTask BindAsync(HttpContext context) + if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) { - if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) - { - return new(result: null); - } - - return new(result: new(uri)); + return new(result: null); } + + return new(result: new(uri)); } + } - private interface IBindAsync + private interface IBindAsync + { + static ValueTask BindAsync(HttpContext context) { - static ValueTask BindAsync(HttpContext context) + if (typeof(T) != typeof(MyBindAsyncFromInterfaceRecord)) { - if (typeof(T) != typeof(MyBindAsyncFromInterfaceRecord)) - { - throw new InvalidOperationException(); - } - - if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) - { - return new(default(T)); - } + throw new InvalidOperationException(); + } - return new(result: (T)(object)new MyBindAsyncFromInterfaceRecord(uri)); + if (!Uri.TryCreate(context.Request.Headers.Referer, UriKind.Absolute, out var uri)) + { + return new(default(T)); } - } - private record MyBindAsyncFromInterfaceRecord(Uri uri) : IBindAsync - { + return new(result: (T)(object)new MyBindAsyncFromInterfaceRecord(uri)); } + } - [Theory] - [MemberData(nameof(TryParsableParameters))] - public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromRouteValue(Delegate action, string? routeValue, object? expectedParameterValue) - { - var httpContext = CreateHttpContext(); - httpContext.Request.RouteValues["tryParsable"] = routeValue; + private record MyBindAsyncFromInterfaceRecord(Uri uri) : IBindAsync + { + } - var factoryResult = RequestDelegateFactory.Create(action); - var requestDelegate = factoryResult.RequestDelegate; + [Theory] + [MemberData(nameof(TryParsableParameters))] + public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromRouteValue(Delegate action, string? routeValue, object? expectedParameterValue) + { + var httpContext = CreateHttpContext(); + httpContext.Request.RouteValues["tryParsable"] = routeValue; - await requestDelegate(httpContext); + var factoryResult = RequestDelegateFactory.Create(action); + var requestDelegate = factoryResult.RequestDelegate; - Assert.Equal(expectedParameterValue, httpContext.Items["tryParsable"]); - } + await requestDelegate(httpContext); - [Theory] - [MemberData(nameof(TryParsableParameters))] - public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromQueryString(Delegate action, string? routeValue, object? expectedParameterValue) + Assert.Equal(expectedParameterValue, httpContext.Items["tryParsable"]); + } + + [Theory] + [MemberData(nameof(TryParsableParameters))] + public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromQueryString(Delegate action, string? routeValue, object? expectedParameterValue) + { + var httpContext = CreateHttpContext(); + httpContext.Request.Query = new QueryCollection(new Dictionary { - var httpContext = CreateHttpContext(); - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["tryParsable"] = routeValue - }); + ["tryParsable"] = routeValue + }); - var factoryResult = RequestDelegateFactory.Create(action); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(action); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - Assert.Equal(expectedParameterValue, httpContext.Items["tryParsable"]); - } + Assert.Equal(expectedParameterValue, httpContext.Items["tryParsable"]); + } + + [Fact] + public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromRouteValueBeforeQueryString() + { + var httpContext = CreateHttpContext(); + + httpContext.Request.RouteValues["tryParsable"] = "42"; - [Fact] - public async Task RequestDelegatePopulatesUnattributedTryParsableParametersFromRouteValueBeforeQueryString() + httpContext.Request.Query = new QueryCollection(new Dictionary { - var httpContext = CreateHttpContext(); + ["tryParsable"] = "invalid!" + }); - httpContext.Request.RouteValues["tryParsable"] = "42"; + var factoryResult = RequestDelegateFactory.Create((HttpContext httpContext, int tryParsable) => + { + httpContext.Items["tryParsable"] = tryParsable; + }); - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["tryParsable"] = "invalid!" - }); + var requestDelegate = factoryResult.RequestDelegate; - var factoryResult = RequestDelegateFactory.Create((HttpContext httpContext, int tryParsable) => - { - httpContext.Items["tryParsable"] = tryParsable; - }); + await requestDelegate(httpContext); - var requestDelegate = factoryResult.RequestDelegate; + Assert.Equal(42, httpContext.Items["tryParsable"]); + } - await requestDelegate(httpContext); + [Fact] + public async Task RequestDelegatePrefersBindAsyncOverTryParse() + { + var httpContext = CreateHttpContext(); - Assert.Equal(42, httpContext.Items["tryParsable"]); - } + httpContext.Request.Headers.Referer = "https://example.org"; - [Fact] - public async Task RequestDelegatePrefersBindAsyncOverTryParse() + var resultFactory = RequestDelegateFactory.Create((HttpContext httpContext, MyBindAsyncRecord myBindAsyncRecord) => { - var httpContext = CreateHttpContext(); + httpContext.Items["myBindAsyncRecord"] = myBindAsyncRecord; + }); - httpContext.Request.Headers.Referer = "https://example.org"; + var requestDelegate = resultFactory.RequestDelegate; - var resultFactory = RequestDelegateFactory.Create((HttpContext httpContext, MyBindAsyncRecord myBindAsyncRecord) => - { - httpContext.Items["myBindAsyncRecord"] = myBindAsyncRecord; - }); + await requestDelegate(httpContext); - var requestDelegate = resultFactory.RequestDelegate; + Assert.Equal(new MyBindAsyncRecord(new Uri("https://example.org")), httpContext.Items["myBindAsyncRecord"]); + } - await requestDelegate(httpContext); + [Fact] + public async Task RequestDelegatePrefersBindAsyncOverTryParseForNonNullableStruct() + { + var httpContext = CreateHttpContext(); - Assert.Equal(new MyBindAsyncRecord(new Uri("https://example.org")), httpContext.Items["myBindAsyncRecord"]); - } + httpContext.Request.Headers.Referer = "https://example.org"; - [Fact] - public async Task RequestDelegatePrefersBindAsyncOverTryParseForNonNullableStruct() + var resultFactory = RequestDelegateFactory.Create((HttpContext httpContext, MyBindAsyncStruct myBindAsyncStruct) => { - var httpContext = CreateHttpContext(); + httpContext.Items["myBindAsyncStruct"] = myBindAsyncStruct; + }); - httpContext.Request.Headers.Referer = "https://example.org"; + var requestDelegate = resultFactory.RequestDelegate; + await requestDelegate(httpContext); - var resultFactory = RequestDelegateFactory.Create((HttpContext httpContext, MyBindAsyncStruct myBindAsyncStruct) => - { - httpContext.Items["myBindAsyncStruct"] = myBindAsyncStruct; - }); + Assert.Equal(new MyBindAsyncStruct(new Uri("https://example.org")), httpContext.Items["myBindAsyncStruct"]); + } - var requestDelegate = resultFactory.RequestDelegate; - await requestDelegate(httpContext); + [Fact] + public async Task RequestDelegateUsesBindAsyncOverTryParseGivenNullableStruct() + { + var httpContext = CreateHttpContext(); - Assert.Equal(new MyBindAsyncStruct(new Uri("https://example.org")), httpContext.Items["myBindAsyncStruct"]); - } + httpContext.Request.Headers.Referer = "https://example.org"; - [Fact] - public async Task RequestDelegateUsesBindAsyncOverTryParseGivenNullableStruct() + var resultFactory = RequestDelegateFactory.Create((HttpContext httpContext, MyBindAsyncStruct? myBindAsyncStruct) => { - var httpContext = CreateHttpContext(); + httpContext.Items["myBindAsyncStruct"] = myBindAsyncStruct; + }); - httpContext.Request.Headers.Referer = "https://example.org"; + var requestDelegate = resultFactory.RequestDelegate; + await requestDelegate(httpContext); - var resultFactory = RequestDelegateFactory.Create((HttpContext httpContext, MyBindAsyncStruct? myBindAsyncStruct) => - { - httpContext.Items["myBindAsyncStruct"] = myBindAsyncStruct; - }); + Assert.Equal(new MyBindAsyncStruct(new Uri("https://example.org")), httpContext.Items["myBindAsyncStruct"]); + } - var requestDelegate = resultFactory.RequestDelegate; - await requestDelegate(httpContext); + [Fact] + public async Task RequestDelegateUsesParameterInfoBindAsyncOverOtherBindAsync() + { + var httpContext = CreateHttpContext(); - Assert.Equal(new MyBindAsyncStruct(new Uri("https://example.org")), httpContext.Items["myBindAsyncStruct"]); - } + httpContext.Request.Headers.Referer = "https://example.org"; - [Fact] - public async Task RequestDelegateUsesParameterInfoBindAsyncOverOtherBindAsync() + var resultFactory = RequestDelegateFactory.Create((HttpContext httpContext, MyBothBindAsyncStruct? myBothBindAsyncStruct) => { - var httpContext = CreateHttpContext(); + httpContext.Items["myBothBindAsyncStruct"] = myBothBindAsyncStruct; + }); - httpContext.Request.Headers.Referer = "https://example.org"; + var requestDelegate = resultFactory.RequestDelegate; + await requestDelegate(httpContext); - var resultFactory = RequestDelegateFactory.Create((HttpContext httpContext, MyBothBindAsyncStruct? myBothBindAsyncStruct) => - { - httpContext.Items["myBothBindAsyncStruct"] = myBothBindAsyncStruct; - }); + Assert.Equal(new MyBothBindAsyncStruct(new Uri("https://example.org")), httpContext.Items["myBothBindAsyncStruct"]); + } - var requestDelegate = resultFactory.RequestDelegate; - await requestDelegate(httpContext); + [Fact] + public async Task RequestDelegateUsesTryParseOverBindAsyncGivenExplicitAttribute() + { + var fromRouteFactoryResult = RequestDelegateFactory.Create((HttpContext httpContext, [FromRoute] MyBindAsyncRecord myBindAsyncRecord) => { }); + var fromQueryFactoryResult = RequestDelegateFactory.Create((HttpContext httpContext, [FromQuery] MyBindAsyncRecord myBindAsyncRecord) => { }); - Assert.Equal(new MyBothBindAsyncStruct(new Uri("https://example.org")), httpContext.Items["myBothBindAsyncStruct"]); - } - [Fact] - public async Task RequestDelegateUsesTryParseOverBindAsyncGivenExplicitAttribute() + var httpContext = CreateHttpContext(); + httpContext.Request.RouteValues["myBindAsyncRecord"] = "foo"; + httpContext.Request.Query = new QueryCollection(new Dictionary { - var fromRouteFactoryResult = RequestDelegateFactory.Create((HttpContext httpContext, [FromRoute] MyBindAsyncRecord myBindAsyncRecord) => { }); - var fromQueryFactoryResult = RequestDelegateFactory.Create((HttpContext httpContext, [FromQuery] MyBindAsyncRecord myBindAsyncRecord) => { }); + ["myBindAsyncRecord"] = "foo" + }); + var fromRouteRequestDelegate = fromRouteFactoryResult.RequestDelegate; + var fromQueryRequestDelegate = fromQueryFactoryResult.RequestDelegate; - var httpContext = CreateHttpContext(); - httpContext.Request.RouteValues["myBindAsyncRecord"] = "foo"; - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["myBindAsyncRecord"] = "foo" - }); + await Assert.ThrowsAsync(() => fromRouteRequestDelegate(httpContext)); + await Assert.ThrowsAsync(() => fromQueryRequestDelegate(httpContext)); + } - var fromRouteRequestDelegate = fromRouteFactoryResult.RequestDelegate; - var fromQueryRequestDelegate = fromQueryFactoryResult.RequestDelegate; + [Fact] + public async Task RequestDelegateCanAwaitValueTasksThatAreNotImmediatelyCompleted() + { + var httpContext = CreateHttpContext(); - await Assert.ThrowsAsync(() => fromRouteRequestDelegate(httpContext)); - await Assert.ThrowsAsync(() => fromQueryRequestDelegate(httpContext)); - } + httpContext.Request.Headers.Referer = "https://example.org"; - [Fact] - public async Task RequestDelegateCanAwaitValueTasksThatAreNotImmediatelyCompleted() - { - var httpContext = CreateHttpContext(); + var resultFactory = RequestDelegateFactory.Create( + (HttpContext httpContext, MyAwaitedBindAsyncRecord myAwaitedBindAsyncRecord, MyAwaitedBindAsyncStruct myAwaitedBindAsyncStruct) => + { + httpContext.Items["myAwaitedBindAsyncRecord"] = myAwaitedBindAsyncRecord; + httpContext.Items["myAwaitedBindAsyncStruct"] = myAwaitedBindAsyncStruct; + }); - httpContext.Request.Headers.Referer = "https://example.org"; + var requestDelegate = resultFactory.RequestDelegate; + await requestDelegate(httpContext); - var resultFactory = RequestDelegateFactory.Create( - (HttpContext httpContext, MyAwaitedBindAsyncRecord myAwaitedBindAsyncRecord, MyAwaitedBindAsyncStruct myAwaitedBindAsyncStruct) => - { - httpContext.Items["myAwaitedBindAsyncRecord"] = myAwaitedBindAsyncRecord; - httpContext.Items["myAwaitedBindAsyncStruct"] = myAwaitedBindAsyncStruct; - }); + Assert.Equal(new MyAwaitedBindAsyncRecord(new Uri("https://example.org")), httpContext.Items["myAwaitedBindAsyncRecord"]); + Assert.Equal(new MyAwaitedBindAsyncStruct(new Uri("https://example.org")), httpContext.Items["myAwaitedBindAsyncStruct"]); + } - var requestDelegate = resultFactory.RequestDelegate; - await requestDelegate(httpContext); + [Fact] + public async Task RequestDelegateUsesBindAsyncFromImplementedInterface() + { + var httpContext = CreateHttpContext(); - Assert.Equal(new MyAwaitedBindAsyncRecord(new Uri("https://example.org")), httpContext.Items["myAwaitedBindAsyncRecord"]); - Assert.Equal(new MyAwaitedBindAsyncStruct(new Uri("https://example.org")), httpContext.Items["myAwaitedBindAsyncStruct"]); - } + httpContext.Request.Headers.Referer = "https://example.org"; - [Fact] - public async Task RequestDelegateUsesBindAsyncFromImplementedInterface() + var resultFactory = RequestDelegateFactory.Create((HttpContext httpContext, MyBindAsyncFromInterfaceRecord myBindAsyncRecord) => { - var httpContext = CreateHttpContext(); - - httpContext.Request.Headers.Referer = "https://example.org"; + httpContext.Items["myBindAsyncFromInterfaceRecord"] = myBindAsyncRecord; + }); - var resultFactory = RequestDelegateFactory.Create((HttpContext httpContext, MyBindAsyncFromInterfaceRecord myBindAsyncRecord) => - { - httpContext.Items["myBindAsyncFromInterfaceRecord"] = myBindAsyncRecord; - }); + var requestDelegate = resultFactory.RequestDelegate; - var requestDelegate = resultFactory.RequestDelegate; - - await requestDelegate(httpContext); + await requestDelegate(httpContext); - Assert.Equal(new MyBindAsyncFromInterfaceRecord(new Uri("https://example.org")), httpContext.Items["myBindAsyncFromInterfaceRecord"]); - } + Assert.Equal(new MyBindAsyncFromInterfaceRecord(new Uri("https://example.org")), httpContext.Items["myBindAsyncFromInterfaceRecord"]); + } - public static object[][] DelegatesWithAttributesOnNotTryParsableParameters + public static object[][] DelegatesWithAttributesOnNotTryParsableParameters + { + get { - get - { - void InvalidFromRoute([FromRoute] object notTryParsable) { } - void InvalidFromQuery([FromQuery] object notTryParsable) { } - void InvalidFromHeader([FromHeader] object notTryParsable) { } + void InvalidFromRoute([FromRoute] object notTryParsable) { } + void InvalidFromQuery([FromQuery] object notTryParsable) { } + void InvalidFromHeader([FromHeader] object notTryParsable) { } - return new[] - { + return new[] + { new object[] { (Action)InvalidFromRoute }, new object[] { (Action)InvalidFromQuery }, new object[] { (Action)InvalidFromHeader }, }; - } } + } - [Theory] - [MemberData(nameof(DelegatesWithAttributesOnNotTryParsableParameters))] - public void CreateThrowsInvalidOperationExceptionWhenAttributeRequiresTryParseMethodThatDoesNotExist(Delegate action) - { - var ex = Assert.Throws(() => RequestDelegateFactory.Create(action)); - Assert.Equal("No public static bool object.TryParse(string, out object) method found for notTryParsable.", ex.Message); - } + [Theory] + [MemberData(nameof(DelegatesWithAttributesOnNotTryParsableParameters))] + public void CreateThrowsInvalidOperationExceptionWhenAttributeRequiresTryParseMethodThatDoesNotExist(Delegate action) + { + var ex = Assert.Throws(() => RequestDelegateFactory.Create(action)); + Assert.Equal("No public static bool object.TryParse(string, out object) method found for notTryParsable.", ex.Message); + } - [Fact] - public void CreateThrowsInvalidOperationExceptionGivenUnnamedArgument() - { - var unnamedParameter = Expression.Parameter(typeof(int)); - var lambda = Expression.Lambda(Expression.Block(), unnamedParameter); - var ex = Assert.Throws(() => RequestDelegateFactory.Create(lambda.Compile())); - Assert.Equal("Encountered a parameter of type 'System.Runtime.CompilerServices.Closure' without a name. Parameters must have a name.", ex.Message); - } + [Fact] + public void CreateThrowsInvalidOperationExceptionGivenUnnamedArgument() + { + var unnamedParameter = Expression.Parameter(typeof(int)); + var lambda = Expression.Lambda(Expression.Block(), unnamedParameter); + var ex = Assert.Throws(() => RequestDelegateFactory.Create(lambda.Compile())); + Assert.Equal("Encountered a parameter of type 'System.Runtime.CompilerServices.Closure' without a name. Parameters must have a name.", ex.Message); + } + + [Fact] + public async Task RequestDelegateLogsTryParsableFailuresAsDebugAndSets400Response() + { + var invoked = false; - [Fact] - public async Task RequestDelegateLogsTryParsableFailuresAsDebugAndSets400Response() + void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2) { - var invoked = false; + invoked = true; + } - void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2) - { - invoked = true; - } + var httpContext = CreateHttpContext(); + httpContext.Request.RouteValues["tryParsable"] = "invalid!"; + httpContext.Request.RouteValues["tryParsable2"] = "invalid again!"; - var httpContext = CreateHttpContext(); - httpContext.Request.RouteValues["tryParsable"] = "invalid!"; - httpContext.Request.RouteValues["tryParsable2"] = "invalid again!"; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); - await requestDelegate(httpContext); + Assert.False(invoked); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + Assert.Equal(400, httpContext.Response.StatusCode); + Assert.False(httpContext.Response.HasStarted); - Assert.False(invoked); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - Assert.Equal(400, httpContext.Response.StatusCode); - Assert.False(httpContext.Response.HasStarted); + var logs = TestSink.Writes.ToArray(); - var logs = TestSink.Writes.ToArray(); + Assert.Equal(2, logs.Length); - Assert.Equal(2, logs.Length); + Assert.Equal(new EventId(3, "ParameterBindingFailed"), logs[0].EventId); + Assert.Equal(LogLevel.Debug, logs[0].LogLevel); + Assert.Equal(@"Failed to bind parameter ""int tryParsable"" from ""invalid!"".", logs[0].Message); - Assert.Equal(new EventId(3, "ParameterBindingFailed"), logs[0].EventId); - Assert.Equal(LogLevel.Debug, logs[0].LogLevel); - Assert.Equal(@"Failed to bind parameter ""int tryParsable"" from ""invalid!"".", logs[0].Message); + Assert.Equal(new EventId(3, "ParameterBindingFailed"), logs[1].EventId); + Assert.Equal(LogLevel.Debug, logs[1].LogLevel); + Assert.Equal(@"Failed to bind parameter ""int tryParsable2"" from ""invalid again!"".", logs[1].Message); + } - Assert.Equal(new EventId(3, "ParameterBindingFailed"), logs[1].EventId); - Assert.Equal(LogLevel.Debug, logs[1].LogLevel); - Assert.Equal(@"Failed to bind parameter ""int tryParsable2"" from ""invalid again!"".", logs[1].Message); - } + [Fact] + public async Task RequestDelegateThrowsForTryParsableFailuresIfThrowOnBadRequest() + { + var invoked = false; - [Fact] - public async Task RequestDelegateThrowsForTryParsableFailuresIfThrowOnBadRequest() + void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2) { - var invoked = false; + invoked = true; + } - void TestAction([FromRoute] int tryParsable, [FromRoute] int tryParsable2) - { - invoked = true; - } + var httpContext = CreateHttpContext(); + httpContext.Request.RouteValues["tryParsable"] = "invalid!"; + httpContext.Request.RouteValues["tryParsable2"] = "invalid again!"; - var httpContext = CreateHttpContext(); - httpContext.Request.RouteValues["tryParsable"] = "invalid!"; - httpContext.Request.RouteValues["tryParsable2"] = "invalid again!"; + var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = true }); + var requestDelegate = factoryResult.RequestDelegate; - var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = true }); - var requestDelegate = factoryResult.RequestDelegate; + var badHttpRequestException = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); - var badHttpRequestException = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); + Assert.False(invoked); - Assert.False(invoked); + // The httpContext should be untouched. + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.Response.HasStarted); - // The httpContext should be untouched. - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.False(httpContext.Response.HasStarted); + // We don't log bad requests when we throw. + Assert.Empty(TestSink.Writes); - // We don't log bad requests when we throw. - Assert.Empty(TestSink.Writes); + Assert.Equal(@"Failed to bind parameter ""int tryParsable"" from ""invalid!"".", badHttpRequestException.Message); + Assert.Equal(400, badHttpRequestException.StatusCode); + } - Assert.Equal(@"Failed to bind parameter ""int tryParsable"" from ""invalid!"".", badHttpRequestException.Message); - Assert.Equal(400, badHttpRequestException.StatusCode); - } + [Fact] + public async Task RequestDelegateLogsBindAsyncFailuresAndSets400Response() + { + // Not supplying any headers will cause the HttpContext BindAsync overload to return null. + var httpContext = CreateHttpContext(); + var invoked = false; - [Fact] - public async Task RequestDelegateLogsBindAsyncFailuresAndSets400Response() + var factoryResult = RequestDelegateFactory.Create((MyBindAsyncRecord myBindAsyncRecord1, MyBindAsyncRecord myBindAsyncRecord2) => { - // Not supplying any headers will cause the HttpContext BindAsync overload to return null. - var httpContext = CreateHttpContext(); - var invoked = false; + invoked = true; + }); - var factoryResult = RequestDelegateFactory.Create((MyBindAsyncRecord myBindAsyncRecord1, MyBindAsyncRecord myBindAsyncRecord2) => - { - invoked = true; - }); + var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + Assert.False(invoked); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + Assert.Equal(400, httpContext.Response.StatusCode); - Assert.False(invoked); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - Assert.Equal(400, httpContext.Response.StatusCode); + var logs = TestSink.Writes.ToArray(); - var logs = TestSink.Writes.ToArray(); + Assert.Equal(2, logs.Length); - Assert.Equal(2, logs.Length); + Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[0].EventId); + Assert.Equal(LogLevel.Debug, logs[0].LogLevel); + Assert.Equal(@"Required parameter ""MyBindAsyncRecord myBindAsyncRecord1"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", logs[0].Message); - Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[0].EventId); - Assert.Equal(LogLevel.Debug, logs[0].LogLevel); - Assert.Equal(@"Required parameter ""MyBindAsyncRecord myBindAsyncRecord1"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", logs[0].Message); + Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[1].EventId); + Assert.Equal(LogLevel.Debug, logs[1].LogLevel); + Assert.Equal(@"Required parameter ""MyBindAsyncRecord myBindAsyncRecord2"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", logs[1].Message); + } - Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[1].EventId); - Assert.Equal(LogLevel.Debug, logs[1].LogLevel); - Assert.Equal(@"Required parameter ""MyBindAsyncRecord myBindAsyncRecord2"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", logs[1].Message); - } + [Fact] + public async Task RequestDelegateThrowsForBindAsyncFailuresIfThrowOnBadRequest() + { + // Not supplying any headers will cause the HttpContext BindAsync overload to return null. + var httpContext = CreateHttpContext(); + var invoked = false; - [Fact] - public async Task RequestDelegateThrowsForBindAsyncFailuresIfThrowOnBadRequest() + var factoryResult = RequestDelegateFactory.Create((MyBindAsyncRecord myBindAsyncRecord1, MyBindAsyncRecord myBindAsyncRecord2) => { - // Not supplying any headers will cause the HttpContext BindAsync overload to return null. - var httpContext = CreateHttpContext(); - var invoked = false; + invoked = true; + }, new() { ThrowOnBadRequest = true }); - var factoryResult = RequestDelegateFactory.Create((MyBindAsyncRecord myBindAsyncRecord1, MyBindAsyncRecord myBindAsyncRecord2) => - { - invoked = true; - }, new() { ThrowOnBadRequest = true }); + var requestDelegate = factoryResult.RequestDelegate; + var badHttpRequestException = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); - var requestDelegate = factoryResult.RequestDelegate; - var badHttpRequestException = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); + Assert.False(invoked); - Assert.False(invoked); + // The httpContext should be untouched. + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.Response.HasStarted); - // The httpContext should be untouched. - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.False(httpContext.Response.HasStarted); + // We don't log bad requests when we throw. + Assert.Empty(TestSink.Writes); - // We don't log bad requests when we throw. - Assert.Empty(TestSink.Writes); + Assert.Equal(@"Required parameter ""MyBindAsyncRecord myBindAsyncRecord1"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", badHttpRequestException.Message); + Assert.Equal(400, badHttpRequestException.StatusCode); + } - Assert.Equal(@"Required parameter ""MyBindAsyncRecord myBindAsyncRecord1"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", badHttpRequestException.Message); - Assert.Equal(400, badHttpRequestException.StatusCode); - } + [Fact] + public async Task RequestDelegateLogsSingleArgBindAsyncFailuresAndSets400Response() + { + // Not supplying any headers will cause the HttpContext BindAsync overload to return null. + var httpContext = CreateHttpContext(); + var invoked = false; - [Fact] - public async Task RequestDelegateLogsSingleArgBindAsyncFailuresAndSets400Response() + var factoryResult = RequestDelegateFactory.Create((MySimpleBindAsyncRecord mySimpleBindAsyncRecord1, + MySimpleBindAsyncRecord mySimpleBindAsyncRecord2) => { - // Not supplying any headers will cause the HttpContext BindAsync overload to return null. - var httpContext = CreateHttpContext(); - var invoked = false; + invoked = true; + }); - var factoryResult = RequestDelegateFactory.Create((MySimpleBindAsyncRecord mySimpleBindAsyncRecord1, - MySimpleBindAsyncRecord mySimpleBindAsyncRecord2) => - { - invoked = true; - }); + var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + Assert.False(invoked); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + Assert.Equal(400, httpContext.Response.StatusCode); - Assert.False(invoked); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - Assert.Equal(400, httpContext.Response.StatusCode); + var logs = TestSink.Writes.ToArray(); - var logs = TestSink.Writes.ToArray(); + Assert.Equal(2, logs.Length); - Assert.Equal(2, logs.Length); + Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[0].EventId); + Assert.Equal(LogLevel.Debug, logs[0].LogLevel); + Assert.Equal(@"Required parameter ""MySimpleBindAsyncRecord mySimpleBindAsyncRecord1"" was not provided from MySimpleBindAsyncRecord.BindAsync(HttpContext).", logs[0].Message); - Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[0].EventId); - Assert.Equal(LogLevel.Debug, logs[0].LogLevel); - Assert.Equal(@"Required parameter ""MySimpleBindAsyncRecord mySimpleBindAsyncRecord1"" was not provided from MySimpleBindAsyncRecord.BindAsync(HttpContext).", logs[0].Message); + Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[1].EventId); + Assert.Equal(LogLevel.Debug, logs[1].LogLevel); + Assert.Equal(@"Required parameter ""MySimpleBindAsyncRecord mySimpleBindAsyncRecord2"" was not provided from MySimpleBindAsyncRecord.BindAsync(HttpContext).", logs[1].Message); + } - Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), logs[1].EventId); - Assert.Equal(LogLevel.Debug, logs[1].LogLevel); - Assert.Equal(@"Required parameter ""MySimpleBindAsyncRecord mySimpleBindAsyncRecord2"" was not provided from MySimpleBindAsyncRecord.BindAsync(HttpContext).", logs[1].Message); - } + [Fact] + public async Task RequestDelegateThrowsForSingleArgBindAsyncFailuresIfThrowOnBadRequest() + { + // Not supplying any headers will cause the HttpContext BindAsync overload to return null. + var httpContext = CreateHttpContext(); + var invoked = false; - [Fact] - public async Task RequestDelegateThrowsForSingleArgBindAsyncFailuresIfThrowOnBadRequest() + var factoryResult = RequestDelegateFactory.Create((MySimpleBindAsyncRecord mySimpleBindAsyncRecord1, + MySimpleBindAsyncRecord mySimpleBindAsyncRecord2) => { - // Not supplying any headers will cause the HttpContext BindAsync overload to return null. - var httpContext = CreateHttpContext(); - var invoked = false; - - var factoryResult = RequestDelegateFactory.Create((MySimpleBindAsyncRecord mySimpleBindAsyncRecord1, - MySimpleBindAsyncRecord mySimpleBindAsyncRecord2) => - { - invoked = true; - }, new() { ThrowOnBadRequest = true }); + invoked = true; + }, new() { ThrowOnBadRequest = true }); - var requestDelegate = factoryResult.RequestDelegate; - var badHttpRequestException = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); + var requestDelegate = factoryResult.RequestDelegate; + var badHttpRequestException = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); - Assert.False(invoked); + Assert.False(invoked); - // The httpContext should be untouched. - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.False(httpContext.Response.HasStarted); + // The httpContext should be untouched. + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.Response.HasStarted); - // We don't log bad requests when we throw. - Assert.Empty(TestSink.Writes); + // We don't log bad requests when we throw. + Assert.Empty(TestSink.Writes); - Assert.Equal(@"Required parameter ""MySimpleBindAsyncRecord mySimpleBindAsyncRecord1"" was not provided from MySimpleBindAsyncRecord.BindAsync(HttpContext).", badHttpRequestException.Message); - Assert.Equal(400, badHttpRequestException.StatusCode); - } + Assert.Equal(@"Required parameter ""MySimpleBindAsyncRecord mySimpleBindAsyncRecord1"" was not provided from MySimpleBindAsyncRecord.BindAsync(HttpContext).", badHttpRequestException.Message); + Assert.Equal(400, badHttpRequestException.StatusCode); + } - [Fact] - public async Task BindAsyncExceptionsAreUncaught() - { - var httpContext = CreateHttpContext(); + [Fact] + public async Task BindAsyncExceptionsAreUncaught() + { + var httpContext = CreateHttpContext(); - var factoryResult = RequestDelegateFactory.Create((MyBindAsyncTypeThatThrows arg1) => { }); + var factoryResult = RequestDelegateFactory.Create((MyBindAsyncTypeThatThrows arg1) => { }); - var requestDelegate = factoryResult.RequestDelegate; + var requestDelegate = factoryResult.RequestDelegate; - var ex = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); - Assert.Equal("BindAsync failed", ex.Message); - } + var ex = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); + Assert.Equal("BindAsync failed", ex.Message); + } - [Fact] - public async Task BindAsyncWithBodyArgument() + [Fact] + public async Task BindAsyncWithBodyArgument() + { + Todo originalTodo = new() { - Todo originalTodo = new() - { - Name = "Write more tests!" - }; + Name = "Write more tests!" + }; - var httpContext = CreateHttpContext(); + var httpContext = CreateHttpContext(); - var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); - var stream = new MemoryStream(requestBodyBytes); - httpContext.Request.Body = stream; + var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); + var stream = new MemoryStream(requestBodyBytes); + httpContext.Request.Body = stream; - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); - httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - var jsonOptions = new JsonOptions(); - jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter()); + var jsonOptions = new JsonOptions(); + jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter()); - var mock = new Mock(); - mock.Setup(m => m.GetService(It.IsAny())).Returns(t => + var mock = new Mock(); + mock.Setup(m => m.GetService(It.IsAny())).Returns(t => + { + if (t == typeof(IOptions)) { - if (t == typeof(IOptions)) - { - return Options.Create(jsonOptions); - } - return null; - }); + return Options.Create(jsonOptions); + } + return null; + }); - httpContext.RequestServices = mock.Object; - httpContext.Request.Headers.Referer = "https://example.org"; + httpContext.RequestServices = mock.Object; + httpContext.Request.Headers.Referer = "https://example.org"; - var invoked = false; + var invoked = false; - var factoryResult = RequestDelegateFactory.Create((HttpContext context, MyBindAsyncRecord myBindAsyncRecord, Todo todo) => - { - invoked = true; - context.Items[nameof(myBindAsyncRecord)] = myBindAsyncRecord; - context.Items[nameof(todo)] = todo; - }); + var factoryResult = RequestDelegateFactory.Create((HttpContext context, MyBindAsyncRecord myBindAsyncRecord, Todo todo) => + { + invoked = true; + context.Items[nameof(myBindAsyncRecord)] = myBindAsyncRecord; + context.Items[nameof(todo)] = todo; + }); - var requestDelegate = factoryResult.RequestDelegate; + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - Assert.True(invoked); - var arg = httpContext.Items["myBindAsyncRecord"] as MyBindAsyncRecord; - Assert.NotNull(arg); - Assert.Equal("https://example.org/", arg!.Uri.ToString()); - var todo = httpContext.Items["todo"] as Todo; - Assert.NotNull(todo); - Assert.Equal("Write more tests!", todo!.Name); - } + Assert.True(invoked); + var arg = httpContext.Items["myBindAsyncRecord"] as MyBindAsyncRecord; + Assert.NotNull(arg); + Assert.Equal("https://example.org/", arg!.Uri.ToString()); + var todo = httpContext.Items["todo"] as Todo; + Assert.NotNull(todo); + Assert.Equal("Write more tests!", todo!.Name); + } - [Fact] - public async Task BindAsyncRunsBeforeBodyBinding() + [Fact] + public async Task BindAsyncRunsBeforeBodyBinding() + { + Todo originalTodo = new() { - Todo originalTodo = new() - { - Name = "Write more tests!" - }; + Name = "Write more tests!" + }; - var httpContext = CreateHttpContext(); + var httpContext = CreateHttpContext(); - var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); - var stream = new MemoryStream(requestBodyBytes); - httpContext.Request.Body = stream; + var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); + var stream = new MemoryStream(requestBodyBytes); + httpContext.Request.Body = stream; - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); - httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - var jsonOptions = new JsonOptions(); - jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter()); + var jsonOptions = new JsonOptions(); + jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter()); - var mock = new Mock(); - mock.Setup(m => m.GetService(It.IsAny())).Returns(t => + var mock = new Mock(); + mock.Setup(m => m.GetService(It.IsAny())).Returns(t => + { + if (t == typeof(IOptions)) { - if (t == typeof(IOptions)) - { - return Options.Create(jsonOptions); - } - return null; - }); + return Options.Create(jsonOptions); + } + return null; + }); - httpContext.RequestServices = mock.Object; - httpContext.Request.Headers.Referer = "https://example.org"; + httpContext.RequestServices = mock.Object; + httpContext.Request.Headers.Referer = "https://example.org"; - var invoked = false; + var invoked = false; - var factoryResult = RequestDelegateFactory.Create((HttpContext context, CustomTodo customTodo, Todo todo) => - { - invoked = true; - context.Items[nameof(customTodo)] = customTodo; - context.Items[nameof(todo)] = todo; - }); + var factoryResult = RequestDelegateFactory.Create((HttpContext context, CustomTodo customTodo, Todo todo) => + { + invoked = true; + context.Items[nameof(customTodo)] = customTodo; + context.Items[nameof(todo)] = todo; + }); - var requestDelegate = factoryResult.RequestDelegate; + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); + + Assert.True(invoked); + var todo0 = httpContext.Items["customTodo"] as Todo; + Assert.NotNull(todo0); + Assert.Equal("Write more tests!", todo0!.Name); + var todo1 = httpContext.Items["todo"] as Todo; + Assert.NotNull(todo1); + Assert.Equal("Write more tests!", todo1!.Name); + } + + [Fact] + public async Task RequestDelegatePopulatesFromQueryParameterBasedOnParameterName() + { + const string paramName = "value"; + const int originalQueryParam = 42; + + int? deserializedRouteParam = null; - Assert.True(invoked); - var todo0 = httpContext.Items["customTodo"] as Todo; - Assert.NotNull(todo0); - Assert.Equal("Write more tests!", todo0!.Name); - var todo1 = httpContext.Items["todo"] as Todo; - Assert.NotNull(todo1); - Assert.Equal("Write more tests!", todo1!.Name); + void TestAction([FromQuery] int value) + { + deserializedRouteParam = value; } - [Fact] - public async Task RequestDelegatePopulatesFromQueryParameterBasedOnParameterName() + var query = new QueryCollection(new Dictionary() { - const string paramName = "value"; - const int originalQueryParam = 42; + [paramName] = originalQueryParam.ToString(NumberFormatInfo.InvariantInfo) + }); - int? deserializedRouteParam = null; + var httpContext = CreateHttpContext(); + httpContext.Request.Query = query; - void TestAction([FromQuery] int value) - { - deserializedRouteParam = value; - } + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; - var query = new QueryCollection(new Dictionary() - { - [paramName] = originalQueryParam.ToString(NumberFormatInfo.InvariantInfo) - }); + await requestDelegate(httpContext); - var httpContext = CreateHttpContext(); - httpContext.Request.Query = query; + Assert.Equal(originalQueryParam, deserializedRouteParam); + } - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; + [Fact] + public async Task RequestDelegatePopulatesFromHeaderParameterBasedOnParameterName() + { + const string customHeaderName = "X-Custom-Header"; + const int originalHeaderParam = 42; - await requestDelegate(httpContext); + int? deserializedRouteParam = null; - Assert.Equal(originalQueryParam, deserializedRouteParam); + void TestAction([FromHeader(Name = customHeaderName)] int value) + { + deserializedRouteParam = value; } - [Fact] - public async Task RequestDelegatePopulatesFromHeaderParameterBasedOnParameterName() - { - const string customHeaderName = "X-Custom-Header"; - const int originalHeaderParam = 42; + var httpContext = CreateHttpContext(); + httpContext.Request.Headers[customHeaderName] = originalHeaderParam.ToString(NumberFormatInfo.InvariantInfo); + + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; + + await requestDelegate(httpContext); - int? deserializedRouteParam = null; + Assert.Equal(originalHeaderParam, deserializedRouteParam); + } - void TestAction([FromHeader(Name = customHeaderName)] int value) + public static object[][] ImplicitFromBodyActions + { + get + { + void TestImpliedFromBody(HttpContext httpContext, Todo todo) { - deserializedRouteParam = value; + httpContext.Items.Add("body", todo); } - var httpContext = CreateHttpContext(); - httpContext.Request.Headers[customHeaderName] = originalHeaderParam.ToString(NumberFormatInfo.InvariantInfo); - - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; + void TestImpliedFromBodyInterface(HttpContext httpContext, ITodo todo) + { + httpContext.Items.Add("body", todo); + } - await requestDelegate(httpContext); + void TestImpliedFromBodyStruct(HttpContext httpContext, TodoStruct todo) + { + httpContext.Items.Add("body", todo); + } - Assert.Equal(originalHeaderParam, deserializedRouteParam); + return new[] + { + new[] { (Action)TestImpliedFromBody }, + new[] { (Action)TestImpliedFromBodyInterface }, + new object[] { (Action)TestImpliedFromBodyStruct }, + }; } + } - public static object[][] ImplicitFromBodyActions + public static object[][] ExplicitFromBodyActions + { + get { - get + void TestExplicitFromBody(HttpContext httpContext, [FromBody] Todo todo) { - void TestImpliedFromBody(HttpContext httpContext, Todo todo) - { - httpContext.Items.Add("body", todo); - } - - void TestImpliedFromBodyInterface(HttpContext httpContext, ITodo todo) - { - httpContext.Items.Add("body", todo); - } - - void TestImpliedFromBodyStruct(HttpContext httpContext, TodoStruct todo) - { - httpContext.Items.Add("body", todo); - } - - return new[] - { - new[] { (Action)TestImpliedFromBody }, - new[] { (Action)TestImpliedFromBodyInterface }, - new object[] { (Action)TestImpliedFromBodyStruct }, - }; + httpContext.Items.Add("body", todo); } - } - public static object[][] ExplicitFromBodyActions - { - get + return new[] { - void TestExplicitFromBody(HttpContext httpContext, [FromBody] Todo todo) - { - httpContext.Items.Add("body", todo); - } - - return new[] - { new[] { (Action)TestExplicitFromBody }, }; - } } + } - public static object[][] FromBodyActions + public static object[][] FromBodyActions + { + get { - get - { - return ExplicitFromBodyActions.Concat(ImplicitFromBodyActions).ToArray(); - } + return ExplicitFromBodyActions.Concat(ImplicitFromBodyActions).ToArray(); } + } - [Theory] - [MemberData(nameof(FromBodyActions))] - public async Task RequestDelegatePopulatesFromBodyParameter(Delegate action) + [Theory] + [MemberData(nameof(FromBodyActions))] + public async Task RequestDelegatePopulatesFromBodyParameter(Delegate action) + { + Todo originalTodo = new() { - Todo originalTodo = new() - { - Name = "Write more tests!" - }; + Name = "Write more tests!" + }; - var httpContext = CreateHttpContext(); + var httpContext = CreateHttpContext(); - var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); - var stream = new MemoryStream(requestBodyBytes); - httpContext.Request.Body = stream; + var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); + var stream = new MemoryStream(requestBodyBytes); + httpContext.Request.Body = stream; - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); - httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - var jsonOptions = new JsonOptions(); - jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter()); + var jsonOptions = new JsonOptions(); + jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter()); - var mock = new Mock(); - mock.Setup(m => m.GetService(It.IsAny())).Returns(t => + var mock = new Mock(); + mock.Setup(m => m.GetService(It.IsAny())).Returns(t => + { + if (t == typeof(IOptions)) { - if (t == typeof(IOptions)) - { - return Options.Create(jsonOptions); - } - return null; - }); - httpContext.RequestServices = mock.Object; + return Options.Create(jsonOptions); + } + return null; + }); + httpContext.RequestServices = mock.Object; - var factoryResult = RequestDelegateFactory.Create(action); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(action); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - var deserializedRequestBody = httpContext.Items["body"]; - Assert.NotNull(deserializedRequestBody); - Assert.Equal(originalTodo.Name, ((ITodo)deserializedRequestBody!).Name); - } + var deserializedRequestBody = httpContext.Items["body"]; + Assert.NotNull(deserializedRequestBody); + Assert.Equal(originalTodo.Name, ((ITodo)deserializedRequestBody!).Name); + } - [Theory] - [MemberData(nameof(ExplicitFromBodyActions))] - public async Task RequestDelegateRejectsEmptyBodyGivenExplicitFromBodyParameter(Delegate action) - { - var httpContext = CreateHttpContext(); - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.Headers["Content-Length"] = "0"; - httpContext.Features.Set(new RequestBodyDetectionFeature(false)); + [Theory] + [MemberData(nameof(ExplicitFromBodyActions))] + public async Task RequestDelegateRejectsEmptyBodyGivenExplicitFromBodyParameter(Delegate action) + { + var httpContext = CreateHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = "0"; + httpContext.Features.Set(new RequestBodyDetectionFeature(false)); - var factoryResult = RequestDelegateFactory.Create(action); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(action); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - Assert.Equal(400, httpContext.Response.StatusCode); - } + Assert.Equal(400, httpContext.Response.StatusCode); + } - [Theory] - [MemberData(nameof(ImplicitFromBodyActions))] - public async Task RequestDelegateRejectsEmptyBodyGivenImplicitFromBodyParameter(Delegate action) - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.Headers["Content-Length"] = "0"; - httpContext.Features.Set(new RequestBodyDetectionFeature(false)); + [Theory] + [MemberData(nameof(ImplicitFromBodyActions))] + public async Task RequestDelegateRejectsEmptyBodyGivenImplicitFromBodyParameter(Delegate action) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = "0"; + httpContext.Features.Set(new RequestBodyDetectionFeature(false)); - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(LoggerFactory); + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var factoryResult = RequestDelegateFactory.Create(action, new RequestDelegateFactoryOptions() { ThrowOnBadRequest = true }); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(action, new RequestDelegateFactoryOptions() { ThrowOnBadRequest = true }); + var requestDelegate = factoryResult.RequestDelegate; - var ex = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); - Assert.StartsWith("Implicit body inferred for parameter", ex.Message); - Assert.EndsWith("but no body was provided. Did you mean to use a Service instead?", ex.Message); - } + var ex = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); + Assert.StartsWith("Implicit body inferred for parameter", ex.Message); + Assert.EndsWith("but no body was provided. Did you mean to use a Service instead?", ex.Message); + } + + [Fact] + public async Task RequestDelegateAllowsEmptyBodyGivenCorrectyConfiguredFromBodyParameter() + { + var todoToBecomeNull = new Todo(); - [Fact] - public async Task RequestDelegateAllowsEmptyBodyGivenCorrectyConfiguredFromBodyParameter() + void TestAction([FromBody(AllowEmpty = true)] Todo todo) { - var todoToBecomeNull = new Todo(); + todoToBecomeNull = todo; + } - void TestAction([FromBody(AllowEmpty = true)] Todo todo) - { - todoToBecomeNull = todo; - } + var httpContext = CreateHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = "0"; - var httpContext = CreateHttpContext(); - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.Headers["Content-Length"] = "0"; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); - await requestDelegate(httpContext); + Assert.Null(todoToBecomeNull); + } - Assert.Null(todoToBecomeNull); - } + [Fact] + public async Task RequestDelegateAllowsEmptyBodyStructGivenCorrectyConfiguredFromBodyParameter() + { + var structToBeZeroed = new BodyStruct + { + Id = 42 + }; - [Fact] - public async Task RequestDelegateAllowsEmptyBodyStructGivenCorrectyConfiguredFromBodyParameter() + void TestAction([FromBody(AllowEmpty = true)] BodyStruct bodyStruct) { - var structToBeZeroed = new BodyStruct - { - Id = 42 - }; + structToBeZeroed = bodyStruct; + } - void TestAction([FromBody(AllowEmpty = true)] BodyStruct bodyStruct) - { - structToBeZeroed = bodyStruct; - } + var httpContext = CreateHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = "0"; - var httpContext = CreateHttpContext(); - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.Headers["Content-Length"] = "0"; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); - await requestDelegate(httpContext); + Assert.Equal(default, structToBeZeroed); + } - Assert.Equal(default, structToBeZeroed); - } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task RequestDelegateLogsIOExceptionsAsDebugDoesNotAbortAndNeverThrows(bool throwOnBadRequests) + { + var invoked = false; - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task RequestDelegateLogsIOExceptionsAsDebugDoesNotAbortAndNeverThrows(bool throwOnBadRequests) + void TestAction([FromBody] Todo todo) { - var invoked = false; + invoked = true; + } - void TestAction([FromBody] Todo todo) - { - invoked = true; - } + var ioException = new IOException(); - var ioException = new IOException(); + var httpContext = CreateHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = "1"; + httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(ioException); + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - var httpContext = CreateHttpContext(); - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.Headers["Content-Length"] = "1"; - httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(ioException); - httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = throwOnBadRequests }); + var requestDelegate = factoryResult.RequestDelegate; - var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = throwOnBadRequests }); - var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); - await requestDelegate(httpContext); + Assert.False(invoked); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); - Assert.False(invoked); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); + var logMessage = Assert.Single(TestSink.Writes); + Assert.Equal(new EventId(1, "RequestBodyIOException"), logMessage.EventId); + Assert.Equal(LogLevel.Debug, logMessage.LogLevel); + Assert.Equal("Reading the request body failed with an IOException.", logMessage.Message); + Assert.Same(ioException, logMessage.Exception); + } - var logMessage = Assert.Single(TestSink.Writes); - Assert.Equal(new EventId(1, "RequestBodyIOException"), logMessage.EventId); - Assert.Equal(LogLevel.Debug, logMessage.LogLevel); - Assert.Equal("Reading the request body failed with an IOException.", logMessage.Message); - Assert.Same(ioException, logMessage.Exception); - } + [Fact] + public async Task RequestDelegateLogsJsonExceptionsAsDebugAndSets400Response() + { + var invoked = false; - [Fact] - public async Task RequestDelegateLogsJsonExceptionsAsDebugAndSets400Response() + void TestAction([FromBody] Todo todo) { - var invoked = false; + invoked = true; + } - void TestAction([FromBody] Todo todo) - { - invoked = true; - } + var jsonException = new JsonException(); - var jsonException = new JsonException(); + var httpContext = CreateHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = "1"; + httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(jsonException); + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - var httpContext = CreateHttpContext(); - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.Headers["Content-Length"] = "1"; - httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(jsonException); - httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); - await requestDelegate(httpContext); + Assert.False(invoked); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + Assert.Equal(400, httpContext.Response.StatusCode); + Assert.False(httpContext.Response.HasStarted); - Assert.False(invoked); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - Assert.Equal(400, httpContext.Response.StatusCode); - Assert.False(httpContext.Response.HasStarted); + var logMessage = Assert.Single(TestSink.Writes); + Assert.Equal(new EventId(2, "InvalidJsonRequestBody"), logMessage.EventId); + Assert.Equal(LogLevel.Debug, logMessage.LogLevel); + Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", logMessage.Message); + Assert.Same(jsonException, logMessage.Exception); + } - var logMessage = Assert.Single(TestSink.Writes); - Assert.Equal(new EventId(2, "InvalidJsonRequestBody"), logMessage.EventId); - Assert.Equal(LogLevel.Debug, logMessage.LogLevel); - Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", logMessage.Message); - Assert.Same(jsonException, logMessage.Exception); - } + [Fact] + public async Task RequestDelegateThrowsForJsonExceptionsIfThrowOnBadRequest() + { + var invoked = false; - [Fact] - public async Task RequestDelegateThrowsForJsonExceptionsIfThrowOnBadRequest() + void TestAction([FromBody] Todo todo) { - var invoked = false; + invoked = true; + } - void TestAction([FromBody] Todo todo) - { - invoked = true; - } + var jsonException = new JsonException(); - var jsonException = new JsonException(); + var httpContext = CreateHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = "1"; + httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(jsonException); + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - var httpContext = CreateHttpContext(); - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.Headers["Content-Length"] = "1"; - httpContext.Request.Body = new ExceptionThrowingRequestBodyStream(jsonException); - httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = true }); + var requestDelegate = factoryResult.RequestDelegate; - var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = true }); - var requestDelegate = factoryResult.RequestDelegate; + var badHttpRequestException = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); - var badHttpRequestException = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); + Assert.False(invoked); - Assert.False(invoked); + // The httpContext should be untouched. + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.Response.HasStarted); - // The httpContext should be untouched. - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.False(httpContext.Response.HasStarted); + // We don't log bad requests when we throw. + Assert.Empty(TestSink.Writes); - // We don't log bad requests when we throw. - Assert.Empty(TestSink.Writes); + Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", badHttpRequestException.Message); + Assert.Equal(400, badHttpRequestException.StatusCode); + Assert.Same(jsonException, badHttpRequestException.InnerException); + } - Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", badHttpRequestException.Message); - Assert.Equal(400, badHttpRequestException.StatusCode); - Assert.Same(jsonException, badHttpRequestException.InnerException); - } + [Fact] + public async Task RequestDelegateLogsMalformedJsonAsDebugAndSets400Response() + { + var invoked = false; - [Fact] - public async Task RequestDelegateLogsMalformedJsonAsDebugAndSets400Response() + void TestAction([FromBody] Todo todo) { - var invoked = false; + invoked = true; + } - void TestAction([FromBody] Todo todo) - { - invoked = true; - } + var httpContext = CreateHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = "1"; + httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{")); + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - var httpContext = CreateHttpContext(); - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.Headers["Content-Length"] = "1"; - httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{")); - httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); - await requestDelegate(httpContext); + Assert.False(invoked); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + Assert.Equal(400, httpContext.Response.StatusCode); + Assert.False(httpContext.Response.HasStarted); - Assert.False(invoked); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - Assert.Equal(400, httpContext.Response.StatusCode); - Assert.False(httpContext.Response.HasStarted); + var logMessage = Assert.Single(TestSink.Writes); + Assert.Equal(new EventId(2, "InvalidJsonRequestBody"), logMessage.EventId); + Assert.Equal(LogLevel.Debug, logMessage.LogLevel); + Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", logMessage.Message); + Assert.IsType(logMessage.Exception); + } - var logMessage = Assert.Single(TestSink.Writes); - Assert.Equal(new EventId(2, "InvalidJsonRequestBody"), logMessage.EventId); - Assert.Equal(LogLevel.Debug, logMessage.LogLevel); - Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", logMessage.Message); - Assert.IsType(logMessage.Exception); - } + [Fact] + public async Task RequestDelegateThrowsForMalformedJsonIfThrowOnBadRequest() + { + var invoked = false; - [Fact] - public async Task RequestDelegateThrowsForMalformedJsonIfThrowOnBadRequest() + void TestAction([FromBody] Todo todo) { - var invoked = false; - - void TestAction([FromBody] Todo todo) - { - invoked = true; - } + invoked = true; + } - var httpContext = CreateHttpContext(); - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.Headers["Content-Length"] = "1"; - httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{")); - httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + var httpContext = CreateHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.Headers["Content-Length"] = "1"; + httpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes("{")); + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = true }); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(TestAction, new() { ThrowOnBadRequest = true }); + var requestDelegate = factoryResult.RequestDelegate; - var badHttpRequestException = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); + var badHttpRequestException = await Assert.ThrowsAsync(() => requestDelegate(httpContext)); - Assert.False(invoked); + Assert.False(invoked); - // The httpContext should be untouched. - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.False(httpContext.Response.HasStarted); + // The httpContext should be untouched. + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.Response.HasStarted); - // We don't log bad requests when we throw. - Assert.Empty(TestSink.Writes); + // We don't log bad requests when we throw. + Assert.Empty(TestSink.Writes); - Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", badHttpRequestException.Message); - Assert.Equal(400, badHttpRequestException.StatusCode); - Assert.IsType(badHttpRequestException.InnerException); - } + Assert.Equal(@"Failed to read parameter ""Todo todo"" from the request body as JSON.", badHttpRequestException.Message); + Assert.Equal(400, badHttpRequestException.StatusCode); + Assert.IsType(badHttpRequestException.InnerException); + } - [Fact] - public void BuildRequestDelegateThrowsInvalidOperationExceptionGivenFromBodyOnMultipleParameters() - { - void TestAttributedInvalidAction([FromBody] int value1, [FromBody] int value2) { } - void TestInferredInvalidAction(Todo value1, Todo value2) { } - void TestBothInvalidAction(Todo value1, [FromBody] int value2) { } + [Fact] + public void BuildRequestDelegateThrowsInvalidOperationExceptionGivenFromBodyOnMultipleParameters() + { + void TestAttributedInvalidAction([FromBody] int value1, [FromBody] int value2) { } + void TestInferredInvalidAction(Todo value1, Todo value2) { } + void TestBothInvalidAction(Todo value1, [FromBody] int value2) { } - Assert.Throws(() => RequestDelegateFactory.Create(TestAttributedInvalidAction)); - Assert.Throws(() => RequestDelegateFactory.Create(TestInferredInvalidAction)); - Assert.Throws(() => RequestDelegateFactory.Create(TestBothInvalidAction)); - } + Assert.Throws(() => RequestDelegateFactory.Create(TestAttributedInvalidAction)); + Assert.Throws(() => RequestDelegateFactory.Create(TestInferredInvalidAction)); + Assert.Throws(() => RequestDelegateFactory.Create(TestBothInvalidAction)); + } - [Fact] - public void BuildRequestDelegateThrowsInvalidOperationExceptionForInvalidTryParse() - { - void TestTryParseStruct(BadTryParseStruct value1) { } - void TestTryParseClass(BadTryParseClass value1) { } + [Fact] + public void BuildRequestDelegateThrowsInvalidOperationExceptionForInvalidTryParse() + { + void TestTryParseStruct(BadTryParseStruct value1) { } + void TestTryParseClass(BadTryParseClass value1) { } - Assert.Throws(() => RequestDelegateFactory.Create(TestTryParseStruct)); - Assert.Throws(() => RequestDelegateFactory.Create(TestTryParseClass)); - } + Assert.Throws(() => RequestDelegateFactory.Create(TestTryParseStruct)); + Assert.Throws(() => RequestDelegateFactory.Create(TestTryParseClass)); + } - private struct BadTryParseStruct - { - public static void TryParse(string? value, out BadTryParseStruct result) { } - } + private struct BadTryParseStruct + { + public static void TryParse(string? value, out BadTryParseStruct result) { } + } - private class BadTryParseClass + private class BadTryParseClass + { + public static void TryParse(string? value, out BadTryParseClass result) { - public static void TryParse(string? value, out BadTryParseClass result) - { - result = new(); - } + result = new(); } + } - [Fact] - public void BuildRequestDelegateThrowsInvalidOperationExceptionForInvalidBindAsync() - { - void TestBindAsyncStruct(BadBindAsyncStruct value1) { } - void TestBindAsyncClass(BadBindAsyncClass value1) { } + [Fact] + public void BuildRequestDelegateThrowsInvalidOperationExceptionForInvalidBindAsync() + { + void TestBindAsyncStruct(BadBindAsyncStruct value1) { } + void TestBindAsyncClass(BadBindAsyncClass value1) { } - var ex = Assert.Throws(() => RequestDelegateFactory.Create(TestBindAsyncStruct)); - Assert.Throws(() => RequestDelegateFactory.Create(TestBindAsyncClass)); - } + var ex = Assert.Throws(() => RequestDelegateFactory.Create(TestBindAsyncStruct)); + Assert.Throws(() => RequestDelegateFactory.Create(TestBindAsyncClass)); + } - private struct BadBindAsyncStruct - { - public static Task BindAsync(HttpContext context, ParameterInfo parameter) => - throw new NotImplementedException(); - } + private struct BadBindAsyncStruct + { + public static Task BindAsync(HttpContext context, ParameterInfo parameter) => + throw new NotImplementedException(); + } - private class BadBindAsyncClass - { - public static Task BindAsync(HttpContext context, ParameterInfo parameter) => - throw new NotImplementedException(); - } + private class BadBindAsyncClass + { + public static Task BindAsync(HttpContext context, ParameterInfo parameter) => + throw new NotImplementedException(); + } - public static object[][] ExplicitFromServiceActions + public static object[][] ExplicitFromServiceActions + { + get { - get + void TestExplicitFromService(HttpContext httpContext, [FromService] MyService myService) { - void TestExplicitFromService(HttpContext httpContext, [FromService] MyService myService) - { - httpContext.Items.Add("service", myService); - } + httpContext.Items.Add("service", myService); + } - void TestExplicitFromIEnumerableService(HttpContext httpContext, [FromService] IEnumerable myServices) - { - httpContext.Items.Add("service", myServices.Single()); - } + void TestExplicitFromIEnumerableService(HttpContext httpContext, [FromService] IEnumerable myServices) + { + httpContext.Items.Add("service", myServices.Single()); + } - void TestExplicitMultipleFromService(HttpContext httpContext, [FromService] MyService myService, [FromService] IEnumerable myServices) - { - httpContext.Items.Add("service", myService); - } + void TestExplicitMultipleFromService(HttpContext httpContext, [FromService] MyService myService, [FromService] IEnumerable myServices) + { + httpContext.Items.Add("service", myService); + } - return new object[][] - { + return new object[][] + { new[] { (Action)TestExplicitFromService }, new[] { (Action>)TestExplicitFromIEnumerableService }, new[] { (Action>)TestExplicitMultipleFromService }, - }; - } + }; } + } - public static object[][] ImplicitFromServiceActions + public static object[][] ImplicitFromServiceActions + { + get { - get + void TestImpliedFromService(HttpContext httpContext, IMyService myService) { - void TestImpliedFromService(HttpContext httpContext, IMyService myService) - { - httpContext.Items.Add("service", myService); - } + httpContext.Items.Add("service", myService); + } - void TestImpliedIEnumerableFromService(HttpContext httpContext, IEnumerable myServices) - { - httpContext.Items.Add("service", myServices.Single()); - } + void TestImpliedIEnumerableFromService(HttpContext httpContext, IEnumerable myServices) + { + httpContext.Items.Add("service", myServices.Single()); + } - void TestImpliedFromServiceBasedOnContainer(HttpContext httpContext, MyService myService) - { - httpContext.Items.Add("service", myService); - } + void TestImpliedFromServiceBasedOnContainer(HttpContext httpContext, MyService myService) + { + httpContext.Items.Add("service", myService); + } - return new object[][] - { + return new object[][] + { new[] { (Action)TestImpliedFromService }, new[] { (Action>)TestImpliedIEnumerableFromService }, new[] { (Action)TestImpliedFromServiceBasedOnContainer }, - }; - } + }; } + } - public static object[][] FromServiceActions + public static object[][] FromServiceActions + { + get { - get - { - return ImplicitFromServiceActions.Concat(ExplicitFromServiceActions).ToArray(); - } + return ImplicitFromServiceActions.Concat(ExplicitFromServiceActions).ToArray(); } + } - [Theory] - [MemberData(nameof(ImplicitFromServiceActions))] - public async Task RequestDelegateRequiresServiceForAllImplicitFromServiceParameters(Delegate action) - { - var httpContext = CreateHttpContext(); + [Theory] + [MemberData(nameof(ImplicitFromServiceActions))] + public async Task RequestDelegateRequiresServiceForAllImplicitFromServiceParameters(Delegate action) + { + var httpContext = CreateHttpContext(); - var factoryResult = RequestDelegateFactory.Create(action); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(action); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - var message = Assert.Single(TestSink.Writes).Message; - Assert.StartsWith("Implicit body inferred for parameter", message); - Assert.EndsWith("but no body was provided. Did you mean to use a Service instead?", message); - } + var message = Assert.Single(TestSink.Writes).Message; + Assert.StartsWith("Implicit body inferred for parameter", message); + Assert.EndsWith("but no body was provided. Did you mean to use a Service instead?", message); + } - [Theory] - [MemberData(nameof(ExplicitFromServiceActions))] - public async Task RequestDelegateWithExplicitFromServiceParameters(Delegate action) + [Theory] + [MemberData(nameof(ExplicitFromServiceActions))] + public async Task RequestDelegateWithExplicitFromServiceParameters(Delegate action) + { + // IEnumerable always resolves from DI but is empty and throws from test method + if (action.Method.Name.Contains("TestExplicitFromIEnumerableService", StringComparison.Ordinal)) { - // IEnumerable always resolves from DI but is empty and throws from test method - if (action.Method.Name.Contains("TestExplicitFromIEnumerableService", StringComparison.Ordinal)) - { - return; - } + return; + } - var httpContext = CreateHttpContext(); + var httpContext = CreateHttpContext(); - var requestDelegateResult = RequestDelegateFactory.Create(action); - var ex = await Assert.ThrowsAsync(() => requestDelegateResult.RequestDelegate(httpContext)); - Assert.Equal("No service for type 'Microsoft.AspNetCore.Routing.Internal.RequestDelegateFactoryTests+MyService' has been registered.", ex.Message); - } + var requestDelegateResult = RequestDelegateFactory.Create(action); + var ex = await Assert.ThrowsAsync(() => requestDelegateResult.RequestDelegate(httpContext)); + Assert.Equal("No service for type 'Microsoft.AspNetCore.Routing.Internal.RequestDelegateFactoryTests+MyService' has been registered.", ex.Message); + } - [Theory] - [MemberData(nameof(FromServiceActions))] - public async Task RequestDelegatePopulatesParametersFromServiceWithAndWithoutAttribute(Delegate action) - { - var myOriginalService = new MyService(); + [Theory] + [MemberData(nameof(FromServiceActions))] + public async Task RequestDelegatePopulatesParametersFromServiceWithAndWithoutAttribute(Delegate action) + { + var myOriginalService = new MyService(); - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - serviceCollection.AddSingleton(myOriginalService); - serviceCollection.AddSingleton(myOriginalService); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(LoggerFactory); + serviceCollection.AddSingleton(myOriginalService); + serviceCollection.AddSingleton(myOriginalService); - var services = serviceCollection.BuildServiceProvider(); + var services = serviceCollection.BuildServiceProvider(); - using var requestScoped = services.CreateScope(); + using var requestScoped = services.CreateScope(); - var httpContext = CreateHttpContext(); - httpContext.RequestServices = requestScoped.ServiceProvider; + var httpContext = CreateHttpContext(); + httpContext.RequestServices = requestScoped.ServiceProvider; - var factoryResult = RequestDelegateFactory.Create(action, options: new() { ServiceProvider = services }); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(action, options: new() { ServiceProvider = services }); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - Assert.Same(myOriginalService, httpContext.Items["service"]); - } + Assert.Same(myOriginalService, httpContext.Items["service"]); + } + + [Fact] + public async Task RequestDelegatePopulatesHttpContextParameterWithoutAttribute() + { + HttpContext? httpContextArgument = null; - [Fact] - public async Task RequestDelegatePopulatesHttpContextParameterWithoutAttribute() + void TestAction(HttpContext httpContext) { - HttpContext? httpContextArgument = null; + httpContextArgument = httpContext; + } - void TestAction(HttpContext httpContext) - { - httpContextArgument = httpContext; - } + var httpContext = CreateHttpContext(); - var httpContext = CreateHttpContext(); + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); - await requestDelegate(httpContext); + Assert.Same(httpContext, httpContextArgument); + } - Assert.Same(httpContext, httpContextArgument); - } + [Fact] + public async Task RequestDelegatePassHttpContextRequestAbortedAsCancellationToken() + { + CancellationToken? cancellationTokenArgument = null; - [Fact] - public async Task RequestDelegatePassHttpContextRequestAbortedAsCancellationToken() + void TestAction(CancellationToken cancellationToken) { - CancellationToken? cancellationTokenArgument = null; + cancellationTokenArgument = cancellationToken; + } - void TestAction(CancellationToken cancellationToken) - { - cancellationTokenArgument = cancellationToken; - } + using var cts = new CancellationTokenSource(); + var httpContext = CreateHttpContext(); + // Reset back to default HttpRequestLifetimeFeature that implements a setter for RequestAborted. + httpContext.Features.Set(new HttpRequestLifetimeFeature()); + httpContext.RequestAborted = cts.Token; - using var cts = new CancellationTokenSource(); - var httpContext = CreateHttpContext(); - // Reset back to default HttpRequestLifetimeFeature that implements a setter for RequestAborted. - httpContext.Features.Set(new HttpRequestLifetimeFeature()); - httpContext.RequestAborted = cts.Token; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); - await requestDelegate(httpContext); + Assert.Equal(httpContext.RequestAborted, cancellationTokenArgument); + } - Assert.Equal(httpContext.RequestAborted, cancellationTokenArgument); - } + [Fact] + public async Task RequestDelegatePassHttpContextUserAsClaimsPrincipal() + { + ClaimsPrincipal? userArgument = null; - [Fact] - public async Task RequestDelegatePassHttpContextUserAsClaimsPrincipal() + void TestAction(ClaimsPrincipal user) { - ClaimsPrincipal? userArgument = null; + userArgument = user; + } - void TestAction(ClaimsPrincipal user) - { - userArgument = user; - } + var httpContext = CreateHttpContext(); + httpContext.User = new ClaimsPrincipal(); - var httpContext = CreateHttpContext(); - httpContext.User = new ClaimsPrincipal(); + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); - await requestDelegate(httpContext); + Assert.Equal(httpContext.User, userArgument); + } - Assert.Equal(httpContext.User, userArgument); - } + [Fact] + public async Task RequestDelegatePassHttpContextRequestAsHttpRequest() + { + HttpRequest? httpRequestArgument = null; - [Fact] - public async Task RequestDelegatePassHttpContextRequestAsHttpRequest() + void TestAction(HttpRequest httpRequest) { - HttpRequest? httpRequestArgument = null; + httpRequestArgument = httpRequest; + } - void TestAction(HttpRequest httpRequest) - { - httpRequestArgument = httpRequest; - } + var httpContext = CreateHttpContext(); - var httpContext = CreateHttpContext(); + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); - await requestDelegate(httpContext); + Assert.Equal(httpContext.Request, httpRequestArgument); + } - Assert.Equal(httpContext.Request, httpRequestArgument); - } + [Fact] + public async Task RequestDelegatePassesHttpContextRresponseAsHttpResponse() + { + HttpResponse? httpResponseArgument = null; - [Fact] - public async Task RequestDelegatePassesHttpContextRresponseAsHttpResponse() + void TestAction(HttpResponse httpResponse) { - HttpResponse? httpResponseArgument = null; - - void TestAction(HttpResponse httpResponse) - { - httpResponseArgument = httpResponse; - } + httpResponseArgument = httpResponse; + } - var httpContext = CreateHttpContext(); + var httpContext = CreateHttpContext(); - var factoryResult = RequestDelegateFactory.Create(TestAction); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(TestAction); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - Assert.Equal(httpContext.Response, httpResponseArgument); - } + Assert.Equal(httpContext.Response, httpResponseArgument); + } - public static IEnumerable ComplexResult + public static IEnumerable ComplexResult + { + get { - get + Todo originalTodo = new() { - Todo originalTodo = new() - { - Name = "Write even more tests!" - }; + Name = "Write even more tests!" + }; - Todo TestAction() => originalTodo; - Task TaskTestAction() => Task.FromResult(originalTodo); - ValueTask ValueTaskTestAction() => ValueTask.FromResult(originalTodo); + Todo TestAction() => originalTodo; + Task TaskTestAction() => Task.FromResult(originalTodo); + ValueTask ValueTaskTestAction() => ValueTask.FromResult(originalTodo); - static Todo StaticTestAction() => new Todo { Name = "Write even more tests!" }; - static Task StaticTaskTestAction() => Task.FromResult(new Todo { Name = "Write even more tests!" }); - static ValueTask StaticValueTaskTestAction() => ValueTask.FromResult(new Todo { Name = "Write even more tests!" }); + static Todo StaticTestAction() => new Todo { Name = "Write even more tests!" }; + static Task StaticTaskTestAction() => Task.FromResult(new Todo { Name = "Write even more tests!" }); + static ValueTask StaticValueTaskTestAction() => ValueTask.FromResult(new Todo { Name = "Write even more tests!" }); - return new List + return new List { new object[] { (Func)TestAction }, new object[] { (Func>)TaskTestAction}, @@ -1947,67 +1947,67 @@ namespace Microsoft.AspNetCore.Routing.Internal new object[] { (Func>)StaticTaskTestAction}, new object[] { (Func>)StaticValueTaskTestAction}, }; - } } + } - [Theory] - [MemberData(nameof(ComplexResult))] - public async Task RequestDelegateWritesComplexReturnValueAsJsonResponseBody(Delegate @delegate) - { - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + [Theory] + [MemberData(nameof(ComplexResult))] + public async Task RequestDelegateWritesComplexReturnValueAsJsonResponseBody(Delegate @delegate) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - var factoryResult = RequestDelegateFactory.Create(@delegate); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - var deserializedResponseBody = JsonSerializer.Deserialize(responseBodyStream.ToArray(), new JsonSerializerOptions - { - // TODO: the output is "{\"id\":0,\"name\":\"Write even more tests!\",\"isComplete\":false}" - // Verify that the camelCased property names are consistent with MVC and if so whether we should keep the behavior. - PropertyNameCaseInsensitive = true - }); + var deserializedResponseBody = JsonSerializer.Deserialize(responseBodyStream.ToArray(), new JsonSerializerOptions + { + // TODO: the output is "{\"id\":0,\"name\":\"Write even more tests!\",\"isComplete\":false}" + // Verify that the camelCased property names are consistent with MVC and if so whether we should keep the behavior. + PropertyNameCaseInsensitive = true + }); - Assert.NotNull(deserializedResponseBody); - Assert.Equal("Write even more tests!", deserializedResponseBody!.Name); - } + Assert.NotNull(deserializedResponseBody); + Assert.Equal("Write even more tests!", deserializedResponseBody!.Name); + } - public static IEnumerable CustomResults + public static IEnumerable CustomResults + { + get { - get - { - var resultString = "Still not enough tests!"; + var resultString = "Still not enough tests!"; - CustomResult TestAction() => new CustomResult(resultString); - Task TaskTestAction() => Task.FromResult(new CustomResult(resultString)); - ValueTask ValueTaskTestAction() => ValueTask.FromResult(new CustomResult(resultString)); + CustomResult TestAction() => new CustomResult(resultString); + Task TaskTestAction() => Task.FromResult(new CustomResult(resultString)); + ValueTask ValueTaskTestAction() => ValueTask.FromResult(new CustomResult(resultString)); - static CustomResult StaticTestAction() => new CustomResult("Still not enough tests!"); - static Task StaticTaskTestAction() => Task.FromResult(new CustomResult("Still not enough tests!")); - static ValueTask StaticValueTaskTestAction() => ValueTask.FromResult(new CustomResult("Still not enough tests!")); + static CustomResult StaticTestAction() => new CustomResult("Still not enough tests!"); + static Task StaticTaskTestAction() => Task.FromResult(new CustomResult("Still not enough tests!")); + static ValueTask StaticValueTaskTestAction() => ValueTask.FromResult(new CustomResult("Still not enough tests!")); - // Object return type where the object is IResult - static object StaticResultAsObject() => new CustomResult("Still not enough tests!"); - static object StaticResultAsTaskObject() => Task.FromResult(new CustomResult("Still not enough tests!")); - static object StaticResultAsValueTaskObject() => ValueTask.FromResult(new CustomResult("Still not enough tests!")); + // Object return type where the object is IResult + static object StaticResultAsObject() => new CustomResult("Still not enough tests!"); + static object StaticResultAsTaskObject() => Task.FromResult(new CustomResult("Still not enough tests!")); + static object StaticResultAsValueTaskObject() => ValueTask.FromResult(new CustomResult("Still not enough tests!")); - // Object return type where the object is Task - static object StaticResultAsTaskIResult() => Task.FromResult(new CustomResult("Still not enough tests!")); + // Object return type where the object is Task + static object StaticResultAsTaskIResult() => Task.FromResult(new CustomResult("Still not enough tests!")); - // Object return type where the object is ValueTask - static object StaticResultAsValueTaskIResult() => ValueTask.FromResult(new CustomResult("Still not enough tests!")); + // Object return type where the object is ValueTask + static object StaticResultAsValueTaskIResult() => ValueTask.FromResult(new CustomResult("Still not enough tests!")); - // Task return type - static Task StaticTaskOfIResultAsObject() => Task.FromResult(new CustomResult("Still not enough tests!")); - static ValueTask StaticValueTaskOfIResultAsObject() => ValueTask.FromResult(new CustomResult("Still not enough tests!")); + // Task return type + static Task StaticTaskOfIResultAsObject() => Task.FromResult(new CustomResult("Still not enough tests!")); + static ValueTask StaticValueTaskOfIResultAsObject() => ValueTask.FromResult(new CustomResult("Still not enough tests!")); - StructResult TestStructAction() => new StructResult(resultString); - Task TaskTestStructAction() => Task.FromResult(new StructResult(resultString)); - ValueTask ValueTaskTestStructAction() => ValueTask.FromResult(new StructResult(resultString)); + StructResult TestStructAction() => new StructResult(resultString); + Task TaskTestStructAction() => Task.FromResult(new StructResult(resultString)); + ValueTask ValueTaskTestStructAction() => ValueTask.FromResult(new StructResult(resultString)); - return new List + return new List { new object[] { (Func)TestAction }, new object[] { (Func>)TaskTestAction}, @@ -2030,53 +2030,53 @@ namespace Microsoft.AspNetCore.Routing.Internal new object[] { (Func>)TaskTestStructAction }, new object[] { (Func>)ValueTaskTestStructAction }, }; - } } + } - [Theory] - [MemberData(nameof(CustomResults))] - public async Task RequestDelegateUsesCustomIResult(Delegate @delegate) - { - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + [Theory] + [MemberData(nameof(CustomResults))] + public async Task RequestDelegateUsesCustomIResult(Delegate @delegate) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - var factoryResult = RequestDelegateFactory.Create(@delegate); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal("Still not enough tests!", decodedResponseBody); - } + Assert.Equal("Still not enough tests!", decodedResponseBody); + } - public static IEnumerable StringResult + public static IEnumerable StringResult + { + get { - get - { - var test = "String Test"; + var test = "String Test"; - string TestAction() => test; - Task TaskTestAction() => Task.FromResult(test); - ValueTask ValueTaskTestAction() => ValueTask.FromResult(test); + string TestAction() => test; + Task TaskTestAction() => Task.FromResult(test); + ValueTask ValueTaskTestAction() => ValueTask.FromResult(test); - static string StaticTestAction() => "String Test"; - static Task StaticTaskTestAction() => Task.FromResult("String Test"); - static ValueTask StaticValueTaskTestAction() => ValueTask.FromResult("String Test"); + static string StaticTestAction() => "String Test"; + static Task StaticTaskTestAction() => Task.FromResult("String Test"); + static ValueTask StaticValueTaskTestAction() => ValueTask.FromResult("String Test"); - // Dynamic via object - static object StaticStringAsObjectTestAction() => "String Test"; - static object StaticTaskStringAsObjectTestAction() => Task.FromResult("String Test"); - static object StaticValueTaskStringAsObjectTestAction() => ValueTask.FromResult("String Test"); + // Dynamic via object + static object StaticStringAsObjectTestAction() => "String Test"; + static object StaticTaskStringAsObjectTestAction() => Task.FromResult("String Test"); + static object StaticValueTaskStringAsObjectTestAction() => ValueTask.FromResult("String Test"); - // Dynamic via Task - static Task StaticStringAsTaskObjectTestAction() => Task.FromResult("String Test"); + // Dynamic via Task + static Task StaticStringAsTaskObjectTestAction() => Task.FromResult("String Test"); - // Dynamic via ValueTask - static ValueTask StaticStringAsValueTaskObjectTestAction() => ValueTask.FromResult("String Test"); + // Dynamic via ValueTask + static ValueTask StaticStringAsValueTaskObjectTestAction() => ValueTask.FromResult("String Test"); - return new List + return new List { new object[] { (Func)TestAction }, new object[] { (Func>)TaskTestAction }, @@ -2094,56 +2094,56 @@ namespace Microsoft.AspNetCore.Routing.Internal }; - } } + } - [Theory] - [MemberData(nameof(StringResult))] - public async Task RequestDelegateWritesStringReturnValueAndSetContentTypeWhenNull(Delegate @delegate) - { - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + [Theory] + [MemberData(nameof(StringResult))] + public async Task RequestDelegateWritesStringReturnValueAndSetContentTypeWhenNull(Delegate @delegate) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - var factoryResult = RequestDelegateFactory.Create(@delegate); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - var responseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + var responseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal("String Test", responseBody); - Assert.Equal("text/plain; charset=utf-8", httpContext.Response.ContentType); - } + Assert.Equal("String Test", responseBody); + Assert.Equal("text/plain; charset=utf-8", httpContext.Response.ContentType); + } - [Theory] - [MemberData(nameof(StringResult))] - public async Task RequestDelegateWritesStringReturnDoNotChangeContentType(Delegate @delegate) - { - var httpContext = CreateHttpContext(); - httpContext.Response.ContentType = "application/json; charset=utf-8"; + [Theory] + [MemberData(nameof(StringResult))] + public async Task RequestDelegateWritesStringReturnDoNotChangeContentType(Delegate @delegate) + { + var httpContext = CreateHttpContext(); + httpContext.Response.ContentType = "application/json; charset=utf-8"; - var factoryResult = RequestDelegateFactory.Create(@delegate); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - Assert.Equal("application/json; charset=utf-8", httpContext.Response.ContentType); - } + Assert.Equal("application/json; charset=utf-8", httpContext.Response.ContentType); + } - public static IEnumerable IntResult + public static IEnumerable IntResult + { + get { - get - { - int TestAction() => 42; - Task TaskTestAction() => Task.FromResult(42); - ValueTask ValueTaskTestAction() => ValueTask.FromResult(42); + int TestAction() => 42; + Task TaskTestAction() => Task.FromResult(42); + ValueTask ValueTaskTestAction() => ValueTask.FromResult(42); - static int StaticTestAction() => 42; - static Task StaticTaskTestAction() => Task.FromResult(42); - static ValueTask StaticValueTaskTestAction() => ValueTask.FromResult(42); + static int StaticTestAction() => 42; + static Task StaticTaskTestAction() => Task.FromResult(42); + static ValueTask StaticValueTaskTestAction() => ValueTask.FromResult(42); - return new List + return new List { new object[] { (Func)TestAction }, new object[] { (Func>)TaskTestAction }, @@ -2152,40 +2152,40 @@ namespace Microsoft.AspNetCore.Routing.Internal new object[] { (Func>)StaticTaskTestAction }, new object[] { (Func>)StaticValueTaskTestAction }, }; - } } + } - [Theory] - [MemberData(nameof(IntResult))] - public async Task RequestDelegateWritesIntReturnValue(Delegate @delegate) - { - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + [Theory] + [MemberData(nameof(IntResult))] + public async Task RequestDelegateWritesIntReturnValue(Delegate @delegate) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - var factoryResult = RequestDelegateFactory.Create(@delegate); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - var responseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + var responseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal("42", responseBody); - } + Assert.Equal("42", responseBody); + } - public static IEnumerable BoolResult + public static IEnumerable BoolResult + { + get { - get - { - bool TestAction() => true; - Task TaskTestAction() => Task.FromResult(true); - ValueTask ValueTaskTestAction() => ValueTask.FromResult(true); + bool TestAction() => true; + Task TaskTestAction() => Task.FromResult(true); + ValueTask ValueTaskTestAction() => ValueTask.FromResult(true); - static bool StaticTestAction() => true; - static Task StaticTaskTestAction() => Task.FromResult(true); - static ValueTask StaticValueTaskTestAction() => ValueTask.FromResult(true); + static bool StaticTestAction() => true; + static Task StaticTaskTestAction() => Task.FromResult(true); + static ValueTask StaticValueTaskTestAction() => ValueTask.FromResult(true); - return new List + return new List { new object[] { (Func)TestAction }, new object[] { (Func>)TaskTestAction }, @@ -2194,38 +2194,38 @@ namespace Microsoft.AspNetCore.Routing.Internal new object[] { (Func>)StaticTaskTestAction }, new object[] { (Func>)StaticValueTaskTestAction }, }; - } } + } - [Theory] - [MemberData(nameof(BoolResult))] - public async Task RequestDelegateWritesBoolReturnValue(Delegate @delegate) - { - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + [Theory] + [MemberData(nameof(BoolResult))] + public async Task RequestDelegateWritesBoolReturnValue(Delegate @delegate) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - var factoryResult = RequestDelegateFactory.Create(@delegate); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - var responseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + var responseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal("true", responseBody); - } + Assert.Equal("true", responseBody); + } - public static IEnumerable NullResult + public static IEnumerable NullResult + { + get { - get - { - IResult? TestAction() => null; - Task? TaskBoolAction() => null; - Task? TaskNullAction() => null; - Task TaskTestAction() => Task.FromResult(null); - ValueTask ValueTaskTestAction() => ValueTask.FromResult(null); + IResult? TestAction() => null; + Task? TaskBoolAction() => null; + Task? TaskNullAction() => null; + Task TaskTestAction() => Task.FromResult(null); + ValueTask ValueTaskTestAction() => ValueTask.FromResult(null); - return new List + return new List { new object[] { (Func)TestAction, "The IResult returned by the Delegate must not be null." }, new object[] { (Func?>)TaskNullAction, "The IResult in Task response must not be null." }, @@ -2233,41 +2233,41 @@ namespace Microsoft.AspNetCore.Routing.Internal new object[] { (Func>)TaskTestAction, "The IResult returned by the Delegate must not be null." }, new object[] { (Func>)ValueTaskTestAction, "The IResult returned by the Delegate must not be null." }, }; - } } + } - [Theory] - [MemberData(nameof(NullResult))] - public async Task RequestDelegateThrowsInvalidOperationExceptionOnNullDelegate(Delegate @delegate, string message) - { - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + [Theory] + [MemberData(nameof(NullResult))] + public async Task RequestDelegateThrowsInvalidOperationExceptionOnNullDelegate(Delegate @delegate, string message) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - var factoryResult = RequestDelegateFactory.Create(@delegate); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; - var exception = await Assert.ThrowsAnyAsync(async () => await requestDelegate(httpContext)); - Assert.Contains(message, exception.Message); - } + var exception = await Assert.ThrowsAnyAsync(async () => await requestDelegate(httpContext)); + Assert.Contains(message, exception.Message); + } - public static IEnumerable NullContentResult + public static IEnumerable NullContentResult + { + get { - get - { - bool? TestBoolAction() => null; - Task TaskTestBoolAction() => Task.FromResult(null); - ValueTask ValueTaskTestBoolAction() => ValueTask.FromResult(null); + bool? TestBoolAction() => null; + Task TaskTestBoolAction() => Task.FromResult(null); + ValueTask ValueTaskTestBoolAction() => ValueTask.FromResult(null); - int? TestIntAction() => null; - Task TaskTestIntAction() => Task.FromResult(null); - ValueTask ValueTaskTestIntAction() => ValueTask.FromResult(null); + int? TestIntAction() => null; + Task TaskTestIntAction() => Task.FromResult(null); + ValueTask ValueTaskTestIntAction() => ValueTask.FromResult(null); - Todo? TestTodoAction() => null; - Task TaskTestTodoAction() => Task.FromResult(null); - ValueTask ValueTaskTestTodoAction() => ValueTask.FromResult(null); + Todo? TestTodoAction() => null; + Task TaskTestTodoAction() => Task.FromResult(null); + ValueTask ValueTaskTestTodoAction() => ValueTask.FromResult(null); - return new List + return new List { new object[] { (Func)TestBoolAction }, new object[] { (Func>)TaskTestBoolAction }, @@ -2279,39 +2279,39 @@ namespace Microsoft.AspNetCore.Routing.Internal new object[] { (Func>)TaskTestTodoAction }, new object[] { (Func>)ValueTaskTestTodoAction }, }; - } } + } - [Theory] - [MemberData(nameof(NullContentResult))] - public async Task RequestDelegateWritesNullReturnNullValue(Delegate @delegate) - { - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + [Theory] + [MemberData(nameof(NullContentResult))] + public async Task RequestDelegateWritesNullReturnNullValue(Delegate @delegate) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - var factoryResult = RequestDelegateFactory.Create(@delegate); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - var responseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + var responseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal("null", responseBody); - } + Assert.Equal("null", responseBody); + } - public static IEnumerable QueryParamOptionalityData + public static IEnumerable QueryParamOptionalityData + { + get { - get - { - string requiredQueryParam(string name) => $"Hello {name}!"; - string defaultValueQueryParam(string name = "DefaultName") => $"Hello {name}!"; - string nullableQueryParam(string? name) => $"Hello {name}!"; - string requiredParseableQueryParam(int age) => $"Age: {age}"; - string defaultValueParseableQueryParam(int age = 12) => $"Age: {age}"; - string nullableQueryParseableParam(int? age) => $"Age: {age}"; + string requiredQueryParam(string name) => $"Hello {name}!"; + string defaultValueQueryParam(string name = "DefaultName") => $"Hello {name}!"; + string nullableQueryParam(string? name) => $"Hello {name}!"; + string requiredParseableQueryParam(int age) => $"Age: {age}"; + string defaultValueParseableQueryParam(int age = 12) => $"Age: {age}"; + string nullableQueryParseableParam(int? age) => $"Age: {age}"; - return new List + return new List { new object?[] { (Func)requiredQueryParam, "name", null, true, null}, new object?[] { (Func)requiredQueryParam, "name", "TestName", false, "Hello TestName!" }, @@ -2327,62 +2327,62 @@ namespace Microsoft.AspNetCore.Routing.Internal new object?[] { (Func)nullableQueryParseableParam, "age", null, false, "Age: " }, new object?[] { (Func)nullableQueryParseableParam, "age", "42", false, "Age: 42"}, }; - } } + } - [Theory] - [MemberData(nameof(QueryParamOptionalityData))] - public async Task RequestDelegateHandlesQueryParamOptionality(Delegate @delegate, string paramName, string? queryParam, bool isInvalid, string? expectedResponse) - { - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + [Theory] + [MemberData(nameof(QueryParamOptionalityData))] + public async Task RequestDelegateHandlesQueryParamOptionality(Delegate @delegate, string paramName, string? queryParam, bool isInvalid, string? expectedResponse) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - if (queryParam is not null) + if (queryParam is not null) + { + httpContext.Request.Query = new QueryCollection(new Dictionary { - httpContext.Request.Query = new QueryCollection(new Dictionary - { - [paramName] = queryParam - }); - } + [paramName] = queryParam + }); + } - var factoryResult = RequestDelegateFactory.Create(@delegate); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - var logs = TestSink.Writes.ToArray(); + var logs = TestSink.Writes.ToArray(); - if (isInvalid) - { - Assert.Equal(400, httpContext.Response.StatusCode); - var log = Assert.Single(logs); - Assert.Equal(LogLevel.Debug, log.LogLevel); - Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId); - var expectedType = paramName == "age" ? "int age" : "string name"; - Assert.Equal($@"Required parameter ""{expectedType}"" was not provided from route or query string.", log.Message); - } - else - { - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal(expectedResponse, decodedResponseBody); - } + if (isInvalid) + { + Assert.Equal(400, httpContext.Response.StatusCode); + var log = Assert.Single(logs); + Assert.Equal(LogLevel.Debug, log.LogLevel); + Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId); + var expectedType = paramName == "age" ? "int age" : "string name"; + Assert.Equal($@"Required parameter ""{expectedType}"" was not provided from route or query string.", log.Message); } - - public static IEnumerable RouteParamOptionalityData + else { - get - { - string requiredRouteParam(string name) => $"Hello {name}!"; - string defaultValueRouteParam(string name = "DefaultName") => $"Hello {name}!"; - string nullableRouteParam(string? name) => $"Hello {name}!"; - string requiredParseableRouteParam(int age) => $"Age: {age}"; - string defaultValueParseableRouteParam(int age = 12) => $"Age: {age}"; - string nullableParseableRouteParam(int? age) => $"Age: {age}"; + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal(expectedResponse, decodedResponseBody); + } + } + + public static IEnumerable RouteParamOptionalityData + { + get + { + string requiredRouteParam(string name) => $"Hello {name}!"; + string defaultValueRouteParam(string name = "DefaultName") => $"Hello {name}!"; + string nullableRouteParam(string? name) => $"Hello {name}!"; + string requiredParseableRouteParam(int age) => $"Age: {age}"; + string defaultValueParseableRouteParam(int age = 12) => $"Age: {age}"; + string nullableParseableRouteParam(int? age) => $"Age: {age}"; - return new List + return new List { new object?[] { (Func)requiredRouteParam, "name", null, true, null}, new object?[] { (Func)requiredRouteParam, "name", "TestName", false, "Hello TestName!" }, @@ -2398,60 +2398,60 @@ namespace Microsoft.AspNetCore.Routing.Internal new object?[] { (Func)nullableParseableRouteParam, "age", null, false, "Age: " }, new object?[] { (Func)nullableParseableRouteParam, "age", "42", false, "Age: 42"}, }; - } } + } - [Theory] - [MemberData(nameof(RouteParamOptionalityData))] - public async Task RequestDelegateHandlesRouteParamOptionality(Delegate @delegate, string paramName, string? routeParam, bool isInvalid, string? expectedResponse) - { - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + [Theory] + [MemberData(nameof(RouteParamOptionalityData))] + public async Task RequestDelegateHandlesRouteParamOptionality(Delegate @delegate, string paramName, string? routeParam, bool isInvalid, string? expectedResponse) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - if (routeParam is not null) - { - httpContext.Request.RouteValues[paramName] = routeParam; - } + if (routeParam is not null) + { + httpContext.Request.RouteValues[paramName] = routeParam; + } - var factoryResult = RequestDelegateFactory.Create(@delegate, new() - { - RouteParameterNames = routeParam is not null ? new[] { paramName } : Array.Empty() - }); + var factoryResult = RequestDelegateFactory.Create(@delegate, new() + { + RouteParameterNames = routeParam is not null ? new[] { paramName } : Array.Empty() + }); - var requestDelegate = factoryResult.RequestDelegate; + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - var logs = TestSink.Writes.ToArray(); + var logs = TestSink.Writes.ToArray(); - if (isInvalid) - { - Assert.Equal(400, httpContext.Response.StatusCode); - var log = Assert.Single(logs); - Assert.Equal(LogLevel.Debug, log.LogLevel); - Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId); - var expectedType = paramName == "age" ? "int age" : "string name"; - Assert.Equal($@"Required parameter ""{expectedType}"" was not provided from query string.", log.Message); - } - else - { - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal(expectedResponse, decodedResponseBody); - } + if (isInvalid) + { + Assert.Equal(400, httpContext.Response.StatusCode); + var log = Assert.Single(logs); + Assert.Equal(LogLevel.Debug, log.LogLevel); + Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId); + var expectedType = paramName == "age" ? "int age" : "string name"; + Assert.Equal($@"Required parameter ""{expectedType}"" was not provided from query string.", log.Message); + } + else + { + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal(expectedResponse, decodedResponseBody); } + } - public static IEnumerable BodyParamOptionalityData + public static IEnumerable BodyParamOptionalityData + { + get { - get - { - string requiredBodyParam(Todo todo) => $"Todo: {todo.Name}"; - string defaultValueBodyParam(Todo? todo = null) => $"Todo: {todo?.Name}"; - string nullableBodyParam(Todo? todo) => $"Todo: {todo?.Name}"; + string requiredBodyParam(Todo todo) => $"Todo: {todo.Name}"; + string defaultValueBodyParam(Todo? todo = null) => $"Todo: {todo?.Name}"; + string nullableBodyParam(Todo? todo) => $"Todo: {todo?.Name}"; - return new List + return new List { new object?[] { (Func)requiredBodyParam, false, true, null }, new object?[] { (Func)requiredBodyParam, true, false, "Todo: Default Todo"}, @@ -2460,101 +2460,101 @@ namespace Microsoft.AspNetCore.Routing.Internal new object?[] { (Func)nullableBodyParam, false, false, "Todo: " }, new object?[] { (Func)nullableBodyParam, true, false, "Todo: Default Todo" }, }; - } } + } + + [Theory] + [MemberData(nameof(BodyParamOptionalityData))] + public async Task RequestDelegateHandlesBodyParamOptionality(Delegate @delegate, bool hasBody, bool isInvalid, string? expectedResponse) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - [Theory] - [MemberData(nameof(BodyParamOptionalityData))] - public async Task RequestDelegateHandlesBodyParamOptionality(Delegate @delegate, bool hasBody, bool isInvalid, string? expectedResponse) + if (hasBody) { - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + var todo = new Todo() { Name = "Default Todo" }; + var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(todo); + var stream = new MemoryStream(requestBodyBytes); + httpContext.Request.Body = stream; + httpContext.Request.Headers["Content-Type"] = "application/json"; + httpContext.Request.ContentLength = stream.Length; + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + } - if (hasBody) - { - var todo = new Todo() { Name = "Default Todo" }; - var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(todo); - var stream = new MemoryStream(requestBodyBytes); - httpContext.Request.Body = stream; - httpContext.Request.Headers["Content-Type"] = "application/json"; - httpContext.Request.ContentLength = stream.Length; - httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - } + var jsonOptions = new JsonOptions(); + jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter()); - var jsonOptions = new JsonOptions(); - jsonOptions.SerializerOptions.Converters.Add(new TodoJsonConverter()); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(LoggerFactory); + serviceCollection.AddSingleton(Options.Create(jsonOptions)); + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - serviceCollection.AddSingleton(Options.Create(jsonOptions)); - httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; - var factoryResult = RequestDelegateFactory.Create(@delegate); - var requestDelegate = factoryResult.RequestDelegate; + var request = requestDelegate(httpContext); - var request = requestDelegate(httpContext); + if (isInvalid) + { + var logs = TestSink.Writes.ToArray(); + Assert.Equal(400, httpContext.Response.StatusCode); + var log = Assert.Single(logs); + Assert.Equal(LogLevel.Debug, log.LogLevel); + Assert.Equal(new EventId(5, "ImplicitBodyNotProvided"), log.EventId); + Assert.Equal(@"Implicit body inferred for parameter ""todo"" but no body was provided. Did you mean to use a Service instead?", log.Message); + } + else + { + await request; + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal(expectedResponse, decodedResponseBody); + } + } - if (isInvalid) + public static IEnumerable BindAsyncParamOptionalityData + { + get + { + void requiredReferenceType(HttpContext context, MyBindAsyncRecord myBindAsyncRecord) { - var logs = TestSink.Writes.ToArray(); - Assert.Equal(400, httpContext.Response.StatusCode); - var log = Assert.Single(logs); - Assert.Equal(LogLevel.Debug, log.LogLevel); - Assert.Equal(new EventId(5, "ImplicitBodyNotProvided"), log.EventId); - Assert.Equal(@"Implicit body inferred for parameter ""todo"" but no body was provided. Did you mean to use a Service instead?", log.Message); + context.Items["uri"] = myBindAsyncRecord.Uri; } - else + void defaultReferenceType(HttpContext context, MyBindAsyncRecord? myBindAsyncRecord = null) { - await request; - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal(expectedResponse, decodedResponseBody); + context.Items["uri"] = myBindAsyncRecord?.Uri; } - } - - public static IEnumerable BindAsyncParamOptionalityData - { - get + void nullableReferenceType(HttpContext context, MyBindAsyncRecord? myBindAsyncRecord) { - void requiredReferenceType(HttpContext context, MyBindAsyncRecord myBindAsyncRecord) - { - context.Items["uri"] = myBindAsyncRecord.Uri; - } - void defaultReferenceType(HttpContext context, MyBindAsyncRecord? myBindAsyncRecord = null) - { - context.Items["uri"] = myBindAsyncRecord?.Uri; - } - void nullableReferenceType(HttpContext context, MyBindAsyncRecord? myBindAsyncRecord) - { - context.Items["uri"] = myBindAsyncRecord?.Uri; - } - void requiredReferenceTypeSimple(HttpContext context, MySimpleBindAsyncRecord mySimpleBindAsyncRecord) - { - context.Items["uri"] = mySimpleBindAsyncRecord.Uri; - } + context.Items["uri"] = myBindAsyncRecord?.Uri; + } + void requiredReferenceTypeSimple(HttpContext context, MySimpleBindAsyncRecord mySimpleBindAsyncRecord) + { + context.Items["uri"] = mySimpleBindAsyncRecord.Uri; + } - void requiredValueType(HttpContext context, MyNullableBindAsyncStruct myNullableBindAsyncStruct) - { - context.Items["uri"] = myNullableBindAsyncStruct.Uri; - } - void defaultValueType(HttpContext context, MyNullableBindAsyncStruct? myNullableBindAsyncStruct = null) - { - context.Items["uri"] = myNullableBindAsyncStruct?.Uri; - } - void nullableValueType(HttpContext context, MyNullableBindAsyncStruct? myNullableBindAsyncStruct) - { - context.Items["uri"] = myNullableBindAsyncStruct?.Uri; - } - void requiredValueTypeSimple(HttpContext context, MySimpleBindAsyncStruct mySimpleBindAsyncStruct) - { - context.Items["uri"] = mySimpleBindAsyncStruct.Uri; - } + void requiredValueType(HttpContext context, MyNullableBindAsyncStruct myNullableBindAsyncStruct) + { + context.Items["uri"] = myNullableBindAsyncStruct.Uri; + } + void defaultValueType(HttpContext context, MyNullableBindAsyncStruct? myNullableBindAsyncStruct = null) + { + context.Items["uri"] = myNullableBindAsyncStruct?.Uri; + } + void nullableValueType(HttpContext context, MyNullableBindAsyncStruct? myNullableBindAsyncStruct) + { + context.Items["uri"] = myNullableBindAsyncStruct?.Uri; + } + void requiredValueTypeSimple(HttpContext context, MySimpleBindAsyncStruct mySimpleBindAsyncStruct) + { + context.Items["uri"] = mySimpleBindAsyncStruct.Uri; + } - return new object?[][] - { + return new object?[][] + { new object?[] { (Action)requiredReferenceType, false, true, false }, new object?[] { (Action)requiredReferenceType, true, false, false, }, new object?[] { (Action)requiredReferenceTypeSimple, true, false, false }, @@ -2574,68 +2574,68 @@ namespace Microsoft.AspNetCore.Routing.Internal new object?[] { (Action)nullableValueType, false, false, true }, new object?[] { (Action)nullableValueType, true, false, true }, - }; - } + }; } + } + + [Theory] + [MemberData(nameof(BindAsyncParamOptionalityData))] + public async Task RequestDelegateHandlesBindAsyncOptionality(Delegate routeHandler, bool includeReferer, bool isInvalid, bool isStruct) + { + var httpContext = CreateHttpContext(); - [Theory] - [MemberData(nameof(BindAsyncParamOptionalityData))] - public async Task RequestDelegateHandlesBindAsyncOptionality(Delegate routeHandler, bool includeReferer, bool isInvalid, bool isStruct) + if (includeReferer) { - var httpContext = CreateHttpContext(); + httpContext.Request.Headers.Referer = "https://example.org"; + } - if (includeReferer) - { - httpContext.Request.Headers.Referer = "https://example.org"; - } + var factoryResult = RequestDelegateFactory.Create(routeHandler); - var factoryResult = RequestDelegateFactory.Create(routeHandler); + var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); + if (isInvalid) + { + Assert.Equal(400, httpContext.Response.StatusCode); + var log = Assert.Single(TestSink.Writes); + Assert.Equal(LogLevel.Debug, log.LogLevel); + Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId); - if (isInvalid) + if (isStruct) { - Assert.Equal(400, httpContext.Response.StatusCode); - var log = Assert.Single(TestSink.Writes); - Assert.Equal(LogLevel.Debug, log.LogLevel); - Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId); - - if (isStruct) - { - Assert.Equal(@"Required parameter ""MyNullableBindAsyncStruct myNullableBindAsyncStruct"" was not provided from MyNullableBindAsyncStruct.BindAsync(HttpContext, ParameterInfo).", log.Message); - } - else - { - Assert.Equal(@"Required parameter ""MyBindAsyncRecord myBindAsyncRecord"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", log.Message); - } + Assert.Equal(@"Required parameter ""MyNullableBindAsyncStruct myNullableBindAsyncStruct"" was not provided from MyNullableBindAsyncStruct.BindAsync(HttpContext, ParameterInfo).", log.Message); } else { - Assert.Equal(200, httpContext.Response.StatusCode); + Assert.Equal(@"Required parameter ""MyBindAsyncRecord myBindAsyncRecord"" was not provided from MyBindAsyncRecord.BindAsync(HttpContext, ParameterInfo).", log.Message); + } + } + else + { + Assert.Equal(200, httpContext.Response.StatusCode); - if (includeReferer) - { - Assert.Equal(new Uri("https://example.org"), httpContext.Items["uri"]); - } - else - { - Assert.Null(httpContext.Items["uri"]); - } + if (includeReferer) + { + Assert.Equal(new Uri("https://example.org"), httpContext.Items["uri"]); + } + else + { + Assert.Null(httpContext.Items["uri"]); } } + } - public static IEnumerable ServiceParamOptionalityData + public static IEnumerable ServiceParamOptionalityData + { + get { - get - { - string requiredExplicitService([FromService] MyService service) => $"Service: {service}"; - string defaultValueExplicitServiceParam([FromService] MyService? service = null) => $"Service: {service}"; - string nullableExplicitServiceParam([FromService] MyService? service) => $"Service: {service}"; + string requiredExplicitService([FromService] MyService service) => $"Service: {service}"; + string defaultValueExplicitServiceParam([FromService] MyService? service = null) => $"Service: {service}"; + string nullableExplicitServiceParam([FromService] MyService? service) => $"Service: {service}"; - return new List + return new List { new object?[] { (Func)requiredExplicitService, false, true}, new object?[] { (Func)requiredExplicitService, true, false}, @@ -2646,303 +2646,303 @@ namespace Microsoft.AspNetCore.Routing.Internal new object?[] { (Func)nullableExplicitServiceParam, false, false}, new object?[] { (Func)nullableExplicitServiceParam, true, false}, }; - } } + } - [Theory] - [MemberData(nameof(ServiceParamOptionalityData))] - public async Task RequestDelegateHandlesServiceParamOptionality(Delegate @delegate, bool hasService, bool isInvalid) - { - var httpContext = CreateHttpContext(); + [Theory] + [MemberData(nameof(ServiceParamOptionalityData))] + public async Task RequestDelegateHandlesServiceParamOptionality(Delegate @delegate, bool hasService, bool isInvalid) + { + var httpContext = CreateHttpContext(); - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - if (hasService) - { - var service = new MyService(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(LoggerFactory); + if (hasService) + { + var service = new MyService(); - serviceCollection.AddSingleton(service); - } - var services = serviceCollection.BuildServiceProvider(); - httpContext.RequestServices = services; - RequestDelegateFactoryOptions options = new() { ServiceProvider = services }; + serviceCollection.AddSingleton(service); + } + var services = serviceCollection.BuildServiceProvider(); + httpContext.RequestServices = services; + RequestDelegateFactoryOptions options = new() { ServiceProvider = services }; - var factoryResult = RequestDelegateFactory.Create(@delegate, options); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(@delegate, options); + var requestDelegate = factoryResult.RequestDelegate; - if (!isInvalid) - { - await requestDelegate(httpContext); - Assert.Equal(200, httpContext.Response.StatusCode); - } - else - { - await Assert.ThrowsAsync(() => requestDelegate(httpContext)); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - } + if (!isInvalid) + { + await requestDelegate(httpContext); + Assert.Equal(200, httpContext.Response.StatusCode); + } + else + { + await Assert.ThrowsAsync(() => requestDelegate(httpContext)); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); } + } - public static IEnumerable AllowEmptyData + public static IEnumerable AllowEmptyData + { + get { - get - { - string disallowEmptyAndNonOptional([FromBody(AllowEmpty = false)] Todo todo) => $"{todo}"; - string allowEmptyAndNonOptional([FromBody(AllowEmpty = true)] Todo todo) => $"{todo}"; - string allowEmptyAndOptional([FromBody(AllowEmpty = true)] Todo? todo = null) => $"{todo}"; - string disallowEmptyAndOptional([FromBody(AllowEmpty = false)] Todo? todo = null) => $"{todo}"; + string disallowEmptyAndNonOptional([FromBody(AllowEmpty = false)] Todo todo) => $"{todo}"; + string allowEmptyAndNonOptional([FromBody(AllowEmpty = true)] Todo todo) => $"{todo}"; + string allowEmptyAndOptional([FromBody(AllowEmpty = true)] Todo? todo = null) => $"{todo}"; + string disallowEmptyAndOptional([FromBody(AllowEmpty = false)] Todo? todo = null) => $"{todo}"; - return new List + return new List { new object?[] { (Func)disallowEmptyAndNonOptional, false }, new object?[] { (Func)allowEmptyAndNonOptional, true }, new object?[] { (Func)allowEmptyAndOptional, true }, new object?[] { (Func)disallowEmptyAndOptional, true } }; - } } + } - [Theory] - [MemberData(nameof(AllowEmptyData))] - public async Task AllowEmptyOverridesOptionality(Delegate @delegate, bool allowsEmptyRequest) - { - var httpContext = CreateHttpContext(); + [Theory] + [MemberData(nameof(AllowEmptyData))] + public async Task AllowEmptyOverridesOptionality(Delegate @delegate, bool allowsEmptyRequest) + { + var httpContext = CreateHttpContext(); - var factoryResult = RequestDelegateFactory.Create(@delegate); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - var logs = TestSink.Writes.ToArray(); + var logs = TestSink.Writes.ToArray(); - if (!allowsEmptyRequest) - { - Assert.Equal(400, httpContext.Response.StatusCode); - var log = Assert.Single(logs); - Assert.Equal(LogLevel.Debug, log.LogLevel); - Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId); - Assert.Equal(@"Required parameter ""Todo todo"" was not provided from body.", log.Message); - } - else - { - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - } + if (!allowsEmptyRequest) + { + Assert.Equal(400, httpContext.Response.StatusCode); + var log = Assert.Single(logs); + Assert.Equal(LogLevel.Debug, log.LogLevel); + Assert.Equal(new EventId(4, "RequiredParameterNotProvided"), log.EventId); + Assert.Equal(@"Required parameter ""Todo todo"" was not provided from body.", log.Message); + } + else + { + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); } + } #nullable disable - [Theory] - [InlineData(true, "Hello TestName!")] - [InlineData(false, "Hello !")] - public async Task CanSetStringParamAsOptionalWithNullabilityDisability(bool provideValue, string expectedResponse) - { - string optionalQueryParam(string name = null) => $"Hello {name}!"; + [Theory] + [InlineData(true, "Hello TestName!")] + [InlineData(false, "Hello !")] + public async Task CanSetStringParamAsOptionalWithNullabilityDisability(bool provideValue, string expectedResponse) + { + string optionalQueryParam(string name = null) => $"Hello {name}!"; - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - if (provideValue) + if (provideValue) + { + httpContext.Request.Query = new QueryCollection(new Dictionary { - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["name"] = "TestName" - }); - } + ["name"] = "TestName" + }); + } - var factoryResult = RequestDelegateFactory.Create(optionalQueryParam); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(optionalQueryParam); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal(expectedResponse, decodedResponseBody); - } + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal(expectedResponse, decodedResponseBody); + } - [Theory] - [InlineData(true, "Age: 42")] - [InlineData(false, "Age: 0")] - public async Task CanSetParseableStringParamAsOptionalWithNullabilityDisability(bool provideValue, string expectedResponse) - { - string optionalQueryParam(int age = default(int)) => $"Age: {age}"; + [Theory] + [InlineData(true, "Age: 42")] + [InlineData(false, "Age: 0")] + public async Task CanSetParseableStringParamAsOptionalWithNullabilityDisability(bool provideValue, string expectedResponse) + { + string optionalQueryParam(int age = default(int)) => $"Age: {age}"; - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - if (provideValue) + if (provideValue) + { + httpContext.Request.Query = new QueryCollection(new Dictionary { - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["age"] = "42" - }); - } + ["age"] = "42" + }); + } - var factoryResult = RequestDelegateFactory.Create(optionalQueryParam); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(optionalQueryParam); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal(expectedResponse, decodedResponseBody); - } + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal(expectedResponse, decodedResponseBody); + } - [Theory] - [InlineData(true, "Age: 42")] - [InlineData(false, "Age: ")] - public async Task TreatsUnknownNullabilityAsOptionalForReferenceType(bool provideValue, string expectedResponse) - { - string optionalQueryParam(string age) => $"Age: {age}"; + [Theory] + [InlineData(true, "Age: 42")] + [InlineData(false, "Age: ")] + public async Task TreatsUnknownNullabilityAsOptionalForReferenceType(bool provideValue, string expectedResponse) + { + string optionalQueryParam(string age) => $"Age: {age}"; - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - if (provideValue) + if (provideValue) + { + httpContext.Request.Query = new QueryCollection(new Dictionary { - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["age"] = "42" - }); - } + ["age"] = "42" + }); + } - var factoryResult = RequestDelegateFactory.Create(optionalQueryParam); + var factoryResult = RequestDelegateFactory.Create(optionalQueryParam); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal(expectedResponse, decodedResponseBody); - } + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal(expectedResponse, decodedResponseBody); + } #nullable enable - [Fact] - public async Task CanExecuteRequestDelegateWithResultsExtension() - { - IResult actionWithExtensionsResult(string name) => Results.Extensions.TestResult(name); + [Fact] + public async Task CanExecuteRequestDelegateWithResultsExtension() + { + IResult actionWithExtensionsResult(string name) => Results.Extensions.TestResult(name); - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["name"] = "Tester" - }); + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["name"] = "Tester" + }); - var factoryResult = RequestDelegateFactory.Create(actionWithExtensionsResult); + var factoryResult = RequestDelegateFactory.Create(actionWithExtensionsResult); - var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal(@"""Hello Tester. This is from an extension method.""", decodedResponseBody); - } + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal(@"""Hello Tester. This is from an extension method.""", decodedResponseBody); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task RequestDelegateRejectsNonJsonContent(bool shouldThrow) - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Headers["Content-Type"] = "application/xml"; - httpContext.Request.Headers["Content-Length"] = "1"; - httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task RequestDelegateRejectsNonJsonContent(bool shouldThrow) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Headers["Content-Type"] = "application/xml"; + httpContext.Request.Headers["Content-Length"] = "1"; + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(LoggerFactory); + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var factoryResult = RequestDelegateFactory.Create((HttpContext context, Todo todo) => - { - }, new RequestDelegateFactoryOptions() { ThrowOnBadRequest = shouldThrow }); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create((HttpContext context, Todo todo) => + { + }, new RequestDelegateFactoryOptions() { ThrowOnBadRequest = shouldThrow }); + var requestDelegate = factoryResult.RequestDelegate; - var request = requestDelegate(httpContext); + var request = requestDelegate(httpContext); - if (shouldThrow) - { - var ex = await Assert.ThrowsAsync(() => request); - Assert.Equal("Expected a supported JSON media type but got \"application/xml\".", ex.Message); - Assert.Equal(StatusCodes.Status415UnsupportedMediaType, ex.StatusCode); - } - else - { - await request; + if (shouldThrow) + { + var ex = await Assert.ThrowsAsync(() => request); + Assert.Equal("Expected a supported JSON media type but got \"application/xml\".", ex.Message); + Assert.Equal(StatusCodes.Status415UnsupportedMediaType, ex.StatusCode); + } + else + { + await request; - Assert.Equal(415, httpContext.Response.StatusCode); - var logMessage = Assert.Single(TestSink.Writes); - Assert.Equal(new EventId(6, "UnexpectedContentType"), logMessage.EventId); - Assert.Equal(LogLevel.Debug, logMessage.LogLevel); - Assert.Equal("Expected a supported JSON media type but got \"application/xml\".", logMessage.Message); - } + Assert.Equal(415, httpContext.Response.StatusCode); + var logMessage = Assert.Single(TestSink.Writes); + Assert.Equal(new EventId(6, "UnexpectedContentType"), logMessage.EventId); + Assert.Equal(LogLevel.Debug, logMessage.LogLevel); + Assert.Equal("Expected a supported JSON media type but got \"application/xml\".", logMessage.Message); } + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task RequestDelegateWithBindAndImplicitBodyRejectsNonJsonContent(bool shouldThrow) + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task RequestDelegateWithBindAndImplicitBodyRejectsNonJsonContent(bool shouldThrow) + { + Todo originalTodo = new() { - Todo originalTodo = new() - { - Name = "Write more tests!" - }; + Name = "Write more tests!" + }; - var httpContext = new DefaultHttpContext(); + var httpContext = new DefaultHttpContext(); - var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); - var stream = new MemoryStream(requestBodyBytes); - httpContext.Request.Body = stream; - httpContext.Request.Headers["Content-Type"] = "application/xml"; - httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); - httpContext.Features.Set(new RequestBodyDetectionFeature(true)); + var requestBodyBytes = JsonSerializer.SerializeToUtf8Bytes(originalTodo); + var stream = new MemoryStream(requestBodyBytes); + httpContext.Request.Body = stream; + httpContext.Request.Headers["Content-Type"] = "application/xml"; + httpContext.Request.Headers["Content-Length"] = stream.Length.ToString(CultureInfo.InvariantCulture); + httpContext.Features.Set(new RequestBodyDetectionFeature(true)); - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(LoggerFactory); - httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(LoggerFactory); + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); - var factoryResult = RequestDelegateFactory.Create((HttpContext context, JsonTodo customTodo, Todo todo) => - { - }, new RequestDelegateFactoryOptions() { ThrowOnBadRequest = shouldThrow }); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create((HttpContext context, JsonTodo customTodo, Todo todo) => + { + }, new RequestDelegateFactoryOptions() { ThrowOnBadRequest = shouldThrow }); + var requestDelegate = factoryResult.RequestDelegate; - var request = requestDelegate(httpContext); + var request = requestDelegate(httpContext); - if (shouldThrow) - { - var ex = await Assert.ThrowsAsync(() => request); - Assert.Equal("Expected a supported JSON media type but got \"application/xml\".", ex.Message); - Assert.Equal(StatusCodes.Status415UnsupportedMediaType, ex.StatusCode); - } - else - { - await request; + if (shouldThrow) + { + var ex = await Assert.ThrowsAsync(() => request); + Assert.Equal("Expected a supported JSON media type but got \"application/xml\".", ex.Message); + Assert.Equal(StatusCodes.Status415UnsupportedMediaType, ex.StatusCode); + } + else + { + await request; - Assert.Equal(415, httpContext.Response.StatusCode); - var logMessage = Assert.Single(TestSink.Writes); - Assert.Equal(new EventId(6, "UnexpectedContentType"), logMessage.EventId); - Assert.Equal(LogLevel.Debug, logMessage.LogLevel); - Assert.Equal("Expected a supported JSON media type but got \"application/xml\".", logMessage.Message); - } + Assert.Equal(415, httpContext.Response.StatusCode); + var logMessage = Assert.Single(TestSink.Writes); + Assert.Equal(new EventId(6, "UnexpectedContentType"), logMessage.EventId); + Assert.Equal(LogLevel.Debug, logMessage.LogLevel); + Assert.Equal("Expected a supported JSON media type but got \"application/xml\".", logMessage.Message); } + } - public static IEnumerable DateTimeDelegates + public static IEnumerable DateTimeDelegates + { + get { - get - { - string dateTimeParsing(DateTime time) => $"Time: {time.ToString("O", CultureInfo.InvariantCulture)}, Kind: {time.Kind}"; + string dateTimeParsing(DateTime time) => $"Time: {time.ToString("O", CultureInfo.InvariantCulture)}, Kind: {time.Kind}"; - return new List + return new List { new object?[] { (Func)dateTimeParsing, "9/20/2021 4:18:44 PM", "Time: 2021-09-20T16:18:44.0000000, Kind: Unspecified" }, new object?[] { (Func)dateTimeParsing, "2021-09-20 4:18:44", "Time: 2021-09-20T04:18:44.0000000, Kind: Unspecified" }, @@ -2953,490 +2953,489 @@ namespace Microsoft.AspNetCore.Routing.Internal new object?[] { (Func)dateTimeParsing, " 2021-09-20T23:30: 02.000+00:00 ", "Time: 2021-09-20T23:30:02.0000000Z, Kind: Utc" }, new object?[] { (Func)dateTimeParsing, "2021-09-20 16:48:02-07:00", "Time: 2021-09-20T23:48:02.0000000Z, Kind: Utc" }, }; - } } + } - [Theory] - [MemberData(nameof(DateTimeDelegates))] - public async Task RequestDelegateCanProcessDateTimesToUtc(Delegate @delegate, string inputTime, string expectedResponse) - { - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + [Theory] + [MemberData(nameof(DateTimeDelegates))] + public async Task RequestDelegateCanProcessDateTimesToUtc(Delegate @delegate, string inputTime, string expectedResponse) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["time"] = inputTime - }); + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["time"] = inputTime + }); - var factoryResult = RequestDelegateFactory.Create(@delegate); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal(expectedResponse, decodedResponseBody); - } + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal(expectedResponse, decodedResponseBody); + } - public static IEnumerable DateTimeOffsetDelegates + public static IEnumerable DateTimeOffsetDelegates + { + get { - get - { - string dateTimeOffsetParsing(DateTimeOffset time) => $"Time: {time.ToString("O", CultureInfo.InvariantCulture)}, Offset: {time.Offset}"; + string dateTimeOffsetParsing(DateTimeOffset time) => $"Time: {time.ToString("O", CultureInfo.InvariantCulture)}, Offset: {time.Offset}"; - return new List + return new List { new object?[] { (Func)dateTimeOffsetParsing, "09/20/2021 16:35:12 +00:00", "Time: 2021-09-20T16:35:12.0000000+00:00, Offset: 00:00:00" }, new object?[] { (Func)dateTimeOffsetParsing, "09/20/2021 11:35:12 +07:00", "Time: 2021-09-20T11:35:12.0000000+07:00, Offset: 07:00:00" }, new object?[] { (Func)dateTimeOffsetParsing, "09/20/2021 16:35:12", "Time: 2021-09-20T16:35:12.0000000+00:00, Offset: 00:00:00" }, new object?[] { (Func)dateTimeOffsetParsing, " 09/20/2021 16:35:12 ", "Time: 2021-09-20T16:35:12.0000000+00:00, Offset: 00:00:00" }, }; - } } + } - [Theory] - [MemberData(nameof(DateTimeOffsetDelegates))] - public async Task RequestDelegateCanProcessDateTimeOffsetsToUtc(Delegate @delegate, string inputTime, string expectedResponse) - { - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + [Theory] + [MemberData(nameof(DateTimeOffsetDelegates))] + public async Task RequestDelegateCanProcessDateTimeOffsetsToUtc(Delegate @delegate, string inputTime, string expectedResponse) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["time"] = inputTime - }); + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["time"] = inputTime + }); - var factoryResult = RequestDelegateFactory.Create(@delegate); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal(expectedResponse, decodedResponseBody); - } + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal(expectedResponse, decodedResponseBody); + } - public static IEnumerable DateOnlyDelegates + public static IEnumerable DateOnlyDelegates + { + get { - get - { - string dateOnlyParsing(DateOnly time) => $"Time: {time.ToString("O", CultureInfo.InvariantCulture)}"; + string dateOnlyParsing(DateOnly time) => $"Time: {time.ToString("O", CultureInfo.InvariantCulture)}"; - return new List + return new List { new object?[] { (Func)dateOnlyParsing, "9/20/2021", "Time: 2021-09-20" }, new object?[] { (Func)dateOnlyParsing, "9 /20 /2021", "Time: 2021-09-20" }, }; - } } + } - [Theory] - [MemberData(nameof(DateOnlyDelegates))] - public async Task RequestDelegateCanProcessDateOnlyValues(Delegate @delegate, string inputTime, string expectedResponse) - { - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + [Theory] + [MemberData(nameof(DateOnlyDelegates))] + public async Task RequestDelegateCanProcessDateOnlyValues(Delegate @delegate, string inputTime, string expectedResponse) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["time"] = inputTime - }); + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["time"] = inputTime + }); - var factoryResult = RequestDelegateFactory.Create(@delegate); - var requestDelegate = factoryResult.RequestDelegate; + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; - await requestDelegate(httpContext); + await requestDelegate(httpContext); - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal(expectedResponse, decodedResponseBody); - } + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal(expectedResponse, decodedResponseBody); + } - public static IEnumerable TimeOnlyDelegates + public static IEnumerable TimeOnlyDelegates + { + get { - get - { - string timeOnlyParsing(TimeOnly time) => $"Time: {time.ToString("O", CultureInfo.InvariantCulture)}"; + string timeOnlyParsing(TimeOnly time) => $"Time: {time.ToString("O", CultureInfo.InvariantCulture)}"; - return new List + return new List { new object?[] { (Func)timeOnlyParsing, "4:34 PM", "Time: 16:34:00.0000000" }, new object?[] { (Func)timeOnlyParsing, " 4:34 PM ", "Time: 16:34:00.0000000" }, }; - } } + } + + [Theory] + [MemberData(nameof(TimeOnlyDelegates))] + public async Task RequestDelegateCanProcessTimeOnlyValues(Delegate @delegate, string inputTime, string expectedResponse) + { + var httpContext = CreateHttpContext(); + var responseBodyStream = new MemoryStream(); + httpContext.Response.Body = responseBodyStream; - [Theory] - [MemberData(nameof(TimeOnlyDelegates))] - public async Task RequestDelegateCanProcessTimeOnlyValues(Delegate @delegate, string inputTime, string expectedResponse) + httpContext.Request.Query = new QueryCollection(new Dictionary { - var httpContext = CreateHttpContext(); - var responseBodyStream = new MemoryStream(); - httpContext.Response.Body = responseBodyStream; + ["time"] = inputTime + }); - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["time"] = inputTime - }); + var factoryResult = RequestDelegateFactory.Create(@delegate); + var requestDelegate = factoryResult.RequestDelegate; - var factoryResult = RequestDelegateFactory.Create(@delegate); - var requestDelegate = factoryResult.RequestDelegate; + await requestDelegate(httpContext); - await requestDelegate(httpContext); + Assert.Equal(200, httpContext.Response.StatusCode); + Assert.False(httpContext.RequestAborted.IsCancellationRequested); + var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); + Assert.Equal(expectedResponse, decodedResponseBody); + } - Assert.Equal(200, httpContext.Response.StatusCode); - Assert.False(httpContext.RequestAborted.IsCancellationRequested); - var decodedResponseBody = Encoding.UTF8.GetString(responseBodyStream.ToArray()); - Assert.Equal(expectedResponse, decodedResponseBody); - } + private DefaultHttpContext CreateHttpContext() + { + var responseFeature = new TestHttpResponseFeature(); - private DefaultHttpContext CreateHttpContext() + return new() { - var responseFeature = new TestHttpResponseFeature(); - - return new() - { - RequestServices = new ServiceCollection().AddSingleton(LoggerFactory).BuildServiceProvider(), - Features = + RequestServices = new ServiceCollection().AddSingleton(LoggerFactory).BuildServiceProvider(), + Features = { [typeof(IHttpResponseFeature)] = responseFeature, [typeof(IHttpResponseBodyFeature)] = responseFeature, [typeof(IHttpRequestLifetimeFeature)] = new TestHttpRequestLifetimeFeature(), } - }; - } + }; + } - private class Todo : ITodo - { - public int Id { get; set; } - public string? Name { get; set; } = "Todo"; - public bool IsComplete { get; set; } - } + private class Todo : ITodo + { + public int Id { get; set; } + public string? Name { get; set; } = "Todo"; + public bool IsComplete { get; set; } + } - private class CustomTodo : Todo + private class CustomTodo : Todo + { + public static async ValueTask BindAsync(HttpContext context, ParameterInfo parameter) { - public static async ValueTask BindAsync(HttpContext context, ParameterInfo parameter) - { - Assert.Equal(typeof(CustomTodo), parameter.ParameterType); - Assert.Equal("customTodo", parameter.Name); + Assert.Equal(typeof(CustomTodo), parameter.ParameterType); + Assert.Equal("customTodo", parameter.Name); - var body = await context.Request.ReadFromJsonAsync(); - context.Request.Body.Position = 0; - return body; - } + var body = await context.Request.ReadFromJsonAsync(); + context.Request.Body.Position = 0; + return body; } + } - private class JsonTodo : Todo + private class JsonTodo : Todo + { + public static async ValueTask BindAsync(HttpContext context, ParameterInfo parameter) { - public static async ValueTask BindAsync(HttpContext context, ParameterInfo parameter) - { - // manually call deserialize so we don't check content type - var body = await JsonSerializer.DeserializeAsync(context.Request.Body); - context.Request.Body.Position = 0; - return body; - } + // manually call deserialize so we don't check content type + var body = await JsonSerializer.DeserializeAsync(context.Request.Body); + context.Request.Body.Position = 0; + return body; } + } - private record struct TodoStruct(int Id, string? Name, bool IsComplete) : ITodo; + private record struct TodoStruct(int Id, string? Name, bool IsComplete) : ITodo; - private interface ITodo - { - public int Id { get; } - public string? Name { get; } - public bool IsComplete { get; } - } + private interface ITodo + { + public int Id { get; } + public string? Name { get; } + public bool IsComplete { get; } + } - class TodoJsonConverter : JsonConverter + class TodoJsonConverter : JsonConverter + { + public override ITodo? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override ITodo? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + var todo = new Todo(); + while (reader.Read()) { - var todo = new Todo(); - while (reader.Read()) + if (reader.TokenType == JsonTokenType.EndObject) { - if (reader.TokenType == JsonTokenType.EndObject) - { - break; - } - - var property = reader.GetString()!; - reader.Read(); - - switch (property.ToLowerInvariant()) - { - case "id": - todo.Id = reader.GetInt32(); - break; - case "name": - todo.Name = reader.GetString(); - break; - case "iscomplete": - todo.IsComplete = reader.GetBoolean(); - break; - default: - break; - } + break; } - return todo; - } + var property = reader.GetString()!; + reader.Read(); - public override void Write(Utf8JsonWriter writer, ITodo value, JsonSerializerOptions options) - { - throw new NotImplementedException(); + switch (property.ToLowerInvariant()) + { + case "id": + todo.Id = reader.GetInt32(); + break; + case "name": + todo.Name = reader.GetString(); + break; + case "iscomplete": + todo.IsComplete = reader.GetBoolean(); + break; + default: + break; + } } - } - private struct BodyStruct - { - public int Id { get; set; } + return todo; } - private class FromRouteAttribute : Attribute, IFromRouteMetadata + public override void Write(Utf8JsonWriter writer, ITodo value, JsonSerializerOptions options) { - public string? Name { get; set; } + throw new NotImplementedException(); } + } - private class FromQueryAttribute : Attribute, IFromQueryMetadata - { - public string? Name { get; set; } - } + private struct BodyStruct + { + public int Id { get; set; } + } - private class FromHeaderAttribute : Attribute, IFromHeaderMetadata - { - public string? Name { get; set; } - } + private class FromRouteAttribute : Attribute, IFromRouteMetadata + { + public string? Name { get; set; } + } - private class FromBodyAttribute : Attribute, IFromBodyMetadata - { - public bool AllowEmpty { get; set; } - } + private class FromQueryAttribute : Attribute, IFromQueryMetadata + { + public string? Name { get; set; } + } - private class FromServiceAttribute : Attribute, IFromServiceMetadata - { - } + private class FromHeaderAttribute : Attribute, IFromHeaderMetadata + { + public string? Name { get; set; } + } - class HttpHandler - { - private int _calls; + private class FromBodyAttribute : Attribute, IFromBodyMetadata + { + public bool AllowEmpty { get; set; } + } - public void Handle(HttpContext httpContext) - { - _calls++; - httpContext.Items["calls"] = _calls; - } - } + private class FromServiceAttribute : Attribute, IFromServiceMetadata + { + } - private interface IMyService - { - } + class HttpHandler + { + private int _calls; - private class MyService : IMyService + public void Handle(HttpContext httpContext) { + _calls++; + httpContext.Items["calls"] = _calls; } + } - private class CustomResult : IResult - { - private readonly string _resultString; + private interface IMyService + { + } - public CustomResult(string resultString) - { - _resultString = resultString; - } + private class MyService : IMyService + { + } - public Task ExecuteAsync(HttpContext httpContext) - { - return httpContext.Response.WriteAsync(_resultString); - } + private class CustomResult : IResult + { + private readonly string _resultString; + + public CustomResult(string resultString) + { + _resultString = resultString; } - private struct StructResult : IResult + public Task ExecuteAsync(HttpContext httpContext) { - private readonly string _resultString; + return httpContext.Response.WriteAsync(_resultString); + } + } - public StructResult(string resultString) - { - _resultString = resultString; - } + private struct StructResult : IResult + { + private readonly string _resultString; - public Task ExecuteAsync(HttpContext httpContext) - { - return httpContext.Response.WriteAsync(_resultString); - } + public StructResult(string resultString) + { + _resultString = resultString; } - private class ExceptionThrowingRequestBodyStream : Stream + public Task ExecuteAsync(HttpContext httpContext) { - private readonly Exception _exceptionToThrow; - - public ExceptionThrowingRequestBodyStream(Exception exceptionToThrow) - { - _exceptionToThrow = exceptionToThrow; - } - - public override bool CanRead => true; + return httpContext.Response.WriteAsync(_resultString); + } + } - public override bool CanSeek => false; + private class ExceptionThrowingRequestBodyStream : Stream + { + private readonly Exception _exceptionToThrow; - public override bool CanWrite => false; + public ExceptionThrowingRequestBodyStream(Exception exceptionToThrow) + { + _exceptionToThrow = exceptionToThrow; + } - public override long Length => throw new NotImplementedException(); + public override bool CanRead => true; - public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override bool CanSeek => false; - public override void Flush() - { - throw new NotImplementedException(); - } + public override bool CanWrite => false; - public override int Read(byte[] buffer, int offset, int count) - { - throw _exceptionToThrow; - } + public override long Length => throw new NotImplementedException(); - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } - public override void SetLength(long value) - { - throw new NotImplementedException(); - } + public override void Flush() + { + throw new NotImplementedException(); + } - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } + public override int Read(byte[] buffer, int offset, int count) + { + throw _exceptionToThrow; } - private class EmptyServiceProvider : IServiceScope, IServiceProvider, IServiceScopeFactory + public override long Seek(long offset, SeekOrigin origin) { - public IServiceProvider ServiceProvider => this; + throw new NotImplementedException(); + } - public IServiceScope CreateScope() - { - return new EmptyServiceProvider(); - } + public override void SetLength(long value) + { + throw new NotImplementedException(); + } - public void Dispose() - { + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + } - } + private class EmptyServiceProvider : IServiceScope, IServiceProvider, IServiceScopeFactory + { + public IServiceProvider ServiceProvider => this; - public object? GetService(Type serviceType) - { - if (serviceType == typeof(IServiceScopeFactory)) - { - return this; - } - return null; - } + public IServiceScope CreateScope() + { + return new EmptyServiceProvider(); } - private class TestHttpRequestLifetimeFeature : IHttpRequestLifetimeFeature + public void Dispose() { - private readonly CancellationTokenSource _requestAbortedCts = new(); - public CancellationToken RequestAborted { get => _requestAbortedCts.Token; set => throw new NotImplementedException(); } + } - public void Abort() + public object? GetService(Type serviceType) + { + if (serviceType == typeof(IServiceScopeFactory)) { - _requestAbortedCts.Cancel(); + return this; } + return null; } + } - private class TestHttpResponseFeature : IHttpResponseFeature, IHttpResponseBodyFeature + private class TestHttpRequestLifetimeFeature : IHttpRequestLifetimeFeature + { + private readonly CancellationTokenSource _requestAbortedCts = new(); + + public CancellationToken RequestAborted { get => _requestAbortedCts.Token; set => throw new NotImplementedException(); } + + public void Abort() { - public int StatusCode { get; set; } = 200; - public string? ReasonPhrase { get; set; } - public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); + _requestAbortedCts.Cancel(); + } + } - public bool HasStarted { get; private set; } + private class TestHttpResponseFeature : IHttpResponseFeature, IHttpResponseBodyFeature + { + public int StatusCode { get; set; } = 200; + public string? ReasonPhrase { get; set; } + public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); - // Assume any access to the response Body/Stream/Writer is writing for test purposes. - public Stream Body - { - get - { - HasStarted = true; - return Stream.Null; - } - set - { - } - } + public bool HasStarted { get; private set; } - public Stream Stream + // Assume any access to the response Body/Stream/Writer is writing for test purposes. + public Stream Body + { + get { - get - { - HasStarted = true; - return Stream.Null; - } + HasStarted = true; + return Stream.Null; } - - public PipeWriter Writer + set { - get - { - HasStarted = true; - return PipeWriter.Create(Stream.Null); - } } + } - public Task StartAsync(CancellationToken cancellationToken = default) + public Stream Stream + { + get { HasStarted = true; - return Task.CompletedTask; + return Stream.Null; } + } - public Task CompleteAsync() + public PipeWriter Writer + { + get { HasStarted = true; - return Task.CompletedTask; + return PipeWriter.Create(Stream.Null); } + } - public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default) - { - HasStarted = true; - return Task.CompletedTask; - } + public Task StartAsync(CancellationToken cancellationToken = default) + { + HasStarted = true; + return Task.CompletedTask; + } - public void DisableBuffering() - { - } + public Task CompleteAsync() + { + HasStarted = true; + return Task.CompletedTask; + } - public void OnStarting(Func callback, object state) - { - } + public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default) + { + HasStarted = true; + return Task.CompletedTask; + } - public void OnCompleted(Func callback, object state) - { - } + public void DisableBuffering() + { } - private class RequestBodyDetectionFeature : IHttpRequestBodyDetectionFeature + public void OnStarting(Func callback, object state) { - public RequestBodyDetectionFeature(bool canHaveBody) - { - CanHaveBody = canHaveBody; - } + } - public bool CanHaveBody { get; } + public void OnCompleted(Func callback, object state) + { } } - internal static class TestExtensionResults + private class RequestBodyDetectionFeature : IHttpRequestBodyDetectionFeature { - public static IResult TestResult(this IResultExtensions resultExtensions, string name) + public RequestBodyDetectionFeature(bool canHaveBody) { - return Results.Ok(FormattableString.Invariant($"Hello {name}. This is from an extension method.")); + CanHaveBody = canHaveBody; } + + public bool CanHaveBody { get; } + } +} + +internal static class TestExtensionResults +{ + public static IResult TestResult(this IResultExtensions resultExtensions, string name) + { + return Results.Ok(FormattableString.Invariant($"Hello {name}. This is from an extension method.")); } } diff --git a/src/Http/Http.Extensions/test/ResponseExtensionTests.cs b/src/Http/Http.Extensions/test/ResponseExtensionTests.cs index ac3570068b..c996a49851 100644 --- a/src/Http/Http.Extensions/test/ResponseExtensionTests.cs +++ b/src/Http/Http.Extensions/test/ResponseExtensionTests.cs @@ -10,72 +10,71 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.Net.Http.Headers; using Xunit; -namespace Microsoft.AspNetCore.Http.Extensions +namespace Microsoft.AspNetCore.Http.Extensions; + +public class ResponseExtensionTests { - public class ResponseExtensionTests + [Fact] + public void Clear_ResetsResponse() { - [Fact] - public void Clear_ResetsResponse() - { - var context = new DefaultHttpContext(); - context.Response.StatusCode = 201; - context.Response.Headers["custom"] = "value"; - context.Response.Body.Write(new byte[100], 0, 100); + var context = new DefaultHttpContext(); + context.Response.StatusCode = 201; + context.Response.Headers["custom"] = "value"; + context.Response.Body.Write(new byte[100], 0, 100); - context.Response.Clear(); + context.Response.Clear(); - Assert.Equal(200, context.Response.StatusCode); - Assert.Equal(string.Empty, context.Response.Headers["custom"].ToString()); - Assert.Equal(0, context.Response.Body.Length); - } + Assert.Equal(200, context.Response.StatusCode); + Assert.Equal(string.Empty, context.Response.Headers["custom"].ToString()); + Assert.Equal(0, context.Response.Body.Length); + } - [Fact] - public void Clear_AlreadyStarted_Throws() - { - var context = new DefaultHttpContext(); - context.Features.Set(new StartedResponseFeature()); + [Fact] + public void Clear_AlreadyStarted_Throws() + { + var context = new DefaultHttpContext(); + context.Features.Set(new StartedResponseFeature()); - Assert.Throws(() => context.Response.Clear()); - } + Assert.Throws(() => context.Response.Clear()); + } - [Theory] - [InlineData(true, false, 301)] - [InlineData(false, false, 302)] - [InlineData(true, true, 308)] - [InlineData(false, true, 307)] - public void Redirect_SetsResponseCorrectly(bool permanent, bool preserveMethod, int expectedStatusCode) - { - var location = "http://localhost/redirect"; - var context = new DefaultHttpContext(); - context.Response.StatusCode = StatusCodes.Status200OK; + [Theory] + [InlineData(true, false, 301)] + [InlineData(false, false, 302)] + [InlineData(true, true, 308)] + [InlineData(false, true, 307)] + public void Redirect_SetsResponseCorrectly(bool permanent, bool preserveMethod, int expectedStatusCode) + { + var location = "http://localhost/redirect"; + var context = new DefaultHttpContext(); + context.Response.StatusCode = StatusCodes.Status200OK; - context.Response.Redirect(location, permanent, preserveMethod); + context.Response.Redirect(location, permanent, preserveMethod); - Assert.Equal(location, context.Response.Headers.Location.First()); - Assert.Equal(expectedStatusCode, context.Response.StatusCode); - } + Assert.Equal(location, context.Response.Headers.Location.First()); + Assert.Equal(expectedStatusCode, context.Response.StatusCode); + } - private class StartedResponseFeature : IHttpResponseFeature - { - public Stream Body { get; set; } + private class StartedResponseFeature : IHttpResponseFeature + { + public Stream Body { get; set; } - public bool HasStarted { get { return true; } } + public bool HasStarted { get { return true; } } - public IHeaderDictionary Headers { get; set; } + public IHeaderDictionary Headers { get; set; } - public string ReasonPhrase { get; set; } + public string ReasonPhrase { get; set; } - public int StatusCode { get; set; } + public int StatusCode { get; set; } - public void OnCompleted(Func callback, object state) - { - throw new NotImplementedException(); - } + public void OnCompleted(Func callback, object state) + { + throw new NotImplementedException(); + } - public void OnStarting(Func callback, object state) - { - throw new NotImplementedException(); - } + public void OnStarting(Func callback, object state) + { + throw new NotImplementedException(); } } } diff --git a/src/Http/Http.Extensions/test/SendFileResponseExtensionsTests.cs b/src/Http/Http.Extensions/test/SendFileResponseExtensionsTests.cs index 2e3ecdbddf..33871c97e0 100644 --- a/src/Http/Http.Extensions/test/SendFileResponseExtensionsTests.cs +++ b/src/Http/Http.Extensions/test/SendFileResponseExtensionsTests.cs @@ -9,137 +9,136 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; using Xunit; -namespace Microsoft.AspNetCore.Http.Extensions.Tests +namespace Microsoft.AspNetCore.Http.Extensions.Tests; + +public class SendFileResponseExtensionsTests { - public class SendFileResponseExtensionsTests + [Fact] + public Task SendFileWhenFileNotFoundThrows() { - [Fact] - public Task SendFileWhenFileNotFoundThrows() - { - var response = new DefaultHttpContext().Response; - return Assert.ThrowsAsync(() => response.SendFileAsync("foo")); - } + var response = new DefaultHttpContext().Response; + return Assert.ThrowsAsync(() => response.SendFileAsync("foo")); + } - [Fact] - public async Task SendFileWorks() - { - var context = new DefaultHttpContext(); - var response = context.Response; - var fakeFeature = new FakeResponseBodyFeature(); - context.Features.Set(fakeFeature); + [Fact] + public async Task SendFileWorks() + { + var context = new DefaultHttpContext(); + var response = context.Response; + var fakeFeature = new FakeResponseBodyFeature(); + context.Features.Set(fakeFeature); - await response.SendFileAsync("bob", 1, 3, CancellationToken.None); + await response.SendFileAsync("bob", 1, 3, CancellationToken.None); - Assert.Equal("bob", fakeFeature.Name); - Assert.Equal(1, fakeFeature.Offset); - Assert.Equal(3, fakeFeature.Length); - Assert.Equal(CancellationToken.None, fakeFeature.Token); - } + Assert.Equal("bob", fakeFeature.Name); + Assert.Equal(1, fakeFeature.Offset); + Assert.Equal(3, fakeFeature.Length); + Assert.Equal(CancellationToken.None, fakeFeature.Token); + } - [Fact] - public async Task SendFile_FallsBackToBodyStream() - { - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - var response = context.Response; - response.Body = body; + [Fact] + public async Task SendFile_FallsBackToBodyStream() + { + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + var response = context.Response; + response.Body = body; - await response.SendFileAsync("testfile1kb.txt", 1, 3, CancellationToken.None); + await response.SendFileAsync("testfile1kb.txt", 1, 3, CancellationToken.None); - Assert.Equal(3, body.Length); - } + Assert.Equal(3, body.Length); + } - [Fact] - public async Task SendFile_Stream_ThrowsWhenCanceled() - { - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - var response = context.Response; - response.Body = body; + [Fact] + public async Task SendFile_Stream_ThrowsWhenCanceled() + { + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + var response = context.Response; + response.Body = body; - await Assert.ThrowsAnyAsync( - () => response.SendFileAsync("testfile1kb.txt", 1, 3, new CancellationToken(canceled: true))); + await Assert.ThrowsAnyAsync( + () => response.SendFileAsync("testfile1kb.txt", 1, 3, new CancellationToken(canceled: true))); - Assert.Equal(0, body.Length); - } + Assert.Equal(0, body.Length); + } - [Fact] - public async Task SendFile_Feature_ThrowsWhenCanceled() - { - var context = new DefaultHttpContext(); - var fakeFeature = new FakeResponseBodyFeature(); - context.Features.Set(fakeFeature); - var response = context.Response; + [Fact] + public async Task SendFile_Feature_ThrowsWhenCanceled() + { + var context = new DefaultHttpContext(); + var fakeFeature = new FakeResponseBodyFeature(); + context.Features.Set(fakeFeature); + var response = context.Response; - await Assert.ThrowsAsync( - () => response.SendFileAsync("testfile1kb.txt", 1, 3, new CancellationToken(canceled: true))); - } + await Assert.ThrowsAsync( + () => response.SendFileAsync("testfile1kb.txt", 1, 3, new CancellationToken(canceled: true))); + } - [Fact] - public async Task SendFile_Stream_AbortsSilentlyWhenRequestCanceled() - { - var body = new MemoryStream(); - var context = new DefaultHttpContext(); - context.RequestAborted = new CancellationToken(canceled: true); - var response = context.Response; - response.Body = body; + [Fact] + public async Task SendFile_Stream_AbortsSilentlyWhenRequestCanceled() + { + var body = new MemoryStream(); + var context = new DefaultHttpContext(); + context.RequestAborted = new CancellationToken(canceled: true); + var response = context.Response; + response.Body = body; + + await response.SendFileAsync("testfile1kb.txt", 1, 3, CancellationToken.None); + + Assert.Equal(0, body.Length); + } + + [Fact] + public async Task SendFile_Feature_AbortsSilentlyWhenRequestCanceled() + { + var context = new DefaultHttpContext(); + var fakeFeature = new FakeResponseBodyFeature(); + context.Features.Set(fakeFeature); + var token = new CancellationToken(canceled: true); + context.RequestAborted = token; + var response = context.Response; - await response.SendFileAsync("testfile1kb.txt", 1, 3, CancellationToken.None); + await response.SendFileAsync("testfile1kb.txt", 1, 3, CancellationToken.None); - Assert.Equal(0, body.Length); + Assert.Equal(token, fakeFeature.Token); + } + + private class FakeResponseBodyFeature : IHttpResponseBodyFeature + { + public string Name { get; set; } = null; + public long Offset { get; set; } = 0; + public long? Length { get; set; } = null; + public CancellationToken Token { get; set; } + + public Stream Stream => throw new System.NotImplementedException(); + + public PipeWriter Writer => throw new System.NotImplementedException(); + + public Task CompleteAsync() + { + throw new System.NotImplementedException(); } - [Fact] - public async Task SendFile_Feature_AbortsSilentlyWhenRequestCanceled() + public void DisableBuffering() { - var context = new DefaultHttpContext(); - var fakeFeature = new FakeResponseBodyFeature(); - context.Features.Set(fakeFeature); - var token = new CancellationToken(canceled: true); - context.RequestAborted = token; - var response = context.Response; + throw new System.NotImplementedException(); + } - await response.SendFileAsync("testfile1kb.txt", 1, 3, CancellationToken.None); + public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) + { + Name = path; + Offset = offset; + Length = length; + Token = cancellation; - Assert.Equal(token, fakeFeature.Token); + cancellation.ThrowIfCancellationRequested(); + return Task.FromResult(0); } - private class FakeResponseBodyFeature : IHttpResponseBodyFeature + public Task StartAsync(CancellationToken token = default) { - public string Name { get; set; } = null; - public long Offset { get; set; } = 0; - public long? Length { get; set; } = null; - public CancellationToken Token { get; set; } - - public Stream Stream => throw new System.NotImplementedException(); - - public PipeWriter Writer => throw new System.NotImplementedException(); - - public Task CompleteAsync() - { - throw new System.NotImplementedException(); - } - - public void DisableBuffering() - { - throw new System.NotImplementedException(); - } - - public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) - { - Name = path; - Offset = offset; - Length = length; - Token = cancellation; - - cancellation.ThrowIfCancellationRequested(); - return Task.FromResult(0); - } - - public Task StartAsync(CancellationToken token = default) - { - throw new System.NotImplementedException(); - } + throw new System.NotImplementedException(); } } } diff --git a/src/Http/Http.Extensions/test/TestStream.cs b/src/Http/Http.Extensions/test/TestStream.cs index 6590f23499..809dec4bd2 100644 --- a/src/Http/Http.Extensions/test/TestStream.cs +++ b/src/Http/Http.Extensions/test/TestStream.cs @@ -6,53 +6,52 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http.Extensions.Tests +namespace Microsoft.AspNetCore.Http.Extensions.Tests; + +public class TestStream : Stream { - public class TestStream : Stream + public override bool CanRead { get; } + public override bool CanSeek { get; } + public override bool CanWrite { get; } + public override long Length { get; } + public override long Position { get; set; } + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + cancellationToken.Register(s => ((TaskCompletionSource)s).SetCanceled(), tcs); + return new ValueTask(tcs.Task); + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { - public override bool CanRead { get; } - public override bool CanSeek { get; } - public override bool CanWrite { get; } - public override long Length { get; } - public override long Position { get; set; } - - public override void Flush() - { - throw new NotImplementedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - - public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(); - cancellationToken.Register(s => ((TaskCompletionSource)s).SetCanceled(), tcs); - return new ValueTask(tcs.Task); - } - - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) - { - var tcs = new TaskCompletionSource(); - cancellationToken.Register(s => ((TaskCompletionSource)s).SetCanceled(), tcs); - return new ValueTask(tcs.Task); - } + var tcs = new TaskCompletionSource(); + cancellationToken.Register(s => ((TaskCompletionSource)s).SetCanceled(), tcs); + return new ValueTask(tcs.Task); } } diff --git a/src/Http/Http.Extensions/test/UriHelperTests.cs b/src/Http/Http.Extensions/test/UriHelperTests.cs index a5a6b9819f..777cefc767 100644 --- a/src/Http/Http.Extensions/test/UriHelperTests.cs +++ b/src/Http/Http.Extensions/test/UriHelperTests.cs @@ -4,196 +4,195 @@ using System; using Xunit; -namespace Microsoft.AspNetCore.Http.Extensions +namespace Microsoft.AspNetCore.Http.Extensions; + +public class UriHelperTests { - public class UriHelperTests + [Fact] + public void EncodeEmptyPartialUrl() + { + var result = UriHelper.BuildRelative(); + + Assert.Equal("/", result); + } + + [Fact] + public void EncodePartialUrl() { - [Fact] - public void EncodeEmptyPartialUrl() - { - var result = UriHelper.BuildRelative(); - - Assert.Equal("/", result); - } - - [Fact] - public void EncodePartialUrl() - { - var result = UriHelper.BuildRelative(new PathString("/un?escaped/base"), new PathString("/un?escaped"), - new QueryString("?name=val%23ue"), new FragmentString("#my%20value")); - - Assert.Equal("/un%3Fescaped/base/un%3Fescaped?name=val%23ue#my%20value", result); - } - - [Fact] - public void EncodeEmptyFullUrl() - { - var result = UriHelper.BuildAbsolute("http", new HostString(string.Empty)); - - Assert.Equal("http:///", result); - } - - [Fact] - public void EncodeFullUrl() - { - var result = UriHelper.BuildAbsolute("http", new HostString("my.HoΨst:80"), new PathString("/un?escaped/base"), new PathString("/un?escaped"), - new QueryString("?name=val%23ue"), new FragmentString("#my%20value")); - - Assert.Equal("http://my.xn--host-cpd:80/un%3Fescaped/base/un%3Fescaped?name=val%23ue#my%20value", result); - } - - [Theory] - [InlineData("http", "example.com", "", "", "", "", "http://example.com/")] - [InlineData("https", "example.com", "", "", "", "", "https://example.com/")] - [InlineData("http", "example.com", "", "/foo/bar", "", "", "http://example.com/foo/bar")] - [InlineData("http", "example.com", "", "/foo/bar", "?baz=1", "", "http://example.com/foo/bar?baz=1")] - [InlineData("http", "example.com", "", "/foo", "", "#col=2", "http://example.com/foo#col=2")] - [InlineData("http", "example.com", "", "/foo", "?bar=1", "#col=2", "http://example.com/foo?bar=1#col=2")] - [InlineData("http", "example.com", "/base", "/foo", "?bar=1", "#col=2", "http://example.com/base/foo?bar=1#col=2")] - [InlineData("http", "example.com", "/base/", "/foo", "?bar=1", "#col=2", "http://example.com/base/foo?bar=1#col=2")] - [InlineData("http", "example.com", "/base/", "", "?bar=1", "#col=2", "http://example.com/base/?bar=1#col=2")] - [InlineData("http", "example.com", "", "", "?bar=1", "#col=2", "http://example.com/?bar=1#col=2")] - [InlineData("http", "example.com", "", "", "", "#frag?stillfrag/stillfrag", "http://example.com/#frag?stillfrag/stillfrag")] - [InlineData("http", "example.com", "", "", "?q/stillq", "#frag?stillfrag/stillfrag", "http://example.com/?q/stillq#frag?stillfrag/stillfrag")] - [InlineData("http", "example.com", "", "/fo#o", "", "#col=2", "http://example.com/fo%23o#col=2")] - [InlineData("http", "example.com", "", "/fo?o", "", "#col=2", "http://example.com/fo%3Fo#col=2")] - [InlineData("ftp", "example.com", "", "/", "", "", "ftp://example.com/")] - [InlineData("ftp", "example.com", "/", "/", "", "", "ftp://example.com/")] - [InlineData("https", "127.0.0.0:80", "", "/bar", "", "", "https://127.0.0.0:80/bar")] - [InlineData("http", "[1080:0:0:0:8:800:200C:417A]", "", "/index.html", "", "", "http://[1080:0:0:0:8:800:200C:417A]/index.html")] - [InlineData("http", "example.com", "", "///", "", "", "http://example.com///")] - [InlineData("http", "example.com", "///", "///", "", "", "http://example.com/////")] - public void BuildAbsoluteGenerationChecks( - string scheme, - string host, - string pathBase, - string path, - string query, - string fragment, - string expectedUri) - { - var uri = UriHelper.BuildAbsolute( - scheme, - new HostString(host), - new PathString(pathBase), - new PathString(path), - new QueryString(query), - new FragmentString(fragment)); - - Assert.Equal(expectedUri, uri); - } - - [Fact] - public void GetEncodedUrlFromRequest() - { - var request = new DefaultHttpContext().Request; - request.Scheme = "http"; - request.Host = new HostString("my.HoΨst:80"); - request.PathBase = new PathString("/un?escaped/base"); - request.Path = new PathString("/un?escaped"); - request.QueryString = new QueryString("?name=val%23ue"); - - Assert.Equal("http://my.xn--host-cpd:80/un%3Fescaped/base/un%3Fescaped?name=val%23ue", request.GetEncodedUrl()); - } - - [Theory] - [InlineData("/un?escaped/base")] - [InlineData(null)] - public void GetDisplayUrlFromRequest(string pathBase) - { - var request = new DefaultHttpContext().Request; - request.Scheme = "http"; - request.Host = new HostString("my.HoΨst:80"); - request.PathBase = new PathString(pathBase); - request.Path = new PathString("/un?escaped"); - request.QueryString = new QueryString("?name=val%23ue"); - - Assert.Equal("http://my.hoψst:80" + pathBase + "/un?escaped?name=val%23ue", request.GetDisplayUrl()); - } - - [Theory] - [InlineData("http://example.com", "http", "example.com", "", "", "")] - [InlineData("https://example.com", "https", "example.com", "", "", "")] - [InlineData("http://example.com/foo/bar", "http", "example.com", "/foo/bar", "", "")] - [InlineData("http://example.com/foo/bar?baz=1", "http", "example.com", "/foo/bar", "?baz=1", "")] - [InlineData("http://example.com/foo#col=2", "http", "example.com", "/foo", "", "#col=2")] - [InlineData("http://example.com/foo?bar=1#col=2", "http", "example.com", "/foo", "?bar=1", "#col=2")] - [InlineData("http://example.com?bar=1#col=2", "http", "example.com", "", "?bar=1", "#col=2")] - [InlineData("http://example.com#frag?stillfrag/stillfrag", "http", "example.com", "", "", "#frag?stillfrag/stillfrag")] - [InlineData("http://example.com?q/stillq#frag?stillfrag/stillfrag", "http", "example.com", "", "?q/stillq", "#frag?stillfrag/stillfrag")] - [InlineData("http://example.com/fo%23o#col=2", "http", "example.com", "/fo#o", "", "#col=2")] - [InlineData("http://example.com/fo%3Fo#col=2", "http", "example.com", "/fo?o", "", "#col=2")] - [InlineData("ftp://example.com/", "ftp", "example.com", "/", "", "")] - [InlineData("https://127.0.0.0:80/bar", "https", "127.0.0.0:80", "/bar", "", "")] - [InlineData("http://[1080:0:0:0:8:800:200C:417A]/index.html", "http", "[1080:0:0:0:8:800:200C:417A]", "/index.html", "", "")] - [InlineData("http://example.com///", "http", "example.com", "///", "", "")] - public void FromAbsoluteUriParsingChecks( - string uri, - string expectedScheme, - string expectedHost, - string expectedPath, - string expectedQuery, - string expectedFragment) - { - string scheme = null; - var host = new HostString(); - var path = new PathString(); - var query = new QueryString(); - var fragment = new FragmentString(); - UriHelper.FromAbsolute(uri, out scheme, out host, out path, out query, out fragment); - - Assert.Equal(scheme, expectedScheme); - Assert.Equal(host, new HostString(expectedHost)); - Assert.Equal(path, new PathString(expectedPath)); - Assert.Equal(query, new QueryString(expectedQuery)); - Assert.Equal(fragment, new FragmentString(expectedFragment)); - } - - [Fact] - public void FromAbsoluteToBuildAbsolute() - { - var scheme = "http"; - var host = new HostString("example.com"); - var path = new PathString("/index.html"); - var query = new QueryString("?foo=1"); - var fragment = new FragmentString("#col=1"); - var request = UriHelper.BuildAbsolute(scheme, host, path:path, query:query, fragment:fragment); - - string resScheme = null; - var resHost = new HostString(); - var resPath = new PathString(); - var resQuery = new QueryString(); - var resFragment = new FragmentString(); - UriHelper.FromAbsolute(request, out resScheme, out resHost, out resPath, out resQuery, out resFragment); - - Assert.Equal(scheme, resScheme); - Assert.Equal(host, resHost); - Assert.Equal(path, resPath); - Assert.Equal(query, resQuery); - Assert.Equal(fragment, resFragment); - } - - [Fact] - public void BuildAbsoluteNullInputThrowsArgumentNullException() - { - var resHost = new HostString(); - var resPath = new PathString(); - var resQuery = new QueryString(); - var resFragment = new FragmentString(); - Assert.Throws(() => UriHelper.BuildAbsolute(null, resHost, resPath, resPath, resQuery, resFragment)); - - } - - [Fact] - public void FromAbsoluteNullInputThrowsArgumentNullException() - { - string resScheme = null; - var resHost = new HostString(); - var resPath = new PathString(); - var resQuery = new QueryString(); - var resFragment = new FragmentString(); - Assert.Throws(() => UriHelper.FromAbsolute(null, out resScheme, out resHost, out resPath, out resQuery, out resFragment)); - - } + var result = UriHelper.BuildRelative(new PathString("/un?escaped/base"), new PathString("/un?escaped"), + new QueryString("?name=val%23ue"), new FragmentString("#my%20value")); + + Assert.Equal("/un%3Fescaped/base/un%3Fescaped?name=val%23ue#my%20value", result); + } + + [Fact] + public void EncodeEmptyFullUrl() + { + var result = UriHelper.BuildAbsolute("http", new HostString(string.Empty)); + + Assert.Equal("http:///", result); + } + + [Fact] + public void EncodeFullUrl() + { + var result = UriHelper.BuildAbsolute("http", new HostString("my.HoΨst:80"), new PathString("/un?escaped/base"), new PathString("/un?escaped"), + new QueryString("?name=val%23ue"), new FragmentString("#my%20value")); + + Assert.Equal("http://my.xn--host-cpd:80/un%3Fescaped/base/un%3Fescaped?name=val%23ue#my%20value", result); + } + + [Theory] + [InlineData("http", "example.com", "", "", "", "", "http://example.com/")] + [InlineData("https", "example.com", "", "", "", "", "https://example.com/")] + [InlineData("http", "example.com", "", "/foo/bar", "", "", "http://example.com/foo/bar")] + [InlineData("http", "example.com", "", "/foo/bar", "?baz=1", "", "http://example.com/foo/bar?baz=1")] + [InlineData("http", "example.com", "", "/foo", "", "#col=2", "http://example.com/foo#col=2")] + [InlineData("http", "example.com", "", "/foo", "?bar=1", "#col=2", "http://example.com/foo?bar=1#col=2")] + [InlineData("http", "example.com", "/base", "/foo", "?bar=1", "#col=2", "http://example.com/base/foo?bar=1#col=2")] + [InlineData("http", "example.com", "/base/", "/foo", "?bar=1", "#col=2", "http://example.com/base/foo?bar=1#col=2")] + [InlineData("http", "example.com", "/base/", "", "?bar=1", "#col=2", "http://example.com/base/?bar=1#col=2")] + [InlineData("http", "example.com", "", "", "?bar=1", "#col=2", "http://example.com/?bar=1#col=2")] + [InlineData("http", "example.com", "", "", "", "#frag?stillfrag/stillfrag", "http://example.com/#frag?stillfrag/stillfrag")] + [InlineData("http", "example.com", "", "", "?q/stillq", "#frag?stillfrag/stillfrag", "http://example.com/?q/stillq#frag?stillfrag/stillfrag")] + [InlineData("http", "example.com", "", "/fo#o", "", "#col=2", "http://example.com/fo%23o#col=2")] + [InlineData("http", "example.com", "", "/fo?o", "", "#col=2", "http://example.com/fo%3Fo#col=2")] + [InlineData("ftp", "example.com", "", "/", "", "", "ftp://example.com/")] + [InlineData("ftp", "example.com", "/", "/", "", "", "ftp://example.com/")] + [InlineData("https", "127.0.0.0:80", "", "/bar", "", "", "https://127.0.0.0:80/bar")] + [InlineData("http", "[1080:0:0:0:8:800:200C:417A]", "", "/index.html", "", "", "http://[1080:0:0:0:8:800:200C:417A]/index.html")] + [InlineData("http", "example.com", "", "///", "", "", "http://example.com///")] + [InlineData("http", "example.com", "///", "///", "", "", "http://example.com/////")] + public void BuildAbsoluteGenerationChecks( + string scheme, + string host, + string pathBase, + string path, + string query, + string fragment, + string expectedUri) + { + var uri = UriHelper.BuildAbsolute( + scheme, + new HostString(host), + new PathString(pathBase), + new PathString(path), + new QueryString(query), + new FragmentString(fragment)); + + Assert.Equal(expectedUri, uri); + } + + [Fact] + public void GetEncodedUrlFromRequest() + { + var request = new DefaultHttpContext().Request; + request.Scheme = "http"; + request.Host = new HostString("my.HoΨst:80"); + request.PathBase = new PathString("/un?escaped/base"); + request.Path = new PathString("/un?escaped"); + request.QueryString = new QueryString("?name=val%23ue"); + + Assert.Equal("http://my.xn--host-cpd:80/un%3Fescaped/base/un%3Fescaped?name=val%23ue", request.GetEncodedUrl()); + } + + [Theory] + [InlineData("/un?escaped/base")] + [InlineData(null)] + public void GetDisplayUrlFromRequest(string pathBase) + { + var request = new DefaultHttpContext().Request; + request.Scheme = "http"; + request.Host = new HostString("my.HoΨst:80"); + request.PathBase = new PathString(pathBase); + request.Path = new PathString("/un?escaped"); + request.QueryString = new QueryString("?name=val%23ue"); + + Assert.Equal("http://my.hoψst:80" + pathBase + "/un?escaped?name=val%23ue", request.GetDisplayUrl()); + } + + [Theory] + [InlineData("http://example.com", "http", "example.com", "", "", "")] + [InlineData("https://example.com", "https", "example.com", "", "", "")] + [InlineData("http://example.com/foo/bar", "http", "example.com", "/foo/bar", "", "")] + [InlineData("http://example.com/foo/bar?baz=1", "http", "example.com", "/foo/bar", "?baz=1", "")] + [InlineData("http://example.com/foo#col=2", "http", "example.com", "/foo", "", "#col=2")] + [InlineData("http://example.com/foo?bar=1#col=2", "http", "example.com", "/foo", "?bar=1", "#col=2")] + [InlineData("http://example.com?bar=1#col=2", "http", "example.com", "", "?bar=1", "#col=2")] + [InlineData("http://example.com#frag?stillfrag/stillfrag", "http", "example.com", "", "", "#frag?stillfrag/stillfrag")] + [InlineData("http://example.com?q/stillq#frag?stillfrag/stillfrag", "http", "example.com", "", "?q/stillq", "#frag?stillfrag/stillfrag")] + [InlineData("http://example.com/fo%23o#col=2", "http", "example.com", "/fo#o", "", "#col=2")] + [InlineData("http://example.com/fo%3Fo#col=2", "http", "example.com", "/fo?o", "", "#col=2")] + [InlineData("ftp://example.com/", "ftp", "example.com", "/", "", "")] + [InlineData("https://127.0.0.0:80/bar", "https", "127.0.0.0:80", "/bar", "", "")] + [InlineData("http://[1080:0:0:0:8:800:200C:417A]/index.html", "http", "[1080:0:0:0:8:800:200C:417A]", "/index.html", "", "")] + [InlineData("http://example.com///", "http", "example.com", "///", "", "")] + public void FromAbsoluteUriParsingChecks( + string uri, + string expectedScheme, + string expectedHost, + string expectedPath, + string expectedQuery, + string expectedFragment) + { + string scheme = null; + var host = new HostString(); + var path = new PathString(); + var query = new QueryString(); + var fragment = new FragmentString(); + UriHelper.FromAbsolute(uri, out scheme, out host, out path, out query, out fragment); + + Assert.Equal(scheme, expectedScheme); + Assert.Equal(host, new HostString(expectedHost)); + Assert.Equal(path, new PathString(expectedPath)); + Assert.Equal(query, new QueryString(expectedQuery)); + Assert.Equal(fragment, new FragmentString(expectedFragment)); + } + + [Fact] + public void FromAbsoluteToBuildAbsolute() + { + var scheme = "http"; + var host = new HostString("example.com"); + var path = new PathString("/index.html"); + var query = new QueryString("?foo=1"); + var fragment = new FragmentString("#col=1"); + var request = UriHelper.BuildAbsolute(scheme, host, path: path, query: query, fragment: fragment); + + string resScheme = null; + var resHost = new HostString(); + var resPath = new PathString(); + var resQuery = new QueryString(); + var resFragment = new FragmentString(); + UriHelper.FromAbsolute(request, out resScheme, out resHost, out resPath, out resQuery, out resFragment); + + Assert.Equal(scheme, resScheme); + Assert.Equal(host, resHost); + Assert.Equal(path, resPath); + Assert.Equal(query, resQuery); + Assert.Equal(fragment, resFragment); + } + + [Fact] + public void BuildAbsoluteNullInputThrowsArgumentNullException() + { + var resHost = new HostString(); + var resPath = new PathString(); + var resQuery = new QueryString(); + var resFragment = new FragmentString(); + Assert.Throws(() => UriHelper.BuildAbsolute(null, resHost, resPath, resPath, resQuery, resFragment)); + + } + + [Fact] + public void FromAbsoluteNullInputThrowsArgumentNullException() + { + string resScheme = null; + var resHost = new HostString(); + var resPath = new PathString(); + var resQuery = new QueryString(); + var resFragment = new FragmentString(); + Assert.Throws(() => UriHelper.FromAbsolute(null, out resScheme, out resHost, out resPath, out resQuery, out resFragment)); + } } diff --git a/src/Http/Http.Features/src/Authentication/IHttpAuthenticationFeature.cs b/src/Http/Http.Features/src/Authentication/IHttpAuthenticationFeature.cs index 55bd77898d..80b3ac93d9 100644 --- a/src/Http/Http.Features/src/Authentication/IHttpAuthenticationFeature.cs +++ b/src/Http/Http.Features/src/Authentication/IHttpAuthenticationFeature.cs @@ -3,16 +3,15 @@ using System.Security.Claims; -namespace Microsoft.AspNetCore.Http.Features.Authentication +namespace Microsoft.AspNetCore.Http.Features.Authentication; + +/// +/// The HTTP authentication feature. +/// +public interface IHttpAuthenticationFeature { /// - /// The HTTP authentication feature. + /// Gets or sets the associated with the HTTP request. /// - public interface IHttpAuthenticationFeature - { - /// - /// Gets or sets the associated with the HTTP request. - /// - ClaimsPrincipal? User { get; set; } - } + ClaimsPrincipal? User { get; set; } } diff --git a/src/Http/Http.Features/src/CookieOptions.cs b/src/Http/Http.Features/src/CookieOptions.cs index e663b54606..79d2dbc07c 100644 --- a/src/Http/Http.Features/src/CookieOptions.cs +++ b/src/Http/Http.Features/src/CookieOptions.cs @@ -3,67 +3,66 @@ using System; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Options used to create a new cookie. +/// +public class CookieOptions { /// - /// Options used to create a new cookie. + /// Creates a default cookie with a path of '/'. /// - public class CookieOptions + public CookieOptions() { - /// - /// Creates a default cookie with a path of '/'. - /// - public CookieOptions() - { - Path = "/"; - } + Path = "/"; + } - /// - /// Gets or sets the domain to associate the cookie with. - /// - /// The domain to associate the cookie with. - public string? Domain { get; set; } + /// + /// Gets or sets the domain to associate the cookie with. + /// + /// The domain to associate the cookie with. + public string? Domain { get; set; } - /// - /// Gets or sets the cookie path. - /// - /// The cookie path. - public string? Path { get; set; } + /// + /// Gets or sets the cookie path. + /// + /// The cookie path. + public string? Path { get; set; } - /// - /// Gets or sets the expiration date and time for the cookie. - /// - /// The expiration date and time for the cookie. - public DateTimeOffset? Expires { get; set; } + /// + /// Gets or sets the expiration date and time for the cookie. + /// + /// The expiration date and time for the cookie. + public DateTimeOffset? Expires { get; set; } - /// - /// Gets or sets a value that indicates whether to transmit the cookie using Secure Sockets Layer (SSL)--that is, over HTTPS only. - /// - /// true to transmit the cookie only over an SSL connection (HTTPS); otherwise, false. - public bool Secure { get; set; } + /// + /// Gets or sets a value that indicates whether to transmit the cookie using Secure Sockets Layer (SSL)--that is, over HTTPS only. + /// + /// true to transmit the cookie only over an SSL connection (HTTPS); otherwise, false. + public bool Secure { get; set; } - /// - /// Gets or sets the value for the SameSite attribute of the cookie. The default value is - /// - /// The representing the enforcement mode of the cookie. - public SameSiteMode SameSite { get; set; } = SameSiteMode.Unspecified; + /// + /// Gets or sets the value for the SameSite attribute of the cookie. The default value is + /// + /// The representing the enforcement mode of the cookie. + public SameSiteMode SameSite { get; set; } = SameSiteMode.Unspecified; - /// - /// Gets or sets a value that indicates whether a cookie is accessible by client-side script. - /// - /// true if a cookie must not be accessible by client-side script; otherwise, false. - public bool HttpOnly { get; set; } + /// + /// Gets or sets a value that indicates whether a cookie is accessible by client-side script. + /// + /// true if a cookie must not be accessible by client-side script; otherwise, false. + public bool HttpOnly { get; set; } - /// - /// Gets or sets the max-age for the cookie. - /// - /// The max-age date and time for the cookie. - public TimeSpan? MaxAge { get; set; } + /// + /// Gets or sets the max-age for the cookie. + /// + /// The max-age date and time for the cookie. + public TimeSpan? MaxAge { get; set; } - /// - /// Indicates if this cookie is essential for the application to function correctly. If true then - /// consent policy checks may be bypassed. The default value is false. - /// - public bool IsEssential { get; set; } - } + /// + /// Indicates if this cookie is essential for the application to function correctly. If true then + /// consent policy checks may be bypassed. The default value is false. + /// + public bool IsEssential { get; set; } } diff --git a/src/Http/Http.Features/src/HttpsCompressionMode.cs b/src/Http/Http.Features/src/HttpsCompressionMode.cs index e3932cacf4..b367478303 100644 --- a/src/Http/Http.Features/src/HttpsCompressionMode.cs +++ b/src/Http/Http.Features/src/HttpsCompressionMode.cs @@ -1,28 +1,27 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Use to dynamically control response compression for HTTPS requests. +/// +public enum HttpsCompressionMode { /// - /// Use to dynamically control response compression for HTTPS requests. + /// No value has been specified, use the configured defaults. /// - public enum HttpsCompressionMode - { - /// - /// No value has been specified, use the configured defaults. - /// - Default = 0, + Default = 0, - /// - /// Opts out of compression over HTTPS. Enabling compression on HTTPS requests for remotely manipulable content - /// may expose security problems. - /// - DoNotCompress, + /// + /// Opts out of compression over HTTPS. Enabling compression on HTTPS requests for remotely manipulable content + /// may expose security problems. + /// + DoNotCompress, - /// - /// Opts into compression over HTTPS. Enabling compression on HTTPS requests for remotely manipulable content - /// may expose security problems. - /// - Compress, - } + /// + /// Opts into compression over HTTPS. Enabling compression on HTTPS requests for remotely manipulable content + /// may expose security problems. + /// + Compress, } diff --git a/src/Http/Http.Features/src/IBadRequestExceptionFeature.cs b/src/Http/Http.Features/src/IBadRequestExceptionFeature.cs index 056696f182..b98206756d 100644 --- a/src/Http/Http.Features/src/IBadRequestExceptionFeature.cs +++ b/src/Http/Http.Features/src/IBadRequestExceptionFeature.cs @@ -1,16 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Provides information about rejected HTTP requests. +/// +public interface IBadRequestExceptionFeature { /// - /// Provides information about rejected HTTP requests. + /// Synchronously retrieves the exception associated with the rejected HTTP request. /// - public interface IBadRequestExceptionFeature - { - /// - /// Synchronously retrieves the exception associated with the rejected HTTP request. - /// - Exception? Error { get; } - } + Exception? Error { get; } } diff --git a/src/Http/Http.Features/src/IFormCollection.cs b/src/Http/Http.Features/src/IFormCollection.cs index 0d3d17264c..64254f83cd 100644 --- a/src/Http/Http.Features/src/IFormCollection.cs +++ b/src/Http/Http.Features/src/IFormCollection.cs @@ -4,91 +4,90 @@ using System.Collections.Generic; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Represents the parsed form values sent with the HttpRequest. +/// +public interface IFormCollection : IEnumerable> { /// - /// Represents the parsed form values sent with the HttpRequest. + /// Gets the number of elements contained in the . /// - public interface IFormCollection : IEnumerable> - { - /// - /// Gets the number of elements contained in the . - /// - /// - /// The number of elements contained in the . - /// - int Count { get; } + /// + /// The number of elements contained in the . + /// + int Count { get; } - /// - /// Gets an containing the keys of the - /// . - /// - /// - /// An containing the keys of the object - /// that implements . - /// - ICollection Keys { get; } + /// + /// Gets an containing the keys of the + /// . + /// + /// + /// An containing the keys of the object + /// that implements . + /// + ICollection Keys { get; } - /// - /// Determines whether the contains an element - /// with the specified key. - /// - /// - /// The key to locate in the . - /// - /// - /// true if the contains an element with - /// the key; otherwise, false. - /// - /// - /// key is null. - /// - bool ContainsKey(string key); + /// + /// Determines whether the contains an element + /// with the specified key. + /// + /// + /// The key to locate in the . + /// + /// + /// true if the contains an element with + /// the key; otherwise, false. + /// + /// + /// key is null. + /// + bool ContainsKey(string key); - /// - /// Gets the value associated with the specified key. - /// - /// - /// The key of the value to get. - /// - /// - /// The key of the value to get. - /// When this method returns, the value associated with the specified key, if the - /// key is found; otherwise, the default value for the type of the value parameter. - /// This parameter is passed uninitialized. - /// - /// - /// true if the object that implements contains - /// an element with the specified key; otherwise, false. - /// - /// - /// key is null. - /// - bool TryGetValue(string key, out StringValues value); + /// + /// Gets the value associated with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The key of the value to get. + /// When this method returns, the value associated with the specified key, if the + /// key is found; otherwise, the default value for the type of the value parameter. + /// This parameter is passed uninitialized. + /// + /// + /// true if the object that implements contains + /// an element with the specified key; otherwise, false. + /// + /// + /// key is null. + /// + bool TryGetValue(string key, out StringValues value); - /// - /// Gets the value with the specified key. - /// - /// - /// The key of the value to get. - /// - /// - /// The element with the specified key, or StringValues.Empty if the key is not present. - /// - /// - /// key is null. - /// - /// - /// has a different indexer contract than - /// , as it will return StringValues.Empty for missing entries - /// rather than throwing an Exception. - /// - StringValues this[string key] { get; } + /// + /// Gets the value with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The element with the specified key, or StringValues.Empty if the key is not present. + /// + /// + /// key is null. + /// + /// + /// has a different indexer contract than + /// , as it will return StringValues.Empty for missing entries + /// rather than throwing an Exception. + /// + StringValues this[string key] { get; } - /// - /// The file collection sent with the request. - /// - /// The files included with the request. - IFormFileCollection Files { get; } - } + /// + /// The file collection sent with the request. + /// + /// The files included with the request. + IFormFileCollection Files { get; } } diff --git a/src/Http/Http.Features/src/IFormFeature.cs b/src/Http/Http.Features/src/IFormFeature.cs index 8782c7470a..1df30f9534 100644 --- a/src/Http/Http.Features/src/IFormFeature.cs +++ b/src/Http/Http.Features/src/IFormFeature.cs @@ -4,44 +4,43 @@ using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Allows reading the request body as a HTTP form. +/// +public interface IFormFeature { /// - /// Allows reading the request body as a HTTP form. + /// Indicates if the request has a supported form content-type. /// - public interface IFormFeature - { - /// - /// Indicates if the request has a supported form content-type. - /// - bool HasFormContentType { get; } + bool HasFormContentType { get; } - /// - /// Gets or sets the parsed form. - /// - /// This API will return a non-null value if the - /// request body was read using or , or - /// if a value was explicitly assigned. - /// - /// - IFormCollection? Form { get; set; } + /// + /// Gets or sets the parsed form. + /// + /// This API will return a non-null value if the + /// request body was read using or , or + /// if a value was explicitly assigned. + /// + /// + IFormCollection? Form { get; set; } - /// - /// Parses the request body as a form. - /// - /// If the request body has not been previously read, this API performs a synchronous (blocking) read - /// on the HTTP input stream which may be unsupported or can adversely affect application performance. - /// Consider using instead. - /// - /// - /// The . - IFormCollection ReadForm(); + /// + /// Parses the request body as a form. + /// + /// If the request body has not been previously read, this API performs a synchronous (blocking) read + /// on the HTTP input stream which may be unsupported or can adversely affect application performance. + /// Consider using instead. + /// + /// + /// The . + IFormCollection ReadForm(); - /// - /// Parses the request body as a form. - /// - /// - /// - Task ReadFormAsync(CancellationToken cancellationToken); - } + /// + /// Parses the request body as a form. + /// + /// + /// + Task ReadFormAsync(CancellationToken cancellationToken); } diff --git a/src/Http/Http.Features/src/IFormFile.cs b/src/Http/Http.Features/src/IFormFile.cs index ef95e56e12..7df30b6846 100644 --- a/src/Http/Http.Features/src/IFormFile.cs +++ b/src/Http/Http.Features/src/IFormFile.cs @@ -5,59 +5,58 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Represents a file sent with the HttpRequest. +/// +public interface IFormFile { /// - /// Represents a file sent with the HttpRequest. - /// - public interface IFormFile - { - /// - /// Gets the raw Content-Type header of the uploaded file. - /// - string ContentType { get; } - - /// - /// Gets the raw Content-Disposition header of the uploaded file. - /// - string ContentDisposition { get; } - - /// - /// Gets the header dictionary of the uploaded file. - /// - IHeaderDictionary Headers { get; } - - /// - /// Gets the file length in bytes. - /// - long Length { get; } - - /// - /// Gets the form field name from the Content-Disposition header. - /// - string Name { get; } - - /// - /// Gets the file name from the Content-Disposition header. - /// - string FileName { get; } - - /// - /// Opens the request stream for reading the uploaded file. - /// - Stream OpenReadStream(); - - /// - /// Copies the contents of the uploaded file to the stream. - /// - /// The stream to copy the file contents to. - void CopyTo(Stream target); - - /// - /// Asynchronously copies the contents of the uploaded file to the stream. - /// - /// The stream to copy the file contents to. - /// - Task CopyToAsync(Stream target, CancellationToken cancellationToken = default(CancellationToken)); - } + /// Gets the raw Content-Type header of the uploaded file. + /// + string ContentType { get; } + + /// + /// Gets the raw Content-Disposition header of the uploaded file. + /// + string ContentDisposition { get; } + + /// + /// Gets the header dictionary of the uploaded file. + /// + IHeaderDictionary Headers { get; } + + /// + /// Gets the file length in bytes. + /// + long Length { get; } + + /// + /// Gets the form field name from the Content-Disposition header. + /// + string Name { get; } + + /// + /// Gets the file name from the Content-Disposition header. + /// + string FileName { get; } + + /// + /// Opens the request stream for reading the uploaded file. + /// + Stream OpenReadStream(); + + /// + /// Copies the contents of the uploaded file to the stream. + /// + /// The stream to copy the file contents to. + void CopyTo(Stream target); + + /// + /// Asynchronously copies the contents of the uploaded file to the stream. + /// + /// The stream to copy the file contents to. + /// + Task CopyToAsync(Stream target, CancellationToken cancellationToken = default(CancellationToken)); } diff --git a/src/Http/Http.Features/src/IFormFileCollection.cs b/src/Http/Http.Features/src/IFormFileCollection.cs index dd05b25921..25092b31a3 100644 --- a/src/Http/Http.Features/src/IFormFileCollection.cs +++ b/src/Http/Http.Features/src/IFormFileCollection.cs @@ -3,40 +3,39 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Represents the collection of files sent with the HttpRequest. +/// +public interface IFormFileCollection : IReadOnlyList { /// - /// Represents the collection of files sent with the HttpRequest. + /// Gets the first file with the specified name. /// - public interface IFormFileCollection : IReadOnlyList - { - /// - /// Gets the first file with the specified name. - /// - /// The name of the file to get. - /// - /// The requested file, or null if it is not present. - /// - IFormFile? this[string name] { get; } + /// The name of the file to get. + /// + /// The requested file, or null if it is not present. + /// + IFormFile? this[string name] { get; } - /// - /// Gets the first file with the specified name. - /// - /// The name of the file to get. - /// - /// The requested file, or null if it is not present. - /// - IFormFile? GetFile(string name); + /// + /// Gets the first file with the specified name. + /// + /// The name of the file to get. + /// + /// The requested file, or null if it is not present. + /// + IFormFile? GetFile(string name); - /// - /// Gets an containing the files of the - /// with the specified name. - /// - /// The name of the files to get. - /// - /// An containing the files of the object - /// that implements . - /// - IReadOnlyList GetFiles(string name); - } + /// + /// Gets an containing the files of the + /// with the specified name. + /// + /// The name of the files to get. + /// + /// An containing the files of the object + /// that implements . + /// + IReadOnlyList GetFiles(string name); } diff --git a/src/Http/Http.Features/src/IHeaderDictionary.Keyed.cs b/src/Http/Http.Features/src/IHeaderDictionary.Keyed.cs index 5455e2ad93..332eac40dd 100644 --- a/src/Http/Http.Features/src/IHeaderDictionary.Keyed.cs +++ b/src/Http/Http.Features/src/IHeaderDictionary.Keyed.cs @@ -3,275 +3,274 @@ using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +public partial interface IHeaderDictionary { - public partial interface IHeaderDictionary - { - /// Gets or sets the Accept HTTP header. - StringValues Accept { get => this[HeaderNames.Accept]; set => this[HeaderNames.Accept] = value; } + /// Gets or sets the Accept HTTP header. + StringValues Accept { get => this[HeaderNames.Accept]; set => this[HeaderNames.Accept] = value; } - /// Gets or sets the Accept-Charset HTTP header. - StringValues AcceptCharset { get => this[HeaderNames.AcceptCharset]; set => this[HeaderNames.AcceptCharset] = value; } + /// Gets or sets the Accept-Charset HTTP header. + StringValues AcceptCharset { get => this[HeaderNames.AcceptCharset]; set => this[HeaderNames.AcceptCharset] = value; } - /// Gets or sets the Accept-Encoding HTTP header. - StringValues AcceptEncoding { get => this[HeaderNames.AcceptEncoding]; set => this[HeaderNames.AcceptEncoding] = value; } + /// Gets or sets the Accept-Encoding HTTP header. + StringValues AcceptEncoding { get => this[HeaderNames.AcceptEncoding]; set => this[HeaderNames.AcceptEncoding] = value; } - /// Gets or sets the Accept-Language HTTP header. - StringValues AcceptLanguage { get => this[HeaderNames.AcceptLanguage]; set => this[HeaderNames.AcceptLanguage] = value; } + /// Gets or sets the Accept-Language HTTP header. + StringValues AcceptLanguage { get => this[HeaderNames.AcceptLanguage]; set => this[HeaderNames.AcceptLanguage] = value; } - /// Gets or sets the Accept-Ranges HTTP header. - StringValues AcceptRanges { get => this[HeaderNames.AcceptRanges]; set => this[HeaderNames.AcceptRanges] = value; } + /// Gets or sets the Accept-Ranges HTTP header. + StringValues AcceptRanges { get => this[HeaderNames.AcceptRanges]; set => this[HeaderNames.AcceptRanges] = value; } - /// Gets or sets the Access-Control-Allow-Credentials HTTP header. - StringValues AccessControlAllowCredentials { get => this[HeaderNames.AccessControlAllowCredentials]; set => this[HeaderNames.AccessControlAllowCredentials] = value; } + /// Gets or sets the Access-Control-Allow-Credentials HTTP header. + StringValues AccessControlAllowCredentials { get => this[HeaderNames.AccessControlAllowCredentials]; set => this[HeaderNames.AccessControlAllowCredentials] = value; } - /// Gets or sets the Access-Control-Allow-Headers HTTP header. - StringValues AccessControlAllowHeaders { get => this[HeaderNames.AccessControlAllowHeaders]; set => this[HeaderNames.AccessControlAllowHeaders] = value; } + /// Gets or sets the Access-Control-Allow-Headers HTTP header. + StringValues AccessControlAllowHeaders { get => this[HeaderNames.AccessControlAllowHeaders]; set => this[HeaderNames.AccessControlAllowHeaders] = value; } - /// Gets or sets the Access-Control-Allow-Methods HTTP header. - StringValues AccessControlAllowMethods { get => this[HeaderNames.AccessControlAllowMethods]; set => this[HeaderNames.AccessControlAllowMethods] = value; } + /// Gets or sets the Access-Control-Allow-Methods HTTP header. + StringValues AccessControlAllowMethods { get => this[HeaderNames.AccessControlAllowMethods]; set => this[HeaderNames.AccessControlAllowMethods] = value; } - /// Gets or sets the Access-Control-Allow-Origin HTTP header. - StringValues AccessControlAllowOrigin { get => this[HeaderNames.AccessControlAllowOrigin]; set => this[HeaderNames.AccessControlAllowOrigin] = value; } + /// Gets or sets the Access-Control-Allow-Origin HTTP header. + StringValues AccessControlAllowOrigin { get => this[HeaderNames.AccessControlAllowOrigin]; set => this[HeaderNames.AccessControlAllowOrigin] = value; } - /// Gets or sets the Access-Control-Expose-Headers HTTP header. - StringValues AccessControlExposeHeaders { get => this[HeaderNames.AccessControlExposeHeaders]; set => this[HeaderNames.AccessControlExposeHeaders] = value; } + /// Gets or sets the Access-Control-Expose-Headers HTTP header. + StringValues AccessControlExposeHeaders { get => this[HeaderNames.AccessControlExposeHeaders]; set => this[HeaderNames.AccessControlExposeHeaders] = value; } - /// Gets or sets the Access-Control-Max-Age HTTP header. - StringValues AccessControlMaxAge { get => this[HeaderNames.AccessControlMaxAge]; set => this[HeaderNames.AccessControlMaxAge] = value; } + /// Gets or sets the Access-Control-Max-Age HTTP header. + StringValues AccessControlMaxAge { get => this[HeaderNames.AccessControlMaxAge]; set => this[HeaderNames.AccessControlMaxAge] = value; } - /// Gets or sets the Access-Control-Request-Headers HTTP header. - StringValues AccessControlRequestHeaders { get => this[HeaderNames.AccessControlRequestHeaders]; set => this[HeaderNames.AccessControlRequestHeaders] = value; } + /// Gets or sets the Access-Control-Request-Headers HTTP header. + StringValues AccessControlRequestHeaders { get => this[HeaderNames.AccessControlRequestHeaders]; set => this[HeaderNames.AccessControlRequestHeaders] = value; } - /// Gets or sets the Access-Control-Request-Method HTTP header. - StringValues AccessControlRequestMethod { get => this[HeaderNames.AccessControlRequestMethod]; set => this[HeaderNames.AccessControlRequestMethod] = value; } + /// Gets or sets the Access-Control-Request-Method HTTP header. + StringValues AccessControlRequestMethod { get => this[HeaderNames.AccessControlRequestMethod]; set => this[HeaderNames.AccessControlRequestMethod] = value; } - /// Gets or sets the Age HTTP header. - StringValues Age { get => this[HeaderNames.Age]; set => this[HeaderNames.Age] = value; } + /// Gets or sets the Age HTTP header. + StringValues Age { get => this[HeaderNames.Age]; set => this[HeaderNames.Age] = value; } - /// Gets or sets the Allow HTTP header. - StringValues Allow { get => this[HeaderNames.Allow]; set => this[HeaderNames.Allow] = value; } + /// Gets or sets the Allow HTTP header. + StringValues Allow { get => this[HeaderNames.Allow]; set => this[HeaderNames.Allow] = value; } - /// Gets or sets the Alt-Svc HTTP header. - StringValues AltSvc { get => this[HeaderNames.AltSvc]; set => this[HeaderNames.AltSvc] = value; } + /// Gets or sets the Alt-Svc HTTP header. + StringValues AltSvc { get => this[HeaderNames.AltSvc]; set => this[HeaderNames.AltSvc] = value; } - /// Gets or sets the Authorization HTTP header. - StringValues Authorization { get => this[HeaderNames.Authorization]; set => this[HeaderNames.Authorization] = value; } + /// Gets or sets the Authorization HTTP header. + StringValues Authorization { get => this[HeaderNames.Authorization]; set => this[HeaderNames.Authorization] = value; } - /// Gets or sets the baggage HTTP header. - StringValues Baggage { get => this[HeaderNames.Baggage]; set => this[HeaderNames.Baggage] = value; } + /// Gets or sets the baggage HTTP header. + StringValues Baggage { get => this[HeaderNames.Baggage]; set => this[HeaderNames.Baggage] = value; } - /// Gets or sets the Cache-Control HTTP header. - StringValues CacheControl { get => this[HeaderNames.CacheControl]; set => this[HeaderNames.CacheControl] = value; } + /// Gets or sets the Cache-Control HTTP header. + StringValues CacheControl { get => this[HeaderNames.CacheControl]; set => this[HeaderNames.CacheControl] = value; } - /// Gets or sets the Connection HTTP header. - StringValues Connection { get => this[HeaderNames.Connection]; set => this[HeaderNames.Connection] = value; } + /// Gets or sets the Connection HTTP header. + StringValues Connection { get => this[HeaderNames.Connection]; set => this[HeaderNames.Connection] = value; } - /// Gets or sets the Content-Disposition HTTP header. - StringValues ContentDisposition { get => this[HeaderNames.ContentDisposition]; set => this[HeaderNames.ContentDisposition] = value; } + /// Gets or sets the Content-Disposition HTTP header. + StringValues ContentDisposition { get => this[HeaderNames.ContentDisposition]; set => this[HeaderNames.ContentDisposition] = value; } - /// Gets or sets the Content-Encoding HTTP header. - StringValues ContentEncoding { get => this[HeaderNames.ContentEncoding]; set => this[HeaderNames.ContentEncoding] = value; } + /// Gets or sets the Content-Encoding HTTP header. + StringValues ContentEncoding { get => this[HeaderNames.ContentEncoding]; set => this[HeaderNames.ContentEncoding] = value; } - /// Gets or sets the Content-Language HTTP header. - StringValues ContentLanguage { get => this[HeaderNames.ContentLanguage]; set => this[HeaderNames.ContentLanguage] = value; } + /// Gets or sets the Content-Language HTTP header. + StringValues ContentLanguage { get => this[HeaderNames.ContentLanguage]; set => this[HeaderNames.ContentLanguage] = value; } - /// Gets or sets the Content-Location HTTP header. - StringValues ContentLocation { get => this[HeaderNames.ContentLocation]; set => this[HeaderNames.ContentLocation] = value; } + /// Gets or sets the Content-Location HTTP header. + StringValues ContentLocation { get => this[HeaderNames.ContentLocation]; set => this[HeaderNames.ContentLocation] = value; } - /// Gets or sets the Content-MD5 HTTP header. - StringValues ContentMD5 { get => this[HeaderNames.ContentMD5]; set => this[HeaderNames.ContentMD5] = value; } + /// Gets or sets the Content-MD5 HTTP header. + StringValues ContentMD5 { get => this[HeaderNames.ContentMD5]; set => this[HeaderNames.ContentMD5] = value; } - /// Gets or sets the Content-Range HTTP header. - StringValues ContentRange { get => this[HeaderNames.ContentRange]; set => this[HeaderNames.ContentRange] = value; } + /// Gets or sets the Content-Range HTTP header. + StringValues ContentRange { get => this[HeaderNames.ContentRange]; set => this[HeaderNames.ContentRange] = value; } - /// Gets or sets the Content-Security-Policy HTTP header. - StringValues ContentSecurityPolicy { get => this[HeaderNames.ContentSecurityPolicy]; set => this[HeaderNames.ContentSecurityPolicy] = value; } + /// Gets or sets the Content-Security-Policy HTTP header. + StringValues ContentSecurityPolicy { get => this[HeaderNames.ContentSecurityPolicy]; set => this[HeaderNames.ContentSecurityPolicy] = value; } - /// Gets or sets the Content-Security-Policy-Report-Only HTTP header. - StringValues ContentSecurityPolicyReportOnly { get => this[HeaderNames.ContentSecurityPolicyReportOnly]; set => this[HeaderNames.ContentSecurityPolicyReportOnly] = value; } + /// Gets or sets the Content-Security-Policy-Report-Only HTTP header. + StringValues ContentSecurityPolicyReportOnly { get => this[HeaderNames.ContentSecurityPolicyReportOnly]; set => this[HeaderNames.ContentSecurityPolicyReportOnly] = value; } - /// Gets or sets the Content-Type HTTP header. - StringValues ContentType { get => this[HeaderNames.ContentType]; set => this[HeaderNames.ContentType] = value; } + /// Gets or sets the Content-Type HTTP header. + StringValues ContentType { get => this[HeaderNames.ContentType]; set => this[HeaderNames.ContentType] = value; } - /// Gets or sets the Correlation-Context HTTP header. - StringValues CorrelationContext { get => this[HeaderNames.CorrelationContext]; set => this[HeaderNames.CorrelationContext] = value; } + /// Gets or sets the Correlation-Context HTTP header. + StringValues CorrelationContext { get => this[HeaderNames.CorrelationContext]; set => this[HeaderNames.CorrelationContext] = value; } - /// Gets or sets the Cookie HTTP header. - StringValues Cookie { get => this[HeaderNames.Cookie]; set => this[HeaderNames.Cookie] = value; } + /// Gets or sets the Cookie HTTP header. + StringValues Cookie { get => this[HeaderNames.Cookie]; set => this[HeaderNames.Cookie] = value; } - /// Gets or sets the Date HTTP header. - StringValues Date { get => this[HeaderNames.Date]; set => this[HeaderNames.Date] = value; } + /// Gets or sets the Date HTTP header. + StringValues Date { get => this[HeaderNames.Date]; set => this[HeaderNames.Date] = value; } - /// Gets or sets the ETag HTTP header. - StringValues ETag { get => this[HeaderNames.ETag]; set => this[HeaderNames.ETag] = value; } + /// Gets or sets the ETag HTTP header. + StringValues ETag { get => this[HeaderNames.ETag]; set => this[HeaderNames.ETag] = value; } - /// Gets or sets the Expires HTTP header. - StringValues Expires { get => this[HeaderNames.Expires]; set => this[HeaderNames.Expires] = value; } + /// Gets or sets the Expires HTTP header. + StringValues Expires { get => this[HeaderNames.Expires]; set => this[HeaderNames.Expires] = value; } - /// Gets or sets the Expect HTTP header. - StringValues Expect { get => this[HeaderNames.Expect]; set => this[HeaderNames.Expect] = value; } + /// Gets or sets the Expect HTTP header. + StringValues Expect { get => this[HeaderNames.Expect]; set => this[HeaderNames.Expect] = value; } - /// Gets or sets the From HTTP header. - StringValues From { get => this[HeaderNames.From]; set => this[HeaderNames.From] = value; } + /// Gets or sets the From HTTP header. + StringValues From { get => this[HeaderNames.From]; set => this[HeaderNames.From] = value; } - /// Gets or sets the Grpc-Accept-Encoding HTTP header. - StringValues GrpcAcceptEncoding { get => this[HeaderNames.GrpcAcceptEncoding]; set => this[HeaderNames.GrpcAcceptEncoding] = value; } + /// Gets or sets the Grpc-Accept-Encoding HTTP header. + StringValues GrpcAcceptEncoding { get => this[HeaderNames.GrpcAcceptEncoding]; set => this[HeaderNames.GrpcAcceptEncoding] = value; } - /// Gets or sets the Grpc-Encoding HTTP header. - StringValues GrpcEncoding { get => this[HeaderNames.GrpcEncoding]; set => this[HeaderNames.GrpcEncoding] = value; } + /// Gets or sets the Grpc-Encoding HTTP header. + StringValues GrpcEncoding { get => this[HeaderNames.GrpcEncoding]; set => this[HeaderNames.GrpcEncoding] = value; } - /// Gets or sets the Grpc-Message HTTP header. - StringValues GrpcMessage { get => this[HeaderNames.GrpcMessage]; set => this[HeaderNames.GrpcMessage] = value; } + /// Gets or sets the Grpc-Message HTTP header. + StringValues GrpcMessage { get => this[HeaderNames.GrpcMessage]; set => this[HeaderNames.GrpcMessage] = value; } - /// Gets or sets the Grpc-Status HTTP header. - StringValues GrpcStatus { get => this[HeaderNames.GrpcStatus]; set => this[HeaderNames.GrpcStatus] = value; } + /// Gets or sets the Grpc-Status HTTP header. + StringValues GrpcStatus { get => this[HeaderNames.GrpcStatus]; set => this[HeaderNames.GrpcStatus] = value; } - /// Gets or sets the Grpc-Timeout HTTP header. - StringValues GrpcTimeout { get => this[HeaderNames.GrpcTimeout]; set => this[HeaderNames.GrpcTimeout] = value; } + /// Gets or sets the Grpc-Timeout HTTP header. + StringValues GrpcTimeout { get => this[HeaderNames.GrpcTimeout]; set => this[HeaderNames.GrpcTimeout] = value; } - /// Gets or sets the Host HTTP header. - StringValues Host { get => this[HeaderNames.Host]; set => this[HeaderNames.Host] = value; } + /// Gets or sets the Host HTTP header. + StringValues Host { get => this[HeaderNames.Host]; set => this[HeaderNames.Host] = value; } - /// Gets or sets the Keep-Alive HTTP header. - StringValues KeepAlive { get => this[HeaderNames.KeepAlive]; set => this[HeaderNames.KeepAlive] = value; } + /// Gets or sets the Keep-Alive HTTP header. + StringValues KeepAlive { get => this[HeaderNames.KeepAlive]; set => this[HeaderNames.KeepAlive] = value; } - /// Gets or sets the If-Match HTTP header. - StringValues IfMatch { get => this[HeaderNames.IfMatch]; set => this[HeaderNames.IfMatch] = value; } + /// Gets or sets the If-Match HTTP header. + StringValues IfMatch { get => this[HeaderNames.IfMatch]; set => this[HeaderNames.IfMatch] = value; } - /// Gets or sets the If-Modified-Since HTTP header. - StringValues IfModifiedSince { get => this[HeaderNames.IfModifiedSince]; set => this[HeaderNames.IfModifiedSince] = value; } + /// Gets or sets the If-Modified-Since HTTP header. + StringValues IfModifiedSince { get => this[HeaderNames.IfModifiedSince]; set => this[HeaderNames.IfModifiedSince] = value; } - /// Gets or sets the If-None-Match HTTP header. - StringValues IfNoneMatch { get => this[HeaderNames.IfNoneMatch]; set => this[HeaderNames.IfNoneMatch] = value; } + /// Gets or sets the If-None-Match HTTP header. + StringValues IfNoneMatch { get => this[HeaderNames.IfNoneMatch]; set => this[HeaderNames.IfNoneMatch] = value; } - /// Gets or sets the If-Range HTTP header. - StringValues IfRange { get => this[HeaderNames.IfRange]; set => this[HeaderNames.IfRange] = value; } + /// Gets or sets the If-Range HTTP header. + StringValues IfRange { get => this[HeaderNames.IfRange]; set => this[HeaderNames.IfRange] = value; } - /// Gets or sets the If-Unmodified-Since HTTP header. - StringValues IfUnmodifiedSince { get => this[HeaderNames.IfUnmodifiedSince]; set => this[HeaderNames.IfUnmodifiedSince] = value; } + /// Gets or sets the If-Unmodified-Since HTTP header. + StringValues IfUnmodifiedSince { get => this[HeaderNames.IfUnmodifiedSince]; set => this[HeaderNames.IfUnmodifiedSince] = value; } - /// Gets or sets the Last-Modified HTTP header. - StringValues LastModified { get => this[HeaderNames.LastModified]; set => this[HeaderNames.LastModified] = value; } + /// Gets or sets the Last-Modified HTTP header. + StringValues LastModified { get => this[HeaderNames.LastModified]; set => this[HeaderNames.LastModified] = value; } - /// Gets or sets the Link HTTP header. - StringValues Link { get => this[HeaderNames.Link]; set => this[HeaderNames.Link] = value; } + /// Gets or sets the Link HTTP header. + StringValues Link { get => this[HeaderNames.Link]; set => this[HeaderNames.Link] = value; } - /// Gets or sets the Location HTTP header. - StringValues Location { get => this[HeaderNames.Location]; set => this[HeaderNames.Location] = value; } + /// Gets or sets the Location HTTP header. + StringValues Location { get => this[HeaderNames.Location]; set => this[HeaderNames.Location] = value; } - /// Gets or sets the Max-Forwards HTTP header. - StringValues MaxForwards { get => this[HeaderNames.MaxForwards]; set => this[HeaderNames.MaxForwards] = value; } + /// Gets or sets the Max-Forwards HTTP header. + StringValues MaxForwards { get => this[HeaderNames.MaxForwards]; set => this[HeaderNames.MaxForwards] = value; } - /// Gets or sets the Origin HTTP header. - StringValues Origin { get => this[HeaderNames.Origin]; set => this[HeaderNames.Origin] = value; } + /// Gets or sets the Origin HTTP header. + StringValues Origin { get => this[HeaderNames.Origin]; set => this[HeaderNames.Origin] = value; } - /// Gets or sets the Pragma HTTP header. - StringValues Pragma { get => this[HeaderNames.Pragma]; set => this[HeaderNames.Pragma] = value; } + /// Gets or sets the Pragma HTTP header. + StringValues Pragma { get => this[HeaderNames.Pragma]; set => this[HeaderNames.Pragma] = value; } - /// Gets or sets the Proxy-Authenticate HTTP header. - StringValues ProxyAuthenticate { get => this[HeaderNames.ProxyAuthenticate]; set => this[HeaderNames.ProxyAuthenticate] = value; } + /// Gets or sets the Proxy-Authenticate HTTP header. + StringValues ProxyAuthenticate { get => this[HeaderNames.ProxyAuthenticate]; set => this[HeaderNames.ProxyAuthenticate] = value; } - /// Gets or sets the Proxy-Authorization HTTP header. - StringValues ProxyAuthorization { get => this[HeaderNames.ProxyAuthorization]; set => this[HeaderNames.ProxyAuthorization] = value; } + /// Gets or sets the Proxy-Authorization HTTP header. + StringValues ProxyAuthorization { get => this[HeaderNames.ProxyAuthorization]; set => this[HeaderNames.ProxyAuthorization] = value; } - /// Gets or sets the Proxy-Connection HTTP header. - StringValues ProxyConnection { get => this[HeaderNames.ProxyConnection]; set => this[HeaderNames.ProxyConnection] = value; } + /// Gets or sets the Proxy-Connection HTTP header. + StringValues ProxyConnection { get => this[HeaderNames.ProxyConnection]; set => this[HeaderNames.ProxyConnection] = value; } - /// Gets or sets the Range HTTP header. - StringValues Range { get => this[HeaderNames.Range]; set => this[HeaderNames.Range] = value; } + /// Gets or sets the Range HTTP header. + StringValues Range { get => this[HeaderNames.Range]; set => this[HeaderNames.Range] = value; } - /// Gets or sets the Referer HTTP header. - StringValues Referer { get => this[HeaderNames.Referer]; set => this[HeaderNames.Referer] = value; } + /// Gets or sets the Referer HTTP header. + StringValues Referer { get => this[HeaderNames.Referer]; set => this[HeaderNames.Referer] = value; } - /// Gets or sets the Retry-After HTTP header. - StringValues RetryAfter { get => this[HeaderNames.RetryAfter]; set => this[HeaderNames.RetryAfter] = value; } + /// Gets or sets the Retry-After HTTP header. + StringValues RetryAfter { get => this[HeaderNames.RetryAfter]; set => this[HeaderNames.RetryAfter] = value; } - /// Gets or sets the Request-Id HTTP header. - StringValues RequestId { get => this[HeaderNames.RequestId]; set => this[HeaderNames.RequestId] = value; } + /// Gets or sets the Request-Id HTTP header. + StringValues RequestId { get => this[HeaderNames.RequestId]; set => this[HeaderNames.RequestId] = value; } - /// Gets or sets the Sec-WebSocket-Accept HTTP header. - StringValues SecWebSocketAccept { get => this[HeaderNames.SecWebSocketAccept]; set => this[HeaderNames.SecWebSocketAccept] = value; } + /// Gets or sets the Sec-WebSocket-Accept HTTP header. + StringValues SecWebSocketAccept { get => this[HeaderNames.SecWebSocketAccept]; set => this[HeaderNames.SecWebSocketAccept] = value; } - /// Gets or sets the Sec-WebSocket-Key HTTP header. - StringValues SecWebSocketKey { get => this[HeaderNames.SecWebSocketKey]; set => this[HeaderNames.SecWebSocketKey] = value; } + /// Gets or sets the Sec-WebSocket-Key HTTP header. + StringValues SecWebSocketKey { get => this[HeaderNames.SecWebSocketKey]; set => this[HeaderNames.SecWebSocketKey] = value; } - /// Gets or sets the Sec-WebSocket-Protocol HTTP header. - StringValues SecWebSocketProtocol { get => this[HeaderNames.SecWebSocketProtocol]; set => this[HeaderNames.SecWebSocketProtocol] = value; } + /// Gets or sets the Sec-WebSocket-Protocol HTTP header. + StringValues SecWebSocketProtocol { get => this[HeaderNames.SecWebSocketProtocol]; set => this[HeaderNames.SecWebSocketProtocol] = value; } - /// Gets or sets the Sec-WebSocket-Version HTTP header. - StringValues SecWebSocketVersion { get => this[HeaderNames.SecWebSocketVersion]; set => this[HeaderNames.SecWebSocketVersion] = value; } + /// Gets or sets the Sec-WebSocket-Version HTTP header. + StringValues SecWebSocketVersion { get => this[HeaderNames.SecWebSocketVersion]; set => this[HeaderNames.SecWebSocketVersion] = value; } - /// Gets or sets the Sec-WebSocket-Extensions HTTP header. - StringValues SecWebSocketExtensions { get => this[HeaderNames.SecWebSocketExtensions]; set => this[HeaderNames.SecWebSocketExtensions] = value; } + /// Gets or sets the Sec-WebSocket-Extensions HTTP header. + StringValues SecWebSocketExtensions { get => this[HeaderNames.SecWebSocketExtensions]; set => this[HeaderNames.SecWebSocketExtensions] = value; } - /// Gets or sets the Server HTTP header. - StringValues Server { get => this[HeaderNames.Server]; set => this[HeaderNames.Server] = value; } + /// Gets or sets the Server HTTP header. + StringValues Server { get => this[HeaderNames.Server]; set => this[HeaderNames.Server] = value; } - /// Gets or sets the Set-Cookie HTTP header. - StringValues SetCookie { get => this[HeaderNames.SetCookie]; set => this[HeaderNames.SetCookie] = value; } + /// Gets or sets the Set-Cookie HTTP header. + StringValues SetCookie { get => this[HeaderNames.SetCookie]; set => this[HeaderNames.SetCookie] = value; } - /// Gets or sets the Strict-Transport-Security HTTP header. - StringValues StrictTransportSecurity { get => this[HeaderNames.StrictTransportSecurity]; set => this[HeaderNames.StrictTransportSecurity] = value; } + /// Gets or sets the Strict-Transport-Security HTTP header. + StringValues StrictTransportSecurity { get => this[HeaderNames.StrictTransportSecurity]; set => this[HeaderNames.StrictTransportSecurity] = value; } - /// Gets or sets the TE HTTP header. - StringValues TE { get => this[HeaderNames.TE]; set => this[HeaderNames.TE] = value; } + /// Gets or sets the TE HTTP header. + StringValues TE { get => this[HeaderNames.TE]; set => this[HeaderNames.TE] = value; } - /// Gets or sets the Trailer HTTP header. - StringValues Trailer { get => this[HeaderNames.Trailer]; set => this[HeaderNames.Trailer] = value; } + /// Gets or sets the Trailer HTTP header. + StringValues Trailer { get => this[HeaderNames.Trailer]; set => this[HeaderNames.Trailer] = value; } - /// Gets or sets the Transfer-Encoding HTTP header. - StringValues TransferEncoding { get => this[HeaderNames.TransferEncoding]; set => this[HeaderNames.TransferEncoding] = value; } + /// Gets or sets the Transfer-Encoding HTTP header. + StringValues TransferEncoding { get => this[HeaderNames.TransferEncoding]; set => this[HeaderNames.TransferEncoding] = value; } - /// Gets or sets the Translate HTTP header. - StringValues Translate { get => this[HeaderNames.Translate]; set => this[HeaderNames.Translate] = value; } + /// Gets or sets the Translate HTTP header. + StringValues Translate { get => this[HeaderNames.Translate]; set => this[HeaderNames.Translate] = value; } - /// Gets or sets the traceparent HTTP header. - StringValues TraceParent { get => this[HeaderNames.TraceParent]; set => this[HeaderNames.TraceParent] = value; } + /// Gets or sets the traceparent HTTP header. + StringValues TraceParent { get => this[HeaderNames.TraceParent]; set => this[HeaderNames.TraceParent] = value; } - /// Gets or sets the tracestate HTTP header. - StringValues TraceState { get => this[HeaderNames.TraceState]; set => this[HeaderNames.TraceState] = value; } + /// Gets or sets the tracestate HTTP header. + StringValues TraceState { get => this[HeaderNames.TraceState]; set => this[HeaderNames.TraceState] = value; } - /// Gets or sets the Upgrade HTTP header. - StringValues Upgrade { get => this[HeaderNames.Upgrade]; set => this[HeaderNames.Upgrade] = value; } + /// Gets or sets the Upgrade HTTP header. + StringValues Upgrade { get => this[HeaderNames.Upgrade]; set => this[HeaderNames.Upgrade] = value; } - /// Gets or sets the Upgrade-Insecure-Requests HTTP header. - StringValues UpgradeInsecureRequests { get => this[HeaderNames.UpgradeInsecureRequests]; set => this[HeaderNames.UpgradeInsecureRequests] = value; } + /// Gets or sets the Upgrade-Insecure-Requests HTTP header. + StringValues UpgradeInsecureRequests { get => this[HeaderNames.UpgradeInsecureRequests]; set => this[HeaderNames.UpgradeInsecureRequests] = value; } - /// Gets or sets the User-Agent HTTP header. - StringValues UserAgent { get => this[HeaderNames.UserAgent]; set => this[HeaderNames.UserAgent] = value; } + /// Gets or sets the User-Agent HTTP header. + StringValues UserAgent { get => this[HeaderNames.UserAgent]; set => this[HeaderNames.UserAgent] = value; } - /// Gets or sets the Vary HTTP header. - StringValues Vary { get => this[HeaderNames.Vary]; set => this[HeaderNames.Vary] = value; } + /// Gets or sets the Vary HTTP header. + StringValues Vary { get => this[HeaderNames.Vary]; set => this[HeaderNames.Vary] = value; } - /// Gets or sets the Via HTTP header. - StringValues Via { get => this[HeaderNames.Via]; set => this[HeaderNames.Via] = value; } + /// Gets or sets the Via HTTP header. + StringValues Via { get => this[HeaderNames.Via]; set => this[HeaderNames.Via] = value; } - /// Gets or sets the Warning HTTP header. - StringValues Warning { get => this[HeaderNames.Warning]; set => this[HeaderNames.Warning] = value; } + /// Gets or sets the Warning HTTP header. + StringValues Warning { get => this[HeaderNames.Warning]; set => this[HeaderNames.Warning] = value; } - /// Gets or sets the Sec-WebSocket-Protocol HTTP header. - StringValues WebSocketSubProtocols { get => this[HeaderNames.WebSocketSubProtocols]; set => this[HeaderNames.WebSocketSubProtocols] = value; } + /// Gets or sets the Sec-WebSocket-Protocol HTTP header. + StringValues WebSocketSubProtocols { get => this[HeaderNames.WebSocketSubProtocols]; set => this[HeaderNames.WebSocketSubProtocols] = value; } - /// Gets or sets the WWW-Authenticate HTTP header. - StringValues WWWAuthenticate { get => this[HeaderNames.WWWAuthenticate]; set => this[HeaderNames.WWWAuthenticate] = value; } + /// Gets or sets the WWW-Authenticate HTTP header. + StringValues WWWAuthenticate { get => this[HeaderNames.WWWAuthenticate]; set => this[HeaderNames.WWWAuthenticate] = value; } - /// Gets or sets the X-Content-Type-Options HTTP header. - StringValues XContentTypeOptions { get => this[HeaderNames.XContentTypeOptions]; set => this[HeaderNames.XContentTypeOptions] = value; } + /// Gets or sets the X-Content-Type-Options HTTP header. + StringValues XContentTypeOptions { get => this[HeaderNames.XContentTypeOptions]; set => this[HeaderNames.XContentTypeOptions] = value; } - /// Gets or sets the X-Frame-Options HTTP header. - StringValues XFrameOptions { get => this[HeaderNames.XFrameOptions]; set => this[HeaderNames.XFrameOptions] = value; } + /// Gets or sets the X-Frame-Options HTTP header. + StringValues XFrameOptions { get => this[HeaderNames.XFrameOptions]; set => this[HeaderNames.XFrameOptions] = value; } - /// Gets or sets the X-Powered-By HTTP header. - StringValues XPoweredBy { get => this[HeaderNames.XPoweredBy]; set => this[HeaderNames.XPoweredBy] = value; } + /// Gets or sets the X-Powered-By HTTP header. + StringValues XPoweredBy { get => this[HeaderNames.XPoweredBy]; set => this[HeaderNames.XPoweredBy] = value; } - /// Gets or sets the X-Requested-With HTTP header. - StringValues XRequestedWith { get => this[HeaderNames.XRequestedWith]; set => this[HeaderNames.XRequestedWith] = value; } + /// Gets or sets the X-Requested-With HTTP header. + StringValues XRequestedWith { get => this[HeaderNames.XRequestedWith]; set => this[HeaderNames.XRequestedWith] = value; } - /// Gets or sets the X-UA-Compatible HTTP header. - StringValues XUACompatible { get => this[HeaderNames.XUACompatible]; set => this[HeaderNames.XUACompatible] = value; } + /// Gets or sets the X-UA-Compatible HTTP header. + StringValues XUACompatible { get => this[HeaderNames.XUACompatible]; set => this[HeaderNames.XUACompatible] = value; } - /// Gets or sets the X-XSS-Protection HTTP header. - StringValues XXSSProtection { get => this[HeaderNames.XXSSProtection]; set => this[HeaderNames.XXSSProtection] = value; } - } + /// Gets or sets the X-XSS-Protection HTTP header. + StringValues XXSSProtection { get => this[HeaderNames.XXSSProtection]; set => this[HeaderNames.XXSSProtection] = value; } } diff --git a/src/Http/Http.Features/src/IHeaderDictionary.cs b/src/Http/Http.Features/src/IHeaderDictionary.cs index 8bf46a1052..6e5d3703d4 100644 --- a/src/Http/Http.Features/src/IHeaderDictionary.cs +++ b/src/Http/Http.Features/src/IHeaderDictionary.cs @@ -4,23 +4,22 @@ using System.Collections.Generic; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Represents HttpRequest and HttpResponse headers +/// +public partial interface IHeaderDictionary : IDictionary { /// - /// Represents HttpRequest and HttpResponse headers + /// IHeaderDictionary has a different indexer contract than IDictionary, where it will return StringValues.Empty for missing entries. /// - public partial interface IHeaderDictionary : IDictionary - { - /// - /// IHeaderDictionary has a different indexer contract than IDictionary, where it will return StringValues.Empty for missing entries. - /// - /// - /// The stored value, or StringValues.Empty if the key is not present. - new StringValues this[string key] { get; set; } + /// + /// The stored value, or StringValues.Empty if the key is not present. + new StringValues this[string key] { get; set; } - /// - /// Strongly typed access to the Content-Length header. Implementations must keep this in sync with the string representation. - /// - long? ContentLength { get; set; } - } + /// + /// Strongly typed access to the Content-Length header. Implementations must keep this in sync with the string representation. + /// + long? ContentLength { get; set; } } diff --git a/src/Http/Http.Features/src/IHttpBodyControlFeature.cs b/src/Http/Http.Features/src/IHttpBodyControlFeature.cs index c000317789..01ee7904c2 100644 --- a/src/Http/Http.Features/src/IHttpBodyControlFeature.cs +++ b/src/Http/Http.Features/src/IHttpBodyControlFeature.cs @@ -1,16 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Controls the IO behavior for the and +/// +public interface IHttpBodyControlFeature { /// - /// Controls the IO behavior for the and + /// Gets or sets a value that controls whether synchronous IO is allowed for the and /// - public interface IHttpBodyControlFeature - { - /// - /// Gets or sets a value that controls whether synchronous IO is allowed for the and - /// - bool AllowSynchronousIO { get; set; } - } + bool AllowSynchronousIO { get; set; } } diff --git a/src/Http/Http.Features/src/IHttpConnectionFeature.cs b/src/Http/Http.Features/src/IHttpConnectionFeature.cs index 2a260df3ee..fca9a367c8 100644 --- a/src/Http/Http.Features/src/IHttpConnectionFeature.cs +++ b/src/Http/Http.Features/src/IHttpConnectionFeature.cs @@ -3,36 +3,35 @@ using System.Net; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Information regarding the TCP/IP connection carrying the request. +/// +public interface IHttpConnectionFeature { /// - /// Information regarding the TCP/IP connection carrying the request. + /// Gets or sets the unique identifier for the connection the request was received on. This is primarily for diagnostic purposes. /// - public interface IHttpConnectionFeature - { - /// - /// Gets or sets the unique identifier for the connection the request was received on. This is primarily for diagnostic purposes. - /// - string ConnectionId { get; set; } + string ConnectionId { get; set; } - /// - /// Gets or sets the IPAddress of the client making the request. Note this may be for a proxy rather than the end user. - /// - IPAddress? RemoteIpAddress { get; set; } + /// + /// Gets or sets the IPAddress of the client making the request. Note this may be for a proxy rather than the end user. + /// + IPAddress? RemoteIpAddress { get; set; } - /// - /// Gets or sets the local IPAddress on which the request was received. - /// - IPAddress? LocalIpAddress { get; set; } + /// + /// Gets or sets the local IPAddress on which the request was received. + /// + IPAddress? LocalIpAddress { get; set; } - /// - /// Gets or sets the remote port of the client making the request. - /// - int RemotePort { get; set; } + /// + /// Gets or sets the remote port of the client making the request. + /// + int RemotePort { get; set; } - /// - /// Gets or sets the local port on which the request was received. - /// - int LocalPort { get; set; } - } + /// + /// Gets or sets the local port on which the request was received. + /// + int LocalPort { get; set; } } diff --git a/src/Http/Http.Features/src/IHttpMaxRequestBodySizeFeature.cs b/src/Http/Http.Features/src/IHttpMaxRequestBodySizeFeature.cs index 2f0af2db5d..541cf8a6ec 100644 --- a/src/Http/Http.Features/src/IHttpMaxRequestBodySizeFeature.cs +++ b/src/Http/Http.Features/src/IHttpMaxRequestBodySizeFeature.cs @@ -1,29 +1,28 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Feature to inspect and modify the maximum request body size for a single request. +/// +public interface IHttpMaxRequestBodySizeFeature { /// - /// Feature to inspect and modify the maximum request body size for a single request. + /// Indicates whether is read-only. + /// If true, this could mean that the request body has already been read from + /// or that was called. /// - public interface IHttpMaxRequestBodySizeFeature - { - /// - /// Indicates whether is read-only. - /// If true, this could mean that the request body has already been read from - /// or that was called. - /// - bool IsReadOnly { get; } + bool IsReadOnly { get; } - /// - /// The maximum allowed size of the current request body in bytes. - /// When set to null, the maximum request body size is unlimited. - /// This cannot be modified after the reading the request body has started. - /// This limit does not affect upgraded connections which are always unlimited. - /// - /// - /// Defaults to the server's global max request body size limit. - /// - long? MaxRequestBodySize { get; set; } - } -} \ No newline at end of file + /// + /// The maximum allowed size of the current request body in bytes. + /// When set to null, the maximum request body size is unlimited. + /// This cannot be modified after the reading the request body has started. + /// This limit does not affect upgraded connections which are always unlimited. + /// + /// + /// Defaults to the server's global max request body size limit. + /// + long? MaxRequestBodySize { get; set; } +} diff --git a/src/Http/Http.Features/src/IHttpRequestBodyDetectionFeature.cs b/src/Http/Http.Features/src/IHttpRequestBodyDetectionFeature.cs index 15eed71722..7d9d64c3fd 100644 --- a/src/Http/Http.Features/src/IHttpRequestBodyDetectionFeature.cs +++ b/src/Http/Http.Features/src/IHttpRequestBodyDetectionFeature.cs @@ -1,29 +1,28 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Used to indicate if the request can have a body. +/// +public interface IHttpRequestBodyDetectionFeature { /// - /// Used to indicate if the request can have a body. + /// Indicates if the request can have a body. /// - public interface IHttpRequestBodyDetectionFeature - { - /// - /// Indicates if the request can have a body. - /// - /// - /// This returns true when: - /// - It's an HTTP/1.x request with a non-zero Content-Length or a 'Transfer-Encoding: chunked' header. - /// - It's an HTTP/2 request that did not set the END_STREAM flag on the initial headers frame. - /// The final request body length may still be zero for the chunked or HTTP/2 scenarios. - /// - /// This returns false when: - /// - It's an HTTP/1.x request with no Content-Length or 'Transfer-Encoding: chunked' header, or the Content-Length is 0. - /// - It's an HTTP/1.x request with Connection: Upgrade (e.g. WebSockets). There is no HTTP request body for these requests and - /// no data should be received until after the upgrade. - /// - It's an HTTP/2 request that set END_STREAM on the initial headers frame. - /// When false, the request body should never return data. - /// - bool CanHaveBody { get; } - } + /// + /// This returns true when: + /// - It's an HTTP/1.x request with a non-zero Content-Length or a 'Transfer-Encoding: chunked' header. + /// - It's an HTTP/2 request that did not set the END_STREAM flag on the initial headers frame. + /// The final request body length may still be zero for the chunked or HTTP/2 scenarios. + /// + /// This returns false when: + /// - It's an HTTP/1.x request with no Content-Length or 'Transfer-Encoding: chunked' header, or the Content-Length is 0. + /// - It's an HTTP/1.x request with Connection: Upgrade (e.g. WebSockets). There is no HTTP request body for these requests and + /// no data should be received until after the upgrade. + /// - It's an HTTP/2 request that set END_STREAM on the initial headers frame. + /// When false, the request body should never return data. + /// + bool CanHaveBody { get; } } diff --git a/src/Http/Http.Features/src/IHttpRequestFeature.cs b/src/Http/Http.Features/src/IHttpRequestFeature.cs index 7f2e93baba..9a014bc7f5 100644 --- a/src/Http/Http.Features/src/IHttpRequestFeature.cs +++ b/src/Http/Http.Features/src/IHttpRequestFeature.cs @@ -3,90 +3,89 @@ using System.IO; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Contains the details of a given request. These properties should all be mutable. +/// None of these properties should ever be set to null. +/// +public interface IHttpRequestFeature { /// - /// Contains the details of a given request. These properties should all be mutable. - /// None of these properties should ever be set to null. + /// Gets or set the HTTP-version as defined in RFC 7230. E.g. "HTTP/1.1" /// - public interface IHttpRequestFeature - { - /// - /// Gets or set the HTTP-version as defined in RFC 7230. E.g. "HTTP/1.1" - /// - string Protocol { get; set; } + string Protocol { get; set; } - /// - /// Gets or set the request uri scheme. E.g. "http" or "https". - /// - /// Note this value is not included in the original request, - /// it is inferred by checking if the transport used a TLS - /// connection or not. - /// - /// - string Scheme { get; set; } + /// + /// Gets or set the request uri scheme. E.g. "http" or "https". + /// + /// Note this value is not included in the original request, + /// it is inferred by checking if the transport used a TLS + /// connection or not. + /// + /// + string Scheme { get; set; } - /// - /// Gets or sets the request method as defined in RFC 7230. E.g. "GET", "HEAD", "POST", etc.. - /// - string Method { get; set; } + /// + /// Gets or sets the request method as defined in RFC 7230. E.g. "GET", "HEAD", "POST", etc.. + /// + string Method { get; set; } - /// - /// Gets or sets the first portion of the request path associated with application root. - /// - /// The value is un-escaped. The value may be . - /// - /// - string PathBase { get; set; } + /// + /// Gets or sets the first portion of the request path associated with application root. + /// + /// The value is un-escaped. The value may be . + /// + /// + string PathBase { get; set; } - /// - /// Gets or sets the portion of the request path that identifies the requested resource. - /// - /// The value is un-escaped. The value may be if contains the - /// full path. - /// - /// - string Path { get; set; } + /// + /// Gets or sets the portion of the request path that identifies the requested resource. + /// + /// The value is un-escaped. The value may be if contains the + /// full path. + /// + /// + string Path { get; set; } - /// - /// Gets or sets the query portion of the request-target as defined in RFC 7230. The value - /// may be . If not empty then the leading '?' will be included. The value - /// is in its original form, without un-escaping. - /// - string QueryString { get; set; } + /// + /// Gets or sets the query portion of the request-target as defined in RFC 7230. The value + /// may be . If not empty then the leading '?' will be included. The value + /// is in its original form, without un-escaping. + /// + string QueryString { get; set; } - /// - /// Gets or sets the request target as it was sent in the HTTP request. - /// - /// This property contains the raw path and full query, as well as other request targets - /// such as * for OPTIONS requests (https://tools.ietf.org/html/rfc7230#section-5.3). - /// - /// - /// - /// This property is not used internally for routing or authorization decisions. It has not - /// been UrlDecoded and care should be taken in its use. - /// - string RawTarget { get; set; } + /// + /// Gets or sets the request target as it was sent in the HTTP request. + /// + /// This property contains the raw path and full query, as well as other request targets + /// such as * for OPTIONS requests (https://tools.ietf.org/html/rfc7230#section-5.3). + /// + /// + /// + /// This property is not used internally for routing or authorization decisions. It has not + /// been UrlDecoded and care should be taken in its use. + /// + string RawTarget { get; set; } - /// - /// Gets or sets headers included in the request, aggregated by header name. - /// - /// The values are not split or merged across header lines. E.g. The following headers: - /// - /// HeaderA: value1, value2 - /// HeaderA: value3 - /// - /// Result in Headers["HeaderA"] = { "value1, value2", "value3" } - /// - /// - IHeaderDictionary Headers { get; set; } + /// + /// Gets or sets headers included in the request, aggregated by header name. + /// + /// The values are not split or merged across header lines. E.g. The following headers: + /// + /// HeaderA: value1, value2 + /// HeaderA: value3 + /// + /// Result in Headers["HeaderA"] = { "value1, value2", "value3" } + /// + /// + IHeaderDictionary Headers { get; set; } - /// - /// Gets or sets a representing the request body, if any. - /// - /// may be used to represent an empty request body. - /// - /// - Stream Body { get; set; } - } + /// + /// Gets or sets a representing the request body, if any. + /// + /// may be used to represent an empty request body. + /// + /// + Stream Body { get; set; } } diff --git a/src/Http/Http.Features/src/IHttpRequestIdentifierFeature.cs b/src/Http/Http.Features/src/IHttpRequestIdentifierFeature.cs index 34464b2b1a..76dffaa236 100644 --- a/src/Http/Http.Features/src/IHttpRequestIdentifierFeature.cs +++ b/src/Http/Http.Features/src/IHttpRequestIdentifierFeature.cs @@ -1,17 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Feature to uniquely identify a request. +/// +public interface IHttpRequestIdentifierFeature { /// - /// Feature to uniquely identify a request. + /// Gets or sets a value to uniquely identify a request. + /// This can be used for logging and diagnostics. /// - public interface IHttpRequestIdentifierFeature - { - /// - /// Gets or sets a value to uniquely identify a request. - /// This can be used for logging and diagnostics. - /// - string TraceIdentifier { get; set; } - } + string TraceIdentifier { get; set; } } diff --git a/src/Http/Http.Features/src/IHttpRequestLifetimeFeature.cs b/src/Http/Http.Features/src/IHttpRequestLifetimeFeature.cs index 263b68b021..a8b80c5cd1 100644 --- a/src/Http/Http.Features/src/IHttpRequestLifetimeFeature.cs +++ b/src/Http/Http.Features/src/IHttpRequestLifetimeFeature.cs @@ -3,24 +3,23 @@ using System.Threading; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Provides access to the HTTP request lifetime operations. +/// +public interface IHttpRequestLifetimeFeature { /// - /// Provides access to the HTTP request lifetime operations. + /// A that fires if the request is aborted and + /// the application should cease processing. The token will not fire if the request + /// completes successfully. /// - public interface IHttpRequestLifetimeFeature - { - /// - /// A that fires if the request is aborted and - /// the application should cease processing. The token will not fire if the request - /// completes successfully. - /// - CancellationToken RequestAborted { get; set; } + CancellationToken RequestAborted { get; set; } - /// - /// Forcefully aborts the request if it has not already completed. This will result in - /// RequestAborted being triggered. - /// - void Abort(); - } + /// + /// Forcefully aborts the request if it has not already completed. This will result in + /// RequestAborted being triggered. + /// + void Abort(); } diff --git a/src/Http/Http.Features/src/IHttpRequestTrailersFeature.cs b/src/Http/Http.Features/src/IHttpRequestTrailersFeature.cs index daf65eddbc..063f63a2f2 100644 --- a/src/Http/Http.Features/src/IHttpRequestTrailersFeature.cs +++ b/src/Http/Http.Features/src/IHttpRequestTrailersFeature.cs @@ -3,24 +3,23 @@ using System; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// This feature exposes HTTP request trailer headers, either for HTTP/1.1 chunked bodies or HTTP/2 trailing headers. +/// +public interface IHttpRequestTrailersFeature { /// - /// This feature exposes HTTP request trailer headers, either for HTTP/1.1 chunked bodies or HTTP/2 trailing headers. + /// Indicates if the are available yet. They may not be available until the + /// request body is fully read. /// - public interface IHttpRequestTrailersFeature - { - /// - /// Indicates if the are available yet. They may not be available until the - /// request body is fully read. - /// - bool Available { get; } + bool Available { get; } - /// - /// The trailing headers received. This will throw if - /// returns false. They may not be available until the request body is fully read. If there are no trailers this will - /// return an empty collection. - /// - IHeaderDictionary Trailers { get; } - } + /// + /// The trailing headers received. This will throw if + /// returns false. They may not be available until the request body is fully read. If there are no trailers this will + /// return an empty collection. + /// + IHeaderDictionary Trailers { get; } } diff --git a/src/Http/Http.Features/src/IHttpResetFeature.cs b/src/Http/Http.Features/src/IHttpResetFeature.cs index 8e3bfe2df8..a11b7cd4e6 100644 --- a/src/Http/Http.Features/src/IHttpResetFeature.cs +++ b/src/Http/Http.Features/src/IHttpResetFeature.cs @@ -1,17 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Used to send reset messages for protocols that support them such as HTTP/2 or HTTP/3. +/// +public interface IHttpResetFeature { /// - /// Used to send reset messages for protocols that support them such as HTTP/2 or HTTP/3. + /// Send a reset message with the given error code. The request will be considered aborted. /// - public interface IHttpResetFeature - { - /// - /// Send a reset message with the given error code. The request will be considered aborted. - /// - /// The error code to send in the reset message. - void Reset(int errorCode); - } + /// The error code to send in the reset message. + void Reset(int errorCode); } diff --git a/src/Http/Http.Features/src/IHttpResponseBodyFeature.cs b/src/Http/Http.Features/src/IHttpResponseBodyFeature.cs index 3c0f9370de..df6eacf8f2 100644 --- a/src/Http/Http.Features/src/IHttpResponseBodyFeature.cs +++ b/src/Http/Http.Features/src/IHttpResponseBodyFeature.cs @@ -6,48 +6,47 @@ using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// An aggregate of the different ways to interact with the response body. +/// +public interface IHttpResponseBodyFeature { /// - /// An aggregate of the different ways to interact with the response body. + /// The for writing the response body. + /// + Stream Stream { get; } + + /// + /// A representing the response body, if any. + /// + PipeWriter Writer { get; } + + /// + /// Opts out of write buffering for the response. + /// + void DisableBuffering(); + + /// + /// Starts the response by calling OnStarting() and making headers unmodifiable. + /// + Task StartAsync(CancellationToken cancellationToken = default); + + /// + /// Sends the requested file in the response body. A response may include multiple writes. + /// + /// The full disk path to the file. + /// The offset in the file to start at. + /// The number of bytes to send, or null to send the remainder of the file. + /// A used to abort the transmission. + /// + Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default); + + /// + /// Flush any remaining response headers, data, or trailers. + /// This may throw if the response is in an invalid state such as a Content-Length mismatch. /// - public interface IHttpResponseBodyFeature - { - /// - /// The for writing the response body. - /// - Stream Stream { get; } - - /// - /// A representing the response body, if any. - /// - PipeWriter Writer { get; } - - /// - /// Opts out of write buffering for the response. - /// - void DisableBuffering(); - - /// - /// Starts the response by calling OnStarting() and making headers unmodifiable. - /// - Task StartAsync(CancellationToken cancellationToken = default); - - /// - /// Sends the requested file in the response body. A response may include multiple writes. - /// - /// The full disk path to the file. - /// The offset in the file to start at. - /// The number of bytes to send, or null to send the remainder of the file. - /// A used to abort the transmission. - /// - Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken = default); - - /// - /// Flush any remaining response headers, data, or trailers. - /// This may throw if the response is in an invalid state such as a Content-Length mismatch. - /// - /// - Task CompleteAsync(); - } + /// + Task CompleteAsync(); } diff --git a/src/Http/Http.Features/src/IHttpResponseFeature.cs b/src/Http/Http.Features/src/IHttpResponseFeature.cs index dc759f6106..2ca35fd491 100644 --- a/src/Http/Http.Features/src/IHttpResponseFeature.cs +++ b/src/Http/Http.Features/src/IHttpResponseFeature.cs @@ -5,62 +5,61 @@ using System; using System.IO; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Represents the fields and state of an HTTP response. +/// +public interface IHttpResponseFeature { /// - /// Represents the fields and state of an HTTP response. + /// Gets or sets the status-code as defined in RFC 7230. /// - public interface IHttpResponseFeature - { - /// - /// Gets or sets the status-code as defined in RFC 7230. - /// - /// Defaults to 200. - int StatusCode { get; set; } + /// Defaults to 200. + int StatusCode { get; set; } - /// - /// Gets or sets the reason-phrase as defined in RFC 7230. Note this field is no longer supported by HTTP/2. - /// - string? ReasonPhrase { get; set; } + /// + /// Gets or sets the reason-phrase as defined in RFC 7230. Note this field is no longer supported by HTTP/2. + /// + string? ReasonPhrase { get; set; } - /// - /// Gets or sets the response headers to send. Headers with multiple values will be emitted as multiple headers. - /// - IHeaderDictionary Headers { get; set; } + /// + /// Gets or sets the response headers to send. Headers with multiple values will be emitted as multiple headers. + /// + IHeaderDictionary Headers { get; set; } - /// - /// Gets or sets the for writing the response body. - /// - [Obsolete("Use IHttpResponseBodyFeature.Stream instead.", error: false)] - Stream Body { get; set; } + /// + /// Gets or sets the for writing the response body. + /// + [Obsolete("Use IHttpResponseBodyFeature.Stream instead.", error: false)] + Stream Body { get; set; } - /// - /// Gets a value that indicates if the response has started. - /// - /// If , the , - /// , and are now immutable, and - /// should no longer be called. - /// - /// - bool HasStarted { get; } + /// + /// Gets a value that indicates if the response has started. + /// + /// If , the , + /// , and are now immutable, and + /// should no longer be called. + /// + /// + bool HasStarted { get; } - /// - /// Registers a callback to be invoked just before the response starts. - /// - /// This is the last chance to modify the , , or - /// . - /// - /// - /// The callback to invoke when starting the response. - /// The state to pass into the callback. - void OnStarting(Func callback, object state); + /// + /// Registers a callback to be invoked just before the response starts. + /// + /// This is the last chance to modify the , , or + /// . + /// + /// + /// The callback to invoke when starting the response. + /// The state to pass into the callback. + void OnStarting(Func callback, object state); - /// - /// Registers a callback to be invoked after a response has fully completed. This is - /// intended for resource cleanup. - /// - /// The callback to invoke after the response has completed. - /// The state to pass into the callback. - void OnCompleted(Func callback, object state); - } + /// + /// Registers a callback to be invoked after a response has fully completed. This is + /// intended for resource cleanup. + /// + /// The callback to invoke after the response has completed. + /// The state to pass into the callback. + void OnCompleted(Func callback, object state); } diff --git a/src/Http/Http.Features/src/IHttpResponseTrailersFeature.cs b/src/Http/Http.Features/src/IHttpResponseTrailersFeature.cs index c1d97bcbb0..9526516b7b 100644 --- a/src/Http/Http.Features/src/IHttpResponseTrailersFeature.cs +++ b/src/Http/Http.Features/src/IHttpResponseTrailersFeature.cs @@ -1,20 +1,19 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Provides access to response trailers. +/// +/// Response trailers allow for additional headers to be sent at the end of an HTTP/1.1 (chunked) or HTTP/2 response. +/// For more details, see RFC7230. +/// +/// +public interface IHttpResponseTrailersFeature { /// - /// Provides access to response trailers. - /// - /// Response trailers allow for additional headers to be sent at the end of an HTTP/1.1 (chunked) or HTTP/2 response. - /// For more details, see RFC7230. - /// + /// Gets or sets the trailer headers. /// - public interface IHttpResponseTrailersFeature - { - /// - /// Gets or sets the trailer headers. - /// - IHeaderDictionary Trailers { get; set; } - } + IHeaderDictionary Trailers { get; set; } } diff --git a/src/Http/Http.Features/src/IHttpUpgradeFeature.cs b/src/Http/Http.Features/src/IHttpUpgradeFeature.cs index b1476d6bd3..a3c77ccef1 100644 --- a/src/Http/Http.Features/src/IHttpUpgradeFeature.cs +++ b/src/Http/Http.Features/src/IHttpUpgradeFeature.cs @@ -4,24 +4,23 @@ using System.IO; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Provides access to server upgrade features. +/// +public interface IHttpUpgradeFeature { /// - /// Provides access to server upgrade features. + /// Indicates if the server can upgrade this request to an opaque, bidirectional stream. /// - public interface IHttpUpgradeFeature - { - /// - /// Indicates if the server can upgrade this request to an opaque, bidirectional stream. - /// - bool IsUpgradableRequest { get; } + bool IsUpgradableRequest { get; } - /// - /// Attempt to upgrade the request to an opaque, bidirectional stream. The response status code - /// and headers need to be set before this is invoked. Check - /// before invoking. - /// - /// - Task UpgradeAsync(); - } + /// + /// Attempt to upgrade the request to an opaque, bidirectional stream. The response status code + /// and headers need to be set before this is invoked. Check + /// before invoking. + /// + /// + Task UpgradeAsync(); } diff --git a/src/Http/Http.Features/src/IHttpWebSocketFeature.cs b/src/Http/Http.Features/src/IHttpWebSocketFeature.cs index 68a394449d..dc1404ae95 100644 --- a/src/Http/Http.Features/src/IHttpWebSocketFeature.cs +++ b/src/Http/Http.Features/src/IHttpWebSocketFeature.cs @@ -4,24 +4,23 @@ using System.Net.WebSockets; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Provides access to server websocket features. +/// +public interface IHttpWebSocketFeature { /// - /// Provides access to server websocket features. + /// Indicates if this is a WebSocket upgrade request. /// - public interface IHttpWebSocketFeature - { - /// - /// Indicates if this is a WebSocket upgrade request. - /// - bool IsWebSocketRequest { get; } + bool IsWebSocketRequest { get; } - /// - /// Attempts to upgrade the request to a . Check - /// before invoking this. - /// - /// The . - /// A . - Task AcceptAsync(WebSocketAcceptContext context); - } + /// + /// Attempts to upgrade the request to a . Check + /// before invoking this. + /// + /// The . + /// A . + Task AcceptAsync(WebSocketAcceptContext context); } diff --git a/src/Http/Http.Features/src/IHttpsCompressionFeature.cs b/src/Http/Http.Features/src/IHttpsCompressionFeature.cs index 73dd2f5942..38e010cd65 100644 --- a/src/Http/Http.Features/src/IHttpsCompressionFeature.cs +++ b/src/Http/Http.Features/src/IHttpsCompressionFeature.cs @@ -1,16 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Configures response compression behavior for HTTPS on a per-request basis. +/// +public interface IHttpsCompressionFeature { /// - /// Configures response compression behavior for HTTPS on a per-request basis. + /// The to use. /// - public interface IHttpsCompressionFeature - { - /// - /// The to use. - /// - HttpsCompressionMode Mode { get; set; } - } + HttpsCompressionMode Mode { get; set; } } diff --git a/src/Http/Http.Features/src/IItemsFeature.cs b/src/Http/Http.Features/src/IItemsFeature.cs index ef760c6503..b4807660bd 100644 --- a/src/Http/Http.Features/src/IItemsFeature.cs +++ b/src/Http/Http.Features/src/IItemsFeature.cs @@ -3,16 +3,15 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Provides a key/value collection that can be used to share data within the scope of this request. +/// +public interface IItemsFeature { /// - /// Provides a key/value collection that can be used to share data within the scope of this request. + /// Gets or sets a a key/value collection that can be used to share data within the scope of this request. /// - public interface IItemsFeature - { - /// - /// Gets or sets a a key/value collection that can be used to share data within the scope of this request. - /// - IDictionary Items { get; set; } - } + IDictionary Items { get; set; } } diff --git a/src/Http/Http.Features/src/IQueryCollection.cs b/src/Http/Http.Features/src/IQueryCollection.cs index 47dd570570..3ae7786327 100644 --- a/src/Http/Http.Features/src/IQueryCollection.cs +++ b/src/Http/Http.Features/src/IQueryCollection.cs @@ -4,85 +4,84 @@ using System.Collections.Generic; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Represents the HttpRequest query string collection +/// +public interface IQueryCollection : IEnumerable> { /// - /// Represents the HttpRequest query string collection + /// Gets the number of elements contained in the . /// - public interface IQueryCollection : IEnumerable> - { - /// - /// Gets the number of elements contained in the . - /// - /// - /// The number of elements contained in the . - /// - int Count { get; } + /// + /// The number of elements contained in the . + /// + int Count { get; } - /// - /// Gets an containing the keys of the - /// . - /// - /// - /// An containing the keys of the object - /// that implements . - /// - ICollection Keys { get; } + /// + /// Gets an containing the keys of the + /// . + /// + /// + /// An containing the keys of the object + /// that implements . + /// + ICollection Keys { get; } - /// - /// Determines whether the contains an element - /// with the specified key. - /// - /// - /// The key to locate in the . - /// - /// - /// true if the contains an element with - /// the key; otherwise, false. - /// - /// - /// key is null. - /// - bool ContainsKey(string key); + /// + /// Determines whether the contains an element + /// with the specified key. + /// + /// + /// The key to locate in the . + /// + /// + /// true if the contains an element with + /// the key; otherwise, false. + /// + /// + /// key is null. + /// + bool ContainsKey(string key); - /// - /// Gets the value associated with the specified key. - /// - /// - /// The key of the value to get. - /// - /// - /// The key of the value to get. - /// When this method returns, the value associated with the specified key, if the - /// key is found; otherwise, the default value for the type of the value parameter. - /// This parameter is passed uninitialized. - /// - /// - /// true if the object that implements contains - /// an element with the specified key; otherwise, false. - /// - /// - /// key is null. - /// - bool TryGetValue(string key, out StringValues value); + /// + /// Gets the value associated with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The key of the value to get. + /// When this method returns, the value associated with the specified key, if the + /// key is found; otherwise, the default value for the type of the value parameter. + /// This parameter is passed uninitialized. + /// + /// + /// true if the object that implements contains + /// an element with the specified key; otherwise, false. + /// + /// + /// key is null. + /// + bool TryGetValue(string key, out StringValues value); - /// - /// Gets the value with the specified key. - /// - /// - /// The key of the value to get. - /// - /// - /// The element with the specified key, or StringValues.Empty if the key is not present. - /// - /// - /// key is null. - /// - /// - /// has a different indexer contract than - /// , as it will return StringValues.Empty for missing entries - /// rather than throwing an Exception. - /// - StringValues this[string key] { get; } - } + /// + /// Gets the value with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The element with the specified key, or StringValues.Empty if the key is not present. + /// + /// + /// key is null. + /// + /// + /// has a different indexer contract than + /// , as it will return StringValues.Empty for missing entries + /// rather than throwing an Exception. + /// + StringValues this[string key] { get; } } diff --git a/src/Http/Http.Features/src/IQueryFeature.cs b/src/Http/Http.Features/src/IQueryFeature.cs index 9b9eb96eac..c2186fc5a5 100644 --- a/src/Http/Http.Features/src/IQueryFeature.cs +++ b/src/Http/Http.Features/src/IQueryFeature.cs @@ -1,16 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Provides access to the associated with the HTTP request. +/// +public interface IQueryFeature { /// - /// Provides access to the associated with the HTTP request. + /// Gets or sets the . /// - public interface IQueryFeature - { - /// - /// Gets or sets the . - /// - IQueryCollection Query { get; set; } - } + IQueryCollection Query { get; set; } } diff --git a/src/Http/Http.Features/src/IRequestBodyPipeFeature.cs b/src/Http/Http.Features/src/IRequestBodyPipeFeature.cs index 506f15e8b5..471fed84d0 100644 --- a/src/Http/Http.Features/src/IRequestBodyPipeFeature.cs +++ b/src/Http/Http.Features/src/IRequestBodyPipeFeature.cs @@ -3,16 +3,15 @@ using System.IO.Pipelines; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Represents the HTTP request body as a . +/// +public interface IRequestBodyPipeFeature { /// - /// Represents the HTTP request body as a . + /// Gets a representing the request body, if any. /// - public interface IRequestBodyPipeFeature - { - /// - /// Gets a representing the request body, if any. - /// - PipeReader Reader { get; } - } + PipeReader Reader { get; } } diff --git a/src/Http/Http.Features/src/IRequestCookieCollection.cs b/src/Http/Http.Features/src/IRequestCookieCollection.cs index 9e77610a33..c9590acb76 100644 --- a/src/Http/Http.Features/src/IRequestCookieCollection.cs +++ b/src/Http/Http.Features/src/IRequestCookieCollection.cs @@ -4,85 +4,84 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Represents the HttpRequest cookie collection +/// +public interface IRequestCookieCollection : IEnumerable> { /// - /// Represents the HttpRequest cookie collection + /// Gets the number of elements contained in the . /// - public interface IRequestCookieCollection : IEnumerable> - { - /// - /// Gets the number of elements contained in the . - /// - /// - /// The number of elements contained in the . - /// - int Count { get; } + /// + /// The number of elements contained in the . + /// + int Count { get; } - /// - /// Gets an containing the keys of the - /// . - /// - /// - /// An containing the keys of the object - /// that implements . - /// - ICollection Keys { get; } + /// + /// Gets an containing the keys of the + /// . + /// + /// + /// An containing the keys of the object + /// that implements . + /// + ICollection Keys { get; } - /// - /// Determines whether the contains an element - /// with the specified key. - /// - /// - /// The key to locate in the . - /// - /// - /// true if the contains an element with - /// the key; otherwise, false. - /// - /// - /// key is null. - /// - bool ContainsKey(string key); + /// + /// Determines whether the contains an element + /// with the specified key. + /// + /// + /// The key to locate in the . + /// + /// + /// true if the contains an element with + /// the key; otherwise, false. + /// + /// + /// key is null. + /// + bool ContainsKey(string key); - /// - /// Gets the value associated with the specified key. - /// - /// - /// The key of the value to get. - /// - /// - /// The key of the value to get. - /// When this method returns, the value associated with the specified key, if the - /// key is found; otherwise, the default value for the type of the value parameter. - /// This parameter is passed uninitialized. - /// - /// - /// true if the object that implements contains - /// an element with the specified key; otherwise, false. - /// - /// - /// key is null. - /// - bool TryGetValue(string key, [MaybeNullWhen(false)] out string? value); + /// + /// Gets the value associated with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The key of the value to get. + /// When this method returns, the value associated with the specified key, if the + /// key is found; otherwise, the default value for the type of the value parameter. + /// This parameter is passed uninitialized. + /// + /// + /// true if the object that implements contains + /// an element with the specified key; otherwise, false. + /// + /// + /// key is null. + /// + bool TryGetValue(string key, [MaybeNullWhen(false)] out string? value); - /// - /// Gets the value with the specified key. - /// - /// - /// The key of the value to get. - /// - /// - /// The element with the specified key, or null if the key is not present. - /// - /// - /// key is null. - /// - /// - /// has a different indexer contract than - /// , as it will return null for missing entries - /// rather than throwing an Exception. - /// - string? this[string key] { get; } - } + /// + /// Gets the value with the specified key. + /// + /// + /// The key of the value to get. + /// + /// + /// The element with the specified key, or null if the key is not present. + /// + /// + /// key is null. + /// + /// + /// has a different indexer contract than + /// , as it will return null for missing entries + /// rather than throwing an Exception. + /// + string? this[string key] { get; } } diff --git a/src/Http/Http.Features/src/IRequestCookiesFeature.cs b/src/Http/Http.Features/src/IRequestCookiesFeature.cs index b4afd39db2..86143bad0b 100644 --- a/src/Http/Http.Features/src/IRequestCookiesFeature.cs +++ b/src/Http/Http.Features/src/IRequestCookiesFeature.cs @@ -1,16 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Provides access to request cookie collection. +/// +public interface IRequestCookiesFeature { /// - /// Provides access to request cookie collection. + /// Gets or sets the request cookies. /// - public interface IRequestCookiesFeature - { - /// - /// Gets or sets the request cookies. - /// - IRequestCookieCollection Cookies { get; set; } - } + IRequestCookieCollection Cookies { get; set; } } diff --git a/src/Http/Http.Features/src/IResponseCookies.cs b/src/Http/Http.Features/src/IResponseCookies.cs index 1f2a65f2fc..c9bf5c3c40 100644 --- a/src/Http/Http.Features/src/IResponseCookies.cs +++ b/src/Http/Http.Features/src/IResponseCookies.cs @@ -4,55 +4,54 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// A wrapper for the response Set-Cookie header. +/// +public interface IResponseCookies { /// - /// A wrapper for the response Set-Cookie header. + /// Add a new cookie and value. /// - public interface IResponseCookies - { - /// - /// Add a new cookie and value. - /// - /// Name of the new cookie. - /// Value of the new cookie. - void Append(string key, string value); + /// Name of the new cookie. + /// Value of the new cookie. + void Append(string key, string value); - /// - /// Add a new cookie. - /// - /// Name of the new cookie. - /// Value of the new cookie. - /// included in the new cookie setting. - void Append(string key, string value, CookieOptions options); + /// + /// Add a new cookie. + /// + /// Name of the new cookie. + /// Value of the new cookie. + /// included in the new cookie setting. + void Append(string key, string value, CookieOptions options); - /// - /// Add elements of specified collection as cookies. - /// - /// Key value pair collections whose elements will be added as cookies. - /// included in new cookie settings. - void Append(ReadOnlySpan> keyValuePairs, CookieOptions options) + /// + /// Add elements of specified collection as cookies. + /// + /// Key value pair collections whose elements will be added as cookies. + /// included in new cookie settings. + void Append(ReadOnlySpan> keyValuePairs, CookieOptions options) + { + foreach (var keyValuePair in keyValuePairs) { - foreach (var keyValuePair in keyValuePairs) - { - Append(keyValuePair.Key, keyValuePair.Value, options); - } + Append(keyValuePair.Key, keyValuePair.Value, options); } + } - /// - /// Sets an expired cookie. - /// - /// Name of the cookie to expire. - void Delete(string key); + /// + /// Sets an expired cookie. + /// + /// Name of the cookie to expire. + void Delete(string key); - /// - /// Sets an expired cookie. - /// - /// Name of the cookie to expire. - /// - /// used to discriminate the particular cookie to expire. The - /// and values are especially important. - /// - void Delete(string key, CookieOptions options); - } + /// + /// Sets an expired cookie. + /// + /// Name of the cookie to expire. + /// + /// used to discriminate the particular cookie to expire. The + /// and values are especially important. + /// + void Delete(string key, CookieOptions options); } diff --git a/src/Http/Http.Features/src/IResponseCookiesFeature.cs b/src/Http/Http.Features/src/IResponseCookiesFeature.cs index ff8de1eac9..a7eede55a4 100644 --- a/src/Http/Http.Features/src/IResponseCookiesFeature.cs +++ b/src/Http/Http.Features/src/IResponseCookiesFeature.cs @@ -1,16 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// A helper for creating the response Set-Cookie header. +/// +public interface IResponseCookiesFeature { /// - /// A helper for creating the response Set-Cookie header. + /// Gets the wrapper for the response Set-Cookie header. /// - public interface IResponseCookiesFeature - { - /// - /// Gets the wrapper for the response Set-Cookie header. - /// - IResponseCookies Cookies { get; } - } -} \ No newline at end of file + IResponseCookies Cookies { get; } +} diff --git a/src/Http/Http.Features/src/IServerVariablesFeature.cs b/src/Http/Http.Features/src/IServerVariablesFeature.cs index b25ac6ec1b..7b8b17e219 100644 --- a/src/Http/Http.Features/src/IServerVariablesFeature.cs +++ b/src/Http/Http.Features/src/IServerVariablesFeature.cs @@ -1,18 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// This feature provides access to request server variables set. +/// +public interface IServerVariablesFeature { /// - /// This feature provides access to request server variables set. + /// Gets or sets the value of a server variable for the current request. /// - public interface IServerVariablesFeature - { - /// - /// Gets or sets the value of a server variable for the current request. - /// - /// The variable name - /// May return null or empty if the variable does not exist or is not set. - string? this[string variableName] { get; set; } - } + /// The variable name + /// May return null or empty if the variable does not exist or is not set. + string? this[string variableName] { get; set; } } diff --git a/src/Http/Http.Features/src/IServiceProvidersFeature.cs b/src/Http/Http.Features/src/IServiceProvidersFeature.cs index b53c2f9dc7..3ef71c8322 100644 --- a/src/Http/Http.Features/src/IServiceProvidersFeature.cs +++ b/src/Http/Http.Features/src/IServiceProvidersFeature.cs @@ -3,16 +3,15 @@ using System; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Provides acccess to the request-scoped . +/// +public interface IServiceProvidersFeature { /// - /// Provides acccess to the request-scoped . + /// Gets or sets the scoped to the current request. /// - public interface IServiceProvidersFeature - { - /// - /// Gets or sets the scoped to the current request. - /// - IServiceProvider RequestServices { get; set; } - } + IServiceProvider RequestServices { get; set; } } diff --git a/src/Http/Http.Features/src/ISession.cs b/src/Http/Http.Features/src/ISession.cs index c5e9a0a879..ecb30922ee 100644 --- a/src/Http/Http.Features/src/ISession.cs +++ b/src/Http/Http.Features/src/ISession.cs @@ -6,68 +6,67 @@ using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Stores user data while the user browses a web application. Session state uses a store maintained by the application +/// to persist data across requests from a client. The session data is backed by a cache and considered ephemeral data. +/// +public interface ISession { /// - /// Stores user data while the user browses a web application. Session state uses a store maintained by the application - /// to persist data across requests from a client. The session data is backed by a cache and considered ephemeral data. + /// Indicates whether the current session loaded successfully. Accessing this property before the session is loaded will cause it to be loaded inline. /// - public interface ISession - { - /// - /// Indicates whether the current session loaded successfully. Accessing this property before the session is loaded will cause it to be loaded inline. - /// - bool IsAvailable { get; } + bool IsAvailable { get; } - /// - /// A unique identifier for the current session. This is not the same as the session cookie - /// since the cookie lifetime may not be the same as the session entry lifetime in the data store. - /// - string Id { get; } + /// + /// A unique identifier for the current session. This is not the same as the session cookie + /// since the cookie lifetime may not be the same as the session entry lifetime in the data store. + /// + string Id { get; } - /// - /// Enumerates all the keys, if any. - /// - IEnumerable Keys { get; } + /// + /// Enumerates all the keys, if any. + /// + IEnumerable Keys { get; } - /// - /// Load the session from the data store. This may throw if the data store is unavailable. - /// - /// - Task LoadAsync(CancellationToken cancellationToken = default(CancellationToken)); + /// + /// Load the session from the data store. This may throw if the data store is unavailable. + /// + /// + Task LoadAsync(CancellationToken cancellationToken = default(CancellationToken)); - /// - /// Store the session in the data store. This may throw if the data store is unavailable. - /// - /// - Task CommitAsync(CancellationToken cancellationToken = default(CancellationToken)); + /// + /// Store the session in the data store. This may throw if the data store is unavailable. + /// + /// + Task CommitAsync(CancellationToken cancellationToken = default(CancellationToken)); - /// - /// Retrieve the value of the given key, if present. - /// - /// - /// - /// The retrieved value. - bool TryGetValue(string key, [NotNullWhen(true)] out byte[]? value); + /// + /// Retrieve the value of the given key, if present. + /// + /// + /// + /// The retrieved value. + bool TryGetValue(string key, [NotNullWhen(true)] out byte[]? value); - /// - /// Set the given key and value in the current session. This will throw if the session - /// was not established prior to sending the response. - /// - /// - /// - void Set(string key, byte[] value); + /// + /// Set the given key and value in the current session. This will throw if the session + /// was not established prior to sending the response. + /// + /// + /// + void Set(string key, byte[] value); - /// - /// Remove the given key from the session if present. - /// - /// - void Remove(string key); + /// + /// Remove the given key from the session if present. + /// + /// + void Remove(string key); - /// - /// Remove all entries from the current session, if any. - /// The session cookie is not removed. - /// - void Clear(); - } + /// + /// Remove all entries from the current session, if any. + /// The session cookie is not removed. + /// + void Clear(); } diff --git a/src/Http/Http.Features/src/ISessionFeature.cs b/src/Http/Http.Features/src/ISessionFeature.cs index 7867124b38..03ca5aba34 100644 --- a/src/Http/Http.Features/src/ISessionFeature.cs +++ b/src/Http/Http.Features/src/ISessionFeature.cs @@ -1,16 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Provides access to the for the current request. +/// +public interface ISessionFeature { /// - /// Provides access to the for the current request. + /// The for the current request. /// - public interface ISessionFeature - { - /// - /// The for the current request. - /// - ISession Session { get; set; } - } -} \ No newline at end of file + ISession Session { get; set; } +} diff --git a/src/Http/Http.Features/src/ITlsConnectionFeature.cs b/src/Http/Http.Features/src/ITlsConnectionFeature.cs index 05502345d5..a36ccb76b3 100644 --- a/src/Http/Http.Features/src/ITlsConnectionFeature.cs +++ b/src/Http/Http.Features/src/ITlsConnectionFeature.cs @@ -5,21 +5,20 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Provides access to TLS features associated with the current HTTP connection. +/// +public interface ITlsConnectionFeature { /// - /// Provides access to TLS features associated with the current HTTP connection. + /// Synchronously retrieves the client certificate, if any. /// - public interface ITlsConnectionFeature - { - /// - /// Synchronously retrieves the client certificate, if any. - /// - X509Certificate2? ClientCertificate { get; set; } + X509Certificate2? ClientCertificate { get; set; } - /// - /// Asynchronously retrieves the client certificate, if any. - /// - Task GetClientCertificateAsync(CancellationToken cancellationToken); - } + /// + /// Asynchronously retrieves the client certificate, if any. + /// + Task GetClientCertificateAsync(CancellationToken cancellationToken); } diff --git a/src/Http/Http.Features/src/ITlsTokenBindingFeature.cs b/src/Http/Http.Features/src/ITlsTokenBindingFeature.cs index 4918e5029a..1640e2f720 100644 --- a/src/Http/Http.Features/src/ITlsTokenBindingFeature.cs +++ b/src/Http/Http.Features/src/ITlsTokenBindingFeature.cs @@ -1,35 +1,34 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Provides information regarding TLS token binding parameters. +/// +/// +/// TLS token bindings help mitigate the risk of impersonation by an attacker in the +/// event an authenticated client's bearer tokens are somehow exfiltrated from the +/// client's machine. See https://datatracker.ietf.org/doc/draft-popov-token-binding/ +/// for more information. +/// +public interface ITlsTokenBindingFeature { /// - /// Provides information regarding TLS token binding parameters. + /// Gets the 'provided' token binding identifier associated with the request. /// - /// - /// TLS token bindings help mitigate the risk of impersonation by an attacker in the - /// event an authenticated client's bearer tokens are somehow exfiltrated from the - /// client's machine. See https://datatracker.ietf.org/doc/draft-popov-token-binding/ - /// for more information. - /// - public interface ITlsTokenBindingFeature - { - /// - /// Gets the 'provided' token binding identifier associated with the request. - /// - /// The token binding identifier, or null if the client did not - /// supply a 'provided' token binding or valid proof of possession of the - /// associated private key. The caller should treat this identifier as an - /// opaque blob and should not try to parse it. - byte[] GetProvidedTokenBindingId(); + /// The token binding identifier, or null if the client did not + /// supply a 'provided' token binding or valid proof of possession of the + /// associated private key. The caller should treat this identifier as an + /// opaque blob and should not try to parse it. + byte[] GetProvidedTokenBindingId(); - /// - /// Gets the 'referred' token binding identifier associated with the request. - /// - /// The token binding identifier, or null if the client did not - /// supply a 'referred' token binding or valid proof of possession of the - /// associated private key. The caller should treat this identifier as an - /// opaque blob and should not try to parse it. - byte[] GetReferredTokenBindingId(); - } + /// + /// Gets the 'referred' token binding identifier associated with the request. + /// + /// The token binding identifier, or null if the client did not + /// supply a 'referred' token binding or valid proof of possession of the + /// associated private key. The caller should treat this identifier as an + /// opaque blob and should not try to parse it. + byte[] GetReferredTokenBindingId(); } diff --git a/src/Http/Http.Features/src/ITrackingConsentFeature.cs b/src/Http/Http.Features/src/ITrackingConsentFeature.cs index 60cf627949..598b4832d9 100644 --- a/src/Http/Http.Features/src/ITrackingConsentFeature.cs +++ b/src/Http/Http.Features/src/ITrackingConsentFeature.cs @@ -1,44 +1,43 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Used to query, grant, and withdraw user consent regarding the storage of user +/// information related to site activity and functionality. +/// +public interface ITrackingConsentFeature { /// - /// Used to query, grant, and withdraw user consent regarding the storage of user - /// information related to site activity and functionality. + /// Indicates if consent is required for the given request. /// - public interface ITrackingConsentFeature - { - /// - /// Indicates if consent is required for the given request. - /// - bool IsConsentNeeded { get; } + bool IsConsentNeeded { get; } - /// - /// Indicates if consent was given. - /// - bool HasConsent { get; } + /// + /// Indicates if consent was given. + /// + bool HasConsent { get; } - /// - /// Indicates either if consent has been given or if consent is not required. - /// - bool CanTrack { get; } + /// + /// Indicates either if consent has been given or if consent is not required. + /// + bool CanTrack { get; } - /// - /// Grants consent for this request. If the response has not yet started then - /// this will also grant consent for future requests. - /// - void GrantConsent(); + /// + /// Grants consent for this request. If the response has not yet started then + /// this will also grant consent for future requests. + /// + void GrantConsent(); - /// - /// Withdraws consent for this request. If the response has not yet started then - /// this will also withdraw consent for future requests. - /// - void WithdrawConsent(); + /// + /// Withdraws consent for this request. If the response has not yet started then + /// this will also withdraw consent for future requests. + /// + void WithdrawConsent(); - /// - /// Creates a consent cookie for use when granting consent from a javascript client. - /// - string CreateConsentCookie(); - } + /// + /// Creates a consent cookie for use when granting consent from a javascript client. + /// + string CreateConsentCookie(); } diff --git a/src/Http/Http.Features/src/SameSiteMode.cs b/src/Http/Http.Features/src/SameSiteMode.cs index 9c8b03d165..7d1efb0402 100644 --- a/src/Http/Http.Features/src/SameSiteMode.cs +++ b/src/Http/Http.Features/src/SameSiteMode.cs @@ -1,22 +1,21 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Used to set the SameSite field on response cookies to indicate if those cookies should be included by the client on future "same-site" or "cross-site" requests. +/// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1 +/// +// This mirrors Microsoft.Net.Http.Headers.SameSiteMode +public enum SameSiteMode { - /// - /// Used to set the SameSite field on response cookies to indicate if those cookies should be included by the client on future "same-site" or "cross-site" requests. - /// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1 - /// - // This mirrors Microsoft.Net.Http.Headers.SameSiteMode - public enum SameSiteMode - { - /// No SameSite field will be set, the client should follow its default cookie policy. - Unspecified = -1, - /// Indicates the client should disable same-site restrictions. - None = 0, - /// Indicates the client should send the cookie with "same-site" requests, and with "cross-site" top-level navigations. - Lax, - /// Indicates the client should only send the cookie with "same-site" requests. - Strict - } + /// No SameSite field will be set, the client should follow its default cookie policy. + Unspecified = -1, + /// Indicates the client should disable same-site restrictions. + None = 0, + /// Indicates the client should send the cookie with "same-site" requests, and with "cross-site" top-level navigations. + Lax, + /// Indicates the client should only send the cookie with "same-site" requests. + Strict } diff --git a/src/Http/Http.Features/src/WebSocketAcceptContext.cs b/src/Http/Http.Features/src/WebSocketAcceptContext.cs index 3942221f96..f9afacb0cb 100644 --- a/src/Http/Http.Features/src/WebSocketAcceptContext.cs +++ b/src/Http/Http.Features/src/WebSocketAcceptContext.cs @@ -4,70 +4,69 @@ using System; using System.Net.WebSockets; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// A context for negotiating a websocket upgrade. +/// +public class WebSocketAcceptContext { + private int _serverMaxWindowBits = 15; + /// - /// A context for negotiating a websocket upgrade. + /// Gets or sets the subprotocol being negotiated. /// - public class WebSocketAcceptContext - { - private int _serverMaxWindowBits = 15; - - /// - /// Gets or sets the subprotocol being negotiated. - /// - public virtual string? SubProtocol { get; set; } + public virtual string? SubProtocol { get; set; } - /// - /// The interval to send pong frames. This is a heart-beat that keeps the connection alive. - /// - public virtual TimeSpan? KeepAliveInterval { get; set; } + /// + /// The interval to send pong frames. This is a heart-beat that keeps the connection alive. + /// + public virtual TimeSpan? KeepAliveInterval { get; set; } - /// - /// Enables support for the 'permessage-deflate' WebSocket extension. - /// Be aware that enabling compression over encrypted connections makes the application subject to CRIME/BREACH type attacks. - /// It is strongly advised to turn off compression when sending data containing secrets by - /// specifying when sending such messages. - /// - public bool DangerousEnableCompression { get; set; } + /// + /// Enables support for the 'permessage-deflate' WebSocket extension. + /// Be aware that enabling compression over encrypted connections makes the application subject to CRIME/BREACH type attacks. + /// It is strongly advised to turn off compression when sending data containing secrets by + /// specifying when sending such messages. + /// + public bool DangerousEnableCompression { get; set; } - /// - /// Disables server context takeover when using compression. - /// This setting reduces the memory overhead of compression at the cost of a potentially worse compression ratio. - /// - /// - /// This property does nothing when is false, - /// or when the client does not use compression. - /// - /// - /// false - /// - public bool DisableServerContextTakeover { get; set; } + /// + /// Disables server context takeover when using compression. + /// This setting reduces the memory overhead of compression at the cost of a potentially worse compression ratio. + /// + /// + /// This property does nothing when is false, + /// or when the client does not use compression. + /// + /// + /// false + /// + public bool DisableServerContextTakeover { get; set; } - /// - /// Sets the maximum base-2 logarithm of the LZ77 sliding window size that can be used for compression. - /// This setting reduces the memory overhead of compression at the cost of a potentially worse compression ratio. - /// - /// - /// This property does nothing when is false, - /// or when the client does not use compression. - /// Valid values are 9 through 15. - /// - /// - /// 15 - /// - public int ServerMaxWindowBits + /// + /// Sets the maximum base-2 logarithm of the LZ77 sliding window size that can be used for compression. + /// This setting reduces the memory overhead of compression at the cost of a potentially worse compression ratio. + /// + /// + /// This property does nothing when is false, + /// or when the client does not use compression. + /// Valid values are 9 through 15. + /// + /// + /// 15 + /// + public int ServerMaxWindowBits + { + get => _serverMaxWindowBits; + set { - get => _serverMaxWindowBits; - set + if (value < 9 || value > 15) { - if (value < 9 || value > 15) - { - throw new ArgumentOutOfRangeException(nameof(ServerMaxWindowBits), - "The argument must be a value from 9 to 15."); - } - _serverMaxWindowBits = value; + throw new ArgumentOutOfRangeException(nameof(ServerMaxWindowBits), + "The argument must be a value from 9 to 15."); } + _serverMaxWindowBits = value; } } } diff --git a/src/Http/Http.Results/src/AcceptedAtRouteResult.cs b/src/Http/Http.Results/src/AcceptedAtRouteResult.cs index e35d06a4fb..1958f7d402 100644 --- a/src/Http/Http.Results/src/AcceptedAtRouteResult.cs +++ b/src/Http/Http.Results/src/AcceptedAtRouteResult.cs @@ -4,64 +4,63 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal sealed class AcceptedAtRouteResult : ObjectResult { - internal sealed class AcceptedAtRouteResult : ObjectResult + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The route data to use for generating the URL. + /// The value to format in the entity body. + public AcceptedAtRouteResult(object? routeValues, object? value) + : this(routeName: null, routeValues: routeValues, value: value) { - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - /// The route data to use for generating the URL. - /// The value to format in the entity body. - public AcceptedAtRouteResult(object? routeValues, object? value) - : this(routeName: null, routeValues: routeValues, value: value) - { - } - - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - /// The name of the route to use for generating the URL. - /// The route data to use for generating the URL. - /// The value to format in the entity body. - public AcceptedAtRouteResult( - string? routeName, - object? routeValues, - object? value) - : base(value, StatusCodes.Status202Accepted) - { - RouteName = routeName; - RouteValues = new RouteValueDictionary(routeValues); - } + } - /// - /// Gets the name of the route to use for generating the URL. - /// - public string? RouteName { get; } + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The name of the route to use for generating the URL. + /// The route data to use for generating the URL. + /// The value to format in the entity body. + public AcceptedAtRouteResult( + string? routeName, + object? routeValues, + object? value) + : base(value, StatusCodes.Status202Accepted) + { + RouteName = routeName; + RouteValues = new RouteValueDictionary(routeValues); + } - /// - /// Gets the route data to use for generating the URL. - /// - public RouteValueDictionary RouteValues { get; } + /// + /// Gets the name of the route to use for generating the URL. + /// + public string? RouteName { get; } - /// - protected override void ConfigureResponseHeaders(HttpContext context) - { - var linkGenerator = context.RequestServices.GetRequiredService(); - var url = linkGenerator.GetUriByAddress( - context, - RouteName, - RouteValues, - fragment: FragmentString.Empty); + /// + /// Gets the route data to use for generating the URL. + /// + public RouteValueDictionary RouteValues { get; } - if (string.IsNullOrEmpty(url)) - { - throw new InvalidOperationException("No route matches the supplied values."); - } + /// + protected override void ConfigureResponseHeaders(HttpContext context) + { + var linkGenerator = context.RequestServices.GetRequiredService(); + var url = linkGenerator.GetUriByAddress( + context, + RouteName, + RouteValues, + fragment: FragmentString.Empty); - context.Response.Headers.Location = url; + if (string.IsNullOrEmpty(url)) + { + throw new InvalidOperationException("No route matches the supplied values."); } + + context.Response.Headers.Location = url; } } diff --git a/src/Http/Http.Results/src/AcceptedResult.cs b/src/Http/Http.Results/src/AcceptedResult.cs index 9ef66d936c..4926e216f3 100644 --- a/src/Http/Http.Results/src/AcceptedResult.cs +++ b/src/Http/Http.Results/src/AcceptedResult.cs @@ -3,67 +3,66 @@ using System; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal sealed class AcceptedResult : ObjectResult { - internal sealed class AcceptedResult : ObjectResult + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + public AcceptedResult() + : base(value: null, StatusCodes.Status202Accepted) + { + } + + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The location at which the status of requested content can be monitored. + /// The value to format in the entity body. + public AcceptedResult(string? location, object? value) + : base(value, StatusCodes.Status202Accepted) { - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - public AcceptedResult() - : base(value: null, StatusCodes.Status202Accepted) + Location = location; + } + + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The location at which the status of requested content can be monitored. + /// The value to format in the entity body. + public AcceptedResult(Uri locationUri, object? value) + : base(value, StatusCodes.Status202Accepted) + { + if (locationUri == null) { + throw new ArgumentNullException(nameof(locationUri)); } - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - /// The location at which the status of requested content can be monitored. - /// The value to format in the entity body. - public AcceptedResult(string? location, object? value) - : base(value, StatusCodes.Status202Accepted) + if (locationUri.IsAbsoluteUri) { - Location = location; + Location = locationUri.AbsoluteUri; } - - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - /// The location at which the status of requested content can be monitored. - /// The value to format in the entity body. - public AcceptedResult(Uri locationUri, object? value) - : base(value, StatusCodes.Status202Accepted) + else { - if (locationUri == null) - { - throw new ArgumentNullException(nameof(locationUri)); - } - - if (locationUri.IsAbsoluteUri) - { - Location = locationUri.AbsoluteUri; - } - else - { - Location = locationUri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); - } + Location = locationUri.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); } + } - /// - /// Gets or sets the location at which the status of the requested content can be monitored. - /// - public string? Location { get; set; } + /// + /// Gets or sets the location at which the status of the requested content can be monitored. + /// + public string? Location { get; set; } - /// - protected override void ConfigureResponseHeaders(HttpContext context) + /// + protected override void ConfigureResponseHeaders(HttpContext context) + { + if (!string.IsNullOrEmpty(Location)) { - if (!string.IsNullOrEmpty(Location)) - { - context.Response.Headers.Location = Location; - } + context.Response.Headers.Location = Location; } } } diff --git a/src/Http/Http.Results/src/BadRequestObjectResult.cs b/src/Http/Http.Results/src/BadRequestObjectResult.cs index bdad2f4f0f..7f58b7c9d6 100644 --- a/src/Http/Http.Results/src/BadRequestObjectResult.cs +++ b/src/Http/Http.Results/src/BadRequestObjectResult.cs @@ -1,13 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal sealed class BadRequestObjectResult : ObjectResult { - internal sealed class BadRequestObjectResult : ObjectResult + public BadRequestObjectResult(object? error) + : base(error, StatusCodes.Status400BadRequest) { - public BadRequestObjectResult(object? error) - : base(error, StatusCodes.Status400BadRequest) - { - } } } diff --git a/src/Http/Http.Results/src/ChallengeResult.cs b/src/Http/Http.Results/src/ChallengeResult.cs index b9c15cfedd..c9948b943a 100644 --- a/src/Http/Http.Results/src/ChallengeResult.cs +++ b/src/Http/Http.Results/src/ChallengeResult.cs @@ -9,112 +9,111 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +/// +/// An that on execution invokes . +/// +internal sealed partial class ChallengeResult : IResult { /// - /// An that on execution invokes . + /// Initializes a new instance of . /// - internal sealed partial class ChallengeResult : IResult + public ChallengeResult() + : this(Array.Empty()) { - /// - /// Initializes a new instance of . - /// - public ChallengeResult() - : this(Array.Empty()) - { - } + } - /// - /// Initializes a new instance of with the - /// specified authentication scheme. - /// - /// The authentication scheme to challenge. - public ChallengeResult(string authenticationScheme) - : this(new[] { authenticationScheme }) - { - } + /// + /// Initializes a new instance of with the + /// specified authentication scheme. + /// + /// The authentication scheme to challenge. + public ChallengeResult(string authenticationScheme) + : this(new[] { authenticationScheme }) + { + } - /// - /// Initializes a new instance of with the - /// specified authentication schemes. - /// - /// The authentication schemes to challenge. - public ChallengeResult(IList authenticationSchemes) - : this(authenticationSchemes, properties: null) - { - } + /// + /// Initializes a new instance of with the + /// specified authentication schemes. + /// + /// The authentication schemes to challenge. + public ChallengeResult(IList authenticationSchemes) + : this(authenticationSchemes, properties: null) + { + } - /// - /// Initializes a new instance of with the - /// specified . - /// - /// used to perform the authentication - /// challenge. - public ChallengeResult(AuthenticationProperties? properties) - : this(Array.Empty(), properties) - { - } + /// + /// Initializes a new instance of with the + /// specified . + /// + /// used to perform the authentication + /// challenge. + public ChallengeResult(AuthenticationProperties? properties) + : this(Array.Empty(), properties) + { + } - /// - /// Initializes a new instance of with the - /// specified authentication scheme and . - /// - /// The authentication schemes to challenge. - /// used to perform the authentication - /// challenge. - public ChallengeResult(string authenticationScheme, AuthenticationProperties? properties) - : this(new[] { authenticationScheme }, properties) - { - } + /// + /// Initializes a new instance of with the + /// specified authentication scheme and . + /// + /// The authentication schemes to challenge. + /// used to perform the authentication + /// challenge. + public ChallengeResult(string authenticationScheme, AuthenticationProperties? properties) + : this(new[] { authenticationScheme }, properties) + { + } - /// - /// Initializes a new instance of with the - /// specified authentication schemes and . - /// - /// The authentication scheme to challenge. - /// used to perform the authentication - /// challenge. - public ChallengeResult(IList authenticationSchemes, AuthenticationProperties? properties) - { - AuthenticationSchemes = authenticationSchemes; - Properties = properties; - } + /// + /// Initializes a new instance of with the + /// specified authentication schemes and . + /// + /// The authentication scheme to challenge. + /// used to perform the authentication + /// challenge. + public ChallengeResult(IList authenticationSchemes, AuthenticationProperties? properties) + { + AuthenticationSchemes = authenticationSchemes; + Properties = properties; + } - public IList AuthenticationSchemes { get; init; } = Array.Empty(); + public IList AuthenticationSchemes { get; init; } = Array.Empty(); - public AuthenticationProperties? Properties { get; init; } + public AuthenticationProperties? Properties { get; init; } - public async Task ExecuteAsync(HttpContext httpContext) - { - var logger = httpContext.RequestServices.GetRequiredService>(); + public async Task ExecuteAsync(HttpContext httpContext) + { + var logger = httpContext.RequestServices.GetRequiredService>(); - Log.ChallengeResultExecuting(logger, AuthenticationSchemes); + Log.ChallengeResultExecuting(logger, AuthenticationSchemes); - if (AuthenticationSchemes != null && AuthenticationSchemes.Count > 0) - { - foreach (var scheme in AuthenticationSchemes) - { - await httpContext.ChallengeAsync(scheme, Properties); - } - } - else + if (AuthenticationSchemes != null && AuthenticationSchemes.Count > 0) + { + foreach (var scheme in AuthenticationSchemes) { - await httpContext.ChallengeAsync(Properties); + await httpContext.ChallengeAsync(scheme, Properties); } } + else + { + await httpContext.ChallengeAsync(Properties); + } + } - private static partial class Log + private static partial class Log + { + public static void ChallengeResultExecuting(ILogger logger, IList authenticationSchemes) { - public static void ChallengeResultExecuting(ILogger logger, IList authenticationSchemes) + if (logger.IsEnabled(LogLevel.Information)) { - if (logger.IsEnabled(LogLevel.Information)) - { - ChallengeResultExecuting(logger, authenticationSchemes.ToArray()); - } + ChallengeResultExecuting(logger, authenticationSchemes.ToArray()); } - - [LoggerMessage(1, LogLevel.Information, "Executing ChallengeResult with authentication schemes ({Schemes}).", EventName = "ChallengeResultExecuting", SkipEnabledCheck = true)] - private static partial void ChallengeResultExecuting(ILogger logger, string[] schemes); } + + [LoggerMessage(1, LogLevel.Information, "Executing ChallengeResult with authentication schemes ({Schemes}).", EventName = "ChallengeResultExecuting", SkipEnabledCheck = true)] + private static partial void ChallengeResultExecuting(ILogger logger, string[] schemes); } } diff --git a/src/Http/Http.Results/src/ConflictObjectResult.cs b/src/Http/Http.Results/src/ConflictObjectResult.cs index 9a90eb7bc0..68308b14d2 100644 --- a/src/Http/Http.Results/src/ConflictObjectResult.cs +++ b/src/Http/Http.Results/src/ConflictObjectResult.cs @@ -1,13 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal sealed class ConflictObjectResult : ObjectResult { - internal sealed class ConflictObjectResult : ObjectResult + public ConflictObjectResult(object? error) : + base(error, StatusCodes.Status409Conflict) { - public ConflictObjectResult(object? error) : - base(error, StatusCodes.Status409Conflict) - { - } } } diff --git a/src/Http/Http.Results/src/ContentResult.cs b/src/Http/Http.Results/src/ContentResult.cs index 347c823d76..ebd987dfed 100644 --- a/src/Http/Http.Results/src/ContentResult.cs +++ b/src/Http/Http.Results/src/ContentResult.cs @@ -7,69 +7,68 @@ using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result -{ - internal sealed partial class ContentResult : IResult - { - private const string DefaultContentType = "text/plain; charset=utf-8"; - private static readonly Encoding DefaultEncoding = Encoding.UTF8; +namespace Microsoft.AspNetCore.Http.Result; - /// - /// Gets or set the content representing the body of the response. - /// - public string? Content { get; init; } +internal sealed partial class ContentResult : IResult +{ + private const string DefaultContentType = "text/plain; charset=utf-8"; + private static readonly Encoding DefaultEncoding = Encoding.UTF8; - /// - /// Gets or sets the Content-Type header for the response. - /// - public string? ContentType { get; init; } + /// + /// Gets or set the content representing the body of the response. + /// + public string? Content { get; init; } - /// - /// Gets or sets the HTTP status code. - /// - public int? StatusCode { get; init; } + /// + /// Gets or sets the Content-Type header for the response. + /// + public string? ContentType { get; init; } - /// - /// Writes the content to the HTTP response. - /// - /// The for the current request. - /// A task that represents the asynchronous execute operation. - public async Task ExecuteAsync(HttpContext httpContext) - { - var response = httpContext.Response; + /// + /// Gets or sets the HTTP status code. + /// + public int? StatusCode { get; init; } - ResponseContentTypeHelper.ResolveContentTypeAndEncoding( - ContentType, - response.ContentType, - (DefaultContentType, DefaultEncoding), - ResponseContentTypeHelper.GetEncoding, - out var resolvedContentType, - out var resolvedContentTypeEncoding); + /// + /// Writes the content to the HTTP response. + /// + /// The for the current request. + /// A task that represents the asynchronous execute operation. + public async Task ExecuteAsync(HttpContext httpContext) + { + var response = httpContext.Response; - response.ContentType = resolvedContentType; + ResponseContentTypeHelper.ResolveContentTypeAndEncoding( + ContentType, + response.ContentType, + (DefaultContentType, DefaultEncoding), + ResponseContentTypeHelper.GetEncoding, + out var resolvedContentType, + out var resolvedContentTypeEncoding); - if (StatusCode != null) - { - response.StatusCode = StatusCode.Value; - } + response.ContentType = resolvedContentType; - var logger = httpContext.RequestServices.GetRequiredService>(); + if (StatusCode != null) + { + response.StatusCode = StatusCode.Value; + } - Log.ContentResultExecuting(logger, resolvedContentType); + var logger = httpContext.RequestServices.GetRequiredService>(); - if (Content != null) - { - response.ContentLength = resolvedContentTypeEncoding.GetByteCount(Content); - await response.WriteAsync(Content, resolvedContentTypeEncoding); - } - } + Log.ContentResultExecuting(logger, resolvedContentType); - private static partial class Log + if (Content != null) { - [LoggerMessage(1, LogLevel.Information, - "Executing ContentResult with HTTP Response ContentType of {ContentType}", - EventName = "ContentResultExecuting")] - internal static partial void ContentResultExecuting(ILogger logger, string contentType); + response.ContentLength = resolvedContentTypeEncoding.GetByteCount(Content); + await response.WriteAsync(Content, resolvedContentTypeEncoding); } } + + private static partial class Log + { + [LoggerMessage(1, LogLevel.Information, + "Executing ContentResult with HTTP Response ContentType of {ContentType}", + EventName = "ContentResultExecuting")] + internal static partial void ContentResultExecuting(ILogger logger, string contentType); + } } diff --git a/src/Http/Http.Results/src/CreatedAtRouteResult.cs b/src/Http/Http.Results/src/CreatedAtRouteResult.cs index e561e32435..4b0bc00747 100644 --- a/src/Http/Http.Results/src/CreatedAtRouteResult.cs +++ b/src/Http/Http.Results/src/CreatedAtRouteResult.cs @@ -5,64 +5,63 @@ using System; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal sealed class CreatedAtRouteResult : ObjectResult { - internal sealed class CreatedAtRouteResult : ObjectResult + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The route data to use for generating the URL. + /// The value to format in the entity body. + public CreatedAtRouteResult(object? routeValues, object? value) + : this(routeName: null, routeValues: routeValues, value: value) { - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - /// The route data to use for generating the URL. - /// The value to format in the entity body. - public CreatedAtRouteResult(object? routeValues, object? value) - : this(routeName: null, routeValues: routeValues, value: value) - { - } - - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - /// The name of the route to use for generating the URL. - /// The route data to use for generating the URL. - /// The value to format in the entity body. - public CreatedAtRouteResult( - string? routeName, - object? routeValues, - object? value) - : base(value, StatusCodes.Status201Created) - { - RouteName = routeName; - RouteValues = routeValues == null ? null : new RouteValueDictionary(routeValues); - } + } - /// - /// Gets or sets the name of the route to use for generating the URL. - /// - public string? RouteName { get; set; } + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The name of the route to use for generating the URL. + /// The route data to use for generating the URL. + /// The value to format in the entity body. + public CreatedAtRouteResult( + string? routeName, + object? routeValues, + object? value) + : base(value, StatusCodes.Status201Created) + { + RouteName = routeName; + RouteValues = routeValues == null ? null : new RouteValueDictionary(routeValues); + } - /// - /// Gets or sets the route data to use for generating the URL. - /// - public RouteValueDictionary? RouteValues { get; set; } + /// + /// Gets or sets the name of the route to use for generating the URL. + /// + public string? RouteName { get; set; } - /// - protected override void ConfigureResponseHeaders(HttpContext context) - { - var linkGenerator = context.RequestServices.GetRequiredService(); - var url = linkGenerator.GetUriByRouteValues( - context, - RouteName, - RouteValues, - fragment: FragmentString.Empty); + /// + /// Gets or sets the route data to use for generating the URL. + /// + public RouteValueDictionary? RouteValues { get; set; } - if (string.IsNullOrEmpty(url)) - { - throw new InvalidOperationException("No route matches the supplied values."); - } + /// + protected override void ConfigureResponseHeaders(HttpContext context) + { + var linkGenerator = context.RequestServices.GetRequiredService(); + var url = linkGenerator.GetUriByRouteValues( + context, + RouteName, + RouteValues, + fragment: FragmentString.Empty); - context.Response.Headers.Location = url; + if (string.IsNullOrEmpty(url)) + { + throw new InvalidOperationException("No route matches the supplied values."); } + + context.Response.Headers.Location = url; } } diff --git a/src/Http/Http.Results/src/CreatedResult.cs b/src/Http/Http.Results/src/CreatedResult.cs index b5b2d04bbf..6979c2160b 100644 --- a/src/Http/Http.Results/src/CreatedResult.cs +++ b/src/Http/Http.Results/src/CreatedResult.cs @@ -3,55 +3,54 @@ using System; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal sealed class CreatedResult : ObjectResult { - internal sealed class CreatedResult : ObjectResult + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The location at which the content has been created. + /// The value to format in the entity body. + public CreatedResult(string location, object? value) + : base(value, StatusCodes.Status201Created) + { + Location = location; + } + + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The location at which the content has been created. + /// The value to format in the entity body. + public CreatedResult(Uri location, object? value) + : base(value, StatusCodes.Status201Created) { - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - /// The location at which the content has been created. - /// The value to format in the entity body. - public CreatedResult(string location, object? value) - : base(value, StatusCodes.Status201Created) + if (location == null) { - Location = location; + throw new ArgumentNullException(nameof(location)); } - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - /// The location at which the content has been created. - /// The value to format in the entity body. - public CreatedResult(Uri location, object? value) - : base(value, StatusCodes.Status201Created) + if (location.IsAbsoluteUri) { - if (location == null) - { - throw new ArgumentNullException(nameof(location)); - } - - if (location.IsAbsoluteUri) - { - Location = location.AbsoluteUri; - } - else - { - Location = location.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); - } + Location = location.AbsoluteUri; } - - /// - /// Gets or sets the location at which the content has been created. - /// - public string Location { get; init; } - - /// - protected override void ConfigureResponseHeaders(HttpContext context) + else { - context.Response.Headers.Location = Location; + Location = location.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped); } } + + /// + /// Gets or sets the location at which the content has been created. + /// + public string Location { get; init; } + + /// + protected override void ConfigureResponseHeaders(HttpContext context) + { + context.Response.Headers.Location = Location; + } } diff --git a/src/Http/Http.Results/src/FileContentResult.cs b/src/Http/Http.Results/src/FileContentResult.cs index 04c341a18b..b00da02890 100644 --- a/src/Http/Http.Results/src/FileContentResult.cs +++ b/src/Http/Http.Results/src/FileContentResult.cs @@ -5,68 +5,67 @@ using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal sealed partial class FileContentResult : FileResult, IResult { - internal sealed partial class FileContentResult : FileResult, IResult + /// + /// Creates a new instance with + /// the provided and the + /// provided . + /// + /// The bytes that represent the file contents. + /// The Content-Type header of the response. + public FileContentResult(byte[] fileContents, string? contentType) + : base(contentType) { - /// - /// Creates a new instance with - /// the provided and the - /// provided . - /// - /// The bytes that represent the file contents. - /// The Content-Type header of the response. - public FileContentResult(byte[] fileContents, string? contentType) - : base(contentType) - { - FileContents = fileContents; - } - - /// - /// Gets or sets the file contents. - /// - public byte[] FileContents { get; init; } + FileContents = fileContents; + } - public Task ExecuteAsync(HttpContext httpContext) - { - var logger = httpContext.RequestServices.GetRequiredService>(); - Log.ExecutingFileResult(logger, this); + /// + /// Gets or sets the file contents. + /// + public byte[] FileContents { get; init; } - var fileResultInfo = new FileResultInfo - { - ContentType = ContentType, - EnableRangeProcessing = EnableRangeProcessing, - EntityTag = EntityTag, - FileDownloadName = FileDownloadName, - LastModified = LastModified, - }; + public Task ExecuteAsync(HttpContext httpContext) + { + var logger = httpContext.RequestServices.GetRequiredService>(); + Log.ExecutingFileResult(logger, this); - var (range, rangeLength, serveBody) = FileResultHelper.SetHeadersAndLog( - httpContext, - fileResultInfo, - FileContents.Length, - EnableRangeProcessing, - LastModified, - EntityTag, - logger); + var fileResultInfo = new FileResultInfo + { + ContentType = ContentType, + EnableRangeProcessing = EnableRangeProcessing, + EntityTag = EntityTag, + FileDownloadName = FileDownloadName, + LastModified = LastModified, + }; - if (!serveBody) - { - return Task.CompletedTask; - } + var (range, rangeLength, serveBody) = FileResultHelper.SetHeadersAndLog( + httpContext, + fileResultInfo, + FileContents.Length, + EnableRangeProcessing, + LastModified, + EntityTag, + logger); - if (range != null && rangeLength == 0) - { - return Task.CompletedTask; - } + if (!serveBody) + { + return Task.CompletedTask; + } - if (range != null) - { - FileResultHelper.Log.WritingRangeToBody(logger); - } + if (range != null && rangeLength == 0) + { + return Task.CompletedTask; + } - var fileContentStream = new MemoryStream(FileContents); - return FileResultHelper.WriteFileAsync(httpContext, fileContentStream, range, rangeLength); + if (range != null) + { + FileResultHelper.Log.WritingRangeToBody(logger); } + + var fileContentStream = new MemoryStream(FileContents); + return FileResultHelper.WriteFileAsync(httpContext, fileContentStream, range, rangeLength); } } diff --git a/src/Http/Http.Results/src/FileResult.cs b/src/Http/Http.Results/src/FileResult.cs index d6a4afea79..d3be83f96f 100644 --- a/src/Http/Http.Results/src/FileResult.cs +++ b/src/Http/Http.Results/src/FileResult.cs @@ -5,83 +5,82 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal abstract partial class FileResult { - internal abstract partial class FileResult - { - private string? _fileDownloadName; + private string? _fileDownloadName; - /// - /// Creates a new instance with - /// the provided . - /// - /// The Content-Type header of the response. - protected FileResult(string? contentType) - { - ContentType = contentType ?? "application/octet-stream"; - } + /// + /// Creates a new instance with + /// the provided . + /// + /// The Content-Type header of the response. + protected FileResult(string? contentType) + { + ContentType = contentType ?? "application/octet-stream"; + } - /// - /// Gets the Content-Type header for the response. - /// - public string ContentType { get; } + /// + /// Gets the Content-Type header for the response. + /// + public string ContentType { get; } - /// - /// Gets the file name that will be used in the Content-Disposition header of the response. - /// - [AllowNull] - public string FileDownloadName - { - get { return _fileDownloadName ?? string.Empty; } - init { _fileDownloadName = value; } - } + /// + /// Gets the file name that will be used in the Content-Disposition header of the response. + /// + [AllowNull] + public string FileDownloadName + { + get { return _fileDownloadName ?? string.Empty; } + init { _fileDownloadName = value; } + } - /// - /// Gets or sets the last modified information associated with the . - /// - public DateTimeOffset? LastModified { get; init; } + /// + /// Gets or sets the last modified information associated with the . + /// + public DateTimeOffset? LastModified { get; init; } - /// - /// Gets or sets the etag associated with the . - /// - public EntityTagHeaderValue? EntityTag { get; init; } + /// + /// Gets or sets the etag associated with the . + /// + public EntityTagHeaderValue? EntityTag { get; init; } - /// - /// Gets or sets the value that enables range processing for the . - /// - public bool EnableRangeProcessing { get; init; } + /// + /// Gets or sets the value that enables range processing for the . + /// + public bool EnableRangeProcessing { get; init; } - protected static partial class Log + protected static partial class Log + { + public static void ExecutingFileResult(ILogger logger, FileResult fileResult) { - public static void ExecutingFileResult(ILogger logger, FileResult fileResult) + if (logger.IsEnabled(LogLevel.Information)) { - if (logger.IsEnabled(LogLevel.Information)) - { - var fileResultType = fileResult.GetType().Name; - ExecutingFileResultWithNoFileName(logger, fileResultType, fileResult.FileDownloadName); - } + var fileResultType = fileResult.GetType().Name; + ExecutingFileResultWithNoFileName(logger, fileResultType, fileResult.FileDownloadName); } + } - public static void ExecutingFileResult(ILogger logger, FileResult fileResult, string fileName) + public static void ExecutingFileResult(ILogger logger, FileResult fileResult, string fileName) + { + if (logger.IsEnabled(LogLevel.Information)) { - if (logger.IsEnabled(LogLevel.Information)) - { - var fileResultType = fileResult.GetType().Name; - ExecutingFileResult(logger, fileResultType, fileName, fileResult.FileDownloadName); - } + var fileResultType = fileResult.GetType().Name; + ExecutingFileResult(logger, fileResultType, fileName, fileResult.FileDownloadName); } + } - [LoggerMessage(1, LogLevel.Information, - "Executing {FileResultType}, sending file with download name '{FileDownloadName}'.", - EventName = "ExecutingFileResultWithNoFileName", - SkipEnabledCheck = true)] - private static partial void ExecutingFileResultWithNoFileName(ILogger logger, string fileResultType, string fileDownloadName); + [LoggerMessage(1, LogLevel.Information, + "Executing {FileResultType}, sending file with download name '{FileDownloadName}'.", + EventName = "ExecutingFileResultWithNoFileName", + SkipEnabledCheck = true)] + private static partial void ExecutingFileResultWithNoFileName(ILogger logger, string fileResultType, string fileDownloadName); - [LoggerMessage(2, LogLevel.Information, - "Executing {FileResultType}, sending file '{FileDownloadPath}' with download name '{FileDownloadName}'.", - EventName = "ExecutingFileResult", - SkipEnabledCheck = true)] - private static partial void ExecutingFileResult(ILogger logger, string fileResultType, string fileDownloadPath, string fileDownloadName); - } + [LoggerMessage(2, LogLevel.Information, + "Executing {FileResultType}, sending file '{FileDownloadPath}' with download name '{FileDownloadName}'.", + EventName = "ExecutingFileResult", + SkipEnabledCheck = true)] + private static partial void ExecutingFileResult(ILogger logger, string fileResultType, string fileDownloadPath, string fileDownloadName); } } diff --git a/src/Http/Http.Results/src/FileStreamResult.cs b/src/Http/Http.Results/src/FileStreamResult.cs index aa69a1db36..ed4202cb5c 100644 --- a/src/Http/Http.Results/src/FileStreamResult.cs +++ b/src/Http/Http.Results/src/FileStreamResult.cs @@ -5,84 +5,83 @@ using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +/// +/// Represents an that when executed will +/// write a file from a stream to the response. +/// +internal sealed class FileStreamResult : FileResult, IResult { /// - /// Represents an that when executed will - /// write a file from a stream to the response. + /// Creates a new instance with + /// the provided and the + /// provided . /// - internal sealed class FileStreamResult : FileResult, IResult + /// The stream with the file. + /// The Content-Type header of the response. + public FileStreamResult(Stream fileStream, string? contentType) + : base(contentType) { - /// - /// Creates a new instance with - /// the provided and the - /// provided . - /// - /// The stream with the file. - /// The Content-Type header of the response. - public FileStreamResult(Stream fileStream, string? contentType) - : base(contentType) + if (fileStream == null) { - if (fileStream == null) - { - throw new ArgumentNullException(nameof(fileStream)); - } - - FileStream = fileStream; + throw new ArgumentNullException(nameof(fileStream)); } - /// - /// Gets or sets the stream with the file that will be sent back as the response. - /// - public Stream FileStream { get; } + FileStream = fileStream; + } - public async Task ExecuteAsync(HttpContext httpContext) - { - var logger = httpContext.RequestServices.GetRequiredService>(); - await using (FileStream) - { - Log.ExecutingFileResult(logger, this); + /// + /// Gets or sets the stream with the file that will be sent back as the response. + /// + public Stream FileStream { get; } - long? fileLength = null; - if (FileStream.CanSeek) - { - fileLength = FileStream.Length; - } + public async Task ExecuteAsync(HttpContext httpContext) + { + var logger = httpContext.RequestServices.GetRequiredService>(); + await using (FileStream) + { + Log.ExecutingFileResult(logger, this); - var fileResultInfo = new FileResultInfo - { - ContentType = ContentType, - EnableRangeProcessing = EnableRangeProcessing, - EntityTag = EntityTag, - FileDownloadName = FileDownloadName, - }; + long? fileLength = null; + if (FileStream.CanSeek) + { + fileLength = FileStream.Length; + } - var (range, rangeLength, serveBody) = FileResultHelper.SetHeadersAndLog( - httpContext, - fileResultInfo, - fileLength, - EnableRangeProcessing, - LastModified, - EntityTag, - logger); + var fileResultInfo = new FileResultInfo + { + ContentType = ContentType, + EnableRangeProcessing = EnableRangeProcessing, + EntityTag = EntityTag, + FileDownloadName = FileDownloadName, + }; - if (!serveBody) - { - return; - } + var (range, rangeLength, serveBody) = FileResultHelper.SetHeadersAndLog( + httpContext, + fileResultInfo, + fileLength, + EnableRangeProcessing, + LastModified, + EntityTag, + logger); - if (range != null && rangeLength == 0) - { - return; - } + if (!serveBody) + { + return; + } - if (range != null) - { - FileResultHelper.Log.WritingRangeToBody(logger); - } + if (range != null && rangeLength == 0) + { + return; + } - await FileResultHelper.WriteFileAsync(httpContext, FileStream, range, rangeLength); + if (range != null) + { + FileResultHelper.Log.WritingRangeToBody(logger); } + + await FileResultHelper.WriteFileAsync(httpContext, FileStream, range, rangeLength); } } } diff --git a/src/Http/Http.Results/src/ForbidResult.cs b/src/Http/Http.Results/src/ForbidResult.cs index 9c87eb65a1..d0745fe9e8 100644 --- a/src/Http/Http.Results/src/ForbidResult.cs +++ b/src/Http/Http.Results/src/ForbidResult.cs @@ -9,117 +9,116 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal sealed partial class ForbidResult : IResult { - internal sealed partial class ForbidResult : IResult + /// + /// Initializes a new instance of . + /// + public ForbidResult() + : this(Array.Empty()) { - /// - /// Initializes a new instance of . - /// - public ForbidResult() - : this(Array.Empty()) - { - } + } - /// - /// Initializes a new instance of with the - /// specified authentication scheme. - /// - /// The authentication scheme to challenge. - public ForbidResult(string authenticationScheme) - : this(new[] { authenticationScheme }) - { - } + /// + /// Initializes a new instance of with the + /// specified authentication scheme. + /// + /// The authentication scheme to challenge. + public ForbidResult(string authenticationScheme) + : this(new[] { authenticationScheme }) + { + } - /// - /// Initializes a new instance of with the - /// specified authentication schemes. - /// - /// The authentication schemes to challenge. - public ForbidResult(IList authenticationSchemes) - : this(authenticationSchemes, properties: null) - { - } + /// + /// Initializes a new instance of with the + /// specified authentication schemes. + /// + /// The authentication schemes to challenge. + public ForbidResult(IList authenticationSchemes) + : this(authenticationSchemes, properties: null) + { + } - /// - /// Initializes a new instance of with the - /// specified . - /// - /// used to perform the authentication - /// challenge. - public ForbidResult(AuthenticationProperties? properties) - : this(Array.Empty(), properties) - { - } + /// + /// Initializes a new instance of with the + /// specified . + /// + /// used to perform the authentication + /// challenge. + public ForbidResult(AuthenticationProperties? properties) + : this(Array.Empty(), properties) + { + } - /// - /// Initializes a new instance of with the - /// specified authentication scheme and . - /// - /// The authentication schemes to challenge. - /// used to perform the authentication - /// challenge. - public ForbidResult(string authenticationScheme, AuthenticationProperties? properties) - : this(new[] { authenticationScheme }, properties) - { - } + /// + /// Initializes a new instance of with the + /// specified authentication scheme and . + /// + /// The authentication schemes to challenge. + /// used to perform the authentication + /// challenge. + public ForbidResult(string authenticationScheme, AuthenticationProperties? properties) + : this(new[] { authenticationScheme }, properties) + { + } - /// - /// Initializes a new instance of with the - /// specified authentication schemes and . - /// - /// The authentication scheme to challenge. - /// used to perform the authentication - /// challenge. - public ForbidResult(IList authenticationSchemes, AuthenticationProperties? properties) - { - AuthenticationSchemes = authenticationSchemes; - Properties = properties; - } + /// + /// Initializes a new instance of with the + /// specified authentication schemes and . + /// + /// The authentication scheme to challenge. + /// used to perform the authentication + /// challenge. + public ForbidResult(IList authenticationSchemes, AuthenticationProperties? properties) + { + AuthenticationSchemes = authenticationSchemes; + Properties = properties; + } - /// - /// Gets or sets the authentication schemes that are challenged. - /// - public IList AuthenticationSchemes { get; init; } + /// + /// Gets or sets the authentication schemes that are challenged. + /// + public IList AuthenticationSchemes { get; init; } - /// - /// Gets or sets the used to perform the authentication challenge. - /// - public AuthenticationProperties? Properties { get; init; } + /// + /// Gets or sets the used to perform the authentication challenge. + /// + public AuthenticationProperties? Properties { get; init; } - /// - public async Task ExecuteAsync(HttpContext httpContext) - { - var logger = httpContext.RequestServices.GetRequiredService>(); + /// + public async Task ExecuteAsync(HttpContext httpContext) + { + var logger = httpContext.RequestServices.GetRequiredService>(); - Log.ForbidResultExecuting(logger, AuthenticationSchemes); + Log.ForbidResultExecuting(logger, AuthenticationSchemes); - if (AuthenticationSchemes != null && AuthenticationSchemes.Count > 0) - { - for (var i = 0; i < AuthenticationSchemes.Count; i++) - { - await httpContext.ForbidAsync(AuthenticationSchemes[i], Properties); - } - } - else + if (AuthenticationSchemes != null && AuthenticationSchemes.Count > 0) + { + for (var i = 0; i < AuthenticationSchemes.Count; i++) { - await httpContext.ForbidAsync(Properties); + await httpContext.ForbidAsync(AuthenticationSchemes[i], Properties); } } + else + { + await httpContext.ForbidAsync(Properties); + } + } - private static partial class Log + private static partial class Log + { + public static void ForbidResultExecuting(ILogger logger, IList authenticationSchemes) { - public static void ForbidResultExecuting(ILogger logger, IList authenticationSchemes) + if (logger.IsEnabled(LogLevel.Information)) { - if (logger.IsEnabled(LogLevel.Information)) - { - ForbidResultExecuting(logger, authenticationSchemes.ToArray()); - } + ForbidResultExecuting(logger, authenticationSchemes.ToArray()); } - - [LoggerMessage(1, LogLevel.Information, "Executing ChallengeResult with authentication schemes ({Schemes}).", EventName = "ChallengeResultExecuting", SkipEnabledCheck = true)] - private static partial void ForbidResultExecuting(ILogger logger, string[] schemes); } + [LoggerMessage(1, LogLevel.Information, "Executing ChallengeResult with authentication schemes ({Schemes}).", EventName = "ChallengeResultExecuting", SkipEnabledCheck = true)] + private static partial void ForbidResultExecuting(ILogger logger, string[] schemes); } + } diff --git a/src/Http/Http.Results/src/IResultExtensions.cs b/src/Http/Http.Results/src/IResultExtensions.cs index 1905055d0f..eaddb35180 100644 --- a/src/Http/Http.Results/src/IResultExtensions.cs +++ b/src/Http/Http.Results/src/IResultExtensions.cs @@ -1,11 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http -{ - /// - /// Provides an interface to registering external methods that provide - /// custom IResult instances. - /// - public interface IResultExtensions { } -} \ No newline at end of file +namespace Microsoft.AspNetCore.Http; + +/// +/// Provides an interface to registering external methods that provide +/// custom IResult instances. +/// +public interface IResultExtensions { } diff --git a/src/Http/Http.Results/src/JsonResult.cs b/src/Http/Http.Results/src/JsonResult.cs index 59e7f9cba4..797f62f8e9 100644 --- a/src/Http/Http.Results/src/JsonResult.cs +++ b/src/Http/Http.Results/src/JsonResult.cs @@ -6,73 +6,72 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +/// +/// An action result which formats the given object as JSON. +/// +internal sealed partial class JsonResult : IResult { /// - /// An action result which formats the given object as JSON. + /// Gets or sets the representing the Content-Type header of the response. /// - internal sealed partial class JsonResult : IResult - { - /// - /// Gets or sets the representing the Content-Type header of the response. - /// - public string? ContentType { get; init; } - - /// - /// Gets or sets the serializer settings. - /// - /// When using System.Text.Json, this should be an instance of - /// - /// - /// When using Newtonsoft.Json, this should be an instance of JsonSerializerSettings. - /// - /// - public JsonSerializerOptions? JsonSerializerOptions { get; init; } + public string? ContentType { get; init; } - /// - /// Gets or sets the HTTP status code. - /// - public int? StatusCode { get; init; } + /// + /// Gets or sets the serializer settings. + /// + /// When using System.Text.Json, this should be an instance of + /// + /// + /// When using Newtonsoft.Json, this should be an instance of JsonSerializerSettings. + /// + /// + public JsonSerializerOptions? JsonSerializerOptions { get; init; } - /// - /// Gets or sets the value to be formatted. - /// - public object? Value { get; init; } + /// + /// Gets or sets the HTTP status code. + /// + public int? StatusCode { get; init; } - /// - /// Write the result as JSON to the HTTP response. - /// - /// The for the current request. - /// A task that represents the asynchronous execute operation. - Task IResult.ExecuteAsync(HttpContext httpContext) - { - var logger = httpContext.RequestServices.GetRequiredService>(); - Log.JsonResultExecuting(logger, Value); + /// + /// Gets or sets the value to be formatted. + /// + public object? Value { get; init; } - if (StatusCode is int statusCode) - { - httpContext.Response.StatusCode = statusCode; - } + /// + /// Write the result as JSON to the HTTP response. + /// + /// The for the current request. + /// A task that represents the asynchronous execute operation. + Task IResult.ExecuteAsync(HttpContext httpContext) + { + var logger = httpContext.RequestServices.GetRequiredService>(); + Log.JsonResultExecuting(logger, Value); - return httpContext.Response.WriteAsJsonAsync(Value, JsonSerializerOptions, ContentType); + if (StatusCode is int statusCode) + { + httpContext.Response.StatusCode = statusCode; } - private static partial class Log + return httpContext.Response.WriteAsJsonAsync(Value, JsonSerializerOptions, ContentType); + } + + private static partial class Log + { + public static void JsonResultExecuting(ILogger logger, object? value) { - public static void JsonResultExecuting(ILogger logger, object? value) + if (logger.IsEnabled(LogLevel.Information)) { - if (logger.IsEnabled(LogLevel.Information)) - { - var type = value == null ? "null" : value.GetType().FullName!; - JsonResultExecuting(logger, type); - } + var type = value == null ? "null" : value.GetType().FullName!; + JsonResultExecuting(logger, type); } - - [LoggerMessage(1, LogLevel.Information, - "Executing JsonResult, writing value of type '{Type}'.", - EventName = "JsonResultExecuting", - SkipEnabledCheck = true)] - private static partial void JsonResultExecuting(ILogger logger, string type); } + + [LoggerMessage(1, LogLevel.Information, + "Executing JsonResult, writing value of type '{Type}'.", + EventName = "JsonResultExecuting", + SkipEnabledCheck = true)] + private static partial void JsonResultExecuting(ILogger logger, string type); } } diff --git a/src/Http/Http.Results/src/LocalRedirectResult.cs b/src/Http/Http.Results/src/LocalRedirectResult.cs index d7a2efb9ec..c7ad5b8d8b 100644 --- a/src/Http/Http.Results/src/LocalRedirectResult.cs +++ b/src/Http/Http.Results/src/LocalRedirectResult.cs @@ -7,105 +7,104 @@ using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +/// +/// An that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), +/// or Permanent Redirect (308) response with a Location header to the supplied local URL. +/// +internal sealed partial class LocalRedirectResult : IResult { /// - /// An that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), - /// or Permanent Redirect (308) response with a Location header to the supplied local URL. + /// Initializes a new instance of the class with the values + /// provided. /// - internal sealed partial class LocalRedirectResult : IResult + /// The local URL to redirect to. + public LocalRedirectResult(string localUrl) + : this(localUrl, permanent: false) { - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - /// The local URL to redirect to. - public LocalRedirectResult(string localUrl) - : this(localUrl, permanent: false) - { - } + } - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - /// The local URL to redirect to. - /// Specifies whether the redirect should be permanent (301) or temporary (302). - public LocalRedirectResult(string localUrl, bool permanent) - : this(localUrl, permanent, preserveMethod: false) + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The local URL to redirect to. + /// Specifies whether the redirect should be permanent (301) or temporary (302). + public LocalRedirectResult(string localUrl, bool permanent) + : this(localUrl, permanent, preserveMethod: false) + { + } + + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The local URL to redirect to. + /// Specifies whether the redirect should be permanent (301) or temporary (302). + /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request's method. + public LocalRedirectResult(string localUrl, bool permanent, bool preserveMethod) + { + if (string.IsNullOrEmpty(localUrl)) { + throw new ArgumentException("Argument cannot be null or empty", nameof(localUrl)); } - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - /// The local URL to redirect to. - /// Specifies whether the redirect should be permanent (301) or temporary (302). - /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request's method. - public LocalRedirectResult(string localUrl, bool permanent, bool preserveMethod) + Permanent = permanent; + PreserveMethod = preserveMethod; + Url = localUrl; + } + + /// + /// Gets or sets the value that specifies that the redirect should be permanent if true or temporary if false. + /// + public bool Permanent { get; } + + /// + /// Gets or sets an indication that the redirect preserves the initial request method. + /// + public bool PreserveMethod { get; } + + /// + /// Gets or sets the local URL to redirect to. + /// + public string Url { get; } + + /// + public Task ExecuteAsync(HttpContext httpContext) + { + if (!SharedUrlHelper.IsLocalUrl(Url)) { - if (string.IsNullOrEmpty(localUrl)) - { - throw new ArgumentException("Argument cannot be null or empty", nameof(localUrl)); - } - - Permanent = permanent; - PreserveMethod = preserveMethod; - Url = localUrl; + throw new InvalidOperationException("The supplied URL is not local. A URL with an absolute path is considered local if it does not have a host/authority part. URLs using virtual paths ('~/') are also local."); } - /// - /// Gets or sets the value that specifies that the redirect should be permanent if true or temporary if false. - /// - public bool Permanent { get; } + var destinationUrl = SharedUrlHelper.Content(httpContext, Url); - /// - /// Gets or sets an indication that the redirect preserves the initial request method. - /// - public bool PreserveMethod { get; } + // IsLocalUrl is called to handle URLs starting with '~/'. + var logger = httpContext.RequestServices.GetRequiredService>(); - /// - /// Gets or sets the local URL to redirect to. - /// - public string Url { get; } + Log.LocalRedirectResultExecuting(logger, destinationUrl); - /// - public Task ExecuteAsync(HttpContext httpContext) + if (PreserveMethod) { - if (!SharedUrlHelper.IsLocalUrl(Url)) - { - throw new InvalidOperationException("The supplied URL is not local. A URL with an absolute path is considered local if it does not have a host/authority part. URLs using virtual paths ('~/') are also local."); - } - - var destinationUrl = SharedUrlHelper.Content(httpContext, Url); - - // IsLocalUrl is called to handle URLs starting with '~/'. - var logger = httpContext.RequestServices.GetRequiredService>(); - - Log.LocalRedirectResultExecuting(logger, destinationUrl); - - if (PreserveMethod) - { - httpContext.Response.StatusCode = Permanent - ? StatusCodes.Status308PermanentRedirect - : StatusCodes.Status307TemporaryRedirect; - httpContext.Response.Headers.Location = destinationUrl; - } - else - { - httpContext.Response.Redirect(destinationUrl, Permanent); - } - - return Task.CompletedTask; + httpContext.Response.StatusCode = Permanent + ? StatusCodes.Status308PermanentRedirect + : StatusCodes.Status307TemporaryRedirect; + httpContext.Response.Headers.Location = destinationUrl; } - - private static partial class Log + else { - [LoggerMessage(1, LogLevel.Information, - "Executing LocalRedirectResult, redirecting to {Destination}.", - EventName = "LocalRedirectResultExecuting")] - public static partial void LocalRedirectResultExecuting(ILogger logger, string destination); + httpContext.Response.Redirect(destinationUrl, Permanent); } + + return Task.CompletedTask; + } + + private static partial class Log + { + [LoggerMessage(1, LogLevel.Information, + "Executing LocalRedirectResult, redirecting to {Destination}.", + EventName = "LocalRedirectResultExecuting")] + public static partial void LocalRedirectResultExecuting(ILogger logger, string destination); } } diff --git a/src/Http/Http.Results/src/NoContentResult.cs b/src/Http/Http.Results/src/NoContentResult.cs index 7b143e8578..582484c406 100644 --- a/src/Http/Http.Results/src/NoContentResult.cs +++ b/src/Http/Http.Results/src/NoContentResult.cs @@ -1,12 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal class NoContentResult : StatusCodeResult { - internal class NoContentResult : StatusCodeResult + public NoContentResult() : base(StatusCodes.Status204NoContent) { - public NoContentResult() : base(StatusCodes.Status204NoContent) - { - } } } diff --git a/src/Http/Http.Results/src/NotFoundObjectResult.cs b/src/Http/Http.Results/src/NotFoundObjectResult.cs index 2588567c54..5ce0e6083b 100644 --- a/src/Http/Http.Results/src/NotFoundObjectResult.cs +++ b/src/Http/Http.Results/src/NotFoundObjectResult.cs @@ -1,13 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal sealed class NotFoundObjectResult : ObjectResult { - internal sealed class NotFoundObjectResult : ObjectResult + public NotFoundObjectResult(object? value) + : base(value, StatusCodes.Status404NotFound) { - public NotFoundObjectResult(object? value) - : base(value, StatusCodes.Status404NotFound) - { - } } } diff --git a/src/Http/Http.Results/src/ObjectResult.cs b/src/Http/Http.Results/src/ObjectResult.cs index 1b57682474..3204d866b5 100644 --- a/src/Http/Http.Results/src/ObjectResult.cs +++ b/src/Http/Http.Results/src/ObjectResult.cs @@ -6,131 +6,130 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal partial class ObjectResult : IResult { - internal partial class ObjectResult : IResult + /// + /// Creates a new instance with the provided . + /// + public ObjectResult(object? value) + { + Value = value; + } + + /// + /// Creates a new instance with the provided . + /// + public ObjectResult(object? value, int? statusCode) { - /// - /// Creates a new instance with the provided . - /// - public ObjectResult(object? value) + Value = value; + StatusCode = statusCode; + } + + /// + /// The object result. + /// + public object? Value { get; } + + /// + /// Gets the HTTP status code. + /// + public int? StatusCode { get; set; } + + /// + /// Gets the value for the Content-Type header. + /// + public string? ContentType { get; set; } + + public Task ExecuteAsync(HttpContext httpContext) + { + var loggerFactory = httpContext.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger(GetType()); + Log.ObjectResultExecuting(logger, Value, StatusCode); + + if (Value is ProblemDetails problemDetails) { - Value = value; + ApplyProblemDetailsDefaults(problemDetails); } - /// - /// Creates a new instance with the provided . - /// - public ObjectResult(object? value, int? statusCode) + if (StatusCode is { } statusCode) { - Value = value; - StatusCode = statusCode; + httpContext.Response.StatusCode = statusCode; } - /// - /// The object result. - /// - public object? Value { get; } + ConfigureResponseHeaders(httpContext); - /// - /// Gets the HTTP status code. - /// - public int? StatusCode { get; set; } + if (Value is null) + { + return Task.CompletedTask; + } - /// - /// Gets the value for the Content-Type header. - /// - public string? ContentType { get; set; } + OnFormatting(httpContext); + return httpContext.Response.WriteAsJsonAsync(Value, Value.GetType(), options: null, contentType: ContentType); + } - public Task ExecuteAsync(HttpContext httpContext) - { - var loggerFactory = httpContext.RequestServices.GetRequiredService(); - var logger = loggerFactory.CreateLogger(GetType()); - Log.ObjectResultExecuting(logger, Value, StatusCode); + protected virtual void OnFormatting(HttpContext httpContext) + { + } - if (Value is ProblemDetails problemDetails) - { - ApplyProblemDetailsDefaults(problemDetails); - } + protected virtual void ConfigureResponseHeaders(HttpContext httpContext) + { + } - if (StatusCode is { } statusCode) + private void ApplyProblemDetailsDefaults(ProblemDetails problemDetails) + { + // We allow StatusCode to be specified either on ProblemDetails or on the ObjectResult and use it to configure the other. + // This lets users write return Conflict(new Problem("some description")) + // or return Problem("some-problem", 422) and have the response have consistent fields. + if (problemDetails.Status is null) + { + if (StatusCode is not null) { - httpContext.Response.StatusCode = statusCode; + problemDetails.Status = StatusCode; } - - ConfigureResponseHeaders(httpContext); - - if (Value is null) + else { - return Task.CompletedTask; + problemDetails.Status = problemDetails is HttpValidationProblemDetails ? + StatusCodes.Status400BadRequest : + StatusCodes.Status500InternalServerError; } - - OnFormatting(httpContext); - return httpContext.Response.WriteAsJsonAsync(Value, Value.GetType(), options: null, contentType: ContentType); } - protected virtual void OnFormatting(HttpContext httpContext) + if (StatusCode is null) { + StatusCode = problemDetails.Status; } - protected virtual void ConfigureResponseHeaders(HttpContext httpContext) + if (ProblemDetailsDefaults.Defaults.TryGetValue(problemDetails.Status.Value, out var defaults)) { + problemDetails.Title ??= defaults.Title; + problemDetails.Type ??= defaults.Type; } + } - private void ApplyProblemDetailsDefaults(ProblemDetails problemDetails) + private static partial class Log + { + public static void ObjectResultExecuting(ILogger logger, object? value, int? statusCode) { - // We allow StatusCode to be specified either on ProblemDetails or on the ObjectResult and use it to configure the other. - // This lets users write return Conflict(new Problem("some description")) - // or return Problem("some-problem", 422) and have the response have consistent fields. - if (problemDetails.Status is null) + if (logger.IsEnabled(LogLevel.Information)) { - if (StatusCode is not null) + if (value is null) { - problemDetails.Status = StatusCode; + ObjectResultExecutingWithoutValue(logger, statusCode ?? StatusCodes.Status200OK); } else { - problemDetails.Status = problemDetails is HttpValidationProblemDetails ? - StatusCodes.Status400BadRequest : - StatusCodes.Status500InternalServerError; + var valueType = value.GetType().FullName!; + ObjectResultExecuting(logger, valueType, statusCode ?? StatusCodes.Status200OK); } } - - if (StatusCode is null) - { - StatusCode = problemDetails.Status; - } - - if (ProblemDetailsDefaults.Defaults.TryGetValue(problemDetails.Status.Value, out var defaults)) - { - problemDetails.Title ??= defaults.Title; - problemDetails.Type ??= defaults.Type; - } } - private static partial class Log - { - public static void ObjectResultExecuting(ILogger logger, object? value, int? statusCode) - { - if (logger.IsEnabled(LogLevel.Information)) - { - if (value is null) - { - ObjectResultExecutingWithoutValue(logger, statusCode ?? StatusCodes.Status200OK); - } - else - { - var valueType = value.GetType().FullName!; - ObjectResultExecuting(logger, valueType, statusCode ?? StatusCodes.Status200OK); - } - } - } - - [LoggerMessage(1, LogLevel.Information, "Writing value of type '{Type}' with status code '{StatusCode}'.", EventName = "ObjectResultExecuting", SkipEnabledCheck = true)] - private static partial void ObjectResultExecuting(ILogger logger, string type, int statusCode); + [LoggerMessage(1, LogLevel.Information, "Writing value of type '{Type}' with status code '{StatusCode}'.", EventName = "ObjectResultExecuting", SkipEnabledCheck = true)] + private static partial void ObjectResultExecuting(ILogger logger, string type, int statusCode); - [LoggerMessage(2, LogLevel.Information, "Executing result with status code '{StatusCode}'.", EventName = "ObjectResultExecutingWithoutValue", SkipEnabledCheck = true)] - private static partial void ObjectResultExecutingWithoutValue(ILogger logger, int statusCode); - } + [LoggerMessage(2, LogLevel.Information, "Executing result with status code '{StatusCode}'.", EventName = "ObjectResultExecutingWithoutValue", SkipEnabledCheck = true)] + private static partial void ObjectResultExecutingWithoutValue(ILogger logger, int statusCode); } } diff --git a/src/Http/Http.Results/src/OkObjectResult.cs b/src/Http/Http.Results/src/OkObjectResult.cs index 830beadac2..70013671de 100644 --- a/src/Http/Http.Results/src/OkObjectResult.cs +++ b/src/Http/Http.Results/src/OkObjectResult.cs @@ -1,13 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal sealed class OkObjectResult : ObjectResult { - internal sealed class OkObjectResult : ObjectResult + public OkObjectResult(object? value) + : base(value, StatusCodes.Status200OK) { - public OkObjectResult(object? value) - : base(value, StatusCodes.Status200OK) - { - } } } diff --git a/src/Http/Http.Results/src/PhysicalFileResult.cs b/src/Http/Http.Results/src/PhysicalFileResult.cs index af01f4bd43..8c02e03270 100644 --- a/src/Http/Http.Results/src/PhysicalFileResult.cs +++ b/src/Http/Http.Results/src/PhysicalFileResult.cs @@ -5,116 +5,115 @@ using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +/// +/// A on execution will write a file from disk to the response +/// using mechanisms provided by the host. +/// +internal sealed partial class PhysicalFileResult : FileResult, IResult { /// - /// A on execution will write a file from disk to the response - /// using mechanisms provided by the host. + /// Creates a new instance with + /// the provided and the provided . + /// + /// The path to the file. The path must be an absolute path. + /// The Content-Type header of the response. + public PhysicalFileResult(string fileName, string? contentType) + : base(contentType) + { + FileName = fileName; + } + + /// + /// Gets or sets the path to the file that will be sent back as the response. /// - internal sealed partial class PhysicalFileResult : FileResult, IResult + public string FileName { get; } + + // For testing + public Func GetFileInfoWrapper { get; init; } = + static path => new FileInfoWrapper(path); + + public Task ExecuteAsync(HttpContext httpContext) { - /// - /// Creates a new instance with - /// the provided and the provided . - /// - /// The path to the file. The path must be an absolute path. - /// The Content-Type header of the response. - public PhysicalFileResult(string fileName, string? contentType) - : base(contentType) + var fileInfo = GetFileInfoWrapper(FileName); + if (!fileInfo.Exists) { - FileName = fileName; + throw new FileNotFoundException($"Could not find file: {FileName}", FileName); } - /// - /// Gets or sets the path to the file that will be sent back as the response. - /// - public string FileName { get; } + var logger = httpContext.RequestServices.GetRequiredService>(); + + Log.ExecutingFileResult(logger, this, FileName); + + var lastModified = LastModified ?? fileInfo.LastWriteTimeUtc; + var fileResultInfo = new FileResultInfo + { + ContentType = ContentType, + EnableRangeProcessing = EnableRangeProcessing, + EntityTag = EntityTag, + FileDownloadName = FileDownloadName, + LastModified = lastModified, + }; + + var (range, rangeLength, serveBody) = FileResultHelper.SetHeadersAndLog( + httpContext, + fileResultInfo, + fileInfo.Length, + EnableRangeProcessing, + lastModified, + EntityTag, + logger); + + if (!serveBody) + { + return Task.CompletedTask; + } - // For testing - public Func GetFileInfoWrapper { get; init; } = - static path => new FileInfoWrapper(path); + if (range != null && rangeLength == 0) + { + return Task.CompletedTask; + } - public Task ExecuteAsync(HttpContext httpContext) + var response = httpContext.Response; + if (!Path.IsPathRooted(FileName)) { - var fileInfo = GetFileInfoWrapper(FileName); - if (!fileInfo.Exists) - { - throw new FileNotFoundException($"Could not find file: {FileName}", FileName); - } - - var logger = httpContext.RequestServices.GetRequiredService>(); - - Log.ExecutingFileResult(logger, this, FileName); - - var lastModified = LastModified ?? fileInfo.LastWriteTimeUtc; - var fileResultInfo = new FileResultInfo - { - ContentType = ContentType, - EnableRangeProcessing = EnableRangeProcessing, - EntityTag = EntityTag, - FileDownloadName = FileDownloadName, - LastModified = lastModified, - }; - - var (range, rangeLength, serveBody) = FileResultHelper.SetHeadersAndLog( - httpContext, - fileResultInfo, - fileInfo.Length, - EnableRangeProcessing, - lastModified, - EntityTag, - logger); - - if (!serveBody) - { - return Task.CompletedTask; - } - - if (range != null && rangeLength == 0) - { - return Task.CompletedTask; - } - - var response = httpContext.Response; - if (!Path.IsPathRooted(FileName)) - { - throw new NotSupportedException($"Path '{FileName}' was not rooted."); - } - - if (range != null) - { - FileResultHelper.Log.WritingRangeToBody(logger); - } - - var offset = 0L; - var count = (long?)null; - if (range != null) - { - offset = range.From ?? 0L; - count = rangeLength; - } - - return response.SendFileAsync( - FileName, - offset: offset, - count: count); + throw new NotSupportedException($"Path '{FileName}' was not rooted."); } - internal readonly struct FileInfoWrapper + if (range != null) { - public FileInfoWrapper(string path) - { - var fileInfo = new FileInfo(path); - Exists = fileInfo.Exists; - Length = fileInfo.Length; - LastWriteTimeUtc = fileInfo.LastWriteTimeUtc; - } + FileResultHelper.Log.WritingRangeToBody(logger); + } - public bool Exists { get; init; } + var offset = 0L; + var count = (long?)null; + if (range != null) + { + offset = range.From ?? 0L; + count = rangeLength; + } - public long Length { get; init; } + return response.SendFileAsync( + FileName, + offset: offset, + count: count); + } - public DateTimeOffset LastWriteTimeUtc { get; init; } + internal readonly struct FileInfoWrapper + { + public FileInfoWrapper(string path) + { + var fileInfo = new FileInfo(path); + Exists = fileInfo.Exists; + Length = fileInfo.Length; + LastWriteTimeUtc = fileInfo.LastWriteTimeUtc; } + + public bool Exists { get; init; } + + public long Length { get; init; } + + public DateTimeOffset LastWriteTimeUtc { get; init; } } } diff --git a/src/Http/Http.Results/src/RedirectResult.cs b/src/Http/Http.Results/src/RedirectResult.cs index ac4c3ea2dd..f8c89e5821 100644 --- a/src/Http/Http.Results/src/RedirectResult.cs +++ b/src/Http/Http.Results/src/RedirectResult.cs @@ -7,80 +7,79 @@ using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal sealed partial class RedirectResult : IResult { - internal sealed partial class RedirectResult : IResult + /// + /// Initializes a new instance of the class with the values + /// provided. + /// + /// The URL to redirect to. + /// Specifies whether the redirect should be permanent (301) or temporary (302). + /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. + public RedirectResult(string url, bool permanent, bool preserveMethod) { - /// - /// Initializes a new instance of the class with the values - /// provided. - /// - /// The URL to redirect to. - /// Specifies whether the redirect should be permanent (301) or temporary (302). - /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. - public RedirectResult(string url, bool permanent, bool preserveMethod) + if (url == null) { - if (url == null) - { - throw new ArgumentNullException(nameof(url)); - } - - if (string.IsNullOrEmpty(url)) - { - throw new ArgumentException("Argument cannot be null or empty", nameof(url)); - } + throw new ArgumentNullException(nameof(url)); + } - Permanent = permanent; - PreserveMethod = preserveMethod; - Url = url; + if (string.IsNullOrEmpty(url)) + { + throw new ArgumentException("Argument cannot be null or empty", nameof(url)); } - /// - /// Gets or sets the value that specifies that the redirect should be permanent if true or temporary if false. - /// - public bool Permanent { get; } + Permanent = permanent; + PreserveMethod = preserveMethod; + Url = url; + } - /// - /// Gets or sets an indication that the redirect preserves the initial request method. - /// - public bool PreserveMethod { get; } + /// + /// Gets or sets the value that specifies that the redirect should be permanent if true or temporary if false. + /// + public bool Permanent { get; } - /// - /// Gets or sets the URL to redirect to. - /// - public string Url { get; } + /// + /// Gets or sets an indication that the redirect preserves the initial request method. + /// + public bool PreserveMethod { get; } - /// - public Task ExecuteAsync(HttpContext httpContext) - { - var logger = httpContext.RequestServices.GetRequiredService>(); + /// + /// Gets or sets the URL to redirect to. + /// + public string Url { get; } - // IsLocalUrl is called to handle URLs starting with '~/'. - var destinationUrl = SharedUrlHelper.IsLocalUrl(Url) ? SharedUrlHelper.Content(httpContext, Url) : Url; + /// + public Task ExecuteAsync(HttpContext httpContext) + { + var logger = httpContext.RequestServices.GetRequiredService>(); - Log.RedirectResultExecuting(logger, destinationUrl); + // IsLocalUrl is called to handle URLs starting with '~/'. + var destinationUrl = SharedUrlHelper.IsLocalUrl(Url) ? SharedUrlHelper.Content(httpContext, Url) : Url; - if (PreserveMethod) - { - httpContext.Response.StatusCode = Permanent - ? StatusCodes.Status308PermanentRedirect - : StatusCodes.Status307TemporaryRedirect; - httpContext.Response.Headers.Location = destinationUrl; - } - else - { - httpContext.Response.Redirect(destinationUrl, Permanent); - } + Log.RedirectResultExecuting(logger, destinationUrl); - return Task.CompletedTask; + if (PreserveMethod) + { + httpContext.Response.StatusCode = Permanent + ? StatusCodes.Status308PermanentRedirect + : StatusCodes.Status307TemporaryRedirect; + httpContext.Response.Headers.Location = destinationUrl; } - - private static partial class Log + else { - [LoggerMessage(1, LogLevel.Information, - "Executing RedirectResult, redirecting to {Destination}.", - EventName = "RedirectResultExecuting")] - public static partial void RedirectResultExecuting(ILogger logger, string destination); + httpContext.Response.Redirect(destinationUrl, Permanent); } + + return Task.CompletedTask; + } + + private static partial class Log + { + [LoggerMessage(1, LogLevel.Information, + "Executing RedirectResult, redirecting to {Destination}.", + EventName = "RedirectResultExecuting")] + public static partial void RedirectResultExecuting(ILogger logger, string destination); } } diff --git a/src/Http/Http.Results/src/RedirectToRouteResult.cs b/src/Http/Http.Results/src/RedirectToRouteResult.cs index e531a11837..b45ca52995 100644 --- a/src/Http/Http.Results/src/RedirectToRouteResult.cs +++ b/src/Http/Http.Results/src/RedirectToRouteResult.cs @@ -7,188 +7,187 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +/// +/// An that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), +/// or Permanent Redirect (308) response with a Location header. +/// Targets a registered route. +/// +internal sealed partial class RedirectToRouteResult : IResult { /// - /// An that returns a Found (302), Moved Permanently (301), Temporary Redirect (307), - /// or Permanent Redirect (308) response with a Location header. - /// Targets a registered route. + /// Initializes a new instance of the with the values + /// provided. /// - internal sealed partial class RedirectToRouteResult : IResult + /// The parameters for the route. + public RedirectToRouteResult(object? routeValues) + : this(routeName: null, routeValues: routeValues) { - /// - /// Initializes a new instance of the with the values - /// provided. - /// - /// The parameters for the route. - public RedirectToRouteResult(object? routeValues) - : this(routeName: null, routeValues: routeValues) - { - } + } - /// - /// Initializes a new instance of the with the values - /// provided. - /// - /// The name of the route. - /// The parameters for the route. - public RedirectToRouteResult( - string? routeName, - object? routeValues) - : this(routeName, routeValues, permanent: false) - { - } + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the route. + /// The parameters for the route. + public RedirectToRouteResult( + string? routeName, + object? routeValues) + : this(routeName, routeValues, permanent: false) + { + } - /// - /// Initializes a new instance of the with the values - /// provided. - /// - /// The name of the route. - /// The parameters for the route. - /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). - public RedirectToRouteResult( - string? routeName, - object? routeValues, - bool permanent) - : this(routeName, routeValues, permanent, fragment: null) - { - } + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the route. + /// The parameters for the route. + /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). + public RedirectToRouteResult( + string? routeName, + object? routeValues, + bool permanent) + : this(routeName, routeValues, permanent, fragment: null) + { + } - /// - /// Initializes a new instance of the with the values - /// provided. - /// - /// The name of the route. - /// The parameters for the route. - /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). - /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. - public RedirectToRouteResult( - string? routeName, - object? routeValues, - bool permanent, - bool preserveMethod) - : this(routeName, routeValues, permanent, preserveMethod, fragment: null) - { - } + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the route. + /// The parameters for the route. + /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). + /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. + public RedirectToRouteResult( + string? routeName, + object? routeValues, + bool permanent, + bool preserveMethod) + : this(routeName, routeValues, permanent, preserveMethod, fragment: null) + { + } - /// - /// Initializes a new instance of the with the values - /// provided. - /// - /// The name of the route. - /// The parameters for the route. - /// The fragment to add to the URL. - public RedirectToRouteResult( - string? routeName, - object? routeValues, - string? fragment) - : this(routeName, routeValues, permanent: false, fragment: fragment) - { - } + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the route. + /// The parameters for the route. + /// The fragment to add to the URL. + public RedirectToRouteResult( + string? routeName, + object? routeValues, + string? fragment) + : this(routeName, routeValues, permanent: false, fragment: fragment) + { + } - /// - /// Initializes a new instance of the with the values - /// provided. - /// - /// The name of the route. - /// The parameters for the route. - /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). - /// The fragment to add to the URL. - public RedirectToRouteResult( - string? routeName, - object? routeValues, - bool permanent, - string? fragment) - : this(routeName, routeValues, permanent, preserveMethod: false, fragment: fragment) - { - } + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the route. + /// The parameters for the route. + /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). + /// The fragment to add to the URL. + public RedirectToRouteResult( + string? routeName, + object? routeValues, + bool permanent, + string? fragment) + : this(routeName, routeValues, permanent, preserveMethod: false, fragment: fragment) + { + } + + /// + /// Initializes a new instance of the with the values + /// provided. + /// + /// The name of the route. + /// The parameters for the route. + /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). + /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. + /// The fragment to add to the URL. + public RedirectToRouteResult( + string? routeName, + object? routeValues, + bool permanent, + bool preserveMethod, + string? fragment) + { + RouteName = routeName; + RouteValues = routeValues == null ? null : new RouteValueDictionary(routeValues); + PreserveMethod = preserveMethod; + Permanent = permanent; + Fragment = fragment; + } + + /// + /// Gets or sets the name of the route to use for generating the URL. + /// + public string? RouteName { get; } + + /// + /// Gets or sets the route data to use for generating the URL. + /// + public RouteValueDictionary? RouteValues { get; } + + /// + /// Gets or sets an indication that the redirect is permanent. + /// + public bool Permanent { get; } + + /// + /// Gets or sets an indication that the redirect preserves the initial request method. + /// + public bool PreserveMethod { get; } + + /// + /// Gets or sets the fragment to add to the URL. + /// + public string? Fragment { get; } - /// - /// Initializes a new instance of the with the values - /// provided. - /// - /// The name of the route. - /// The parameters for the route. - /// If set to true, makes the redirect permanent (301). Otherwise a temporary redirect is used (302). - /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. - /// The fragment to add to the URL. - public RedirectToRouteResult( - string? routeName, - object? routeValues, - bool permanent, - bool preserveMethod, - string? fragment) + /// + public Task ExecuteAsync(HttpContext httpContext) + { + var linkGenerator = httpContext.RequestServices.GetRequiredService(); + + var destinationUrl = linkGenerator.GetUriByRouteValues( + httpContext, + RouteName, + RouteValues, + fragment: Fragment == null ? FragmentString.Empty : new FragmentString("#" + Fragment)); + if (string.IsNullOrEmpty(destinationUrl)) { - RouteName = routeName; - RouteValues = routeValues == null ? null : new RouteValueDictionary(routeValues); - PreserveMethod = preserveMethod; - Permanent = permanent; - Fragment = fragment; + throw new InvalidOperationException("No route matches the supplied values."); } - /// - /// Gets or sets the name of the route to use for generating the URL. - /// - public string? RouteName { get; } - - /// - /// Gets or sets the route data to use for generating the URL. - /// - public RouteValueDictionary? RouteValues { get; } - - /// - /// Gets or sets an indication that the redirect is permanent. - /// - public bool Permanent { get; } - - /// - /// Gets or sets an indication that the redirect preserves the initial request method. - /// - public bool PreserveMethod { get; } - - /// - /// Gets or sets the fragment to add to the URL. - /// - public string? Fragment { get; } - - /// - public Task ExecuteAsync(HttpContext httpContext) + var logger = httpContext.RequestServices.GetRequiredService>(); + Log.RedirectToRouteResultExecuting(logger, destinationUrl, RouteName); + + if (PreserveMethod) { - var linkGenerator = httpContext.RequestServices.GetRequiredService(); - - var destinationUrl = linkGenerator.GetUriByRouteValues( - httpContext, - RouteName, - RouteValues, - fragment: Fragment == null ? FragmentString.Empty : new FragmentString("#" + Fragment)); - if (string.IsNullOrEmpty(destinationUrl)) - { - throw new InvalidOperationException("No route matches the supplied values."); - } - - var logger = httpContext.RequestServices.GetRequiredService>(); - Log.RedirectToRouteResultExecuting(logger, destinationUrl, RouteName); - - if (PreserveMethod) - { - httpContext.Response.StatusCode = Permanent ? - StatusCodes.Status308PermanentRedirect : StatusCodes.Status307TemporaryRedirect; - httpContext.Response.Headers.Location = destinationUrl; - } - else - { - httpContext.Response.Redirect(destinationUrl, Permanent); - } - - return Task.CompletedTask; + httpContext.Response.StatusCode = Permanent ? + StatusCodes.Status308PermanentRedirect : StatusCodes.Status307TemporaryRedirect; + httpContext.Response.Headers.Location = destinationUrl; } - - private static partial class Log + else { - [LoggerMessage(1, LogLevel.Information, - "Executing RedirectToRouteResult, redirecting to {Destination} from route {RouteName}.", - EventName = "RedirectToRouteResultExecuting")] - public static partial void RedirectToRouteResultExecuting(ILogger logger, string destination, string? routeName); + httpContext.Response.Redirect(destinationUrl, Permanent); } + + return Task.CompletedTask; + } + + private static partial class Log + { + [LoggerMessage(1, LogLevel.Information, + "Executing RedirectToRouteResult, redirecting to {Destination} from route {RouteName}.", + EventName = "RedirectToRouteResultExecuting")] + public static partial void RedirectToRouteResultExecuting(ILogger logger, string destination, string? routeName); } } diff --git a/src/Http/Http.Results/src/ResultExtensions.cs b/src/Http/Http.Results/src/ResultExtensions.cs index cab72d4ff2..d10a3bd59a 100644 --- a/src/Http/Http.Results/src/ResultExtensions.cs +++ b/src/Http/Http.Results/src/ResultExtensions.cs @@ -1,11 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http -{ - /// - /// Implements an interface for registering external methods that provide - /// custom IResult instances. - /// - internal class ResultExtensions : IResultExtensions { } -} \ No newline at end of file +namespace Microsoft.AspNetCore.Http; + +/// +/// Implements an interface for registering external methods that provide +/// custom IResult instances. +/// +internal class ResultExtensions : IResultExtensions { } diff --git a/src/Http/Http.Results/src/Results.cs b/src/Http/Http.Results/src/Results.cs index 9632ac0a3b..0b7f0a228d 100644 --- a/src/Http/Http.Results/src/Results.cs +++ b/src/Http/Http.Results/src/Results.cs @@ -11,622 +11,621 @@ using Microsoft.AspNetCore.Http.Result; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// A factory for . +/// +public static class Results { /// - /// A factory for . + /// Creates an that on execution invokes . + /// + /// The behavior of this method depends on the in use. + /// and + /// are among likely status results. + /// + /// + /// used to perform the authentication + /// challenge. + /// The authentication schemes to challenge. + /// The created for the response. + public static IResult Challenge( + AuthenticationProperties? properties = null, + IList? authenticationSchemes = null) + => new ChallengeResult { AuthenticationSchemes = authenticationSchemes ?? Array.Empty(), Properties = properties }; + + /// + /// Creates a that on execution invokes . + /// + /// By default, executing this result returns a . Some authentication schemes, such as cookies, + /// will convert to a redirect to show a login page. + /// + /// + /// used to perform the authentication + /// challenge. + /// The authentication schemes to challenge. + /// The created for the response. + /// + /// Some authentication schemes, such as cookies, will convert to + /// a redirect to show a login page. + /// + public static IResult Forbid(AuthenticationProperties? properties = null, IList? authenticationSchemes = null) + => new ForbidResult { Properties = properties, AuthenticationSchemes = authenticationSchemes ?? Array.Empty(), }; + + /// + /// Creates an that on execution invokes . + /// + /// The containing the user claims. + /// used to perform the sign-in operation. + /// The authentication scheme to use for the sign-in operation. + /// The created for the response. + public static IResult SignIn( + ClaimsPrincipal principal, + AuthenticationProperties? properties = null, + string? authenticationScheme = null) + => new SignInResult(authenticationScheme, principal, properties); + + /// + /// Creates an that on execution invokes . + /// + /// used to perform the sign-out operation. + /// The authentication scheme to use for the sign-out operation. + /// The created for the response. + public static IResult SignOut(AuthenticationProperties? properties = null, IList? authenticationSchemes = null) + => new SignOutResult(authenticationSchemes ?? Array.Empty(), properties); + + /// + /// Writes the string to the HTTP response. + /// + /// This is an alias for . + /// + /// + /// The content to write to the response. + /// The content type (MIME type). + /// The content encoding. + /// The created object for the response. + /// + /// If encoding is provided by both the 'charset' and the parameters, then + /// the parameter is chosen as the final encoding. + /// + public static IResult Content(string content, string? contentType = null, Encoding? contentEncoding = null) + => Text(content, contentType, contentEncoding); + + /// + /// Writes the string to the HTTP response. + /// + /// This is an alias for . + /// /// - public static class Results + /// The content to write to the response. + /// The content type (MIME type). + /// The content encoding. + /// The created object for the response. + /// + /// If encoding is provided by both the 'charset' and the parameters, then + /// the parameter is chosen as the final encoding. + /// + public static IResult Text(string content, string? contentType = null, Encoding? contentEncoding = null) { - /// - /// Creates an that on execution invokes . - /// - /// The behavior of this method depends on the in use. - /// and - /// are among likely status results. - /// - /// - /// used to perform the authentication - /// challenge. - /// The authentication schemes to challenge. - /// The created for the response. - public static IResult Challenge( - AuthenticationProperties? properties = null, - IList? authenticationSchemes = null) - => new ChallengeResult { AuthenticationSchemes = authenticationSchemes ?? Array.Empty(), Properties = properties }; - - /// - /// Creates a that on execution invokes . - /// - /// By default, executing this result returns a . Some authentication schemes, such as cookies, - /// will convert to a redirect to show a login page. - /// - /// - /// used to perform the authentication - /// challenge. - /// The authentication schemes to challenge. - /// The created for the response. - /// - /// Some authentication schemes, such as cookies, will convert to - /// a redirect to show a login page. - /// - public static IResult Forbid(AuthenticationProperties? properties = null, IList? authenticationSchemes = null) - => new ForbidResult { Properties = properties, AuthenticationSchemes = authenticationSchemes ?? Array.Empty(), }; - - /// - /// Creates an that on execution invokes . - /// - /// The containing the user claims. - /// used to perform the sign-in operation. - /// The authentication scheme to use for the sign-in operation. - /// The created for the response. - public static IResult SignIn( - ClaimsPrincipal principal, - AuthenticationProperties? properties = null, - string? authenticationScheme = null) - => new SignInResult(authenticationScheme, principal, properties); - - /// - /// Creates an that on execution invokes . - /// - /// used to perform the sign-out operation. - /// The authentication scheme to use for the sign-out operation. - /// The created for the response. - public static IResult SignOut(AuthenticationProperties? properties = null, IList? authenticationSchemes = null) - => new SignOutResult(authenticationSchemes ?? Array.Empty(), properties); - - /// - /// Writes the string to the HTTP response. - /// - /// This is an alias for . - /// - /// - /// The content to write to the response. - /// The content type (MIME type). - /// The content encoding. - /// The created object for the response. - /// - /// If encoding is provided by both the 'charset' and the parameters, then - /// the parameter is chosen as the final encoding. - /// - public static IResult Content(string content, string? contentType = null, Encoding? contentEncoding = null) - => Text(content, contentType, contentEncoding); - - /// - /// Writes the string to the HTTP response. - /// - /// This is an alias for . - /// - /// - /// The content to write to the response. - /// The content type (MIME type). - /// The content encoding. - /// The created object for the response. - /// - /// If encoding is provided by both the 'charset' and the parameters, then - /// the parameter is chosen as the final encoding. - /// - public static IResult Text(string content, string? contentType = null, Encoding? contentEncoding = null) + MediaTypeHeaderValue? mediaTypeHeaderValue = null; + if (contentType is not null) { - MediaTypeHeaderValue? mediaTypeHeaderValue = null; - if (contentType is not null) - { - mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(contentType); - mediaTypeHeaderValue.Encoding = contentEncoding ?? mediaTypeHeaderValue.Encoding; - } - - return new ContentResult - { - Content = content, - ContentType = mediaTypeHeaderValue?.ToString() - }; + mediaTypeHeaderValue = MediaTypeHeaderValue.Parse(contentType); + mediaTypeHeaderValue.Encoding = contentEncoding ?? mediaTypeHeaderValue.Encoding; } - /// - /// Writes the string to the HTTP response. - /// - /// The content to write to the response. - /// The content type (MIME type). - /// The created object for the response. - public static IResult Content(string content, MediaTypeHeaderValue contentType) - => new ContentResult - { - Content = content, - ContentType = contentType.ToString() - }; + return new ContentResult + { + Content = content, + ContentType = mediaTypeHeaderValue?.ToString() + }; + } - /// - /// Creates a that serializes the specified object to JSON. - /// - /// The object to write as JSON. - /// The serializer options use when serializing the value. - /// The content-type to set on the response. - /// The status code to set on the response. - /// The created that serializes the specified - /// as JSON format for the response. - /// Callers should cache an instance of serializer settings to avoid - /// recreating cached data with each call. - public static IResult Json(object? data, JsonSerializerOptions? options = null, string? contentType = null, int? statusCode = null) - => new JsonResult - { - Value = data, - JsonSerializerOptions = options, - ContentType = contentType, - StatusCode = statusCode, - }; + /// + /// Writes the string to the HTTP response. + /// + /// The content to write to the response. + /// The content type (MIME type). + /// The created object for the response. + public static IResult Content(string content, MediaTypeHeaderValue contentType) + => new ContentResult + { + Content = content, + ContentType = contentType.ToString() + }; - /// - /// Writes the byte-array content to the response. - /// - /// This supports range requests ( or - /// if the range is not satisfiable). - /// - /// - /// This API is an alias for . - /// - /// The file contents. - /// The Content-Type of the file. - /// The suggested file name. - /// Set to true to enable range requests processing. - /// The of when the file was last modified. - /// The associated with the file. - /// The created for the response. + /// + /// Creates a that serializes the specified object to JSON. + /// + /// The object to write as JSON. + /// The serializer options use when serializing the value. + /// The content-type to set on the response. + /// The status code to set on the response. + /// The created that serializes the specified + /// as JSON format for the response. + /// Callers should cache an instance of serializer settings to avoid + /// recreating cached data with each call. + public static IResult Json(object? data, JsonSerializerOptions? options = null, string? contentType = null, int? statusCode = null) + => new JsonResult + { + Value = data, + JsonSerializerOptions = options, + ContentType = contentType, + StatusCode = statusCode, + }; + + /// + /// Writes the byte-array content to the response. + /// + /// This supports range requests ( or + /// if the range is not satisfiable). + /// + /// + /// This API is an alias for . + /// + /// The file contents. + /// The Content-Type of the file. + /// The suggested file name. + /// Set to true to enable range requests processing. + /// The of when the file was last modified. + /// The associated with the file. + /// The created for the response. #pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters - public static IResult File( + public static IResult File( #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters byte[] fileContents, - string? contentType = null, - string? fileDownloadName = null, - bool enableRangeProcessing = false, - DateTimeOffset? lastModified = null, - EntityTagHeaderValue? entityTag = null) - => new FileContentResult(fileContents, contentType) - { - FileDownloadName = fileDownloadName, - EnableRangeProcessing = enableRangeProcessing, - LastModified = lastModified, - EntityTag = entityTag, - }; + string? contentType = null, + string? fileDownloadName = null, + bool enableRangeProcessing = false, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null) + => new FileContentResult(fileContents, contentType) + { + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + LastModified = lastModified, + EntityTag = entityTag, + }; - /// - /// Writes the byte-array content to the response. - /// - /// This supports range requests ( or - /// if the range is not satisfiable). - /// - /// - /// This API is an alias for . - /// - /// The file contents. - /// The Content-Type of the file. - /// The suggested file name. - /// Set to true to enable range requests processing. - /// The of when the file was last modified. - /// The associated with the file. - /// The created for the response. - public static IResult Bytes( - byte[] contents, - string? contentType = null, - string? fileDownloadName = null, - bool enableRangeProcessing = false, - DateTimeOffset? lastModified = null, - EntityTagHeaderValue? entityTag = null) - => new FileContentResult(contents, contentType) - { - FileDownloadName = fileDownloadName, - EnableRangeProcessing = enableRangeProcessing, - LastModified = lastModified, - EntityTag = entityTag, - }; + /// + /// Writes the byte-array content to the response. + /// + /// This supports range requests ( or + /// if the range is not satisfiable). + /// + /// + /// This API is an alias for . + /// + /// The file contents. + /// The Content-Type of the file. + /// The suggested file name. + /// Set to true to enable range requests processing. + /// The of when the file was last modified. + /// The associated with the file. + /// The created for the response. + public static IResult Bytes( + byte[] contents, + string? contentType = null, + string? fileDownloadName = null, + bool enableRangeProcessing = false, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null) + => new FileContentResult(contents, contentType) + { + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + LastModified = lastModified, + EntityTag = entityTag, + }; - /// - /// Writes the specified to the response. - /// - /// This supports range requests ( or - /// if the range is not satisfiable). - /// - /// - /// This API is an alias for . - /// - /// - /// The with the contents of the file. - /// The Content-Type of the file. - /// The the file name to be used in the Content-Disposition header. - /// The of when the file was last modified. - /// Used to configure the Last-Modified response header and perform conditional range requests. - /// The to be configure the ETag response header - /// and perform conditional requests. - /// Set to true to enable range requests processing. - /// The created for the response. - /// - /// The parameter is disposed after the response is sent. - /// + /// + /// Writes the specified to the response. + /// + /// This supports range requests ( or + /// if the range is not satisfiable). + /// + /// + /// This API is an alias for . + /// + /// + /// The with the contents of the file. + /// The Content-Type of the file. + /// The the file name to be used in the Content-Disposition header. + /// The of when the file was last modified. + /// Used to configure the Last-Modified response header and perform conditional range requests. + /// The to be configure the ETag response header + /// and perform conditional requests. + /// Set to true to enable range requests processing. + /// The created for the response. + /// + /// The parameter is disposed after the response is sent. + /// #pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters - public static IResult File( + public static IResult File( #pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters Stream fileStream, - string? contentType = null, - string? fileDownloadName = null, - DateTimeOffset? lastModified = null, - EntityTagHeaderValue? entityTag = null, - bool enableRangeProcessing = false) + string? contentType = null, + string? fileDownloadName = null, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null, + bool enableRangeProcessing = false) + { + return new FileStreamResult(fileStream, contentType) { - return new FileStreamResult(fileStream, contentType) + LastModified = lastModified, + EntityTag = entityTag, + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + }; + } + + /// + /// Writes the specified to the response. + /// + /// This supports range requests ( or + /// if the range is not satisfiable). + /// + /// + /// This API is an alias for . + /// + /// + /// The to write to the response. + /// The Content-Type of the response. Defaults to application/octet-stream. + /// The the file name to be used in the Content-Disposition header. + /// The of when the file was last modified. + /// Used to configure the Last-Modified response header and perform conditional range requests. + /// The to be configure the ETag response header + /// and perform conditional requests. + /// Set to true to enable range requests processing. + /// The created for the response. + /// + /// The parameter is disposed after the response is sent. + /// + public static IResult Stream( + Stream stream, + string? contentType = null, + string? fileDownloadName = null, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null, + bool enableRangeProcessing = false) + { + return new FileStreamResult(stream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + FileDownloadName = fileDownloadName, + EnableRangeProcessing = enableRangeProcessing, + }; + } + + /// + /// Writes the file at the specified to the response. + /// + /// This supports range requests ( or + /// if the range is not satisfiable). + /// + /// + /// The path to the file. When not rooted, resolves the path relative to . + /// The Content-Type of the file. + /// The suggested file name. + /// The of when the file was last modified. + /// The associated with the file. + /// Set to true to enable range requests processing. + /// The created for the response. +#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters + public static IResult File( +#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters + string path, + string? contentType = null, + string? fileDownloadName = null, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue? entityTag = null, + bool enableRangeProcessing = false) + { + if (Path.IsPathRooted(path)) + { + return new PhysicalFileResult(path, contentType) { + FileDownloadName = fileDownloadName, LastModified = lastModified, EntityTag = entityTag, - FileDownloadName = fileDownloadName, EnableRangeProcessing = enableRangeProcessing, }; } - - /// - /// Writes the specified to the response. - /// - /// This supports range requests ( or - /// if the range is not satisfiable). - /// - /// - /// This API is an alias for . - /// - /// - /// The to write to the response. - /// The Content-Type of the response. Defaults to application/octet-stream. - /// The the file name to be used in the Content-Disposition header. - /// The of when the file was last modified. - /// Used to configure the Last-Modified response header and perform conditional range requests. - /// The to be configure the ETag response header - /// and perform conditional requests. - /// Set to true to enable range requests processing. - /// The created for the response. - /// - /// The parameter is disposed after the response is sent. - /// - public static IResult Stream( - Stream stream, - string? contentType = null, - string? fileDownloadName = null, - DateTimeOffset? lastModified = null, - EntityTagHeaderValue? entityTag = null, - bool enableRangeProcessing = false) + else { - return new FileStreamResult(stream, contentType) + return new VirtualFileResult(path, contentType) { + FileDownloadName = fileDownloadName, LastModified = lastModified, EntityTag = entityTag, - FileDownloadName = fileDownloadName, EnableRangeProcessing = enableRangeProcessing, }; } + } - /// - /// Writes the file at the specified to the response. - /// - /// This supports range requests ( or - /// if the range is not satisfiable). - /// - /// - /// The path to the file. When not rooted, resolves the path relative to . - /// The Content-Type of the file. - /// The suggested file name. - /// The of when the file was last modified. - /// The associated with the file. - /// Set to true to enable range requests processing. - /// The created for the response. -#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters - public static IResult File( -#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters - string path, - string? contentType = null, - string? fileDownloadName = null, - DateTimeOffset? lastModified = null, - EntityTagHeaderValue? entityTag = null, - bool enableRangeProcessing = false) + /// + /// Redirects to the specified . + /// + /// When and are set, sets the status code. + /// When is set, sets the status code. + /// When is set, sets the status code. + /// Otherwise, configures . + /// + /// + /// The URL to redirect to. + /// Specifies whether the redirect should be permanent (301) or temporary (302). + /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. + /// The created for the response. + public static IResult Redirect(string url, bool permanent = false, bool preserveMethod = false) + => new RedirectResult(url, permanent, preserveMethod); + + /// + /// Redirects to the specified . + /// + /// When and are set, sets the status code. + /// When is set, sets the status code. + /// When is set, sets the status code. + /// Otherwise, configures . + /// + /// + /// The local URL to redirect to. + /// Specifies whether the redirect should be permanent (301) or temporary (302). + /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. + /// The created for the response. + public static IResult LocalRedirect(string localUrl, bool permanent = false, bool preserveMethod = false) + => new LocalRedirectResult(localUrl, permanent, preserveMethod); + + /// + /// Redirects to the specified route. + /// + /// When and are set, sets the status code. + /// When is set, sets the status code. + /// When is set, sets the status code. + /// Otherwise, configures . + /// + /// + /// The name of the route. + /// The parameters for a route. + /// Specifies whether the redirect should be permanent (301) or temporary (302). + /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. + /// The fragment to add to the URL. + /// The created for the response. + public static IResult RedirectToRoute(string? routeName = null, object? routeValues = null, bool permanent = false, bool preserveMethod = false, string? fragment = null) + => new RedirectToRouteResult( + routeName: routeName, + routeValues: routeValues, + permanent: permanent, + preserveMethod: preserveMethod, + fragment: fragment); + + /// + /// Creates a object by specifying a . + /// + /// The status code to set on the response. + /// The created object for the response. + public static IResult StatusCode(int statusCode) + => new StatusCodeResult(statusCode); + + /// + /// Produces a response. + /// + /// The value to be included in the HTTP response body. + /// The created for the response. + public static IResult NotFound(object? value = null) + => new NotFoundObjectResult(value); + + /// + /// Produces a response. + /// + /// The created for the response. + public static IResult Unauthorized() + => new UnauthorizedResult(); + + /// + /// Produces a response. + /// + /// An error object to be included in the HTTP response body. + /// The created for the response. + public static IResult BadRequest(object? error = null) + => new BadRequestObjectResult(error); + + /// + /// Produces a response. + /// + /// An error object to be included in the HTTP response body. + /// The created for the response. + public static IResult Conflict(object? error = null) + => new ConflictObjectResult(error); + + /// + /// Produces a response. + /// + /// The created for the response. + public static IResult NoContent() + => new NoContentResult(); + + /// + /// Produces a response. + /// + /// The value to be included in the HTTP response body. + /// The created for the response. + public static IResult Ok(object? value = null) + => new OkObjectResult(value); + + /// + /// Produces a response. + /// + /// An error object to be included in the HTTP response body. + /// The created for the response. + public static IResult UnprocessableEntity(object? error = null) + => new UnprocessableEntityObjectResult(error); + + /// + /// Produces a response. + /// + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The value for . + /// The created for the response. + public static IResult Problem( + string? detail = null, + string? instance = null, + int? statusCode = null, + string? title = null, + string? type = null, + IDictionary? extensions = null) + { + var problemDetails = new ProblemDetails { - if (Path.IsPathRooted(path)) - { - return new PhysicalFileResult(path, contentType) - { - FileDownloadName = fileDownloadName, - LastModified = lastModified, - EntityTag = entityTag, - EnableRangeProcessing = enableRangeProcessing, - }; - } - else + Detail = detail, + Instance = instance, + Status = statusCode, + Title = title, + Type = type, + }; + + if (extensions is not null) + { + foreach (var extension in extensions) { - return new VirtualFileResult(path, contentType) - { - FileDownloadName = fileDownloadName, - LastModified = lastModified, - EntityTag = entityTag, - EnableRangeProcessing = enableRangeProcessing, - }; + problemDetails.Extensions.Add(extension); } } - /// - /// Redirects to the specified . - /// - /// When and are set, sets the status code. - /// When is set, sets the status code. - /// When is set, sets the status code. - /// Otherwise, configures . - /// - /// - /// The URL to redirect to. - /// Specifies whether the redirect should be permanent (301) or temporary (302). - /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. - /// The created for the response. - public static IResult Redirect(string url, bool permanent = false, bool preserveMethod = false) - => new RedirectResult(url, permanent, preserveMethod); - - /// - /// Redirects to the specified . - /// - /// When and are set, sets the status code. - /// When is set, sets the status code. - /// When is set, sets the status code. - /// Otherwise, configures . - /// - /// - /// The local URL to redirect to. - /// Specifies whether the redirect should be permanent (301) or temporary (302). - /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. - /// The created for the response. - public static IResult LocalRedirect(string localUrl, bool permanent = false, bool preserveMethod = false) - => new LocalRedirectResult(localUrl, permanent, preserveMethod); - - /// - /// Redirects to the specified route. - /// - /// When and are set, sets the status code. - /// When is set, sets the status code. - /// When is set, sets the status code. - /// Otherwise, configures . - /// - /// - /// The name of the route. - /// The parameters for a route. - /// Specifies whether the redirect should be permanent (301) or temporary (302). - /// If set to true, make the temporary redirect (307) or permanent redirect (308) preserve the initial request method. - /// The fragment to add to the URL. - /// The created for the response. - public static IResult RedirectToRoute(string? routeName = null, object? routeValues = null, bool permanent = false, bool preserveMethod = false, string? fragment = null) - => new RedirectToRouteResult( - routeName: routeName, - routeValues: routeValues, - permanent: permanent, - preserveMethod: preserveMethod, - fragment: fragment); - - /// - /// Creates a object by specifying a . - /// - /// The status code to set on the response. - /// The created object for the response. - public static IResult StatusCode(int statusCode) - => new StatusCodeResult(statusCode); - - /// - /// Produces a response. - /// - /// The value to be included in the HTTP response body. - /// The created for the response. - public static IResult NotFound(object? value = null) - => new NotFoundObjectResult(value); - - /// - /// Produces a response. - /// - /// The created for the response. - public static IResult Unauthorized() - => new UnauthorizedResult(); - - /// - /// Produces a response. - /// - /// An error object to be included in the HTTP response body. - /// The created for the response. - public static IResult BadRequest(object? error = null) - => new BadRequestObjectResult(error); - - /// - /// Produces a response. - /// - /// An error object to be included in the HTTP response body. - /// The created for the response. - public static IResult Conflict(object? error = null) - => new ConflictObjectResult(error); - - /// - /// Produces a response. - /// - /// The created for the response. - public static IResult NoContent() - => new NoContentResult(); - - /// - /// Produces a response. - /// - /// The value to be included in the HTTP response body. - /// The created for the response. - public static IResult Ok(object? value = null) - => new OkObjectResult(value); - - /// - /// Produces a response. - /// - /// An error object to be included in the HTTP response body. - /// The created for the response. - public static IResult UnprocessableEntity(object? error = null) - => new UnprocessableEntityObjectResult(error); - - /// - /// Produces a response. - /// - /// The value for . - /// The value for . - /// The value for . - /// The value for . - /// The value for . - /// The value for . - /// The created for the response. - public static IResult Problem( - string? detail = null, - string? instance = null, - int? statusCode = null, - string? title = null, - string? type = null, - IDictionary? extensions = null) + return new ObjectResult(problemDetails) { - var problemDetails = new ProblemDetails - { - Detail = detail, - Instance = instance, - Status = statusCode, - Title = title, - Type = type, - }; - - if (extensions is not null) - { - foreach (var extension in extensions) - { - problemDetails.Extensions.Add(extension); - } - } - - return new ObjectResult(problemDetails) - { - ContentType = "application/problem+json", - }; - } + ContentType = "application/problem+json", + }; + } - /// - /// Produces a response. - /// - /// The object to produce a response from. - /// The created for the response. - public static IResult Problem(ProblemDetails problemDetails) + /// + /// Produces a response. + /// + /// The object to produce a response from. + /// The created for the response. + public static IResult Problem(ProblemDetails problemDetails) + { + return new ObjectResult(problemDetails) { - return new ObjectResult(problemDetails) - { - ContentType = "application/problem+json", - }; - } + ContentType = "application/problem+json", + }; + } - /// - /// Produces a response - /// with a value. - /// - /// One or more validation errors. - /// The value for . - /// The value for . - /// The status code. - /// The value for . Defaults to "One or more validation errors occurred." - /// The value for . - /// The value for . - /// The created for the response. - public static IResult ValidationProblem( - IDictionary errors, - string? detail = null, - string? instance = null, - int? statusCode = null, - string? title = null, - string? type = null, - IDictionary? extensions = null) + /// + /// Produces a response + /// with a value. + /// + /// One or more validation errors. + /// The value for . + /// The value for . + /// The status code. + /// The value for . Defaults to "One or more validation errors occurred." + /// The value for . + /// The value for . + /// The created for the response. + public static IResult ValidationProblem( + IDictionary errors, + string? detail = null, + string? instance = null, + int? statusCode = null, + string? title = null, + string? type = null, + IDictionary? extensions = null) + { + var problemDetails = new HttpValidationProblemDetails(errors) { - var problemDetails = new HttpValidationProblemDetails(errors) - { - Detail = detail, - Instance = instance, - Type = type, - Status = statusCode, - }; - - problemDetails.Title = title ?? problemDetails.Title; + Detail = detail, + Instance = instance, + Type = type, + Status = statusCode, + }; - if (extensions is not null) - { - foreach (var extension in extensions) - { - problemDetails.Extensions.Add(extension); - } - } + problemDetails.Title = title ?? problemDetails.Title; - return new ObjectResult(problemDetails) + if (extensions is not null) + { + foreach (var extension in extensions) { - ContentType = "application/problem+json", - }; + problemDetails.Extensions.Add(extension); + } } - /// - /// Produces a response. - /// - /// The URI at which the content has been created. - /// The value to be included in the HTTP response body. - /// The created for the response. - public static IResult Created(string uri, object? value) + return new ObjectResult(problemDetails) { - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } + ContentType = "application/problem+json", + }; + } - return new CreatedResult(uri, value); + /// + /// Produces a response. + /// + /// The URI at which the content has been created. + /// The value to be included in the HTTP response body. + /// The created for the response. + public static IResult Created(string uri, object? value) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); } - /// - /// Produces a response. - /// - /// The URI at which the content has been created. - /// The value to be included in the HTTP response body. - /// The created for the response. - public static IResult Created(Uri uri, object? value) - { - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } + return new CreatedResult(uri, value); + } - return new CreatedResult(uri, value); + /// + /// Produces a response. + /// + /// The URI at which the content has been created. + /// The value to be included in the HTTP response body. + /// The created for the response. + public static IResult Created(Uri uri, object? value) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); } - /// - /// Produces a response. - /// - /// The name of the route to use for generating the URL. - /// The route data to use for generating the URL. - /// The value to be included in the HTTP response body. - /// The created for the response. - public static IResult CreatedAtRoute(string? routeName = null, object? routeValues = null, object? value = null) - => new CreatedAtRouteResult(routeName, routeValues, value); - - /// - /// Produces a response. - /// - /// The URI with the location at which the status of requested content can be monitored. - /// The optional content value to format in the response body. - /// The created for the response. - public static IResult Accepted(string? uri = null, object? value = null) - => new AcceptedResult(uri, value); - - /// - /// Produces a response. - /// - /// The name of the route to use for generating the URL. - /// The route data to use for generating the URL. - /// The optional content value to format in the response body. - /// The created for the response. - public static IResult AcceptedAtRoute(string? routeName = null, object? routeValues = null, object? value = null) - => new AcceptedAtRouteResult(routeName, routeValues, value); - - /// - /// Provides a container for external libraries to extend - /// the default `Results` set with their own samples. - /// - public static IResultExtensions Extensions { get; } = new ResultExtensions(); + return new CreatedResult(uri, value); } + + /// + /// Produces a response. + /// + /// The name of the route to use for generating the URL. + /// The route data to use for generating the URL. + /// The value to be included in the HTTP response body. + /// The created for the response. + public static IResult CreatedAtRoute(string? routeName = null, object? routeValues = null, object? value = null) + => new CreatedAtRouteResult(routeName, routeValues, value); + + /// + /// Produces a response. + /// + /// The URI with the location at which the status of requested content can be monitored. + /// The optional content value to format in the response body. + /// The created for the response. + public static IResult Accepted(string? uri = null, object? value = null) + => new AcceptedResult(uri, value); + + /// + /// Produces a response. + /// + /// The name of the route to use for generating the URL. + /// The route data to use for generating the URL. + /// The optional content value to format in the response body. + /// The created for the response. + public static IResult AcceptedAtRoute(string? routeName = null, object? routeValues = null, object? value = null) + => new AcceptedAtRouteResult(routeName, routeValues, value); + + /// + /// Provides a container for external libraries to extend + /// the default `Results` set with their own samples. + /// + public static IResultExtensions Extensions { get; } = new ResultExtensions(); } diff --git a/src/Http/Http.Results/src/SignInResult.cs b/src/Http/Http.Results/src/SignInResult.cs index 3efa8919d5..9ce2b632d4 100644 --- a/src/Http/Http.Results/src/SignInResult.cs +++ b/src/Http/Http.Results/src/SignInResult.cs @@ -8,90 +8,89 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +/// +/// An that on execution invokes . +/// +internal sealed partial class SignInResult : IResult { /// - /// An that on execution invokes . + /// Initializes a new instance of with the + /// default authentication scheme. /// - internal sealed partial class SignInResult : IResult + /// The claims principal containing the user claims. + public SignInResult(ClaimsPrincipal principal) + : this(authenticationScheme: null, principal, properties: null) { - /// - /// Initializes a new instance of with the - /// default authentication scheme. - /// - /// The claims principal containing the user claims. - public SignInResult(ClaimsPrincipal principal) - : this(authenticationScheme: null, principal, properties: null) - { - } + } - /// - /// Initializes a new instance of with the - /// specified authentication scheme. - /// - /// The authentication scheme to use when signing in the user. - /// The claims principal containing the user claims. - public SignInResult(string? authenticationScheme, ClaimsPrincipal principal) - : this(authenticationScheme, principal, properties: null) - { - } + /// + /// Initializes a new instance of with the + /// specified authentication scheme. + /// + /// The authentication scheme to use when signing in the user. + /// The claims principal containing the user claims. + public SignInResult(string? authenticationScheme, ClaimsPrincipal principal) + : this(authenticationScheme, principal, properties: null) + { + } - /// - /// Initializes a new instance of with the - /// default authentication scheme and . - /// - /// The claims principal containing the user claims. - /// used to perform the sign-in operation. - public SignInResult(ClaimsPrincipal principal, AuthenticationProperties? properties) - : this(authenticationScheme: null, principal, properties) - { - } + /// + /// Initializes a new instance of with the + /// default authentication scheme and . + /// + /// The claims principal containing the user claims. + /// used to perform the sign-in operation. + public SignInResult(ClaimsPrincipal principal, AuthenticationProperties? properties) + : this(authenticationScheme: null, principal, properties) + { + } - /// - /// Initializes a new instance of with the - /// specified authentication scheme and . - /// - /// The authentication schemes to use when signing in the user. - /// The claims principal containing the user claims. - /// used to perform the sign-in operation. - public SignInResult(string? authenticationScheme, ClaimsPrincipal principal, AuthenticationProperties? properties) - { - Principal = principal ?? throw new ArgumentNullException(nameof(principal)); - AuthenticationScheme = authenticationScheme; - Properties = properties; - } + /// + /// Initializes a new instance of with the + /// specified authentication scheme and . + /// + /// The authentication schemes to use when signing in the user. + /// The claims principal containing the user claims. + /// used to perform the sign-in operation. + public SignInResult(string? authenticationScheme, ClaimsPrincipal principal, AuthenticationProperties? properties) + { + Principal = principal ?? throw new ArgumentNullException(nameof(principal)); + AuthenticationScheme = authenticationScheme; + Properties = properties; + } - /// - /// Gets or sets the authentication scheme that is used to perform the sign-in operation. - /// - public string? AuthenticationScheme { get; set; } + /// + /// Gets or sets the authentication scheme that is used to perform the sign-in operation. + /// + public string? AuthenticationScheme { get; set; } - /// - /// Gets or sets the containing the user claims. - /// - public ClaimsPrincipal Principal { get; set; } + /// + /// Gets or sets the containing the user claims. + /// + public ClaimsPrincipal Principal { get; set; } - /// - /// Gets or sets the used to perform the sign-in operation. - /// - public AuthenticationProperties? Properties { get; set; } + /// + /// Gets or sets the used to perform the sign-in operation. + /// + public AuthenticationProperties? Properties { get; set; } - /// - public Task ExecuteAsync(HttpContext httpContext) - { - var logger = httpContext.RequestServices.GetRequiredService>(); + /// + public Task ExecuteAsync(HttpContext httpContext) + { + var logger = httpContext.RequestServices.GetRequiredService>(); - Log.SignInResultExecuting(logger, AuthenticationScheme, Principal); + Log.SignInResultExecuting(logger, AuthenticationScheme, Principal); - return httpContext.SignInAsync(AuthenticationScheme, Principal, Properties); - } + return httpContext.SignInAsync(AuthenticationScheme, Principal, Properties); + } - private static partial class Log - { - [LoggerMessage(1, LogLevel.Information, - "Executing SignInResult with authentication scheme ({Scheme}) and the following principal: {Principal}.", - EventName = "SignInResultExecuting")] - public static partial void SignInResultExecuting(ILogger logger, string? scheme, ClaimsPrincipal principal); - } + private static partial class Log + { + [LoggerMessage(1, LogLevel.Information, + "Executing SignInResult with authentication scheme ({Scheme}) and the following principal: {Principal}.", + EventName = "SignInResultExecuting")] + public static partial void SignInResultExecuting(ILogger logger, string? scheme, ClaimsPrincipal principal); } } diff --git a/src/Http/Http.Results/src/SignOutResult.cs b/src/Http/Http.Results/src/SignOutResult.cs index 6698aa71e4..7b99b5a1fa 100644 --- a/src/Http/Http.Results/src/SignOutResult.cs +++ b/src/Http/Http.Results/src/SignOutResult.cs @@ -9,119 +9,118 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +/// +/// An that on execution invokes . +/// +internal sealed partial class SignOutResult : IResult { /// - /// An that on execution invokes . + /// Initializes a new instance of with the default sign out scheme. /// - internal sealed partial class SignOutResult : IResult + public SignOutResult() + : this(Array.Empty()) { - /// - /// Initializes a new instance of with the default sign out scheme. - /// - public SignOutResult() - : this(Array.Empty()) - { - } + } - /// - /// Initializes a new instance of with the default sign out scheme. - /// specified authentication scheme and . - /// - /// used to perform the sign-out operation. - public SignOutResult(AuthenticationProperties properties) - : this(Array.Empty(), properties) - { - } + /// + /// Initializes a new instance of with the default sign out scheme. + /// specified authentication scheme and . + /// + /// used to perform the sign-out operation. + public SignOutResult(AuthenticationProperties properties) + : this(Array.Empty(), properties) + { + } - /// - /// Initializes a new instance of with the - /// specified authentication scheme. - /// - /// The authentication scheme to use when signing out the user. - public SignOutResult(string authenticationScheme) - : this(new[] { authenticationScheme }) - { - } + /// + /// Initializes a new instance of with the + /// specified authentication scheme. + /// + /// The authentication scheme to use when signing out the user. + public SignOutResult(string authenticationScheme) + : this(new[] { authenticationScheme }) + { + } - /// - /// Initializes a new instance of with the - /// specified authentication schemes. - /// - /// The authentication schemes to use when signing out the user. - public SignOutResult(IList authenticationSchemes) - : this(authenticationSchemes, properties: null) - { - } + /// + /// Initializes a new instance of with the + /// specified authentication schemes. + /// + /// The authentication schemes to use when signing out the user. + public SignOutResult(IList authenticationSchemes) + : this(authenticationSchemes, properties: null) + { + } - /// - /// Initializes a new instance of with the - /// specified authentication scheme and . - /// - /// The authentication schemes to use when signing out the user. - /// used to perform the sign-out operation. - public SignOutResult(string authenticationScheme, AuthenticationProperties? properties) - : this(new[] { authenticationScheme }, properties) - { - } + /// + /// Initializes a new instance of with the + /// specified authentication scheme and . + /// + /// The authentication schemes to use when signing out the user. + /// used to perform the sign-out operation. + public SignOutResult(string authenticationScheme, AuthenticationProperties? properties) + : this(new[] { authenticationScheme }, properties) + { + } - /// - /// Initializes a new instance of with the - /// specified authentication schemes and . - /// - /// The authentication scheme to use when signing out the user. - /// used to perform the sign-out operation. - public SignOutResult(IList authenticationSchemes, AuthenticationProperties? properties) - { - AuthenticationSchemes = authenticationSchemes ?? throw new ArgumentNullException(nameof(authenticationSchemes)); - Properties = properties; - } + /// + /// Initializes a new instance of with the + /// specified authentication schemes and . + /// + /// The authentication scheme to use when signing out the user. + /// used to perform the sign-out operation. + public SignOutResult(IList authenticationSchemes, AuthenticationProperties? properties) + { + AuthenticationSchemes = authenticationSchemes ?? throw new ArgumentNullException(nameof(authenticationSchemes)); + Properties = properties; + } - /// - /// Gets or sets the authentication schemes that are challenged. - /// - public IList AuthenticationSchemes { get; init; } + /// + /// Gets or sets the authentication schemes that are challenged. + /// + public IList AuthenticationSchemes { get; init; } - /// - /// Gets or sets the used to perform the sign-out operation. - /// - public AuthenticationProperties? Properties { get; init; } + /// + /// Gets or sets the used to perform the sign-out operation. + /// + public AuthenticationProperties? Properties { get; init; } - /// - public async Task ExecuteAsync(HttpContext httpContext) - { - var logger = httpContext.RequestServices.GetRequiredService>(); + /// + public async Task ExecuteAsync(HttpContext httpContext) + { + var logger = httpContext.RequestServices.GetRequiredService>(); - Log.SignOutResultExecuting(logger, AuthenticationSchemes); + Log.SignOutResultExecuting(logger, AuthenticationSchemes); - if (AuthenticationSchemes.Count == 0) - { - await httpContext.SignOutAsync(Properties); - } - else + if (AuthenticationSchemes.Count == 0) + { + await httpContext.SignOutAsync(Properties); + } + else + { + for (var i = 0; i < AuthenticationSchemes.Count; i++) { - for (var i = 0; i < AuthenticationSchemes.Count; i++) - { - await httpContext.SignOutAsync(AuthenticationSchemes[i], Properties); - } + await httpContext.SignOutAsync(AuthenticationSchemes[i], Properties); } } + } - private static partial class Log + private static partial class Log + { + public static void SignOutResultExecuting(ILogger logger, IList authenticationSchemes) { - public static void SignOutResultExecuting(ILogger logger, IList authenticationSchemes) + if (logger.IsEnabled(LogLevel.Information)) { - if (logger.IsEnabled(LogLevel.Information)) - { - SignOutResultExecuting(logger, authenticationSchemes.ToArray()); - } + SignOutResultExecuting(logger, authenticationSchemes.ToArray()); } - - [LoggerMessage(1, LogLevel.Information, - "Executing SignOutResult with authentication schemes ({Schemes}).", - EventName = "SignOutResultExecuting", - SkipEnabledCheck = true)] - private static partial void SignOutResultExecuting(ILogger logger, string[] schemes); } + + [LoggerMessage(1, LogLevel.Information, + "Executing SignOutResult with authentication schemes ({Schemes}).", + EventName = "SignOutResultExecuting", + SkipEnabledCheck = true)] + private static partial void SignOutResultExecuting(ILogger logger, string[] schemes); } } diff --git a/src/Http/Http.Results/src/StatusCodeResult.cs b/src/Http/Http.Results/src/StatusCodeResult.cs index 0a7c171f51..58d1461329 100644 --- a/src/Http/Http.Results/src/StatusCodeResult.cs +++ b/src/Http/Http.Results/src/StatusCodeResult.cs @@ -5,47 +5,46 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal partial class StatusCodeResult : IResult { - internal partial class StatusCodeResult : IResult + /// + /// Initializes a new instance of the class + /// with the given . + /// + /// The HTTP status code of the response. + public StatusCodeResult(int statusCode) + { + StatusCode = statusCode; + } + + /// + /// Gets the HTTP status code. + /// + public int StatusCode { get; } + + /// + /// Sets the status code on the HTTP response. + /// + /// The for the current request. + /// A task that represents the asynchronous execute operation. + public Task ExecuteAsync(HttpContext httpContext) + { + var factory = httpContext.RequestServices.GetRequiredService(); + var logger = factory.CreateLogger(GetType()); + + Log.StatusCodeResultExecuting(logger, StatusCode); + + httpContext.Response.StatusCode = StatusCode; + return Task.CompletedTask; + } + + private static partial class Log { - /// - /// Initializes a new instance of the class - /// with the given . - /// - /// The HTTP status code of the response. - public StatusCodeResult(int statusCode) - { - StatusCode = statusCode; - } - - /// - /// Gets the HTTP status code. - /// - public int StatusCode { get; } - - /// - /// Sets the status code on the HTTP response. - /// - /// The for the current request. - /// A task that represents the asynchronous execute operation. - public Task ExecuteAsync(HttpContext httpContext) - { - var factory = httpContext.RequestServices.GetRequiredService(); - var logger = factory.CreateLogger(GetType()); - - Log.StatusCodeResultExecuting(logger, StatusCode); - - httpContext.Response.StatusCode = StatusCode; - return Task.CompletedTask; - } - - private static partial class Log - { - [LoggerMessage(1, LogLevel.Information, - "Executing StatusCodeResult, setting HTTP status code {StatusCode}.", - EventName = "StatusCodeResultExecuting")] - public static partial void StatusCodeResultExecuting(ILogger logger, int statusCode); - } + [LoggerMessage(1, LogLevel.Information, + "Executing StatusCodeResult, setting HTTP status code {StatusCode}.", + EventName = "StatusCodeResultExecuting")] + public static partial void StatusCodeResultExecuting(ILogger logger, int statusCode); } } diff --git a/src/Http/Http.Results/src/UnauthorizedResult.cs b/src/Http/Http.Results/src/UnauthorizedResult.cs index fd2135362e..28ec97eaf4 100644 --- a/src/Http/Http.Results/src/UnauthorizedResult.cs +++ b/src/Http/Http.Results/src/UnauthorizedResult.cs @@ -1,12 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal sealed class UnauthorizedResult : StatusCodeResult { - internal sealed class UnauthorizedResult : StatusCodeResult + public UnauthorizedResult() : base(StatusCodes.Status401Unauthorized) { - public UnauthorizedResult() : base(StatusCodes.Status401Unauthorized) - { - } } } diff --git a/src/Http/Http.Results/src/UnprocessableEntityObjectResult.cs b/src/Http/Http.Results/src/UnprocessableEntityObjectResult.cs index 3be9fa3608..fd7b456eb8 100644 --- a/src/Http/Http.Results/src/UnprocessableEntityObjectResult.cs +++ b/src/Http/Http.Results/src/UnprocessableEntityObjectResult.cs @@ -1,13 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal sealed class UnprocessableEntityObjectResult : ObjectResult { - internal sealed class UnprocessableEntityObjectResult : ObjectResult + public UnprocessableEntityObjectResult(object? error) + : base(error, StatusCodes.Status422UnprocessableEntity) { - public UnprocessableEntityObjectResult(object? error) - : base(error, StatusCodes.Status422UnprocessableEntity) - { - } } } diff --git a/src/Http/Http.Results/src/VirtualFileResult.cs b/src/Http/Http.Results/src/VirtualFileResult.cs index b5cb0bc9e4..2e83b13625 100644 --- a/src/Http/Http.Results/src/VirtualFileResult.cs +++ b/src/Http/Http.Results/src/VirtualFileResult.cs @@ -8,106 +8,105 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +/// +/// A that on execution writes the file specified using a virtual path to the response +/// using mechanisms provided by the host. +/// +internal sealed class VirtualFileResult : FileResult, IResult { + private string _fileName; + /// - /// A that on execution writes the file specified using a virtual path to the response - /// using mechanisms provided by the host. + /// Creates a new instance with the provided + /// and the provided . /// - internal sealed class VirtualFileResult : FileResult, IResult + /// The path to the file. The path must be relative/virtual. + /// The Content-Type header of the response. + public VirtualFileResult(string fileName, string? contentType) + : base(contentType) { - private string _fileName; - - /// - /// Creates a new instance with the provided - /// and the provided . - /// - /// The path to the file. The path must be relative/virtual. - /// The Content-Type header of the response. - public VirtualFileResult(string fileName, string? contentType) - : base(contentType) + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + } + + /// + /// Gets or sets the path to the file that will be sent back as the response. + /// + public string FileName + { + get => _fileName; + [MemberNotNull(nameof(_fileName))] + set => _fileName = value ?? throw new ArgumentNullException(nameof(value)); + } + + /// + public Task ExecuteAsync(HttpContext httpContext) + { + var hostingEnvironment = httpContext.RequestServices.GetRequiredService(); + var logger = httpContext.RequestServices.GetRequiredService>(); + + var fileInfo = GetFileInformation(hostingEnvironment.WebRootFileProvider); + if (!fileInfo.Exists) { - FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + throw new FileNotFoundException($"Could not find file: {FileName}.", FileName); } - /// - /// Gets or sets the path to the file that will be sent back as the response. - /// - public string FileName + Log.ExecutingFileResult(logger, this); + + var lastModified = LastModified ?? fileInfo.LastModified; + var fileResultInfo = new FileResultInfo + { + ContentType = ContentType, + FileDownloadName = FileDownloadName, + EnableRangeProcessing = EnableRangeProcessing, + EntityTag = EntityTag, + LastModified = lastModified, + }; + + var (range, rangeLength, serveBody) = FileResultHelper.SetHeadersAndLog( + httpContext, + fileResultInfo, + fileInfo.Length, + EnableRangeProcessing, + lastModified, + EntityTag, + logger); + + if (!serveBody) { - get => _fileName; - [MemberNotNull(nameof(_fileName))] - set => _fileName = value ?? throw new ArgumentNullException(nameof(value)); + return Task.CompletedTask; } - /// - public Task ExecuteAsync(HttpContext httpContext) + if (range != null) { - var hostingEnvironment = httpContext.RequestServices.GetRequiredService(); - var logger = httpContext.RequestServices.GetRequiredService>(); - - var fileInfo = GetFileInformation(hostingEnvironment.WebRootFileProvider); - if (!fileInfo.Exists) - { - throw new FileNotFoundException($"Could not find file: {FileName}.", FileName); - } - - Log.ExecutingFileResult(logger, this); - - var lastModified = LastModified ?? fileInfo.LastModified; - var fileResultInfo = new FileResultInfo - { - ContentType = ContentType, - FileDownloadName = FileDownloadName, - EnableRangeProcessing = EnableRangeProcessing, - EntityTag = EntityTag, - LastModified = lastModified, - }; - - var (range, rangeLength, serveBody) = FileResultHelper.SetHeadersAndLog( - httpContext, - fileResultInfo, - fileInfo.Length, - EnableRangeProcessing, - lastModified, - EntityTag, - logger); - - if (!serveBody) - { - return Task.CompletedTask; - } - - if (range != null) - { - FileResultHelper.Log.WritingRangeToBody(logger); - } - - var response = httpContext.Response; - var offset = 0L; - var count = (long?)null; - if (range != null) - { - offset = range.From ?? 0L; - count = rangeLength; - } - - return response.SendFileAsync( - fileInfo, - offset, - count); + FileResultHelper.Log.WritingRangeToBody(logger); } - internal IFileInfo GetFileInformation(IFileProvider fileProvider) + var response = httpContext.Response; + var offset = 0L; + var count = (long?)null; + if (range != null) { - var normalizedPath = FileName; - if (normalizedPath.StartsWith("~", StringComparison.Ordinal)) - { - normalizedPath = normalizedPath.Substring(1); - } - - var fileInfo = fileProvider.GetFileInfo(normalizedPath); - return fileInfo; + offset = range.From ?? 0L; + count = rangeLength; } + + return response.SendFileAsync( + fileInfo, + offset, + count); + } + + internal IFileInfo GetFileInformation(IFileProvider fileProvider) + { + var normalizedPath = FileName; + if (normalizedPath.StartsWith("~", StringComparison.Ordinal)) + { + normalizedPath = normalizedPath.Substring(1); + } + + var fileInfo = fileProvider.GetFileInfo(normalizedPath); + return fileInfo; } } diff --git a/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs b/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs index c2f1e31339..f9626d7df5 100644 --- a/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedAtRouteResultTests.cs @@ -12,43 +12,43 @@ using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class AcceptedAtRouteResultTests { - public class AcceptedAtRouteResultTests + [Fact] + public async Task ExecuteResultAsync_FormatsData() { - [Fact] - public async Task ExecuteResultAsync_FormatsData() - { - // Arrange - var url = "testAction"; - var linkGenerator = new TestLinkGenerator { Url = url }; - var httpContext = GetHttpContext(linkGenerator); - var stream = new MemoryStream(); - httpContext.Response.Body = stream; + // Arrange + var url = "testAction"; + var linkGenerator = new TestLinkGenerator { Url = url }; + var httpContext = GetHttpContext(linkGenerator); + var stream = new MemoryStream(); + httpContext.Response.Body = stream; - var routeValues = new RouteValueDictionary(new Dictionary() + var routeValues = new RouteValueDictionary(new Dictionary() { { "test", "case" }, { "sample", "route" } }); - // Act - var result = new AcceptedAtRouteResult( - routeName: "sample", - routeValues: routeValues, - value: "Hello world"); - await result.ExecuteAsync(httpContext); + // Act + var result = new AcceptedAtRouteResult( + routeName: "sample", + routeValues: routeValues, + value: "Hello world"); + await result.ExecuteAsync(httpContext); - // Assert - var response = Encoding.UTF8.GetString(stream.ToArray()); - Assert.Equal("\"Hello world\"", response); - } + // Assert + var response = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal("\"Hello world\"", response); + } - public static TheoryData AcceptedAtRouteData + public static TheoryData AcceptedAtRouteData + { + get { - get - { - return new TheoryData + return new TheoryData { null, new Dictionary() @@ -62,59 +62,58 @@ namespace Microsoft.AspNetCore.Http.Result { "sample", "route" } }), }; - } } + } - [Theory] - [MemberData(nameof(AcceptedAtRouteData))] - public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader(object values) - { - // Arrange - var expectedUrl = "testAction"; - var linkGenerator = new TestLinkGenerator { Url = expectedUrl }; - var httpContext = GetHttpContext(linkGenerator); + [Theory] + [MemberData(nameof(AcceptedAtRouteData))] + public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader(object values) + { + // Arrange + var expectedUrl = "testAction"; + var linkGenerator = new TestLinkGenerator { Url = expectedUrl }; + var httpContext = GetHttpContext(linkGenerator); - // Act - var result = new AcceptedAtRouteResult(routeValues: values, value: null); - await result.ExecuteAsync(httpContext); + // Act + var result = new AcceptedAtRouteResult(routeValues: values, value: null); + await result.ExecuteAsync(httpContext); - // Assert - Assert.Equal(StatusCodes.Status202Accepted, httpContext.Response.StatusCode); - Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]); - } + // Assert + Assert.Equal(StatusCodes.Status202Accepted, httpContext.Response.StatusCode); + Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]); + } - [Fact] - public async Task ExecuteResultAsync_ThrowsIfRouteUrlIsNull() - { - // Arrange - var linkGenerator = new TestLinkGenerator(); - var httpContext = GetHttpContext(linkGenerator); + [Fact] + public async Task ExecuteResultAsync_ThrowsIfRouteUrlIsNull() + { + // Arrange + var linkGenerator = new TestLinkGenerator(); + var httpContext = GetHttpContext(linkGenerator); - // Act - var result = new AcceptedAtRouteResult( - routeName: null, - routeValues: new Dictionary(), - value: null); + // Act + var result = new AcceptedAtRouteResult( + routeName: null, + routeValues: new Dictionary(), + value: null); - // Assert - await ExceptionAssert.ThrowsAsync(() => - result.ExecuteAsync(httpContext), - "No route matches the supplied values."); - } + // Assert + await ExceptionAssert.ThrowsAsync(() => + result.ExecuteAsync(httpContext), + "No route matches the supplied values."); + } - private static HttpContext GetHttpContext(LinkGenerator linkGenerator) - { - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = CreateServices(linkGenerator); - return httpContext; - } + private static HttpContext GetHttpContext(LinkGenerator linkGenerator) + { + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = CreateServices(linkGenerator); + return httpContext; + } - private static IServiceProvider CreateServices(LinkGenerator linkGenerator) - { - var services = new ServiceCollection(); - services.AddLogging(); - services.AddSingleton(linkGenerator); - return services.BuildServiceProvider(); - } + private static IServiceProvider CreateServices(LinkGenerator linkGenerator) + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddSingleton(linkGenerator); + return services.BuildServiceProvider(); } } diff --git a/src/Http/Http.Results/test/AcceptedResultTests.cs b/src/Http/Http.Results/test/AcceptedResultTests.cs index bb54726153..3074a1c50f 100644 --- a/src/Http/Http.Results/test/AcceptedResultTests.cs +++ b/src/Http/Http.Results/test/AcceptedResultTests.cs @@ -8,54 +8,53 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class AcceptedResultTests { - public class AcceptedResultTests + [Fact] + public async Task ExecuteResultAsync_FormatsData() + { + // Arrange + var httpContext = GetHttpContext(); + var stream = new MemoryStream(); + httpContext.Response.Body = stream; + // Act + var result = new AcceptedResult("my-location", value: "Hello world"); + await result.ExecuteAsync(httpContext); + + // Assert + var response = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal("\"Hello world\"", response); + } + + [Fact] + public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader() + { + // Arrange + var expectedUrl = "testAction"; + var httpContext = GetHttpContext(); + + // Act + var result = new AcceptedResult(expectedUrl, value: "some-value"); + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status202Accepted, httpContext.Response.StatusCode); + Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]); + } + + private static HttpContext GetHttpContext() + { + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = CreateServices(); + return httpContext; + } + + private static IServiceProvider CreateServices() { - [Fact] - public async Task ExecuteResultAsync_FormatsData() - { - // Arrange - var httpContext = GetHttpContext(); - var stream = new MemoryStream(); - httpContext.Response.Body = stream; - // Act - var result = new AcceptedResult("my-location", value: "Hello world"); - await result.ExecuteAsync(httpContext); - - // Assert - var response = Encoding.UTF8.GetString(stream.ToArray()); - Assert.Equal("\"Hello world\"", response); - } - - [Fact] - public async Task ExecuteResultAsync_SetsStatusCodeAndLocationHeader() - { - // Arrange - var expectedUrl = "testAction"; - var httpContext = GetHttpContext(); - - // Act - var result = new AcceptedResult(expectedUrl, value: "some-value"); - await result.ExecuteAsync(httpContext); - - // Assert - Assert.Equal(StatusCodes.Status202Accepted, httpContext.Response.StatusCode); - Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]); - } - - private static HttpContext GetHttpContext() - { - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = CreateServices(); - return httpContext; - } - - private static IServiceProvider CreateServices() - { - var services = new ServiceCollection(); - services.AddLogging(); - return services.BuildServiceProvider(); - } + var services = new ServiceCollection(); + services.AddLogging(); + return services.BuildServiceProvider(); } } diff --git a/src/Http/Http.Results/test/BadRequestObjectResultTests.cs b/src/Http/Http.Results/test/BadRequestObjectResultTests.cs index 7c3c57be31..ccec17c255 100644 --- a/src/Http/Http.Results/test/BadRequestObjectResultTests.cs +++ b/src/Http/Http.Results/test/BadRequestObjectResultTests.cs @@ -3,20 +3,19 @@ using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class BadRequestObjectResultTests { - public class BadRequestObjectResultTests + [Fact] + public void BadRequestObjectResult_SetsStatusCodeAndValue() { - [Fact] - public void BadRequestObjectResult_SetsStatusCodeAndValue() - { - // Arrange & Act - var obj = new object(); - var badRequestObjectResult = new BadRequestObjectResult(obj); + // Arrange & Act + var obj = new object(); + var badRequestObjectResult = new BadRequestObjectResult(obj); - // Assert - Assert.Equal(StatusCodes.Status400BadRequest, badRequestObjectResult.StatusCode); - Assert.Equal(obj, badRequestObjectResult.Value); - } + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, badRequestObjectResult.StatusCode); + Assert.Equal(obj, badRequestObjectResult.Value); } } diff --git a/src/Http/Http.Results/test/ChallengeResultTest.cs b/src/Http/Http.Results/test/ChallengeResultTest.cs index 672d3b34ae..3bed9c0378 100644 --- a/src/Http/Http.Results/test/ChallengeResultTest.cs +++ b/src/Http/Http.Results/test/ChallengeResultTest.cs @@ -9,54 +9,53 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class ChallengeResultTest { - public class ChallengeResultTest + [Fact] + public async Task ChallengeResult_ExecuteAsync() + { + // Arrange + var result = new ChallengeResult("", null); + var auth = new Mock(); + var httpContext = GetHttpContext(auth); + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + auth.Verify(c => c.ChallengeAsync(httpContext, "", null), Times.Exactly(1)); + } + + [Fact] + public async Task ChallengeResult_ExecuteAsync_NoSchemes() + { + // Arrange + var result = new ChallengeResult(new string[] { }, null); + var auth = new Mock(); + var httpContext = GetHttpContext(auth); + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + auth.Verify(c => c.ChallengeAsync(httpContext, null, null), Times.Exactly(1)); + } + + private static DefaultHttpContext GetHttpContext(Mock auth) + { + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = CreateServices() + .AddSingleton(auth.Object) + .BuildServiceProvider(); + return httpContext; + } + + private static IServiceCollection CreateServices() { - [Fact] - public async Task ChallengeResult_ExecuteAsync() - { - // Arrange - var result = new ChallengeResult("", null); - var auth = new Mock(); - var httpContext = GetHttpContext(auth); - - // Act - await result.ExecuteAsync(httpContext); - - // Assert - auth.Verify(c => c.ChallengeAsync(httpContext, "", null), Times.Exactly(1)); - } - - [Fact] - public async Task ChallengeResult_ExecuteAsync_NoSchemes() - { - // Arrange - var result = new ChallengeResult(new string[] { }, null); - var auth = new Mock(); - var httpContext = GetHttpContext(auth); - - // Act - await result.ExecuteAsync(httpContext); - - // Assert - auth.Verify(c => c.ChallengeAsync(httpContext, null, null), Times.Exactly(1)); - } - - private static DefaultHttpContext GetHttpContext(Mock auth) - { - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = CreateServices() - .AddSingleton(auth.Object) - .BuildServiceProvider(); - return httpContext; - } - - private static IServiceCollection CreateServices() - { - var services = new ServiceCollection(); - services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); - return services; - } + var services = new ServiceCollection(); + services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + return services; } } diff --git a/src/Http/Http.Results/test/ConflictObjectResultTest.cs b/src/Http/Http.Results/test/ConflictObjectResultTest.cs index fba55eecce..1b2c573102 100644 --- a/src/Http/Http.Results/test/ConflictObjectResultTest.cs +++ b/src/Http/Http.Results/test/ConflictObjectResultTest.cs @@ -3,20 +3,19 @@ using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class ConflictObjectResultTest { - public class ConflictObjectResultTest + [Fact] + public void ConflictObjectResult_SetsStatusCodeAndValue() { - [Fact] - public void ConflictObjectResult_SetsStatusCodeAndValue() - { - // Arrange & Act - var obj = new object(); - var conflictObjectResult = new ConflictObjectResult(obj); + // Arrange & Act + var obj = new object(); + var conflictObjectResult = new ConflictObjectResult(obj); - // Assert - Assert.Equal(StatusCodes.Status409Conflict, conflictObjectResult.StatusCode); - Assert.Equal(obj, conflictObjectResult.Value); - } + // Assert + Assert.Equal(StatusCodes.Status409Conflict, conflictObjectResult.StatusCode); + Assert.Equal(obj, conflictObjectResult.Value); } } diff --git a/src/Http/Http.Results/test/ContentResultTest.cs b/src/Http/Http.Results/test/ContentResultTest.cs index 1f50e0001a..09e0ada15d 100644 --- a/src/Http/Http.Results/test/ContentResultTest.cs +++ b/src/Http/Http.Results/test/ContentResultTest.cs @@ -10,37 +10,37 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Net.Http.Headers; using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class ContentResultTest { - public class ContentResultTest + [Fact] + public async Task ContentResult_ExecuteAsync_Response_NullContent_SetsContentTypeAndEncoding() { - [Fact] - public async Task ContentResult_ExecuteAsync_Response_NullContent_SetsContentTypeAndEncoding() + // Arrange + var contentResult = new ContentResult { - // Arrange - var contentResult = new ContentResult + Content = null, + ContentType = new MediaTypeHeaderValue("text/plain") { - Content = null, - ContentType = new MediaTypeHeaderValue("text/plain") - { - Encoding = Encoding.Unicode - }.ToString() - }; - var httpContext = GetHttpContext(); + Encoding = Encoding.Unicode + }.ToString() + }; + var httpContext = GetHttpContext(); - // Act - await contentResult.ExecuteAsync(httpContext); + // Act + await contentResult.ExecuteAsync(httpContext); - // Assert - Assert.Equal("text/plain; charset=utf-16", httpContext.Response.ContentType); - } + // Assert + Assert.Equal("text/plain; charset=utf-16", httpContext.Response.ContentType); + } - public static TheoryData ContentResultContentTypeData + public static TheoryData ContentResultContentTypeData + { + get { - get - { - // contentType, content, responseContentType, expectedContentType, expectedData - return new TheoryData + // contentType, content, responseContentType, expectedContentType, expectedData + return new TheoryData { { null, @@ -99,54 +99,53 @@ namespace Microsoft.AspNetCore.Http.Result new byte[] { 97, 98, 99, 100 } }, }; - } } + } - [Theory] - [MemberData(nameof(ContentResultContentTypeData))] - public async Task ContentResult_ExecuteAsync_SetContentTypeAndEncoding_OnResponse( - MediaTypeHeaderValue contentType, - string content, - string responseContentType, - string expectedContentType, - byte[] expectedContentData) + [Theory] + [MemberData(nameof(ContentResultContentTypeData))] + public async Task ContentResult_ExecuteAsync_SetContentTypeAndEncoding_OnResponse( + MediaTypeHeaderValue contentType, + string content, + string responseContentType, + string expectedContentType, + byte[] expectedContentData) + { + // Arrange + var contentResult = new ContentResult { - // Arrange - var contentResult = new ContentResult - { - Content = content, - ContentType = contentType?.ToString() - }; - var httpContext = GetHttpContext(); - var memoryStream = new MemoryStream(); - httpContext.Response.Body = memoryStream; - httpContext.Response.ContentType = responseContentType; + Content = content, + ContentType = contentType?.ToString() + }; + var httpContext = GetHttpContext(); + var memoryStream = new MemoryStream(); + httpContext.Response.Body = memoryStream; + httpContext.Response.ContentType = responseContentType; - // Act - await contentResult.ExecuteAsync(httpContext); + // Act + await contentResult.ExecuteAsync(httpContext); - // Assert - var finalResponseContentType = httpContext.Response.ContentType; - Assert.Equal(expectedContentType, finalResponseContentType); - Assert.Equal(expectedContentData, memoryStream.ToArray()); - Assert.Equal(expectedContentData.Length, httpContext.Response.ContentLength); - } + // Assert + var finalResponseContentType = httpContext.Response.ContentType; + Assert.Equal(expectedContentType, finalResponseContentType); + Assert.Equal(expectedContentData, memoryStream.ToArray()); + Assert.Equal(expectedContentData.Length, httpContext.Response.ContentLength); + } - private static IServiceCollection CreateServices() - { - var services = new ServiceCollection(); - services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); - return services; - } + private static IServiceCollection CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + return services; + } - private static HttpContext GetHttpContext() - { - var services = CreateServices(); + private static HttpContext GetHttpContext() + { + var services = CreateServices(); - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = services.BuildServiceProvider(); + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = services.BuildServiceProvider(); - return httpContext; - } + return httpContext; } } diff --git a/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs b/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs index 7f4cd28dd9..7fdedb717f 100644 --- a/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs +++ b/src/Http/Http.Results/test/CreatedAtRouteResultTests.cs @@ -12,82 +12,81 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public partial class CreatedAtRouteResultTests { - public partial class CreatedAtRouteResultTests + public static IEnumerable CreatedAtRouteData { - public static IEnumerable CreatedAtRouteData + get { - get - { - yield return new object[] { null }; - yield return - new object[] { + yield return new object[] { null }; + yield return + new object[] { new Dictionary() { { "hello", "world" } } - }; - yield return - new object[] { + }; + yield return + new object[] { new RouteValueDictionary(new Dictionary() { { "test", "case" }, { "sample", "route" } }) - }; - } + }; } + } - [Theory] - [MemberData(nameof(CreatedAtRouteData))] - public async Task CreatedAtRouteResult_ReturnsStatusCode_SetsLocationHeader(object values) - { - // Arrange - var expectedUrl = "testAction"; - var httpContext = GetHttpContext(expectedUrl); + [Theory] + [MemberData(nameof(CreatedAtRouteData))] + public async Task CreatedAtRouteResult_ReturnsStatusCode_SetsLocationHeader(object values) + { + // Arrange + var expectedUrl = "testAction"; + var httpContext = GetHttpContext(expectedUrl); - // Act - var result = new CreatedAtRouteResult(routeName: null, routeValues: values, value: null); - await result.ExecuteAsync(httpContext); + // Act + var result = new CreatedAtRouteResult(routeName: null, routeValues: values, value: null); + await result.ExecuteAsync(httpContext); - // Assert - Assert.Equal(StatusCodes.Status201Created, httpContext.Response.StatusCode); - Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]); - } + // Assert + Assert.Equal(StatusCodes.Status201Created, httpContext.Response.StatusCode); + Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]); + } - [Fact] - public async Task CreatedAtRouteResult_ThrowsOnNullUrl() - { - // Arrange - var httpContext = GetHttpContext(expectedUrl: null); + [Fact] + public async Task CreatedAtRouteResult_ThrowsOnNullUrl() + { + // Arrange + var httpContext = GetHttpContext(expectedUrl: null); - var result = new CreatedAtRouteResult( - routeName: null, - routeValues: new Dictionary(), - value: null); + var result = new CreatedAtRouteResult( + routeName: null, + routeValues: new Dictionary(), + value: null); - // Act & Assert - await ExceptionAssert.ThrowsAsync( - async () => await result.ExecuteAsync(httpContext), - "No route matches the supplied values."); - } + // Act & Assert + await ExceptionAssert.ThrowsAsync( + async () => await result.ExecuteAsync(httpContext), + "No route matches the supplied values."); + } - private static HttpContext GetHttpContext(string expectedUrl) - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.PathBase = new PathString(""); - httpContext.Response.Body = new MemoryStream(); - httpContext.RequestServices = CreateServices(expectedUrl); - return httpContext; - } + private static HttpContext GetHttpContext(string expectedUrl) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.PathBase = new PathString(""); + httpContext.Response.Body = new MemoryStream(); + httpContext.RequestServices = CreateServices(expectedUrl); + return httpContext; + } - private static IServiceProvider CreateServices(string expectedUrl) + private static IServiceProvider CreateServices(string expectedUrl) + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddSingleton(new TestLinkGenerator { - var services = new ServiceCollection(); - services.AddSingleton(); - services.AddSingleton(new TestLinkGenerator - { - Url = expectedUrl - }); + Url = expectedUrl + }); - return services.BuildServiceProvider(); - } + return services.BuildServiceProvider(); } } diff --git a/src/Http/Http.Results/test/CreatedResultTest.cs b/src/Http/Http.Results/test/CreatedResultTest.cs index 28f4868e8f..06bc459d36 100644 --- a/src/Http/Http.Results/test/CreatedResultTest.cs +++ b/src/Http/Http.Results/test/CreatedResultTest.cs @@ -9,71 +9,70 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class CreatedResultTests { - public class CreatedResultTests + [Fact] + public void CreatedResult_SetsLocation() { - [Fact] - public void CreatedResult_SetsLocation() - { - // Arrange - var location = "http://test/location"; + // Arrange + var location = "http://test/location"; - // Act - var result = new CreatedResult(location, "testInput"); + // Act + var result = new CreatedResult(location, "testInput"); - // Assert - Assert.Same(location, result.Location); - } + // Assert + Assert.Same(location, result.Location); + } - [Fact] - public async Task CreatedResult_ReturnsStatusCode_SetsLocationHeader() - { - // Arrange - var location = "/test/"; - var httpContext = GetHttpContext(); - var result = new CreatedResult(location, "testInput"); + [Fact] + public async Task CreatedResult_ReturnsStatusCode_SetsLocationHeader() + { + // Arrange + var location = "/test/"; + var httpContext = GetHttpContext(); + var result = new CreatedResult(location, "testInput"); - // Act - await result.ExecuteAsync(httpContext); + // Act + await result.ExecuteAsync(httpContext); - // Assert - Assert.Equal(StatusCodes.Status201Created, httpContext.Response.StatusCode); - Assert.Equal(location, httpContext.Response.Headers["Location"]); - } + // Assert + Assert.Equal(StatusCodes.Status201Created, httpContext.Response.StatusCode); + Assert.Equal(location, httpContext.Response.Headers["Location"]); + } - [Fact] - public async Task CreatedResult_OverwritesLocationHeader() - { - // Arrange - var location = "/test/"; - var httpContext = GetHttpContext(); - httpContext.Response.Headers["Location"] = "/different/location/"; - var result = new CreatedResult(location, "testInput"); + [Fact] + public async Task CreatedResult_OverwritesLocationHeader() + { + // Arrange + var location = "/test/"; + var httpContext = GetHttpContext(); + httpContext.Response.Headers["Location"] = "/different/location/"; + var result = new CreatedResult(location, "testInput"); - // Act - await result.ExecuteAsync(httpContext); + // Act + await result.ExecuteAsync(httpContext); - // Assert - Assert.Equal(StatusCodes.Status201Created, httpContext.Response.StatusCode); - Assert.Equal(location, httpContext.Response.Headers["Location"]); - } + // Assert + Assert.Equal(StatusCodes.Status201Created, httpContext.Response.StatusCode); + Assert.Equal(location, httpContext.Response.Headers["Location"]); + } - private static HttpContext GetHttpContext() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.PathBase = new PathString(""); - httpContext.Response.Body = new MemoryStream(); - httpContext.RequestServices = CreateServices(); - return httpContext; - } + private static HttpContext GetHttpContext() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.PathBase = new PathString(""); + httpContext.Response.Body = new MemoryStream(); + httpContext.RequestServices = CreateServices(); + return httpContext; + } - private static IServiceProvider CreateServices() - { - var services = new ServiceCollection(); - services.AddSingleton(); + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(); - return services.BuildServiceProvider(); - } + return services.BuildServiceProvider(); } } diff --git a/src/Http/Http.Results/test/FileContentResultTest.cs b/src/Http/Http.Results/test/FileContentResultTest.cs index 6a2205c2b2..02ab23f267 100644 --- a/src/Http/Http.Results/test/FileContentResultTest.cs +++ b/src/Http/Http.Results/test/FileContentResultTest.cs @@ -9,30 +9,29 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class FileContentResultTest : FileContentResultTestBase { - public class FileContentResultTest : FileContentResultTestBase + protected override Task ExecuteAsync( + HttpContext httpContext, + byte[] buffer, + string contentType, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue entityTag = null, + bool enableRangeProcessing = false) { - protected override Task ExecuteAsync( - HttpContext httpContext, - byte[] buffer, - string contentType, - DateTimeOffset? lastModified = null, - EntityTagHeaderValue entityTag = null, - bool enableRangeProcessing = false) + var result = new FileContentResult(buffer, contentType) { - var result = new FileContentResult(buffer, contentType) - { - EntityTag = entityTag, - LastModified = lastModified, - EnableRangeProcessing = enableRangeProcessing, - }; + EntityTag = entityTag, + LastModified = lastModified, + EnableRangeProcessing = enableRangeProcessing, + }; - httpContext.RequestServices = new ServiceCollection() - .AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)) - .BuildServiceProvider(); + httpContext.RequestServices = new ServiceCollection() + .AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)) + .BuildServiceProvider(); - return result.ExecuteAsync(httpContext); - } + return result.ExecuteAsync(httpContext); } } diff --git a/src/Http/Http.Results/test/FileStreamResultTest.cs b/src/Http/Http.Results/test/FileStreamResultTest.cs index fca9982ac3..6fc5cf6209 100644 --- a/src/Http/Http.Results/test/FileStreamResultTest.cs +++ b/src/Http/Http.Results/test/FileStreamResultTest.cs @@ -8,79 +8,78 @@ using Microsoft.AspNetCore.Internal; using Microsoft.Net.Http.Headers; using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class FileStreamResultTest : FileStreamResultTestBase { - public class FileStreamResultTest : FileStreamResultTestBase + protected override Task ExecuteAsync( + HttpContext httpContext, + Stream stream, + string contentType, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue entityTag = null, + bool enableRangeProcessing = false) { - protected override Task ExecuteAsync( - HttpContext httpContext, - Stream stream, - string contentType, - DateTimeOffset? lastModified = null, - EntityTagHeaderValue entityTag = null, - bool enableRangeProcessing = false) + var fileStreamResult = new FileStreamResult(stream, contentType) { - var fileStreamResult = new FileStreamResult(stream, contentType) - { - LastModified = lastModified, - EntityTag = entityTag, - EnableRangeProcessing = enableRangeProcessing - }; - - return fileStreamResult.ExecuteAsync(httpContext); - } + LastModified = lastModified, + EntityTag = entityTag, + EnableRangeProcessing = enableRangeProcessing + }; - [Fact] - public void Constructor_SetsFileName() - { - // Arrange - var stream = Stream.Null; + return fileStreamResult.ExecuteAsync(httpContext); + } - // Act - var result = new FileStreamResult(stream, "text/plain"); + [Fact] + public void Constructor_SetsFileName() + { + // Arrange + var stream = Stream.Null; - // Assert - Assert.Equal(stream, result.FileStream); - } + // Act + var result = new FileStreamResult(stream, "text/plain"); - [Fact] - public void Constructor_SetsContentTypeAndParameters() - { - // Arrange - var stream = Stream.Null; - var contentType = "text/plain; charset=us-ascii; p1=p1-value"; - var expectedMediaType = contentType; + // Assert + Assert.Equal(stream, result.FileStream); + } - // Act - var result = new FileStreamResult(stream, contentType); + [Fact] + public void Constructor_SetsContentTypeAndParameters() + { + // Arrange + var stream = Stream.Null; + var contentType = "text/plain; charset=us-ascii; p1=p1-value"; + var expectedMediaType = contentType; - // Assert - Assert.Equal(stream, result.FileStream); - Assert.Equal(expectedMediaType, result.ContentType); - } + // Act + var result = new FileStreamResult(stream, contentType); - [Fact] - public void Constructor_SetsLastModifiedAndEtag() - { - // Arrange - var stream = Stream.Null; - var contentType = "text/plain"; - var expectedMediaType = contentType; - var lastModified = new DateTimeOffset(); - var entityTag = new EntityTagHeaderValue("\"Etag\""); + // Assert + Assert.Equal(stream, result.FileStream); + Assert.Equal(expectedMediaType, result.ContentType); + } - // Act - var result = new FileStreamResult(stream, contentType) - { - LastModified = lastModified, - EntityTag = entityTag, - }; + [Fact] + public void Constructor_SetsLastModifiedAndEtag() + { + // Arrange + var stream = Stream.Null; + var contentType = "text/plain"; + var expectedMediaType = contentType; + var lastModified = new DateTimeOffset(); + var entityTag = new EntityTagHeaderValue("\"Etag\""); - // Assert - Assert.Equal(lastModified, result.LastModified); - Assert.Equal(entityTag, result.EntityTag); - Assert.Equal(expectedMediaType, result.ContentType); - } + // Act + var result = new FileStreamResult(stream, contentType) + { + LastModified = lastModified, + EntityTag = entityTag, + }; + // Assert + Assert.Equal(lastModified, result.LastModified); + Assert.Equal(entityTag, result.EntityTag); + Assert.Equal(expectedMediaType, result.ContentType); } + } diff --git a/src/Http/Http.Results/test/ForbidResultTest.cs b/src/Http/Http.Results/test/ForbidResultTest.cs index f123d38469..42236c0066 100644 --- a/src/Http/Http.Results/test/ForbidResultTest.cs +++ b/src/Http/Http.Results/test/ForbidResultTest.cs @@ -10,120 +10,119 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class ForbidResultTest { - public class ForbidResultTest + [Fact] + public async Task ExecuteResultAsync_InvokesForbidAsyncOnAuthenticationService() { - [Fact] - public async Task ExecuteResultAsync_InvokesForbidAsyncOnAuthenticationService() - { - // Arrange - var auth = new Mock(); - auth - .Setup(c => c.ForbidAsync(It.IsAny(), "", null)) - .Returns(Task.CompletedTask) - .Verifiable(); - var httpContext = GetHttpContext(auth.Object); - var result = new ForbidResult("", null); - - // Act - await result.ExecuteAsync(httpContext); - - // Assert - auth.Verify(); - } - - [Fact] - public async Task ExecuteResultAsync_InvokesForbidAsyncOnAllConfiguredSchemes() + // Arrange + var auth = new Mock(); + auth + .Setup(c => c.ForbidAsync(It.IsAny(), "", null)) + .Returns(Task.CompletedTask) + .Verifiable(); + var httpContext = GetHttpContext(auth.Object); + var result = new ForbidResult("", null); + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + auth.Verify(); + } + + [Fact] + public async Task ExecuteResultAsync_InvokesForbidAsyncOnAllConfiguredSchemes() + { + // Arrange + var authProperties = new AuthenticationProperties(); + var auth = new Mock(); + auth + .Setup(c => c.ForbidAsync(It.IsAny(), "Scheme1", authProperties)) + .Returns(Task.CompletedTask) + .Verifiable(); + auth + .Setup(c => c.ForbidAsync(It.IsAny(), "Scheme2", authProperties)) + .Returns(Task.CompletedTask) + .Verifiable(); + var httpContext = GetHttpContext(auth.Object); + var result = new ForbidResult(new[] { "Scheme1", "Scheme2" }, authProperties); + var routeData = new RouteData(); + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + auth.Verify(); + } + + public static TheoryData ExecuteResultAsync_InvokesForbidAsyncWithAuthPropertiesData => + new TheoryData { - // Arrange - var authProperties = new AuthenticationProperties(); - var auth = new Mock(); - auth - .Setup(c => c.ForbidAsync(It.IsAny(), "Scheme1", authProperties)) - .Returns(Task.CompletedTask) - .Verifiable(); - auth - .Setup(c => c.ForbidAsync(It.IsAny(), "Scheme2", authProperties)) - .Returns(Task.CompletedTask) - .Verifiable(); - var httpContext = GetHttpContext(auth.Object); - var result = new ForbidResult(new[] { "Scheme1", "Scheme2" }, authProperties); - var routeData = new RouteData(); - - // Act - await result.ExecuteAsync(httpContext); - - // Assert - auth.Verify(); - } - - public static TheoryData ExecuteResultAsync_InvokesForbidAsyncWithAuthPropertiesData => - new TheoryData - { null, new AuthenticationProperties() - }; + }; - [Theory] - [MemberData(nameof(ExecuteResultAsync_InvokesForbidAsyncWithAuthPropertiesData))] - public async Task ExecuteResultAsync_InvokesForbidAsyncWithAuthProperties(AuthenticationProperties expected) - { - // Arrange - var auth = new Mock(); - auth - .Setup(c => c.ForbidAsync(It.IsAny(), null, expected)) - .Returns(Task.CompletedTask) - .Verifiable(); - var result = new ForbidResult(expected); - var httpContext = GetHttpContext(auth.Object); - - // Act - await result.ExecuteAsync(httpContext); - - // Assert - auth.Verify(); - } - - [Theory] - [MemberData(nameof(ExecuteResultAsync_InvokesForbidAsyncWithAuthPropertiesData))] - public async Task ExecuteResultAsync_InvokesForbidAsyncWithAuthProperties_WhenAuthenticationSchemesIsEmpty( - AuthenticationProperties expected) - { - // Arrange - var auth = new Mock(); - auth - .Setup(c => c.ForbidAsync(It.IsAny(), null, expected)) - .Returns(Task.CompletedTask) - .Verifiable(); - var httpContext = GetHttpContext(auth.Object); - var result = new ForbidResult(expected) - { - AuthenticationSchemes = new string[0] - }; - var routeData = new RouteData(); - - // Act - await result.ExecuteAsync(httpContext); - - // Assert - auth.Verify(); - } - - private static DefaultHttpContext GetHttpContext(IAuthenticationService auth) - { - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = CreateServices() - .AddSingleton(auth) - .BuildServiceProvider(); - return httpContext; - } - - private static IServiceCollection CreateServices() + [Theory] + [MemberData(nameof(ExecuteResultAsync_InvokesForbidAsyncWithAuthPropertiesData))] + public async Task ExecuteResultAsync_InvokesForbidAsyncWithAuthProperties(AuthenticationProperties expected) + { + // Arrange + var auth = new Mock(); + auth + .Setup(c => c.ForbidAsync(It.IsAny(), null, expected)) + .Returns(Task.CompletedTask) + .Verifiable(); + var result = new ForbidResult(expected); + var httpContext = GetHttpContext(auth.Object); + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + auth.Verify(); + } + + [Theory] + [MemberData(nameof(ExecuteResultAsync_InvokesForbidAsyncWithAuthPropertiesData))] + public async Task ExecuteResultAsync_InvokesForbidAsyncWithAuthProperties_WhenAuthenticationSchemesIsEmpty( + AuthenticationProperties expected) + { + // Arrange + var auth = new Mock(); + auth + .Setup(c => c.ForbidAsync(It.IsAny(), null, expected)) + .Returns(Task.CompletedTask) + .Verifiable(); + var httpContext = GetHttpContext(auth.Object); + var result = new ForbidResult(expected) { - var services = new ServiceCollection(); - services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); - return services; - } + AuthenticationSchemes = new string[0] + }; + var routeData = new RouteData(); + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + auth.Verify(); + } + + private static DefaultHttpContext GetHttpContext(IAuthenticationService auth) + { + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = CreateServices() + .AddSingleton(auth) + .BuildServiceProvider(); + return httpContext; + } + + private static IServiceCollection CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + return services; } } diff --git a/src/Http/Http.Results/test/LocalRedirectResultTest.cs b/src/Http/Http.Results/test/LocalRedirectResultTest.cs index d059f50540..c641cfcb06 100644 --- a/src/Http/Http.Results/test/LocalRedirectResultTest.cs +++ b/src/Http/Http.Results/test/LocalRedirectResultTest.cs @@ -8,131 +8,130 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class LocalRedirectResultTest { - public class LocalRedirectResultTest + [Fact] + public void Constructor_WithParameterUrl_SetsResultUrlAndNotPermanentOrPreserveMethod() + { + // Arrange + var url = "/test/url"; + + // Act + var result = new LocalRedirectResult(url); + + // Assert + Assert.False(result.PreserveMethod); + Assert.False(result.Permanent); + Assert.Same(url, result.Url); + } + + [Fact] + public void Constructor_WithParameterUrlAndPermanent_SetsResultUrlAndPermanentNotPreserveMethod() + { + // Arrange + var url = "/test/url"; + + // Act + var result = new LocalRedirectResult(url, permanent: true); + + // Assert + Assert.False(result.PreserveMethod); + Assert.True(result.Permanent); + Assert.Same(url, result.Url); + } + + [Fact] + public void Constructor_WithParameterUrlAndPermanent_SetsResultUrlPermanentAndPreserveMethod() + { + // Arrange + var url = "/test/url"; + + // Act + var result = new LocalRedirectResult(url, permanent: true, preserveMethod: true); + + // Assert + Assert.True(result.PreserveMethod); + Assert.True(result.Permanent); + Assert.Same(url, result.Url); + } + + [Fact] + public async Task Execute_ReturnsExpectedValues() + { + // Arrange + var appRoot = "/"; + var contentPath = "~/Home/About"; + var expectedPath = "/Home/About"; + + var httpContext = GetHttpContext(appRoot); + var result = new LocalRedirectResult(contentPath); + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(expectedPath, httpContext.Response.Headers.Location.ToString()); + Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode); + } + + [Theory] + [InlineData("", "//")] + [InlineData("", "/\\")] + [InlineData("", "//foo")] + [InlineData("", "/\\foo")] + [InlineData("", "Home/About")] + [InlineData("/myapproot", "http://www.example.com")] + public async Task Execute_Throws_ForNonLocalUrl( + string appRoot, + string contentPath) + { + // Arrange + var httpContext = GetHttpContext(appRoot); + var result = new LocalRedirectResult(contentPath); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => result.ExecuteAsync(httpContext)); + Assert.Equal( + "The supplied URL is not local. A URL with an absolute path is considered local if it does not " + + "have a host/authority part. URLs using virtual paths ('~/') are also local.", + exception.Message); + } + + [Theory] + [InlineData("", "~//")] + [InlineData("", "~/\\")] + [InlineData("", "~//foo")] + [InlineData("", "~/\\foo")] + public async Task Execute_Throws_ForNonLocalUrlTilde( + string appRoot, + string contentPath) + { + // Arrange + var httpContext = GetHttpContext(appRoot); + var result = new LocalRedirectResult(contentPath); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => result.ExecuteAsync(httpContext)); + Assert.Equal( + "The supplied URL is not local. A URL with an absolute path is considered local if it does not " + + "have a host/authority part. URLs using virtual paths ('~/') are also local.", + exception.Message); + } + + private static IServiceProvider GetServiceProvider() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddTransient(typeof(ILogger<>), typeof(NullLogger<>)); + return serviceCollection.BuildServiceProvider(); + } + + private static HttpContext GetHttpContext(string appRoot) { - [Fact] - public void Constructor_WithParameterUrl_SetsResultUrlAndNotPermanentOrPreserveMethod() - { - // Arrange - var url = "/test/url"; - - // Act - var result = new LocalRedirectResult(url); - - // Assert - Assert.False(result.PreserveMethod); - Assert.False(result.Permanent); - Assert.Same(url, result.Url); - } - - [Fact] - public void Constructor_WithParameterUrlAndPermanent_SetsResultUrlAndPermanentNotPreserveMethod() - { - // Arrange - var url = "/test/url"; - - // Act - var result = new LocalRedirectResult(url, permanent: true); - - // Assert - Assert.False(result.PreserveMethod); - Assert.True(result.Permanent); - Assert.Same(url, result.Url); - } - - [Fact] - public void Constructor_WithParameterUrlAndPermanent_SetsResultUrlPermanentAndPreserveMethod() - { - // Arrange - var url = "/test/url"; - - // Act - var result = new LocalRedirectResult(url, permanent: true, preserveMethod: true); - - // Assert - Assert.True(result.PreserveMethod); - Assert.True(result.Permanent); - Assert.Same(url, result.Url); - } - - [Fact] - public async Task Execute_ReturnsExpectedValues() - { - // Arrange - var appRoot = "/"; - var contentPath = "~/Home/About"; - var expectedPath = "/Home/About"; - - var httpContext = GetHttpContext(appRoot); - var result = new LocalRedirectResult(contentPath); - - // Act - await result.ExecuteAsync(httpContext); - - // Assert - Assert.Equal(expectedPath, httpContext.Response.Headers.Location.ToString()); - Assert.Equal(StatusCodes.Status302Found, httpContext.Response.StatusCode); - } - - [Theory] - [InlineData("", "//")] - [InlineData("", "/\\")] - [InlineData("", "//foo")] - [InlineData("", "/\\foo")] - [InlineData("", "Home/About")] - [InlineData("/myapproot", "http://www.example.com")] - public async Task Execute_Throws_ForNonLocalUrl( - string appRoot, - string contentPath) - { - // Arrange - var httpContext = GetHttpContext(appRoot); - var result = new LocalRedirectResult(contentPath); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => result.ExecuteAsync(httpContext)); - Assert.Equal( - "The supplied URL is not local. A URL with an absolute path is considered local if it does not " + - "have a host/authority part. URLs using virtual paths ('~/') are also local.", - exception.Message); - } - - [Theory] - [InlineData("", "~//")] - [InlineData("", "~/\\")] - [InlineData("", "~//foo")] - [InlineData("", "~/\\foo")] - public async Task Execute_Throws_ForNonLocalUrlTilde( - string appRoot, - string contentPath) - { - // Arrange - var httpContext = GetHttpContext(appRoot); - var result = new LocalRedirectResult(contentPath); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => result.ExecuteAsync(httpContext)); - Assert.Equal( - "The supplied URL is not local. A URL with an absolute path is considered local if it does not " + - "have a host/authority part. URLs using virtual paths ('~/') are also local.", - exception.Message); - } - - private static IServiceProvider GetServiceProvider() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddTransient(typeof(ILogger<>), typeof(NullLogger<>)); - return serviceCollection.BuildServiceProvider(); - } - - private static HttpContext GetHttpContext(string appRoot) - { - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = GetServiceProvider(); - httpContext.Request.PathBase = new PathString(appRoot); - return httpContext; - } + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = GetServiceProvider(); + httpContext.Request.PathBase = new PathString(appRoot); + return httpContext; } } diff --git a/src/Http/Http.Results/test/NotFoundObjectResultTest.cs b/src/Http/Http.Results/test/NotFoundObjectResultTest.cs index af6387f9fa..59604dc062 100644 --- a/src/Http/Http.Results/test/NotFoundObjectResultTest.cs +++ b/src/Http/Http.Results/test/NotFoundObjectResultTest.cs @@ -9,59 +9,58 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class NotFoundObjectResultTest { - public class NotFoundObjectResultTest + [Fact] + public void NotFoundObjectResult_InitializesStatusCode() { - [Fact] - public void NotFoundObjectResult_InitializesStatusCode() - { - // Arrange & act - var notFound = new NotFoundObjectResult(null); + // Arrange & act + var notFound = new NotFoundObjectResult(null); - // Assert - Assert.Equal(StatusCodes.Status404NotFound, notFound.StatusCode); - } + // Assert + Assert.Equal(StatusCodes.Status404NotFound, notFound.StatusCode); + } - [Fact] - public void NotFoundObjectResult_InitializesStatusCodeAndResponseContent() - { - // Arrange & act - var notFound = new NotFoundObjectResult("Test Content"); + [Fact] + public void NotFoundObjectResult_InitializesStatusCodeAndResponseContent() + { + // Arrange & act + var notFound = new NotFoundObjectResult("Test Content"); - // Assert - Assert.Equal(StatusCodes.Status404NotFound, notFound.StatusCode); - Assert.Equal("Test Content", notFound.Value); - } + // Assert + Assert.Equal(StatusCodes.Status404NotFound, notFound.StatusCode); + Assert.Equal("Test Content", notFound.Value); + } - [Fact] - public async Task NotFoundObjectResult_ExecuteSuccessful() - { - // Arrange - var httpContext = GetHttpContext(); - var result = new NotFoundObjectResult("Test Content"); + [Fact] + public async Task NotFoundObjectResult_ExecuteSuccessful() + { + // Arrange + var httpContext = GetHttpContext(); + var result = new NotFoundObjectResult("Test Content"); - // Act - await result.ExecuteAsync(httpContext); + // Act + await result.ExecuteAsync(httpContext); - // Assert - Assert.Equal(StatusCodes.Status404NotFound, httpContext.Response.StatusCode); - } + // Assert + Assert.Equal(StatusCodes.Status404NotFound, httpContext.Response.StatusCode); + } - private static HttpContext GetHttpContext() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.PathBase = new PathString(""); - httpContext.Response.Body = new MemoryStream(); - httpContext.RequestServices = CreateServices(); - return httpContext; - } + private static HttpContext GetHttpContext() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.PathBase = new PathString(""); + httpContext.Response.Body = new MemoryStream(); + httpContext.RequestServices = CreateServices(); + return httpContext; + } - private static IServiceProvider CreateServices() - { - var services = new ServiceCollection(); - services.AddSingleton(); - return services.BuildServiceProvider(); - } + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(); + return services.BuildServiceProvider(); } } diff --git a/src/Http/Http.Results/test/ObjectResultTests.cs b/src/Http/Http.Results/test/ObjectResultTests.cs index 849708dc33..59160a9dcb 100644 --- a/src/Http/Http.Results/test/ObjectResultTests.cs +++ b/src/Http/Http.Results/test/ObjectResultTests.cs @@ -10,195 +10,194 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class ObjectResultTests { - public class ObjectResultTests + [Fact] + public async Task ObjectResult_ExecuteAsync_WithNullValue_Works() { - [Fact] - public async Task ObjectResult_ExecuteAsync_WithNullValue_Works() + // Arrange + var result = new ObjectResult(value: null, 411); + + var httpContext = new DefaultHttpContext() { - // Arrange - var result = new ObjectResult(value: null, 411); + RequestServices = CreateServices(), + }; - var httpContext = new DefaultHttpContext() - { - RequestServices = CreateServices(), - }; + // Act + await result.ExecuteAsync(httpContext); - // Act - await result.ExecuteAsync(httpContext); + // Assert + Assert.Equal(411, httpContext.Response.StatusCode); + } - // Assert - Assert.Equal(411, httpContext.Response.StatusCode); - } + [Fact] + public async Task ObjectResult_ExecuteAsync_SetsStatusCode() + { + // Arrange + var result = new ObjectResult("Hello", 407); - [Fact] - public async Task ObjectResult_ExecuteAsync_SetsStatusCode() + var httpContext = new DefaultHttpContext() { - // Arrange - var result = new ObjectResult("Hello", 407); + RequestServices = CreateServices(), + }; - var httpContext = new DefaultHttpContext() - { - RequestServices = CreateServices(), - }; + // Act + await result.ExecuteAsync(httpContext); - // Act - await result.ExecuteAsync(httpContext); - - // Assert - Assert.Equal(407, httpContext.Response.StatusCode); - } + // Assert + Assert.Equal(407, httpContext.Response.StatusCode); + } - [Fact] - public async Task ObjectResult_ExecuteAsync_JsonSerializesBody() + [Fact] + public async Task ObjectResult_ExecuteAsync_JsonSerializesBody() + { + // Arrange + var result = new ObjectResult("Hello", 407); + var stream = new MemoryStream(); + var httpContext = new DefaultHttpContext() { - // Arrange - var result = new ObjectResult("Hello", 407); - var stream = new MemoryStream(); - var httpContext = new DefaultHttpContext() - { - RequestServices = CreateServices(), - Response = + RequestServices = CreateServices(), + Response = { Body = stream, }, - }; + }; - // Act - await result.ExecuteAsync(httpContext); + // Act + await result.ExecuteAsync(httpContext); - // Assert - Assert.Equal("\"Hello\"", Encoding.UTF8.GetString(stream.ToArray())); - } + // Assert + Assert.Equal("\"Hello\"", Encoding.UTF8.GetString(stream.ToArray())); + } + + [Fact] + public async Task ExecuteAsync_UsesDefaults_ForProblemDetails() + { + // Arrange + var details = new ProblemDetails(); - [Fact] - public async Task ExecuteAsync_UsesDefaults_ForProblemDetails() + var result = new ObjectResult(details); + var stream = new MemoryStream(); + var httpContext = new DefaultHttpContext() { - // Arrange - var details = new ProblemDetails(); - - var result = new ObjectResult(details); - var stream = new MemoryStream(); - var httpContext = new DefaultHttpContext() - { - RequestServices = CreateServices(), - Response = + RequestServices = CreateServices(), + Response = { Body = stream, }, - }; - - // Act - await result.ExecuteAsync(httpContext); - - // Assert - Assert.Equal(StatusCodes.Status500InternalServerError, httpContext.Response.StatusCode); - stream.Position = 0; - var responseDetails = JsonSerializer.Deserialize(stream); - Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", responseDetails.Type); - Assert.Equal("An error occurred while processing your request.", responseDetails.Title); - Assert.Equal(StatusCodes.Status500InternalServerError, responseDetails.Status); - } - - [Fact] - public async Task ExecuteAsync_UsesDefaults_ForValidationProblemDetails() + }; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status500InternalServerError, httpContext.Response.StatusCode); + stream.Position = 0; + var responseDetails = JsonSerializer.Deserialize(stream); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", responseDetails.Type); + Assert.Equal("An error occurred while processing your request.", responseDetails.Title); + Assert.Equal(StatusCodes.Status500InternalServerError, responseDetails.Status); + } + + [Fact] + public async Task ExecuteAsync_UsesDefaults_ForValidationProblemDetails() + { + // Arrange + var details = new HttpValidationProblemDetails(); + + var result = new ObjectResult(details); + var stream = new MemoryStream(); + var httpContext = new DefaultHttpContext() { - // Arrange - var details = new HttpValidationProblemDetails(); - - var result = new ObjectResult(details); - var stream = new MemoryStream(); - var httpContext = new DefaultHttpContext() - { - RequestServices = CreateServices(), - Response = + RequestServices = CreateServices(), + Response = { Body = stream, }, - }; - - // Act - await result.ExecuteAsync(httpContext); - - // Assert - Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode); - stream.Position = 0; - var responseDetails = JsonSerializer.Deserialize(stream); - Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", responseDetails.Type); - Assert.Equal("One or more validation errors occurred.", responseDetails.Title); - Assert.Equal(StatusCodes.Status400BadRequest, responseDetails.Status); - } - - [Fact] - public async Task ExecuteAsync_SetsProblemDetailsStatus_ForValidationProblemDetails() - { - // Arrange - var details = new HttpValidationProblemDetails(); - - var result = new ObjectResult(details, StatusCodes.Status422UnprocessableEntity); - var httpContext = new DefaultHttpContext() - { - RequestServices = CreateServices(), - }; - - // Act - await result.ExecuteAsync(httpContext); + }; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode); + stream.Position = 0; + var responseDetails = JsonSerializer.Deserialize(stream); + Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", responseDetails.Type); + Assert.Equal("One or more validation errors occurred.", responseDetails.Title); + Assert.Equal(StatusCodes.Status400BadRequest, responseDetails.Status); + } - // Assert - Assert.Equal(StatusCodes.Status422UnprocessableEntity, details.Status.Value); - } + [Fact] + public async Task ExecuteAsync_SetsProblemDetailsStatus_ForValidationProblemDetails() + { + // Arrange + var details = new HttpValidationProblemDetails(); - [Fact] - public async Task ExecuteAsync_GetsStatusCodeFromProblemDetails() + var result = new ObjectResult(details, StatusCodes.Status422UnprocessableEntity); + var httpContext = new DefaultHttpContext() { - // Arrange - var details = new ProblemDetails { Status = StatusCodes.Status413RequestEntityTooLarge, }; + RequestServices = CreateServices(), + }; - var result = new ObjectResult(details); + // Act + await result.ExecuteAsync(httpContext); - var httpContext = new DefaultHttpContext() - { - RequestServices = CreateServices(), - }; + // Assert + Assert.Equal(StatusCodes.Status422UnprocessableEntity, details.Status.Value); + } - // Act - await result.ExecuteAsync(httpContext); + [Fact] + public async Task ExecuteAsync_GetsStatusCodeFromProblemDetails() + { + // Arrange + var details = new ProblemDetails { Status = StatusCodes.Status413RequestEntityTooLarge, }; - // Assert - Assert.Equal(StatusCodes.Status413RequestEntityTooLarge, details.Status.Value); - Assert.Equal(StatusCodes.Status413RequestEntityTooLarge, result.StatusCode.Value); - Assert.Equal(StatusCodes.Status413RequestEntityTooLarge, httpContext.Response.StatusCode); - } + var result = new ObjectResult(details); - [Fact] - public async Task ExecuteAsync_UsesStatusCodeFromResultTypeForProblemDetails() + var httpContext = new DefaultHttpContext() { - // Arrange - var details = new ProblemDetails { Status = StatusCodes.Status422UnprocessableEntity, }; + RequestServices = CreateServices(), + }; - var result = new BadRequestObjectResult(details); + // Act + await result.ExecuteAsync(httpContext); - var httpContext = new DefaultHttpContext() - { - RequestServices = CreateServices(), - }; + // Assert + Assert.Equal(StatusCodes.Status413RequestEntityTooLarge, details.Status.Value); + Assert.Equal(StatusCodes.Status413RequestEntityTooLarge, result.StatusCode.Value); + Assert.Equal(StatusCodes.Status413RequestEntityTooLarge, httpContext.Response.StatusCode); + } - // Act - await result.ExecuteAsync(httpContext); + [Fact] + public async Task ExecuteAsync_UsesStatusCodeFromResultTypeForProblemDetails() + { + // Arrange + var details = new ProblemDetails { Status = StatusCodes.Status422UnprocessableEntity, }; - // Assert - Assert.Equal(StatusCodes.Status422UnprocessableEntity, details.Status.Value); - Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode.Value); - Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode); - } + var result = new BadRequestObjectResult(details); - private static IServiceProvider CreateServices() + var httpContext = new DefaultHttpContext() { - var services = new ServiceCollection(); - services.AddSingleton(NullLoggerFactory.Instance); + RequestServices = CreateServices(), + }; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(StatusCodes.Status422UnprocessableEntity, details.Status.Value); + Assert.Equal(StatusCodes.Status400BadRequest, result.StatusCode.Value); + Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode); + } + + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); - return services.BuildServiceProvider(); - } + return services.BuildServiceProvider(); } } diff --git a/src/Http/Http.Results/test/OkObjectResultTest.cs b/src/Http/Http.Results/test/OkObjectResultTest.cs index 39b7f47db7..d154863360 100644 --- a/src/Http/Http.Results/test/OkObjectResultTest.cs +++ b/src/Http/Http.Results/test/OkObjectResultTest.cs @@ -9,38 +9,37 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class OkObjectResultTest { - public class OkObjectResultTest + [Fact] + public async Task OkObjectResult_SetsStatusCodeAndValue() { - [Fact] - public async Task OkObjectResult_SetsStatusCodeAndValue() - { - // Arrange - var result = new OkObjectResult("Hello world"); - var httpContext = GetHttpContext(); + // Arrange + var result = new OkObjectResult("Hello world"); + var httpContext = GetHttpContext(); - // Act - await result.ExecuteAsync(httpContext); + // Act + await result.ExecuteAsync(httpContext); - // Assert - Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); - } + // Assert + Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); + } - private static HttpContext GetHttpContext() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.PathBase = new PathString(""); - httpContext.Response.Body = new MemoryStream(); - httpContext.RequestServices = CreateServices(); - return httpContext; - } + private static HttpContext GetHttpContext() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.PathBase = new PathString(""); + httpContext.Response.Body = new MemoryStream(); + httpContext.RequestServices = CreateServices(); + return httpContext; + } - private static IServiceProvider CreateServices() - { - var services = new ServiceCollection(); - services.AddSingleton(); - return services.BuildServiceProvider(); - } + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(); + return services.BuildServiceProvider(); } } diff --git a/src/Http/Http.Results/test/PhysicalFileResultTest.cs b/src/Http/Http.Results/test/PhysicalFileResultTest.cs index 1a99d1aa93..f64808ee54 100644 --- a/src/Http/Http.Results/test/PhysicalFileResultTest.cs +++ b/src/Http/Http.Results/test/PhysicalFileResultTest.cs @@ -6,36 +6,35 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Internal; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class PhysicalFileResultTest : PhysicalFileResultTestBase { - public class PhysicalFileResultTest : PhysicalFileResultTestBase + protected override Task ExecuteAsync( + HttpContext httpContext, + string path, + string contentType, + DateTimeOffset? lastModified = null, + EntityTagHeaderValue entityTag = null, + bool enableRangeProcessing = false) { - protected override Task ExecuteAsync( - HttpContext httpContext, - string path, - string contentType, - DateTimeOffset? lastModified = null, - EntityTagHeaderValue entityTag = null, - bool enableRangeProcessing = false) + var fileResult = new PhysicalFileResult(path, contentType) { - var fileResult = new PhysicalFileResult(path, contentType) + LastModified = lastModified, + EntityTag = entityTag, + EnableRangeProcessing = enableRangeProcessing, + GetFileInfoWrapper = (path) => { - LastModified = lastModified, - EntityTag = entityTag, - EnableRangeProcessing = enableRangeProcessing, - GetFileInfoWrapper = (path) => + var lastModified = DateTimeOffset.MinValue.AddDays(1); + return new() { - var lastModified = DateTimeOffset.MinValue.AddDays(1); - return new() - { - Exists = true, - Length = 34, - LastWriteTimeUtc = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0)) - }; - } - }; + Exists = true, + Length = 34, + LastWriteTimeUtc = new DateTimeOffset(lastModified.Year, lastModified.Month, lastModified.Day, lastModified.Hour, lastModified.Minute, lastModified.Second, TimeSpan.FromSeconds(0)) + }; + } + }; - return fileResult.ExecuteAsync(httpContext); - } + return fileResult.ExecuteAsync(httpContext); } } diff --git a/src/Http/Http.Results/test/RedirectResultTest.cs b/src/Http/Http.Results/test/RedirectResultTest.cs index 2b0567e12a..a6fc4d31f8 100644 --- a/src/Http/Http.Results/test/RedirectResultTest.cs +++ b/src/Http/Http.Results/test/RedirectResultTest.cs @@ -4,29 +4,28 @@ using Microsoft.AspNetCore.Internal; using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class RedirectResultTest : RedirectResultTestBase { - public class RedirectResultTest : RedirectResultTestBase + [Fact] + public void RedirectResult_Constructor_WithParameterUrlPermanentAndPreservesMethod_SetsResultUrlPermanentAndPreservesMethod() { - [Fact] - public void RedirectResult_Constructor_WithParameterUrlPermanentAndPreservesMethod_SetsResultUrlPermanentAndPreservesMethod() - { - // Arrange - var url = "/test/url"; + // Arrange + var url = "/test/url"; - // Act - var result = new RedirectResult(url, permanent: true, preserveMethod: true); + // Act + var result = new RedirectResult(url, permanent: true, preserveMethod: true); - // Assert - Assert.True(result.PreserveMethod); - Assert.True(result.Permanent); - Assert.Same(url, result.Url); - } + // Assert + Assert.True(result.PreserveMethod); + Assert.True(result.Permanent); + Assert.Same(url, result.Url); + } - protected override Task ExecuteAsync(HttpContext httpContext, string contentPath) - { - var redirectResult = new RedirectResult(contentPath, false, false); - return redirectResult.ExecuteAsync(httpContext); - } + protected override Task ExecuteAsync(HttpContext httpContext, string contentPath) + { + var redirectResult = new RedirectResult(contentPath, false, false); + return redirectResult.ExecuteAsync(httpContext); } } diff --git a/src/Http/Http.Results/test/RedirectToRouteResultTest.cs b/src/Http/Http.Results/test/RedirectToRouteResultTest.cs index b2e2e9c05a..2d5af54aae 100644 --- a/src/Http/Http.Results/test/RedirectToRouteResultTest.cs +++ b/src/Http/Http.Results/test/RedirectToRouteResultTest.cs @@ -12,100 +12,99 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class RedirectToRouteResultTest { - public class RedirectToRouteResultTest + [Fact] + public async Task RedirectToRoute_Execute_ThrowsOnNullUrl() + { + // Arrange + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = CreateServices(null).BuildServiceProvider(); + + var result = new RedirectToRouteResult(null, new Dictionary()); + + // Act & Assert + await ExceptionAssert.ThrowsAsync( + async () => + { + await result.ExecuteAsync(httpContext); + }, + "No route matches the supplied values."); + } + + [Fact] + public async Task ExecuteResultAsync_UsesRouteName_ToGenerateLocationHeader() { - [Fact] - public async Task RedirectToRoute_Execute_ThrowsOnNullUrl() - { - // Arrange - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = CreateServices(null).BuildServiceProvider(); - - var result = new RedirectToRouteResult(null, new Dictionary()); - - // Act & Assert - await ExceptionAssert.ThrowsAsync( - async () => - { - await result.ExecuteAsync(httpContext); - }, - "No route matches the supplied values."); - } - - [Fact] - public async Task ExecuteResultAsync_UsesRouteName_ToGenerateLocationHeader() - { - // Arrange - var routeName = "orders_api"; - var locationUrl = "/api/orders/10"; - - var httpContext = GetHttpContext(locationUrl); - - var result = new RedirectToRouteResult(routeName, new { id = 10 }); - - // Act - await result.ExecuteAsync(httpContext); - - // Assert - Assert.True(httpContext.Response.Headers.ContainsKey("Location"), "Location header not found"); - Assert.Equal(locationUrl, httpContext.Response.Headers["Location"]); - } - - [Fact] - public async Task ExecuteResultAsync_WithFragment_PassesCorrectValuesToRedirect() - { - // Arrange - var expectedUrl = "/SampleAction#test"; - var expectedStatusCode = StatusCodes.Status301MovedPermanently; - var httpContext = GetHttpContext(expectedUrl); - - var result = new RedirectToRouteResult("Sample", null, true, "test"); - - // Act - await result.ExecuteAsync(httpContext); - - // Assert - Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode); - Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]); - } - - [Fact] - public async Task ExecuteResultAsync_WithFragment_PassesCorrectValuesToRedirect_WithPreserveMethod() - { - // Arrange - var expectedUrl = "/SampleAction#test"; - var expectedStatusCode = StatusCodes.Status308PermanentRedirect; - - var httpContext = GetHttpContext(expectedUrl); - var result = new RedirectToRouteResult("Sample", null, true, true, "test"); - - // Act - await result.ExecuteAsync(httpContext); - - // Assert - Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode); - Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]); - } - - private static HttpContext GetHttpContext(string path) - { - var services = CreateServices(path); - - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = services.BuildServiceProvider(); - - return httpContext; - } - - private static IServiceCollection CreateServices(string path) - { - var services = new ServiceCollection(); - services.AddSingleton(new TestLinkGenerator { Url = path }); - - services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); - return services; - } + // Arrange + var routeName = "orders_api"; + var locationUrl = "/api/orders/10"; + + var httpContext = GetHttpContext(locationUrl); + + var result = new RedirectToRouteResult(routeName, new { id = 10 }); + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.True(httpContext.Response.Headers.ContainsKey("Location"), "Location header not found"); + Assert.Equal(locationUrl, httpContext.Response.Headers["Location"]); + } + + [Fact] + public async Task ExecuteResultAsync_WithFragment_PassesCorrectValuesToRedirect() + { + // Arrange + var expectedUrl = "/SampleAction#test"; + var expectedStatusCode = StatusCodes.Status301MovedPermanently; + var httpContext = GetHttpContext(expectedUrl); + + var result = new RedirectToRouteResult("Sample", null, true, "test"); + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode); + Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]); + } + + [Fact] + public async Task ExecuteResultAsync_WithFragment_PassesCorrectValuesToRedirect_WithPreserveMethod() + { + // Arrange + var expectedUrl = "/SampleAction#test"; + var expectedStatusCode = StatusCodes.Status308PermanentRedirect; + + var httpContext = GetHttpContext(expectedUrl); + var result = new RedirectToRouteResult("Sample", null, true, true, "test"); + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode); + Assert.Equal(expectedUrl, httpContext.Response.Headers["Location"]); + } + + private static HttpContext GetHttpContext(string path) + { + var services = CreateServices(path); + + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = services.BuildServiceProvider(); + + return httpContext; + } + + private static IServiceCollection CreateServices(string path) + { + var services = new ServiceCollection(); + services.AddSingleton(new TestLinkGenerator { Url = path }); + + services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + return services; } } diff --git a/src/Http/Http.Results/test/SignInResultTest.cs b/src/Http/Http.Results/test/SignInResultTest.cs index 2de4d09df7..ba80cce51a 100644 --- a/src/Http/Http.Results/test/SignInResultTest.cs +++ b/src/Http/Http.Results/test/SignInResultTest.cs @@ -10,86 +10,85 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class SignInResultTest { - public class SignInResultTest + [Fact] + public async Task ExecuteAsync_InvokesSignInAsyncOnAuthenticationManager() { - [Fact] - public async Task ExecuteAsync_InvokesSignInAsyncOnAuthenticationManager() - { - // Arrange - var principal = new ClaimsPrincipal(); - var auth = new Mock(); - auth - .Setup(c => c.SignInAsync(It.IsAny(), "", principal, null)) - .Returns(Task.CompletedTask) - .Verifiable(); + // Arrange + var principal = new ClaimsPrincipal(); + var auth = new Mock(); + auth + .Setup(c => c.SignInAsync(It.IsAny(), "", principal, null)) + .Returns(Task.CompletedTask) + .Verifiable(); - var httpContext = GetHttpContext(auth.Object); - var result = new SignInResult("", principal, null); + var httpContext = GetHttpContext(auth.Object); + var result = new SignInResult("", principal, null); - // Act - await result.ExecuteAsync(httpContext); + // Act + await result.ExecuteAsync(httpContext); - // Assert - auth.Verify(); - } + // Assert + auth.Verify(); + } - [Fact] - public async Task ExecuteAsync_InvokesSignInAsyncOnAuthenticationManagerWithDefaultScheme() - { - // Arrange - var principal = new ClaimsPrincipal(); - var auth = new Mock(); - auth - .Setup(c => c.SignInAsync(It.IsAny(), null, principal, null)) - .Returns(Task.CompletedTask) - .Verifiable(); - var httpContext = GetHttpContext(auth.Object); - var result = new SignInResult(principal); + [Fact] + public async Task ExecuteAsync_InvokesSignInAsyncOnAuthenticationManagerWithDefaultScheme() + { + // Arrange + var principal = new ClaimsPrincipal(); + var auth = new Mock(); + auth + .Setup(c => c.SignInAsync(It.IsAny(), null, principal, null)) + .Returns(Task.CompletedTask) + .Verifiable(); + var httpContext = GetHttpContext(auth.Object); + var result = new SignInResult(principal); - // Act - await result.ExecuteAsync(httpContext); + // Act + await result.ExecuteAsync(httpContext); - // Assert - auth.Verify(); - } + // Assert + auth.Verify(); + } - [Fact] - public async Task ExecuteAsync_InvokesSignInAsyncOnConfiguredScheme() - { - // Arrange - var principal = new ClaimsPrincipal(); - var authProperties = new AuthenticationProperties(); - var auth = new Mock(); - auth - .Setup(c => c.SignInAsync(It.IsAny(), "Scheme1", principal, authProperties)) - .Returns(Task.CompletedTask) - .Verifiable(); - var httpContext = GetHttpContext(auth.Object); - var result = new SignInResult("Scheme1", principal, authProperties); + [Fact] + public async Task ExecuteAsync_InvokesSignInAsyncOnConfiguredScheme() + { + // Arrange + var principal = new ClaimsPrincipal(); + var authProperties = new AuthenticationProperties(); + var auth = new Mock(); + auth + .Setup(c => c.SignInAsync(It.IsAny(), "Scheme1", principal, authProperties)) + .Returns(Task.CompletedTask) + .Verifiable(); + var httpContext = GetHttpContext(auth.Object); + var result = new SignInResult("Scheme1", principal, authProperties); - // Act - await result.ExecuteAsync(httpContext); + // Act + await result.ExecuteAsync(httpContext); - // Assert - auth.Verify(); - } + // Assert + auth.Verify(); + } - private static DefaultHttpContext GetHttpContext(IAuthenticationService auth) - { - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = CreateServices() - .AddSingleton(auth) - .BuildServiceProvider(); - return httpContext; - } + private static DefaultHttpContext GetHttpContext(IAuthenticationService auth) + { + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = CreateServices() + .AddSingleton(auth) + .BuildServiceProvider(); + return httpContext; + } - private static IServiceCollection CreateServices() - { - var services = new ServiceCollection(); - services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); - return services; - } + private static IServiceCollection CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + return services; } } diff --git a/src/Http/Http.Results/test/SignOutResultTest.cs b/src/Http/Http.Results/test/SignOutResultTest.cs index 055ee04637..8f7368be11 100644 --- a/src/Http/Http.Results/test/SignOutResultTest.cs +++ b/src/Http/Http.Results/test/SignOutResultTest.cs @@ -10,86 +10,85 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class SignOutResultTest { - public class SignOutResultTest + [Fact] + public async Task ExecuteAsync_NoArgsInvokesDefaultSignOut() { - [Fact] - public async Task ExecuteAsync_NoArgsInvokesDefaultSignOut() - { - // Arrange - var auth = new Mock(); - auth - .Setup(c => c.SignOutAsync(It.IsAny(), null, null)) - .Returns(Task.CompletedTask) - .Verifiable(); - var httpContext = GetHttpContext(auth.Object); - var result = new SignOutResult(); + // Arrange + var auth = new Mock(); + auth + .Setup(c => c.SignOutAsync(It.IsAny(), null, null)) + .Returns(Task.CompletedTask) + .Verifiable(); + var httpContext = GetHttpContext(auth.Object); + var result = new SignOutResult(); - // Act - await result.ExecuteAsync(httpContext); + // Act + await result.ExecuteAsync(httpContext); - // Assert - auth.Verify(); - } + // Assert + auth.Verify(); + } - [Fact] - public async Task ExecuteAsync_InvokesSignOutAsyncOnAuthenticationManager() - { - // Arrange - var auth = new Mock(); - auth - .Setup(c => c.SignOutAsync(It.IsAny(), "", null)) - .Returns(Task.CompletedTask) - .Verifiable(); - var httpContext = GetHttpContext(auth.Object); - var result = new SignOutResult("", null); + [Fact] + public async Task ExecuteAsync_InvokesSignOutAsyncOnAuthenticationManager() + { + // Arrange + var auth = new Mock(); + auth + .Setup(c => c.SignOutAsync(It.IsAny(), "", null)) + .Returns(Task.CompletedTask) + .Verifiable(); + var httpContext = GetHttpContext(auth.Object); + var result = new SignOutResult("", null); - // Act - await result.ExecuteAsync(httpContext); + // Act + await result.ExecuteAsync(httpContext); - // Assert - auth.Verify(); - } + // Assert + auth.Verify(); + } - [Fact] - public async Task ExecuteAsync_InvokesSignOutAsyncOnAllConfiguredSchemes() - { - // Arrange - var authProperties = new AuthenticationProperties(); - var auth = new Mock(); - auth - .Setup(c => c.SignOutAsync(It.IsAny(), "Scheme1", authProperties)) - .Returns(Task.CompletedTask) - .Verifiable(); - auth - .Setup(c => c.SignOutAsync(It.IsAny(), "Scheme2", authProperties)) - .Returns(Task.CompletedTask) - .Verifiable(); - var httpContext = GetHttpContext(auth.Object); - var result = new SignOutResult(new[] { "Scheme1", "Scheme2" }, authProperties); + [Fact] + public async Task ExecuteAsync_InvokesSignOutAsyncOnAllConfiguredSchemes() + { + // Arrange + var authProperties = new AuthenticationProperties(); + var auth = new Mock(); + auth + .Setup(c => c.SignOutAsync(It.IsAny(), "Scheme1", authProperties)) + .Returns(Task.CompletedTask) + .Verifiable(); + auth + .Setup(c => c.SignOutAsync(It.IsAny(), "Scheme2", authProperties)) + .Returns(Task.CompletedTask) + .Verifiable(); + var httpContext = GetHttpContext(auth.Object); + var result = new SignOutResult(new[] { "Scheme1", "Scheme2" }, authProperties); - // Act - await result.ExecuteAsync(httpContext); + // Act + await result.ExecuteAsync(httpContext); - // Assert - auth.Verify(); - } + // Assert + auth.Verify(); + } - private static DefaultHttpContext GetHttpContext(IAuthenticationService auth) - { - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = CreateServices() - .AddSingleton(auth) - .BuildServiceProvider(); - return httpContext; - } + private static DefaultHttpContext GetHttpContext(IAuthenticationService auth) + { + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = CreateServices() + .AddSingleton(auth) + .BuildServiceProvider(); + return httpContext; + } - private static IServiceCollection CreateServices() - { - var services = new ServiceCollection(); - services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); - return services; - } + private static IServiceCollection CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>)); + return services; } } diff --git a/src/Http/Http.Results/test/StatusCodeResultTests.cs b/src/Http/Http.Results/test/StatusCodeResultTests.cs index bc32a64ae4..85dca965ce 100644 --- a/src/Http/Http.Results/test/StatusCodeResultTests.cs +++ b/src/Http/Http.Results/test/StatusCodeResultTests.cs @@ -6,40 +6,39 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class StatusCodeResultTests { - public class StatusCodeResultTests + [Fact] + public void StatusCodeResult_ExecuteResultSetsResponseStatusCode() { - [Fact] - public void StatusCodeResult_ExecuteResultSetsResponseStatusCode() - { - // Arrange - var result = new StatusCodeResult(StatusCodes.Status404NotFound); + // Arrange + var result = new StatusCodeResult(StatusCodes.Status404NotFound); - var httpContext = GetHttpContext(); + var httpContext = GetHttpContext(); - // Act - result.ExecuteAsync(httpContext); + // Act + result.ExecuteAsync(httpContext); - // Assert - Assert.Equal(StatusCodes.Status404NotFound, httpContext.Response.StatusCode); - } + // Assert + Assert.Equal(StatusCodes.Status404NotFound, httpContext.Response.StatusCode); + } - private static IServiceCollection CreateServices() - { - var services = new ServiceCollection(); - services.AddSingleton(NullLoggerFactory.Instance); - return services; - } + private static IServiceCollection CreateServices() + { + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + return services; + } - private static HttpContext GetHttpContext() - { - var services = CreateServices(); + private static HttpContext GetHttpContext() + { + var services = CreateServices(); - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = services.BuildServiceProvider(); + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = services.BuildServiceProvider(); - return httpContext; - } + return httpContext; } } diff --git a/src/Http/Http.Results/test/TestLinkGenerator.cs b/src/Http/Http.Results/test/TestLinkGenerator.cs index ec026e13a7..8f8d9e0167 100644 --- a/src/Http/Http.Results/test/TestLinkGenerator.cs +++ b/src/Http/Http.Results/test/TestLinkGenerator.cs @@ -4,26 +4,25 @@ using System; using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +internal sealed class TestLinkGenerator : LinkGenerator { - internal sealed class TestLinkGenerator : LinkGenerator - { - public string Url { get; set; } + public string Url { get; set; } - public override string GetPathByAddress(HttpContext httpContext, TAddress address, RouteValueDictionary values, RouteValueDictionary ambientValues = null, PathString? pathBase = null, FragmentString fragment = default, LinkOptions options = null) - { - throw new NotImplementedException(); - } + public override string GetPathByAddress(HttpContext httpContext, TAddress address, RouteValueDictionary values, RouteValueDictionary ambientValues = null, PathString? pathBase = null, FragmentString fragment = default, LinkOptions options = null) + { + throw new NotImplementedException(); + } - public override string GetPathByAddress(TAddress address, RouteValueDictionary values, PathString pathBase = default, FragmentString fragment = default, LinkOptions options = null) - { - throw new NotImplementedException(); - } + public override string GetPathByAddress(TAddress address, RouteValueDictionary values, PathString pathBase = default, FragmentString fragment = default, LinkOptions options = null) + { + throw new NotImplementedException(); + } - public override string GetUriByAddress(HttpContext httpContext, TAddress address, RouteValueDictionary values, RouteValueDictionary ambientValues = null, string scheme = null, HostString? host = null, PathString? pathBase = null, FragmentString fragment = default, LinkOptions options = null) - => Url; + public override string GetUriByAddress(HttpContext httpContext, TAddress address, RouteValueDictionary values, RouteValueDictionary ambientValues = null, string scheme = null, HostString? host = null, PathString? pathBase = null, FragmentString fragment = default, LinkOptions options = null) + => Url; - public override string GetUriByAddress(TAddress address, RouteValueDictionary values, string scheme, HostString host, PathString pathBase = default, FragmentString fragment = default, LinkOptions options = null) - => Url; - } + public override string GetUriByAddress(TAddress address, RouteValueDictionary values, string scheme, HostString host, PathString pathBase = default, FragmentString fragment = default, LinkOptions options = null) + => Url; } diff --git a/src/Http/Http.Results/test/UnauthorizedResultTests.cs b/src/Http/Http.Results/test/UnauthorizedResultTests.cs index f4050fe6e4..f6e2640fa7 100644 --- a/src/Http/Http.Results/test/UnauthorizedResultTests.cs +++ b/src/Http/Http.Results/test/UnauthorizedResultTests.cs @@ -3,18 +3,17 @@ using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class UnauthorizedResultTests { - public class UnauthorizedResultTests + [Fact] + public void UnauthorizedResult_InitializesStatusCode() { - [Fact] - public void UnauthorizedResult_InitializesStatusCode() - { - // Arrange & act - var result = new UnauthorizedResult(); + // Arrange & act + var result = new UnauthorizedResult(); - // Assert - Assert.Equal(StatusCodes.Status401Unauthorized, result.StatusCode); - } + // Assert + Assert.Equal(StatusCodes.Status401Unauthorized, result.StatusCode); } } diff --git a/src/Http/Http.Results/test/UnprocessableEntityObjectResultTests.cs b/src/Http/Http.Results/test/UnprocessableEntityObjectResultTests.cs index 1d5b68413a..7c26e98fb5 100644 --- a/src/Http/Http.Results/test/UnprocessableEntityObjectResultTests.cs +++ b/src/Http/Http.Results/test/UnprocessableEntityObjectResultTests.cs @@ -3,20 +3,19 @@ using Xunit; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class UnprocessableEntityObjectResultTests { - public class UnprocessableEntityObjectResultTests + [Fact] + public void UnprocessableEntityObjectResult_SetsStatusCodeAndValue() { - [Fact] - public void UnprocessableEntityObjectResult_SetsStatusCodeAndValue() - { - // Arrange & Act - var obj = new object(); - var result = new UnprocessableEntityObjectResult(obj); + // Arrange & Act + var obj = new object(); + var result = new UnprocessableEntityObjectResult(obj); - // Assert - Assert.Equal(StatusCodes.Status422UnprocessableEntity, result.StatusCode); - Assert.Equal(obj, result.Value); - } + // Assert + Assert.Equal(StatusCodes.Status422UnprocessableEntity, result.StatusCode); + Assert.Equal(obj, result.Value); } } diff --git a/src/Http/Http.Results/test/VirtualFileResultTest.cs b/src/Http/Http.Results/test/VirtualFileResultTest.cs index e38ed10778..76946cdb0a 100644 --- a/src/Http/Http.Results/test/VirtualFileResultTest.cs +++ b/src/Http/Http.Results/test/VirtualFileResultTest.cs @@ -6,20 +6,19 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Internal; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http.Result +namespace Microsoft.AspNetCore.Http.Result; + +public class VirtualFileResultTest : VirtualFileResultTestBase { - public class VirtualFileResultTest : VirtualFileResultTestBase + protected override Task ExecuteAsync(HttpContext httpContext, string path, string contentType, DateTimeOffset? lastModified = null, EntityTagHeaderValue entityTag = null, bool enableRangeProcessing = false) { - protected override Task ExecuteAsync(HttpContext httpContext, string path, string contentType, DateTimeOffset? lastModified = null, EntityTagHeaderValue entityTag = null, bool enableRangeProcessing = false) + var result = new VirtualFileResult(path, contentType) { - var result = new VirtualFileResult(path, contentType) - { - LastModified = lastModified, - EntityTag = entityTag, - EnableRangeProcessing = enableRangeProcessing, - }; + LastModified = lastModified, + EntityTag = entityTag, + EnableRangeProcessing = enableRangeProcessing, + }; - return result.ExecuteAsync(httpContext); - } + return result.ExecuteAsync(httpContext); } } diff --git a/src/Http/Http/perf/Microbenchmarks/AdaptiveCapacityDictionaryBenchmark.cs b/src/Http/Http/perf/Microbenchmarks/AdaptiveCapacityDictionaryBenchmark.cs index 1cf5d3f344..5d9b0ddffe 100644 --- a/src/Http/Http/perf/Microbenchmarks/AdaptiveCapacityDictionaryBenchmark.cs +++ b/src/Http/Http/perf/Microbenchmarks/AdaptiveCapacityDictionaryBenchmark.cs @@ -6,25 +6,25 @@ using System.Collections.Generic; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Internal; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +public class AdaptiveCapacityDictionaryBenchmark { - public class AdaptiveCapacityDictionaryBenchmark - { - private AdaptiveCapacityDictionary _smallCapDict; - private AdaptiveCapacityDictionary _smallCapDictTen; - private AdaptiveCapacityDictionary _filledSmallDictionary; - private Dictionary _dict; - private Dictionary _dictTen; - private Dictionary _filledDictTen; - private KeyValuePair _oneValue; - private List> _tenValues; - - [IterationSetup] - public void Setup() - { - _oneValue = new KeyValuePair("a", "b"); + private AdaptiveCapacityDictionary _smallCapDict; + private AdaptiveCapacityDictionary _smallCapDictTen; + private AdaptiveCapacityDictionary _filledSmallDictionary; + private Dictionary _dict; + private Dictionary _dictTen; + private Dictionary _filledDictTen; + private KeyValuePair _oneValue; + private List> _tenValues; + + [IterationSetup] + public void Setup() + { + _oneValue = new KeyValuePair("a", "b"); - _tenValues = new List>() + _tenValues = new List>() { new KeyValuePair("a", "b"), new KeyValuePair("c", "d"), @@ -38,298 +38,297 @@ namespace Microsoft.AspNetCore.Http new KeyValuePair("s", "t"), }; - _smallCapDict = new AdaptiveCapacityDictionary(capacity: 1, StringComparer.OrdinalIgnoreCase); - _smallCapDictTen = new AdaptiveCapacityDictionary(capacity: 10, StringComparer.OrdinalIgnoreCase); - _filledSmallDictionary = new AdaptiveCapacityDictionary(capacity: 10, StringComparer.OrdinalIgnoreCase); - foreach (var a in _tenValues) - { - _filledSmallDictionary[a.Key] = a.Value; - } - - _dict = new Dictionary(1, StringComparer.OrdinalIgnoreCase); - _dictTen = new Dictionary(10, StringComparer.OrdinalIgnoreCase); - _filledDictTen = new Dictionary(10, StringComparer.OrdinalIgnoreCase); - - foreach (var a in _tenValues) - { - _filledDictTen[a.Key] = a.Value; - } - } - - [Benchmark] - public void OneValue_SmallDict() + _smallCapDict = new AdaptiveCapacityDictionary(capacity: 1, StringComparer.OrdinalIgnoreCase); + _smallCapDictTen = new AdaptiveCapacityDictionary(capacity: 10, StringComparer.OrdinalIgnoreCase); + _filledSmallDictionary = new AdaptiveCapacityDictionary(capacity: 10, StringComparer.OrdinalIgnoreCase); + foreach (var a in _tenValues) { - _smallCapDict[_oneValue.Key] = _oneValue.Value; - _ = _smallCapDict[_oneValue.Key]; + _filledSmallDictionary[a.Key] = a.Value; } - [Benchmark] - public void OneValue_Dict() - { - _dict[_oneValue.Key] = _oneValue.Value; - _ = _dict[_oneValue.Key]; - } + _dict = new Dictionary(1, StringComparer.OrdinalIgnoreCase); + _dictTen = new Dictionary(10, StringComparer.OrdinalIgnoreCase); + _filledDictTen = new Dictionary(10, StringComparer.OrdinalIgnoreCase); - [Benchmark] - public void OneValue_SmallDict_Set() + foreach (var a in _tenValues) { - _smallCapDict[_oneValue.Key] = _oneValue.Value; + _filledDictTen[a.Key] = a.Value; } + } - [Benchmark] - public void OneValue_Dict_Set() - { - _dict[_oneValue.Key] = _oneValue.Value; - } + [Benchmark] + public void OneValue_SmallDict() + { + _smallCapDict[_oneValue.Key] = _oneValue.Value; + _ = _smallCapDict[_oneValue.Key]; + } + [Benchmark] + public void OneValue_Dict() + { + _dict[_oneValue.Key] = _oneValue.Value; + _ = _dict[_oneValue.Key]; + } - [Benchmark] - public void OneValue_SmallDict_Get() - { - _smallCapDict.TryGetValue("test", out var val); - } + [Benchmark] + public void OneValue_SmallDict_Set() + { + _smallCapDict[_oneValue.Key] = _oneValue.Value; + } - [Benchmark] - public void OneValue_Dict_Get() - { - _dict.TryGetValue("test", out var val); - } + [Benchmark] + public void OneValue_Dict_Set() + { + _dict[_oneValue.Key] = _oneValue.Value; + } + + + [Benchmark] + public void OneValue_SmallDict_Get() + { + _smallCapDict.TryGetValue("test", out var val); + } + + [Benchmark] + public void OneValue_Dict_Get() + { + _dict.TryGetValue("test", out var val); + } - [Benchmark] - public void FourValues_SmallDict() + [Benchmark] + public void FourValues_SmallDict() + { + for (var i = 0; i < 4; i++) { - for (var i = 0; i < 4; i++) - { - var val = _tenValues[i]; - _smallCapDictTen[val.Key] = val.Value; - _ = _smallCapDictTen[val.Key]; - } + var val = _tenValues[i]; + _smallCapDictTen[val.Key] = val.Value; + _ = _smallCapDictTen[val.Key]; } + } - [Benchmark] - public void FiveValues_SmallDict() + [Benchmark] + public void FiveValues_SmallDict() + { + for (var i = 0; i < 5; i++) { - for (var i = 0; i < 5; i++) - { - var val = _tenValues[i]; - _smallCapDictTen[val.Key] = val.Value; - _ = _smallCapDictTen[val.Key]; - } + var val = _tenValues[i]; + _smallCapDictTen[val.Key] = val.Value; + _ = _smallCapDictTen[val.Key]; } + } - [Benchmark] - public void SixValues_SmallDict() + [Benchmark] + public void SixValues_SmallDict() + { + for (var i = 0; i < 6; i++) { - for (var i = 0; i < 6; i++) - { - var val = _tenValues[i]; - _smallCapDictTen[val.Key] = val.Value; - _ = _smallCapDictTen[val.Key]; - } + var val = _tenValues[i]; + _smallCapDictTen[val.Key] = val.Value; + _ = _smallCapDictTen[val.Key]; } + } - [Benchmark] - public void SevenValues_SmallDict() + [Benchmark] + public void SevenValues_SmallDict() + { + for (var i = 0; i < 7; i++) { - for (var i = 0; i < 7; i++) - { - var val = _tenValues[i]; - _smallCapDictTen[val.Key] = val.Value; - _ = _smallCapDictTen[val.Key]; - } + var val = _tenValues[i]; + _smallCapDictTen[val.Key] = val.Value; + _ = _smallCapDictTen[val.Key]; } + } - [Benchmark] - public void EightValues_SmallDict() + [Benchmark] + public void EightValues_SmallDict() + { + for (var i = 0; i < 8; i++) { - for (var i = 0; i < 8; i++) - { - var val = _tenValues[i]; - _smallCapDictTen[val.Key] = val.Value; - _ = _smallCapDictTen[val.Key]; - } + var val = _tenValues[i]; + _smallCapDictTen[val.Key] = val.Value; + _ = _smallCapDictTen[val.Key]; } + } - [Benchmark] - public void NineValues_SmallDict() + [Benchmark] + public void NineValues_SmallDict() + { + for (var i = 0; i < 9; i++) { - for (var i = 0; i < 9; i++) - { - var val = _tenValues[i]; - _smallCapDictTen[val.Key] = val.Value; - _ = _smallCapDictTen[val.Key]; - } + var val = _tenValues[i]; + _smallCapDictTen[val.Key] = val.Value; + _ = _smallCapDictTen[val.Key]; } + } - [Benchmark] - public void TenValues_SmallDict() + [Benchmark] + public void TenValues_SmallDict() + { + for (var i = 0; i < 10; i++) { - for (var i = 0; i < 10; i++) - { - var val = _tenValues[i]; - _smallCapDictTen[val.Key] = val.Value; - _ = _smallCapDictTen[val.Key]; - } + var val = _tenValues[i]; + _smallCapDictTen[val.Key] = val.Value; + _ = _smallCapDictTen[val.Key]; } + } - [Benchmark] - public void FourValues_Dict() + [Benchmark] + public void FourValues_Dict() + { + for (var i = 0; i < 4; i++) { - for (var i = 0; i < 4; i++) - { - var val = _tenValues[i]; - _dictTen[val.Key] = val.Value; - _ = _dictTen[val.Key]; - } + var val = _tenValues[i]; + _dictTen[val.Key] = val.Value; + _ = _dictTen[val.Key]; } + } - [Benchmark] - public void FiveValues_Dict() + [Benchmark] + public void FiveValues_Dict() + { + for (var i = 0; i < 5; i++) { - for (var i = 0; i < 5; i++) - { - var val = _tenValues[i]; - _dictTen[val.Key] = val.Value; - _ = _dictTen[val.Key]; - } + var val = _tenValues[i]; + _dictTen[val.Key] = val.Value; + _ = _dictTen[val.Key]; } - [Benchmark] - public void SixValues_Dict() + } + [Benchmark] + public void SixValues_Dict() + { + for (var i = 0; i < 6; i++) { - for (var i = 0; i < 6; i++) - { - var val = _tenValues[i]; - _dictTen[val.Key] = val.Value; - _ = _dictTen[val.Key]; - } + var val = _tenValues[i]; + _dictTen[val.Key] = val.Value; + _ = _dictTen[val.Key]; } - [Benchmark] - public void SevenValues_Dict() + } + [Benchmark] + public void SevenValues_Dict() + { + for (var i = 0; i < 7; i++) { - for (var i = 0; i < 7; i++) - { - var val = _tenValues[i]; - _dictTen[val.Key] = val.Value; - _ = _dictTen[val.Key]; - } + var val = _tenValues[i]; + _dictTen[val.Key] = val.Value; + _ = _dictTen[val.Key]; } - [Benchmark] - public void EightValues_Dict() + } + [Benchmark] + public void EightValues_Dict() + { + for (var i = 0; i < 8; i++) { - for (var i = 0; i < 8; i++) - { - var val = _tenValues[i]; - _dictTen[val.Key] = val.Value; - _ = _dictTen[val.Key]; - } + var val = _tenValues[i]; + _dictTen[val.Key] = val.Value; + _ = _dictTen[val.Key]; } - [Benchmark] - public void NineValues_Dict() + } + [Benchmark] + public void NineValues_Dict() + { + for (var i = 0; i < 9; i++) { - for (var i = 0; i < 9; i++) - { - var val = _tenValues[i]; - _dictTen[val.Key] = val.Value; - _ = _dictTen[val.Key]; - } + var val = _tenValues[i]; + _dictTen[val.Key] = val.Value; + _ = _dictTen[val.Key]; } + } - [Benchmark] - public void TenValues_Dict() + [Benchmark] + public void TenValues_Dict() + { + for (var i = 0; i < 10; i++) { - for (var i = 0; i < 10; i++) - { - var val = _tenValues[i]; - _dictTen[val.Key] = val.Value; - _ = _dictTen[val.Key]; - } + var val = _tenValues[i]; + _dictTen[val.Key] = val.Value; + _ = _dictTen[val.Key]; } + } - [Benchmark] - public void FourValues_SmallDictGet() - { - _ = _filledSmallDictionary["g"]; - } + [Benchmark] + public void FourValues_SmallDictGet() + { + _ = _filledSmallDictionary["g"]; + } - [Benchmark] - public void FiveValues_SmallDictGet() - { - _ = _filledSmallDictionary["i"]; - } + [Benchmark] + public void FiveValues_SmallDictGet() + { + _ = _filledSmallDictionary["i"]; + } - [Benchmark] - public void SixValues_SmallDictGetGet() - { - _ = _filledSmallDictionary["k"]; + [Benchmark] + public void SixValues_SmallDictGetGet() + { + _ = _filledSmallDictionary["k"]; - } + } - [Benchmark] - public void SevenValues_SmallDictGetGet() - { - _ = _filledSmallDictionary["m"]; - } + [Benchmark] + public void SevenValues_SmallDictGetGet() + { + _ = _filledSmallDictionary["m"]; + } - [Benchmark] - public void EightValues_SmallDictGet() - { - _ = _filledSmallDictionary["o"]; - } + [Benchmark] + public void EightValues_SmallDictGet() + { + _ = _filledSmallDictionary["o"]; + } - [Benchmark] - public void NineValues_SmallDictGet() - { - _ = _filledSmallDictionary["q"]; - } + [Benchmark] + public void NineValues_SmallDictGet() + { + _ = _filledSmallDictionary["q"]; + } - [Benchmark] - public void TenValues_SmallDictGet() - { - _ = _filledSmallDictionary["s"]; - } + [Benchmark] + public void TenValues_SmallDictGet() + { + _ = _filledSmallDictionary["s"]; + } - [Benchmark] - public void TenValues_DictGet() - { - _ = _filledDictTen["s"]; - } + [Benchmark] + public void TenValues_DictGet() + { + _ = _filledDictTen["s"]; + } - [Benchmark] - public void SmallDict() - { - _ = new AdaptiveCapacityDictionary(capacity: 1); - } + [Benchmark] + public void SmallDict() + { + _ = new AdaptiveCapacityDictionary(capacity: 1); + } - [Benchmark] - public void Dict() - { - _ = new Dictionary(capacity: 1); - } + [Benchmark] + public void Dict() + { + _ = new Dictionary(capacity: 1); + } - [Benchmark] - public void SmallDictFour() - { - _ = new AdaptiveCapacityDictionary(capacity: 4); - } + [Benchmark] + public void SmallDictFour() + { + _ = new AdaptiveCapacityDictionary(capacity: 4); + } - [Benchmark] - public void DictFour() - { - _ = new Dictionary(capacity: 4); - } + [Benchmark] + public void DictFour() + { + _ = new Dictionary(capacity: 4); + } - [Benchmark] - public void SmallDictTen() - { - _ = new AdaptiveCapacityDictionary(capacity: 10); - } + [Benchmark] + public void SmallDictTen() + { + _ = new AdaptiveCapacityDictionary(capacity: 10); + } - [Benchmark] - public void DictTen() - { - _ = new Dictionary(capacity: 10); - } + [Benchmark] + public void DictTen() + { + _ = new Dictionary(capacity: 10); } } diff --git a/src/Http/Http/perf/Microbenchmarks/HeaderUtilitiesBenchmark.cs b/src/Http/Http/perf/Microbenchmarks/HeaderUtilitiesBenchmark.cs index 05b3a8a46e..e1597ef619 100644 --- a/src/Http/Http/perf/Microbenchmarks/HeaderUtilitiesBenchmark.cs +++ b/src/Http/Http/perf/Microbenchmarks/HeaderUtilitiesBenchmark.cs @@ -5,20 +5,19 @@ using System; using BenchmarkDotNet.Attributes; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +public class HeaderUtilitiesBenchmark { - public class HeaderUtilitiesBenchmark + [Benchmark] + public StringSegment UnescapeAsQuotedString() { - [Benchmark] - public StringSegment UnescapeAsQuotedString() - { - return HeaderUtilities.UnescapeAsQuotedString("\"hello\\\"foo\\\\bar\\\\baz\\\\\""); - } + return HeaderUtilities.UnescapeAsQuotedString("\"hello\\\"foo\\\\bar\\\\baz\\\\\""); + } - [Benchmark] - public StringSegment EscapeAsQuotedString() - { - return HeaderUtilities.EscapeAsQuotedString("\"hello\\\"foo\\\\bar\\\\baz\\\\\""); - } + [Benchmark] + public StringSegment EscapeAsQuotedString() + { + return HeaderUtilities.EscapeAsQuotedString("\"hello\\\"foo\\\\bar\\\\baz\\\\\""); } } diff --git a/src/Http/Http/perf/Microbenchmarks/QueryCollectionBenchmarks.cs b/src/Http/Http/perf/Microbenchmarks/QueryCollectionBenchmarks.cs index c7368a2c88..6d5c8a93f6 100644 --- a/src/Http/Http/perf/Microbenchmarks/QueryCollectionBenchmarks.cs +++ b/src/Http/Http/perf/Microbenchmarks/QueryCollectionBenchmarks.cs @@ -7,91 +7,90 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.WebUtilities; using static Microsoft.AspNetCore.Http.Features.QueryFeature; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class QueryCollectionBenchmarks { - [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] - [CategoriesColumn] - public class QueryCollectionBenchmarks - { - private string _queryString; - private string _singleValue; - private string _singleValueWithPlus; - private string _encoded; + private string _queryString; + private string _singleValue; + private string _singleValueWithPlus; + private string _encoded; - [IterationSetup] - public void Setup() - { - _queryString = "?key1=value1&key2=value2&key3=value3&key4=&key5="; - _singleValue = "?key1=value1"; - _singleValueWithPlus = "?key1=value1+value2+value3"; - _encoded = "?key1=value%231"; - } + [IterationSetup] + public void Setup() + { + _queryString = "?key1=value1&key2=value2&key3=value3&key4=&key5="; + _singleValue = "?key1=value1"; + _singleValueWithPlus = "?key1=value1+value2+value3"; + _encoded = "?key1=value%231"; + } - [Benchmark(Description = "ParseNew")] - [BenchmarkCategory("QueryString")] - public void ParseNew() - { - _ = QueryFeature.ParseNullableQueryInternal(_queryString); - } + [Benchmark(Description = "ParseNew")] + [BenchmarkCategory("QueryString")] + public void ParseNew() + { + _ = QueryFeature.ParseNullableQueryInternal(_queryString); + } - [Benchmark(Description = "ParseNew")] - [BenchmarkCategory("Single")] - public void ParseNewSingle() - { - _ = QueryFeature.ParseNullableQueryInternal(_singleValue); - } + [Benchmark(Description = "ParseNew")] + [BenchmarkCategory("Single")] + public void ParseNewSingle() + { + _ = QueryFeature.ParseNullableQueryInternal(_singleValue); + } - [Benchmark(Description = "ParseNew")] - [BenchmarkCategory("SingleWithPlus")] - public void ParseNewSingleWithPlus() - { - _ = QueryFeature.ParseNullableQueryInternal(_singleValueWithPlus); - } + [Benchmark(Description = "ParseNew")] + [BenchmarkCategory("SingleWithPlus")] + public void ParseNewSingleWithPlus() + { + _ = QueryFeature.ParseNullableQueryInternal(_singleValueWithPlus); + } - [Benchmark(Description = "ParseNew")] - [BenchmarkCategory("Encoded")] - public void ParseNewEncoded() - { - _ = QueryFeature.ParseNullableQueryInternal(_encoded); - } + [Benchmark(Description = "ParseNew")] + [BenchmarkCategory("Encoded")] + public void ParseNewEncoded() + { + _ = QueryFeature.ParseNullableQueryInternal(_encoded); + } - [Benchmark(Description = "QueryHelpersParse")] - [BenchmarkCategory("QueryString")] - public void QueryHelpersParse() - { - _ = QueryHelpers.ParseNullableQuery(_queryString); - } + [Benchmark(Description = "QueryHelpersParse")] + [BenchmarkCategory("QueryString")] + public void QueryHelpersParse() + { + _ = QueryHelpers.ParseNullableQuery(_queryString); + } - [Benchmark(Description = "QueryHelpersParse")] - [BenchmarkCategory("Single")] - public void QueryHelpersParseSingle() - { - _ = QueryHelpers.ParseNullableQuery(_singleValue); - } + [Benchmark(Description = "QueryHelpersParse")] + [BenchmarkCategory("Single")] + public void QueryHelpersParseSingle() + { + _ = QueryHelpers.ParseNullableQuery(_singleValue); + } - [Benchmark(Description = "QueryHelpersParse")] - [BenchmarkCategory("SingleWithPlus")] - public void QueryHelpersParseSingleWithPlus() - { - _ = QueryHelpers.ParseNullableQuery(_singleValueWithPlus); - } + [Benchmark(Description = "QueryHelpersParse")] + [BenchmarkCategory("SingleWithPlus")] + public void QueryHelpersParseSingleWithPlus() + { + _ = QueryHelpers.ParseNullableQuery(_singleValueWithPlus); + } - [Benchmark(Description = "QueryHelpersParse")] - [BenchmarkCategory("Encoded")] - public void QueryHelpersParseEncoded() - { - _ = QueryHelpers.ParseNullableQuery(_encoded); - } + [Benchmark(Description = "QueryHelpersParse")] + [BenchmarkCategory("Encoded")] + public void QueryHelpersParseEncoded() + { + _ = QueryHelpers.ParseNullableQuery(_encoded); + } - [Benchmark] - [BenchmarkCategory("Constructor")] - public void Constructor() + [Benchmark] + [BenchmarkCategory("Constructor")] + public void Constructor() + { + var dict = new KvpAccumulator(); + if (dict.HasValues) { - var dict = new KvpAccumulator(); - if (dict.HasValues) - { - return; - } + return; } } } diff --git a/src/Http/Http/perf/Microbenchmarks/RequestCookieCollectionBenchmarks.cs b/src/Http/Http/perf/Microbenchmarks/RequestCookieCollectionBenchmarks.cs index 219a077886..b68a170809 100644 --- a/src/Http/Http/perf/Microbenchmarks/RequestCookieCollectionBenchmarks.cs +++ b/src/Http/Http/perf/Microbenchmarks/RequestCookieCollectionBenchmarks.cs @@ -4,22 +4,21 @@ using BenchmarkDotNet.Attributes; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +public class RequestCookieCollectionBenchmarks { - public class RequestCookieCollectionBenchmarks - { - private StringValues _cookie; + private StringValues _cookie; - [IterationSetup] - public void Setup() - { - _cookie = ".AspNetCore.Cookies=CfDJ8BAklVa9EYREk8_ipRUUYJYhRsleKr485k18s_q5XD6vcRJ-DtowUuLCwwMiY728zRZ3rVFY3DEcXDAQUOTtg1e4tkSIVmYLX38Q6mqdFFyw-8dksclDywe9vnN84cEWvfV0wP3EgOsJGHaND7kTJ47gr7Pc1tLHWOm4Pe7Q1vrT9EkcTMr1Wts3aptBl3bdOLLqjmSdgk-OI7qG7uQGz1OGdnSer6-KLUPBcfXblzs4YCjvwu3bGnM42xLGtkZNIF8izPpyqKkIf7ec6O6LEHMp4gcq86PGHCXHn5NKuNSD"; - } + [IterationSetup] + public void Setup() + { + _cookie = ".AspNetCore.Cookies=CfDJ8BAklVa9EYREk8_ipRUUYJYhRsleKr485k18s_q5XD6vcRJ-DtowUuLCwwMiY728zRZ3rVFY3DEcXDAQUOTtg1e4tkSIVmYLX38Q6mqdFFyw-8dksclDywe9vnN84cEWvfV0wP3EgOsJGHaND7kTJ47gr7Pc1tLHWOm4Pe7Q1vrT9EkcTMr1Wts3aptBl3bdOLLqjmSdgk-OI7qG7uQGz1OGdnSer6-KLUPBcfXblzs4YCjvwu3bGnM42xLGtkZNIF8izPpyqKkIf7ec6O6LEHMp4gcq86PGHCXHn5NKuNSD"; + } - [Benchmark] - public void Parse_TypicalCookie() - { - _ = RequestCookieCollection.Parse(_cookie); - } + [Benchmark] + public void Parse_TypicalCookie() + { + _ = RequestCookieCollection.Parse(_cookie); } } diff --git a/src/Http/Http/perf/Microbenchmarks/RouteValueDictionaryBenchmark.cs b/src/Http/Http/perf/Microbenchmarks/RouteValueDictionaryBenchmark.cs index 2ea18befda..1544409586 100644 --- a/src/Http/Http/perf/Microbenchmarks/RouteValueDictionaryBenchmark.cs +++ b/src/Http/Http/perf/Microbenchmarks/RouteValueDictionaryBenchmark.cs @@ -4,322 +4,321 @@ using System; using BenchmarkDotNet.Attributes; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class RouteValueDictionaryBenchmark { - public class RouteValueDictionaryBenchmark - { - private RouteValueDictionary _arrayValues; - private RouteValueDictionary _propertyValues; - private RouteValueDictionary _arrayValuesEmpty; + private RouteValueDictionary _arrayValues; + private RouteValueDictionary _propertyValues; + private RouteValueDictionary _arrayValuesEmpty; - // We modify the route value dictionaries in many of these benchmarks. - [IterationSetup] - public void Setup() - { - _arrayValues = new RouteValueDictionary() + // We modify the route value dictionaries in many of these benchmarks. + [IterationSetup] + public void Setup() + { + _arrayValues = new RouteValueDictionary() { { "action", "Index" }, { "controller", "Home" }, { "id", "17" }, }; - _arrayValuesEmpty = new RouteValueDictionary(); - _propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); - } + _arrayValuesEmpty = new RouteValueDictionary(); + _propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); + } - [Benchmark] - public void Ctor_Values_RouteValueDictionary_EmptyArray() - { - new RouteValueDictionary(_arrayValuesEmpty); - } + [Benchmark] + public void Ctor_Values_RouteValueDictionary_EmptyArray() + { + new RouteValueDictionary(_arrayValuesEmpty); + } - [Benchmark] - public RouteValueDictionary Ctor_Values_RouteValueDictionary_Array() - { - return new RouteValueDictionary(_arrayValues); - } + [Benchmark] + public RouteValueDictionary Ctor_Values_RouteValueDictionary_Array() + { + return new RouteValueDictionary(_arrayValues); + } - [Benchmark] - public RouteValueDictionary AddSingleItem() - { - var dictionary = new RouteValueDictionary + [Benchmark] + public RouteValueDictionary AddSingleItem() + { + var dictionary = new RouteValueDictionary { { "action", "Index" } }; - return dictionary; - } + return dictionary; + } - [Benchmark] - public RouteValueDictionary AddThreeItems() - { - var dictionary = new RouteValueDictionary + [Benchmark] + public RouteValueDictionary AddThreeItems() + { + var dictionary = new RouteValueDictionary { { "action", "Index" }, { "controller", "Home" }, { "id", "15" } }; - return dictionary; - } + return dictionary; + } - [Benchmark] - public void ContainsKey_Array_Found() - { - _arrayValues.ContainsKey("id"); - } + [Benchmark] + public void ContainsKey_Array_Found() + { + _arrayValues.ContainsKey("id"); + } - [Benchmark] - public void ContainsKey_Array_NotFound() - { - _arrayValues.ContainsKey("name"); - } + [Benchmark] + public void ContainsKey_Array_NotFound() + { + _arrayValues.ContainsKey("name"); + } - [Benchmark] - public void ContainsKey_Properties_Found() - { - _propertyValues.ContainsKey("id"); - } + [Benchmark] + public void ContainsKey_Properties_Found() + { + _propertyValues.ContainsKey("id"); + } - [Benchmark] - public void ContainsKey_Properties_NotFound() - { - _propertyValues.ContainsKey("name"); - } + [Benchmark] + public void ContainsKey_Properties_NotFound() + { + _propertyValues.ContainsKey("name"); + } - [Benchmark] - public void TryAdd_Properties_AtCapacity_KeyExists() - { - var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17", area = "root" }); - propertyValues.TryAdd("id", "15"); - } + [Benchmark] + public void TryAdd_Properties_AtCapacity_KeyExists() + { + var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17", area = "root" }); + propertyValues.TryAdd("id", "15"); + } - [Benchmark] - public void TryAdd_Properties_AtCapacity_KeyDoesNotExist() - { - var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17", area = "root" }); - _propertyValues.TryAdd("name", "Service"); - } + [Benchmark] + public void TryAdd_Properties_AtCapacity_KeyDoesNotExist() + { + var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17", area = "root" }); + _propertyValues.TryAdd("name", "Service"); + } - [Benchmark] - public void TryAdd_Properties_NotAtCapacity_KeyExists() - { - var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); - propertyValues.TryAdd("id", "15"); - } + [Benchmark] + public void TryAdd_Properties_NotAtCapacity_KeyExists() + { + var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); + propertyValues.TryAdd("id", "15"); + } - [Benchmark] - public void TryAdd_Properties_NotAtCapacity_KeyDoesNotExist() - { - var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); - _propertyValues.TryAdd("name", "Service"); - } + [Benchmark] + public void TryAdd_Properties_NotAtCapacity_KeyDoesNotExist() + { + var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); + _propertyValues.TryAdd("name", "Service"); + } - [Benchmark] - public void TryAdd_Array_AtCapacity_KeyExists() - { - var arrayValues = new RouteValueDictionary + [Benchmark] + public void TryAdd_Array_AtCapacity_KeyExists() + { + var arrayValues = new RouteValueDictionary { { "action", "Index" }, { "controller", "Home" }, { "id", "17" }, { "area", "root" } }; - arrayValues.TryAdd("id", "15"); - } + arrayValues.TryAdd("id", "15"); + } - [Benchmark] - public void TryAdd_Array_AtCapacity_KeyDoesNotExist() - { - var arrayValues = new RouteValueDictionary + [Benchmark] + public void TryAdd_Array_AtCapacity_KeyDoesNotExist() + { + var arrayValues = new RouteValueDictionary { { "action", "Index" }, { "controller", "Home" }, { "id", "17" }, { "area", "root" } }; - arrayValues.TryAdd("name", "Service"); - } + arrayValues.TryAdd("name", "Service"); + } - [Benchmark] - public void TryAdd_Array_NotAtCapacity_KeyExists() - { - var arrayValues = new RouteValueDictionary + [Benchmark] + public void TryAdd_Array_NotAtCapacity_KeyExists() + { + var arrayValues = new RouteValueDictionary { { "action", "Index" }, { "controller", "Home" }, { "id", "17" } }; - arrayValues.TryAdd("id", "15"); - } + arrayValues.TryAdd("id", "15"); + } - [Benchmark] - public void TryAdd_Array_NotAtCapacity_KeyDoesNotExist() - { - var arrayValues = new RouteValueDictionary + [Benchmark] + public void TryAdd_Array_NotAtCapacity_KeyDoesNotExist() + { + var arrayValues = new RouteValueDictionary { { "action", "Index" }, { "controller", "Home" }, { "id", "17" }, }; - arrayValues.TryAdd("name", "Service"); - } + arrayValues.TryAdd("name", "Service"); + } - [Benchmark] - public void ConditionalAdd_Array() - { - var arrayValues = new RouteValueDictionary() + [Benchmark] + public void ConditionalAdd_Array() + { + var arrayValues = new RouteValueDictionary() { { "action", "Index" }, { "controller", "Home" }, { "id", "17" }, }; - if (!arrayValues.ContainsKey("name")) - { - arrayValues.Add("name", "Service"); - } - } - - [Benchmark] - public void ConditionalAdd_Properties() + if (!arrayValues.ContainsKey("name")) { - var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); - - if (!propertyValues.ContainsKey("name")) - { - propertyValues.Add("name", "Service"); - } + arrayValues.Add("name", "Service"); } + } - [Benchmark] - public RouteValueDictionary ConditionalAdd_ContainsKey_Array() - { - var dictionary = _arrayValues; - - if (!dictionary.ContainsKey("action")) - { - dictionary.Add("action", "Index"); - } - - if (!dictionary.ContainsKey("controller")) - { - dictionary.Add("controller", "Home"); - } - - if (!dictionary.ContainsKey("area")) - { - dictionary.Add("area", "Admin"); - } - - return dictionary; - } + [Benchmark] + public void ConditionalAdd_Properties() + { + var propertyValues = new RouteValueDictionary(new { action = "Index", controller = "Home", id = "17" }); - [Benchmark] - public RouteValueDictionary ConditionalAdd_TryAdd() + if (!propertyValues.ContainsKey("name")) { - var dictionary = _arrayValues; - - dictionary.TryAdd("action", "Index"); - dictionary.TryAdd("controller", "Home"); - dictionary.TryAdd("area", "Admin"); - - return dictionary; + propertyValues.Add("name", "Service"); } + } - [Benchmark] - public RouteValueDictionary ForEachThreeItems_Array() - { - var dictionary = _arrayValues; - foreach (var kvp in dictionary) - { - GC.KeepAlive(kvp.Value); - } - return dictionary; - } + [Benchmark] + public RouteValueDictionary ConditionalAdd_ContainsKey_Array() + { + var dictionary = _arrayValues; - [Benchmark] - public RouteValueDictionary ForEachThreeItems_Properties() + if (!dictionary.ContainsKey("action")) { - var dictionary = _propertyValues; - foreach (var kvp in dictionary) - { - GC.KeepAlive(kvp.Value); - } - return dictionary; + dictionary.Add("action", "Index"); } - [Benchmark] - public RouteValueDictionary GetThreeItems_Array() + if (!dictionary.ContainsKey("controller")) { - var dictionary = _arrayValues; - GC.KeepAlive(dictionary["action"]); - GC.KeepAlive(dictionary["controller"]); - GC.KeepAlive(dictionary["id"]); - return dictionary; + dictionary.Add("controller", "Home"); } - [Benchmark] - public RouteValueDictionary GetThreeItems_Properties() + if (!dictionary.ContainsKey("area")) { - var dictionary = _propertyValues; - GC.KeepAlive(dictionary["action"]); - GC.KeepAlive(dictionary["controller"]); - GC.KeepAlive(dictionary["id"]); - return dictionary; + dictionary.Add("area", "Admin"); } - [Benchmark] - public RouteValueDictionary SetSingleItem() - { - var dictionary = new RouteValueDictionary - { - ["action"] = "Index" - }; - return dictionary; - } + return dictionary; + } + + [Benchmark] + public RouteValueDictionary ConditionalAdd_TryAdd() + { + var dictionary = _arrayValues; + + dictionary.TryAdd("action", "Index"); + dictionary.TryAdd("controller", "Home"); + dictionary.TryAdd("area", "Admin"); - [Benchmark] - public RouteValueDictionary SetExistingItem() + return dictionary; + } + + [Benchmark] + public RouteValueDictionary ForEachThreeItems_Array() + { + var dictionary = _arrayValues; + foreach (var kvp in dictionary) { - var dictionary = _arrayValues; - dictionary["action"] = "About"; - return dictionary; + GC.KeepAlive(kvp.Value); } + return dictionary; + } - [Benchmark] - public RouteValueDictionary SetThreeItems() + [Benchmark] + public RouteValueDictionary ForEachThreeItems_Properties() + { + var dictionary = _propertyValues; + foreach (var kvp in dictionary) { - var dictionary = new RouteValueDictionary - { - ["action"] = "Index", - ["controller"] = "Home", - ["id"] = "15" - }; - return dictionary; + GC.KeepAlive(kvp.Value); } + return dictionary; + } - [Benchmark] - public RouteValueDictionary TryGetValueThreeItems_Array() + [Benchmark] + public RouteValueDictionary GetThreeItems_Array() + { + var dictionary = _arrayValues; + GC.KeepAlive(dictionary["action"]); + GC.KeepAlive(dictionary["controller"]); + GC.KeepAlive(dictionary["id"]); + return dictionary; + } + + [Benchmark] + public RouteValueDictionary GetThreeItems_Properties() + { + var dictionary = _propertyValues; + GC.KeepAlive(dictionary["action"]); + GC.KeepAlive(dictionary["controller"]); + GC.KeepAlive(dictionary["id"]); + return dictionary; + } + + [Benchmark] + public RouteValueDictionary SetSingleItem() + { + var dictionary = new RouteValueDictionary { - var dictionary = _arrayValues; - dictionary.TryGetValue("action", out var action); - dictionary.TryGetValue("controller", out var controller); - dictionary.TryGetValue("id", out var id); - GC.KeepAlive(action); - GC.KeepAlive(controller); - GC.KeepAlive(id); - return dictionary; - } + ["action"] = "Index" + }; + return dictionary; + } + + [Benchmark] + public RouteValueDictionary SetExistingItem() + { + var dictionary = _arrayValues; + dictionary["action"] = "About"; + return dictionary; + } - [Benchmark] - public RouteValueDictionary TryGetValueThreeItems_Properties() + [Benchmark] + public RouteValueDictionary SetThreeItems() + { + var dictionary = new RouteValueDictionary { - var dictionary = _propertyValues; - dictionary.TryGetValue("action", out var action); - dictionary.TryGetValue("controller", out var controller); - dictionary.TryGetValue("id", out var id); - GC.KeepAlive(action); - GC.KeepAlive(controller); - GC.KeepAlive(id); - return dictionary; - } + ["action"] = "Index", + ["controller"] = "Home", + ["id"] = "15" + }; + return dictionary; + } + + [Benchmark] + public RouteValueDictionary TryGetValueThreeItems_Array() + { + var dictionary = _arrayValues; + dictionary.TryGetValue("action", out var action); + dictionary.TryGetValue("controller", out var controller); + dictionary.TryGetValue("id", out var id); + GC.KeepAlive(action); + GC.KeepAlive(controller); + GC.KeepAlive(id); + return dictionary; + } + + [Benchmark] + public RouteValueDictionary TryGetValueThreeItems_Properties() + { + var dictionary = _propertyValues; + dictionary.TryGetValue("action", out var action); + dictionary.TryGetValue("controller", out var controller); + dictionary.TryGetValue("id", out var id); + GC.KeepAlive(action); + GC.KeepAlive(controller); + GC.KeepAlive(id); + return dictionary; } } diff --git a/src/Http/Http/src/BindingAddress.cs b/src/Http/Http/src/BindingAddress.cs index 6a5a135d6a..ff61870954 100644 --- a/src/Http/Http/src/BindingAddress.cs +++ b/src/Http/Http/src/BindingAddress.cs @@ -5,229 +5,228 @@ using System; using System.Globalization; using System.IO; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// An address that a HTTP server may bind to. +/// +public class BindingAddress { + private const string UnixPipeHostPrefix = "unix:/"; + + private BindingAddress(string host, string pathBase, int port, string scheme) + { + Host = host; + PathBase = pathBase; + Port = port; + Scheme = scheme; + } + /// - /// An address that a HTTP server may bind to. + /// Initializes a new instance of . /// - public class BindingAddress + [Obsolete("This constructor is obsolete and will be removed in a future version. Use BindingAddress.Parse(address) to create a BindingAddress instance.")] + public BindingAddress() { - private const string UnixPipeHostPrefix = "unix:/"; + throw new InvalidOperationException("This constructor is obsolete and will be removed in a future version. Use BindingAddress.Parse(address) to create a BindingAddress instance."); + } - private BindingAddress(string host, string pathBase, int port, string scheme) - { - Host = host; - PathBase = pathBase; - Port = port; - Scheme = scheme; - } + /// + /// Gets the host component. + /// + public string Host { get; } - /// - /// Initializes a new instance of . - /// - [Obsolete("This constructor is obsolete and will be removed in a future version. Use BindingAddress.Parse(address) to create a BindingAddress instance.")] - public BindingAddress() - { - throw new InvalidOperationException("This constructor is obsolete and will be removed in a future version. Use BindingAddress.Parse(address) to create a BindingAddress instance."); - } + /// + /// Gets the path component. + /// + public string PathBase { get; } - /// - /// Gets the host component. - /// - public string Host { get; } - - /// - /// Gets the path component. - /// - public string PathBase { get; } - - /// - /// Gets the port. - /// - public int Port { get; } - - /// - /// Gets the scheme component. - /// - public string Scheme { get; } - - /// - /// Gets a value that determines if this instance represents a Unix pipe. - /// - /// Returns if starts with unix:// prefix. - /// - /// - public bool IsUnixPipe => Host.StartsWith(UnixPipeHostPrefix, StringComparison.Ordinal); - - /// - /// Gets the unix pipe path if this instance represents a Unix pipe. - /// - public string UnixPipePath - { - get - { - if (!IsUnixPipe) - { - throw new InvalidOperationException("Binding address is not a unix pipe."); - } + /// + /// Gets the port. + /// + public int Port { get; } - return GetUnixPipePath(Host); - } - } + /// + /// Gets the scheme component. + /// + public string Scheme { get; } - private static string GetUnixPipePath(string host) + /// + /// Gets a value that determines if this instance represents a Unix pipe. + /// + /// Returns if starts with unix:// prefix. + /// + /// + public bool IsUnixPipe => Host.StartsWith(UnixPipeHostPrefix, StringComparison.Ordinal); + + /// + /// Gets the unix pipe path if this instance represents a Unix pipe. + /// + public string UnixPipePath + { + get { - var unixPipeHostPrefixLength = UnixPipeHostPrefix.Length; - if (!OperatingSystem.IsWindows()) + if (!IsUnixPipe) { - // "/" character in unix refers to root. Windows has drive letters and volume separator (c:) - unixPipeHostPrefixLength--; + throw new InvalidOperationException("Binding address is not a unix pipe."); } - return host.Substring(unixPipeHostPrefixLength); + + return GetUnixPipePath(Host); } + } - /// - public override string ToString() + private static string GetUnixPipePath(string host) + { + var unixPipeHostPrefixLength = UnixPipeHostPrefix.Length; + if (!OperatingSystem.IsWindows()) { - if (IsUnixPipe) - { - return Scheme.ToLowerInvariant() + Uri.SchemeDelimiter + Host.ToLowerInvariant(); - } - else - { - return Scheme.ToLowerInvariant() + Uri.SchemeDelimiter + Host.ToLowerInvariant() + ":" + Port.ToString(CultureInfo.InvariantCulture) + PathBase; - } + // "/" character in unix refers to root. Windows has drive letters and volume separator (c:) + unixPipeHostPrefixLength--; } + return host.Substring(unixPipeHostPrefixLength); + } - /// - public override int GetHashCode() + /// + public override string ToString() + { + if (IsUnixPipe) { - return ToString().GetHashCode(); + return Scheme.ToLowerInvariant() + Uri.SchemeDelimiter + Host.ToLowerInvariant(); } - - /// - public override bool Equals(object? obj) + else { - var other = obj as BindingAddress; - if (other == null) - { - return false; - } - return string.Equals(Scheme, other.Scheme, StringComparison.OrdinalIgnoreCase) - && string.Equals(Host, other.Host, StringComparison.OrdinalIgnoreCase) - && Port == other.Port - && PathBase == other.PathBase; + return Scheme.ToLowerInvariant() + Uri.SchemeDelimiter + Host.ToLowerInvariant() + ":" + Port.ToString(CultureInfo.InvariantCulture) + PathBase; } + } + + /// + public override int GetHashCode() + { + return ToString().GetHashCode(); + } - /// - /// Parses the specified as a . - /// - /// The address to parse. - /// The parsed address. - public static BindingAddress Parse(string address) + /// + public override bool Equals(object? obj) + { + var other = obj as BindingAddress; + if (other == null) { - // A null/empty address will throw FormatException - address = address ?? string.Empty; + return false; + } + return string.Equals(Scheme, other.Scheme, StringComparison.OrdinalIgnoreCase) + && string.Equals(Host, other.Host, StringComparison.OrdinalIgnoreCase) + && Port == other.Port + && PathBase == other.PathBase; + } - var schemeDelimiterStart = address.IndexOf(Uri.SchemeDelimiter, StringComparison.Ordinal); - if (schemeDelimiterStart < 0) - { - throw new FormatException($"Invalid url: '{address}'"); - } - var schemeDelimiterEnd = schemeDelimiterStart + Uri.SchemeDelimiter.Length; + /// + /// Parses the specified as a . + /// + /// The address to parse. + /// The parsed address. + public static BindingAddress Parse(string address) + { + // A null/empty address will throw FormatException + address = address ?? string.Empty; - var isUnixPipe = address.IndexOf(UnixPipeHostPrefix, schemeDelimiterEnd, StringComparison.Ordinal) == schemeDelimiterEnd; + var schemeDelimiterStart = address.IndexOf(Uri.SchemeDelimiter, StringComparison.Ordinal); + if (schemeDelimiterStart < 0) + { + throw new FormatException($"Invalid url: '{address}'"); + } + var schemeDelimiterEnd = schemeDelimiterStart + Uri.SchemeDelimiter.Length; - int pathDelimiterStart; - int pathDelimiterEnd; - if (!isUnixPipe) - { - pathDelimiterStart = address.IndexOf("/", schemeDelimiterEnd, StringComparison.Ordinal); - pathDelimiterEnd = pathDelimiterStart; - } - else + var isUnixPipe = address.IndexOf(UnixPipeHostPrefix, schemeDelimiterEnd, StringComparison.Ordinal) == schemeDelimiterEnd; + + int pathDelimiterStart; + int pathDelimiterEnd; + if (!isUnixPipe) + { + pathDelimiterStart = address.IndexOf("/", schemeDelimiterEnd, StringComparison.Ordinal); + pathDelimiterEnd = pathDelimiterStart; + } + else + { + var unixPipeHostPrefixLength = UnixPipeHostPrefix.Length; + if (OperatingSystem.IsWindows()) { - var unixPipeHostPrefixLength = UnixPipeHostPrefix.Length; - if (OperatingSystem.IsWindows()) + // Windows has drive letters and volume separator (c:) + unixPipeHostPrefixLength += 2; + if (schemeDelimiterEnd + unixPipeHostPrefixLength > address.Length) { - // Windows has drive letters and volume separator (c:) - unixPipeHostPrefixLength += 2; - if (schemeDelimiterEnd + unixPipeHostPrefixLength > address.Length) - { - throw new FormatException($"Invalid url: '{address}'"); - } + throw new FormatException($"Invalid url: '{address}'"); } - - pathDelimiterStart = address.IndexOf(":", schemeDelimiterEnd + unixPipeHostPrefixLength, StringComparison.Ordinal); - pathDelimiterEnd = pathDelimiterStart + ":".Length; } - if (pathDelimiterStart < 0) - { - pathDelimiterStart = pathDelimiterEnd = address.Length; - } + pathDelimiterStart = address.IndexOf(":", schemeDelimiterEnd + unixPipeHostPrefixLength, StringComparison.Ordinal); + pathDelimiterEnd = pathDelimiterStart + ":".Length; + } + + if (pathDelimiterStart < 0) + { + pathDelimiterStart = pathDelimiterEnd = address.Length; + } - var scheme = address.Substring(0, schemeDelimiterStart); - string? host = null; - var port = 0; + var scheme = address.Substring(0, schemeDelimiterStart); + string? host = null; + var port = 0; - var hasSpecifiedPort = false; - if (!isUnixPipe) + var hasSpecifiedPort = false; + if (!isUnixPipe) + { + var portDelimiterStart = address.LastIndexOf(":", pathDelimiterStart - 1, pathDelimiterStart - schemeDelimiterEnd, StringComparison.Ordinal); + if (portDelimiterStart >= 0) { - var portDelimiterStart = address.LastIndexOf(":", pathDelimiterStart - 1, pathDelimiterStart - schemeDelimiterEnd, StringComparison.Ordinal); - if (portDelimiterStart >= 0) - { - var portDelimiterEnd = portDelimiterStart + ":".Length; - - var portString = address.Substring(portDelimiterEnd, pathDelimiterStart - portDelimiterEnd); - int portNumber; - if (int.TryParse(portString, NumberStyles.Integer, CultureInfo.InvariantCulture, out portNumber)) - { - hasSpecifiedPort = true; - host = address.Substring(schemeDelimiterEnd, portDelimiterStart - schemeDelimiterEnd); - port = portNumber; - } - } + var portDelimiterEnd = portDelimiterStart + ":".Length; - if (!hasSpecifiedPort) + var portString = address.Substring(portDelimiterEnd, pathDelimiterStart - portDelimiterEnd); + int portNumber; + if (int.TryParse(portString, NumberStyles.Integer, CultureInfo.InvariantCulture, out portNumber)) { - if (string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase)) - { - port = 80; - } - else if (string.Equals(scheme, "https", StringComparison.OrdinalIgnoreCase)) - { - port = 443; - } + hasSpecifiedPort = true; + host = address.Substring(schemeDelimiterEnd, portDelimiterStart - schemeDelimiterEnd); + port = portNumber; } } if (!hasSpecifiedPort) { - host = address.Substring(schemeDelimiterEnd, pathDelimiterStart - schemeDelimiterEnd); + if (string.Equals(scheme, "http", StringComparison.OrdinalIgnoreCase)) + { + port = 80; + } + else if (string.Equals(scheme, "https", StringComparison.OrdinalIgnoreCase)) + { + port = 443; + } } + } - if (string.IsNullOrEmpty(host)) - { - throw new FormatException($"Invalid url: '{address}'"); - } + if (!hasSpecifiedPort) + { + host = address.Substring(schemeDelimiterEnd, pathDelimiterStart - schemeDelimiterEnd); + } - if (isUnixPipe && !Path.IsPathRooted(GetUnixPipePath(host))) - { - throw new FormatException($"Invalid url, unix socket path must be absolute: '{address}'"); - } + if (string.IsNullOrEmpty(host)) + { + throw new FormatException($"Invalid url: '{address}'"); + } - string pathBase; - if (address[address.Length - 1] == '/') - { - pathBase = address.Substring(pathDelimiterEnd, address.Length - pathDelimiterEnd - 1); - } - else - { - pathBase = address.Substring(pathDelimiterEnd); - } + if (isUnixPipe && !Path.IsPathRooted(GetUnixPipePath(host))) + { + throw new FormatException($"Invalid url, unix socket path must be absolute: '{address}'"); + } - return new BindingAddress(host: host, pathBase: pathBase, port: port, scheme: scheme); + string pathBase; + if (address[address.Length - 1] == '/') + { + pathBase = address.Substring(pathDelimiterEnd, address.Length - pathDelimiterEnd - 1); } + else + { + pathBase = address.Substring(pathDelimiterEnd); + } + + return new BindingAddress(host: host, pathBase: pathBase, port: port, scheme: scheme); } } diff --git a/src/Http/Http/src/Builder/ApplicationBuilder.cs b/src/Http/Http/src/Builder/ApplicationBuilder.cs index ae70aed021..a7301fdcaf 100644 --- a/src/Http/Http/src/Builder/ApplicationBuilder.cs +++ b/src/Http/Http/src/Builder/ApplicationBuilder.cs @@ -8,140 +8,139 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Internal; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Default implementation for . +/// +public class ApplicationBuilder : IApplicationBuilder { + private const string ServerFeaturesKey = "server.Features"; + private const string ApplicationServicesKey = "application.Services"; + + private readonly List> _components = new(); + /// - /// Default implementation for . + /// Initializes a new instance of . /// - public class ApplicationBuilder : IApplicationBuilder + /// The for application services. + public ApplicationBuilder(IServiceProvider serviceProvider) { - private const string ServerFeaturesKey = "server.Features"; - private const string ApplicationServicesKey = "application.Services"; - - private readonly List> _components = new(); + Properties = new Dictionary(StringComparer.Ordinal); + ApplicationServices = serviceProvider; + } - /// - /// Initializes a new instance of . - /// - /// The for application services. - public ApplicationBuilder(IServiceProvider serviceProvider) - { - Properties = new Dictionary(StringComparer.Ordinal); - ApplicationServices = serviceProvider; - } + /// + /// Initializes a new instance of . + /// + /// The for application services. + /// The server instance that hosts the application. + public ApplicationBuilder(IServiceProvider serviceProvider, object server) + : this(serviceProvider) + { + SetProperty(ServerFeaturesKey, server); + } - /// - /// Initializes a new instance of . - /// - /// The for application services. - /// The server instance that hosts the application. - public ApplicationBuilder(IServiceProvider serviceProvider, object server) - : this(serviceProvider) - { - SetProperty(ServerFeaturesKey, server); - } + private ApplicationBuilder(ApplicationBuilder builder) + { + Properties = new CopyOnWriteDictionary(builder.Properties, StringComparer.Ordinal); + } - private ApplicationBuilder(ApplicationBuilder builder) + /// + /// Gets the for application services. + /// + public IServiceProvider ApplicationServices + { + get { - Properties = new CopyOnWriteDictionary(builder.Properties, StringComparer.Ordinal); + return GetProperty(ApplicationServicesKey)!; } - - /// - /// Gets the for application services. - /// - public IServiceProvider ApplicationServices + set { - get - { - return GetProperty(ApplicationServicesKey)!; - } - set - { - SetProperty(ApplicationServicesKey, value); - } + SetProperty(ApplicationServicesKey, value); } + } - /// - /// Gets the for server features. - /// - public IFeatureCollection ServerFeatures + /// + /// Gets the for server features. + /// + public IFeatureCollection ServerFeatures + { + get { - get - { - return GetProperty(ServerFeaturesKey)!; - } + return GetProperty(ServerFeaturesKey)!; } + } - /// - /// Gets a set of properties for . - /// - public IDictionary Properties { get; } + /// + /// Gets a set of properties for . + /// + public IDictionary Properties { get; } - private T? GetProperty(string key) - { - return Properties.TryGetValue(key, out var value) ? (T?)value : default(T); - } + private T? GetProperty(string key) + { + return Properties.TryGetValue(key, out var value) ? (T?)value : default(T); + } - private void SetProperty(string key, T value) - { - Properties[key] = value; - } + private void SetProperty(string key, T value) + { + Properties[key] = value; + } - /// - /// Adds the middleware to the application request pipeline. - /// - /// The middleware. - /// An instance of after the operation has completed. - public IApplicationBuilder Use(Func middleware) - { - _components.Add(middleware); - return this; - } + /// + /// Adds the middleware to the application request pipeline. + /// + /// The middleware. + /// An instance of after the operation has completed. + public IApplicationBuilder Use(Func middleware) + { + _components.Add(middleware); + return this; + } - /// - /// Creates a copy of this application builder. - /// - /// The created clone has the same properties as the current instance, but does not copy - /// the request pipeline. - /// - /// - /// The cloned instance. - public IApplicationBuilder New() - { - return new ApplicationBuilder(this); - } + /// + /// Creates a copy of this application builder. + /// + /// The created clone has the same properties as the current instance, but does not copy + /// the request pipeline. + /// + /// + /// The cloned instance. + public IApplicationBuilder New() + { + return new ApplicationBuilder(this); + } - /// - /// Produces a that executes added middlewares. - /// - /// The . - public RequestDelegate Build() + /// + /// Produces a that executes added middlewares. + /// + /// The . + public RequestDelegate Build() + { + RequestDelegate app = context => { - RequestDelegate app = context => - { // If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened. // This could happen if user code sets an endpoint, but they forgot to add the UseEndpoint middleware. var endpoint = context.GetEndpoint(); - var endpointRequestDelegate = endpoint?.RequestDelegate; - if (endpointRequestDelegate != null) - { - var message = - $"The request reached the end of the pipeline without executing the endpoint: '{endpoint!.DisplayName}'. " + - $"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " + - $"routing."; - throw new InvalidOperationException(message); - } - - context.Response.StatusCode = StatusCodes.Status404NotFound; - return Task.CompletedTask; - }; - - for (var c = _components.Count - 1; c >= 0; c--) + var endpointRequestDelegate = endpoint?.RequestDelegate; + if (endpointRequestDelegate != null) { - app = _components[c](app); + var message = + $"The request reached the end of the pipeline without executing the endpoint: '{endpoint!.DisplayName}'. " + + $"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " + + $"routing."; + throw new InvalidOperationException(message); } - return app; + context.Response.StatusCode = StatusCodes.Status404NotFound; + return Task.CompletedTask; + }; + + for (var c = _components.Count - 1; c >= 0; c--) + { + app = _components[c](app); } + + return app; } } diff --git a/src/Http/Http/src/DefaultHttpContext.cs b/src/Http/Http/src/DefaultHttpContext.cs index 01afc2d26c..ad10681de3 100644 --- a/src/Http/Http/src/DefaultHttpContext.cs +++ b/src/Http/Http/src/DefaultHttpContext.cs @@ -12,242 +12,241 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features.Authentication; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Represents an implementation of the HTTP Context class. +/// +public sealed class DefaultHttpContext : HttpContext { + // The initial size of the feature collection when using the default constructor; based on number of common features + // https://github.com/dotnet/aspnetcore/issues/31249 + private const int DefaultFeatureCollectionSize = 10; + + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private static readonly Func _newItemsFeature = f => new ItemsFeature(); + private static readonly Func _newServiceProvidersFeature = context => new RequestServicesFeature(context, context.ServiceScopeFactory); + private static readonly Func _newHttpAuthenticationFeature = f => new HttpAuthenticationFeature(); + private static readonly Func _newHttpRequestLifetimeFeature = f => new HttpRequestLifetimeFeature(); + private static readonly Func _newSessionFeature = f => new DefaultSessionFeature(); + private static readonly Func _nullSessionFeature = f => null; + private static readonly Func _newHttpRequestIdentifierFeature = f => new HttpRequestIdentifierFeature(); + + private FeatureReferences _features; + + private readonly DefaultHttpRequest _request; + private readonly DefaultHttpResponse _response; + + private DefaultConnectionInfo? _connection; + private DefaultWebSocketManager? _websockets; + + // This is field exists to make analyzing memory dumps easier. + // https://github.com/dotnet/aspnetcore/issues/29709 + internal bool _active; + /// - /// Represents an implementation of the HTTP Context class. + /// Initializes a new instance of the class. /// - public sealed class DefaultHttpContext : HttpContext + public DefaultHttpContext() + : this(new FeatureCollection(DefaultFeatureCollectionSize)) { - // The initial size of the feature collection when using the default constructor; based on number of common features - // https://github.com/dotnet/aspnetcore/issues/31249 - private const int DefaultFeatureCollectionSize = 10; - - // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 - private static readonly Func _newItemsFeature = f => new ItemsFeature(); - private static readonly Func _newServiceProvidersFeature = context => new RequestServicesFeature(context, context.ServiceScopeFactory); - private static readonly Func _newHttpAuthenticationFeature = f => new HttpAuthenticationFeature(); - private static readonly Func _newHttpRequestLifetimeFeature = f => new HttpRequestLifetimeFeature(); - private static readonly Func _newSessionFeature = f => new DefaultSessionFeature(); - private static readonly Func _nullSessionFeature = f => null; - private static readonly Func _newHttpRequestIdentifierFeature = f => new HttpRequestIdentifierFeature(); - - private FeatureReferences _features; - - private readonly DefaultHttpRequest _request; - private readonly DefaultHttpResponse _response; - - private DefaultConnectionInfo? _connection; - private DefaultWebSocketManager? _websockets; - - // This is field exists to make analyzing memory dumps easier. - // https://github.com/dotnet/aspnetcore/issues/29709 - internal bool _active; - - /// - /// Initializes a new instance of the class. - /// - public DefaultHttpContext() - : this(new FeatureCollection(DefaultFeatureCollectionSize)) - { - Features.Set(new HttpRequestFeature()); - Features.Set(new HttpResponseFeature()); - Features.Set(new StreamResponseBodyFeature(Stream.Null)); - } + Features.Set(new HttpRequestFeature()); + Features.Set(new HttpResponseFeature()); + Features.Set(new StreamResponseBodyFeature(Stream.Null)); + } - /// - /// Initializes a new instance of the class with provided features. - /// - /// Initial set of features for the . - public DefaultHttpContext(IFeatureCollection features) - { - _features.Initalize(features); - _request = new DefaultHttpRequest(this); - _response = new DefaultHttpResponse(this); - } + /// + /// Initializes a new instance of the class with provided features. + /// + /// Initial set of features for the . + public DefaultHttpContext(IFeatureCollection features) + { + _features.Initalize(features); + _request = new DefaultHttpRequest(this); + _response = new DefaultHttpResponse(this); + } - /// - /// Reinitialize the current instant of the class with features passed in. - /// - /// - /// This method allows the consumer to re-use the for another request, rather than having to allocate a new instance. - /// - /// The new set of features for the . - public void Initialize(IFeatureCollection features) - { - var revision = features.Revision; - _features.Initalize(features, revision); - _request.Initialize(revision); - _response.Initialize(revision); - _connection?.Initialize(features, revision); - _websockets?.Initialize(features, revision); - _active = true; - } + /// + /// Reinitialize the current instant of the class with features passed in. + /// + /// + /// This method allows the consumer to re-use the for another request, rather than having to allocate a new instance. + /// + /// The new set of features for the . + public void Initialize(IFeatureCollection features) + { + var revision = features.Revision; + _features.Initalize(features, revision); + _request.Initialize(revision); + _response.Initialize(revision); + _connection?.Initialize(features, revision); + _websockets?.Initialize(features, revision); + _active = true; + } - /// - /// Uninitialize all the features in the . - /// - public void Uninitialize() - { - _features = default; - _request.Uninitialize(); - _response.Uninitialize(); - _connection?.Uninitialize(); - _websockets?.Uninitialize(); - _active = false; - } + /// + /// Uninitialize all the features in the . + /// + public void Uninitialize() + { + _features = default; + _request.Uninitialize(); + _response.Uninitialize(); + _connection?.Uninitialize(); + _websockets?.Uninitialize(); + _active = false; + } - /// - /// Gets or set the for this instance. - /// - /// - /// - /// - public FormOptions FormOptions { get; set; } = default!; + /// + /// Gets or set the for this instance. + /// + /// + /// + /// + public FormOptions FormOptions { get; set; } = default!; - /// - /// Gets or sets the for this instance. - /// - /// - /// - /// - public IServiceScopeFactory ServiceScopeFactory { get; set; } = default!; + /// + /// Gets or sets the for this instance. + /// + /// + /// + /// + public IServiceScopeFactory ServiceScopeFactory { get; set; } = default!; - private IItemsFeature ItemsFeature => - _features.Fetch(ref _features.Cache.Items, _newItemsFeature)!; + private IItemsFeature ItemsFeature => + _features.Fetch(ref _features.Cache.Items, _newItemsFeature)!; - private IServiceProvidersFeature ServiceProvidersFeature => - _features.Fetch(ref _features.Cache.ServiceProviders, this, _newServiceProvidersFeature)!; + private IServiceProvidersFeature ServiceProvidersFeature => + _features.Fetch(ref _features.Cache.ServiceProviders, this, _newServiceProvidersFeature)!; - private IHttpAuthenticationFeature HttpAuthenticationFeature => - _features.Fetch(ref _features.Cache.Authentication, _newHttpAuthenticationFeature)!; + private IHttpAuthenticationFeature HttpAuthenticationFeature => + _features.Fetch(ref _features.Cache.Authentication, _newHttpAuthenticationFeature)!; - private IHttpRequestLifetimeFeature LifetimeFeature => - _features.Fetch(ref _features.Cache.Lifetime, _newHttpRequestLifetimeFeature)!; + private IHttpRequestLifetimeFeature LifetimeFeature => + _features.Fetch(ref _features.Cache.Lifetime, _newHttpRequestLifetimeFeature)!; - private ISessionFeature SessionFeature => - _features.Fetch(ref _features.Cache.Session, _newSessionFeature)!; + private ISessionFeature SessionFeature => + _features.Fetch(ref _features.Cache.Session, _newSessionFeature)!; - private ISessionFeature? SessionFeatureOrNull => - _features.Fetch(ref _features.Cache.Session, _nullSessionFeature); + private ISessionFeature? SessionFeatureOrNull => + _features.Fetch(ref _features.Cache.Session, _nullSessionFeature); - private IHttpRequestIdentifierFeature RequestIdentifierFeature => - _features.Fetch(ref _features.Cache.RequestIdentifier, _newHttpRequestIdentifierFeature)!; + private IHttpRequestIdentifierFeature RequestIdentifierFeature => + _features.Fetch(ref _features.Cache.RequestIdentifier, _newHttpRequestIdentifierFeature)!; - /// - public override IFeatureCollection Features => _features.Collection ?? ContextDisposed(); + /// + public override IFeatureCollection Features => _features.Collection ?? ContextDisposed(); - /// - public override HttpRequest Request => _request; + /// + public override HttpRequest Request => _request; - /// - public override HttpResponse Response => _response; + /// + public override HttpResponse Response => _response; - /// - public override ConnectionInfo Connection => _connection ?? (_connection = new DefaultConnectionInfo(Features)); + /// + public override ConnectionInfo Connection => _connection ?? (_connection = new DefaultConnectionInfo(Features)); - /// - public override WebSocketManager WebSockets => _websockets ?? (_websockets = new DefaultWebSocketManager(Features)); + /// + public override WebSocketManager WebSockets => _websockets ?? (_websockets = new DefaultWebSocketManager(Features)); - /// - public override ClaimsPrincipal User + /// + public override ClaimsPrincipal User + { + get { - get + var user = HttpAuthenticationFeature.User; + if (user == null) { - var user = HttpAuthenticationFeature.User; - if (user == null) - { - user = new ClaimsPrincipal(new ClaimsIdentity()); - HttpAuthenticationFeature.User = user; - } - return user; + user = new ClaimsPrincipal(new ClaimsIdentity()); + HttpAuthenticationFeature.User = user; } - set { HttpAuthenticationFeature.User = value; } + return user; } + set { HttpAuthenticationFeature.User = value; } + } - /// - public override IDictionary Items - { - get { return ItemsFeature.Items; } - set { ItemsFeature.Items = value; } - } + /// + public override IDictionary Items + { + get { return ItemsFeature.Items; } + set { ItemsFeature.Items = value; } + } - /// - public override IServiceProvider RequestServices - { - get { return ServiceProvidersFeature.RequestServices; } - set { ServiceProvidersFeature.RequestServices = value; } - } + /// + public override IServiceProvider RequestServices + { + get { return ServiceProvidersFeature.RequestServices; } + set { ServiceProvidersFeature.RequestServices = value; } + } - /// - public override CancellationToken RequestAborted - { - get { return LifetimeFeature.RequestAborted; } - set { LifetimeFeature.RequestAborted = value; } - } + /// + public override CancellationToken RequestAborted + { + get { return LifetimeFeature.RequestAborted; } + set { LifetimeFeature.RequestAborted = value; } + } - /// - public override string TraceIdentifier - { - get { return RequestIdentifierFeature.TraceIdentifier; } - set { RequestIdentifierFeature.TraceIdentifier = value; } - } + /// + public override string TraceIdentifier + { + get { return RequestIdentifierFeature.TraceIdentifier; } + set { RequestIdentifierFeature.TraceIdentifier = value; } + } - /// - public override ISession Session + /// + public override ISession Session + { + get { - get - { - var feature = SessionFeatureOrNull; - if (feature == null) - { - throw new InvalidOperationException("Session has not been configured for this application " + - "or request."); - } - return feature.Session; - } - set + var feature = SessionFeatureOrNull; + if (feature == null) { - SessionFeature.Session = value; + throw new InvalidOperationException("Session has not been configured for this application " + + "or request."); } + return feature.Session; } - - // This property exists because of backwards compatibility. - // We send an anonymous object with an HttpContext property - // via DiagnosticListener in various events throughout the pipeline. Instead - // we just send the HttpContext to avoid extra allocations - /// - /// This API is used by ASP.NET Core's infrastructure and should not be used by application code. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public HttpContext HttpContext => this; - - /// - public override void Abort() + set { - LifetimeFeature.Abort(); + SessionFeature.Session = value; } + } - private static IFeatureCollection ContextDisposed() - { - ThrowContextDisposed(); - return null; - } + // This property exists because of backwards compatibility. + // We send an anonymous object with an HttpContext property + // via DiagnosticListener in various events throughout the pipeline. Instead + // we just send the HttpContext to avoid extra allocations + /// + /// This API is used by ASP.NET Core's infrastructure and should not be used by application code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public HttpContext HttpContext => this; - [DoesNotReturn] - private static void ThrowContextDisposed() - { - throw new ObjectDisposedException(nameof(HttpContext), $"Request has finished and {nameof(HttpContext)} disposed."); - } + /// + public override void Abort() + { + LifetimeFeature.Abort(); + } - struct FeatureInterfaces - { - public IItemsFeature? Items; - public IServiceProvidersFeature? ServiceProviders; - public IHttpAuthenticationFeature? Authentication; - public IHttpRequestLifetimeFeature? Lifetime; - public ISessionFeature? Session; - public IHttpRequestIdentifierFeature? RequestIdentifier; - } + private static IFeatureCollection ContextDisposed() + { + ThrowContextDisposed(); + return null; + } + + [DoesNotReturn] + private static void ThrowContextDisposed() + { + throw new ObjectDisposedException(nameof(HttpContext), $"Request has finished and {nameof(HttpContext)} disposed."); + } + + struct FeatureInterfaces + { + public IItemsFeature? Items; + public IServiceProvidersFeature? ServiceProviders; + public IHttpAuthenticationFeature? Authentication; + public IHttpRequestLifetimeFeature? Lifetime; + public ISessionFeature? Session; + public IHttpRequestIdentifierFeature? RequestIdentifier; } } diff --git a/src/Http/Http/src/Extensions/HttpRequestRewindExtensions.cs b/src/Http/Http/src/Extensions/HttpRequestRewindExtensions.cs index 5f077c2deb..ea0c8bca6e 100644 --- a/src/Http/Http/src/Extensions/HttpRequestRewindExtensions.cs +++ b/src/Http/Http/src/Extensions/HttpRequestRewindExtensions.cs @@ -1,89 +1,88 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Extension methods for enabling buffering in an . +/// +public static class HttpRequestRewindExtensions { /// - /// Extension methods for enabling buffering in an . + /// Ensure the can be read multiple times. Normally + /// buffers request bodies in memory; writes requests larger than 30K bytes to disk. /// - public static class HttpRequestRewindExtensions + /// The to prepare. + /// + /// Temporary files for larger requests are written to the location named in the ASPNETCORE_TEMP + /// environment variable, if any. If that environment variable is not defined, these files are written to the + /// current user's temporary folder. Files are automatically deleted at the end of their associated requests. + /// + public static void EnableBuffering(this HttpRequest request) { - /// - /// Ensure the can be read multiple times. Normally - /// buffers request bodies in memory; writes requests larger than 30K bytes to disk. - /// - /// The to prepare. - /// - /// Temporary files for larger requests are written to the location named in the ASPNETCORE_TEMP - /// environment variable, if any. If that environment variable is not defined, these files are written to the - /// current user's temporary folder. Files are automatically deleted at the end of their associated requests. - /// - public static void EnableBuffering(this HttpRequest request) - { - BufferingHelper.EnableRewind(request); - } + BufferingHelper.EnableRewind(request); + } - /// - /// Ensure the can be read multiple times. Normally - /// buffers request bodies in memory; writes requests larger than bytes to - /// disk. - /// - /// The to prepare. - /// - /// The maximum size in bytes of the in-memory used to buffer the - /// stream. Larger request bodies are written to disk. - /// - /// - /// Temporary files for larger requests are written to the location named in the ASPNETCORE_TEMP - /// environment variable, if any. If that environment variable is not defined, these files are written to the - /// current user's temporary folder. Files are automatically deleted at the end of their associated requests. - /// - public static void EnableBuffering(this HttpRequest request, int bufferThreshold) - { - BufferingHelper.EnableRewind(request, bufferThreshold); - } + /// + /// Ensure the can be read multiple times. Normally + /// buffers request bodies in memory; writes requests larger than bytes to + /// disk. + /// + /// The to prepare. + /// + /// The maximum size in bytes of the in-memory used to buffer the + /// stream. Larger request bodies are written to disk. + /// + /// + /// Temporary files for larger requests are written to the location named in the ASPNETCORE_TEMP + /// environment variable, if any. If that environment variable is not defined, these files are written to the + /// current user's temporary folder. Files are automatically deleted at the end of their associated requests. + /// + public static void EnableBuffering(this HttpRequest request, int bufferThreshold) + { + BufferingHelper.EnableRewind(request, bufferThreshold); + } - /// - /// Ensure the can be read multiple times. Normally - /// buffers request bodies in memory; writes requests larger than 30K bytes to disk. - /// - /// The to prepare. - /// - /// The maximum size in bytes of the request body. An attempt to read beyond this limit will cause an - /// . - /// - /// - /// Temporary files for larger requests are written to the location named in the ASPNETCORE_TEMP - /// environment variable, if any. If that environment variable is not defined, these files are written to the - /// current user's temporary folder. Files are automatically deleted at the end of their associated requests. - /// - public static void EnableBuffering(this HttpRequest request, long bufferLimit) - { - BufferingHelper.EnableRewind(request, bufferLimit: bufferLimit); - } + /// + /// Ensure the can be read multiple times. Normally + /// buffers request bodies in memory; writes requests larger than 30K bytes to disk. + /// + /// The to prepare. + /// + /// The maximum size in bytes of the request body. An attempt to read beyond this limit will cause an + /// . + /// + /// + /// Temporary files for larger requests are written to the location named in the ASPNETCORE_TEMP + /// environment variable, if any. If that environment variable is not defined, these files are written to the + /// current user's temporary folder. Files are automatically deleted at the end of their associated requests. + /// + public static void EnableBuffering(this HttpRequest request, long bufferLimit) + { + BufferingHelper.EnableRewind(request, bufferLimit: bufferLimit); + } - /// - /// Ensure the can be read multiple times. Normally - /// buffers request bodies in memory; writes requests larger than bytes to - /// disk. - /// - /// The to prepare. - /// - /// The maximum size in bytes of the in-memory used to buffer the - /// stream. Larger request bodies are written to disk. - /// - /// - /// The maximum size in bytes of the request body. An attempt to read beyond this limit will cause an - /// . - /// - /// - /// Temporary files for larger requests are written to the location named in the ASPNETCORE_TEMP - /// environment variable, if any. If that environment variable is not defined, these files are written to the - /// current user's temporary folder. Files are automatically deleted at the end of their associated requests. - /// - public static void EnableBuffering(this HttpRequest request, int bufferThreshold, long bufferLimit) - { - BufferingHelper.EnableRewind(request, bufferThreshold, bufferLimit); - } + /// + /// Ensure the can be read multiple times. Normally + /// buffers request bodies in memory; writes requests larger than bytes to + /// disk. + /// + /// The to prepare. + /// + /// The maximum size in bytes of the in-memory used to buffer the + /// stream. Larger request bodies are written to disk. + /// + /// + /// The maximum size in bytes of the request body. An attempt to read beyond this limit will cause an + /// . + /// + /// + /// Temporary files for larger requests are written to the location named in the ASPNETCORE_TEMP + /// environment variable, if any. If that environment variable is not defined, these files are written to the + /// current user's temporary folder. Files are automatically deleted at the end of their associated requests. + /// + public static void EnableBuffering(this HttpRequest request, int bufferThreshold, long bufferLimit) + { + BufferingHelper.EnableRewind(request, bufferThreshold, bufferLimit); } } diff --git a/src/Http/Http/src/Features/Authentication/HttpAuthenticationFeature.cs b/src/Http/Http/src/Features/Authentication/HttpAuthenticationFeature.cs index d5f28ca62a..35adbfbaaa 100644 --- a/src/Http/Http/src/Features/Authentication/HttpAuthenticationFeature.cs +++ b/src/Http/Http/src/Features/Authentication/HttpAuthenticationFeature.cs @@ -3,14 +3,13 @@ using System.Security.Claims; -namespace Microsoft.AspNetCore.Http.Features.Authentication +namespace Microsoft.AspNetCore.Http.Features.Authentication; + +/// +/// Default implementation for . +/// +public class HttpAuthenticationFeature : IHttpAuthenticationFeature { - /// - /// Default implementation for . - /// - public class HttpAuthenticationFeature : IHttpAuthenticationFeature - { - /// - public ClaimsPrincipal? User { get; set; } - } + /// + public ClaimsPrincipal? User { get; set; } } diff --git a/src/Http/Http/src/Features/DefaultConnectionLifetimeNotificationFeature.cs b/src/Http/Http/src/Features/DefaultConnectionLifetimeNotificationFeature.cs index d7042cf990..0850f4f948 100644 --- a/src/Http/Http/src/Features/DefaultConnectionLifetimeNotificationFeature.cs +++ b/src/Http/Http/src/Features/DefaultConnectionLifetimeNotificationFeature.cs @@ -4,36 +4,35 @@ using System.Threading; using Microsoft.AspNetCore.Connections.Features; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Default implementation of . +/// +internal sealed class DefaultConnectionLifetimeNotificationFeature : IConnectionLifetimeNotificationFeature { + private readonly IHttpResponseFeature? _httpResponseFeature; + /// - /// Default implementation of . + /// /// - internal sealed class DefaultConnectionLifetimeNotificationFeature : IConnectionLifetimeNotificationFeature + /// + public DefaultConnectionLifetimeNotificationFeature(IHttpResponseFeature? httpResponseFeature) { - private readonly IHttpResponseFeature? _httpResponseFeature; - - /// - /// - /// - /// - public DefaultConnectionLifetimeNotificationFeature(IHttpResponseFeature? httpResponseFeature) - { - _httpResponseFeature = httpResponseFeature; - } + _httpResponseFeature = httpResponseFeature; + } - /// - public CancellationToken ConnectionClosedRequested { get; set; } + /// + public CancellationToken ConnectionClosedRequested { get; set; } - /// - public void RequestClose() + /// + public void RequestClose() + { + if (_httpResponseFeature != null) { - if (_httpResponseFeature != null) + if (!_httpResponseFeature.HasStarted) { - if (!_httpResponseFeature.HasStarted) - { - _httpResponseFeature.Headers.Connection = "close"; - } + _httpResponseFeature.Headers.Connection = "close"; } } } diff --git a/src/Http/Http/src/Features/DefaultSessionFeature.cs b/src/Http/Http/src/Features/DefaultSessionFeature.cs index 4ed6c1a979..dc2569637e 100644 --- a/src/Http/Http/src/Features/DefaultSessionFeature.cs +++ b/src/Http/Http/src/Features/DefaultSessionFeature.cs @@ -1,15 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// This type exists only for the purpose of unit testing where the user can directly set the +/// property without the need for creating a . +/// +public class DefaultSessionFeature : ISessionFeature { - /// - /// This type exists only for the purpose of unit testing where the user can directly set the - /// property without the need for creating a . - /// - public class DefaultSessionFeature : ISessionFeature - { - /// - public ISession Session { get; set; } = default!; - } + /// + public ISession Session { get; set; } = default!; } diff --git a/src/Http/Http/src/Features/FormFeature.cs b/src/Http/Http/src/Features/FormFeature.cs index e758b1b035..4892b5207d 100644 --- a/src/Http/Http/src/Features/FormFeature.cs +++ b/src/Http/Http/src/Features/FormFeature.cs @@ -11,326 +11,325 @@ using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Default implementation for . +/// +public class FormFeature : IFormFeature { + private readonly HttpRequest _request; + private readonly FormOptions _options; + private Task? _parsedFormTask; + private IFormCollection? _form; + /// - /// Default implementation for . + /// Initializes a new instance of . /// - public class FormFeature : IFormFeature + /// The to use as the backing store. + public FormFeature(IFormCollection form) { - private readonly HttpRequest _request; - private readonly FormOptions _options; - private Task? _parsedFormTask; - private IFormCollection? _form; - - /// - /// Initializes a new instance of . - /// - /// The to use as the backing store. - public FormFeature(IFormCollection form) + if (form == null) { - if (form == null) - { - throw new ArgumentNullException(nameof(form)); - } + throw new ArgumentNullException(nameof(form)); + } + + Form = form; + _request = default!; + _options = FormOptions.Default; + } + + /// + /// Initializes a new instance of . + /// + /// The . + public FormFeature(HttpRequest request) + : this(request, FormOptions.Default) + { + } - Form = form; - _request = default!; - _options = FormOptions.Default; + /// + /// Initializes a new instance of . + /// + /// The . + /// The . + public FormFeature(HttpRequest request, FormOptions options) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _request = request; + _options = options; + } - /// - /// Initializes a new instance of . - /// - /// The . - public FormFeature(HttpRequest request) - : this(request, FormOptions.Default) + private MediaTypeHeaderValue? ContentType + { + get { + MediaTypeHeaderValue.TryParse(_request.ContentType, out var mt); + return mt; } + } - /// - /// Initializes a new instance of . - /// - /// The . - /// The . - public FormFeature(HttpRequest request, FormOptions options) + /// + public bool HasFormContentType + { + get { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - if (options == null) + // Set directly + if (Form != null) { - throw new ArgumentNullException(nameof(options)); + return true; } - _request = request; - _options = options; + var contentType = ContentType; + return HasApplicationFormContentType(contentType) || HasMultipartFormContentType(contentType); } + } - private MediaTypeHeaderValue? ContentType + /// + public IFormCollection? Form + { + get { return _form; } + set { - get - { - MediaTypeHeaderValue.TryParse(_request.ContentType, out var mt); - return mt; - } + _parsedFormTask = null; + _form = value; } + } - /// - public bool HasFormContentType + /// + public IFormCollection ReadForm() + { + if (Form != null) { - get - { - // Set directly - if (Form != null) - { - return true; - } - - var contentType = ContentType; - return HasApplicationFormContentType(contentType) || HasMultipartFormContentType(contentType); - } + return Form; } - /// - public IFormCollection? Form + if (!HasFormContentType) { - get { return _form; } - set - { - _parsedFormTask = null; - _form = value; - } + throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType); } - /// - public IFormCollection ReadForm() + // TODO: Issue #456 Avoid Sync-over-Async http://blogs.msdn.com/b/pfxteam/archive/2012/04/13/10293638.aspx + // TODO: How do we prevent thread exhaustion? + return ReadFormAsync().GetAwaiter().GetResult(); + } + + /// + public Task ReadFormAsync() => ReadFormAsync(CancellationToken.None); + + /// + public Task ReadFormAsync(CancellationToken cancellationToken) + { + // Avoid state machine and task allocation for repeated reads + if (_parsedFormTask == null) { if (Form != null) { - return Form; + _parsedFormTask = Task.FromResult(Form); } - - if (!HasFormContentType) + else { - throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType); + _parsedFormTask = InnerReadFormAsync(cancellationToken); } + } + return _parsedFormTask; + } - // TODO: Issue #456 Avoid Sync-over-Async http://blogs.msdn.com/b/pfxteam/archive/2012/04/13/10293638.aspx - // TODO: How do we prevent thread exhaustion? - return ReadFormAsync().GetAwaiter().GetResult(); + private async Task InnerReadFormAsync(CancellationToken cancellationToken) + { + if (!HasFormContentType) + { + throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType); } - /// - public Task ReadFormAsync() => ReadFormAsync(CancellationToken.None); + cancellationToken.ThrowIfCancellationRequested(); - /// - public Task ReadFormAsync(CancellationToken cancellationToken) + if (_request.ContentLength == 0) { - // Avoid state machine and task allocation for repeated reads - if (_parsedFormTask == null) - { - if (Form != null) - { - _parsedFormTask = Task.FromResult(Form); - } - else - { - _parsedFormTask = InnerReadFormAsync(cancellationToken); - } - } - return _parsedFormTask; + return FormCollection.Empty; } - private async Task InnerReadFormAsync(CancellationToken cancellationToken) + if (_options.BufferBody) { - if (!HasFormContentType) - { - throw new InvalidOperationException("Incorrect Content-Type: " + _request.ContentType); - } + _request.EnableRewind(_options.MemoryBufferThreshold, _options.BufferBodyLengthLimit); + } - cancellationToken.ThrowIfCancellationRequested(); + FormCollection? formFields = null; + FormFileCollection? files = null; - if (_request.ContentLength == 0) + // Some of these code paths use StreamReader which does not support cancellation tokens. + using (cancellationToken.Register((state) => ((HttpContext)state!).Abort(), _request.HttpContext)) + { + var contentType = ContentType; + // Check the content-type + if (HasApplicationFormContentType(contentType)) { - return FormCollection.Empty; + var encoding = FilterEncoding(contentType.Encoding); + var formReader = new FormPipeReader(_request.BodyReader, encoding) + { + ValueCountLimit = _options.ValueCountLimit, + KeyLengthLimit = _options.KeyLengthLimit, + ValueLengthLimit = _options.ValueLengthLimit, + }; + formFields = new FormCollection(await formReader.ReadFormAsync(cancellationToken)); } - - if (_options.BufferBody) + else if (HasMultipartFormContentType(contentType)) { - _request.EnableRewind(_options.MemoryBufferThreshold, _options.BufferBodyLengthLimit); - } + var formAccumulator = new KeyValueAccumulator(); - FormCollection? formFields = null; - FormFileCollection? files = null; - - // Some of these code paths use StreamReader which does not support cancellation tokens. - using (cancellationToken.Register((state) => ((HttpContext)state!).Abort(), _request.HttpContext)) - { - var contentType = ContentType; - // Check the content-type - if (HasApplicationFormContentType(contentType)) + var boundary = GetBoundary(contentType, _options.MultipartBoundaryLengthLimit); + var multipartReader = new MultipartReader(boundary, _request.Body) { - var encoding = FilterEncoding(contentType.Encoding); - var formReader = new FormPipeReader(_request.BodyReader, encoding) - { - ValueCountLimit = _options.ValueCountLimit, - KeyLengthLimit = _options.KeyLengthLimit, - ValueLengthLimit = _options.ValueLengthLimit, - }; - formFields = new FormCollection(await formReader.ReadFormAsync(cancellationToken)); - } - else if (HasMultipartFormContentType(contentType)) + HeadersCountLimit = _options.MultipartHeadersCountLimit, + HeadersLengthLimit = _options.MultipartHeadersLengthLimit, + BodyLengthLimit = _options.MultipartBodyLengthLimit, + }; + var section = await multipartReader.ReadNextSectionAsync(cancellationToken); + while (section != null) { - var formAccumulator = new KeyValueAccumulator(); - - var boundary = GetBoundary(contentType, _options.MultipartBoundaryLengthLimit); - var multipartReader = new MultipartReader(boundary, _request.Body) + // Parse the content disposition here and pass it further to avoid reparsings + if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition)) { - HeadersCountLimit = _options.MultipartHeadersCountLimit, - HeadersLengthLimit = _options.MultipartHeadersLengthLimit, - BodyLengthLimit = _options.MultipartBodyLengthLimit, - }; - var section = await multipartReader.ReadNextSectionAsync(cancellationToken); - while (section != null) + throw new InvalidDataException("Form section has invalid Content-Disposition value: " + section.ContentDisposition); + } + + if (contentDisposition.IsFileDisposition()) { - // Parse the content disposition here and pass it further to avoid reparsings - if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition)) + var fileSection = new FileMultipartSection(section, contentDisposition); + + // Enable buffering for the file if not already done for the full body + section.EnableRewind( + _request.HttpContext.Response.RegisterForDispose, + _options.MemoryBufferThreshold, _options.MultipartBodyLengthLimit); + + // Find the end + await section.Body.DrainAsync(cancellationToken); + + var name = fileSection.Name; + var fileName = fileSection.FileName; + + FormFile file; + if (section.BaseStreamOffset.HasValue) + { + // Relative reference to buffered request body + file = new FormFile(_request.Body, section.BaseStreamOffset.GetValueOrDefault(), section.Body.Length, name, fileName); + } + else { - throw new InvalidDataException("Form section has invalid Content-Disposition value: " + section.ContentDisposition); + // Individually buffered file body + file = new FormFile(section.Body, 0, section.Body.Length, name, fileName); } + file.Headers = new HeaderDictionary(section.Headers); - if (contentDisposition.IsFileDisposition()) + if (files == null) { - var fileSection = new FileMultipartSection(section, contentDisposition); - - // Enable buffering for the file if not already done for the full body - section.EnableRewind( - _request.HttpContext.Response.RegisterForDispose, - _options.MemoryBufferThreshold, _options.MultipartBodyLengthLimit); - - // Find the end - await section.Body.DrainAsync(cancellationToken); - - var name = fileSection.Name; - var fileName = fileSection.FileName; - - FormFile file; - if (section.BaseStreamOffset.HasValue) - { - // Relative reference to buffered request body - file = new FormFile(_request.Body, section.BaseStreamOffset.GetValueOrDefault(), section.Body.Length, name, fileName); - } - else - { - // Individually buffered file body - file = new FormFile(section.Body, 0, section.Body.Length, name, fileName); - } - file.Headers = new HeaderDictionary(section.Headers); - - if (files == null) - { - files = new FormFileCollection(); - } - if (files.Count >= _options.ValueCountLimit) - { - throw new InvalidDataException($"Form value count limit {_options.ValueCountLimit} exceeded."); - } - files.Add(file); + files = new FormFileCollection(); } - else if (contentDisposition.IsFormDisposition()) + if (files.Count >= _options.ValueCountLimit) { - var formDataSection = new FormMultipartSection(section, contentDisposition); + throw new InvalidDataException($"Form value count limit {_options.ValueCountLimit} exceeded."); + } + files.Add(file); + } + else if (contentDisposition.IsFormDisposition()) + { + var formDataSection = new FormMultipartSection(section, contentDisposition); - // Content-Disposition: form-data; name="key" - // - // value + // Content-Disposition: form-data; name="key" + // + // value - // Do not limit the key name length here because the multipart headers length limit is already in effect. - var key = formDataSection.Name; - var value = await formDataSection.GetValueAsync(); + // Do not limit the key name length here because the multipart headers length limit is already in effect. + var key = formDataSection.Name; + var value = await formDataSection.GetValueAsync(); - formAccumulator.Append(key, value); - if (formAccumulator.ValueCount > _options.ValueCountLimit) - { - throw new InvalidDataException($"Form value count limit {_options.ValueCountLimit} exceeded."); - } - } - else + formAccumulator.Append(key, value); + if (formAccumulator.ValueCount > _options.ValueCountLimit) { - System.Diagnostics.Debug.Assert(false, "Unrecognized content-disposition for this section: " + section.ContentDisposition); + throw new InvalidDataException($"Form value count limit {_options.ValueCountLimit} exceeded."); } - - section = await multipartReader.ReadNextSectionAsync(cancellationToken); } - - if (formAccumulator.HasValues) + else { - formFields = new FormCollection(formAccumulator.GetResults(), files); + System.Diagnostics.Debug.Assert(false, "Unrecognized content-disposition for this section: " + section.ContentDisposition); } - } - } - // Rewind so later readers don't have to. - if (_request.Body.CanSeek) - { - _request.Body.Seek(0, SeekOrigin.Begin); - } + section = await multipartReader.ReadNextSectionAsync(cancellationToken); + } - if (formFields != null) - { - Form = formFields; - } - else if (files != null) - { - Form = new FormCollection(null, files); - } - else - { - Form = FormCollection.Empty; + if (formAccumulator.HasValues) + { + formFields = new FormCollection(formAccumulator.GetResults(), files); + } } - - return Form; } - private static Encoding FilterEncoding(Encoding? encoding) + // Rewind so later readers don't have to. + if (_request.Body.CanSeek) { - // UTF-7 is insecure and should not be honored. UTF-8 will succeed for most cases. - // https://docs.microsoft.com/en-us/dotnet/core/compatibility/syslib-warnings/syslib0001 - if (encoding == null || encoding.CodePage == 65000) - { - return Encoding.UTF8; - } - return encoding; + _request.Body.Seek(0, SeekOrigin.Begin); } - private static bool HasApplicationFormContentType([NotNullWhen(true)] MediaTypeHeaderValue? contentType) + if (formFields != null) + { + Form = formFields; + } + else if (files != null) { - // Content-Type: application/x-www-form-urlencoded; charset=utf-8 - return contentType != null && contentType.MediaType.Equals("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase); + Form = new FormCollection(null, files); } + else + { + Form = FormCollection.Empty; + } + + return Form; + } - private static bool HasMultipartFormContentType([NotNullWhen(true)] MediaTypeHeaderValue? contentType) + private static Encoding FilterEncoding(Encoding? encoding) + { + // UTF-7 is insecure and should not be honored. UTF-8 will succeed for most cases. + // https://docs.microsoft.com/en-us/dotnet/core/compatibility/syslib-warnings/syslib0001 + if (encoding == null || encoding.CodePage == 65000) { - // Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq - return contentType != null && contentType.MediaType.Equals("multipart/form-data", StringComparison.OrdinalIgnoreCase); + return Encoding.UTF8; } + return encoding; + } + + private static bool HasApplicationFormContentType([NotNullWhen(true)] MediaTypeHeaderValue? contentType) + { + // Content-Type: application/x-www-form-urlencoded; charset=utf-8 + return contentType != null && contentType.MediaType.Equals("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase); + } + + private static bool HasMultipartFormContentType([NotNullWhen(true)] MediaTypeHeaderValue? contentType) + { + // Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymx2fSWqWSd0OxQqq + return contentType != null && contentType.MediaType.Equals("multipart/form-data", StringComparison.OrdinalIgnoreCase); + } - // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq" - // The spec says 70 characters is a reasonable limit. - private static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit) + // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq" + // The spec says 70 characters is a reasonable limit. + private static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit) + { + var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary); + if (StringSegment.IsNullOrEmpty(boundary)) { - var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary); - if (StringSegment.IsNullOrEmpty(boundary)) - { - throw new InvalidDataException("Missing content-type boundary."); - } - if (boundary.Length > lengthLimit) - { - throw new InvalidDataException($"Multipart boundary length limit {lengthLimit} exceeded."); - } - return boundary.ToString(); + throw new InvalidDataException("Missing content-type boundary."); + } + if (boundary.Length > lengthLimit) + { + throw new InvalidDataException($"Multipart boundary length limit {lengthLimit} exceeded."); } + return boundary.ToString(); } } diff --git a/src/Http/Http/src/Features/FormOptions.cs b/src/Http/Http/src/Features/FormOptions.cs index 60628c66a4..f7b7eecaa5 100644 --- a/src/Http/Http/src/Features/FormOptions.cs +++ b/src/Http/Http/src/Features/FormOptions.cs @@ -4,108 +4,107 @@ using System.IO; using Microsoft.AspNetCore.WebUtilities; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Options to configure reading the request body as a HTTP form. +/// +public class FormOptions { + internal static readonly FormOptions Default = new FormOptions(); + + /// + /// Default value for . + /// Defaults to 65,536 bytes‬, which is approximately 64KB. + /// + public const int DefaultMemoryBufferThreshold = 1024 * 64; + + /// + /// Default value for . + /// Defaults to 134,217,728 bytes‬, which is 128MB. + /// + public const int DefaultBufferBodyLengthLimit = 1024 * 1024 * 128; + + /// + /// Default value for . + /// Defaults to 128 bytes‬. + /// + public const int DefaultMultipartBoundaryLengthLimit = 128; + + /// + /// Default value for . + /// Defaults to 134,217,728 bytes‬, which is approximately 128MB. + /// + public const long DefaultMultipartBodyLengthLimit = 1024 * 1024 * 128; + + /// + /// Enables full request body buffering. Use this if multiple components need to read the raw stream. + /// Defaults to false. + /// + public bool BufferBody { get; set; } + + /// + /// If is enabled, this many bytes of the body will be buffered in memory. + /// If this threshold is exceeded then the buffer will be moved to a temp file on disk instead. + /// This also applies when buffering individual multipart section bodies. + /// Defaults to 65,536 bytes‬, which is approximately 64KB. + /// + public int MemoryBufferThreshold { get; set; } = DefaultMemoryBufferThreshold; + + /// + /// If is enabled, this is the limit for the total number of bytes that will + /// be buffered. Forms that exceed this limit will throw an when parsed. + /// Defaults to 134,217,728 bytes‬, which is approximately 128MB. + /// + public long BufferBodyLengthLimit { get; set; } = DefaultBufferBodyLengthLimit; + + /// + /// A limit for the number of form entries to allow. + /// Forms that exceed this limit will throw an when parsed. + /// Defaults to 1024. + /// + public int ValueCountLimit { get; set; } = FormReader.DefaultValueCountLimit; + + /// + /// A limit on the length of individual keys. Forms containing keys that exceed this limit will + /// throw an when parsed. + /// Defaults to 2,048 bytes‬, which is approximately 2KB. + /// + public int KeyLengthLimit { get; set; } = FormReader.DefaultKeyLengthLimit; + + /// + /// A limit on the length of individual form values. Forms containing values that exceed this + /// limit will throw an when parsed. + /// Defaults to 4,194,304 bytes‬, which is approximately 4MB. + /// + public int ValueLengthLimit { get; set; } = FormReader.DefaultValueLengthLimit; + + /// + /// A limit for the length of the boundary identifier. Forms with boundaries that exceed this + /// limit will throw an when parsed. + /// Defaults to 128 bytes‬. + /// + public int MultipartBoundaryLengthLimit { get; set; } = DefaultMultipartBoundaryLengthLimit; + + /// + /// A limit for the number of headers to allow in each multipart section. Headers with the same name will + /// be combined. Form sections that exceed this limit will throw an + /// when parsed. + /// Defaults to 16. + /// + public int MultipartHeadersCountLimit { get; set; } = MultipartReader.DefaultHeadersCountLimit; + + /// + /// A limit for the total length of the header keys and values in each multipart section. + /// Form sections that exceed this limit will throw an when parsed. + /// Defaults to 16,384‬ bytes‬, which is approximately 16KB. + /// + public int MultipartHeadersLengthLimit { get; set; } = MultipartReader.DefaultHeadersLengthLimit; + /// - /// Options to configure reading the request body as a HTTP form. + /// A limit for the length of each multipart body. Forms sections that exceed this limit will throw an + /// when parsed. + /// Defaults to 134,217,728 bytes‬, which is approximately 128MB. /// - public class FormOptions - { - internal static readonly FormOptions Default = new FormOptions(); - - /// - /// Default value for . - /// Defaults to 65,536 bytes‬, which is approximately 64KB. - /// - public const int DefaultMemoryBufferThreshold = 1024 * 64; - - /// - /// Default value for . - /// Defaults to 134,217,728 bytes‬, which is 128MB. - /// - public const int DefaultBufferBodyLengthLimit = 1024 * 1024 * 128; - - /// - /// Default value for . - /// Defaults to 128 bytes‬. - /// - public const int DefaultMultipartBoundaryLengthLimit = 128; - - /// - /// Default value for . - /// Defaults to 134,217,728 bytes‬, which is approximately 128MB. - /// - public const long DefaultMultipartBodyLengthLimit = 1024 * 1024 * 128; - - /// - /// Enables full request body buffering. Use this if multiple components need to read the raw stream. - /// Defaults to false. - /// - public bool BufferBody { get; set; } - - /// - /// If is enabled, this many bytes of the body will be buffered in memory. - /// If this threshold is exceeded then the buffer will be moved to a temp file on disk instead. - /// This also applies when buffering individual multipart section bodies. - /// Defaults to 65,536 bytes‬, which is approximately 64KB. - /// - public int MemoryBufferThreshold { get; set; } = DefaultMemoryBufferThreshold; - - /// - /// If is enabled, this is the limit for the total number of bytes that will - /// be buffered. Forms that exceed this limit will throw an when parsed. - /// Defaults to 134,217,728 bytes‬, which is approximately 128MB. - /// - public long BufferBodyLengthLimit { get; set; } = DefaultBufferBodyLengthLimit; - - /// - /// A limit for the number of form entries to allow. - /// Forms that exceed this limit will throw an when parsed. - /// Defaults to 1024. - /// - public int ValueCountLimit { get; set; } = FormReader.DefaultValueCountLimit; - - /// - /// A limit on the length of individual keys. Forms containing keys that exceed this limit will - /// throw an when parsed. - /// Defaults to 2,048 bytes‬, which is approximately 2KB. - /// - public int KeyLengthLimit { get; set; } = FormReader.DefaultKeyLengthLimit; - - /// - /// A limit on the length of individual form values. Forms containing values that exceed this - /// limit will throw an when parsed. - /// Defaults to 4,194,304 bytes‬, which is approximately 4MB. - /// - public int ValueLengthLimit { get; set; } = FormReader.DefaultValueLengthLimit; - - /// - /// A limit for the length of the boundary identifier. Forms with boundaries that exceed this - /// limit will throw an when parsed. - /// Defaults to 128 bytes‬. - /// - public int MultipartBoundaryLengthLimit { get; set; } = DefaultMultipartBoundaryLengthLimit; - - /// - /// A limit for the number of headers to allow in each multipart section. Headers with the same name will - /// be combined. Form sections that exceed this limit will throw an - /// when parsed. - /// Defaults to 16. - /// - public int MultipartHeadersCountLimit { get; set; } = MultipartReader.DefaultHeadersCountLimit; - - /// - /// A limit for the total length of the header keys and values in each multipart section. - /// Form sections that exceed this limit will throw an when parsed. - /// Defaults to 16,384‬ bytes‬, which is approximately 16KB. - /// - public int MultipartHeadersLengthLimit { get; set; } = MultipartReader.DefaultHeadersLengthLimit; - - /// - /// A limit for the length of each multipart body. Forms sections that exceed this limit will throw an - /// when parsed. - /// Defaults to 134,217,728 bytes‬, which is approximately 128MB. - /// - public long MultipartBodyLengthLimit { get; set; } = DefaultMultipartBodyLengthLimit; - } + public long MultipartBodyLengthLimit { get; set; } = DefaultMultipartBodyLengthLimit; } diff --git a/src/Http/Http/src/Features/HttpConnectionFeature.cs b/src/Http/Http/src/Features/HttpConnectionFeature.cs index 4268e401b6..6a4ba99a97 100644 --- a/src/Http/Http/src/Features/HttpConnectionFeature.cs +++ b/src/Http/Http/src/Features/HttpConnectionFeature.cs @@ -3,26 +3,25 @@ using System.Net; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Default implementation for . +/// +public class HttpConnectionFeature : IHttpConnectionFeature { - /// - /// Default implementation for . - /// - public class HttpConnectionFeature : IHttpConnectionFeature - { - /// - public string ConnectionId { get; set; } = default!; + /// + public string ConnectionId { get; set; } = default!; - /// - public IPAddress? LocalIpAddress { get; set; } + /// + public IPAddress? LocalIpAddress { get; set; } - /// - public int LocalPort { get; set; } + /// + public int LocalPort { get; set; } - /// - public IPAddress? RemoteIpAddress { get; set; } + /// + public IPAddress? RemoteIpAddress { get; set; } - /// - public int RemotePort { get; set; } - } + /// + public int RemotePort { get; set; } } diff --git a/src/Http/Http/src/Features/HttpRequestFeature.cs b/src/Http/Http/src/Features/HttpRequestFeature.cs index 2882014c39..69d6c9e2be 100644 --- a/src/Http/Http/src/Features/HttpRequestFeature.cs +++ b/src/Http/Http/src/Features/HttpRequestFeature.cs @@ -3,54 +3,53 @@ using System.IO; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Default implementation for . +/// +public class HttpRequestFeature : IHttpRequestFeature { /// - /// Default implementation for . + /// Initiaizes a new instance of . /// - public class HttpRequestFeature : IHttpRequestFeature + public HttpRequestFeature() { - /// - /// Initiaizes a new instance of . - /// - public HttpRequestFeature() - { - Headers = new HeaderDictionary(); - Body = Stream.Null; - Protocol = string.Empty; - Scheme = string.Empty; - Method = string.Empty; - PathBase = string.Empty; - Path = string.Empty; - QueryString = string.Empty; - RawTarget = string.Empty; - } - - /// - public string Protocol { get; set; } - - /// - public string Scheme { get; set; } - - /// - public string Method { get; set; } - - /// - public string PathBase { get; set; } - - /// - public string Path { get; set; } - - /// - public string QueryString { get; set; } - - /// - public string RawTarget { get; set; } - - /// - public IHeaderDictionary Headers { get; set; } - - /// - public Stream Body { get; set; } + Headers = new HeaderDictionary(); + Body = Stream.Null; + Protocol = string.Empty; + Scheme = string.Empty; + Method = string.Empty; + PathBase = string.Empty; + Path = string.Empty; + QueryString = string.Empty; + RawTarget = string.Empty; } + + /// + public string Protocol { get; set; } + + /// + public string Scheme { get; set; } + + /// + public string Method { get; set; } + + /// + public string PathBase { get; set; } + + /// + public string Path { get; set; } + + /// + public string QueryString { get; set; } + + /// + public string RawTarget { get; set; } + + /// + public IHeaderDictionary Headers { get; set; } + + /// + public Stream Body { get; set; } } diff --git a/src/Http/Http/src/Features/HttpRequestIdentifierFeature.cs b/src/Http/Http/src/Features/HttpRequestIdentifierFeature.cs index 3cf0cb094f..5bd927b5da 100644 --- a/src/Http/Http/src/Features/HttpRequestIdentifierFeature.cs +++ b/src/Http/Http/src/Features/HttpRequestIdentifierFeature.cs @@ -4,60 +4,59 @@ using System; using System.Threading; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Default implementation for . +/// +public class HttpRequestIdentifierFeature : IHttpRequestIdentifierFeature { - /// - /// Default implementation for . - /// - public class HttpRequestIdentifierFeature : IHttpRequestIdentifierFeature - { - // Base32 encoding - in ascii sort order for easy text based sorting - private static readonly char[] s_encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV".ToCharArray(); - // Seed the _requestId for this application instance with - // the number of 100-nanosecond intervals that have elapsed since 12:00:00 midnight, January 1, 0001 - // for a roughly increasing _requestId over restarts - private static long _requestId = DateTime.UtcNow.Ticks; + // Base32 encoding - in ascii sort order for easy text based sorting + private static readonly char[] s_encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV".ToCharArray(); + // Seed the _requestId for this application instance with + // the number of 100-nanosecond intervals that have elapsed since 12:00:00 midnight, January 1, 0001 + // for a roughly increasing _requestId over restarts + private static long _requestId = DateTime.UtcNow.Ticks; - private string? _id; + private string? _id; - /// - public string TraceIdentifier + /// + public string TraceIdentifier + { + get { - get + // Don't incur the cost of generating the request ID until it's asked for + if (_id == null) { - // Don't incur the cost of generating the request ID until it's asked for - if (_id == null) - { - _id = GenerateRequestId(Interlocked.Increment(ref _requestId)); - } - return _id; - } - set - { - _id = value; + _id = GenerateRequestId(Interlocked.Increment(ref _requestId)); } + return _id; } + set + { + _id = value; + } + } - private static string GenerateRequestId(long id) + private static string GenerateRequestId(long id) + { + return string.Create(13, id, (buffer, value) => { - return string.Create(13, id, (buffer, value) => - { - var encode32Chars = s_encode32Chars; + var encode32Chars = s_encode32Chars; - buffer[12] = encode32Chars[value & 31]; - buffer[11] = encode32Chars[(value >> 5) & 31]; - buffer[10] = encode32Chars[(value >> 10) & 31]; - buffer[9] = encode32Chars[(value >> 15) & 31]; - buffer[8] = encode32Chars[(value >> 20) & 31]; - buffer[7] = encode32Chars[(value >> 25) & 31]; - buffer[6] = encode32Chars[(value >> 30) & 31]; - buffer[5] = encode32Chars[(value >> 35) & 31]; - buffer[4] = encode32Chars[(value >> 40) & 31]; - buffer[3] = encode32Chars[(value >> 45) & 31]; - buffer[2] = encode32Chars[(value >> 50) & 31]; - buffer[1] = encode32Chars[(value >> 55) & 31]; - buffer[0] = encode32Chars[(value >> 60) & 31]; - }); - } + buffer[12] = encode32Chars[value & 31]; + buffer[11] = encode32Chars[(value >> 5) & 31]; + buffer[10] = encode32Chars[(value >> 10) & 31]; + buffer[9] = encode32Chars[(value >> 15) & 31]; + buffer[8] = encode32Chars[(value >> 20) & 31]; + buffer[7] = encode32Chars[(value >> 25) & 31]; + buffer[6] = encode32Chars[(value >> 30) & 31]; + buffer[5] = encode32Chars[(value >> 35) & 31]; + buffer[4] = encode32Chars[(value >> 40) & 31]; + buffer[3] = encode32Chars[(value >> 45) & 31]; + buffer[2] = encode32Chars[(value >> 50) & 31]; + buffer[1] = encode32Chars[(value >> 55) & 31]; + buffer[0] = encode32Chars[(value >> 60) & 31]; + }); } } diff --git a/src/Http/Http/src/Features/HttpRequestLifetimeFeature.cs b/src/Http/Http/src/Features/HttpRequestLifetimeFeature.cs index 6775f134fd..50ed489368 100644 --- a/src/Http/Http/src/Features/HttpRequestLifetimeFeature.cs +++ b/src/Http/Http/src/Features/HttpRequestLifetimeFeature.cs @@ -3,19 +3,18 @@ using System.Threading; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Default implementation for . +/// +public class HttpRequestLifetimeFeature : IHttpRequestLifetimeFeature { - /// - /// Default implementation for . - /// - public class HttpRequestLifetimeFeature : IHttpRequestLifetimeFeature - { - /// - public CancellationToken RequestAborted { get; set; } + /// + public CancellationToken RequestAborted { get; set; } - /// - public void Abort() - { - } + /// + public void Abort() + { } } diff --git a/src/Http/Http/src/Features/HttpResponseFeature.cs b/src/Http/Http/src/Features/HttpResponseFeature.cs index fd21362f2f..a7d2fd02ca 100644 --- a/src/Http/Http/src/Features/HttpResponseFeature.cs +++ b/src/Http/Http/src/Features/HttpResponseFeature.cs @@ -5,46 +5,45 @@ using System; using System.IO; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Default implementation for . +/// +public class HttpResponseFeature : IHttpResponseFeature { /// - /// Default implementation for . + /// Initializes a new instance of . /// - public class HttpResponseFeature : IHttpResponseFeature + public HttpResponseFeature() + { + StatusCode = 200; + Headers = new HeaderDictionary(); + Body = Stream.Null; + } + + /// + public int StatusCode { get; set; } + + /// + public string? ReasonPhrase { get; set; } + + /// + public IHeaderDictionary Headers { get; set; } + + /// + public Stream Body { get; set; } + + /// + public virtual bool HasStarted => false; + + /// + public virtual void OnStarting(Func callback, object state) + { + } + + /// + public virtual void OnCompleted(Func callback, object state) { - /// - /// Initializes a new instance of . - /// - public HttpResponseFeature() - { - StatusCode = 200; - Headers = new HeaderDictionary(); - Body = Stream.Null; - } - - /// - public int StatusCode { get; set; } - - /// - public string? ReasonPhrase { get; set; } - - /// - public IHeaderDictionary Headers { get; set; } - - /// - public Stream Body { get; set; } - - /// - public virtual bool HasStarted => false; - - /// - public virtual void OnStarting(Func callback, object state) - { - } - - /// - public virtual void OnCompleted(Func callback, object state) - { - } } } diff --git a/src/Http/Http/src/Features/IHttpActivityFeature.cs b/src/Http/Http/src/Features/IHttpActivityFeature.cs index 0ec4bd223f..2e7c165413 100644 --- a/src/Http/Http/src/Features/IHttpActivityFeature.cs +++ b/src/Http/Http/src/Features/IHttpActivityFeature.cs @@ -3,16 +3,15 @@ using System.Diagnostics; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Feature to access the associated with a request. +/// +public interface IHttpActivityFeature { /// - /// Feature to access the associated with a request. + /// Returns the associated with the current request. /// - public interface IHttpActivityFeature - { - /// - /// Returns the associated with the current request. - /// - Activity Activity { get; set; } - } + Activity Activity { get; set; } } diff --git a/src/Http/Http/src/Features/ItemsFeature.cs b/src/Http/Http/src/Features/ItemsFeature.cs index 5a4e1f28f6..3533617461 100644 --- a/src/Http/Http/src/Features/ItemsFeature.cs +++ b/src/Http/Http/src/Features/ItemsFeature.cs @@ -3,22 +3,21 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Default implementation for . +/// +public class ItemsFeature : IItemsFeature { /// - /// Default implementation for . + /// Initializes a new instance of . /// - public class ItemsFeature : IItemsFeature + public ItemsFeature() { - /// - /// Initializes a new instance of . - /// - public ItemsFeature() - { - Items = new ItemsDictionary(); - } - - /// - public IDictionary Items { get; set; } + Items = new ItemsDictionary(); } + + /// + public IDictionary Items { get; set; } } diff --git a/src/Http/Http/src/Features/QueryFeature.cs b/src/Http/Http/src/Features/QueryFeature.cs index 66c0bfa030..abc0f85c1e 100644 --- a/src/Http/Http/src/Features/QueryFeature.cs +++ b/src/Http/Http/src/Features/QueryFeature.cs @@ -8,219 +8,218 @@ using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Default implementation for . +/// +public class QueryFeature : IQueryFeature { + // Lambda hoisted to static readonly field to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private static readonly Func _nullRequestFeature = f => null; + + private FeatureReferences _features; + + private string? _original; + private IQueryCollection? _parsedValues; + /// - /// Default implementation for . + /// Initializes a new instance of . /// - public class QueryFeature : IQueryFeature + /// The to use as a backing store. + public QueryFeature(IQueryCollection query) { - // Lambda hoisted to static readonly field to improve inlining https://github.com/dotnet/roslyn/issues/13624 - private static readonly Func _nullRequestFeature = f => null; - - private FeatureReferences _features; + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } - private string? _original; - private IQueryCollection? _parsedValues; + _parsedValues = query; + } - /// - /// Initializes a new instance of . - /// - /// The to use as a backing store. - public QueryFeature(IQueryCollection query) + /// + /// Initializes a new instance of . + /// + /// The to initialize. + public QueryFeature(IFeatureCollection features) + { + if (features == null) { - if (query == null) - { - throw new ArgumentNullException(nameof(query)); - } - - _parsedValues = query; + throw new ArgumentNullException(nameof(features)); } - /// - /// Initializes a new instance of . - /// - /// The to initialize. - public QueryFeature(IFeatureCollection features) + _features.Initalize(features); + } + + private IHttpRequestFeature HttpRequestFeature => + _features.Fetch(ref _features.Cache, _nullRequestFeature)!; + + /// + public IQueryCollection Query + { + get { - if (features == null) + if (_features.Collection is null) { - throw new ArgumentNullException(nameof(features)); + return _parsedValues ?? QueryCollection.Empty; } - _features.Initalize(features); - } + var current = HttpRequestFeature.QueryString; + if (_parsedValues == null || !string.Equals(_original, current, StringComparison.Ordinal)) + { + _original = current; - private IHttpRequestFeature HttpRequestFeature => - _features.Fetch(ref _features.Cache, _nullRequestFeature)!; + var result = ParseNullableQueryInternal(current); - /// - public IQueryCollection Query + _parsedValues = result is not null + ? new QueryCollectionInternal(result) + : QueryCollection.Empty; + } + return _parsedValues; + } + set { - get + _parsedValues = value; + if (_features.Collection != null) { - if (_features.Collection is null) + if (value == null) { - return _parsedValues ?? QueryCollection.Empty; + _original = string.Empty; + HttpRequestFeature.QueryString = string.Empty; } - - var current = HttpRequestFeature.QueryString; - if (_parsedValues == null || !string.Equals(_original, current, StringComparison.Ordinal)) - { - _original = current; - - var result = ParseNullableQueryInternal(current); - - _parsedValues = result is not null - ? new QueryCollectionInternal(result) - : QueryCollection.Empty; - } - return _parsedValues; - } - set - { - _parsedValues = value; - if (_features.Collection != null) + else { - if (value == null) - { - _original = string.Empty; - HttpRequestFeature.QueryString = string.Empty; - } - else - { - _original = QueryString.Create(_parsedValues).ToString(); - HttpRequestFeature.QueryString = _original; - } + _original = QueryString.Create(_parsedValues).ToString(); + HttpRequestFeature.QueryString = _original; } } } + } + + /// + /// Parse a query string into its component key and value parts. + /// + /// The raw query string value, with or without the leading '?'. + /// A collection of parsed keys and values, null if there are no entries. + [SkipLocalsInit] + internal static AdaptiveCapacityDictionary? ParseNullableQueryInternal(string? queryString) + { + if (string.IsNullOrEmpty(queryString) || (queryString.Length == 1 && queryString[0] == '?')) + { + return null; + } + var accumulator = new KvpAccumulator(); + var enumerable = new QueryStringEnumerable(queryString); + foreach (var pair in enumerable) + { + accumulator.Append(pair.DecodeName().Span, pair.DecodeValue().Span); + } + + return accumulator.HasValues + ? accumulator.GetResults() + : null; + } + + internal struct KvpAccumulator + { /// - /// Parse a query string into its component key and value parts. + /// This API supports infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. /// - /// The raw query string value, with or without the leading '?'. - /// A collection of parsed keys and values, null if there are no entries. - [SkipLocalsInit] - internal static AdaptiveCapacityDictionary? ParseNullableQueryInternal(string? queryString) + private AdaptiveCapacityDictionary _accumulator; + private AdaptiveCapacityDictionary> _expandingAccumulator; + + public void Append(ReadOnlySpan key, ReadOnlySpan value) + => Append(key.ToString(), value.ToString()); + + /// + /// This API supports infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public void Append(string key, string value) { - if (string.IsNullOrEmpty(queryString) || (queryString.Length == 1 && queryString[0] == '?')) + if (_accumulator is null) { - return null; + _accumulator = new AdaptiveCapacityDictionary(StringComparer.OrdinalIgnoreCase); } - var accumulator = new KvpAccumulator(); - var enumerable = new QueryStringEnumerable(queryString); - foreach (var pair in enumerable) + if (!_accumulator.TryGetValue(key, out var values)) + { + // First value for this key + _accumulator[key] = new StringValues(value); + } + else { - accumulator.Append(pair.DecodeName().Span, pair.DecodeValue().Span); + AppendToExpandingAccumulator(key, value, values); } - return accumulator.HasValues - ? accumulator.GetResults() - : null; + ValueCount++; } - internal struct KvpAccumulator + private void AppendToExpandingAccumulator(string key, string value, StringValues values) { - /// - /// This API supports infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - private AdaptiveCapacityDictionary _accumulator; - private AdaptiveCapacityDictionary> _expandingAccumulator; - - public void Append(ReadOnlySpan key, ReadOnlySpan value) - => Append(key.ToString(), value.ToString()); - - /// - /// This API supports infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public void Append(string key, string value) + // When there are some values for the same key, so switch to expanding accumulator, and + // add a zero count marker in the accumulator to indicate that switch. + + if (values.Count != 0) { - if (_accumulator is null) - { - _accumulator = new AdaptiveCapacityDictionary(StringComparer.OrdinalIgnoreCase); - } + _accumulator[key] = default; - if (!_accumulator.TryGetValue(key, out var values)) - { - // First value for this key - _accumulator[key] = new StringValues(value); - } - else + if (_expandingAccumulator is null) { - AppendToExpandingAccumulator(key, value, values); + _expandingAccumulator = new AdaptiveCapacityDictionary>(capacity: 5, StringComparer.OrdinalIgnoreCase); } - ValueCount++; - } + // Already 2 (1 existing + the new one) entries so use List's expansion mechanism for more + var list = new List(); - private void AppendToExpandingAccumulator(string key, string value, StringValues values) - { - // When there are some values for the same key, so switch to expanding accumulator, and - // add a zero count marker in the accumulator to indicate that switch. - - if (values.Count != 0) - { - _accumulator[key] = default; + list.AddRange(values); + list.Add(value); - if (_expandingAccumulator is null) - { - _expandingAccumulator = new AdaptiveCapacityDictionary>(capacity: 5, StringComparer.OrdinalIgnoreCase); - } + _expandingAccumulator[key] = list; + } + else + { + // The marker indicates we are in the expanding accumulator, so just append to the list. + _expandingAccumulator[key].Add(value); + } + } - // Already 2 (1 existing + the new one) entries so use List's expansion mechanism for more - var list = new List(); + /// + /// This API supports infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public bool HasValues => ValueCount > 0; - list.AddRange(values); - list.Add(value); + /// + /// This API supports infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public int KeyCount => _accumulator?.Count ?? 0; - _expandingAccumulator[key] = list; - } - else - { - // The marker indicates we are in the expanding accumulator, so just append to the list. - _expandingAccumulator[key].Add(value); - } - } + /// + /// This API supports infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public int ValueCount { get; private set; } - /// - /// This API supports infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public bool HasValues => ValueCount > 0; - - /// - /// This API supports infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public int KeyCount => _accumulator?.Count ?? 0; - - /// - /// This API supports infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public int ValueCount { get; private set; } - - /// - /// This API supports infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public AdaptiveCapacityDictionary GetResults() + /// + /// This API supports infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public AdaptiveCapacityDictionary GetResults() + { + if (_expandingAccumulator != null) { - if (_expandingAccumulator != null) + // Coalesce count 3+ multi-value entries into _accumulator dictionary + foreach (var entry in _expandingAccumulator) { - // Coalesce count 3+ multi-value entries into _accumulator dictionary - foreach (var entry in _expandingAccumulator) - { - _accumulator[entry.Key] = new StringValues(entry.Value.ToArray()); - } + _accumulator[entry.Key] = new StringValues(entry.Value.ToArray()); } - - return _accumulator ?? new AdaptiveCapacityDictionary(0, StringComparer.OrdinalIgnoreCase); } + + return _accumulator ?? new AdaptiveCapacityDictionary(0, StringComparer.OrdinalIgnoreCase); } } } diff --git a/src/Http/Http/src/Features/RequestBodyPipeFeature.cs b/src/Http/Http/src/Features/RequestBodyPipeFeature.cs index a9aa3e8db5..0b546d0a04 100644 --- a/src/Http/Http/src/Features/RequestBodyPipeFeature.cs +++ b/src/Http/Http/src/Features/RequestBodyPipeFeature.cs @@ -6,50 +6,49 @@ using System.IO; using System.IO.Pipelines; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Default implementation for . +/// +public class RequestBodyPipeFeature : IRequestBodyPipeFeature { + private PipeReader? _internalPipeReader; + private Stream? _streamInstanceWhenWrapped; + private readonly HttpContext _context; + /// - /// Default implementation for . + /// Initializes a new instance of . /// - public class RequestBodyPipeFeature : IRequestBodyPipeFeature + /// + public RequestBodyPipeFeature(HttpContext context) { - private PipeReader? _internalPipeReader; - private Stream? _streamInstanceWhenWrapped; - private readonly HttpContext _context; - - /// - /// Initializes a new instance of . - /// - /// - public RequestBodyPipeFeature(HttpContext context) + if (context == null) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - _context = context; + throw new ArgumentNullException(nameof(context)); } + _context = context; + } - /// - public PipeReader Reader + /// + public PipeReader Reader + { + get { - get + if (_internalPipeReader == null || + !ReferenceEquals(_streamInstanceWhenWrapped, _context.Request.Body)) { - if (_internalPipeReader == null || - !ReferenceEquals(_streamInstanceWhenWrapped, _context.Request.Body)) - { - _streamInstanceWhenWrapped = _context.Request.Body; - _internalPipeReader = PipeReader.Create(_context.Request.Body); - - _context.Response.OnCompleted((self) => - { - ((PipeReader)self).Complete(); - return Task.CompletedTask; - }, _internalPipeReader); - } + _streamInstanceWhenWrapped = _context.Request.Body; + _internalPipeReader = PipeReader.Create(_context.Request.Body); - return _internalPipeReader; + _context.Response.OnCompleted((self) => + { + ((PipeReader)self).Complete(); + return Task.CompletedTask; + }, _internalPipeReader); } + + return _internalPipeReader; } } } diff --git a/src/Http/Http/src/Features/RequestCookiesFeature.cs b/src/Http/Http/src/Features/RequestCookiesFeature.cs index 6adb9a2c62..9add9ab21d 100644 --- a/src/Http/Http/src/Features/RequestCookiesFeature.cs +++ b/src/Http/Http/src/Features/RequestCookiesFeature.cs @@ -6,96 +6,95 @@ using System.Collections.Generic; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Default implementation for . +/// +public class RequestCookiesFeature : IRequestCookiesFeature { + // Lambda hoisted to static readonly field to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private static readonly Func _nullRequestFeature = f => null; + + private FeatureReferences _features; + private StringValues _original; + private IRequestCookieCollection? _parsedValues; + /// - /// Default implementation for . + /// Initializes a new instance of . /// - public class RequestCookiesFeature : IRequestCookiesFeature + /// The to use as backing store. + public RequestCookiesFeature(IRequestCookieCollection cookies) { - // Lambda hoisted to static readonly field to improve inlining https://github.com/dotnet/roslyn/issues/13624 - private static readonly Func _nullRequestFeature = f => null; - - private FeatureReferences _features; - private StringValues _original; - private IRequestCookieCollection? _parsedValues; - - /// - /// Initializes a new instance of . - /// - /// The to use as backing store. - public RequestCookiesFeature(IRequestCookieCollection cookies) + if (cookies == null) { - if (cookies == null) - { - throw new ArgumentNullException(nameof(cookies)); - } - - _parsedValues = cookies; + throw new ArgumentNullException(nameof(cookies)); } - /// - /// Initializes a new instance of . - /// - /// The to initialize. - public RequestCookiesFeature(IFeatureCollection features) - { - if (features == null) - { - throw new ArgumentNullException(nameof(features)); - } + _parsedValues = cookies; + } - _features.Initalize(features); + /// + /// Initializes a new instance of . + /// + /// The to initialize. + public RequestCookiesFeature(IFeatureCollection features) + { + if (features == null) + { + throw new ArgumentNullException(nameof(features)); } - private IHttpRequestFeature HttpRequestFeature => - _features.Fetch(ref _features.Cache, _nullRequestFeature)!; + _features.Initalize(features); + } + + private IHttpRequestFeature HttpRequestFeature => + _features.Fetch(ref _features.Cache, _nullRequestFeature)!; - /// - public IRequestCookieCollection Cookies + /// + public IRequestCookieCollection Cookies + { + get { - get + if (_features.Collection == null) { - if (_features.Collection == null) + if (_parsedValues == null) { - if (_parsedValues == null) - { - _parsedValues = RequestCookieCollection.Empty; - } - return _parsedValues; + _parsedValues = RequestCookieCollection.Empty; } + return _parsedValues; + } - var headers = HttpRequestFeature.Headers; - var current = headers.Cookie; - - if (_parsedValues == null || _original != current) - { - _original = current; - _parsedValues = RequestCookieCollection.Parse(current); - } + var headers = HttpRequestFeature.Headers; + var current = headers.Cookie; - return _parsedValues; + if (_parsedValues == null || _original != current) + { + _original = current; + _parsedValues = RequestCookieCollection.Parse(current); } - set + + return _parsedValues; + } + set + { + _parsedValues = value; + _original = StringValues.Empty; + if (_features.Collection != null) { - _parsedValues = value; - _original = StringValues.Empty; - if (_features.Collection != null) + if (_parsedValues == null || _parsedValues.Count == 0) { - if (_parsedValues == null || _parsedValues.Count == 0) - { - HttpRequestFeature.Headers.Cookie = default; - } - else + HttpRequestFeature.Headers.Cookie = default; + } + else + { + var headers = new List(_parsedValues.Count); + foreach (var pair in _parsedValues) { - var headers = new List(_parsedValues.Count); - foreach (var pair in _parsedValues) - { - headers.Add(new CookieHeaderValue(pair.Key, pair.Value).ToString()); - } - _original = headers.ToArray(); - HttpRequestFeature.Headers.Cookie = _original; + headers.Add(new CookieHeaderValue(pair.Key, pair.Value).ToString()); } + _original = headers.ToArray(); + HttpRequestFeature.Headers.Cookie = _original; } } } diff --git a/src/Http/Http/src/Features/RequestServicesFeature.cs b/src/Http/Http/src/Features/RequestServicesFeature.cs index b670170c6e..0a29155c10 100644 --- a/src/Http/Http/src/Features/RequestServicesFeature.cs +++ b/src/Http/Http/src/Features/RequestServicesFeature.cs @@ -5,89 +5,88 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// An implementation for for accessing request services. +/// +public class RequestServicesFeature : IServiceProvidersFeature, IDisposable, IAsyncDisposable { + private readonly IServiceScopeFactory? _scopeFactory; + private IServiceProvider? _requestServices; + private IServiceScope? _scope; + private bool _requestServicesSet; + private readonly HttpContext _context; + /// - /// An implementation for for accessing request services. + /// Initializes a new instance of . /// - public class RequestServicesFeature : IServiceProvidersFeature, IDisposable, IAsyncDisposable + /// The . + /// The . + public RequestServicesFeature(HttpContext context, IServiceScopeFactory? scopeFactory) { - private readonly IServiceScopeFactory? _scopeFactory; - private IServiceProvider? _requestServices; - private IServiceScope? _scope; - private bool _requestServicesSet; - private readonly HttpContext _context; - - /// - /// Initializes a new instance of . - /// - /// The . - /// The . - public RequestServicesFeature(HttpContext context, IServiceScopeFactory? scopeFactory) - { - _context = context; - _scopeFactory = scopeFactory; - } + _context = context; + _scopeFactory = scopeFactory; + } - /// - public IServiceProvider RequestServices + /// + public IServiceProvider RequestServices + { + get { - get + if (!_requestServicesSet && _scopeFactory != null) { - if (!_requestServicesSet && _scopeFactory != null) - { - _context.Response.RegisterForDisposeAsync(this); - _scope = _scopeFactory.CreateScope(); - _requestServices = _scope.ServiceProvider; - _requestServicesSet = true; - } - return _requestServices!; - } - - set - { - _requestServices = value; + _context.Response.RegisterForDisposeAsync(this); + _scope = _scopeFactory.CreateScope(); + _requestServices = _scope.ServiceProvider; _requestServicesSet = true; } + return _requestServices!; } - /// - public ValueTask DisposeAsync() + set { - switch (_scope) - { - case IAsyncDisposable asyncDisposable: - var vt = asyncDisposable.DisposeAsync(); - if (!vt.IsCompletedSuccessfully) - { - return Awaited(this, vt); - } - // If its a IValueTaskSource backed ValueTask, - // inform it its result has been read so it can reset - vt.GetAwaiter().GetResult(); - break; - case IDisposable disposable: - disposable.Dispose(); - break; - } + _requestServices = value; + _requestServicesSet = true; + } + } - _scope = null; - _requestServices = null; + /// + public ValueTask DisposeAsync() + { + switch (_scope) + { + case IAsyncDisposable asyncDisposable: + var vt = asyncDisposable.DisposeAsync(); + if (!vt.IsCompletedSuccessfully) + { + return Awaited(this, vt); + } + // If its a IValueTaskSource backed ValueTask, + // inform it its result has been read so it can reset + vt.GetAwaiter().GetResult(); + break; + case IDisposable disposable: + disposable.Dispose(); + break; + } - return default; + _scope = null; + _requestServices = null; - static async ValueTask Awaited(RequestServicesFeature servicesFeature, ValueTask vt) - { - await vt; - servicesFeature._scope = null; - servicesFeature._requestServices = null; - } - } + return default; - /// - public void Dispose() + static async ValueTask Awaited(RequestServicesFeature servicesFeature, ValueTask vt) { - DisposeAsync().AsTask().GetAwaiter().GetResult(); + await vt; + servicesFeature._scope = null; + servicesFeature._requestServices = null; } } + + /// + public void Dispose() + { + DisposeAsync().AsTask().GetAwaiter().GetResult(); + } } diff --git a/src/Http/Http/src/Features/ResponseCookiesFeature.cs b/src/Http/Http/src/Features/ResponseCookiesFeature.cs index 8e4da199fe..ff4cdd121b 100644 --- a/src/Http/Http/src/Features/ResponseCookiesFeature.cs +++ b/src/Http/Http/src/Features/ResponseCookiesFeature.cs @@ -5,54 +5,53 @@ using System; using System.Text; using Microsoft.Extensions.ObjectPool; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Default implementation of . +/// +public class ResponseCookiesFeature : IResponseCookiesFeature { + private readonly IFeatureCollection _features; + private IResponseCookies? _cookiesCollection; + /// - /// Default implementation of . + /// Initializes a new instance. /// - public class ResponseCookiesFeature : IResponseCookiesFeature + /// + /// containing all defined features, including this + /// and the . + /// + public ResponseCookiesFeature(IFeatureCollection features) { - private readonly IFeatureCollection _features; - private IResponseCookies? _cookiesCollection; - - /// - /// Initializes a new instance. - /// - /// - /// containing all defined features, including this - /// and the . - /// - public ResponseCookiesFeature(IFeatureCollection features) - { - _features = features ?? throw new ArgumentNullException(nameof(features)); - } + _features = features ?? throw new ArgumentNullException(nameof(features)); + } - /// - /// Initializes a new instance. - /// - /// - /// containing all defined features, including this - /// and the . - /// - /// The , if available. - [Obsolete("This constructor is obsolete and will be removed in a future version.")] - public ResponseCookiesFeature(IFeatureCollection features, ObjectPool? builderPool) - { - _features = features ?? throw new ArgumentNullException(nameof(features)); - } + /// + /// Initializes a new instance. + /// + /// + /// containing all defined features, including this + /// and the . + /// + /// The , if available. + [Obsolete("This constructor is obsolete and will be removed in a future version.")] + public ResponseCookiesFeature(IFeatureCollection features, ObjectPool? builderPool) + { + _features = features ?? throw new ArgumentNullException(nameof(features)); + } - /// - public IResponseCookies Cookies + /// + public IResponseCookies Cookies + { + get { - get + if (_cookiesCollection == null) { - if (_cookiesCollection == null) - { - _cookiesCollection = new ResponseCookies(_features); - } - - return _cookiesCollection; + _cookiesCollection = new ResponseCookies(_features); } + + return _cookiesCollection; } } } diff --git a/src/Http/Http/src/Features/RouteValuesFeature.cs b/src/Http/Http/src/Features/RouteValuesFeature.cs index bca9a66c54..e97df5b8e4 100644 --- a/src/Http/Http/src/Features/RouteValuesFeature.cs +++ b/src/Http/Http/src/Features/RouteValuesFeature.cs @@ -3,32 +3,31 @@ using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// A feature for routing values. Use +/// to access the values associated with the current request. +/// +public class RouteValuesFeature : IRouteValuesFeature { + private RouteValueDictionary? _routeValues; + /// - /// A feature for routing values. Use - /// to access the values associated with the current request. + /// Gets or sets the associated with the currrent + /// request. /// - public class RouteValuesFeature : IRouteValuesFeature + public RouteValueDictionary RouteValues { - private RouteValueDictionary? _routeValues; - - /// - /// Gets or sets the associated with the currrent - /// request. - /// - public RouteValueDictionary RouteValues + get { - get + if (_routeValues == null) { - if (_routeValues == null) - { - _routeValues = new RouteValueDictionary(); - } - - return _routeValues; + _routeValues = new RouteValueDictionary(); } - set => _routeValues = value; + + return _routeValues; } + set => _routeValues = value; } } diff --git a/src/Http/Http/src/Features/ServiceProvidersFeature.cs b/src/Http/Http/src/Features/ServiceProvidersFeature.cs index ee92def94a..a687137bcc 100644 --- a/src/Http/Http/src/Features/ServiceProvidersFeature.cs +++ b/src/Http/Http/src/Features/ServiceProvidersFeature.cs @@ -3,14 +3,13 @@ using System; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Default implementation for . +/// +public class ServiceProvidersFeature : IServiceProvidersFeature { - /// - /// Default implementation for . - /// - public class ServiceProvidersFeature : IServiceProvidersFeature - { - /// - public IServiceProvider RequestServices { get; set; } = default!; - } + /// + public IServiceProvider RequestServices { get; set; } = default!; } diff --git a/src/Http/Http/src/Features/TlsConnectionFeature.cs b/src/Http/Http/src/Features/TlsConnectionFeature.cs index a11afe3881..0f52f65f84 100644 --- a/src/Http/Http/src/Features/TlsConnectionFeature.cs +++ b/src/Http/Http/src/Features/TlsConnectionFeature.cs @@ -5,20 +5,19 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +/// +/// Default implementation for . +/// +public class TlsConnectionFeature : ITlsConnectionFeature { - /// - /// Default implementation for . - /// - public class TlsConnectionFeature : ITlsConnectionFeature - { - /// - public X509Certificate2? ClientCertificate { get; set; } + /// + public X509Certificate2? ClientCertificate { get; set; } - /// - public Task GetClientCertificateAsync(CancellationToken cancellationToken) - { - return Task.FromResult(ClientCertificate); - } + /// + public Task GetClientCertificateAsync(CancellationToken cancellationToken) + { + return Task.FromResult(ClientCertificate); } } diff --git a/src/Http/Http/src/FormCollection.cs b/src/Http/Http/src/FormCollection.cs index 2ec522f7aa..aeef7c09dd 100644 --- a/src/Http/Http/src/FormCollection.cs +++ b/src/Http/Http/src/FormCollection.cs @@ -6,230 +6,229 @@ using System.Collections; using System.Collections.Generic; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Contains the parsed HTTP form values. +/// +public class FormCollection : IFormCollection { /// - /// Contains the parsed HTTP form values. + /// An empty . /// - public class FormCollection : IFormCollection - { - /// - /// An empty . - /// - public static readonly FormCollection Empty = new FormCollection(); - private static readonly string[] EmptyKeys = Array.Empty(); + public static readonly FormCollection Empty = new FormCollection(); + private static readonly string[] EmptyKeys = Array.Empty(); - // Pre-box - private static readonly IEnumerator> EmptyIEnumeratorType = default(Enumerator); - private static readonly IEnumerator EmptyIEnumerator = default(Enumerator); + // Pre-box + private static readonly IEnumerator> EmptyIEnumeratorType = default(Enumerator); + private static readonly IEnumerator EmptyIEnumerator = default(Enumerator); - private static readonly IFormFileCollection EmptyFiles = new FormFileCollection(); + private static readonly IFormFileCollection EmptyFiles = new FormFileCollection(); - private IFormFileCollection? _files; + private IFormFileCollection? _files; - private FormCollection() - { - // For static Empty - } + private FormCollection() + { + // For static Empty + } - /// - /// Initializes a new instance of . - /// - /// The backing fields. - /// The files associated with the form. - public FormCollection(Dictionary? fields, IFormFileCollection? files = null) - { - // can be null - Store = fields; - _files = files; - } + /// + /// Initializes a new instance of . + /// + /// The backing fields. + /// The files associated with the form. + public FormCollection(Dictionary? fields, IFormFileCollection? files = null) + { + // can be null + Store = fields; + _files = files; + } - /// - /// Gets the files associated with the HTTP form. - /// - public IFormFileCollection Files - { - get => _files ?? EmptyFiles; - private set => _files = value; - } + /// + /// Gets the files associated with the HTTP form. + /// + public IFormFileCollection Files + { + get => _files ?? EmptyFiles; + private set => _files = value; + } - private Dictionary? Store { get; set; } + private Dictionary? Store { get; set; } - /// - /// Get or sets the associated value from the collection as a single string. - /// - /// The header name. - /// the associated value from the collection as a - /// or if the key is not present. - public StringValues this[string key] + /// + /// Get or sets the associated value from the collection as a single string. + /// + /// The header name. + /// the associated value from the collection as a + /// or if the key is not present. + public StringValues this[string key] + { + get { - get + if (Store == null) { - if (Store == null) - { - return StringValues.Empty; - } - - if (TryGetValue(key, out var value)) - { - return value; - } return StringValues.Empty; } - } - /// - public int Count - { - get + if (TryGetValue(key, out var value)) { - return Store?.Count ?? 0; + return value; } + return StringValues.Empty; } + } - /// - public ICollection Keys + /// + public int Count + { + get { - get - { - if (Store == null) - { - return EmptyKeys; - } - return Store.Keys; - } + return Store?.Count ?? 0; } + } - /// - public bool ContainsKey(string key) + /// + public ICollection Keys + { + get { if (Store == null) { - return false; + return EmptyKeys; } - return Store.ContainsKey(key); + return Store.Keys; } + } - /// - public bool TryGetValue(string key, out StringValues value) + /// + public bool ContainsKey(string key) + { + if (Store == null) { - if (Store == null) - { - value = default(StringValues); - return false; - } - return Store.TryGetValue(key, out value); + return false; } + return Store.ContainsKey(key); + } - /// - /// Returns an struct enumerator that iterates through a collection without boxing and - /// is also used via the interface. - /// - /// An object that can be used to iterate through the collection. - public Enumerator GetEnumerator() + /// + public bool TryGetValue(string key, out StringValues value) + { + if (Store == null) + { + value = default(StringValues); + return false; + } + return Store.TryGetValue(key, out value); + } + + /// + /// Returns an struct enumerator that iterates through a collection without boxing and + /// is also used via the interface. + /// + /// An object that can be used to iterate through the collection. + public Enumerator GetEnumerator() + { + if (Store == null || Store.Count == 0) { - if (Store == null || Store.Count == 0) - { - // Non-boxed Enumerator - return default; - } // Non-boxed Enumerator - return new Enumerator(Store.GetEnumerator()); + return default; } + // Non-boxed Enumerator + return new Enumerator(Store.GetEnumerator()); + } - /// - /// Returns an enumerator that iterates through a collection, boxes in non-empty path. - /// - /// An object that can be used to iterate through the collection. - IEnumerator> IEnumerable>.GetEnumerator() + /// + /// Returns an enumerator that iterates through a collection, boxes in non-empty path. + /// + /// An object that can be used to iterate through the collection. + IEnumerator> IEnumerable>.GetEnumerator() + { + if (Store == null || Store.Count == 0) { - if (Store == null || Store.Count == 0) - { - // Non-boxed Enumerator - return EmptyIEnumeratorType; - } - // Boxed Enumerator - return Store.GetEnumerator(); + // Non-boxed Enumerator + return EmptyIEnumeratorType; + } + // Boxed Enumerator + return Store.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection, boxes in non-empty path. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumerator; + } + // Boxed Enumerator + return Store.GetEnumerator(); + } + + /// + /// Enumerates a . + /// + public struct Enumerator : IEnumerator> + { + // Do NOT make this readonly, or MoveNext will not work + private Dictionary.Enumerator _dictionaryEnumerator; + private readonly bool _notEmpty; + + internal Enumerator(Dictionary.Enumerator dictionaryEnumerator) + { + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; } /// - /// Returns an enumerator that iterates through a collection, boxes in non-empty path. + /// Advances the enumerator to the next element of the . /// - /// An object that can be used to iterate through the collection. - IEnumerator IEnumerable.GetEnumerator() + /// if the enumerator was successfully advanced to the next element; + /// if the enumerator has passed the end of the collection. + public bool MoveNext() { - if (Store == null || Store.Count == 0) + if (_notEmpty) { - // Non-boxed Enumerator - return EmptyIEnumerator; + return _dictionaryEnumerator.MoveNext(); } - // Boxed Enumerator - return Store.GetEnumerator(); + return false; } /// - /// Enumerates a . + /// Gets the element at the current position of the enumerator. /// - public struct Enumerator : IEnumerator> + public KeyValuePair Current { - // Do NOT make this readonly, or MoveNext will not work - private Dictionary.Enumerator _dictionaryEnumerator; - private readonly bool _notEmpty; - - internal Enumerator(Dictionary.Enumerator dictionaryEnumerator) - { - _dictionaryEnumerator = dictionaryEnumerator; - _notEmpty = true; - } - - /// - /// Advances the enumerator to the next element of the . - /// - /// if the enumerator was successfully advanced to the next element; - /// if the enumerator has passed the end of the collection. - public bool MoveNext() + get { if (_notEmpty) { - return _dictionaryEnumerator.MoveNext(); - } - return false; - } - - /// - /// Gets the element at the current position of the enumerator. - /// - public KeyValuePair Current - { - get - { - if (_notEmpty) - { - return _dictionaryEnumerator.Current; - } - return default; + return _dictionaryEnumerator.Current; } + return default; } + } - /// - public void Dispose() - { - } + /// + public void Dispose() + { + } - object IEnumerator.Current + object IEnumerator.Current + { + get { - get - { - return Current; - } + return Current; } + } - void IEnumerator.Reset() + void IEnumerator.Reset() + { + if (_notEmpty) { - if (_notEmpty) - { - ((IEnumerator)_dictionaryEnumerator).Reset(); - } + ((IEnumerator)_dictionaryEnumerator).Reset(); } } } diff --git a/src/Http/Http/src/FormFile.cs b/src/Http/Http/src/FormFile.cs index b7b78bed11..ff57e28cde 100644 --- a/src/Http/Http/src/FormFile.cs +++ b/src/Http/Http/src/FormFile.cs @@ -1,115 +1,114 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Default implementation of . +/// +public class FormFile : IFormFile { + // Stream.CopyTo method uses 80KB as the default buffer size. + private const int DefaultBufferSize = 80 * 1024; + + private readonly Stream _baseStream; + private readonly long _baseStreamOffset; + /// - /// Default implementation of . + /// Initializes a new instance of . /// - public class FormFile : IFormFile + /// The containing the form file. + /// The offset at which the form file begins. + /// The length of the form file. + /// The name of the form file from the Content-Disposition header. + /// The file name from the Content-Disposition header. + public FormFile(Stream baseStream, long baseStreamOffset, long length, string name, string fileName) { - // Stream.CopyTo method uses 80KB as the default buffer size. - private const int DefaultBufferSize = 80 * 1024; - - private readonly Stream _baseStream; - private readonly long _baseStreamOffset; + _baseStream = baseStream; + _baseStreamOffset = baseStreamOffset; + Length = length; + Name = name; + FileName = fileName; + } - /// - /// Initializes a new instance of . - /// - /// The containing the form file. - /// The offset at which the form file begins. - /// The length of the form file. - /// The name of the form file from the Content-Disposition header. - /// The file name from the Content-Disposition header. - public FormFile(Stream baseStream, long baseStreamOffset, long length, string name, string fileName) - { - _baseStream = baseStream; - _baseStreamOffset = baseStreamOffset; - Length = length; - Name = name; - FileName = fileName; - } + /// + /// Gets the raw Content-Disposition header of the uploaded file. + /// + public string ContentDisposition + { + get { return Headers.ContentDisposition.ToString(); } + set { Headers.ContentDisposition = value; } + } - /// - /// Gets the raw Content-Disposition header of the uploaded file. - /// - public string ContentDisposition - { - get { return Headers.ContentDisposition.ToString(); } - set { Headers.ContentDisposition = value; } - } + /// + /// Gets the raw Content-Type header of the uploaded file. + /// + public string ContentType + { + get { return Headers.ContentType.ToString(); } + set { Headers.ContentType = value; } + } - /// - /// Gets the raw Content-Type header of the uploaded file. - /// - public string ContentType - { - get { return Headers.ContentType.ToString(); } - set { Headers.ContentType = value; } - } + /// + /// Gets the header dictionary of the uploaded file. + /// + public IHeaderDictionary Headers { get; set; } = default!; - /// - /// Gets the header dictionary of the uploaded file. - /// - public IHeaderDictionary Headers { get; set; } = default!; + /// + /// Gets the file length in bytes. + /// + public long Length { get; } - /// - /// Gets the file length in bytes. - /// - public long Length { get; } + /// + /// Gets the name from the Content-Disposition header. + /// + public string Name { get; } - /// - /// Gets the name from the Content-Disposition header. - /// - public string Name { get; } + /// + /// Gets the file name from the Content-Disposition header. + /// + public string FileName { get; } - /// - /// Gets the file name from the Content-Disposition header. - /// - public string FileName { get; } + /// + /// Opens the request stream for reading the uploaded file. + /// + public Stream OpenReadStream() + { + return new ReferenceReadStream(_baseStream, _baseStreamOffset, Length); + } - /// - /// Opens the request stream for reading the uploaded file. - /// - public Stream OpenReadStream() + /// + /// Copies the contents of the uploaded file to the stream. + /// + /// The stream to copy the file contents to. + public void CopyTo(Stream target) + { + if (target == null) { - return new ReferenceReadStream(_baseStream, _baseStreamOffset, Length); + throw new ArgumentNullException(nameof(target)); } - /// - /// Copies the contents of the uploaded file to the stream. - /// - /// The stream to copy the file contents to. - public void CopyTo(Stream target) + using (var readStream = OpenReadStream()) { - if (target == null) - { - throw new ArgumentNullException(nameof(target)); - } - - using (var readStream = OpenReadStream()) - { - readStream.CopyTo(target, DefaultBufferSize); - } + readStream.CopyTo(target, DefaultBufferSize); } + } - /// - /// Asynchronously copies the contents of the uploaded file to the stream. - /// - /// The stream to copy the file contents to. - /// - public async Task CopyToAsync(Stream target, CancellationToken cancellationToken = default(CancellationToken)) + /// + /// Asynchronously copies the contents of the uploaded file to the stream. + /// + /// The stream to copy the file contents to. + /// + public async Task CopyToAsync(Stream target, CancellationToken cancellationToken = default(CancellationToken)) + { + if (target == null) { - if (target == null) - { - throw new ArgumentNullException(nameof(target)); - } + throw new ArgumentNullException(nameof(target)); + } - using (var readStream = OpenReadStream()) - { - await readStream.CopyToAsync(target, DefaultBufferSize, cancellationToken); - } + using (var readStream = OpenReadStream()) + { + await readStream.CopyToAsync(target, DefaultBufferSize, cancellationToken); } } } diff --git a/src/Http/Http/src/FormFileCollection.cs b/src/Http/Http/src/FormFileCollection.cs index 0e3e460e91..f38fbba16c 100644 --- a/src/Http/Http/src/FormFileCollection.cs +++ b/src/Http/Http/src/FormFileCollection.cs @@ -4,44 +4,43 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Default implementation of . +/// +public class FormFileCollection : List, IFormFileCollection { - /// - /// Default implementation of . - /// - public class FormFileCollection : List, IFormFileCollection - { - /// - public IFormFile? this[string name] => GetFile(name); + /// + public IFormFile? this[string name] => GetFile(name); - /// - public IFormFile? GetFile(string name) + /// + public IFormFile? GetFile(string name) + { + foreach (var file in this) { - foreach (var file in this) + if (string.Equals(name, file.Name, StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(name, file.Name, StringComparison.OrdinalIgnoreCase)) - { - return file; - } + return file; } - - return null; } - /// - public IReadOnlyList GetFiles(string name) - { - var files = new List(); + return null; + } - foreach (var file in this) + /// + public IReadOnlyList GetFiles(string name) + { + var files = new List(); + + foreach (var file in this) + { + if (string.Equals(name, file.Name, StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(name, file.Name, StringComparison.OrdinalIgnoreCase)) - { - files.Add(file); - } + files.Add(file); } - - return files; } + + return files; } } diff --git a/src/Http/Http/src/HeaderDictionary.cs b/src/Http/Http/src/HeaderDictionary.cs index 4cda2646ba..a1cb5db23a 100644 --- a/src/Http/Http/src/HeaderDictionary.cs +++ b/src/Http/Http/src/HeaderDictionary.cs @@ -8,430 +8,429 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Represents a wrapper for RequestHeaders and ResponseHeaders. +/// +public class HeaderDictionary : IHeaderDictionary { + private static readonly string[] EmptyKeys = Array.Empty(); + private static readonly StringValues[] EmptyValues = Array.Empty(); + // Pre-box + private static readonly IEnumerator> EmptyIEnumeratorType = default(Enumerator); + private static readonly IEnumerator EmptyIEnumerator = default(Enumerator); + /// - /// Represents a wrapper for RequestHeaders and ResponseHeaders. + /// Initializes a new instance of . /// - public class HeaderDictionary : IHeaderDictionary + public HeaderDictionary() { - private static readonly string[] EmptyKeys = Array.Empty(); - private static readonly StringValues[] EmptyValues = Array.Empty(); - // Pre-box - private static readonly IEnumerator> EmptyIEnumeratorType = default(Enumerator); - private static readonly IEnumerator EmptyIEnumerator = default(Enumerator); + } - /// - /// Initializes a new instance of . - /// - public HeaderDictionary() - { - } + /// + /// Initializes a new instance of . + /// + /// The value to use as the backing store. + public HeaderDictionary(Dictionary? store) + { + Store = store; + } - /// - /// Initializes a new instance of . - /// - /// The value to use as the backing store. - public HeaderDictionary(Dictionary? store) - { - Store = store; - } + /// + /// Initializes a new instance of . + /// + /// The initial number of headers that this instance can contain. + public HeaderDictionary(int capacity) + { + EnsureStore(capacity); + } - /// - /// Initializes a new instance of . - /// - /// The initial number of headers that this instance can contain. - public HeaderDictionary(int capacity) + private Dictionary? Store { get; set; } + + [MemberNotNull(nameof(Store))] + private void EnsureStore(int capacity) + { + if (Store == null) { - EnsureStore(capacity); + Store = new Dictionary(capacity, StringComparer.OrdinalIgnoreCase); } + } - private Dictionary? Store { get; set; } - - [MemberNotNull(nameof(Store))] - private void EnsureStore(int capacity) + /// + /// Get or sets the associated value from the collection as a single string. + /// + /// The header name. + /// the associated value from the collection as a StringValues or StringValues.Empty if the key is not present. + public StringValues this[string key] + { + get { if (Store == null) { - Store = new Dictionary(capacity, StringComparer.OrdinalIgnoreCase); + return StringValues.Empty; } - } - /// - /// Get or sets the associated value from the collection as a single string. - /// - /// The header name. - /// the associated value from the collection as a StringValues or StringValues.Empty if the key is not present. - public StringValues this[string key] + if (TryGetValue(key, out var value)) + { + return value; + } + return StringValues.Empty; + } + set { - get + if (key == null) { - if (Store == null) - { - return StringValues.Empty; - } + throw new ArgumentNullException(nameof(key)); + } + ThrowIfReadOnly(); - if (TryGetValue(key, out var value)) - { - return value; - } - return StringValues.Empty; + if (value.Count == 0) + { + Store?.Remove(key); } - set + else { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } - ThrowIfReadOnly(); - - if (value.Count == 0) - { - Store?.Remove(key); - } - else - { - EnsureStore(1); - Store[key] = value; - } + EnsureStore(1); + Store[key] = value; } } + } - StringValues IDictionary.this[string key] + StringValues IDictionary.this[string key] + { + get { return this[key]; } + set { - get { return this[key]; } - set - { - ThrowIfReadOnly(); - this[key] = value; - } + ThrowIfReadOnly(); + this[key] = value; } + } - /// - public long? ContentLength + /// + public long? ContentLength + { + get { - get + long value; + var rawValue = this[HeaderNames.ContentLength]; + if (rawValue.Count == 1 && + !string.IsNullOrEmpty(rawValue[0]) && + HeaderUtilities.TryParseNonNegativeInt64(new StringSegment(rawValue[0]).Trim(), out value)) { - long value; - var rawValue = this[HeaderNames.ContentLength]; - if (rawValue.Count == 1 && - !string.IsNullOrEmpty(rawValue[0]) && - HeaderUtilities.TryParseNonNegativeInt64(new StringSegment(rawValue[0]).Trim(), out value)) - { - return value; - } + return value; + } - return null; + return null; + } + set + { + ThrowIfReadOnly(); + if (value.HasValue) + { + this[HeaderNames.ContentLength] = HeaderUtilities.FormatNonNegativeInt64(value.GetValueOrDefault()); } - set + else { - ThrowIfReadOnly(); - if (value.HasValue) - { - this[HeaderNames.ContentLength] = HeaderUtilities.FormatNonNegativeInt64(value.GetValueOrDefault()); - } - else - { - this.Remove(HeaderNames.ContentLength); - } + this.Remove(HeaderNames.ContentLength); } } + } - /// - /// Gets the number of elements contained in the ;. - /// - /// The number of elements contained in the . - public int Count => Store?.Count ?? 0; + /// + /// Gets the number of elements contained in the ;. + /// + /// The number of elements contained in the . + public int Count => Store?.Count ?? 0; - /// - /// Gets a value that indicates whether the is in read-only mode. - /// - /// true if the is in read-only mode; otherwise, false. - public bool IsReadOnly { get; set; } + /// + /// Gets a value that indicates whether the is in read-only mode. + /// + /// true if the is in read-only mode; otherwise, false. + public bool IsReadOnly { get; set; } - /// - /// Gets the collection of HTTP header names in this instance. - /// - public ICollection Keys + /// + /// Gets the collection of HTTP header names in this instance. + /// + public ICollection Keys + { + get { - get + if (Store == null) { - if (Store == null) - { - return EmptyKeys; - } - return Store.Keys; + return EmptyKeys; } + return Store.Keys; } + } - /// - /// Gets the collection of HTTP header values in this instance. - /// - public ICollection Values + /// + /// Gets the collection of HTTP header values in this instance. + /// + public ICollection Values + { + get { - get + if (Store == null) { - if (Store == null) - { - return EmptyValues; - } - return Store.Values; + return EmptyValues; } + return Store.Values; } + } - /// - /// Adds a new header item to the collection. - /// - /// The item to add. - public void Add(KeyValuePair item) + /// + /// Adds a new header item to the collection. + /// + /// The item to add. + public void Add(KeyValuePair item) + { + if (item.Key == null) { - if (item.Key == null) - { - throw new ArgumentException("The key is null"); - } - ThrowIfReadOnly(); - EnsureStore(1); - Store.Add(item.Key, item.Value); + throw new ArgumentException("The key is null"); } + ThrowIfReadOnly(); + EnsureStore(1); + Store.Add(item.Key, item.Value); + } - /// - /// Adds the given header and values to the collection. - /// - /// The header name. - /// The header values. - public void Add(string key, StringValues value) + /// + /// Adds the given header and values to the collection. + /// + /// The header name. + /// The header values. + public void Add(string key, StringValues value) + { + if (key == null) { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } - ThrowIfReadOnly(); - EnsureStore(1); - Store.Add(key, value); + throw new ArgumentNullException(nameof(key)); } + ThrowIfReadOnly(); + EnsureStore(1); + Store.Add(key, value); + } - /// - /// Clears the entire list of objects. - /// - public void Clear() + /// + /// Clears the entire list of objects. + /// + public void Clear() + { + ThrowIfReadOnly(); + Store?.Clear(); + } + + /// + /// Returns a value indicating whether the specified object occurs within this collection. + /// + /// The item. + /// true if the specified object occurs within this collection; otherwise, false. + public bool Contains(KeyValuePair item) + { + if (Store == null || + !Store.TryGetValue(item.Key, out var value) || + !StringValues.Equals(value, item.Value)) { - ThrowIfReadOnly(); - Store?.Clear(); + return false; } + return true; + } - /// - /// Returns a value indicating whether the specified object occurs within this collection. - /// - /// The item. - /// true if the specified object occurs within this collection; otherwise, false. - public bool Contains(KeyValuePair item) + /// + /// Determines whether the contains a specific key. + /// + /// The key. + /// true if the contains a specific key; otherwise, false. + public bool ContainsKey(string key) + { + if (Store == null) { - if (Store == null || - !Store.TryGetValue(item.Key, out var value) || - !StringValues.Equals(value, item.Value)) - { - return false; - } - return true; + return false; } + return Store.ContainsKey(key); + } - /// - /// Determines whether the contains a specific key. - /// - /// The key. - /// true if the contains a specific key; otherwise, false. - public bool ContainsKey(string key) + /// + /// Copies the elements to a one-dimensional Array instance at the specified index. + /// + /// The one-dimensional Array that is the destination of the specified objects copied from the . + /// The zero-based index in at which copying begins. + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + if (Store == null) { - if (Store == null) - { - return false; - } - return Store.ContainsKey(key); + return; } - /// - /// Copies the elements to a one-dimensional Array instance at the specified index. - /// - /// The one-dimensional Array that is the destination of the specified objects copied from the . - /// The zero-based index in at which copying begins. - public void CopyTo(KeyValuePair[] array, int arrayIndex) + foreach (var item in Store) { - if (Store == null) - { - return; - } + array[arrayIndex] = item; + arrayIndex++; + } + } - foreach (var item in Store) - { - array[arrayIndex] = item; - arrayIndex++; - } + /// + /// Removes the given item from the the collection. + /// + /// The item. + /// true if the specified object was removed from the collection; otherwise, false. + public bool Remove(KeyValuePair item) + { + ThrowIfReadOnly(); + if (Store == null) + { + return false; } - /// - /// Removes the given item from the the collection. - /// - /// The item. - /// true if the specified object was removed from the collection; otherwise, false. - public bool Remove(KeyValuePair item) + if (Store.TryGetValue(item.Key, out var value) && StringValues.Equals(item.Value, value)) { - ThrowIfReadOnly(); - if (Store == null) - { - return false; - } + return Store.Remove(item.Key); + } + return false; + } - if (Store.TryGetValue(item.Key, out var value) && StringValues.Equals(item.Value, value)) - { - return Store.Remove(item.Key); - } + /// + /// Removes the given header from the collection. + /// + /// The header name. + /// true if the specified object was removed from the collection; otherwise, false. + public bool Remove(string key) + { + ThrowIfReadOnly(); + if (Store == null) + { return false; } + return Store.Remove(key); + } - /// - /// Removes the given header from the collection. - /// - /// The header name. - /// true if the specified object was removed from the collection; otherwise, false. - public bool Remove(string key) + /// + /// Retrieves a value from the dictionary. + /// + /// The header name. + /// The value. + /// true if the contains the key; otherwise, false. + public bool TryGetValue(string key, out StringValues value) + { + if (Store == null) { - ThrowIfReadOnly(); - if (Store == null) - { - return false; - } - return Store.Remove(key); + value = default(StringValues); + return false; } + return Store.TryGetValue(key, out value); + } - /// - /// Retrieves a value from the dictionary. - /// - /// The header name. - /// The value. - /// true if the contains the key; otherwise, false. - public bool TryGetValue(string key, out StringValues value) + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + public Enumerator GetEnumerator() + { + if (Store == null || Store.Count == 0) { - if (Store == null) - { - value = default(StringValues); - return false; - } - return Store.TryGetValue(key, out value); + // Non-boxed Enumerator + return default; } + return new Enumerator(Store.GetEnumerator()); + } - /// - /// Returns an enumerator that iterates through a collection. - /// - /// An object that can be used to iterate through the collection. - public Enumerator GetEnumerator() + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator> IEnumerable>.GetEnumerator() + { + if (Store == null || Store.Count == 0) { - if (Store == null || Store.Count == 0) - { - // Non-boxed Enumerator - return default; - } - return new Enumerator(Store.GetEnumerator()); + // Non-boxed Enumerator + return EmptyIEnumeratorType; } + return Store.GetEnumerator(); + } - /// - /// Returns an enumerator that iterates through a collection. - /// - /// An object that can be used to iterate through the collection. - IEnumerator> IEnumerable>.GetEnumerator() + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + if (Store == null || Store.Count == 0) { - if (Store == null || Store.Count == 0) - { - // Non-boxed Enumerator - return EmptyIEnumeratorType; - } - return Store.GetEnumerator(); + // Non-boxed Enumerator + return EmptyIEnumerator; } + return Store.GetEnumerator(); + } - /// - /// Returns an enumerator that iterates through a collection. - /// - /// An object that can be used to iterate through the collection. - IEnumerator IEnumerable.GetEnumerator() + private void ThrowIfReadOnly() + { + if (IsReadOnly) { - if (Store == null || Store.Count == 0) - { - // Non-boxed Enumerator - return EmptyIEnumerator; - } - return Store.GetEnumerator(); + throw new InvalidOperationException("The response headers cannot be modified because the response has already started."); } + } + + /// + /// Enumerates a . + /// + public struct Enumerator : IEnumerator> + { + // Do NOT make this readonly, or MoveNext will not work + private Dictionary.Enumerator _dictionaryEnumerator; + private readonly bool _notEmpty; - private void ThrowIfReadOnly() + internal Enumerator(Dictionary.Enumerator dictionaryEnumerator) { - if (IsReadOnly) - { - throw new InvalidOperationException("The response headers cannot be modified because the response has already started."); - } + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; } /// - /// Enumerates a . + /// Advances the enumerator to the next element of the . /// - public struct Enumerator : IEnumerator> + /// if the enumerator was successfully advanced to the next element; + /// if the enumerator has passed the end of the collection. + public bool MoveNext() { - // Do NOT make this readonly, or MoveNext will not work - private Dictionary.Enumerator _dictionaryEnumerator; - private readonly bool _notEmpty; - - internal Enumerator(Dictionary.Enumerator dictionaryEnumerator) + if (_notEmpty) { - _dictionaryEnumerator = dictionaryEnumerator; - _notEmpty = true; + return _dictionaryEnumerator.MoveNext(); } + return false; + } - /// - /// Advances the enumerator to the next element of the . - /// - /// if the enumerator was successfully advanced to the next element; - /// if the enumerator has passed the end of the collection. - public bool MoveNext() + /// + /// Gets the element at the current position of the enumerator. + /// + public KeyValuePair Current + { + get { if (_notEmpty) { - return _dictionaryEnumerator.MoveNext(); - } - return false; - } - - /// - /// Gets the element at the current position of the enumerator. - /// - public KeyValuePair Current - { - get - { - if (_notEmpty) - { - return _dictionaryEnumerator.Current; - } - return default(KeyValuePair); + return _dictionaryEnumerator.Current; } + return default(KeyValuePair); } + } - /// - public void Dispose() - { - } + /// + public void Dispose() + { + } - object IEnumerator.Current + object IEnumerator.Current + { + get { - get - { - return Current; - } + return Current; } + } - void IEnumerator.Reset() + void IEnumerator.Reset() + { + if (_notEmpty) { - if (_notEmpty) - { - ((IEnumerator)_dictionaryEnumerator).Reset(); - } + ((IEnumerator)_dictionaryEnumerator).Reset(); } } } diff --git a/src/Http/Http/src/HttpContextAccessor.cs b/src/Http/Http/src/HttpContextAccessor.cs index 08c43d44d2..3135b582c6 100644 --- a/src/Http/Http/src/HttpContextAccessor.cs +++ b/src/Http/Http/src/HttpContextAccessor.cs @@ -3,43 +3,42 @@ using System.Threading; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Provides an implementation of based on the current execution context. +/// +public class HttpContextAccessor : IHttpContextAccessor { - /// - /// Provides an implementation of based on the current execution context. - /// - public class HttpContextAccessor : IHttpContextAccessor - { - private static readonly AsyncLocal _httpContextCurrent = new AsyncLocal(); + private static readonly AsyncLocal _httpContextCurrent = new AsyncLocal(); - /// - public HttpContext? HttpContext + /// + public HttpContext? HttpContext + { + get { - get + return _httpContextCurrent.Value?.Context; + } + set + { + var holder = _httpContextCurrent.Value; + if (holder != null) { - return _httpContextCurrent.Value?.Context; + // Clear current HttpContext trapped in the AsyncLocals, as its done. + holder.Context = null; } - set - { - var holder = _httpContextCurrent.Value; - if (holder != null) - { - // Clear current HttpContext trapped in the AsyncLocals, as its done. - holder.Context = null; - } - if (value != null) - { - // Use an object indirection to hold the HttpContext in the AsyncLocal, - // so it can be cleared in all ExecutionContexts when its cleared. - _httpContextCurrent.Value = new HttpContextHolder { Context = value }; - } + if (value != null) + { + // Use an object indirection to hold the HttpContext in the AsyncLocal, + // so it can be cleared in all ExecutionContexts when its cleared. + _httpContextCurrent.Value = new HttpContextHolder { Context = value }; } } + } - private class HttpContextHolder - { - public HttpContext? Context; - } + private class HttpContextHolder + { + public HttpContext? Context; } } diff --git a/src/Http/Http/src/HttpServiceCollectionExtensions.cs b/src/Http/Http/src/HttpServiceCollectionExtensions.cs index 22b2800d80..9955f48696 100644 --- a/src/Http/Http/src/HttpServiceCollectionExtensions.cs +++ b/src/Http/Http/src/HttpServiceCollectionExtensions.cs @@ -5,27 +5,26 @@ using System; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection.Extensions; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for configuring HttpContext services. +/// +public static class HttpServiceCollectionExtensions { /// - /// Extension methods for configuring HttpContext services. + /// Adds a default implementation for the service. /// - public static class HttpServiceCollectionExtensions + /// The . + /// The service collection. + public static IServiceCollection AddHttpContextAccessor(this IServiceCollection services) { - /// - /// Adds a default implementation for the service. - /// - /// The . - /// The service collection. - public static IServiceCollection AddHttpContextAccessor(this IServiceCollection services) + if (services == null) { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - services.TryAddSingleton(); - return services; + throw new ArgumentNullException(nameof(services)); } + + services.TryAddSingleton(); + return services; } } diff --git a/src/Http/Http/src/Internal/BufferingHelper.cs b/src/Http/Http/src/Internal/BufferingHelper.cs index db224aed8e..7f8c9d32e0 100644 --- a/src/Http/Http/src/Internal/BufferingHelper.cs +++ b/src/Http/Http/src/Internal/BufferingHelper.cs @@ -5,49 +5,48 @@ using System; using Microsoft.AspNetCore.Internal; using Microsoft.AspNetCore.WebUtilities; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +internal static class BufferingHelper { - internal static class BufferingHelper - { - internal const int DefaultBufferThreshold = 1024 * 30; + internal const int DefaultBufferThreshold = 1024 * 30; - public static HttpRequest EnableRewind(this HttpRequest request, int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null) + public static HttpRequest EnableRewind(this HttpRequest request, int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null) + { + if (request == null) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } + throw new ArgumentNullException(nameof(request)); + } - var body = request.Body; - if (!body.CanSeek) - { - var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, AspNetCoreTempDirectory.TempDirectoryFactory); - request.Body = fileStream; - request.HttpContext.Response.RegisterForDispose(fileStream); - } - return request; + var body = request.Body; + if (!body.CanSeek) + { + var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, AspNetCoreTempDirectory.TempDirectoryFactory); + request.Body = fileStream; + request.HttpContext.Response.RegisterForDispose(fileStream); } + return request; + } - public static MultipartSection EnableRewind(this MultipartSection section, Action registerForDispose, - int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null) + public static MultipartSection EnableRewind(this MultipartSection section, Action registerForDispose, + int bufferThreshold = DefaultBufferThreshold, long? bufferLimit = null) + { + if (section == null) + { + throw new ArgumentNullException(nameof(section)); + } + if (registerForDispose == null) { - if (section == null) - { - throw new ArgumentNullException(nameof(section)); - } - if (registerForDispose == null) - { - throw new ArgumentNullException(nameof(registerForDispose)); - } + throw new ArgumentNullException(nameof(registerForDispose)); + } - var body = section.Body; - if (!body.CanSeek) - { - var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, AspNetCoreTempDirectory.TempDirectoryFactory); - section.Body = fileStream; - registerForDispose(fileStream); - } - return section; + var body = section.Body; + if (!body.CanSeek) + { + var fileStream = new FileBufferingReadStream(body, bufferThreshold, bufferLimit, AspNetCoreTempDirectory.TempDirectoryFactory); + section.Body = fileStream; + registerForDispose(fileStream); } + return section; } } diff --git a/src/Http/Http/src/Internal/DefaultConnectionInfo.cs b/src/Http/Http/src/Internal/DefaultConnectionInfo.cs index 7aeb898644..aba7af8827 100644 --- a/src/Http/Http/src/Internal/DefaultConnectionInfo.cs +++ b/src/Http/Http/src/Internal/DefaultConnectionInfo.cs @@ -6,101 +6,100 @@ using System.Net; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Http +internal sealed class DefaultConnectionInfo : ConnectionInfo { - internal sealed class DefaultConnectionInfo : ConnectionInfo + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private static readonly Func _newHttpConnectionFeature = f => new HttpConnectionFeature(); + private static readonly Func _newTlsConnectionFeature = f => new TlsConnectionFeature(); + private static readonly Func _newConnectionLifetime = f => new DefaultConnectionLifetimeNotificationFeature(f.Get()); + + private FeatureReferences _features; + + public DefaultConnectionInfo(IFeatureCollection features) + { + Initialize(features); + } + + public void Initialize(IFeatureCollection features) + { + _features.Initalize(features); + } + + public void Initialize(IFeatureCollection features, int revision) + { + _features.Initalize(features, revision); + } + + public void Uninitialize() + { + _features = default; + } + + private IHttpConnectionFeature HttpConnectionFeature => + _features.Fetch(ref _features.Cache.Connection, _newHttpConnectionFeature)!; + + private ITlsConnectionFeature TlsConnectionFeature => + _features.Fetch(ref _features.Cache.TlsConnection, _newTlsConnectionFeature)!; + + private IConnectionLifetimeNotificationFeature ConnectionLifetime => + _features.Fetch(ref _features.Cache.ConnectionLifetime, _newConnectionLifetime)!; + + /// + public override string Id + { + get { return HttpConnectionFeature.ConnectionId; } + set { HttpConnectionFeature.ConnectionId = value; } + } + + public override IPAddress? RemoteIpAddress + { + get { return HttpConnectionFeature.RemoteIpAddress; } + set { HttpConnectionFeature.RemoteIpAddress = value; } + } + + public override int RemotePort + { + get { return HttpConnectionFeature.RemotePort; } + set { HttpConnectionFeature.RemotePort = value; } + } + + public override IPAddress? LocalIpAddress + { + get { return HttpConnectionFeature.LocalIpAddress; } + set { HttpConnectionFeature.LocalIpAddress = value; } + } + + public override int LocalPort + { + get { return HttpConnectionFeature.LocalPort; } + set { HttpConnectionFeature.LocalPort = value; } + } + + public override X509Certificate2? ClientCertificate + { + get { return TlsConnectionFeature.ClientCertificate; } + set { TlsConnectionFeature.ClientCertificate = value; } + } + + public override Task GetClientCertificateAsync(CancellationToken cancellationToken = default) + { + return TlsConnectionFeature.GetClientCertificateAsync(cancellationToken); + } + + public override void RequestClose() + { + ConnectionLifetime.RequestClose(); + } + + struct FeatureInterfaces { - // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 - private static readonly Func _newHttpConnectionFeature = f => new HttpConnectionFeature(); - private static readonly Func _newTlsConnectionFeature = f => new TlsConnectionFeature(); - private static readonly Func _newConnectionLifetime = f => new DefaultConnectionLifetimeNotificationFeature(f.Get()); - - private FeatureReferences _features; - - public DefaultConnectionInfo(IFeatureCollection features) - { - Initialize(features); - } - - public void Initialize(IFeatureCollection features) - { - _features.Initalize(features); - } - - public void Initialize(IFeatureCollection features, int revision) - { - _features.Initalize(features, revision); - } - - public void Uninitialize() - { - _features = default; - } - - private IHttpConnectionFeature HttpConnectionFeature => - _features.Fetch(ref _features.Cache.Connection, _newHttpConnectionFeature)!; - - private ITlsConnectionFeature TlsConnectionFeature=> - _features.Fetch(ref _features.Cache.TlsConnection, _newTlsConnectionFeature)!; - - private IConnectionLifetimeNotificationFeature ConnectionLifetime => - _features.Fetch(ref _features.Cache.ConnectionLifetime, _newConnectionLifetime)!; - - /// - public override string Id - { - get { return HttpConnectionFeature.ConnectionId; } - set { HttpConnectionFeature.ConnectionId = value; } - } - - public override IPAddress? RemoteIpAddress - { - get { return HttpConnectionFeature.RemoteIpAddress; } - set { HttpConnectionFeature.RemoteIpAddress = value; } - } - - public override int RemotePort - { - get { return HttpConnectionFeature.RemotePort; } - set { HttpConnectionFeature.RemotePort = value; } - } - - public override IPAddress? LocalIpAddress - { - get { return HttpConnectionFeature.LocalIpAddress; } - set { HttpConnectionFeature.LocalIpAddress = value; } - } - - public override int LocalPort - { - get { return HttpConnectionFeature.LocalPort; } - set { HttpConnectionFeature.LocalPort = value; } - } - - public override X509Certificate2? ClientCertificate - { - get { return TlsConnectionFeature.ClientCertificate; } - set { TlsConnectionFeature.ClientCertificate = value; } - } - - public override Task GetClientCertificateAsync(CancellationToken cancellationToken = default) - { - return TlsConnectionFeature.GetClientCertificateAsync(cancellationToken); - } - - public override void RequestClose() - { - ConnectionLifetime.RequestClose(); - } - - struct FeatureInterfaces - { - public IHttpConnectionFeature? Connection; - public ITlsConnectionFeature? TlsConnection; - public IConnectionLifetimeNotificationFeature? ConnectionLifetime; - } + public IHttpConnectionFeature? Connection; + public ITlsConnectionFeature? TlsConnection; + public IConnectionLifetimeNotificationFeature? ConnectionLifetime; } } diff --git a/src/Http/Http/src/Internal/DefaultHttpRequest.cs b/src/Http/Http/src/Internal/DefaultHttpRequest.cs index 6b3a2b730e..1f36aaba55 100644 --- a/src/Http/Http/src/Internal/DefaultHttpRequest.cs +++ b/src/Http/Http/src/Internal/DefaultHttpRequest.cs @@ -10,183 +10,182 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +internal sealed class DefaultHttpRequest : HttpRequest { - internal sealed class DefaultHttpRequest : HttpRequest - { - private const string Http = "http"; - private const string Https = "https"; - - // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 - private static readonly Func _nullRequestFeature = f => null; - private static readonly Func _newQueryFeature = f => new QueryFeature(f); - private static readonly Func _newFormFeature = r => new FormFeature(r, r._context.FormOptions ?? FormOptions.Default); - private static readonly Func _newRequestCookiesFeature = f => new RequestCookiesFeature(f); - private static readonly Func _newRouteValuesFeature = f => new RouteValuesFeature(); - private static readonly Func _newRequestBodyPipeFeature = context => new RequestBodyPipeFeature(context); - - private readonly DefaultHttpContext _context; - private FeatureReferences _features; - - public DefaultHttpRequest(DefaultHttpContext context) - { - _context = context; - _features.Initalize(context.Features); - } - - public void Initialize() - { - _features.Initalize(_context.Features); - } - - public void Initialize(int revision) - { - _features.Initalize(_context.Features, revision); - } - - public void Uninitialize() - { - _features = default; - } - - public override HttpContext HttpContext => _context; - - private IHttpRequestFeature HttpRequestFeature => - _features.Fetch(ref _features.Cache.Request, _nullRequestFeature)!; - - private IQueryFeature QueryFeature => - _features.Fetch(ref _features.Cache.Query, _newQueryFeature)!; - - private IFormFeature FormFeature => - _features.Fetch(ref _features.Cache.Form, this, _newFormFeature)!; - - private IRequestCookiesFeature RequestCookiesFeature => - _features.Fetch(ref _features.Cache.Cookies, _newRequestCookiesFeature)!; - - private IRouteValuesFeature RouteValuesFeature => - _features.Fetch(ref _features.Cache.RouteValues, _newRouteValuesFeature)!; - - private IRequestBodyPipeFeature RequestBodyPipeFeature => - _features.Fetch(ref _features.Cache.BodyPipe, this.HttpContext, _newRequestBodyPipeFeature)!; - - public override PathString PathBase - { - get { return new PathString(HttpRequestFeature.PathBase); } - set { HttpRequestFeature.PathBase = value.Value ?? string.Empty; } - } - - public override PathString Path - { - get { return new PathString(HttpRequestFeature.Path); } - set { HttpRequestFeature.Path = value.Value ?? string.Empty; } - } - - public override QueryString QueryString - { - get { return new QueryString(HttpRequestFeature.QueryString); } - set { HttpRequestFeature.QueryString = value.Value ?? string.Empty; } - } - - public override long? ContentLength - { - get { return Headers.ContentLength; } - set { Headers.ContentLength = value; } - } - - public override Stream Body - { - get { return HttpRequestFeature.Body; } - set { HttpRequestFeature.Body = value; } - } - - public override string Method - { - get { return HttpRequestFeature.Method; } - set { HttpRequestFeature.Method = value; } - } - - public override string Scheme - { - get { return HttpRequestFeature.Scheme; } - set { HttpRequestFeature.Scheme = value; } - } - - public override bool IsHttps - { - get { return string.Equals(Https, Scheme, StringComparison.OrdinalIgnoreCase); } - set { Scheme = value ? Https : Http; } - } - - public override HostString Host - { - get { return HostString.FromUriComponent(Headers.Host.ToString()); } - set { Headers.Host = value.ToUriComponent(); } - } - - public override IQueryCollection Query - { - get { return QueryFeature.Query; } - set { QueryFeature.Query = value; } - } - - public override string Protocol - { - get { return HttpRequestFeature.Protocol; } - set { HttpRequestFeature.Protocol = value; } - } - - public override IHeaderDictionary Headers - { - get { return HttpRequestFeature.Headers; } - } - - public override IRequestCookieCollection Cookies - { - get { return RequestCookiesFeature.Cookies; } - set { RequestCookiesFeature.Cookies = value; } - } - - public override string? ContentType - { - get { return Headers.ContentType; } - set { Headers.ContentType = value; } - } - - public override bool HasFormContentType - { - get { return FormFeature.HasFormContentType; } - } - - public override IFormCollection Form - { - get { return FormFeature.ReadForm(); } - set { FormFeature.Form = value; } - } - - public override Task ReadFormAsync(CancellationToken cancellationToken) - { - return FormFeature.ReadFormAsync(cancellationToken); - } - - public override RouteValueDictionary RouteValues - { - get { return RouteValuesFeature.RouteValues; } - set { RouteValuesFeature.RouteValues = value; } - } - - public override PipeReader BodyReader - { - get { return RequestBodyPipeFeature.Reader; } - } - - struct FeatureInterfaces - { - public IHttpRequestFeature? Request; - public IQueryFeature? Query; - public IFormFeature? Form; - public IRequestCookiesFeature? Cookies; - public IRouteValuesFeature? RouteValues; - public IRequestBodyPipeFeature? BodyPipe; - } + private const string Http = "http"; + private const string Https = "https"; + + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private static readonly Func _nullRequestFeature = f => null; + private static readonly Func _newQueryFeature = f => new QueryFeature(f); + private static readonly Func _newFormFeature = r => new FormFeature(r, r._context.FormOptions ?? FormOptions.Default); + private static readonly Func _newRequestCookiesFeature = f => new RequestCookiesFeature(f); + private static readonly Func _newRouteValuesFeature = f => new RouteValuesFeature(); + private static readonly Func _newRequestBodyPipeFeature = context => new RequestBodyPipeFeature(context); + + private readonly DefaultHttpContext _context; + private FeatureReferences _features; + + public DefaultHttpRequest(DefaultHttpContext context) + { + _context = context; + _features.Initalize(context.Features); + } + + public void Initialize() + { + _features.Initalize(_context.Features); + } + + public void Initialize(int revision) + { + _features.Initalize(_context.Features, revision); + } + + public void Uninitialize() + { + _features = default; + } + + public override HttpContext HttpContext => _context; + + private IHttpRequestFeature HttpRequestFeature => + _features.Fetch(ref _features.Cache.Request, _nullRequestFeature)!; + + private IQueryFeature QueryFeature => + _features.Fetch(ref _features.Cache.Query, _newQueryFeature)!; + + private IFormFeature FormFeature => + _features.Fetch(ref _features.Cache.Form, this, _newFormFeature)!; + + private IRequestCookiesFeature RequestCookiesFeature => + _features.Fetch(ref _features.Cache.Cookies, _newRequestCookiesFeature)!; + + private IRouteValuesFeature RouteValuesFeature => + _features.Fetch(ref _features.Cache.RouteValues, _newRouteValuesFeature)!; + + private IRequestBodyPipeFeature RequestBodyPipeFeature => + _features.Fetch(ref _features.Cache.BodyPipe, this.HttpContext, _newRequestBodyPipeFeature)!; + + public override PathString PathBase + { + get { return new PathString(HttpRequestFeature.PathBase); } + set { HttpRequestFeature.PathBase = value.Value ?? string.Empty; } + } + + public override PathString Path + { + get { return new PathString(HttpRequestFeature.Path); } + set { HttpRequestFeature.Path = value.Value ?? string.Empty; } + } + + public override QueryString QueryString + { + get { return new QueryString(HttpRequestFeature.QueryString); } + set { HttpRequestFeature.QueryString = value.Value ?? string.Empty; } + } + + public override long? ContentLength + { + get { return Headers.ContentLength; } + set { Headers.ContentLength = value; } + } + + public override Stream Body + { + get { return HttpRequestFeature.Body; } + set { HttpRequestFeature.Body = value; } + } + + public override string Method + { + get { return HttpRequestFeature.Method; } + set { HttpRequestFeature.Method = value; } + } + + public override string Scheme + { + get { return HttpRequestFeature.Scheme; } + set { HttpRequestFeature.Scheme = value; } + } + + public override bool IsHttps + { + get { return string.Equals(Https, Scheme, StringComparison.OrdinalIgnoreCase); } + set { Scheme = value ? Https : Http; } + } + + public override HostString Host + { + get { return HostString.FromUriComponent(Headers.Host.ToString()); } + set { Headers.Host = value.ToUriComponent(); } + } + + public override IQueryCollection Query + { + get { return QueryFeature.Query; } + set { QueryFeature.Query = value; } + } + + public override string Protocol + { + get { return HttpRequestFeature.Protocol; } + set { HttpRequestFeature.Protocol = value; } + } + + public override IHeaderDictionary Headers + { + get { return HttpRequestFeature.Headers; } + } + + public override IRequestCookieCollection Cookies + { + get { return RequestCookiesFeature.Cookies; } + set { RequestCookiesFeature.Cookies = value; } + } + + public override string? ContentType + { + get { return Headers.ContentType; } + set { Headers.ContentType = value; } + } + + public override bool HasFormContentType + { + get { return FormFeature.HasFormContentType; } + } + + public override IFormCollection Form + { + get { return FormFeature.ReadForm(); } + set { FormFeature.Form = value; } + } + + public override Task ReadFormAsync(CancellationToken cancellationToken) + { + return FormFeature.ReadFormAsync(cancellationToken); + } + + public override RouteValueDictionary RouteValues + { + get { return RouteValuesFeature.RouteValues; } + set { RouteValuesFeature.RouteValues = value; } + } + + public override PipeReader BodyReader + { + get { return RequestBodyPipeFeature.Reader; } + } + + struct FeatureInterfaces + { + public IHttpRequestFeature? Request; + public IQueryFeature? Query; + public IFormFeature? Form; + public IRequestCookiesFeature? Cookies; + public IRouteValuesFeature? RouteValues; + public IRequestBodyPipeFeature? BodyPipe; } } diff --git a/src/Http/Http/src/Internal/DefaultHttpResponse.cs b/src/Http/Http/src/Internal/DefaultHttpResponse.cs index d5a66396e9..f694460f61 100644 --- a/src/Http/Http/src/Internal/DefaultHttpResponse.cs +++ b/src/Http/Http/src/Internal/DefaultHttpResponse.cs @@ -9,172 +9,171 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +internal sealed class DefaultHttpResponse : HttpResponse { - internal sealed class DefaultHttpResponse : HttpResponse - { - // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 - private static readonly Func _nullResponseFeature = f => null; - private static readonly Func _nullResponseBodyFeature = f => null; - private static readonly Func _newResponseCookiesFeature = f => new ResponseCookiesFeature(f); + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private static readonly Func _nullResponseFeature = f => null; + private static readonly Func _nullResponseBodyFeature = f => null; + private static readonly Func _newResponseCookiesFeature = f => new ResponseCookiesFeature(f); - private readonly DefaultHttpContext _context; - private FeatureReferences _features; + private readonly DefaultHttpContext _context; + private FeatureReferences _features; - public DefaultHttpResponse(DefaultHttpContext context) - { - _context = context; - _features.Initalize(context.Features); - } + public DefaultHttpResponse(DefaultHttpContext context) + { + _context = context; + _features.Initalize(context.Features); + } - public void Initialize() - { - _features.Initalize(_context.Features); - } + public void Initialize() + { + _features.Initalize(_context.Features); + } - public void Initialize(int revision) - { - _features.Initalize(_context.Features, revision); - } + public void Initialize(int revision) + { + _features.Initalize(_context.Features, revision); + } - public void Uninitialize() - { - _features = default; - } + public void Uninitialize() + { + _features = default; + } - private IHttpResponseFeature HttpResponseFeature => - _features.Fetch(ref _features.Cache.Response, _nullResponseFeature)!; + private IHttpResponseFeature HttpResponseFeature => + _features.Fetch(ref _features.Cache.Response, _nullResponseFeature)!; - private IHttpResponseBodyFeature HttpResponseBodyFeature => - _features.Fetch(ref _features.Cache.ResponseBody, _nullResponseBodyFeature)!; + private IHttpResponseBodyFeature HttpResponseBodyFeature => + _features.Fetch(ref _features.Cache.ResponseBody, _nullResponseBodyFeature)!; - private IResponseCookiesFeature ResponseCookiesFeature => - _features.Fetch(ref _features.Cache.Cookies, _newResponseCookiesFeature)!; + private IResponseCookiesFeature ResponseCookiesFeature => + _features.Fetch(ref _features.Cache.Cookies, _newResponseCookiesFeature)!; - public override HttpContext HttpContext { get { return _context; } } + public override HttpContext HttpContext { get { return _context; } } - public override int StatusCode - { - get { return HttpResponseFeature.StatusCode; } - set { HttpResponseFeature.StatusCode = value; } - } + public override int StatusCode + { + get { return HttpResponseFeature.StatusCode; } + set { HttpResponseFeature.StatusCode = value; } + } - public override IHeaderDictionary Headers - { - get { return HttpResponseFeature.Headers; } - } + public override IHeaderDictionary Headers + { + get { return HttpResponseFeature.Headers; } + } - public override Stream Body + public override Stream Body + { + get { return HttpResponseBodyFeature.Stream; } + set { - get { return HttpResponseBodyFeature.Stream; } - set + var otherFeature = _features.Collection.Get()!; + + if (otherFeature is StreamResponseBodyFeature streamFeature + && streamFeature.PriorFeature != null + && object.ReferenceEquals(value, streamFeature.PriorFeature.Stream)) { - var otherFeature = _features.Collection.Get()!; - - if (otherFeature is StreamResponseBodyFeature streamFeature - && streamFeature.PriorFeature != null - && object.ReferenceEquals(value, streamFeature.PriorFeature.Stream)) - { - // They're reverting the stream back to the prior one. Revert the whole feature. - _features.Collection.Set(streamFeature.PriorFeature); - return; - } - - _features.Collection.Set(new StreamResponseBodyFeature(value, otherFeature)); + // They're reverting the stream back to the prior one. Revert the whole feature. + _features.Collection.Set(streamFeature.PriorFeature); + return; } + + _features.Collection.Set(new StreamResponseBodyFeature(value, otherFeature)); } + } - public override long? ContentLength + public override long? ContentLength + { + get { return Headers.ContentLength; } + set { Headers.ContentLength = value; } + } + + public override string? ContentType + { + get { - get { return Headers.ContentLength; } - set { Headers.ContentLength = value; } + return Headers.ContentType; } - - public override string? ContentType + set { - get + if (string.IsNullOrEmpty(value)) { - return Headers.ContentType; + HttpResponseFeature.Headers.ContentType = default; } - set + else { - if (string.IsNullOrEmpty(value)) - { - HttpResponseFeature.Headers.ContentType = default; - } - else - { - HttpResponseFeature.Headers.ContentType = value; - } + HttpResponseFeature.Headers.ContentType = value; } } + } - public override IResponseCookies Cookies - { - get { return ResponseCookiesFeature.Cookies; } - } + public override IResponseCookies Cookies + { + get { return ResponseCookiesFeature.Cookies; } + } - public override bool HasStarted - { - get { return HttpResponseFeature.HasStarted; } - } + public override bool HasStarted + { + get { return HttpResponseFeature.HasStarted; } + } - public override PipeWriter BodyWriter - { - get { return HttpResponseBodyFeature.Writer; } - } + public override PipeWriter BodyWriter + { + get { return HttpResponseBodyFeature.Writer; } + } - public override void OnStarting(Func callback, object state) + public override void OnStarting(Func callback, object state) + { + if (callback == null) { - if (callback == null) - { - throw new ArgumentNullException(nameof(callback)); - } - - HttpResponseFeature.OnStarting(callback, state); + throw new ArgumentNullException(nameof(callback)); } - public override void OnCompleted(Func callback, object state) - { - if (callback == null) - { - throw new ArgumentNullException(nameof(callback)); - } + HttpResponseFeature.OnStarting(callback, state); + } - HttpResponseFeature.OnCompleted(callback, state); + public override void OnCompleted(Func callback, object state) + { + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); } - public override void Redirect(string location, bool permanent) - { - if (permanent) - { - HttpResponseFeature.StatusCode = 301; - } - else - { - HttpResponseFeature.StatusCode = 302; - } + HttpResponseFeature.OnCompleted(callback, state); + } - Headers.Location = location; + public override void Redirect(string location, bool permanent) + { + if (permanent) + { + HttpResponseFeature.StatusCode = 301; } - - public override Task StartAsync(CancellationToken cancellationToken = default) + else { - if (HasStarted) - { - return Task.CompletedTask; - } - - return HttpResponseBodyFeature.StartAsync(cancellationToken); + HttpResponseFeature.StatusCode = 302; } - public override Task CompleteAsync() => HttpResponseBodyFeature.CompleteAsync(); + Headers.Location = location; + } - struct FeatureInterfaces + public override Task StartAsync(CancellationToken cancellationToken = default) + { + if (HasStarted) { - public IHttpResponseFeature? Response; - public IHttpResponseBodyFeature? ResponseBody; - public IResponseCookiesFeature? Cookies; + return Task.CompletedTask; } + + return HttpResponseBodyFeature.StartAsync(cancellationToken); + } + + public override Task CompleteAsync() => HttpResponseBodyFeature.CompleteAsync(); + + struct FeatureInterfaces + { + public IHttpResponseFeature? Response; + public IHttpResponseBodyFeature? ResponseBody; + public IResponseCookiesFeature? Cookies; } } diff --git a/src/Http/Http/src/Internal/DefaultWebSocketManager.cs b/src/Http/Http/src/Internal/DefaultWebSocketManager.cs index 3263655f4c..e9b86490a2 100644 --- a/src/Http/Http/src/Internal/DefaultWebSocketManager.cs +++ b/src/Http/Http/src/Internal/DefaultWebSocketManager.cs @@ -8,79 +8,78 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +internal sealed class DefaultWebSocketManager : WebSocketManager { - internal sealed class DefaultWebSocketManager : WebSocketManager - { - // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 - private static readonly Func _nullRequestFeature = f => null; - private static readonly Func _nullWebSocketFeature = f => null; + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private static readonly Func _nullRequestFeature = f => null; + private static readonly Func _nullWebSocketFeature = f => null; - private FeatureReferences _features; - private static readonly WebSocketAcceptContext _defaultWebSocketAcceptContext = new WebSocketAcceptContext(); + private FeatureReferences _features; + private static readonly WebSocketAcceptContext _defaultWebSocketAcceptContext = new WebSocketAcceptContext(); - public DefaultWebSocketManager(IFeatureCollection features) - { - Initialize(features); - } + public DefaultWebSocketManager(IFeatureCollection features) + { + Initialize(features); + } - public void Initialize(IFeatureCollection features) - { - _features.Initalize(features); - } + public void Initialize(IFeatureCollection features) + { + _features.Initalize(features); + } - public void Initialize(IFeatureCollection features, int revision) - { - _features.Initalize(features, revision); - } + public void Initialize(IFeatureCollection features, int revision) + { + _features.Initalize(features, revision); + } - public void Uninitialize() - { - _features = default; - } + public void Uninitialize() + { + _features = default; + } - private IHttpRequestFeature HttpRequestFeature => - _features.Fetch(ref _features.Cache.Request, _nullRequestFeature)!; + private IHttpRequestFeature HttpRequestFeature => + _features.Fetch(ref _features.Cache.Request, _nullRequestFeature)!; - private IHttpWebSocketFeature WebSocketFeature => - _features.Fetch(ref _features.Cache.WebSockets, _nullWebSocketFeature)!; + private IHttpWebSocketFeature WebSocketFeature => + _features.Fetch(ref _features.Cache.WebSockets, _nullWebSocketFeature)!; - public override bool IsWebSocketRequest + public override bool IsWebSocketRequest + { + get { - get - { - return WebSocketFeature != null && WebSocketFeature.IsWebSocketRequest; - } + return WebSocketFeature != null && WebSocketFeature.IsWebSocketRequest; } + } - public override IList WebSocketRequestedProtocols + public override IList WebSocketRequestedProtocols + { + get { - get - { - return HttpRequestFeature.Headers.GetCommaSeparatedValues(HeaderNames.WebSocketSubProtocols); - } + return HttpRequestFeature.Headers.GetCommaSeparatedValues(HeaderNames.WebSocketSubProtocols); } + } - public override Task AcceptWebSocketAsync(string? subProtocol) - { - var acceptContext = subProtocol is null ? _defaultWebSocketAcceptContext : - new WebSocketAcceptContext() { SubProtocol = subProtocol }; - return AcceptWebSocketAsync(acceptContext); - } + public override Task AcceptWebSocketAsync(string? subProtocol) + { + var acceptContext = subProtocol is null ? _defaultWebSocketAcceptContext : + new WebSocketAcceptContext() { SubProtocol = subProtocol }; + return AcceptWebSocketAsync(acceptContext); + } - public override Task AcceptWebSocketAsync(WebSocketAcceptContext acceptContext) + public override Task AcceptWebSocketAsync(WebSocketAcceptContext acceptContext) + { + if (WebSocketFeature == null) { - if (WebSocketFeature == null) - { - throw new NotSupportedException("WebSockets are not supported"); - } - return WebSocketFeature.AcceptAsync(acceptContext); + throw new NotSupportedException("WebSockets are not supported"); } + return WebSocketFeature.AcceptAsync(acceptContext); + } - struct FeatureInterfaces - { - public IHttpRequestFeature? Request; - public IHttpWebSocketFeature? WebSockets; - } + struct FeatureInterfaces + { + public IHttpRequestFeature? Request; + public IHttpWebSocketFeature? WebSockets; } } diff --git a/src/Http/Http/src/Internal/ItemsDictionary.cs b/src/Http/Http/src/Internal/ItemsDictionary.cs index 2ad6a2d3aa..ac58b7356d 100644 --- a/src/Http/Http/src/Internal/ItemsDictionary.cs +++ b/src/Http/Http/src/Internal/ItemsDictionary.cs @@ -5,160 +5,159 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +internal class ItemsDictionary : IDictionary { - internal class ItemsDictionary : IDictionary - { - private IDictionary? _items; + private IDictionary? _items; - public ItemsDictionary() - {} + public ItemsDictionary() + { } - public ItemsDictionary(IDictionary items) - { - _items = items; - } + public ItemsDictionary(IDictionary items) + { + _items = items; + } - public IDictionary Items => this; + public IDictionary Items => this; - // Replace the indexer with one that returns null for missing values - object? IDictionary.this[object key] + // Replace the indexer with one that returns null for missing values + object? IDictionary.this[object key] + { + get { - get - { - if (_items != null && _items.TryGetValue(key, out var value)) - { - return value; - } - return null; - } - set + if (_items != null && _items.TryGetValue(key, out var value)) { - EnsureDictionary(); - _items[key] = value; + return value; } + return null; } - - void IDictionary.Add(object key, object? value) + set { EnsureDictionary(); - _items.Add(key, value); + _items[key] = value; } + } + + void IDictionary.Add(object key, object? value) + { + EnsureDictionary(); + _items.Add(key, value); + } - bool IDictionary.ContainsKey(object key) - => _items != null && _items.ContainsKey(key); + bool IDictionary.ContainsKey(object key) + => _items != null && _items.ContainsKey(key); - ICollection IDictionary.Keys + ICollection IDictionary.Keys + { + get { - get + if (_items == null) { - if (_items == null) - { - return EmptyDictionary.Dictionary.Keys; - } - - return _items.Keys; + return EmptyDictionary.Dictionary.Keys; } + + return _items.Keys; } + } - bool IDictionary.Remove(object key) - => _items != null && _items.Remove(key); + bool IDictionary.Remove(object key) + => _items != null && _items.Remove(key); - bool IDictionary.TryGetValue(object key, out object? value) - { - value = null; - return _items != null && _items.TryGetValue(key, out value); - } + bool IDictionary.TryGetValue(object key, out object? value) + { + value = null; + return _items != null && _items.TryGetValue(key, out value); + } - ICollection IDictionary.Values + ICollection IDictionary.Values + { + get { - get + if (_items == null) { - if (_items == null) - { - return EmptyDictionary.Dictionary.Values; - } - - return _items.Values; + return EmptyDictionary.Dictionary.Values; } - } - void ICollection>.Add(KeyValuePair item) - { - EnsureDictionary(); - _items.Add(item); + return _items.Values; } + } - void ICollection>.Clear() => _items?.Clear(); + void ICollection>.Add(KeyValuePair item) + { + EnsureDictionary(); + _items.Add(item); + } - bool ICollection>.Contains(KeyValuePair item) - => _items != null && _items.Contains(item); + void ICollection>.Clear() => _items?.Clear(); - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) - { - if (_items == null) - { - //Delegate to Empty Dictionary to do the argument checking. - EmptyDictionary.Collection.CopyTo(array, arrayIndex); - } + bool ICollection>.Contains(KeyValuePair item) + => _items != null && _items.Contains(item); - _items?.CopyTo(array, arrayIndex); + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + if (_items == null) + { + //Delegate to Empty Dictionary to do the argument checking. + EmptyDictionary.Collection.CopyTo(array, arrayIndex); } - int ICollection>.Count => _items?.Count ?? 0; + _items?.CopyTo(array, arrayIndex); + } - bool ICollection>.IsReadOnly => _items?.IsReadOnly ?? false; + int ICollection>.Count => _items?.Count ?? 0; - bool ICollection>.Remove(KeyValuePair item) - { - if (_items == null) - { - return false; - } + bool ICollection>.IsReadOnly => _items?.IsReadOnly ?? false; - if (_items.TryGetValue(item.Key, out var value) && Equals(item.Value, value)) - { - return _items.Remove(item.Key); - } + bool ICollection>.Remove(KeyValuePair item) + { + if (_items == null) + { return false; } - [MemberNotNull(nameof(_items))] - private void EnsureDictionary() + if (_items.TryGetValue(item.Key, out var value) && Equals(item.Value, value)) { - if (_items == null) - { - _items = new Dictionary(); - } + return _items.Remove(item.Key); } + return false; + } - IEnumerator> IEnumerable>.GetEnumerator() - => _items?.GetEnumerator() ?? EmptyEnumerator.Instance; + [MemberNotNull(nameof(_items))] + private void EnsureDictionary() + { + if (_items == null) + { + _items = new Dictionary(); + } + } - IEnumerator IEnumerable.GetEnumerator() => _items?.GetEnumerator() ?? EmptyEnumerator.Instance; + IEnumerator> IEnumerable>.GetEnumerator() + => _items?.GetEnumerator() ?? EmptyEnumerator.Instance; - private class EmptyEnumerator : IEnumerator> - { - // In own class so only initalized if GetEnumerator is called on an empty ItemsDictionary - public static readonly IEnumerator> Instance = new EmptyEnumerator(); - public KeyValuePair Current => default; + IEnumerator IEnumerable.GetEnumerator() => _items?.GetEnumerator() ?? EmptyEnumerator.Instance; - object? IEnumerator.Current => null; + private class EmptyEnumerator : IEnumerator> + { + // In own class so only initalized if GetEnumerator is called on an empty ItemsDictionary + public static readonly IEnumerator> Instance = new EmptyEnumerator(); + public KeyValuePair Current => default; - public void Dispose() - { } + object? IEnumerator.Current => null; - public bool MoveNext() => false; + public void Dispose() + { } - public void Reset() - { } - } + public bool MoveNext() => false; - private static class EmptyDictionary - { - // In own class so only initalized if CopyTo is called on an empty ItemsDictionary - public static readonly IDictionary Dictionary = new Dictionary(); - public static ICollection> Collection => Dictionary; - } + public void Reset() + { } + } + + private static class EmptyDictionary + { + // In own class so only initalized if CopyTo is called on an empty ItemsDictionary + public static readonly IDictionary Dictionary = new Dictionary(); + public static ICollection> Collection => Dictionary; } } diff --git a/src/Http/Http/src/Internal/ReferenceReadStream.cs b/src/Http/Http/src/Internal/ReferenceReadStream.cs index a68658c0d8..4766e3286a 100644 --- a/src/Http/Http/src/Internal/ReferenceReadStream.cs +++ b/src/Http/Http/src/Internal/ReferenceReadStream.cs @@ -6,156 +6,155 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http -{ - /// - /// A Stream that wraps another stream starting at a certain offset and reading for the given length. - /// - internal sealed class ReferenceReadStream : Stream - { - private readonly Stream _inner; - private readonly long _innerOffset; - private readonly long _length; - private long _position; - - private bool _disposed; +namespace Microsoft.AspNetCore.Http; - public ReferenceReadStream(Stream inner, long offset, long length) - { - if (inner == null) - { - throw new ArgumentNullException(nameof(inner)); - } +/// +/// A Stream that wraps another stream starting at a certain offset and reading for the given length. +/// +internal sealed class ReferenceReadStream : Stream +{ + private readonly Stream _inner; + private readonly long _innerOffset; + private readonly long _length; + private long _position; - _inner = inner; - _innerOffset = offset; - _length = length; - _inner.Position = offset; - } + private bool _disposed; - public override bool CanRead + public ReferenceReadStream(Stream inner, long offset, long length) + { + if (inner == null) { - get { return true; } + throw new ArgumentNullException(nameof(inner)); } - public override bool CanSeek - { - get { return _inner.CanSeek; } - } + _inner = inner; + _innerOffset = offset; + _length = length; + _inner.Position = offset; + } - public override bool CanWrite - { - get { return false; } - } + public override bool CanRead + { + get { return true; } + } - public override long Length - { - get { return _length; } - } + public override bool CanSeek + { + get { return _inner.CanSeek; } + } - public override long Position - { - get { return _position; } - set - { - ThrowIfDisposed(); - if (value < 0 || value > Length) - { - throw new ArgumentOutOfRangeException(nameof(value), value, $"The Position must be within the length of the Stream: {Length}"); - } - VerifyPosition(); - _position = value; - _inner.Position = _innerOffset + _position; - } - } + public override bool CanWrite + { + get { return false; } + } - // Throws if the position in the underlying stream has changed without our knowledge, indicating someone else is trying - // to use the stream at the same time which could lead to data corruption. - private void VerifyPosition() - { - if (_inner.Position != _innerOffset + _position) - { - throw new InvalidOperationException("The inner stream position has changed unexpectedly."); - } - } + public override long Length + { + get { return _length; } + } - public override long Seek(long offset, SeekOrigin origin) + public override long Position + { + get { return _position; } + set { - if (origin == SeekOrigin.Begin) - { - Position = offset; - } - else if (origin == SeekOrigin.End) - { - Position = Length + offset; - } - else // if (origin == SeekOrigin.Current) + ThrowIfDisposed(); + if (value < 0 || value > Length) { - Position = Position + offset; + throw new ArgumentOutOfRangeException(nameof(value), value, $"The Position must be within the length of the Stream: {Length}"); } - return Position; - } - - public override int Read(byte[] buffer, int offset, int count) - { - ThrowIfDisposed(); VerifyPosition(); - var toRead = Math.Min(count, _length - _position); - var read = _inner.Read(buffer, offset, (int)toRead); - _position += read; - return read; + _position = value; + _inner.Position = _innerOffset + _position; } + } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - => ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); - - public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) + // Throws if the position in the underlying stream has changed without our knowledge, indicating someone else is trying + // to use the stream at the same time which could lead to data corruption. + private void VerifyPosition() + { + if (_inner.Position != _innerOffset + _position) { - ThrowIfDisposed(); - VerifyPosition(); - var toRead = (int)Math.Min(buffer.Length, _length - _position); - var read = await _inner.ReadAsync(buffer.Slice(0, toRead), cancellationToken); - _position += read; - return read; + throw new InvalidOperationException("The inner stream position has changed unexpectedly."); } + } - public override void Write(byte[] buffer, int offset, int count) + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) { - throw new NotSupportedException(); + Position = offset; } - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + else if (origin == SeekOrigin.End) { - throw new NotSupportedException(); + Position = Length + offset; } - - public override void SetLength(long value) + else // if (origin == SeekOrigin.Current) { - throw new NotSupportedException(); + Position = Position + offset; } + return Position; + } - public override void Flush() - { - } + public override int Read(byte[] buffer, int offset, int count) + { + ThrowIfDisposed(); + VerifyPosition(); + var toRead = Math.Min(count, _length - _position); + var read = _inner.Read(buffer, offset, (int)toRead); + _position += read; + return read; + } - public override Task FlushAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) + { + ThrowIfDisposed(); + VerifyPosition(); + var toRead = (int)Math.Min(buffer.Length, _length - _position); + var read = await _inner.ReadAsync(buffer.Slice(0, toRead), cancellationToken); + _position += read; + return read; + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + } - protected override void Dispose(bool disposing) + public override Task FlushAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + protected override void Dispose(bool disposing) + { + if (disposing) { - if (disposing) - { - _disposed = true; - } + _disposed = true; } + } - private void ThrowIfDisposed() + private void ThrowIfDisposed() + { + if (_disposed) { - if (_disposed) - { - throw new ObjectDisposedException(nameof(ReferenceReadStream)); - } + throw new ObjectDisposedException(nameof(ReferenceReadStream)); } } } diff --git a/src/Http/Http/src/Internal/RequestCookieCollection.cs b/src/Http/Http/src/Internal/RequestCookieCollection.cs index 2901940073..c1d648a4ab 100644 --- a/src/Http/Http/src/Internal/RequestCookieCollection.cs +++ b/src/Http/Http/src/Internal/RequestCookieCollection.cs @@ -5,229 +5,228 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Primitives; +using System.Linq; using Microsoft.AspNetCore.Internal; +using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -using System.Linq; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +internal class RequestCookieCollection : IRequestCookieCollection { - internal class RequestCookieCollection : IRequestCookieCollection - { - public static readonly RequestCookieCollection Empty = new RequestCookieCollection(); - private static readonly string[] EmptyKeys = Array.Empty(); + public static readonly RequestCookieCollection Empty = new RequestCookieCollection(); + private static readonly string[] EmptyKeys = Array.Empty(); - // Pre-box - private static readonly IEnumerator> EmptyIEnumeratorType = default(Enumerator); - private static readonly IEnumerator EmptyIEnumerator = default(Enumerator); + // Pre-box + private static readonly IEnumerator> EmptyIEnumeratorType = default(Enumerator); + private static readonly IEnumerator EmptyIEnumerator = default(Enumerator); - private AdaptiveCapacityDictionary Store { get; set; } + private AdaptiveCapacityDictionary Store { get; set; } - public RequestCookieCollection() - { - Store = new AdaptiveCapacityDictionary(StringComparer.OrdinalIgnoreCase); - } + public RequestCookieCollection() + { + Store = new AdaptiveCapacityDictionary(StringComparer.OrdinalIgnoreCase); + } - public RequestCookieCollection(int capacity) - { - Store = new AdaptiveCapacityDictionary(capacity, StringComparer.OrdinalIgnoreCase); - } + public RequestCookieCollection(int capacity) + { + Store = new AdaptiveCapacityDictionary(capacity, StringComparer.OrdinalIgnoreCase); + } - // For tests - public RequestCookieCollection(Dictionary store) - { - Store = new AdaptiveCapacityDictionary(store); - } + // For tests + public RequestCookieCollection(Dictionary store) + { + Store = new AdaptiveCapacityDictionary(store); + } - public string? this[string key] + public string? this[string key] + { + get { - get + if (key == null) { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - if (Store == null) - { - return null; - } + throw new ArgumentNullException(nameof(key)); + } - if (TryGetValue(key, out var value)) - { - return value; - } + if (Store == null) + { return null; } - } - public static RequestCookieCollection Parse(StringValues values) - => ParseInternal(values, AppContext.TryGetSwitch(ResponseCookies.EnableCookieNameEncoding, out var enabled) && enabled); - - internal static RequestCookieCollection ParseInternal(StringValues values, bool enableCookieNameEncoding) - { - if (values.Count == 0) + if (TryGetValue(key, out var value)) { - return Empty; + return value; } + return null; + } + } - // Do not set the collection capacity based on StringValues.Count, the Cookie header is supposed to be a single combined value. - var collection = new RequestCookieCollection(); - var store = collection.Store!; + public static RequestCookieCollection Parse(StringValues values) + => ParseInternal(values, AppContext.TryGetSwitch(ResponseCookies.EnableCookieNameEncoding, out var enabled) && enabled); - if (CookieHeaderParserShared.TryParseValues(values, store, enableCookieNameEncoding, supportsMultipleValues: true)) - { - if (store.Count == 0) - { - return Empty; - } - - return collection; - } + internal static RequestCookieCollection ParseInternal(StringValues values, bool enableCookieNameEncoding) + { + if (values.Count == 0) + { return Empty; } - public int Count + // Do not set the collection capacity based on StringValues.Count, the Cookie header is supposed to be a single combined value. + var collection = new RequestCookieCollection(); + var store = collection.Store!; + + if (CookieHeaderParserShared.TryParseValues(values, store, enableCookieNameEncoding, supportsMultipleValues: true)) { - get + if (store.Count == 0) { - if (Store == null) - { - return 0; - } - return Store.Count; + return Empty; } + + return collection; } + return Empty; + } - public ICollection Keys + public int Count + { + get { - get + if (Store == null) { - if (Store == null) - { - return EmptyKeys; - } - return Store.Keys; + return 0; } + return Store.Count; } + } - public bool ContainsKey(string key) + public ICollection Keys + { + get { if (Store == null) { - return false; + return EmptyKeys; } - return Store.ContainsKey(key); + return Store.Keys; } + } - public bool TryGetValue(string key, [MaybeNullWhen(false)] out string? value) + public bool ContainsKey(string key) + { + if (Store == null) { - if (Store == null) - { - value = null; - return false; - } + return false; + } + return Store.ContainsKey(key); + } - return Store.TryGetValue(key, out value); + public bool TryGetValue(string key, [MaybeNullWhen(false)] out string? value) + { + if (Store == null) + { + value = null; + return false; } - /// - /// Returns an struct enumerator that iterates through a collection without boxing. - /// - /// An object that can be used to iterate through the collection. - public Enumerator GetEnumerator() + return Store.TryGetValue(key, out value); + } + + /// + /// Returns an struct enumerator that iterates through a collection without boxing. + /// + /// An object that can be used to iterate through the collection. + public Enumerator GetEnumerator() + { + if (Store == null || Store.Count == 0) { - if (Store == null || Store.Count == 0) - { - // Non-boxed Enumerator - return default; - } // Non-boxed Enumerator - return new Enumerator(Store.GetEnumerator()); + return default; } + // Non-boxed Enumerator + return new Enumerator(Store.GetEnumerator()); + } - /// - /// Returns an enumerator that iterates through a collection, boxes in non-empty path. - /// - /// An object that can be used to iterate through the collection. - IEnumerator> IEnumerable>.GetEnumerator() + /// + /// Returns an enumerator that iterates through a collection, boxes in non-empty path. + /// + /// An object that can be used to iterate through the collection. + IEnumerator> IEnumerable>.GetEnumerator() + { + if (Store == null || Store.Count == 0) { - if (Store == null || Store.Count == 0) - { - // Non-boxed Enumerator - return EmptyIEnumeratorType; - } - // Boxed Enumerator - return GetEnumerator(); + // Non-boxed Enumerator + return EmptyIEnumeratorType; } + // Boxed Enumerator + return GetEnumerator(); + } - /// - /// Returns an enumerator that iterates through a collection, boxes in non-empty path. - /// - /// An object that can be used to iterate through the collection. - IEnumerator IEnumerable.GetEnumerator() + /// + /// Returns an enumerator that iterates through a collection, boxes in non-empty path. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + if (Store == null || Store.Count == 0) { - if (Store == null || Store.Count == 0) - { - // Non-boxed Enumerator - return EmptyIEnumerator; - } - // Boxed Enumerator - return GetEnumerator(); + // Non-boxed Enumerator + return EmptyIEnumerator; } + // Boxed Enumerator + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator> + { + // Do NOT make this readonly, or MoveNext will not work + private AdaptiveCapacityDictionary.Enumerator _dictionaryEnumerator; + private readonly bool _notEmpty; - public struct Enumerator : IEnumerator> + internal Enumerator(AdaptiveCapacityDictionary.Enumerator dictionaryEnumerator) { - // Do NOT make this readonly, or MoveNext will not work - private AdaptiveCapacityDictionary.Enumerator _dictionaryEnumerator; - private readonly bool _notEmpty; + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; + } - internal Enumerator(AdaptiveCapacityDictionary.Enumerator dictionaryEnumerator) + public bool MoveNext() + { + if (_notEmpty) { - _dictionaryEnumerator = dictionaryEnumerator; - _notEmpty = true; + return _dictionaryEnumerator.MoveNext(); } + return false; + } - public bool MoveNext() + public KeyValuePair Current + { + get { if (_notEmpty) { - return _dictionaryEnumerator.MoveNext(); - } - return false; - } - - public KeyValuePair Current - { - get - { - if (_notEmpty) - { - var current = _dictionaryEnumerator.Current; - return new KeyValuePair(current.Key, (string)current.Value!); - } - return default(KeyValuePair); + var current = _dictionaryEnumerator.Current; + return new KeyValuePair(current.Key, (string)current.Value!); } + return default(KeyValuePair); } + } - object IEnumerator.Current + object IEnumerator.Current + { + get { - get - { - return Current; - } + return Current; } + } - public void Dispose() - { - } + public void Dispose() + { + } - public void Reset() + public void Reset() + { + if (_notEmpty) { - if (_notEmpty) - { - ((IEnumerator)_dictionaryEnumerator).Reset(); - } + ((IEnumerator)_dictionaryEnumerator).Reset(); } } } diff --git a/src/Http/Http/src/Internal/ResponseCookies.cs b/src/Http/Http/src/Internal/ResponseCookies.cs index 2e326cb2e9..80ba3fae57 100644 --- a/src/Http/Http/src/Internal/ResponseCookies.cs +++ b/src/Http/Http/src/Internal/ResponseCookies.cs @@ -9,214 +9,213 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// A wrapper for the response Set-Cookie header. +/// +internal partial class ResponseCookies : IResponseCookies { + internal const string EnableCookieNameEncoding = "Microsoft.AspNetCore.Http.EnableCookieNameEncoding"; + internal bool _enableCookieNameEncoding = AppContext.TryGetSwitch(EnableCookieNameEncoding, out var enabled) && enabled; + + private readonly IFeatureCollection _features; + private ILogger? _logger; + /// - /// A wrapper for the response Set-Cookie header. + /// Create a new wrapper. /// - internal partial class ResponseCookies : IResponseCookies + internal ResponseCookies(IFeatureCollection features) { - internal const string EnableCookieNameEncoding = "Microsoft.AspNetCore.Http.EnableCookieNameEncoding"; - internal bool _enableCookieNameEncoding = AppContext.TryGetSwitch(EnableCookieNameEncoding, out var enabled) && enabled; + _features = features; + Headers = _features.Get()!.Headers; + } - private readonly IFeatureCollection _features; - private ILogger? _logger; + private IHeaderDictionary Headers { get; set; } - /// - /// Create a new wrapper. - /// - internal ResponseCookies(IFeatureCollection features) + /// + public void Append(string key, string value) + { + var setCookieHeaderValue = new SetCookieHeaderValue( + _enableCookieNameEncoding ? Uri.EscapeDataString(key) : key, + Uri.EscapeDataString(value)) { - _features = features; - Headers = _features.Get()!.Headers; - } + Path = "/" + }; + var cookieValue = setCookieHeaderValue.ToString(); - private IHeaderDictionary Headers { get; set; } + Headers.SetCookie = StringValues.Concat(Headers.SetCookie, cookieValue); + } - /// - public void Append(string key, string value) + /// + public void Append(string key, string value, CookieOptions options) + { + if (options == null) { - var setCookieHeaderValue = new SetCookieHeaderValue( - _enableCookieNameEncoding ? Uri.EscapeDataString(key) : key, - Uri.EscapeDataString(value)) - { - Path = "/" - }; - var cookieValue = setCookieHeaderValue.ToString(); - - Headers.SetCookie = StringValues.Concat(Headers.SetCookie, cookieValue); + throw new ArgumentNullException(nameof(options)); } - /// - public void Append(string key, string value, CookieOptions options) + // SameSite=None cookies must be marked as Secure. + if (!options.Secure && options.SameSite == SameSiteMode.None) { - if (options == null) + if (_logger == null) { - throw new ArgumentNullException(nameof(options)); + var services = _features.Get()?.RequestServices; + _logger = services?.GetService>(); } - // SameSite=None cookies must be marked as Secure. - if (!options.Secure && options.SameSite == SameSiteMode.None) + if (_logger != null) { - if (_logger == null) - { - var services = _features.Get()?.RequestServices; - _logger = services?.GetService>(); - } - - if (_logger != null) - { - Log.SameSiteCookieNotSecure(_logger, key); - } + Log.SameSiteCookieNotSecure(_logger, key); } + } - var setCookieHeaderValue = new SetCookieHeaderValue( - _enableCookieNameEncoding ? Uri.EscapeDataString(key) : key, - Uri.EscapeDataString(value)) - { - Domain = options.Domain, - Path = options.Path, - Expires = options.Expires, - MaxAge = options.MaxAge, - Secure = options.Secure, - SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite, - HttpOnly = options.HttpOnly - }; - - var cookieValue = setCookieHeaderValue.ToString(); - - Headers.SetCookie = StringValues.Concat(Headers.SetCookie, cookieValue); + var setCookieHeaderValue = new SetCookieHeaderValue( + _enableCookieNameEncoding ? Uri.EscapeDataString(key) : key, + Uri.EscapeDataString(value)) + { + Domain = options.Domain, + Path = options.Path, + Expires = options.Expires, + MaxAge = options.MaxAge, + Secure = options.Secure, + SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite, + HttpOnly = options.HttpOnly + }; + + var cookieValue = setCookieHeaderValue.ToString(); + + Headers.SetCookie = StringValues.Concat(Headers.SetCookie, cookieValue); + } + + /// + public void Append(ReadOnlySpan> keyValuePairs, CookieOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); } - /// - public void Append(ReadOnlySpan> keyValuePairs, CookieOptions options) + // SameSite=None cookies must be marked as Secure. + if (!options.Secure && options.SameSite == SameSiteMode.None) { - if (options == null) + if (_logger == null) { - throw new ArgumentNullException(nameof(options)); + var services = _features.Get()?.RequestServices; + _logger = services?.GetService>(); } - // SameSite=None cookies must be marked as Secure. - if (!options.Secure && options.SameSite == SameSiteMode.None) + if (_logger != null) { - if (_logger == null) - { - var services = _features.Get()?.RequestServices; - _logger = services?.GetService>(); - } - - if (_logger != null) + foreach (var keyValuePair in keyValuePairs) { - foreach (var keyValuePair in keyValuePairs) - { - Log.SameSiteCookieNotSecure(_logger, keyValuePair.Key); - } + Log.SameSiteCookieNotSecure(_logger, keyValuePair.Key); } } - - var setCookieHeaderValue = new SetCookieHeaderValue(string.Empty) - { - Domain = options.Domain, - Path = options.Path, - Expires = options.Expires, - MaxAge = options.MaxAge, - Secure = options.Secure, - SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite, - HttpOnly = options.HttpOnly - }; - - var cookierHeaderValue = setCookieHeaderValue.ToString()[1..]; - var cookies = new string[keyValuePairs.Length]; - var position = 0; - - foreach (var keyValuePair in keyValuePairs) - { - var key = _enableCookieNameEncoding ? Uri.EscapeDataString(keyValuePair.Key) : keyValuePair.Key; - cookies[position] = string.Concat(key, "=", Uri.EscapeDataString(keyValuePair.Value), cookierHeaderValue); - position++; - } - - // Can't use += as StringValues does not override operator+ - // and the implict conversions will cause an incorrect string concat https://github.com/dotnet/runtime/issues/52507 - Headers.SetCookie = StringValues.Concat(Headers.SetCookie, cookies); } - /// - public void Delete(string key) + var setCookieHeaderValue = new SetCookieHeaderValue(string.Empty) + { + Domain = options.Domain, + Path = options.Path, + Expires = options.Expires, + MaxAge = options.MaxAge, + Secure = options.Secure, + SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite, + HttpOnly = options.HttpOnly + }; + + var cookierHeaderValue = setCookieHeaderValue.ToString()[1..]; + var cookies = new string[keyValuePairs.Length]; + var position = 0; + + foreach (var keyValuePair in keyValuePairs) { - Delete(key, new CookieOptions() { Path = "/" }); + var key = _enableCookieNameEncoding ? Uri.EscapeDataString(keyValuePair.Key) : keyValuePair.Key; + cookies[position] = string.Concat(key, "=", Uri.EscapeDataString(keyValuePair.Value), cookierHeaderValue); + position++; } - /// - public void Delete(string key, CookieOptions options) + // Can't use += as StringValues does not override operator+ + // and the implict conversions will cause an incorrect string concat https://github.com/dotnet/runtime/issues/52507 + Headers.SetCookie = StringValues.Concat(Headers.SetCookie, cookies); + } + + /// + public void Delete(string key) + { + Delete(key, new CookieOptions() { Path = "/" }); + } + + /// + public void Delete(string key, CookieOptions options) + { + if (options == null) { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } + throw new ArgumentNullException(nameof(options)); + } - var encodedKeyPlusEquals = (_enableCookieNameEncoding ? Uri.EscapeDataString(key) : key) + "="; - var domainHasValue = !string.IsNullOrEmpty(options.Domain); - var pathHasValue = !string.IsNullOrEmpty(options.Path); + var encodedKeyPlusEquals = (_enableCookieNameEncoding ? Uri.EscapeDataString(key) : key) + "="; + var domainHasValue = !string.IsNullOrEmpty(options.Domain); + var pathHasValue = !string.IsNullOrEmpty(options.Path); - Func rejectPredicate; - if (domainHasValue && pathHasValue) - { - rejectPredicate = (value, encKeyPlusEquals, opts) => - value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase) && - value.IndexOf($"domain={opts.Domain}", StringComparison.OrdinalIgnoreCase) != -1 && - value.IndexOf($"path={opts.Path}", StringComparison.OrdinalIgnoreCase) != -1; - } - else if (domainHasValue) - { - rejectPredicate = (value, encKeyPlusEquals, opts) => - value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase) && - value.IndexOf($"domain={opts.Domain}", StringComparison.OrdinalIgnoreCase) != -1; - } - else if (pathHasValue) - { - rejectPredicate = (value, encKeyPlusEquals, opts) => - value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase) && - value.IndexOf($"path={opts.Path}", StringComparison.OrdinalIgnoreCase) != -1; - } - else - { - rejectPredicate = (value, encKeyPlusEquals, opts) => value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase); - } + Func rejectPredicate; + if (domainHasValue && pathHasValue) + { + rejectPredicate = (value, encKeyPlusEquals, opts) => + value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase) && + value.IndexOf($"domain={opts.Domain}", StringComparison.OrdinalIgnoreCase) != -1 && + value.IndexOf($"path={opts.Path}", StringComparison.OrdinalIgnoreCase) != -1; + } + else if (domainHasValue) + { + rejectPredicate = (value, encKeyPlusEquals, opts) => + value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase) && + value.IndexOf($"domain={opts.Domain}", StringComparison.OrdinalIgnoreCase) != -1; + } + else if (pathHasValue) + { + rejectPredicate = (value, encKeyPlusEquals, opts) => + value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase) && + value.IndexOf($"path={opts.Path}", StringComparison.OrdinalIgnoreCase) != -1; + } + else + { + rejectPredicate = (value, encKeyPlusEquals, opts) => value.StartsWith(encKeyPlusEquals, StringComparison.OrdinalIgnoreCase); + } - var existingValues = Headers.SetCookie; - if (!StringValues.IsNullOrEmpty(existingValues)) - { - var values = existingValues.ToArray(); - var newValues = new List(); + var existingValues = Headers.SetCookie; + if (!StringValues.IsNullOrEmpty(existingValues)) + { + var values = existingValues.ToArray(); + var newValues = new List(); - for (var i = 0; i < values.Length; i++) + for (var i = 0; i < values.Length; i++) + { + var value = values[i] ?? string.Empty; + if (!rejectPredicate(value, encodedKeyPlusEquals, options)) { - var value = values[i] ?? string.Empty; - if (!rejectPredicate(value, encodedKeyPlusEquals, options)) - { - newValues.Add(value); - } + newValues.Add(value); } - - Headers.SetCookie = new StringValues(newValues.ToArray()); } - Append(key, string.Empty, new CookieOptions - { - Path = options.Path, - Domain = options.Domain, - Expires = DateTimeOffset.UnixEpoch, - Secure = options.Secure, - HttpOnly = options.HttpOnly, - SameSite = options.SameSite - }); + Headers.SetCookie = new StringValues(newValues.ToArray()); } - private static partial class Log + Append(key, string.Empty, new CookieOptions { - [LoggerMessage(1, LogLevel.Warning, "The cookie '{name}' has set 'SameSite=None' and must also set 'Secure'.", EventName = "SameSiteNotSecure")] - public static partial void SameSiteCookieNotSecure(ILogger logger, string name); - } + Path = options.Path, + Domain = options.Domain, + Expires = DateTimeOffset.UnixEpoch, + Secure = options.Secure, + HttpOnly = options.HttpOnly, + SameSite = options.SameSite + }); + } + + private static partial class Log + { + [LoggerMessage(1, LogLevel.Warning, "The cookie '{name}' has set 'SameSite=None' and must also set 'Secure'.", EventName = "SameSiteNotSecure")] + public static partial void SameSiteCookieNotSecure(ILogger logger, string name); } } diff --git a/src/Http/Http/src/MiddlewareFactory.cs b/src/Http/Http/src/MiddlewareFactory.cs index c1b28f4feb..83a7ddd8a1 100644 --- a/src/Http/Http/src/MiddlewareFactory.cs +++ b/src/Http/Http/src/MiddlewareFactory.cs @@ -4,37 +4,36 @@ using System; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Default implementation for . +/// +public class MiddlewareFactory : IMiddlewareFactory { + // The default middleware factory is just an IServiceProvider proxy. + // This should be registered as a scoped service so that the middleware instances + // don't end up being singletons. + private readonly IServiceProvider _serviceProvider; + /// - /// Default implementation for . + /// Initializes a new instance of . /// - public class MiddlewareFactory : IMiddlewareFactory + /// The application services. + public MiddlewareFactory(IServiceProvider serviceProvider) { - // The default middleware factory is just an IServiceProvider proxy. - // This should be registered as a scoped service so that the middleware instances - // don't end up being singletons. - private readonly IServiceProvider _serviceProvider; - - /// - /// Initializes a new instance of . - /// - /// The application services. - public MiddlewareFactory(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } + _serviceProvider = serviceProvider; + } - /// - public IMiddleware? Create(Type middlewareType) - { - return _serviceProvider.GetRequiredService(middlewareType) as IMiddleware; - } + /// + public IMiddleware? Create(Type middlewareType) + { + return _serviceProvider.GetRequiredService(middlewareType) as IMiddleware; + } - /// - public void Release(IMiddleware middleware) - { - // The container owns the lifetime of the service - } + /// + public void Release(IMiddleware middleware) + { + // The container owns the lifetime of the service } } diff --git a/src/Http/Http/src/QueryCollection.cs b/src/Http/Http/src/QueryCollection.cs index 41328aa3ee..a8941c7163 100644 --- a/src/Http/Http/src/QueryCollection.cs +++ b/src/Http/Http/src/QueryCollection.cs @@ -6,246 +6,245 @@ using System.Collections; using System.Collections.Generic; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// The HttpRequest query string collection +/// +public class QueryCollection : IQueryCollection { /// - /// The HttpRequest query string collection + /// Gets an empty . /// - public class QueryCollection : IQueryCollection - { - /// - /// Gets an empty . - /// - public static readonly QueryCollection Empty = new QueryCollection(); - private static readonly string[] EmptyKeys = Array.Empty(); - // Pre-box - private static readonly IEnumerator> EmptyIEnumeratorType = default(Enumerator); - private static readonly IEnumerator EmptyIEnumerator = default(Enumerator); + public static readonly QueryCollection Empty = new QueryCollection(); + private static readonly string[] EmptyKeys = Array.Empty(); + // Pre-box + private static readonly IEnumerator> EmptyIEnumeratorType = default(Enumerator); + private static readonly IEnumerator EmptyIEnumerator = default(Enumerator); - private Dictionary? Store { get; } + private Dictionary? Store { get; } - /// - /// Initializes a new instance of . - /// - public QueryCollection() - { - } + /// + /// Initializes a new instance of . + /// + public QueryCollection() + { + } - /// - /// Initializes a new instance of . - /// - /// The backing store. - public QueryCollection(Dictionary store) - { - Store = store; - } + /// + /// Initializes a new instance of . + /// + /// The backing store. + public QueryCollection(Dictionary store) + { + Store = store; + } - /// - /// Creates a shallow copy of the specified . - /// - /// The to clone. - public QueryCollection(QueryCollection store) - { - Store = store.Store; - } + /// + /// Creates a shallow copy of the specified . + /// + /// The to clone. + public QueryCollection(QueryCollection store) + { + Store = store.Store; + } - /// - /// Initializes a new instance of . - /// - /// The initial number of query items that this instance can contain. - public QueryCollection(int capacity) - { - Store = new Dictionary(capacity, StringComparer.OrdinalIgnoreCase); - } + /// + /// Initializes a new instance of . + /// + /// The initial number of query items that this instance can contain. + public QueryCollection(int capacity) + { + Store = new Dictionary(capacity, StringComparer.OrdinalIgnoreCase); + } - /// - /// Gets the associated set of values from the collection. - /// - /// The key name. - /// the associated value from the collection as a StringValues or StringValues.Empty if the key is not present. - public StringValues this[string key] + /// + /// Gets the associated set of values from the collection. + /// + /// The key name. + /// the associated value from the collection as a StringValues or StringValues.Empty if the key is not present. + public StringValues this[string key] + { + get { - get + if (Store == null) { - if (Store == null) - { - return StringValues.Empty; - } - - if (TryGetValue(key, out var value)) - { - return value; - } return StringValues.Empty; } - } - /// - /// Gets the number of elements contained in the ;. - /// - /// The number of elements contained in the . - public int Count - { - get + if (TryGetValue(key, out var value)) { - if (Store == null) - { - return 0; - } - return Store.Count; + return value; } + return StringValues.Empty; } + } - /// - /// Gets the collection of query names in this instance. - /// - public ICollection Keys + /// + /// Gets the number of elements contained in the ;. + /// + /// The number of elements contained in the . + public int Count + { + get { - get + if (Store == null) { - if (Store == null) - { - return EmptyKeys; - } - return Store.Keys; + return 0; } + return Store.Count; } + } - /// - /// Determines whether the contains a specific key. - /// - /// The key. - /// true if the contains a specific key; otherwise, false. - public bool ContainsKey(string key) + /// + /// Gets the collection of query names in this instance. + /// + public ICollection Keys + { + get { if (Store == null) { - return false; + return EmptyKeys; } - return Store.ContainsKey(key); + return Store.Keys; } + } - /// - /// Retrieves a value from the collection. - /// - /// The key. - /// The value. - /// true if the contains the key; otherwise, false. - public bool TryGetValue(string key, out StringValues value) + /// + /// Determines whether the contains a specific key. + /// + /// The key. + /// true if the contains a specific key; otherwise, false. + public bool ContainsKey(string key) + { + if (Store == null) { - if (Store == null) - { - value = default(StringValues); - return false; - } - return Store.TryGetValue(key, out value); + return false; } + return Store.ContainsKey(key); + } - /// - /// Returns an enumerator that iterates through a collection. - /// - /// An object that can be used to iterate through the collection. - public Enumerator GetEnumerator() + /// + /// Retrieves a value from the collection. + /// + /// The key. + /// The value. + /// true if the contains the key; otherwise, false. + public bool TryGetValue(string key, out StringValues value) + { + if (Store == null) { - if (Store == null || Store.Count == 0) - { - // Non-boxed Enumerator - return default; - } - return new Enumerator(Store.GetEnumerator()); + value = default(StringValues); + return false; + } + return Store.TryGetValue(key, out value); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + public Enumerator GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return default; } + return new Enumerator(Store.GetEnumerator()); + } - /// - /// Returns an enumerator that iterates through a collection. - /// - /// An object that can be used to iterate through the collection. - IEnumerator> IEnumerable>.GetEnumerator() + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator> IEnumerable>.GetEnumerator() + { + if (Store == null || Store.Count == 0) { - if (Store == null || Store.Count == 0) - { - // Non-boxed Enumerator - return EmptyIEnumeratorType; - } - return Store.GetEnumerator(); + // Non-boxed Enumerator + return EmptyIEnumeratorType; + } + return Store.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + if (Store == null || Store.Count == 0) + { + // Non-boxed Enumerator + return EmptyIEnumerator; + } + return Store.GetEnumerator(); + } + + /// + /// Enumerates a . + /// + public struct Enumerator : IEnumerator> + { + // Do NOT make this readonly, or MoveNext will not work + private Dictionary.Enumerator _dictionaryEnumerator; + private readonly bool _notEmpty; + + internal Enumerator(Dictionary.Enumerator dictionaryEnumerator) + { + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; } /// - /// Returns an enumerator that iterates through a collection. + /// Advances the enumerator to the next element of the . /// - /// An object that can be used to iterate through the collection. - IEnumerator IEnumerable.GetEnumerator() + /// if the enumerator was successfully advanced to the next element; + /// if the enumerator has passed the end of the collection. + public bool MoveNext() { - if (Store == null || Store.Count == 0) + if (_notEmpty) { - // Non-boxed Enumerator - return EmptyIEnumerator; + return _dictionaryEnumerator.MoveNext(); } - return Store.GetEnumerator(); + return false; } /// - /// Enumerates a . + /// Gets the element at the current position of the enumerator. /// - public struct Enumerator : IEnumerator> + public KeyValuePair Current { - // Do NOT make this readonly, or MoveNext will not work - private Dictionary.Enumerator _dictionaryEnumerator; - private readonly bool _notEmpty; - - internal Enumerator(Dictionary.Enumerator dictionaryEnumerator) - { - _dictionaryEnumerator = dictionaryEnumerator; - _notEmpty = true; - } - - /// - /// Advances the enumerator to the next element of the . - /// - /// if the enumerator was successfully advanced to the next element; - /// if the enumerator has passed the end of the collection. - public bool MoveNext() + get { if (_notEmpty) { - return _dictionaryEnumerator.MoveNext(); - } - return false; - } - - /// - /// Gets the element at the current position of the enumerator. - /// - public KeyValuePair Current - { - get - { - if (_notEmpty) - { - return _dictionaryEnumerator.Current; - } - return default(KeyValuePair); + return _dictionaryEnumerator.Current; } + return default(KeyValuePair); } + } - /// - public void Dispose() - { - } + /// + public void Dispose() + { + } - object IEnumerator.Current + object IEnumerator.Current + { + get { - get - { - return Current; - } + return Current; } + } - void IEnumerator.Reset() + void IEnumerator.Reset() + { + if (_notEmpty) { - if (_notEmpty) - { - ((IEnumerator)_dictionaryEnumerator).Reset(); - } + ((IEnumerator)_dictionaryEnumerator).Reset(); } } } diff --git a/src/Http/Http/src/QueryCollectionInternal.cs b/src/Http/Http/src/QueryCollectionInternal.cs index fa553fad8b..3e49233165 100644 --- a/src/Http/Http/src/QueryCollectionInternal.cs +++ b/src/Http/Http/src/QueryCollectionInternal.cs @@ -6,123 +6,122 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// The HttpRequest query string collection +/// +internal class QueryCollectionInternal : IQueryCollection { + private AdaptiveCapacityDictionary Store { get; } + /// - /// The HttpRequest query string collection + /// Initializes a new instance of . /// - internal class QueryCollectionInternal : IQueryCollection + /// The backing store. + internal QueryCollectionInternal(AdaptiveCapacityDictionary store) { - private AdaptiveCapacityDictionary Store { get; } + Store = store; + } - /// - /// Initializes a new instance of . - /// - /// The backing store. - internal QueryCollectionInternal(AdaptiveCapacityDictionary store) - { - Store = store; - } + /// + /// Gets the associated set of values from the collection. + /// + /// The key name. + /// the associated value from the collection as a StringValues or StringValues.Empty if the key is not present. + public StringValues this[string key] => TryGetValue(key, out var value) ? value : StringValues.Empty; - /// - /// Gets the associated set of values from the collection. - /// - /// The key name. - /// the associated value from the collection as a StringValues or StringValues.Empty if the key is not present. - public StringValues this[string key] => TryGetValue(key, out var value) ? value : StringValues.Empty; + /// + /// Gets the number of elements contained in the ;. + /// + /// The number of elements contained in the . + public int Count => Store.Count; - /// - /// Gets the number of elements contained in the ;. - /// - /// The number of elements contained in the . - public int Count => Store.Count; + /// + /// Gets the collection of query names in this instance. + /// + public ICollection Keys => Store.Keys; - /// - /// Gets the collection of query names in this instance. - /// - public ICollection Keys => Store.Keys; + /// + /// Determines whether the contains a specific key. + /// + /// The key. + /// true if the contains a specific key; otherwise, false. + public bool ContainsKey(string key) => Store.ContainsKey(key); - /// - /// Determines whether the contains a specific key. - /// - /// The key. - /// true if the contains a specific key; otherwise, false. - public bool ContainsKey(string key) => Store.ContainsKey(key); + /// + /// Retrieves a value from the collection. + /// + /// The key. + /// The value. + /// true if the contains the key; otherwise, false. + public bool TryGetValue(string key, out StringValues value) => Store.TryGetValue(key, out value); - /// - /// Retrieves a value from the collection. - /// - /// The key. - /// The value. - /// true if the contains the key; otherwise, false. - public bool TryGetValue(string key, out StringValues value) => Store.TryGetValue(key, out value); + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + public Enumerator GetEnumerator() => new Enumerator(Store.GetEnumerator()); - /// - /// Returns an enumerator that iterates through a collection. - /// - /// An object that can be used to iterate through the collection. - public Enumerator GetEnumerator() => new Enumerator(Store.GetEnumerator()); + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator> IEnumerable>.GetEnumerator() + => Store.GetEnumerator(); - /// - /// Returns an enumerator that iterates through a collection. - /// - /// An object that can be used to iterate through the collection. - IEnumerator> IEnumerable>.GetEnumerator() - => Store.GetEnumerator(); + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() => Store.GetEnumerator(); - /// - /// Returns an enumerator that iterates through a collection. - /// - /// An object that can be used to iterate through the collection. - IEnumerator IEnumerable.GetEnumerator() => Store.GetEnumerator(); + /// + /// Enumerates a . + /// + public struct Enumerator : IEnumerator> + { + // Do NOT make this readonly, or MoveNext will not work + private AdaptiveCapacityDictionary.Enumerator _dictionaryEnumerator; + private readonly bool _notEmpty; + + internal Enumerator(AdaptiveCapacityDictionary.Enumerator dictionaryEnumerator) + { + _dictionaryEnumerator = dictionaryEnumerator; + _notEmpty = true; + } /// - /// Enumerates a . + /// Advances the enumerator to the next element of the . /// - public struct Enumerator : IEnumerator> + /// if the enumerator was successfully advanced to the next element; + /// if the enumerator has passed the end of the collection. + public bool MoveNext() { - // Do NOT make this readonly, or MoveNext will not work - private AdaptiveCapacityDictionary.Enumerator _dictionaryEnumerator; - private readonly bool _notEmpty; - - internal Enumerator(AdaptiveCapacityDictionary.Enumerator dictionaryEnumerator) + if (_notEmpty) { - _dictionaryEnumerator = dictionaryEnumerator; - _notEmpty = true; - } - - /// - /// Advances the enumerator to the next element of the . - /// - /// if the enumerator was successfully advanced to the next element; - /// if the enumerator has passed the end of the collection. - public bool MoveNext() - { - if (_notEmpty) - { - return _dictionaryEnumerator.MoveNext(); - } - return false; + return _dictionaryEnumerator.MoveNext(); } + return false; + } - /// - /// Gets the element at the current position of the enumerator. - /// - public KeyValuePair Current => _notEmpty ? _dictionaryEnumerator.Current : default; + /// + /// Gets the element at the current position of the enumerator. + /// + public KeyValuePair Current => _notEmpty ? _dictionaryEnumerator.Current : default; - /// - public void Dispose() - { - } + /// + public void Dispose() + { + } - object IEnumerator.Current => Current; + object IEnumerator.Current => Current; - void IEnumerator.Reset() + void IEnumerator.Reset() + { + if (_notEmpty) { - if (_notEmpty) - { - ((IEnumerator)_dictionaryEnumerator).Reset(); - } + ((IEnumerator)_dictionaryEnumerator).Reset(); } } } diff --git a/src/Http/Http/src/RequestFormReaderExtensions.cs b/src/Http/Http/src/RequestFormReaderExtensions.cs index e1cd094650..c3819858e1 100644 --- a/src/Http/Http/src/RequestFormReaderExtensions.cs +++ b/src/Http/Http/src/RequestFormReaderExtensions.cs @@ -6,46 +6,45 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Extension for . +/// +public static class RequestFormReaderExtensions { /// - /// Extension for . + /// Read the request body as a form with the given options. These options will only be used + /// if the form has not already been read. /// - public static class RequestFormReaderExtensions + /// The request. + /// Options for reading the form. + /// + /// The parsed form. + public static Task ReadFormAsync(this HttpRequest request, FormOptions options, + CancellationToken cancellationToken = new CancellationToken()) { - /// - /// Read the request body as a form with the given options. These options will only be used - /// if the form has not already been read. - /// - /// The request. - /// Options for reading the form. - /// - /// The parsed form. - public static Task ReadFormAsync(this HttpRequest request, FormOptions options, - CancellationToken cancellationToken = new CancellationToken()) + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + if (options == null) { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } + throw new ArgumentNullException(nameof(options)); + } - if (!request.HasFormContentType) - { - throw new InvalidOperationException("Incorrect Content-Type: " + request.ContentType); - } + if (!request.HasFormContentType) + { + throw new InvalidOperationException("Incorrect Content-Type: " + request.ContentType); + } - var features = request.HttpContext.Features; - var formFeature = features.Get(); - if (formFeature == null || formFeature.Form == null) - { - // We haven't read the form yet, replace the reader with one using our own options. - features.Set(new FormFeature(request, options)); - } - return request.ReadFormAsync(cancellationToken); + var features = request.HttpContext.Features; + var formFeature = features.Get(); + if (formFeature == null || formFeature.Form == null) + { + // We haven't read the form yet, replace the reader with one using our own options. + features.Set(new FormFeature(request, options)); } + return request.ReadFormAsync(cancellationToken); } } diff --git a/src/Http/Http/src/SendFileFallback.cs b/src/Http/Http/src/SendFileFallback.cs index 67c2910149..22ae53c101 100644 --- a/src/Http/Http/src/SendFileFallback.cs +++ b/src/Http/Http/src/SendFileFallback.cs @@ -6,55 +6,54 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Helper type that allows copying a file to a Stream. +/// +/// This type is part of ASP.NET Core's infrastructure and should not used by application code. +/// +/// +public static class SendFileFallback { /// - /// Helper type that allows copying a file to a Stream. - /// - /// This type is part of ASP.NET Core's infrastructure and should not used by application code. - /// + /// Copies the segment of the file to the destination stream. /// - public static class SendFileFallback + /// The stream to write the file segment to. + /// The full disk path to the file. + /// The offset in the file to start at. + /// The number of bytes to send, or null to send the remainder of the file. + /// A used to abort the transmission. + /// + public static async Task SendFileAsync(Stream destination, string filePath, long offset, long? count, CancellationToken cancellationToken) { - /// - /// Copies the segment of the file to the destination stream. - /// - /// The stream to write the file segment to. - /// The full disk path to the file. - /// The offset in the file to start at. - /// The number of bytes to send, or null to send the remainder of the file. - /// A used to abort the transmission. - /// - public static async Task SendFileAsync(Stream destination, string filePath, long offset, long? count, CancellationToken cancellationToken) + var fileInfo = new FileInfo(filePath); + if (offset < 0 || offset > fileInfo.Length) + { + throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty); + } + if (count.HasValue && + (count.Value < 0 || count.Value > fileInfo.Length - offset)) { - var fileInfo = new FileInfo(filePath); - if (offset < 0 || offset > fileInfo.Length) - { - throw new ArgumentOutOfRangeException(nameof(offset), offset, string.Empty); - } - if (count.HasValue && - (count.Value < 0 || count.Value > fileInfo.Length - offset)) - { - throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty); - } + throw new ArgumentOutOfRangeException(nameof(count), count, string.Empty); + } - cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - const int bufferSize = 1024 * 16; + const int bufferSize = 1024 * 16; - var fileStream = new FileStream( - filePath, - FileMode.Open, - FileAccess.Read, - FileShare.ReadWrite, - bufferSize: bufferSize, - options: FileOptions.Asynchronous | FileOptions.SequentialScan); + var fileStream = new FileStream( + filePath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + bufferSize: bufferSize, + options: FileOptions.Asynchronous | FileOptions.SequentialScan); - using (fileStream) - { - fileStream.Seek(offset, SeekOrigin.Begin); - await StreamCopyOperationInternal.CopyToAsync(fileStream, destination, count, bufferSize, cancellationToken); - } + using (fileStream) + { + fileStream.Seek(offset, SeekOrigin.Begin); + await StreamCopyOperationInternal.CopyToAsync(fileStream, destination, count, bufferSize, cancellationToken); } } } diff --git a/src/Http/Http/src/StreamResponseBodyFeature.cs b/src/Http/Http/src/StreamResponseBodyFeature.cs index 74c42cebab..fd162d953a 100644 --- a/src/Http/Http/src/StreamResponseBodyFeature.cs +++ b/src/Http/Http/src/StreamResponseBodyFeature.cs @@ -8,146 +8,145 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// An implementation of that aproximates all of the APIs over the given Stream. +/// +public class StreamResponseBodyFeature : IHttpResponseBodyFeature { + private PipeWriter? _pipeWriter; + private bool _started; + private bool _completed; + private bool _disposed; + /// - /// An implementation of that aproximates all of the APIs over the given Stream. + /// Wraps the given stream. /// - public class StreamResponseBodyFeature : IHttpResponseBodyFeature + /// + public StreamResponseBodyFeature(Stream stream) { - private PipeWriter? _pipeWriter; - private bool _started; - private bool _completed; - private bool _disposed; - - /// - /// Wraps the given stream. - /// - /// - public StreamResponseBodyFeature(Stream stream) - { - Stream = stream ?? throw new ArgumentNullException(nameof(stream)); - } + Stream = stream ?? throw new ArgumentNullException(nameof(stream)); + } - /// - /// Wraps the given stream and tracks the prior feature instance. - /// - /// - /// - public StreamResponseBodyFeature(Stream stream, IHttpResponseBodyFeature priorFeature) - { - Stream = stream ?? throw new ArgumentNullException(nameof(stream)); - PriorFeature = priorFeature; - } + /// + /// Wraps the given stream and tracks the prior feature instance. + /// + /// + /// + public StreamResponseBodyFeature(Stream stream, IHttpResponseBodyFeature priorFeature) + { + Stream = stream ?? throw new ArgumentNullException(nameof(stream)); + PriorFeature = priorFeature; + } - /// - /// The original response body stream. - /// - public Stream Stream { get; } + /// + /// The original response body stream. + /// + public Stream Stream { get; } - /// - /// The prior feature, if any. - /// - public IHttpResponseBodyFeature? PriorFeature { get; } + /// + /// The prior feature, if any. + /// + public IHttpResponseBodyFeature? PriorFeature { get; } - /// - /// A PipeWriter adapted over the given stream. - /// - public PipeWriter Writer + /// + /// A PipeWriter adapted over the given stream. + /// + public PipeWriter Writer + { + get { - get + if (_pipeWriter == null) { - if (_pipeWriter == null) + _pipeWriter = PipeWriter.Create(Stream, new StreamPipeWriterOptions(leaveOpen: true)); + if (_completed) { - _pipeWriter = PipeWriter.Create(Stream, new StreamPipeWriterOptions(leaveOpen: true)); - if (_completed) - { - _pipeWriter.Complete(); - } + _pipeWriter.Complete(); } - - return _pipeWriter; } + + return _pipeWriter; } + } - /// - /// Opts out of write buffering for the response. - /// - public virtual void DisableBuffering() + /// + /// Opts out of write buffering for the response. + /// + public virtual void DisableBuffering() + { + PriorFeature?.DisableBuffering(); + } + + /// + /// Copies the specified file segment to the given response stream. + /// This calls StartAsync if it has not previously been called. + /// + /// The full disk path to the file. + /// The offset in the file to start at. + /// The number of bytes to send, or null to send the remainder of the file. + /// A used to abort the transmission. + /// + public virtual async Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken) + { + if (!_started) { - PriorFeature?.DisableBuffering(); + await StartAsync(cancellationToken); } + await SendFileFallback.SendFileAsync(Stream, path, offset, count, cancellationToken); + } - /// - /// Copies the specified file segment to the given response stream. - /// This calls StartAsync if it has not previously been called. - /// - /// The full disk path to the file. - /// The offset in the file to start at. - /// The number of bytes to send, or null to send the remainder of the file. - /// A used to abort the transmission. - /// - public virtual async Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellationToken) + /// + /// Flushes the given stream if this has not previously been called. + /// + /// + /// + public virtual Task StartAsync(CancellationToken cancellationToken = default) + { + if (!_started) { - if (!_started) - { - await StartAsync(cancellationToken); - } - await SendFileFallback.SendFileAsync(Stream, path, offset, count, cancellationToken); + _started = true; + return Stream.FlushAsync(cancellationToken); } + return Task.CompletedTask; + } - /// - /// Flushes the given stream if this has not previously been called. - /// - /// - /// - public virtual Task StartAsync(CancellationToken cancellationToken = default) + /// + /// This calls StartAsync if it has not previously been called. + /// It will complete the adapted pipe if it exists. + /// + /// + public virtual async Task CompleteAsync() + { + // CompleteAsync is registered with HttpResponse.OnCompleted and there's no way to unregister it. + // Prevent it from running by marking as disposed. + if (_disposed) { - if (!_started) - { - _started = true; - return Stream.FlushAsync(cancellationToken); - } - return Task.CompletedTask; + return; } - - /// - /// This calls StartAsync if it has not previously been called. - /// It will complete the adapted pipe if it exists. - /// - /// - public virtual async Task CompleteAsync() + if (_completed) { - // CompleteAsync is registered with HttpResponse.OnCompleted and there's no way to unregister it. - // Prevent it from running by marking as disposed. - if (_disposed) - { - return; - } - if (_completed) - { - return; - } - - if (!_started) - { - await StartAsync(); - } - - _completed = true; + return; + } - if (_pipeWriter != null) - { - await _pipeWriter.CompleteAsync(); - } + if (!_started) + { + await StartAsync(); } - /// - /// Prevents CompleteAsync from operating. - /// - public void Dispose() + _completed = true; + + if (_pipeWriter != null) { - _disposed = true; + await _pipeWriter.CompleteAsync(); } } + + /// + /// Prevents CompleteAsync from operating. + /// + public void Dispose() + { + _disposed = true; + } } diff --git a/src/Http/Http/test/ApplicationBuilderTests.cs b/src/Http/Http/test/ApplicationBuilderTests.cs index bde96e5b5b..0a6d08d178 100644 --- a/src/Http/Http/test/ApplicationBuilderTests.cs +++ b/src/Http/Http/test/ApplicationBuilderTests.cs @@ -6,90 +6,89 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; -namespace Microsoft.AspNetCore.Builder.Internal +namespace Microsoft.AspNetCore.Builder.Internal; + +public class ApplicationBuilderTests { - public class ApplicationBuilderTests + [Fact] + public void BuildReturnsCallableDelegate() { - [Fact] - public void BuildReturnsCallableDelegate() - { - var builder = new ApplicationBuilder(null); - var app = builder.Build(); + var builder = new ApplicationBuilder(null); + var app = builder.Build(); - var httpContext = new DefaultHttpContext(); + var httpContext = new DefaultHttpContext(); - app.Invoke(httpContext); - Assert.Equal(404, httpContext.Response.StatusCode); - } + app.Invoke(httpContext); + Assert.Equal(404, httpContext.Response.StatusCode); + } - [Fact] - public async Task BuildImplicitlyThrowsForMatchedEndpointAsLastStep() - { - var builder = new ApplicationBuilder(null); - var app = builder.Build(); - - var endpointCalled = false; - var endpoint = new Endpoint( - context => - { - endpointCalled = true; - return Task.CompletedTask; - }, - EndpointMetadataCollection.Empty, - "Test endpoint"); - - var httpContext = new DefaultHttpContext(); - httpContext.SetEndpoint(endpoint); - - var ex = await Assert.ThrowsAsync(() => app.Invoke(httpContext)); - - var expected = - "The request reached the end of the pipeline without executing the endpoint: 'Test endpoint'. " + - "Please register the EndpointMiddleware using 'IApplicationBuilder.UseEndpoints(...)' if " + - "using routing."; - Assert.Equal(expected, ex.Message); - Assert.False(endpointCalled); - } - - [Fact] - public void BuildDoesNotCallMatchedEndpointWhenTerminated() - { - var builder = new ApplicationBuilder(null); - builder.Run(context => + [Fact] + public async Task BuildImplicitlyThrowsForMatchedEndpointAsLastStep() + { + var builder = new ApplicationBuilder(null); + var app = builder.Build(); + + var endpointCalled = false; + var endpoint = new Endpoint( + context => { + endpointCalled = true; + return Task.CompletedTask; + }, + EndpointMetadataCollection.Empty, + "Test endpoint"); + + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(endpoint); + + var ex = await Assert.ThrowsAsync(() => app.Invoke(httpContext)); + + var expected = + "The request reached the end of the pipeline without executing the endpoint: 'Test endpoint'. " + + "Please register the EndpointMiddleware using 'IApplicationBuilder.UseEndpoints(...)' if " + + "using routing."; + Assert.Equal(expected, ex.Message); + Assert.False(endpointCalled); + } + + [Fact] + public void BuildDoesNotCallMatchedEndpointWhenTerminated() + { + var builder = new ApplicationBuilder(null); + builder.Run(context => + { // Do not call next return Task.CompletedTask; - }); - var app = builder.Build(); + }); + var app = builder.Build(); - var endpointCalled = false; - var endpoint = new Endpoint( - context => - { - endpointCalled = true; - return Task.CompletedTask; - }, - EndpointMetadataCollection.Empty, - "Test endpoint"); + var endpointCalled = false; + var endpoint = new Endpoint( + context => + { + endpointCalled = true; + return Task.CompletedTask; + }, + EndpointMetadataCollection.Empty, + "Test endpoint"); - var httpContext = new DefaultHttpContext(); - httpContext.SetEndpoint(endpoint); + var httpContext = new DefaultHttpContext(); + httpContext.SetEndpoint(endpoint); - app.Invoke(httpContext); + app.Invoke(httpContext); - Assert.False(endpointCalled); - } + Assert.False(endpointCalled); + } - [Fact] - public void PropertiesDictionaryIsDistinctAfterNew() - { - var builder1 = new ApplicationBuilder(null); - builder1.Properties["test"] = "value1"; + [Fact] + public void PropertiesDictionaryIsDistinctAfterNew() + { + var builder1 = new ApplicationBuilder(null); + builder1.Properties["test"] = "value1"; - var builder2 = builder1.New(); - builder2.Properties["test"] = "value2"; + var builder2 = builder1.New(); + builder2.Properties["test"] = "value2"; - Assert.Equal("value1", builder1.Properties["test"]); - } + Assert.Equal("value1", builder1.Properties["test"]); } } diff --git a/src/Http/Http/test/BindingAddressTests.cs b/src/Http/Http/test/BindingAddressTests.cs index edf5b1e044..37ad03919f 100644 --- a/src/Http/Http/test/BindingAddressTests.cs +++ b/src/Http/Http/test/BindingAddressTests.cs @@ -5,109 +5,108 @@ using System; using Microsoft.AspNetCore.Testing; using Xunit; -namespace Microsoft.AspNetCore.Http.Tests +namespace Microsoft.AspNetCore.Http.Tests; + +public class BindingAddressTests { - public class BindingAddressTests + [Theory] + [InlineData("")] + [InlineData("5000")] + [InlineData("//noscheme")] + public void FromUriThrowsForUrlsWithoutSchemeDelimiter(string url) { - [Theory] - [InlineData("")] - [InlineData("5000")] - [InlineData("//noscheme")] - public void FromUriThrowsForUrlsWithoutSchemeDelimiter(string url) - { - Assert.Throws(() => BindingAddress.Parse(url)); - } - - [Theory] - [InlineData("://")] - [InlineData("://:5000")] - [InlineData("http://")] - [InlineData("http://:5000")] - [InlineData("http:///")] - [InlineData("http:///:5000")] - [InlineData("http:////")] - [InlineData("http:////:5000")] - public void FromUriThrowsForUrlsWithoutHost(string url) - { - Assert.Throws(() => BindingAddress.Parse(url)); - } + Assert.Throws(() => BindingAddress.Parse(url)); + } - [ConditionalTheory] - [InlineData("http://unix:/")] - [InlineData("http://unix:/c")] - [InlineData("http://unix:/wrong.path")] - [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows has drive letters and volume separator (c:), testing this url on unix or osx provides completely different output.")] - public void FromUriThrowsForUrlsWithWrongFilePathOnWindows(string url) - { - Assert.Throws(() => BindingAddress.Parse(url)); - } + [Theory] + [InlineData("://")] + [InlineData("://:5000")] + [InlineData("http://")] + [InlineData("http://:5000")] + [InlineData("http:///")] + [InlineData("http:///:5000")] + [InlineData("http:////")] + [InlineData("http:////:5000")] + public void FromUriThrowsForUrlsWithoutHost(string url) + { + Assert.Throws(() => BindingAddress.Parse(url)); + } - [Theory] - [InlineData("://emptyscheme", "", "emptyscheme", 0, "", "://emptyscheme:0")] - [InlineData("http://+", "http", "+", 80, "", "http://+:80")] - [InlineData("http://*", "http", "*", 80, "", "http://*:80")] - [InlineData("http://localhost", "http", "localhost", 80, "", "http://localhost:80")] - [InlineData("http://www.example.com", "http", "www.example.com", 80, "", "http://www.example.com:80")] - [InlineData("https://www.example.com", "https", "www.example.com", 443, "", "https://www.example.com:443")] - [InlineData("http://www.example.com/", "http", "www.example.com", 80, "", "http://www.example.com:80")] - [InlineData("http://www.example.com/foo?bar=baz", "http", "www.example.com", 80, "/foo?bar=baz", "http://www.example.com:80/foo?bar=baz")] - [InlineData("http://www.example.com:5000", "http", "www.example.com", 5000, "", null)] - [InlineData("https://www.example.com:5000", "https", "www.example.com", 5000, "", null)] - [InlineData("http://www.example.com:5000/", "http", "www.example.com", 5000, "", "http://www.example.com:5000")] - [InlineData("http://www.example.com:NOTAPORT", "http", "www.example.com:NOTAPORT", 80, "", "http://www.example.com:notaport:80")] - [InlineData("https://www.example.com:NOTAPORT", "https", "www.example.com:NOTAPORT", 443, "", "https://www.example.com:notaport:443")] - [InlineData("http://www.example.com:NOTAPORT/", "http", "www.example.com:NOTAPORT", 80, "", "http://www.example.com:notaport:80")] - [InlineData("http://foo:/tmp/kestrel-test.sock:5000/doesn't/matter", "http", "foo:", 80, "/tmp/kestrel-test.sock:5000/doesn't/matter", "http://foo::80/tmp/kestrel-test.sock:5000/doesn't/matter")] - [InlineData("http://unix:foo/tmp/kestrel-test.sock", "http", "unix:foo", 80, "/tmp/kestrel-test.sock", "http://unix:foo:80/tmp/kestrel-test.sock")] - [InlineData("http://unix:5000/tmp/kestrel-test.sock", "http", "unix", 5000, "/tmp/kestrel-test.sock", "http://unix:5000/tmp/kestrel-test.sock")] - public void UrlsAreParsedCorrectly(string url, string scheme, string host, int port, string pathBase, string toString) - { - var serverAddress = BindingAddress.Parse(url); + [ConditionalTheory] + [InlineData("http://unix:/")] + [InlineData("http://unix:/c")] + [InlineData("http://unix:/wrong.path")] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows has drive letters and volume separator (c:), testing this url on unix or osx provides completely different output.")] + public void FromUriThrowsForUrlsWithWrongFilePathOnWindows(string url) + { + Assert.Throws(() => BindingAddress.Parse(url)); + } - Assert.Equal(scheme, serverAddress.Scheme); - Assert.Equal(host, serverAddress.Host); - Assert.Equal(port, serverAddress.Port); - Assert.Equal(pathBase, serverAddress.PathBase); + [Theory] + [InlineData("://emptyscheme", "", "emptyscheme", 0, "", "://emptyscheme:0")] + [InlineData("http://+", "http", "+", 80, "", "http://+:80")] + [InlineData("http://*", "http", "*", 80, "", "http://*:80")] + [InlineData("http://localhost", "http", "localhost", 80, "", "http://localhost:80")] + [InlineData("http://www.example.com", "http", "www.example.com", 80, "", "http://www.example.com:80")] + [InlineData("https://www.example.com", "https", "www.example.com", 443, "", "https://www.example.com:443")] + [InlineData("http://www.example.com/", "http", "www.example.com", 80, "", "http://www.example.com:80")] + [InlineData("http://www.example.com/foo?bar=baz", "http", "www.example.com", 80, "/foo?bar=baz", "http://www.example.com:80/foo?bar=baz")] + [InlineData("http://www.example.com:5000", "http", "www.example.com", 5000, "", null)] + [InlineData("https://www.example.com:5000", "https", "www.example.com", 5000, "", null)] + [InlineData("http://www.example.com:5000/", "http", "www.example.com", 5000, "", "http://www.example.com:5000")] + [InlineData("http://www.example.com:NOTAPORT", "http", "www.example.com:NOTAPORT", 80, "", "http://www.example.com:notaport:80")] + [InlineData("https://www.example.com:NOTAPORT", "https", "www.example.com:NOTAPORT", 443, "", "https://www.example.com:notaport:443")] + [InlineData("http://www.example.com:NOTAPORT/", "http", "www.example.com:NOTAPORT", 80, "", "http://www.example.com:notaport:80")] + [InlineData("http://foo:/tmp/kestrel-test.sock:5000/doesn't/matter", "http", "foo:", 80, "/tmp/kestrel-test.sock:5000/doesn't/matter", "http://foo::80/tmp/kestrel-test.sock:5000/doesn't/matter")] + [InlineData("http://unix:foo/tmp/kestrel-test.sock", "http", "unix:foo", 80, "/tmp/kestrel-test.sock", "http://unix:foo:80/tmp/kestrel-test.sock")] + [InlineData("http://unix:5000/tmp/kestrel-test.sock", "http", "unix", 5000, "/tmp/kestrel-test.sock", "http://unix:5000/tmp/kestrel-test.sock")] + public void UrlsAreParsedCorrectly(string url, string scheme, string host, int port, string pathBase, string toString) + { + var serverAddress = BindingAddress.Parse(url); - Assert.Equal(toString ?? url, serverAddress.ToString()); - } + Assert.Equal(scheme, serverAddress.Scheme); + Assert.Equal(host, serverAddress.Host); + Assert.Equal(port, serverAddress.Port); + Assert.Equal(pathBase, serverAddress.PathBase); - [ConditionalTheory] - [InlineData("http://unix:/tmp/kestrel-test.sock", "http", "unix:/tmp/kestrel-test.sock", 0, "", null)] - [InlineData("https://unix:/tmp/kestrel-test.sock", "https", "unix:/tmp/kestrel-test.sock", 0, "", null)] - [InlineData("http://unix:/tmp/kestrel-test.sock:", "http", "unix:/tmp/kestrel-test.sock", 0, "", "http://unix:/tmp/kestrel-test.sock")] - [InlineData("http://unix:/tmp/kestrel-test.sock:/", "http", "unix:/tmp/kestrel-test.sock", 0, "", "http://unix:/tmp/kestrel-test.sock")] - [InlineData("http://unix:/tmp/kestrel-test.sock:5000/doesn't/matter", "http", "unix:/tmp/kestrel-test.sock", 0, "5000/doesn't/matter", "http://unix:/tmp/kestrel-test.sock")] - [OSSkipCondition(OperatingSystems.Windows)] - public void UnixSocketUrlsAreParsedCorrectlyOnUnix(string url, string scheme, string host, int port, string pathBase, string toString) - { - var serverAddress = BindingAddress.Parse(url); + Assert.Equal(toString ?? url, serverAddress.ToString()); + } - Assert.Equal(scheme, serverAddress.Scheme); - Assert.Equal(host, serverAddress.Host); - Assert.Equal(port, serverAddress.Port); - Assert.Equal(pathBase, serverAddress.PathBase); + [ConditionalTheory] + [InlineData("http://unix:/tmp/kestrel-test.sock", "http", "unix:/tmp/kestrel-test.sock", 0, "", null)] + [InlineData("https://unix:/tmp/kestrel-test.sock", "https", "unix:/tmp/kestrel-test.sock", 0, "", null)] + [InlineData("http://unix:/tmp/kestrel-test.sock:", "http", "unix:/tmp/kestrel-test.sock", 0, "", "http://unix:/tmp/kestrel-test.sock")] + [InlineData("http://unix:/tmp/kestrel-test.sock:/", "http", "unix:/tmp/kestrel-test.sock", 0, "", "http://unix:/tmp/kestrel-test.sock")] + [InlineData("http://unix:/tmp/kestrel-test.sock:5000/doesn't/matter", "http", "unix:/tmp/kestrel-test.sock", 0, "5000/doesn't/matter", "http://unix:/tmp/kestrel-test.sock")] + [OSSkipCondition(OperatingSystems.Windows)] + public void UnixSocketUrlsAreParsedCorrectlyOnUnix(string url, string scheme, string host, int port, string pathBase, string toString) + { + var serverAddress = BindingAddress.Parse(url); - Assert.Equal(toString ?? url, serverAddress.ToString()); - } + Assert.Equal(scheme, serverAddress.Scheme); + Assert.Equal(host, serverAddress.Host); + Assert.Equal(port, serverAddress.Port); + Assert.Equal(pathBase, serverAddress.PathBase); - [ConditionalTheory] - [InlineData("http://unix:/c:/foo/bar/pipe.socket", "http", "unix:/c:/foo/bar/pipe.socket", 0, "", null)] - [InlineData("http://unix:/c:/foo/bar/pipe.socket:", "http", "unix:/c:/foo/bar/pipe.socket", 0, "", "http://unix:/c:/foo/bar/pipe.socket")] - [InlineData("http://unix:/c:/foo/bar/pipe.socket:/", "http", "unix:/c:/foo/bar/pipe.socket", 0, "", "http://unix:/c:/foo/bar/pipe.socket")] - [InlineData("http://unix:/c:/foo/bar/pipe.socket:5000/doesn't/matter", "http", "unix:/c:/foo/bar/pipe.socket", 0, "5000/doesn't/matter", "http://unix:/c:/foo/bar/pipe.socket")] - [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows has drive letters and volume separator (c:), testing this url on unix or osx provides completely different output.")] - public void UnixSocketUrlsAreParsedCorrectlyOnWindows(string url, string scheme, string host, int port, string pathBase, string toString) - { - var serverAddress = BindingAddress.Parse(url); + Assert.Equal(toString ?? url, serverAddress.ToString()); + } - Assert.Equal(scheme, serverAddress.Scheme); - Assert.Equal(host, serverAddress.Host); - Assert.Equal(port, serverAddress.Port); - Assert.Equal(pathBase, serverAddress.PathBase); + [ConditionalTheory] + [InlineData("http://unix:/c:/foo/bar/pipe.socket", "http", "unix:/c:/foo/bar/pipe.socket", 0, "", null)] + [InlineData("http://unix:/c:/foo/bar/pipe.socket:", "http", "unix:/c:/foo/bar/pipe.socket", 0, "", "http://unix:/c:/foo/bar/pipe.socket")] + [InlineData("http://unix:/c:/foo/bar/pipe.socket:/", "http", "unix:/c:/foo/bar/pipe.socket", 0, "", "http://unix:/c:/foo/bar/pipe.socket")] + [InlineData("http://unix:/c:/foo/bar/pipe.socket:5000/doesn't/matter", "http", "unix:/c:/foo/bar/pipe.socket", 0, "5000/doesn't/matter", "http://unix:/c:/foo/bar/pipe.socket")] + [OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX, SkipReason = "Windows has drive letters and volume separator (c:), testing this url on unix or osx provides completely different output.")] + public void UnixSocketUrlsAreParsedCorrectlyOnWindows(string url, string scheme, string host, int port, string pathBase, string toString) + { + var serverAddress = BindingAddress.Parse(url); - Assert.Equal(toString ?? url, serverAddress.ToString()); - } + Assert.Equal(scheme, serverAddress.Scheme); + Assert.Equal(host, serverAddress.Host); + Assert.Equal(port, serverAddress.Port); + Assert.Equal(pathBase, serverAddress.PathBase); + Assert.Equal(toString ?? url, serverAddress.ToString()); } + } diff --git a/src/Http/Http/test/DefaultHttpContextTests.cs b/src/Http/Http/test/DefaultHttpContextTests.cs index ce59fa8043..6a3561eaad 100644 --- a/src/Http/Http/test/DefaultHttpContextTests.cs +++ b/src/Http/Http/test/DefaultHttpContextTests.cs @@ -15,518 +15,517 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +public class DefaultHttpContextTests { - public class DefaultHttpContextTests + [Fact] + public void GetOnSessionProperty_ThrowsOnMissingSessionFeature() { - [Fact] - public void GetOnSessionProperty_ThrowsOnMissingSessionFeature() - { - // Arrange - var context = new DefaultHttpContext(); + // Arrange + var context = new DefaultHttpContext(); - // Act & Assert - var exception = Assert.Throws(() => context.Session); - Assert.Equal("Session has not been configured for this application or request.", exception.Message); - } + // Act & Assert + var exception = Assert.Throws(() => context.Session); + Assert.Equal("Session has not been configured for this application or request.", exception.Message); + } - [Fact] - public void GetOnSessionProperty_ReturnsAvailableSession() - { - // Arrange - var context = new DefaultHttpContext(); - var session = new TestSession(); - session.Set("key1", null); - session.Set("key2", null); - var feature = new BlahSessionFeature(); - feature.Session = session; - context.Features.Set(feature); - - // Act & Assert - Assert.Same(session, context.Session); - context.Session.Set("key3", null); - Assert.Equal(3, context.Session.Keys.Count()); - } + [Fact] + public void GetOnSessionProperty_ReturnsAvailableSession() + { + // Arrange + var context = new DefaultHttpContext(); + var session = new TestSession(); + session.Set("key1", null); + session.Set("key2", null); + var feature = new BlahSessionFeature(); + feature.Session = session; + context.Features.Set(feature); + + // Act & Assert + Assert.Same(session, context.Session); + context.Session.Set("key3", null); + Assert.Equal(3, context.Session.Keys.Count()); + } - [Fact] - public void AllowsSettingSession_WithoutSettingUpSessionFeature_Upfront() - { - // Arrange - var session = new TestSession(); - var context = new DefaultHttpContext(); + [Fact] + public void AllowsSettingSession_WithoutSettingUpSessionFeature_Upfront() + { + // Arrange + var session = new TestSession(); + var context = new DefaultHttpContext(); - // Act - context.Session = session; + // Act + context.Session = session; - // Assert - Assert.Same(session, context.Session); - } + // Assert + Assert.Same(session, context.Session); + } - [Fact] - public void SettingSession_OverridesAvailableSession() - { - // Arrange - var context = new DefaultHttpContext(); - var session = new TestSession(); - session.Set("key1", null); - session.Set("key2", null); - var feature = new BlahSessionFeature(); - feature.Session = session; - context.Features.Set(feature); - - // Act - context.Session = new TestSession(); - - // Assert - Assert.NotSame(session, context.Session); - Assert.Empty(context.Session.Keys); - } + [Fact] + public void SettingSession_OverridesAvailableSession() + { + // Arrange + var context = new DefaultHttpContext(); + var session = new TestSession(); + session.Set("key1", null); + session.Set("key2", null); + var feature = new BlahSessionFeature(); + feature.Session = session; + context.Features.Set(feature); + + // Act + context.Session = new TestSession(); + + // Assert + Assert.NotSame(session, context.Session); + Assert.Empty(context.Session.Keys); + } - [Fact] - public void EmptyUserIsNeverNull() - { - var context = new DefaultHttpContext(new FeatureCollection()); - Assert.NotNull(context.User); - Assert.Single(context.User.Identities); - Assert.True(object.ReferenceEquals(context.User, context.User)); - Assert.False(context.User.Identity.IsAuthenticated); - Assert.True(string.IsNullOrEmpty(context.User.Identity.AuthenticationType)); - - context.User = null; - Assert.NotNull(context.User); - Assert.Single(context.User.Identities); - Assert.True(object.ReferenceEquals(context.User, context.User)); - Assert.False(context.User.Identity.IsAuthenticated); - Assert.True(string.IsNullOrEmpty(context.User.Identity.AuthenticationType)); - - context.User = new ClaimsPrincipal(); - Assert.NotNull(context.User); - Assert.Empty(context.User.Identities); - Assert.True(object.ReferenceEquals(context.User, context.User)); - Assert.Null(context.User.Identity); - - context.User = new ClaimsPrincipal(new ClaimsIdentity("SomeAuthType")); - Assert.Equal("SomeAuthType", context.User.Identity.AuthenticationType); - Assert.True(context.User.Identity.IsAuthenticated); - } + [Fact] + public void EmptyUserIsNeverNull() + { + var context = new DefaultHttpContext(new FeatureCollection()); + Assert.NotNull(context.User); + Assert.Single(context.User.Identities); + Assert.True(object.ReferenceEquals(context.User, context.User)); + Assert.False(context.User.Identity.IsAuthenticated); + Assert.True(string.IsNullOrEmpty(context.User.Identity.AuthenticationType)); + + context.User = null; + Assert.NotNull(context.User); + Assert.Single(context.User.Identities); + Assert.True(object.ReferenceEquals(context.User, context.User)); + Assert.False(context.User.Identity.IsAuthenticated); + Assert.True(string.IsNullOrEmpty(context.User.Identity.AuthenticationType)); + + context.User = new ClaimsPrincipal(); + Assert.NotNull(context.User); + Assert.Empty(context.User.Identities); + Assert.True(object.ReferenceEquals(context.User, context.User)); + Assert.Null(context.User.Identity); + + context.User = new ClaimsPrincipal(new ClaimsIdentity("SomeAuthType")); + Assert.Equal("SomeAuthType", context.User.Identity.AuthenticationType); + Assert.True(context.User.Identity.IsAuthenticated); + } - [Fact] - public void GetItems_DefaultCollectionProvided() - { - var context = new DefaultHttpContext(new FeatureCollection()); - Assert.Null(context.Features.Get()); - var items = context.Items; - Assert.NotNull(context.Features.Get()); - Assert.NotNull(items); - Assert.Same(items, context.Items); - var item = new object(); - context.Items["foo"] = item; - Assert.Same(item, context.Items["foo"]); - } + [Fact] + public void GetItems_DefaultCollectionProvided() + { + var context = new DefaultHttpContext(new FeatureCollection()); + Assert.Null(context.Features.Get()); + var items = context.Items; + Assert.NotNull(context.Features.Get()); + Assert.NotNull(items); + Assert.Same(items, context.Items); + var item = new object(); + context.Items["foo"] = item; + Assert.Same(item, context.Items["foo"]); + } - [Fact] - public void GetItems_DefaultRequestIdentifierAvailable() - { - var context = new DefaultHttpContext(new FeatureCollection()); - Assert.Null(context.Features.Get()); - var traceIdentifier = context.TraceIdentifier; - Assert.NotNull(context.Features.Get()); - Assert.NotNull(traceIdentifier); - Assert.Same(traceIdentifier, context.TraceIdentifier); - - context.TraceIdentifier = "Hello"; - Assert.Same("Hello", context.TraceIdentifier); - } + [Fact] + public void GetItems_DefaultRequestIdentifierAvailable() + { + var context = new DefaultHttpContext(new FeatureCollection()); + Assert.Null(context.Features.Get()); + var traceIdentifier = context.TraceIdentifier; + Assert.NotNull(context.Features.Get()); + Assert.NotNull(traceIdentifier); + Assert.Same(traceIdentifier, context.TraceIdentifier); + + context.TraceIdentifier = "Hello"; + Assert.Same("Hello", context.TraceIdentifier); + } - [Fact] - public void SetItems_NewCollectionUsed() - { - var context = new DefaultHttpContext(new FeatureCollection()); - Assert.Null(context.Features.Get()); - var items = new Dictionary(); - context.Items = items; - Assert.NotNull(context.Features.Get()); - Assert.Same(items, context.Items); - var item = new object(); - items["foo"] = item; - Assert.Same(item, context.Items["foo"]); - } + [Fact] + public void SetItems_NewCollectionUsed() + { + var context = new DefaultHttpContext(new FeatureCollection()); + Assert.Null(context.Features.Get()); + var items = new Dictionary(); + context.Items = items; + Assert.NotNull(context.Features.Get()); + Assert.Same(items, context.Items); + var item = new object(); + items["foo"] = item; + Assert.Same(item, context.Items["foo"]); + } - [Fact] - public void UpdateFeatures_ClearsCachedFeatures() - { - var features = new FeatureCollection(); - features.Set(new HttpRequestFeature()); - features.Set(new HttpResponseFeature()); - features.Set(new StreamResponseBodyFeature(Stream.Null)); - features.Set(new TestHttpWebSocketFeature()); - - // FeatureCollection is set. all cached interfaces are null. - var context = new DefaultHttpContext(features); - TestAllCachedFeaturesAreNull(context, features); - Assert.Equal(4, features.Count()); - - // getting feature properties populates feature collection with defaults - TestAllCachedFeaturesAreSet(context, features); - Assert.NotEqual(4, features.Count()); - - // FeatureCollection is null. and all cached interfaces are null. - // only top level is tested because child objects are inaccessible. - context.Uninitialize(); - TestCachedFeaturesAreNull(context, null); - - - var newFeatures = new FeatureCollection(); - newFeatures.Set(new HttpRequestFeature()); - newFeatures.Set(new HttpResponseFeature()); - newFeatures.Set(new StreamResponseBodyFeature(Stream.Null)); - newFeatures.Set(new TestHttpWebSocketFeature()); - - // FeatureCollection is set to newFeatures. all cached interfaces are null. - context.Initialize(newFeatures); - TestAllCachedFeaturesAreNull(context, newFeatures); - Assert.Equal(4, newFeatures.Count()); - - // getting feature properties populates new feature collection with defaults - TestAllCachedFeaturesAreSet(context, newFeatures); - Assert.NotEqual(4, newFeatures.Count()); - } + [Fact] + public void UpdateFeatures_ClearsCachedFeatures() + { + var features = new FeatureCollection(); + features.Set(new HttpRequestFeature()); + features.Set(new HttpResponseFeature()); + features.Set(new StreamResponseBodyFeature(Stream.Null)); + features.Set(new TestHttpWebSocketFeature()); + + // FeatureCollection is set. all cached interfaces are null. + var context = new DefaultHttpContext(features); + TestAllCachedFeaturesAreNull(context, features); + Assert.Equal(4, features.Count()); + + // getting feature properties populates feature collection with defaults + TestAllCachedFeaturesAreSet(context, features); + Assert.NotEqual(4, features.Count()); + + // FeatureCollection is null. and all cached interfaces are null. + // only top level is tested because child objects are inaccessible. + context.Uninitialize(); + TestCachedFeaturesAreNull(context, null); + + + var newFeatures = new FeatureCollection(); + newFeatures.Set(new HttpRequestFeature()); + newFeatures.Set(new HttpResponseFeature()); + newFeatures.Set(new StreamResponseBodyFeature(Stream.Null)); + newFeatures.Set(new TestHttpWebSocketFeature()); + + // FeatureCollection is set to newFeatures. all cached interfaces are null. + context.Initialize(newFeatures); + TestAllCachedFeaturesAreNull(context, newFeatures); + Assert.Equal(4, newFeatures.Count()); + + // getting feature properties populates new feature collection with defaults + TestAllCachedFeaturesAreSet(context, newFeatures); + Assert.NotEqual(4, newFeatures.Count()); + } - [Fact] - public void RequestServicesAreNotOverwrittenIfAlreadySet() - { - var serviceProvider = new ServiceCollection() - .BuildServiceProvider(); + [Fact] + public void RequestServicesAreNotOverwrittenIfAlreadySet() + { + var serviceProvider = new ServiceCollection() + .BuildServiceProvider(); - var scopeFactory = serviceProvider.GetRequiredService(); + var scopeFactory = serviceProvider.GetRequiredService(); - var context = new DefaultHttpContext(); - context.ServiceScopeFactory = scopeFactory; - context.RequestServices = serviceProvider; + var context = new DefaultHttpContext(); + context.ServiceScopeFactory = scopeFactory; + context.RequestServices = serviceProvider; - Assert.Same(serviceProvider, context.RequestServices); - } + Assert.Same(serviceProvider, context.RequestServices); + } - [Fact] - public async Task RequestServicesAreDisposedOnCompleted() - { - var serviceProvider = new ServiceCollection() - .AddTransient() - .BuildServiceProvider(); + [Fact] + public async Task RequestServicesAreDisposedOnCompleted() + { + var serviceProvider = new ServiceCollection() + .AddTransient() + .BuildServiceProvider(); - var scopeFactory = serviceProvider.GetRequiredService(); - DisposableThing instance = null; + var scopeFactory = serviceProvider.GetRequiredService(); + DisposableThing instance = null; - var context = new DefaultHttpContext(); - context.ServiceScopeFactory = scopeFactory; - var responseFeature = new TestHttpResponseFeature(); - context.Features.Set(responseFeature); + var context = new DefaultHttpContext(); + context.ServiceScopeFactory = scopeFactory; + var responseFeature = new TestHttpResponseFeature(); + context.Features.Set(responseFeature); - Assert.NotNull(context.RequestServices); - Assert.Single(responseFeature.CompletedCallbacks); + Assert.NotNull(context.RequestServices); + Assert.Single(responseFeature.CompletedCallbacks); - instance = context.RequestServices.GetRequiredService(); + instance = context.RequestServices.GetRequiredService(); - var callback = responseFeature.CompletedCallbacks[0]; - await callback.callback(callback.state); + var callback = responseFeature.CompletedCallbacks[0]; + await callback.callback(callback.state); - Assert.Null(context.RequestServices); - Assert.True(instance.Disposed); - } + Assert.Null(context.RequestServices); + Assert.True(instance.Disposed); + } - [Fact] - public async Task RequestServicesAreDisposedAsynOnCompleted() - { - var serviceProvider = new AsyncDisposableServiceProvider(new ServiceCollection() - .AddTransient() - .BuildServiceProvider()); + [Fact] + public async Task RequestServicesAreDisposedAsynOnCompleted() + { + var serviceProvider = new AsyncDisposableServiceProvider(new ServiceCollection() + .AddTransient() + .BuildServiceProvider()); - var scopeFactory = serviceProvider.GetRequiredService(); - DisposableThing instance = null; + var scopeFactory = serviceProvider.GetRequiredService(); + DisposableThing instance = null; - var context = new DefaultHttpContext(); - context.ServiceScopeFactory = scopeFactory; - var responseFeature = new TestHttpResponseFeature(); - context.Features.Set(responseFeature); + var context = new DefaultHttpContext(); + context.ServiceScopeFactory = scopeFactory; + var responseFeature = new TestHttpResponseFeature(); + context.Features.Set(responseFeature); - Assert.NotNull(context.RequestServices); - Assert.Single(responseFeature.CompletedCallbacks); + Assert.NotNull(context.RequestServices); + Assert.Single(responseFeature.CompletedCallbacks); - instance = context.RequestServices.GetRequiredService(); + instance = context.RequestServices.GetRequiredService(); - var callback = responseFeature.CompletedCallbacks[0]; - await callback.callback(callback.state); + var callback = responseFeature.CompletedCallbacks[0]; + await callback.callback(callback.state); - Assert.Null(context.RequestServices); - Assert.True(instance.Disposed); - var scope = Assert.Single(serviceProvider.Scopes); - Assert.True(scope.DisposeAsyncCalled); - Assert.False(scope.DisposeCalled); - } + Assert.Null(context.RequestServices); + Assert.True(instance.Disposed); + var scope = Assert.Single(serviceProvider.Scopes); + Assert.True(scope.DisposeAsyncCalled); + Assert.False(scope.DisposeCalled); + } - [Fact] - public void InternalActiveFlagIsSetAndUnset() - { - var context = new DefaultHttpContext(); + [Fact] + public void InternalActiveFlagIsSetAndUnset() + { + var context = new DefaultHttpContext(); - Assert.False(context._active); + Assert.False(context._active); - context.Initialize(new FeatureCollection()); + context.Initialize(new FeatureCollection()); - Assert.True(context._active); + Assert.True(context._active); - context.Uninitialize(); + context.Uninitialize(); - Assert.False(context._active); - } + Assert.False(context._active); + } - void TestAllCachedFeaturesAreNull(HttpContext context, IFeatureCollection features) - { - TestCachedFeaturesAreNull(context, features); - TestCachedFeaturesAreNull(context.Request, features); - TestCachedFeaturesAreNull(context.Response, features); - TestCachedFeaturesAreNull(context.Connection, features); - TestCachedFeaturesAreNull(context.WebSockets, features); - } + void TestAllCachedFeaturesAreNull(HttpContext context, IFeatureCollection features) + { + TestCachedFeaturesAreNull(context, features); + TestCachedFeaturesAreNull(context.Request, features); + TestCachedFeaturesAreNull(context.Response, features); + TestCachedFeaturesAreNull(context.Connection, features); + TestCachedFeaturesAreNull(context.WebSockets, features); + } - void TestCachedFeaturesAreNull(object value, IFeatureCollection features) - { - var type = value.GetType(); + void TestCachedFeaturesAreNull(object value, IFeatureCollection features) + { + var type = value.GetType(); - var field = type - .GetFields(BindingFlags.NonPublic | BindingFlags.Instance) - .Single(f => - f.FieldType.GetTypeInfo().IsGenericType && - f.FieldType.GetGenericTypeDefinition() == typeof(FeatureReferences<>)); + var field = type + .GetFields(BindingFlags.NonPublic | BindingFlags.Instance) + .Single(f => + f.FieldType.GetTypeInfo().IsGenericType && + f.FieldType.GetGenericTypeDefinition() == typeof(FeatureReferences<>)); - var boxedExpectedStruct = features == null ? - Activator.CreateInstance(field.FieldType) : - Activator.CreateInstance(field.FieldType, features); + var boxedExpectedStruct = features == null ? + Activator.CreateInstance(field.FieldType) : + Activator.CreateInstance(field.FieldType, features); - var boxedActualStruct = field.GetValue(value); + var boxedActualStruct = field.GetValue(value); - Assert.Equal(boxedExpectedStruct, boxedActualStruct); - } + Assert.Equal(boxedExpectedStruct, boxedActualStruct); + } - void TestAllCachedFeaturesAreSet(HttpContext context, IFeatureCollection features) - { - TestCachedFeaturesAreSet(context, features); - TestCachedFeaturesAreSet(context.Request, features); - TestCachedFeaturesAreSet(context.Response, features); - TestCachedFeaturesAreSet(context.Connection, features); - TestCachedFeaturesAreSet(context.WebSockets, features); - } + void TestAllCachedFeaturesAreSet(HttpContext context, IFeatureCollection features) + { + TestCachedFeaturesAreSet(context, features); + TestCachedFeaturesAreSet(context.Request, features); + TestCachedFeaturesAreSet(context.Response, features); + TestCachedFeaturesAreSet(context.Connection, features); + TestCachedFeaturesAreSet(context.WebSockets, features); + } - void TestCachedFeaturesAreSet(object value, IFeatureCollection features) - { - var type = value.GetType(); + void TestCachedFeaturesAreSet(object value, IFeatureCollection features) + { + var type = value.GetType(); - var properties = type - .GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance) - .Where(p => p.PropertyType.GetTypeInfo().IsInterface); + var properties = type + .GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.PropertyType.GetTypeInfo().IsInterface); - TestFeatureProperties(value, features, properties); + TestFeatureProperties(value, features, properties); - var fields = type - .GetFields(BindingFlags.NonPublic | BindingFlags.Instance) - .Where(f => f.FieldType.GetTypeInfo().IsInterface && f.GetCustomAttribute() == null); + var fields = type + .GetFields(BindingFlags.NonPublic | BindingFlags.Instance) + .Where(f => f.FieldType.GetTypeInfo().IsInterface && f.GetCustomAttribute() == null); - foreach (var field in fields) + foreach (var field in fields) + { + if (field.FieldType == typeof(IFeatureCollection)) { - if (field.FieldType == typeof(IFeatureCollection)) - { - Assert.Same(features, field.GetValue(value)); - } - else - { - var v = field.GetValue(value); - Assert.Same(features[field.FieldType], v); - Assert.NotNull(v); - } + Assert.Same(features, field.GetValue(value)); + } + else + { + var v = field.GetValue(value); + Assert.Same(features[field.FieldType], v); + Assert.NotNull(v); } - } - private static void TestFeatureProperties(object value, IFeatureCollection features, IEnumerable properties) + } + + private static void TestFeatureProperties(object value, IFeatureCollection features, IEnumerable properties) + { + foreach (var property in properties) { - foreach (var property in properties) + if (property.PropertyType == typeof(IFeatureCollection)) { - if (property.PropertyType == typeof(IFeatureCollection)) - { - Assert.Same(features, property.GetValue(value)); - } - else + Assert.Same(features, property.GetValue(value)); + } + else + { + if (property.Name.Contains("Feature")) { - if (property.Name.Contains("Feature")) - { - var v = property.GetValue(value); - Assert.Same(features[property.PropertyType], v); - Assert.NotNull(v); - } + var v = property.GetValue(value); + Assert.Same(features[property.PropertyType], v); + Assert.NotNull(v); } } } + } + + private HttpContext CreateContext() + { + var context = new DefaultHttpContext(); + return context; + } - private HttpContext CreateContext() + private class DisposableThing : IDisposable + { + public bool Disposed { get; set; } + public void Dispose() { - var context = new DefaultHttpContext(); - return context; + Disposed = true; } + } - private class DisposableThing : IDisposable + private class TestHttpResponseFeature : IHttpResponseFeature + { + public List<(Func callback, object state)> CompletedCallbacks = new List<(Func callback, object state)>(); + + public int StatusCode { get; set; } + public string ReasonPhrase { get; set; } + public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); + public Stream Body { get; set; } + + public bool HasStarted => false; + + public void OnCompleted(Func callback, object state) { - public bool Disposed { get; set; } - public void Dispose() - { - Disposed = true; - } + CompletedCallbacks.Add((callback, state)); } - private class TestHttpResponseFeature : IHttpResponseFeature + public void OnStarting(Func callback, object state) { - public List<(Func callback, object state)> CompletedCallbacks = new List<(Func callback, object state)>(); + } + } + + private class TestSession : ISession + { + private readonly Dictionary _store + = new Dictionary(StringComparer.OrdinalIgnoreCase); - public int StatusCode { get; set; } - public string ReasonPhrase { get; set; } - public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); - public Stream Body { get; set; } + public string Id { get; set; } - public bool HasStarted => false; + public bool IsAvailable { get; } = true; - public void OnCompleted(Func callback, object state) - { - CompletedCallbacks.Add((callback, state)); - } + public IEnumerable Keys { get { return _store.Keys; } } - public void OnStarting(Func callback, object state) - { - } + public void Clear() + { + _store.Clear(); } - private class TestSession : ISession + public Task CommitAsync(CancellationToken cancellationToken) { - private readonly Dictionary _store - = new Dictionary(StringComparer.OrdinalIgnoreCase); + return Task.FromResult(0); + } - public string Id { get; set; } + public Task LoadAsync(CancellationToken cancellationToken) + { + return Task.FromResult(0); + } - public bool IsAvailable { get; } = true; + public void Remove(string key) + { + _store.Remove(key); + } - public IEnumerable Keys { get { return _store.Keys; } } + public void Set(string key, byte[] value) + { + _store[key] = value; + } - public void Clear() - { - _store.Clear(); - } + public bool TryGetValue(string key, out byte[] value) + { + return _store.TryGetValue(key, out value); + } + } - public Task CommitAsync(CancellationToken cancellationToken) - { - return Task.FromResult(0); - } + private class BlahSessionFeature : ISessionFeature + { + public ISession Session { get; set; } + } - public Task LoadAsync(CancellationToken cancellationToken) + private class TestHttpWebSocketFeature : IHttpWebSocketFeature + { + public bool IsWebSocketRequest + { + get { - return Task.FromResult(0); + throw new NotImplementedException(); } + } - public void Remove(string key) - { - _store.Remove(key); - } + public Task AcceptAsync(WebSocketAcceptContext context) + { + throw new NotImplementedException(); + } + } - public void Set(string key, byte[] value) - { - _store[key] = value; - } + private class AsyncDisposableServiceProvider : IServiceProvider, IDisposable, IServiceScopeFactory + { + private readonly ServiceProvider _serviceProvider; + + public AsyncDisposableServiceProvider(ServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } - public bool TryGetValue(string key, out byte[] value) + public List Scopes { get; } = new List(); + + public object GetService(Type serviceType) + { + if (serviceType == typeof(IServiceScopeFactory)) { - return _store.TryGetValue(key, out value); + return this; } + + return _serviceProvider.GetService(serviceType); } - private class BlahSessionFeature : ISessionFeature + public void Dispose() { - public ISession Session { get; set; } + _serviceProvider.Dispose(); } - private class TestHttpWebSocketFeature : IHttpWebSocketFeature + public IServiceScope CreateScope() { - public bool IsWebSocketRequest - { - get - { - throw new NotImplementedException(); - } - } - - public Task AcceptAsync(WebSocketAcceptContext context) - { - throw new NotImplementedException(); - } + var scope = new AsyncServiceScope(_serviceProvider.GetService().CreateScope()); + Scopes.Add(scope); + return scope; } - private class AsyncDisposableServiceProvider : IServiceProvider, IDisposable, IServiceScopeFactory + internal class AsyncServiceScope : IServiceScope, IAsyncDisposable { - private readonly ServiceProvider _serviceProvider; + private readonly IServiceScope _scope; - public AsyncDisposableServiceProvider(ServiceProvider serviceProvider) + public AsyncServiceScope(IServiceScope scope) { - _serviceProvider = serviceProvider; + _scope = scope; } - public List Scopes { get; } = new List(); - - public object GetService(Type serviceType) - { - if (serviceType == typeof(IServiceScopeFactory)) - { - return this; - } + public bool DisposeCalled { get; set; } - return _serviceProvider.GetService(serviceType); - } + public bool DisposeAsyncCalled { get; set; } public void Dispose() { - _serviceProvider.Dispose(); + DisposeCalled = true; + _scope.Dispose(); } - public IServiceScope CreateScope() + public ValueTask DisposeAsync() { - var scope = new AsyncServiceScope(_serviceProvider.GetService().CreateScope()); - Scopes.Add(scope); - return scope; + DisposeAsyncCalled = true; + _scope.Dispose(); + return default; } - internal class AsyncServiceScope : IServiceScope, IAsyncDisposable - { - private readonly IServiceScope _scope; - - public AsyncServiceScope(IServiceScope scope) - { - _scope = scope; - } - - public bool DisposeCalled { get; set; } - - public bool DisposeAsyncCalled { get; set; } - - public void Dispose() - { - DisposeCalled = true; - _scope.Dispose(); - } - - public ValueTask DisposeAsync() - { - DisposeAsyncCalled = true; - _scope.Dispose(); - return default; - } - - public IServiceProvider ServiceProvider => _scope.ServiceProvider; - } + public IServiceProvider ServiceProvider => _scope.ServiceProvider; } } } diff --git a/src/Http/Http/test/Features/FakeResponseFeature.cs b/src/Http/Http/test/Features/FakeResponseFeature.cs index 854d745ffc..be4239ce4d 100644 --- a/src/Http/Http/test/Features/FakeResponseFeature.cs +++ b/src/Http/Http/test/Features/FakeResponseFeature.cs @@ -5,25 +5,24 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +public class FakeResponseFeature : HttpResponseFeature { - public class FakeResponseFeature : HttpResponseFeature - { - List, object>> _onCompletedCallbacks = new List, object>>(); + List, object>> _onCompletedCallbacks = new List, object>>(); - public override void OnCompleted(Func callback, object state) - { - _onCompletedCallbacks.Add(new Tuple, object>(callback, state)); - } + public override void OnCompleted(Func callback, object state) + { + _onCompletedCallbacks.Add(new Tuple, object>(callback, state)); + } - public async Task CompleteAsync() + public async Task CompleteAsync() + { + var callbacks = _onCompletedCallbacks; + _onCompletedCallbacks = null; + foreach (var callback in callbacks) { - var callbacks = _onCompletedCallbacks; - _onCompletedCallbacks = null; - foreach (var callback in callbacks) - { - await callback.Item1(callback.Item2); - } + await callback.Item1(callback.Item2); } } } diff --git a/src/Http/Http/test/Features/FormFeatureTests.cs b/src/Http/Http/test/Features/FormFeatureTests.cs index 9426ce6dd1..b9a3498485 100644 --- a/src/Http/Http/test/Features/FormFeatureTests.cs +++ b/src/Http/Http/test/Features/FormFeatureTests.cs @@ -9,621 +9,620 @@ using System.Text; using System.Threading.Tasks; using Xunit; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +public class FormFeatureTests { - public class FormFeatureTests + [Fact] + public async Task ReadFormAsync_0ContentLength_ReturnsEmptyForm() { - [Fact] - public async Task ReadFormAsync_0ContentLength_ReturnsEmptyForm() - { - var context = new DefaultHttpContext(); - var responseFeature = new FakeResponseFeature(); - context.Features.Set(responseFeature); - context.Request.ContentType = MultipartContentType; - context.Request.ContentLength = 0; + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.ContentLength = 0; - var formFeature = new FormFeature(context.Request, new FormOptions()); - context.Features.Set(formFeature); + var formFeature = new FormFeature(context.Request, new FormOptions()); + context.Features.Set(formFeature); - var formCollection = await context.Request.ReadFormAsync(); + var formCollection = await context.Request.ReadFormAsync(); - Assert.Same(FormCollection.Empty, formCollection); - } + Assert.Same(FormCollection.Empty, formCollection); + } - [Fact] - public async Task FormFeatureReadsOptionsFromDefaultHttpContext() + [Fact] + public async Task FormFeatureReadsOptionsFromDefaultHttpContext() + { + var context = new DefaultHttpContext(); + context.Request.ContentType = "application/x-www-form-urlencoded; charset=utf-8"; + context.FormOptions = new FormOptions { - var context = new DefaultHttpContext(); - context.Request.ContentType = "application/x-www-form-urlencoded; charset=utf-8"; - context.FormOptions = new FormOptions - { - ValueCountLimit = 1 - }; + ValueCountLimit = 1 + }; - var formContent = Encoding.UTF8.GetBytes("foo=bar&baz=2"); - context.Request.Body = new NonSeekableReadStream(formContent); + var formContent = Encoding.UTF8.GetBytes("foo=bar&baz=2"); + context.Request.Body = new NonSeekableReadStream(formContent); - var exception = await Assert.ThrowsAsync(() => context.Request.ReadFormAsync()); + var exception = await Assert.ThrowsAsync(() => context.Request.ReadFormAsync()); - Assert.Equal("Form value count limit 1 exceeded.", exception.Message); - } + Assert.Equal("Form value count limit 1 exceeded.", exception.Message); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_SimpleData_ReturnsParsedFormCollection(bool bufferRequest) + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_SimpleData_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes("foo=bar&baz=2"); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = "application/x-www-form-urlencoded; charset=utf-8"; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.Equal("bar", formCollection["foo"]); + Assert.Equal("2", formCollection["baz"]); + Assert.Equal(bufferRequest, context.Request.Body.CanSeek); + if (bufferRequest) { - var formContent = Encoding.UTF8.GetBytes("foo=bar&baz=2"); - var context = new DefaultHttpContext(); - var responseFeature = new FakeResponseFeature(); - context.Features.Set(responseFeature); - context.Request.ContentType = "application/x-www-form-urlencoded; charset=utf-8"; - context.Request.Body = new NonSeekableReadStream(formContent); - - IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); - context.Features.Set(formFeature); - - var formCollection = await context.Request.ReadFormAsync(); - - Assert.Equal("bar", formCollection["foo"]); - Assert.Equal("2", formCollection["baz"]); - Assert.Equal(bufferRequest, context.Request.Body.CanSeek); - if (bufferRequest) - { - Assert.Equal(0, context.Request.Body.Position); - } + Assert.Equal(0, context.Request.Body.Position); + } - // Cached - formFeature = context.Features.Get(); - Assert.NotNull(formFeature); - Assert.NotNull(formFeature.Form); - Assert.Same(formFeature.Form, formCollection); + // Cached + formFeature = context.Features.Get(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); - // Cleanup - await responseFeature.CompleteAsync(); - } + // Cleanup + await responseFeature.CompleteAsync(); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_SimpleData_ReplacePipeReader_ReturnsParsedFormCollection(bool bufferRequest) - { - var formContent = Encoding.UTF8.GetBytes("foo=bar&baz=2"); - var context = new DefaultHttpContext(); - var responseFeature = new FakeResponseFeature(); - context.Features.Set(responseFeature); - context.Request.ContentType = "application/x-www-form-urlencoded; charset=utf-8"; + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_SimpleData_ReplacePipeReader_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes("foo=bar&baz=2"); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = "application/x-www-form-urlencoded; charset=utf-8"; - var pipe = new Pipe(); - await pipe.Writer.WriteAsync(formContent); - pipe.Writer.Complete(); + var pipe = new Pipe(); + await pipe.Writer.WriteAsync(formContent); + pipe.Writer.Complete(); - var mockFeature = new MockRequestBodyPipeFeature(); - mockFeature.Reader = pipe.Reader; - context.Features.Set(mockFeature); + var mockFeature = new MockRequestBodyPipeFeature(); + mockFeature.Reader = pipe.Reader; + context.Features.Set(mockFeature); - IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); - context.Features.Set(formFeature); + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); - var formCollection = await context.Request.ReadFormAsync(); + var formCollection = await context.Request.ReadFormAsync(); - Assert.Equal("bar", formCollection["foo"]); - Assert.Equal("2", formCollection["baz"]); + Assert.Equal("bar", formCollection["foo"]); + Assert.Equal("2", formCollection["baz"]); - // Cached - formFeature = context.Features.Get(); - Assert.NotNull(formFeature); - Assert.NotNull(formFeature.Form); - Assert.Same(formFeature.Form, formCollection); + // Cached + formFeature = context.Features.Get(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); - // Cleanup - await responseFeature.CompleteAsync(); - } + // Cleanup + await responseFeature.CompleteAsync(); + } - private class MockRequestBodyPipeFeature : IRequestBodyPipeFeature - { - public PipeReader Reader { get; set; } - } + private class MockRequestBodyPipeFeature : IRequestBodyPipeFeature + { + public PipeReader Reader { get; set; } + } - private const string MultipartContentType = "multipart/form-data; boundary=WebKitFormBoundary5pDRpGheQXaM8k3T"; + private const string MultipartContentType = "multipart/form-data; boundary=WebKitFormBoundary5pDRpGheQXaM8k3T"; - private const string MultipartContentTypeWithSpecialCharacters = "multipart/form-data; boundary=\"WebKitFormBoundary/:5pDRpGheQXaM8k3T\""; + private const string MultipartContentTypeWithSpecialCharacters = "multipart/form-data; boundary=\"WebKitFormBoundary/:5pDRpGheQXaM8k3T\""; - private const string EmptyMultipartForm = "--WebKitFormBoundary5pDRpGheQXaM8k3T--"; + private const string EmptyMultipartForm = "--WebKitFormBoundary5pDRpGheQXaM8k3T--"; - // Note that CRLF (\r\n) is required. You can't use multi-line C# strings here because the line breaks on Linux are just LF. - private const string MultipartFormEnd = "--WebKitFormBoundary5pDRpGheQXaM8k3T--\r\n"; + // Note that CRLF (\r\n) is required. You can't use multi-line C# strings here because the line breaks on Linux are just LF. + private const string MultipartFormEnd = "--WebKitFormBoundary5pDRpGheQXaM8k3T--\r\n"; - private const string MultipartFormEndWithSpecialCharacters = "--WebKitFormBoundary/:5pDRpGheQXaM8k3T--\r\n"; + private const string MultipartFormEndWithSpecialCharacters = "--WebKitFormBoundary/:5pDRpGheQXaM8k3T--\r\n"; - private const string MultipartFormField = "--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + + private const string MultipartFormField = "--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + "Content-Disposition: form-data; name=\"description\"\r\n" + "\r\n" + "Foo\r\n"; - private const string MultipartFormFile = "--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + + private const string MultipartFormFile = "--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + "Content-Disposition: form-data; name=\"myfile1\"; filename=\"temp.html\"\r\n" + "Content-Type: text/html\r\n" + "\r\n" + "Hello World\r\n"; - private const string MultipartFormEncodedFilename = "--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + + private const string MultipartFormEncodedFilename = "--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + "Content-Disposition: form-data; name=\"myfile1\"; filename=\"temp.html\"; filename*=utf-8\'\'t%c3%a9mp.html\r\n" + "Content-Type: text/html\r\n" + "\r\n" + "Hello World\r\n"; - private const string MultipartFormFileSpecialCharacters = "--WebKitFormBoundary/:5pDRpGheQXaM8k3T\r\n" + + private const string MultipartFormFileSpecialCharacters = "--WebKitFormBoundary/:5pDRpGheQXaM8k3T\r\n" + "Content-Disposition: form-data; name=\"description\"\r\n" + "\r\n" + "Foo\r\n"; - private const string InvalidContentDispositionValue = "form-data; name=\"description\" - filename=\"temp.html\""; + private const string InvalidContentDispositionValue = "form-data; name=\"description\" - filename=\"temp.html\""; - private const string MultipartFormFileInvalidContentDispositionValue = "--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + + private const string MultipartFormFileInvalidContentDispositionValue = "--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + "Content-Disposition: " + InvalidContentDispositionValue + "\r\n" + "\r\n" + "Foo\r\n"; - private const string MultipartFormWithField = - MultipartFormField + - MultipartFormEnd; + private const string MultipartFormWithField = + MultipartFormField + + MultipartFormEnd; - private const string MultipartFormWithFile = - MultipartFormFile + - MultipartFormEnd; + private const string MultipartFormWithFile = + MultipartFormFile + + MultipartFormEnd; - private const string MultipartFormWithFieldAndFile = - MultipartFormField + - MultipartFormFile + - MultipartFormEnd; + private const string MultipartFormWithFieldAndFile = + MultipartFormField + + MultipartFormFile + + MultipartFormEnd; - private const string MultipartFormWithEncodedFilename = - MultipartFormEncodedFilename + - MultipartFormEnd; + private const string MultipartFormWithEncodedFilename = + MultipartFormEncodedFilename + + MultipartFormEnd; - private const string MultipartFormWithSpecialCharacters = - MultipartFormFileSpecialCharacters + - MultipartFormEndWithSpecialCharacters; + private const string MultipartFormWithSpecialCharacters = + MultipartFormFileSpecialCharacters + + MultipartFormEndWithSpecialCharacters; - private const string MultipartFormWithInvalidContentDispositionValue = - MultipartFormFileInvalidContentDispositionValue + - MultipartFormEnd; + private const string MultipartFormWithInvalidContentDispositionValue = + MultipartFormFileInvalidContentDispositionValue + + MultipartFormEnd; - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadForm_EmptyMultipart_ReturnsParsedFormCollection(bool bufferRequest) - { - var formContent = Encoding.UTF8.GetBytes(EmptyMultipartForm); - var context = new DefaultHttpContext(); - var responseFeature = new FakeResponseFeature(); - context.Features.Set(responseFeature); - context.Request.ContentType = MultipartContentType; - context.Request.Body = new NonSeekableReadStream(formContent); - - IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); - context.Features.Set(formFeature); - - var formCollection = context.Request.Form; - - Assert.NotNull(formCollection); - - // Cached - formFeature = context.Features.Get(); - Assert.NotNull(formFeature); - Assert.NotNull(formFeature.Form); - Assert.Same(formCollection, formFeature.Form); - Assert.Same(formCollection, await context.Request.ReadFormAsync()); - - // Content - Assert.Equal(0, formCollection.Count); - Assert.NotNull(formCollection.Files); - Assert.Equal(0, formCollection.Files.Count); - - // Cleanup - await responseFeature.CompleteAsync(); - } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadForm_EmptyMultipart_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(EmptyMultipartForm); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); + + var formCollection = context.Request.Form; + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formCollection, formFeature.Form); + Assert.Same(formCollection, await context.Request.ReadFormAsync()); + + // Content + Assert.Equal(0, formCollection.Count); + Assert.NotNull(formCollection.Files); + Assert.Equal(0, formCollection.Files.Count); + + // Cleanup + await responseFeature.CompleteAsync(); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadForm_MultipartWithField_ReturnsParsedFormCollection(bool bufferRequest) - { - var formContent = Encoding.UTF8.GetBytes(MultipartFormWithField); - var context = new DefaultHttpContext(); - var responseFeature = new FakeResponseFeature(); - context.Features.Set(responseFeature); - context.Request.ContentType = MultipartContentType; - context.Request.Body = new NonSeekableReadStream(formContent); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadForm_MultipartWithField_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithField); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); - IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); - context.Features.Set(formFeature); + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); - var formCollection = context.Request.Form; + var formCollection = context.Request.Form; - Assert.NotNull(formCollection); + Assert.NotNull(formCollection); - // Cached - formFeature = context.Features.Get(); - Assert.NotNull(formFeature); - Assert.NotNull(formFeature.Form); - Assert.Same(formCollection, formFeature.Form); - Assert.Same(formCollection, await context.Request.ReadFormAsync()); + // Cached + formFeature = context.Features.Get(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formCollection, formFeature.Form); + Assert.Same(formCollection, await context.Request.ReadFormAsync()); - // Content - Assert.Equal(1, formCollection.Count); - Assert.Equal("Foo", formCollection["description"]); + // Content + Assert.Equal(1, formCollection.Count); + Assert.Equal("Foo", formCollection["description"]); - Assert.NotNull(formCollection.Files); - Assert.Equal(0, formCollection.Files.Count); + Assert.NotNull(formCollection.Files); + Assert.Equal(0, formCollection.Files.Count); - // Cleanup - await responseFeature.CompleteAsync(); - } + // Cleanup + await responseFeature.CompleteAsync(); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_MultipartWithFile_ReturnsParsedFormCollection(bool bufferRequest) + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_MultipartWithFile_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithFile); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); + Assert.Same(formCollection, context.Request.Form); + + // Content + Assert.Equal(0, formCollection.Count); + + Assert.NotNull(formCollection.Files); + Assert.Equal(1, formCollection.Files.Count); + + var file = formCollection.Files["myfile1"]; + Assert.Equal("myfile1", file.Name); + Assert.Equal("temp.html", file.FileName); + Assert.Equal("text/html", file.ContentType); + Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""", file.ContentDisposition); + var body = file.OpenReadStream(); + using (var reader = new StreamReader(body)) { - var formContent = Encoding.UTF8.GetBytes(MultipartFormWithFile); - var context = new DefaultHttpContext(); - var responseFeature = new FakeResponseFeature(); - context.Features.Set(responseFeature); - context.Request.ContentType = MultipartContentType; - context.Request.Body = new NonSeekableReadStream(formContent); - - IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); - context.Features.Set(formFeature); - - var formCollection = await context.Request.ReadFormAsync(); - - Assert.NotNull(formCollection); - - // Cached - formFeature = context.Features.Get(); - Assert.NotNull(formFeature); - Assert.NotNull(formFeature.Form); - Assert.Same(formFeature.Form, formCollection); - Assert.Same(formCollection, context.Request.Form); - - // Content - Assert.Equal(0, formCollection.Count); - - Assert.NotNull(formCollection.Files); - Assert.Equal(1, formCollection.Files.Count); - - var file = formCollection.Files["myfile1"]; - Assert.Equal("myfile1", file.Name); - Assert.Equal("temp.html", file.FileName); - Assert.Equal("text/html", file.ContentType); - Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""", file.ContentDisposition); - var body = file.OpenReadStream(); - using (var reader = new StreamReader(body)) - { - Assert.True(body.CanSeek); - var content = reader.ReadToEnd(); - Assert.Equal("Hello World", content); - } - - await responseFeature.CompleteAsync(); + Assert.True(body.CanSeek); + var content = reader.ReadToEnd(); + Assert.Equal("Hello World", content); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_MultipartWithFileAndQuotedBoundaryString_ReturnsParsedFormCollection(bool bufferRequest) - { - var formContent = Encoding.UTF8.GetBytes(MultipartFormWithSpecialCharacters); - var context = new DefaultHttpContext(); - var responseFeature = new FakeResponseFeature(); - context.Features.Set(responseFeature); - context.Request.ContentType = MultipartContentTypeWithSpecialCharacters; - context.Request.Body = new NonSeekableReadStream(formContent); - - IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); - context.Features.Set(formFeature); + await responseFeature.CompleteAsync(); + } - var formCollection = context.Request.Form; + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_MultipartWithFileAndQuotedBoundaryString_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithSpecialCharacters); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentTypeWithSpecialCharacters; + context.Request.Body = new NonSeekableReadStream(formContent); - Assert.NotNull(formCollection); + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); - // Cached - formFeature = context.Features.Get(); - Assert.NotNull(formFeature); - Assert.NotNull(formFeature.Form); - Assert.Same(formCollection, formFeature.Form); - Assert.Same(formCollection, await context.Request.ReadFormAsync()); + var formCollection = context.Request.Form; - // Content - Assert.Equal(1, formCollection.Count); - Assert.Equal("Foo", formCollection["description"]); + Assert.NotNull(formCollection); - Assert.NotNull(formCollection.Files); - Assert.Equal(0, formCollection.Files.Count); + // Cached + formFeature = context.Features.Get(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formCollection, formFeature.Form); + Assert.Same(formCollection, await context.Request.ReadFormAsync()); - // Cleanup - await responseFeature.CompleteAsync(); - } + // Content + Assert.Equal(1, formCollection.Count); + Assert.Equal("Foo", formCollection["description"]); - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_MultipartWithEncodedFilename_ReturnsParsedFormCollection(bool bufferRequest) - { - var formContent = Encoding.UTF8.GetBytes(MultipartFormWithEncodedFilename); - var context = new DefaultHttpContext(); - var responseFeature = new FakeResponseFeature(); - context.Features.Set(responseFeature); - context.Request.ContentType = MultipartContentType; - context.Request.Body = new NonSeekableReadStream(formContent); - - IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); - context.Features.Set(formFeature); - - var formCollection = await context.Request.ReadFormAsync(); - - Assert.NotNull(formCollection); - - // Cached - formFeature = context.Features.Get(); - Assert.NotNull(formFeature); - Assert.NotNull(formFeature.Form); - Assert.Same(formFeature.Form, formCollection); - Assert.Same(formCollection, context.Request.Form); - - // Content - Assert.Equal(0, formCollection.Count); - - Assert.NotNull(formCollection.Files); - Assert.Equal(1, formCollection.Files.Count); - - var file = formCollection.Files["myfile1"]; - Assert.Equal("myfile1", file.Name); - Assert.Equal("t\u00e9mp.html", file.FileName); - Assert.Equal("text/html", file.ContentType); - Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""; filename*=utf-8''t%c3%a9mp.html", file.ContentDisposition); - var body = file.OpenReadStream(); - using (var reader = new StreamReader(body)) - { - Assert.True(body.CanSeek); - var content = reader.ReadToEnd(); - Assert.Equal("Hello World", content); - } + Assert.NotNull(formCollection.Files); + Assert.Equal(0, formCollection.Files.Count); - await responseFeature.CompleteAsync(); - } + // Cleanup + await responseFeature.CompleteAsync(); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_MultipartWithFieldAndFile_ReturnsParsedFormCollection(bool bufferRequest) + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_MultipartWithEncodedFilename_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithEncodedFilename); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); + Assert.Same(formCollection, context.Request.Form); + + // Content + Assert.Equal(0, formCollection.Count); + + Assert.NotNull(formCollection.Files); + Assert.Equal(1, formCollection.Files.Count); + + var file = formCollection.Files["myfile1"]; + Assert.Equal("myfile1", file.Name); + Assert.Equal("t\u00e9mp.html", file.FileName); + Assert.Equal("text/html", file.ContentType); + Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""; filename*=utf-8''t%c3%a9mp.html", file.ContentDisposition); + var body = file.OpenReadStream(); + using (var reader = new StreamReader(body)) { - var formContent = Encoding.UTF8.GetBytes(MultipartFormWithFieldAndFile); - var context = new DefaultHttpContext(); - var responseFeature = new FakeResponseFeature(); - context.Features.Set(responseFeature); - context.Request.ContentType = MultipartContentType; - context.Request.Body = new NonSeekableReadStream(formContent); - - IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); - context.Features.Set(formFeature); - - var formCollection = await context.Request.ReadFormAsync(); - - Assert.NotNull(formCollection); - - // Cached - formFeature = context.Features.Get(); - Assert.NotNull(formFeature); - Assert.NotNull(formFeature.Form); - Assert.Same(formFeature.Form, formCollection); - Assert.Same(formCollection, context.Request.Form); - - // Content - Assert.Equal(1, formCollection.Count); - Assert.Equal("Foo", formCollection["description"]); - - Assert.NotNull(formCollection.Files); - Assert.Equal(1, formCollection.Files.Count); - - var file = formCollection.Files["myfile1"]; - Assert.Equal("text/html", file.ContentType); - Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""", file.ContentDisposition); - var body = file.OpenReadStream(); - using (var reader = new StreamReader(body)) - { - Assert.True(body.CanSeek); - var content = reader.ReadToEnd(); - Assert.Equal("Hello World", content); - } - - await responseFeature.CompleteAsync(); + Assert.True(body.CanSeek); + var content = reader.ReadToEnd(); + Assert.Equal("Hello World", content); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_ValueCountLimitExceeded_Throw(bool bufferRequest) + await responseFeature.CompleteAsync(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_MultipartWithFieldAndFile_ReturnsParsedFormCollection(bool bufferRequest) + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithFieldAndFile); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); + Assert.Same(formCollection, context.Request.Form); + + // Content + Assert.Equal(1, formCollection.Count); + Assert.Equal("Foo", formCollection["description"]); + + Assert.NotNull(formCollection.Files); + Assert.Equal(1, formCollection.Files.Count); + + var file = formCollection.Files["myfile1"]; + Assert.Equal("text/html", file.ContentType); + Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""", file.ContentDisposition); + var body = file.OpenReadStream(); + using (var reader = new StreamReader(body)) { - var formContent = new List(); - formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormField)); - formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormField)); - formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormField)); - formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormEnd)); - - var context = new DefaultHttpContext(); - var responseFeature = new FakeResponseFeature(); - context.Features.Set(responseFeature); - context.Request.ContentType = MultipartContentType; - context.Request.Body = new NonSeekableReadStream(formContent.ToArray()); - - IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest, ValueCountLimit = 2 }); - context.Features.Set(formFeature); - - var exception = await Assert.ThrowsAsync(() => context.Request.ReadFormAsync()); - Assert.Equal("Form value count limit 2 exceeded.", exception.Message); + Assert.True(body.CanSeek); + var content = reader.ReadToEnd(); + Assert.Equal("Hello World", content); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_ValueCountLimitExceededWithFiles_Throw(bool bufferRequest) - { - var formContent = new List(); - formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormFile)); - formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormFile)); - formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormFile)); - formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormEnd)); + await responseFeature.CompleteAsync(); + } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueCountLimitExceeded_Throw(bool bufferRequest) + { + var formContent = new List(); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormField)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormField)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormField)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormEnd)); + + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent.ToArray()); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest, ValueCountLimit = 2 }); + context.Features.Set(formFeature); + + var exception = await Assert.ThrowsAsync(() => context.Request.ReadFormAsync()); + Assert.Equal("Form value count limit 2 exceeded.", exception.Message); + } - var context = new DefaultHttpContext(); - var responseFeature = new FakeResponseFeature(); - context.Features.Set(responseFeature); - context.Request.ContentType = MultipartContentType; - context.Request.Body = new NonSeekableReadStream(formContent.ToArray()); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueCountLimitExceededWithFiles_Throw(bool bufferRequest) + { + var formContent = new List(); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormFile)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormFile)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormFile)); + formContent.AddRange(Encoding.UTF8.GetBytes(MultipartFormEnd)); - IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest, ValueCountLimit = 2 }); - context.Features.Set(formFeature); - var exception = await Assert.ThrowsAsync(() => context.Request.ReadFormAsync()); - Assert.Equal("Form value count limit 2 exceeded.", exception.Message); - } + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent.ToArray()); - [Theory] - // FileBufferingReadStream transitions to disk storage after 30kb, and stops pooling buffers at 1mb. - [InlineData(true, 1024)] - [InlineData(false, 1024)] - [InlineData(true, 40 * 1024)] - [InlineData(false, 40 * 1024)] - [InlineData(true, 4 * 1024 * 1024)] - [InlineData(false, 4 * 1024 * 1024)] - public async Task ReadFormAsync_MultipartWithFieldAndMediumFile_ReturnsParsedFormCollection(bool bufferRequest, int fileSize) - { - var fileContents = CreateFile(fileSize); - var formContent = CreateMultipartWithFormAndFile(fileContents); - var context = new DefaultHttpContext(); - var responseFeature = new FakeResponseFeature(); - context.Features.Set(responseFeature); - context.Request.ContentType = MultipartContentType; - context.Request.Body = new NonSeekableReadStream(formContent); - - IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); - context.Features.Set(formFeature); - - var formCollection = await context.Request.ReadFormAsync(); - - Assert.NotNull(formCollection); - - // Cached - formFeature = context.Features.Get(); - Assert.NotNull(formFeature); - Assert.NotNull(formFeature.Form); - Assert.Same(formFeature.Form, formCollection); - Assert.Same(formCollection, context.Request.Form); - - // Content - Assert.Equal(1, formCollection.Count); - Assert.Equal("Foo", formCollection["description"]); - - Assert.NotNull(formCollection.Files); - Assert.Equal(1, formCollection.Files.Count); - - var file = formCollection.Files["myfile1"]; - Assert.Equal("text/html", file.ContentType); - Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""", file.ContentDisposition); - using (var body = file.OpenReadStream()) - { - Assert.True(body.CanSeek); - CompareStreams(fileContents, body); - } + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest, ValueCountLimit = 2 }); + context.Features.Set(formFeature); - await responseFeature.CompleteAsync(); - } + var exception = await Assert.ThrowsAsync(() => context.Request.ReadFormAsync()); + Assert.Equal("Form value count limit 2 exceeded.", exception.Message); + } - [Fact] - public async Task ReadFormAsync_MultipartWithInvalidContentDisposition_Throw() + [Theory] + // FileBufferingReadStream transitions to disk storage after 30kb, and stops pooling buffers at 1mb. + [InlineData(true, 1024)] + [InlineData(false, 1024)] + [InlineData(true, 40 * 1024)] + [InlineData(false, 40 * 1024)] + [InlineData(true, 4 * 1024 * 1024)] + [InlineData(false, 4 * 1024 * 1024)] + public async Task ReadFormAsync_MultipartWithFieldAndMediumFile_ReturnsParsedFormCollection(bool bufferRequest, int fileSize) + { + var fileContents = CreateFile(fileSize); + var formContent = CreateMultipartWithFormAndFile(fileContents); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); + + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions() { BufferBody = bufferRequest }); + context.Features.Set(formFeature); + + var formCollection = await context.Request.ReadFormAsync(); + + Assert.NotNull(formCollection); + + // Cached + formFeature = context.Features.Get(); + Assert.NotNull(formFeature); + Assert.NotNull(formFeature.Form); + Assert.Same(formFeature.Form, formCollection); + Assert.Same(formCollection, context.Request.Form); + + // Content + Assert.Equal(1, formCollection.Count); + Assert.Equal("Foo", formCollection["description"]); + + Assert.NotNull(formCollection.Files); + Assert.Equal(1, formCollection.Files.Count); + + var file = formCollection.Files["myfile1"]; + Assert.Equal("text/html", file.ContentType); + Assert.Equal(@"form-data; name=""myfile1""; filename=""temp.html""", file.ContentDisposition); + using (var body = file.OpenReadStream()) { - var formContent = Encoding.UTF8.GetBytes(MultipartFormWithInvalidContentDispositionValue); - var context = new DefaultHttpContext(); - var responseFeature = new FakeResponseFeature(); - context.Features.Set(responseFeature); - context.Request.ContentType = MultipartContentType; - context.Request.Body = new NonSeekableReadStream(formContent); + Assert.True(body.CanSeek); + CompareStreams(fileContents, body); + } - IFormFeature formFeature = new FormFeature(context.Request, new FormOptions()); - context.Features.Set(formFeature); + await responseFeature.CompleteAsync(); + } - var exception = await Assert.ThrowsAsync(() => context.Request.ReadFormAsync()); + [Fact] + public async Task ReadFormAsync_MultipartWithInvalidContentDisposition_Throw() + { + var formContent = Encoding.UTF8.GetBytes(MultipartFormWithInvalidContentDispositionValue); + var context = new DefaultHttpContext(); + var responseFeature = new FakeResponseFeature(); + context.Features.Set(responseFeature); + context.Request.ContentType = MultipartContentType; + context.Request.Body = new NonSeekableReadStream(formContent); - Assert.Equal("Form section has invalid Content-Disposition value: " + InvalidContentDispositionValue, exception.Message); - } + IFormFeature formFeature = new FormFeature(context.Request, new FormOptions()); + context.Features.Set(formFeature); + + var exception = await Assert.ThrowsAsync(() => context.Request.ReadFormAsync()); - private Stream CreateFile(int size) + Assert.Equal("Form section has invalid Content-Disposition value: " + InvalidContentDispositionValue, exception.Message); + } + + private Stream CreateFile(int size) + { + var stream = new MemoryStream(size); + var bytes = Encoding.ASCII.GetBytes("HelloWorld_ABCDEFGHIJKLMNOPQRSTUVWXYZ.abcdefghijklmnopqrstuvwxyz,0123456789;"); + int written = 0; + while (written < size) { - var stream = new MemoryStream(size); - var bytes = Encoding.ASCII.GetBytes("HelloWorld_ABCDEFGHIJKLMNOPQRSTUVWXYZ.abcdefghijklmnopqrstuvwxyz,0123456789;"); - int written = 0; - while (written < size) - { - var toWrite = Math.Min(size - written, bytes.Length); - stream.Write(bytes, 0, toWrite); - written += toWrite; - } - stream.Position = 0; - return stream; + var toWrite = Math.Min(size - written, bytes.Length); + stream.Write(bytes, 0, toWrite); + written += toWrite; } + stream.Position = 0; + return stream; + } - private Stream CreateMultipartWithFormAndFile(Stream fileContents) - { - var stream = new MemoryStream(); - var header = + private Stream CreateMultipartWithFormAndFile(Stream fileContents) + { + var stream = new MemoryStream(); + var header = MultipartFormField + "--WebKitFormBoundary5pDRpGheQXaM8k3T\r\n" + "Content-Disposition: form-data; name=\"myfile1\"; filename=\"temp.html\"\r\n" + "Content-Type: text/html\r\n" + "\r\n"; - var footer = + var footer = "\r\n--WebKitFormBoundary5pDRpGheQXaM8k3T--"; - var bytes = Encoding.ASCII.GetBytes(header); - stream.Write(bytes, 0, bytes.Length); + var bytes = Encoding.ASCII.GetBytes(header); + stream.Write(bytes, 0, bytes.Length); - fileContents.CopyTo(stream); - fileContents.Position = 0; + fileContents.CopyTo(stream); + fileContents.Position = 0; - bytes = Encoding.ASCII.GetBytes(footer); - stream.Write(bytes, 0, bytes.Length); - stream.Position = 0; - return stream; - } + bytes = Encoding.ASCII.GetBytes(footer); + stream.Write(bytes, 0, bytes.Length); + stream.Position = 0; + return stream; + } - private void CompareStreams(Stream streamA, Stream streamB) + private void CompareStreams(Stream streamA, Stream streamB) + { + Assert.Equal(streamA.Length, streamB.Length); + byte[] bytesA = new byte[1024], bytesB = new byte[1024]; + var readA = streamA.Read(bytesA, 0, bytesA.Length); + var readB = streamB.Read(bytesB, 0, bytesB.Length); + Assert.Equal(readA, readB); + var loops = 0; + while (readA > 0) { - Assert.Equal(streamA.Length, streamB.Length); - byte[] bytesA = new byte[1024], bytesB = new byte[1024]; - var readA = streamA.Read(bytesA, 0, bytesA.Length); - var readB = streamB.Read(bytesB, 0, bytesB.Length); - Assert.Equal(readA, readB); - var loops = 0; - while (readA > 0) + for (int i = 0; i < readA; i++) { - for (int i = 0; i < readA; i++) + if (bytesA[i] != bytesB[i]) { - if (bytesA[i] != bytesB[i]) - { - throw new Exception($"Value mismatch at loop {loops}, index {i}; A:{bytesA[i]}, B:{bytesB[i]}"); - } + throw new Exception($"Value mismatch at loop {loops}, index {i}; A:{bytesA[i]}, B:{bytesB[i]}"); } - - readA = streamA.Read(bytesA, 0, bytesA.Length); - readB = streamB.Read(bytesB, 0, bytesB.Length); - Assert.Equal(readA, readB); - loops++; } + + readA = streamA.Read(bytesA, 0, bytesA.Length); + readB = streamB.Read(bytesB, 0, bytesB.Length); + Assert.Equal(readA, readB); + loops++; } } } diff --git a/src/Http/Http/test/Features/HttpRequestIdentifierFeatureTests.cs b/src/Http/Http/test/Features/HttpRequestIdentifierFeatureTests.cs index 4d148f17d8..1bd917115e 100644 --- a/src/Http/Http/test/Features/HttpRequestIdentifierFeatureTests.cs +++ b/src/Http/Http/test/Features/HttpRequestIdentifierFeatureTests.cs @@ -3,41 +3,40 @@ using Xunit; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +public class HttpRequestIdentifierFeatureTests { - public class HttpRequestIdentifierFeatureTests + [Fact] + public void TraceIdentifier_ReturnsId() { - [Fact] - public void TraceIdentifier_ReturnsId() - { - var feature = new HttpRequestIdentifierFeature(); + var feature = new HttpRequestIdentifierFeature(); - var id = feature.TraceIdentifier; + var id = feature.TraceIdentifier; - Assert.NotNull(id); - } + Assert.NotNull(id); + } - [Fact] - public void TraceIdentifier_ReturnsStableId() - { - var feature = new HttpRequestIdentifierFeature(); + [Fact] + public void TraceIdentifier_ReturnsStableId() + { + var feature = new HttpRequestIdentifierFeature(); - var id1 = feature.TraceIdentifier; - var id2 = feature.TraceIdentifier; + var id1 = feature.TraceIdentifier; + var id2 = feature.TraceIdentifier; - Assert.Equal(id1, id2); - } + Assert.Equal(id1, id2); + } - [Fact] - public void TraceIdentifier_ReturnsUniqueIdForDifferentInstances() - { - var feature1 = new HttpRequestIdentifierFeature(); - var feature2 = new HttpRequestIdentifierFeature(); + [Fact] + public void TraceIdentifier_ReturnsUniqueIdForDifferentInstances() + { + var feature1 = new HttpRequestIdentifierFeature(); + var feature2 = new HttpRequestIdentifierFeature(); - var id1 = feature1.TraceIdentifier; - var id2 = feature2.TraceIdentifier; + var id1 = feature1.TraceIdentifier; + var id2 = feature2.TraceIdentifier; - Assert.NotEqual(id1, id2); - } + Assert.NotEqual(id1, id2); } -} \ No newline at end of file +} diff --git a/src/Http/Http/test/Features/NonSeekableReadStream.cs b/src/Http/Http/test/Features/NonSeekableReadStream.cs index 39cc8f5a25..c31f0b204e 100644 --- a/src/Http/Http/test/Features/NonSeekableReadStream.cs +++ b/src/Http/Http/test/Features/NonSeekableReadStream.cs @@ -6,67 +6,66 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +public class NonSeekableReadStream : Stream { - public class NonSeekableReadStream : Stream + private readonly Stream _inner; + + public NonSeekableReadStream(byte[] data) + : this(new MemoryStream(data)) + { + } + + public NonSeekableReadStream(Stream inner) + { + _inner = inner; + } + + public override bool CanRead => _inner.CanRead; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _inner.Read(buffer, offset, count); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - private readonly Stream _inner; - - public NonSeekableReadStream(byte[] data) - : this(new MemoryStream(data)) - { - } - - public NonSeekableReadStream(Stream inner) - { - _inner = inner; - } - - public override bool CanRead => _inner.CanRead; - - public override bool CanSeek => false; - - public override bool CanWrite => false; - - public override long Length - { - get { throw new NotSupportedException(); } - } - - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - public override void Flush() - { - throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - return _inner.Read(buffer, offset, count); - } - - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _inner.ReadAsync(buffer, offset, count, cancellationToken); - } + return _inner.ReadAsync(buffer, offset, count, cancellationToken); } } diff --git a/src/Http/Http/test/Features/QueryFeatureTests.cs b/src/Http/Http/test/Features/QueryFeatureTests.cs index 6c6849b105..926fbf2085 100644 --- a/src/Http/Http/test/Features/QueryFeatureTests.cs +++ b/src/Http/Http/test/Features/QueryFeatureTests.cs @@ -4,255 +4,254 @@ using System.Linq; using Xunit; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +public class QueryFeatureTests { - public class QueryFeatureTests + [Fact] + public void QueryReturnsParsedQueryCollection() { - [Fact] - public void QueryReturnsParsedQueryCollection() - { - // Arrange - var features = new FeatureCollection(); - features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "foo=bar" }; + // Arrange + var features = new FeatureCollection(); + features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "foo=bar" }; - var provider = new QueryFeature(features); + var provider = new QueryFeature(features); - // Act - var queryCollection = provider.Query; + // Act + var queryCollection = provider.Query; - // Assert - Assert.Equal("bar", queryCollection["foo"]); - } + // Assert + Assert.Equal("bar", queryCollection["foo"]); + } - [Theory] - [InlineData("?key1=value1&key2=value2")] - [InlineData("key1=value1&key2=value2")] - public void ParseQueryWithUniqueKeysWorks(string queryString) - { - var features = new FeatureCollection(); - features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = queryString }; + [Theory] + [InlineData("?key1=value1&key2=value2")] + [InlineData("key1=value1&key2=value2")] + public void ParseQueryWithUniqueKeysWorks(string queryString) + { + var features = new FeatureCollection(); + features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = queryString }; - var provider = new QueryFeature(features); + var provider = new QueryFeature(features); - var queryCollection = provider.Query; + var queryCollection = provider.Query; - Assert.Equal(2, queryCollection.Count); - Assert.Equal("value1", queryCollection["key1"].FirstOrDefault()); - Assert.Equal("value2", queryCollection["key2"].FirstOrDefault()); - } + Assert.Equal(2, queryCollection.Count); + Assert.Equal("value1", queryCollection["key1"].FirstOrDefault()); + Assert.Equal("value2", queryCollection["key2"].FirstOrDefault()); + } - [Theory] - [InlineData("?q", "q")] - [InlineData("?q&", "q")] - [InlineData("?q1=abc&q2", "q2")] - [InlineData("?q=", "q")] - [InlineData("?q=&", "q")] - public void KeyWithoutValuesAddedToQueryCollection(string queryString, string emptyParam) - { - var features = new FeatureCollection(); - features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = queryString }; + [Theory] + [InlineData("?q", "q")] + [InlineData("?q&", "q")] + [InlineData("?q1=abc&q2", "q2")] + [InlineData("?q=", "q")] + [InlineData("?q=&", "q")] + public void KeyWithoutValuesAddedToQueryCollection(string queryString, string emptyParam) + { + var features = new FeatureCollection(); + features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = queryString }; - var provider = new QueryFeature(features); + var provider = new QueryFeature(features); - var queryCollection = provider.Query; + var queryCollection = provider.Query; - Assert.True(queryCollection.Keys.Contains(emptyParam)); - Assert.Equal(string.Empty, queryCollection[emptyParam]); - } + Assert.True(queryCollection.Keys.Contains(emptyParam)); + Assert.Equal(string.Empty, queryCollection[emptyParam]); + } - [Theory] - [InlineData("?&&")] - [InlineData("?&")] - [InlineData("&&")] - public void EmptyKeysNotAddedToQueryCollection(string queryString) - { - var features = new FeatureCollection(); - features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = queryString }; + [Theory] + [InlineData("?&&")] + [InlineData("?&")] + [InlineData("&&")] + public void EmptyKeysNotAddedToQueryCollection(string queryString) + { + var features = new FeatureCollection(); + features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = queryString }; - var provider = new QueryFeature(features); + var provider = new QueryFeature(features); - var queryCollection = provider.Query; + var queryCollection = provider.Query; - Assert.Equal(0, queryCollection.Count); - } + Assert.Equal(0, queryCollection.Count); + } - [Fact] - public void ParseQueryWithEmptyKeyWorks() - { - var features = new FeatureCollection(); - features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?=value1&=" }; + [Fact] + public void ParseQueryWithEmptyKeyWorks() + { + var features = new FeatureCollection(); + features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?=value1&=" }; - var provider = new QueryFeature(features); + var provider = new QueryFeature(features); - var queryCollection = provider.Query; + var queryCollection = provider.Query; - Assert.Single(queryCollection); - Assert.Equal(new[] { "value1", "" }, queryCollection[""]); - } + Assert.Single(queryCollection); + Assert.Equal(new[] { "value1", "" }, queryCollection[""]); + } - [Fact] - public void ParseQueryWithDuplicateKeysGroups() - { - var features = new FeatureCollection(); - features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?key1=valueA&key2=valueB&key1=valueC" }; + [Fact] + public void ParseQueryWithDuplicateKeysGroups() + { + var features = new FeatureCollection(); + features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?key1=valueA&key2=valueB&key1=valueC" }; - var provider = new QueryFeature(features); + var provider = new QueryFeature(features); - var queryCollection = provider.Query; + var queryCollection = provider.Query; - Assert.Equal(2, queryCollection.Count); - Assert.Equal(new[] { "valueA", "valueC" }, queryCollection["key1"]); - Assert.Equal("valueB", queryCollection["key2"].FirstOrDefault()); - } + Assert.Equal(2, queryCollection.Count); + Assert.Equal(new[] { "valueA", "valueC" }, queryCollection["key1"]); + Assert.Equal("valueB", queryCollection["key2"].FirstOrDefault()); + } - [Fact] - public void ParseQueryWithThreefoldKeysGroups() - { - var features = new FeatureCollection(); - features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?key1=valueA&key2=valueB&key1=valueC&key1=valueD" }; + [Fact] + public void ParseQueryWithThreefoldKeysGroups() + { + var features = new FeatureCollection(); + features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?key1=valueA&key2=valueB&key1=valueC&key1=valueD" }; - var provider = new QueryFeature(features); + var provider = new QueryFeature(features); - var queryCollection = provider.Query; + var queryCollection = provider.Query; - Assert.Equal(2, queryCollection.Count); - Assert.Equal(new[] { "valueA", "valueC", "valueD" }, queryCollection["key1"]); - Assert.Equal("valueB", queryCollection["key2"].FirstOrDefault()); - } + Assert.Equal(2, queryCollection.Count); + Assert.Equal(new[] { "valueA", "valueC", "valueD" }, queryCollection["key1"]); + Assert.Equal("valueB", queryCollection["key2"].FirstOrDefault()); + } - [Fact] - public void ParseQueryWithEmptyValuesWorks() - { - var features = new FeatureCollection(); - features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?key1=&key2=" }; + [Fact] + public void ParseQueryWithEmptyValuesWorks() + { + var features = new FeatureCollection(); + features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?key1=&key2=" }; - var provider = new QueryFeature(features); + var provider = new QueryFeature(features); - var queryCollection = provider.Query; + var queryCollection = provider.Query; - Assert.Equal(2, queryCollection.Count); - Assert.Equal(string.Empty, queryCollection["key1"].FirstOrDefault()); - Assert.Equal(string.Empty, queryCollection["key2"].FirstOrDefault()); - } + Assert.Equal(2, queryCollection.Count); + Assert.Equal(string.Empty, queryCollection["key1"].FirstOrDefault()); + Assert.Equal(string.Empty, queryCollection["key2"].FirstOrDefault()); + } - [Theory] - [InlineData("?")] - [InlineData("")] - [InlineData(null)] - public void ParseEmptyOrNullQueryWorks(string queryString) - { - var features = new FeatureCollection(); - features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = queryString }; + [Theory] + [InlineData("?")] + [InlineData("")] + [InlineData(null)] + public void ParseEmptyOrNullQueryWorks(string queryString) + { + var features = new FeatureCollection(); + features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = queryString }; - var provider = new QueryFeature(features); + var provider = new QueryFeature(features); - var queryCollection = provider.Query; + var queryCollection = provider.Query; - Assert.Empty(queryCollection); - } + Assert.Empty(queryCollection); + } - [Fact] - public void ParseQueryWithEncodedKeyWorks() - { - var features = new FeatureCollection(); - features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?fields+%5BtodoItems%5D" }; + [Fact] + public void ParseQueryWithEncodedKeyWorks() + { + var features = new FeatureCollection(); + features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?fields+%5BtodoItems%5D" }; - var provider = new QueryFeature(features); + var provider = new QueryFeature(features); - var queryCollection = provider.Query; + var queryCollection = provider.Query; - Assert.Single(queryCollection); - Assert.Equal("", queryCollection["fields [todoItems]"].FirstOrDefault()); - } + Assert.Single(queryCollection); + Assert.Equal("", queryCollection["fields [todoItems]"].FirstOrDefault()); + } - [Fact] - public void ParseQueryWithEncodedValueWorks() - { - var features = new FeatureCollection(); - features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?=fields+%5BtodoItems%5D" }; + [Fact] + public void ParseQueryWithEncodedValueWorks() + { + var features = new FeatureCollection(); + features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?=fields+%5BtodoItems%5D" }; - var provider = new QueryFeature(features); + var provider = new QueryFeature(features); - var queryCollection = provider.Query; + var queryCollection = provider.Query; - Assert.Single(queryCollection); - Assert.Equal("fields [todoItems]", queryCollection[""].FirstOrDefault()); - } + Assert.Single(queryCollection); + Assert.Equal("fields [todoItems]", queryCollection[""].FirstOrDefault()); + } - [Fact] - public void ParseQueryWithEncodedKeyEmptyValueWorks() - { - var features = new FeatureCollection(); - features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?fields+%5BtodoItems%5D=" }; + [Fact] + public void ParseQueryWithEncodedKeyEmptyValueWorks() + { + var features = new FeatureCollection(); + features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?fields+%5BtodoItems%5D=" }; - var provider = new QueryFeature(features); + var provider = new QueryFeature(features); - var queryCollection = provider.Query; + var queryCollection = provider.Query; - Assert.Single(queryCollection); - Assert.Equal("", queryCollection["fields [todoItems]"].FirstOrDefault()); - } + Assert.Single(queryCollection); + Assert.Equal("", queryCollection["fields [todoItems]"].FirstOrDefault()); + } - [Fact] - public void ParseQueryWithEncodedKeyEncodedValueWorks() - { - var features = new FeatureCollection(); - features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?fields+%5BtodoItems%5D=%5B+1+%5D" }; + [Fact] + public void ParseQueryWithEncodedKeyEncodedValueWorks() + { + var features = new FeatureCollection(); + features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?fields+%5BtodoItems%5D=%5B+1+%5D" }; - var provider = new QueryFeature(features); + var provider = new QueryFeature(features); - var queryCollection = provider.Query; + var queryCollection = provider.Query; - Assert.Single(queryCollection); - Assert.Equal("[ 1 ]", queryCollection["fields [todoItems]"].FirstOrDefault()); - } + Assert.Single(queryCollection); + Assert.Equal("[ 1 ]", queryCollection["fields [todoItems]"].FirstOrDefault()); + } - [Fact] - public void ParseQueryWithEncodedKeyEncodedValuesWorks() - { - var features = new FeatureCollection(); - features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?fields+%5BtodoItems%5D=%5B+1+%5D&fields+%5BtodoItems%5D=%5B+2+%5D" }; + [Fact] + public void ParseQueryWithEncodedKeyEncodedValuesWorks() + { + var features = new FeatureCollection(); + features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?fields+%5BtodoItems%5D=%5B+1+%5D&fields+%5BtodoItems%5D=%5B+2+%5D" }; - var provider = new QueryFeature(features); + var provider = new QueryFeature(features); - var queryCollection = provider.Query; + var queryCollection = provider.Query; - Assert.Single(queryCollection); - Assert.Equal(new[] { "[ 1 ]", "[ 2 ]" }, queryCollection["fields [todoItems]"]); - } + Assert.Single(queryCollection); + Assert.Equal(new[] { "[ 1 ]", "[ 2 ]" }, queryCollection["fields [todoItems]"]); + } - [Fact] - public void CaseInsensitiveWithManyKeys() + [Fact] + public void CaseInsensitiveWithManyKeys() + { + // need to use over 10 keys to test dictionary storage code path + var features = new FeatureCollection(); + features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { - // need to use over 10 keys to test dictionary storage code path - var features = new FeatureCollection(); - features[typeof(IHttpRequestFeature)] = new HttpRequestFeature - { - QueryString = "?a=0&b=0&c=1&d=2&e=3&f=4&g=5&h=6&i=7&j=8&k=9&" + - "key=1&Key=2&key=3&Key=4&KEy=5&KEY=6&kEY=7&KeY=8&kEy=9&keY=10" - }; + QueryString = "?a=0&b=0&c=1&d=2&e=3&f=4&g=5&h=6&i=7&j=8&k=9&" + + "key=1&Key=2&key=3&Key=4&KEy=5&KEY=6&kEY=7&KeY=8&kEy=9&keY=10" + }; - var provider = new QueryFeature(features); + var provider = new QueryFeature(features); - var queryCollection = provider.Query; + var queryCollection = provider.Query; - Assert.Equal(12, queryCollection.Count); - Assert.Equal(new[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" }, queryCollection["KEY"]); - } + Assert.Equal(12, queryCollection.Count); + Assert.Equal(new[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" }, queryCollection["KEY"]); + } - [Fact] - public void CaseInsensitiveWithFewKeys() - { - // need to use less than 10 keys to test array storage code path - var features = new FeatureCollection(); - features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?key=1&Key=2&key=3&Key=4&KEy=5" }; + [Fact] + public void CaseInsensitiveWithFewKeys() + { + // need to use less than 10 keys to test array storage code path + var features = new FeatureCollection(); + features[typeof(IHttpRequestFeature)] = new HttpRequestFeature { QueryString = "?key=1&Key=2&key=3&Key=4&KEy=5" }; - var provider = new QueryFeature(features); + var provider = new QueryFeature(features); - var queryCollection = provider.Query; + var queryCollection = provider.Query; - Assert.Equal(1, queryCollection.Count); - Assert.Equal(new[] { "1", "2", "3", "4", "5" }, queryCollection["KEY"]); - } + Assert.Equal(1, queryCollection.Count); + Assert.Equal(new[] { "1", "2", "3", "4", "5" }, queryCollection["KEY"]); } } diff --git a/src/Http/Http/test/Features/RequestBodyPipeFeatureTests.cs b/src/Http/Http/test/Features/RequestBodyPipeFeatureTests.cs index fac9f76027..842c6f02e5 100644 --- a/src/Http/Http/test/Features/RequestBodyPipeFeatureTests.cs +++ b/src/Http/Http/test/Features/RequestBodyPipeFeatureTests.cs @@ -8,41 +8,40 @@ using System.Text; using System.Threading.Tasks; using Xunit; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +public class RequestBodyPipeFeatureTests { - public class RequestBodyPipeFeatureTests + [Fact] + public void RequestBodyReturnsStreamPipeReader() + { + var context = new DefaultHttpContext(); + var expectedStream = new MemoryStream(); + context.Request.Body = expectedStream; + + var feature = new RequestBodyPipeFeature(context); + + var pipeBody = feature.Reader; + + Assert.NotNull(pipeBody); + } + + [Fact] + public async Task RequestBodyGetsDataFromSecondStream() + { + var context = new DefaultHttpContext(); + context.Request.Body = new MemoryStream(Encoding.ASCII.GetBytes("hahaha")); + var feature = new RequestBodyPipeFeature(context); + var _ = feature.Reader; + + var expectedString = "abcdef"; + context.Request.Body = new MemoryStream(Encoding.ASCII.GetBytes(expectedString)); + var data = await feature.Reader.ReadAsync(); + Assert.Equal(expectedString, GetStringFromReadResult(data)); + } + + private static string GetStringFromReadResult(ReadResult data) { - [Fact] - public void RequestBodyReturnsStreamPipeReader() - { - var context = new DefaultHttpContext(); - var expectedStream = new MemoryStream(); - context.Request.Body = expectedStream; - - var feature = new RequestBodyPipeFeature(context); - - var pipeBody = feature.Reader; - - Assert.NotNull(pipeBody); - } - - [Fact] - public async Task RequestBodyGetsDataFromSecondStream() - { - var context = new DefaultHttpContext(); - context.Request.Body = new MemoryStream(Encoding.ASCII.GetBytes("hahaha")); - var feature = new RequestBodyPipeFeature(context); - var _ = feature.Reader; - - var expectedString = "abcdef"; - context.Request.Body = new MemoryStream(Encoding.ASCII.GetBytes(expectedString)); - var data = await feature.Reader.ReadAsync(); - Assert.Equal(expectedString, GetStringFromReadResult(data)); - } - - private static string GetStringFromReadResult(ReadResult data) - { - return Encoding.ASCII.GetString(data.Buffer.ToArray()); - } + return Encoding.ASCII.GetString(data.Buffer.ToArray()); } } diff --git a/src/Http/Http/test/Features/StreamResponseBodyFeatureTests.cs b/src/Http/Http/test/Features/StreamResponseBodyFeatureTests.cs index afc8ff4520..8e16560bf1 100644 --- a/src/Http/Http/test/Features/StreamResponseBodyFeatureTests.cs +++ b/src/Http/Http/test/Features/StreamResponseBodyFeatureTests.cs @@ -9,85 +9,84 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -namespace Microsoft.AspNetCore.Http.Features +namespace Microsoft.AspNetCore.Http.Features; + +public class StreamResponseBodyFeatureTests { - public class StreamResponseBodyFeatureTests + [Fact] + public async Task CompleteAsyncCallsStartAsync() { - [Fact] - public async Task CompleteAsyncCallsStartAsync() - { - // Arrange - var stream = new MemoryStream(); - var streamResponseBodyFeature = new TestStreamResponseBodyFeature(stream); - - // Act - await streamResponseBodyFeature.CompleteAsync(); - - //Assert - Assert.Equal(1, streamResponseBodyFeature.StartCalled); - } - - [Fact] - public async Task CompleteAsyncWontCallsStartAsyncIfAlreadyStarted() - { - // Arrange - var stream = new MemoryStream(); - var streamResponseBodyFeature = new TestStreamResponseBodyFeature(stream); - await streamResponseBodyFeature.StartAsync(); - - // Act - await streamResponseBodyFeature.CompleteAsync(); - - //Assert - Assert.Equal(1, streamResponseBodyFeature.StartCalled); - } - - [Fact] - public void DisableBufferingCallsInnerFeature() - { - // Arrange - var stream = new MemoryStream(); - - var innerFeature = new InnerDisableBufferingFeature(stream, null); - var streamResponseBodyFeature = new StreamResponseBodyFeature(stream, innerFeature); - - // Act - streamResponseBodyFeature.DisableBuffering(); - - //Assert - Assert.True(innerFeature.DisableBufferingCalled); - } + // Arrange + var stream = new MemoryStream(); + var streamResponseBodyFeature = new TestStreamResponseBodyFeature(stream); + + // Act + await streamResponseBodyFeature.CompleteAsync(); + + //Assert + Assert.Equal(1, streamResponseBodyFeature.StartCalled); } - public class TestStreamResponseBodyFeature : StreamResponseBodyFeature + [Fact] + public async Task CompleteAsyncWontCallsStartAsyncIfAlreadyStarted() { - public TestStreamResponseBodyFeature(Stream stream) - : base(stream) - { + // Arrange + var stream = new MemoryStream(); + var streamResponseBodyFeature = new TestStreamResponseBodyFeature(stream); + await streamResponseBodyFeature.StartAsync(); + + // Act + await streamResponseBodyFeature.CompleteAsync(); + + //Assert + Assert.Equal(1, streamResponseBodyFeature.StartCalled); + } - } + [Fact] + public void DisableBufferingCallsInnerFeature() + { + // Arrange + var stream = new MemoryStream(); + + var innerFeature = new InnerDisableBufferingFeature(stream, null); + var streamResponseBodyFeature = new StreamResponseBodyFeature(stream, innerFeature); + + // Act + streamResponseBodyFeature.DisableBuffering(); + + //Assert + Assert.True(innerFeature.DisableBufferingCalled); + } +} - public override Task StartAsync(CancellationToken cancellationToken = default) - { - StartCalled++; - return base.StartAsync(cancellationToken); - } +public class TestStreamResponseBodyFeature : StreamResponseBodyFeature +{ + public TestStreamResponseBodyFeature(Stream stream) + : base(stream) + { - public int StartCalled { get; private set; } } - public class InnerDisableBufferingFeature : StreamResponseBodyFeature + public override Task StartAsync(CancellationToken cancellationToken = default) { - public InnerDisableBufferingFeature(Stream stream, IHttpResponseBodyFeature priorFeature) - : base(stream, priorFeature) - { - } + StartCalled++; + return base.StartAsync(cancellationToken); + } - public override void DisableBuffering() - { - DisableBufferingCalled = true; - } + public int StartCalled { get; private set; } +} - public bool DisableBufferingCalled { get; set; } +public class InnerDisableBufferingFeature : StreamResponseBodyFeature +{ + public InnerDisableBufferingFeature(Stream stream, IHttpResponseBodyFeature priorFeature) + : base(stream, priorFeature) + { } + + public override void DisableBuffering() + { + DisableBufferingCalled = true; + } + + public bool DisableBufferingCalled { get; set; } } diff --git a/src/Http/Http/test/HeaderDictionaryTests.cs b/src/Http/Http/test/HeaderDictionaryTests.cs index f4fc5f776e..27de61aec5 100644 --- a/src/Http/Http/test/HeaderDictionaryTests.cs +++ b/src/Http/Http/test/HeaderDictionaryTests.cs @@ -7,11 +7,11 @@ using System.Linq; using Microsoft.Extensions.Primitives; using Xunit; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +public class HeaderDictionaryTests { - public class HeaderDictionaryTests - { - public static TheoryData HeaderSegmentData => new TheoryData> + public static TheoryData HeaderSegmentData => new TheoryData> { new[] { "Value1", "Value2", "Value3", "Value4" }, new[] { "Value1", "", "Value3", "Value4" }, @@ -21,100 +21,99 @@ namespace Microsoft.AspNetCore.Http new[] { "", null, "", null }, }; - [Fact] - public void PropertiesAreAccessible() - { - var headers = new HeaderDictionary( - new Dictionary(StringComparer.OrdinalIgnoreCase) - { + [Fact] + public void PropertiesAreAccessible() + { + var headers = new HeaderDictionary( + new Dictionary(StringComparer.OrdinalIgnoreCase) + { { "Header1", "Value1" } - }); - - Assert.Single(headers); - Assert.Equal(new[] { "Header1" }, headers.Keys); - Assert.True(headers.ContainsKey("header1")); - Assert.False(headers.ContainsKey("header2")); - Assert.Equal("Value1", headers["header1"]); - Assert.Equal(new[] { "Value1" }, headers["header1"].ToArray()); - } - - [Theory] - [MemberData(nameof(HeaderSegmentData))] - public void EmptyHeaderSegmentsAreIgnored(IEnumerable segments) - { - var header = string.Join(",", segments); + }); + + Assert.Single(headers); + Assert.Equal(new[] { "Header1" }, headers.Keys); + Assert.True(headers.ContainsKey("header1")); + Assert.False(headers.ContainsKey("header2")); + Assert.Equal("Value1", headers["header1"]); + Assert.Equal(new[] { "Value1" }, headers["header1"].ToArray()); + } + + [Theory] + [MemberData(nameof(HeaderSegmentData))] + public void EmptyHeaderSegmentsAreIgnored(IEnumerable segments) + { + var header = string.Join(",", segments); - var headers = new HeaderDictionary( - new Dictionary(StringComparer.OrdinalIgnoreCase) - { + var headers = new HeaderDictionary( + new Dictionary(StringComparer.OrdinalIgnoreCase) + { { "Header1", header}, - }); + }); - var result = headers.GetCommaSeparatedValues("Header1"); - var expectedResult = segments.Where(s => !string.IsNullOrEmpty(s)); + var result = headers.GetCommaSeparatedValues("Header1"); + var expectedResult = segments.Where(s => !string.IsNullOrEmpty(s)); - Assert.Equal(expectedResult, result); - } + Assert.Equal(expectedResult, result); + } - [Fact] - public void EmptyQuotedHeaderSegmentsAreIgnored() - { - var headers = new HeaderDictionary( - new Dictionary(StringComparer.OrdinalIgnoreCase) - { + [Fact] + public void EmptyQuotedHeaderSegmentsAreIgnored() + { + var headers = new HeaderDictionary( + new Dictionary(StringComparer.OrdinalIgnoreCase) + { { "Header1", "Value1,\"\",,Value2" }, - }); + }); - var result = headers.GetCommaSeparatedValues("Header1"); - Assert.Equal(new[] { "Value1", "Value2" }, result); - } + var result = headers.GetCommaSeparatedValues("Header1"); + Assert.Equal(new[] { "Value1", "Value2" }, result); + } - [Fact] - public void ReadActionsWorkWhenReadOnly() - { - var headers = new HeaderDictionary( - new Dictionary(StringComparer.OrdinalIgnoreCase) - { + [Fact] + public void ReadActionsWorkWhenReadOnly() + { + var headers = new HeaderDictionary( + new Dictionary(StringComparer.OrdinalIgnoreCase) + { { "Header1", "Value1" } - }); + }); - headers.IsReadOnly = true; + headers.IsReadOnly = true; - Assert.Single(headers); - Assert.Equal(new[] { "Header1" }, headers.Keys); - Assert.True(headers.ContainsKey("header1")); - Assert.False(headers.ContainsKey("header2")); - Assert.Equal("Value1", headers["header1"]); - Assert.Equal(new[] { "Value1" }, headers["header1"].ToArray()); - } + Assert.Single(headers); + Assert.Equal(new[] { "Header1" }, headers.Keys); + Assert.True(headers.ContainsKey("header1")); + Assert.False(headers.ContainsKey("header2")); + Assert.Equal("Value1", headers["header1"]); + Assert.Equal(new[] { "Value1" }, headers["header1"].ToArray()); + } - [Fact] - public void WriteActionsThrowWhenReadOnly() - { - var headers = new HeaderDictionary(); - headers.IsReadOnly = true; - - Assert.Throws(() => headers["header1"] = "value1"); - Assert.Throws(() => ((IDictionary)headers)["header1"] = "value1"); - Assert.Throws(() => headers.ContentLength = 12); - Assert.Throws(() => headers.Add(new KeyValuePair("header1", "value1"))); - Assert.Throws(() => headers.Add("header1", "value1")); - Assert.Throws(() => headers.Clear()); - Assert.Throws(() => headers.Remove(new KeyValuePair("header1", "value1"))); - Assert.Throws(() => headers.Remove("header1")); - } - - [Fact] - public void GetCommaSeparatedValues_WorksForUnquotedHeaderValuesEndingWithSpace() - { - var headers = new HeaderDictionary + [Fact] + public void WriteActionsThrowWhenReadOnly() + { + var headers = new HeaderDictionary(); + headers.IsReadOnly = true; + + Assert.Throws(() => headers["header1"] = "value1"); + Assert.Throws(() => ((IDictionary)headers)["header1"] = "value1"); + Assert.Throws(() => headers.ContentLength = 12); + Assert.Throws(() => headers.Add(new KeyValuePair("header1", "value1"))); + Assert.Throws(() => headers.Add("header1", "value1")); + Assert.Throws(() => headers.Clear()); + Assert.Throws(() => headers.Remove(new KeyValuePair("header1", "value1"))); + Assert.Throws(() => headers.Remove("header1")); + } + + [Fact] + public void GetCommaSeparatedValues_WorksForUnquotedHeaderValuesEndingWithSpace() + { + var headers = new HeaderDictionary { { "Via", "value " }, }; - var result = headers.GetCommaSeparatedValues("Via"); + var result = headers.GetCommaSeparatedValues("Via"); - Assert.Equal(new[]{"value "}, result); - } + Assert.Equal(new[] { "value " }, result); } } diff --git a/src/Http/Http/test/HttpContextAccessorTests.cs b/src/Http/Http/test/HttpContextAccessorTests.cs index 9cce449171..b3df46ef5b 100644 --- a/src/Http/Http/test/HttpContextAccessorTests.cs +++ b/src/Http/Http/test/HttpContextAccessorTests.cs @@ -12,177 +12,176 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Features; using Xunit; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +public class HttpContextAccessorTests { - public class HttpContextAccessorTests + [Fact] + public async Task HttpContextAccessor_GettingHttpContextReturnsHttpContext() { - [Fact] - public async Task HttpContextAccessor_GettingHttpContextReturnsHttpContext() - { - var accessor = new HttpContextAccessor(); + var accessor = new HttpContextAccessor(); - var context = new DefaultHttpContext(); - context.TraceIdentifier = "1"; - accessor.HttpContext = context; + var context = new DefaultHttpContext(); + context.TraceIdentifier = "1"; + accessor.HttpContext = context; - await Task.Delay(100); + await Task.Delay(100); - Assert.Same(context, accessor.HttpContext); - } + Assert.Same(context, accessor.HttpContext); + } - [Fact] - public void HttpContextAccessor_GettingHttpContextWithOutSettingReturnsNull() - { - var accessor = new HttpContextAccessor(); + [Fact] + public void HttpContextAccessor_GettingHttpContextWithOutSettingReturnsNull() + { + var accessor = new HttpContextAccessor(); - Assert.Null(accessor.HttpContext); - } + Assert.Null(accessor.HttpContext); + } - [Fact] - public async Task HttpContextAccessor_GettingHttpContextReturnsNullHttpContextIfSetToNull() - { - var accessor = new HttpContextAccessor(); + [Fact] + public async Task HttpContextAccessor_GettingHttpContextReturnsNullHttpContextIfSetToNull() + { + var accessor = new HttpContextAccessor(); - var context = new DefaultHttpContext(); - accessor.HttpContext = context; + var context = new DefaultHttpContext(); + accessor.HttpContext = context; - var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var waitForNullTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var afterNullCheckTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var waitForNullTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var afterNullCheckTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - ThreadPool.QueueUserWorkItem(async _ => - { + ThreadPool.QueueUserWorkItem(async _ => + { // The HttpContext flows with the execution context Assert.Same(context, accessor.HttpContext); - checkAsyncFlowTcs.SetResult(null); + checkAsyncFlowTcs.SetResult(null); - await waitForNullTcs.Task; + await waitForNullTcs.Task; - try - { - Assert.Null(accessor.HttpContext); + try + { + Assert.Null(accessor.HttpContext); - afterNullCheckTcs.SetResult(null); - } - catch (Exception ex) - { - afterNullCheckTcs.SetException(ex); - } - }); + afterNullCheckTcs.SetResult(null); + } + catch (Exception ex) + { + afterNullCheckTcs.SetException(ex); + } + }); - await checkAsyncFlowTcs.Task; + await checkAsyncFlowTcs.Task; - // Null out the accessor - accessor.HttpContext = null; + // Null out the accessor + accessor.HttpContext = null; - waitForNullTcs.SetResult(null); + waitForNullTcs.SetResult(null); - Assert.Null(accessor.HttpContext); + Assert.Null(accessor.HttpContext); - await afterNullCheckTcs.Task; - } + await afterNullCheckTcs.Task; + } - [Fact] - public async Task HttpContextAccessor_GettingHttpContextReturnsNullHttpContextIfChanged() - { - var accessor = new HttpContextAccessor(); + [Fact] + public async Task HttpContextAccessor_GettingHttpContextReturnsNullHttpContextIfChanged() + { + var accessor = new HttpContextAccessor(); - var context = new DefaultHttpContext(); - accessor.HttpContext = context; + var context = new DefaultHttpContext(); + accessor.HttpContext = context; - var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var waitForNullTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var afterNullCheckTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var waitForNullTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var afterNullCheckTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - ThreadPool.QueueUserWorkItem(async _ => - { + ThreadPool.QueueUserWorkItem(async _ => + { // The HttpContext flows with the execution context Assert.Same(context, accessor.HttpContext); - checkAsyncFlowTcs.SetResult(null); + checkAsyncFlowTcs.SetResult(null); - await waitForNullTcs.Task; + await waitForNullTcs.Task; - try - { - Assert.Null(accessor.HttpContext); + try + { + Assert.Null(accessor.HttpContext); - afterNullCheckTcs.SetResult(null); - } - catch (Exception ex) - { - afterNullCheckTcs.SetException(ex); - } - }); + afterNullCheckTcs.SetResult(null); + } + catch (Exception ex) + { + afterNullCheckTcs.SetException(ex); + } + }); - await checkAsyncFlowTcs.Task; + await checkAsyncFlowTcs.Task; - // Set a new http context - var context2 = new DefaultHttpContext(); - accessor.HttpContext = context2; + // Set a new http context + var context2 = new DefaultHttpContext(); + accessor.HttpContext = context2; - waitForNullTcs.SetResult(null); + waitForNullTcs.SetResult(null); - Assert.Same(context2, accessor.HttpContext); + Assert.Same(context2, accessor.HttpContext); - await afterNullCheckTcs.Task; - } + await afterNullCheckTcs.Task; + } - [Fact] - public async Task HttpContextAccessor_GettingHttpContextDoesNotFlowIfAccessorSetToNull() - { - var accessor = new HttpContextAccessor(); + [Fact] + public async Task HttpContextAccessor_GettingHttpContextDoesNotFlowIfAccessorSetToNull() + { + var accessor = new HttpContextAccessor(); - var context = new DefaultHttpContext(); - accessor.HttpContext = context; + var context = new DefaultHttpContext(); + accessor.HttpContext = context; - var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - accessor.HttpContext = null; + accessor.HttpContext = null; - ThreadPool.QueueUserWorkItem(_ => + ThreadPool.QueueUserWorkItem(_ => + { + try { - try - { // The HttpContext flows with the execution context Assert.Null(accessor.HttpContext); - checkAsyncFlowTcs.SetResult(null); - } - catch (Exception ex) - { - checkAsyncFlowTcs.SetException(ex); - } - }); - - await checkAsyncFlowTcs.Task; - } - - [Fact] - public async Task HttpContextAccessor_GettingHttpContextDoesNotFlowIfExecutionContextDoesNotFlow() - { - var accessor = new HttpContextAccessor(); + checkAsyncFlowTcs.SetResult(null); + } + catch (Exception ex) + { + checkAsyncFlowTcs.SetException(ex); + } + }); + + await checkAsyncFlowTcs.Task; + } + + [Fact] + public async Task HttpContextAccessor_GettingHttpContextDoesNotFlowIfExecutionContextDoesNotFlow() + { + var accessor = new HttpContextAccessor(); - var context = new DefaultHttpContext(); - accessor.HttpContext = context; + var context = new DefaultHttpContext(); + accessor.HttpContext = context; - var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var checkAsyncFlowTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - ThreadPool.UnsafeQueueUserWorkItem(_ => + ThreadPool.UnsafeQueueUserWorkItem(_ => + { + try { - try - { // The HttpContext flows with the execution context Assert.Null(accessor.HttpContext); - checkAsyncFlowTcs.SetResult(null); - } - catch (Exception ex) - { - checkAsyncFlowTcs.SetException(ex); - } - }, null); - - await checkAsyncFlowTcs.Task; - } + checkAsyncFlowTcs.SetResult(null); + } + catch (Exception ex) + { + checkAsyncFlowTcs.SetException(ex); + } + }, null); + + await checkAsyncFlowTcs.Task; } -} \ No newline at end of file +} diff --git a/src/Http/Http/test/HttpServiceCollectionExtensionsTests.cs b/src/Http/Http/test/HttpServiceCollectionExtensionsTests.cs index 2f06064f92..367384b6f5 100644 --- a/src/Http/Http/test/HttpServiceCollectionExtensionsTests.cs +++ b/src/Http/Http/test/HttpServiceCollectionExtensionsTests.cs @@ -5,29 +5,28 @@ using System; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Microsoft.AspNetCore.Http.Tests +namespace Microsoft.AspNetCore.Http.Tests; + +public class HttpServiceCollectionExtensionsTests { - public class HttpServiceCollectionExtensionsTests + [Fact] + public void AddHttpContextAccessor_AddsWithCorrectLifetime() { - [Fact] - public void AddHttpContextAccessor_AddsWithCorrectLifetime() - { - // Arrange - var services = new ServiceCollection(); + // Arrange + var services = new ServiceCollection(); - // Act - services.AddHttpContextAccessor(); + // Act + services.AddHttpContextAccessor(); - // Assert - var descriptor = services[0]; - Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime); - Assert.Equal(typeof(HttpContextAccessor), descriptor.ImplementationType); - } + // Assert + var descriptor = services[0]; + Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime); + Assert.Equal(typeof(HttpContextAccessor), descriptor.ImplementationType); + } - [Fact] - public void AddHttpContextAccessor_ThrowsWithoutServices() - { - Assert.Throws("services", () => HttpServiceCollectionExtensions.AddHttpContextAccessor(null)); - } + [Fact] + public void AddHttpContextAccessor_ThrowsWithoutServices() + { + Assert.Throws("services", () => HttpServiceCollectionExtensions.AddHttpContextAccessor(null)); } } diff --git a/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs b/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs index 2475f87b23..9924c8117c 100644 --- a/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs +++ b/src/Http/Http/test/Internal/DefaultHttpRequestTests.cs @@ -11,287 +11,286 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Primitives; using Xunit; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +public class DefaultHttpRequestTests { - public class DefaultHttpRequestTests + [Theory] + [InlineData(0)] + [InlineData(9001)] + [InlineData(65535)] + public void GetContentLength_ReturnsParsedHeader(long value) { - [Theory] - [InlineData(0)] - [InlineData(9001)] - [InlineData(65535)] - public void GetContentLength_ReturnsParsedHeader(long value) - { - // Arrange - var request = GetRequestWithContentLength(value.ToString(CultureInfo.InvariantCulture)); + // Arrange + var request = GetRequestWithContentLength(value.ToString(CultureInfo.InvariantCulture)); - // Act and Assert - Assert.Equal(value, request.ContentLength); - } + // Act and Assert + Assert.Equal(value, request.ContentLength); + } - [Fact] - public void GetContentLength_ReturnsNullIfHeaderDoesNotExist() - { - // Arrange - var request = GetRequestWithContentLength(contentLength: null); + [Fact] + public void GetContentLength_ReturnsNullIfHeaderDoesNotExist() + { + // Arrange + var request = GetRequestWithContentLength(contentLength: null); - // Act and Assert - Assert.Null(request.ContentLength); - } + // Act and Assert + Assert.Null(request.ContentLength); + } - [Theory] - [InlineData("cant-parse-this")] - [InlineData("-1000")] - [InlineData("1000.00")] - [InlineData("100/5")] - public void GetContentLength_ReturnsNullIfHeaderCannotBeParsed(string contentLength) - { - // Arrange - var request = GetRequestWithContentLength(contentLength); + [Theory] + [InlineData("cant-parse-this")] + [InlineData("-1000")] + [InlineData("1000.00")] + [InlineData("100/5")] + public void GetContentLength_ReturnsNullIfHeaderCannotBeParsed(string contentLength) + { + // Arrange + var request = GetRequestWithContentLength(contentLength); - // Act and Assert - Assert.Null(request.ContentLength); - } + // Act and Assert + Assert.Null(request.ContentLength); + } - [Fact] - public void GetContentType_ReturnsNullIfHeaderDoesNotExist() - { - // Arrange - var request = GetRequestWithContentType(contentType: null); + [Fact] + public void GetContentType_ReturnsNullIfHeaderDoesNotExist() + { + // Arrange + var request = GetRequestWithContentType(contentType: null); - // Act and Assert - Assert.Null(request.ContentType); - } + // Act and Assert + Assert.Null(request.ContentType); + } - [Fact] - public void Host_GetsHostFromHeaders() - { - // Arrange - const string expected = "localhost:9001"; + [Fact] + public void Host_GetsHostFromHeaders() + { + // Arrange + const string expected = "localhost:9001"; - var headers = new HeaderDictionary() + var headers = new HeaderDictionary() { { "Host", expected }, }; - var request = CreateRequest(headers); + var request = CreateRequest(headers); - // Act - var host = request.Host; + // Act + var host = request.Host; - // Assert - Assert.Equal(expected, host.Value); - } + // Assert + Assert.Equal(expected, host.Value); + } - [Fact] - public void Host_DecodesPunyCode() - { - // Arrange - const string expected = "löcalhöst"; + [Fact] + public void Host_DecodesPunyCode() + { + // Arrange + const string expected = "löcalhöst"; - var headers = new HeaderDictionary() + var headers = new HeaderDictionary() { { "Host", "xn--lcalhst-90ae" }, }; - var request = CreateRequest(headers); + var request = CreateRequest(headers); - // Act - var host = request.Host; + // Act + var host = request.Host; - // Assert - Assert.Equal(expected, host.Value); - } + // Assert + Assert.Equal(expected, host.Value); + } - [Fact] - public void Host_EncodesPunyCode() - { - // Arrange - const string expected = "xn--lcalhst-90ae"; + [Fact] + public void Host_EncodesPunyCode() + { + // Arrange + const string expected = "xn--lcalhst-90ae"; - var headers = new HeaderDictionary(); + var headers = new HeaderDictionary(); - var request = CreateRequest(headers); + var request = CreateRequest(headers); - // Act - request.Host = new HostString("löcalhöst"); + // Act + request.Host = new HostString("löcalhöst"); - // Assert - Assert.Equal(expected, headers["Host"][0]); - } + // Assert + Assert.Equal(expected, headers["Host"][0]); + } - [Fact] - public void IsHttps_CorrectlyReflectsScheme() - { - var request = new DefaultHttpContext().Request; - Assert.Equal(string.Empty, request.Scheme); - Assert.False(request.IsHttps); - request.IsHttps = true; - Assert.Equal("https", request.Scheme); - request.IsHttps = false; - Assert.Equal("http", request.Scheme); - request.Scheme = "ftp"; - Assert.False(request.IsHttps); - request.Scheme = "HTTPS"; - Assert.True(request.IsHttps); - } + [Fact] + public void IsHttps_CorrectlyReflectsScheme() + { + var request = new DefaultHttpContext().Request; + Assert.Equal(string.Empty, request.Scheme); + Assert.False(request.IsHttps); + request.IsHttps = true; + Assert.Equal("https", request.Scheme); + request.IsHttps = false; + Assert.Equal("http", request.Scheme); + request.Scheme = "ftp"; + Assert.False(request.IsHttps); + request.Scheme = "HTTPS"; + Assert.True(request.IsHttps); + } - [Fact] - public void Query_GetAndSet() - { - var request = new DefaultHttpContext().Request; - var requestFeature = request.HttpContext.Features.Get(); - Assert.Equal(string.Empty, requestFeature.QueryString); - Assert.Equal(QueryString.Empty, request.QueryString); - var query0 = request.Query; - Assert.NotNull(query0); - Assert.Equal(0, query0.Count); - - requestFeature.QueryString = "?name0=value0&name1=value1"; - var query1 = request.Query; - Assert.NotSame(query0, query1); - Assert.Equal(2, query1.Count); - Assert.Equal("value0", query1["name0"]); - Assert.Equal("value1", query1["name1"]); - - var query2 = new QueryCollection( new Dictionary() + [Fact] + public void Query_GetAndSet() + { + var request = new DefaultHttpContext().Request; + var requestFeature = request.HttpContext.Features.Get(); + Assert.Equal(string.Empty, requestFeature.QueryString); + Assert.Equal(QueryString.Empty, request.QueryString); + var query0 = request.Query; + Assert.NotNull(query0); + Assert.Equal(0, query0.Count); + + requestFeature.QueryString = "?name0=value0&name1=value1"; + var query1 = request.Query; + Assert.NotSame(query0, query1); + Assert.Equal(2, query1.Count); + Assert.Equal("value0", query1["name0"]); + Assert.Equal("value1", query1["name1"]); + + var query2 = new QueryCollection(new Dictionary() { { "name2", "value2" } }); - request.Query = query2; - Assert.Same(query2, request.Query); - Assert.Equal("?name2=value2", requestFeature.QueryString); - Assert.Equal(new QueryString("?name2=value2"), request.QueryString); - } + request.Query = query2; + Assert.Same(query2, request.Query); + Assert.Equal("?name2=value2", requestFeature.QueryString); + Assert.Equal(new QueryString("?name2=value2"), request.QueryString); + } - [Fact] - public void Cookies_GetAndSet() - { - var request = new DefaultHttpContext().Request; - var cookieHeaders = request.Headers["Cookie"]; - Assert.Empty(cookieHeaders); - var cookies0 = request.Cookies; - Assert.Empty(cookies0); - Assert.Null(cookies0["key0"]); - Assert.False(cookies0.ContainsKey("key0")); - - var newCookies = new[] { "name0=value0%2C", "name1=value1" }; - request.Headers["Cookie"] = newCookies; - - cookies0 = RequestCookieCollection.Parse(newCookies); - var cookies1 = request.Cookies; - Assert.Equal(cookies0, cookies1); - Assert.Equal(2, cookies1.Count); - Assert.Equal("value0,", cookies1["name0"]); - Assert.Equal("value1", cookies1["name1"]); - Assert.Equal(newCookies, request.Headers["Cookie"]); - - var cookies2 = new RequestCookieCollection(new Dictionary() + [Fact] + public void Cookies_GetAndSet() + { + var request = new DefaultHttpContext().Request; + var cookieHeaders = request.Headers["Cookie"]; + Assert.Empty(cookieHeaders); + var cookies0 = request.Cookies; + Assert.Empty(cookies0); + Assert.Null(cookies0["key0"]); + Assert.False(cookies0.ContainsKey("key0")); + + var newCookies = new[] { "name0=value0%2C", "name1=value1" }; + request.Headers["Cookie"] = newCookies; + + cookies0 = RequestCookieCollection.Parse(newCookies); + var cookies1 = request.Cookies; + Assert.Equal(cookies0, cookies1); + Assert.Equal(2, cookies1.Count); + Assert.Equal("value0,", cookies1["name0"]); + Assert.Equal("value1", cookies1["name1"]); + Assert.Equal(newCookies, request.Headers["Cookie"]); + + var cookies2 = new RequestCookieCollection(new Dictionary() { { "name2", "value2" } }); - request.Cookies = cookies2; - Assert.Equal(cookies2, request.Cookies); - Assert.Equal("value2", request.Cookies["name2"]); - cookieHeaders = request.Headers["Cookie"]; - Assert.Equal(new[] { "name2=value2" }, cookieHeaders); - } + request.Cookies = cookies2; + Assert.Equal(cookies2, request.Cookies); + Assert.Equal("value2", request.Cookies["name2"]); + cookieHeaders = request.Headers["Cookie"]; + Assert.Equal(new[] { "name2=value2" }, cookieHeaders); + } - [Fact] - public void RouteValues_GetAndSet() - { - var context = new DefaultHttpContext(); - var request = context.Request; + [Fact] + public void RouteValues_GetAndSet() + { + var context = new DefaultHttpContext(); + var request = context.Request; - var routeValuesFeature = context.Features.Get(); - // No feature set for initial DefaultHttpRequest - Assert.Null(routeValuesFeature); + var routeValuesFeature = context.Features.Get(); + // No feature set for initial DefaultHttpRequest + Assert.Null(routeValuesFeature); - // Route values returns empty collection by default - Assert.Empty(request.RouteValues); + // Route values returns empty collection by default + Assert.Empty(request.RouteValues); - // Get and set value on request route values - request.RouteValues["new"] = "setvalue"; - Assert.Equal("setvalue", request.RouteValues["new"]); + // Get and set value on request route values + request.RouteValues["new"] = "setvalue"; + Assert.Equal("setvalue", request.RouteValues["new"]); - routeValuesFeature = context.Features.Get(); - // Accessing DefaultHttpRequest.RouteValues creates feature - Assert.NotNull(routeValuesFeature); + routeValuesFeature = context.Features.Get(); + // Accessing DefaultHttpRequest.RouteValues creates feature + Assert.NotNull(routeValuesFeature); - request.RouteValues = new RouteValueDictionary(new { key = "value" }); - // Can set DefaultHttpRequest.RouteValues - Assert.NotNull(request.RouteValues); - Assert.Equal("value", request.RouteValues["key"]); + request.RouteValues = new RouteValueDictionary(new { key = "value" }); + // Can set DefaultHttpRequest.RouteValues + Assert.NotNull(request.RouteValues); + Assert.Equal("value", request.RouteValues["key"]); - // DefaultHttpRequest.RouteValues uses feature - Assert.Equal(routeValuesFeature.RouteValues, request.RouteValues); + // DefaultHttpRequest.RouteValues uses feature + Assert.Equal(routeValuesFeature.RouteValues, request.RouteValues); - // Setting route values to null sets empty collection on request - routeValuesFeature.RouteValues = null; - Assert.Empty(request.RouteValues); + // Setting route values to null sets empty collection on request + routeValuesFeature.RouteValues = null; + Assert.Empty(request.RouteValues); - var customRouteValuesFeature = new CustomRouteValuesFeature - { - RouteValues = new RouteValueDictionary(new { key = "customvalue" }) - }; - context.Features.Set(customRouteValuesFeature); - // Can override DefaultHttpRequest.RouteValues with custom feature - Assert.Equal(customRouteValuesFeature.RouteValues, request.RouteValues); + var customRouteValuesFeature = new CustomRouteValuesFeature + { + RouteValues = new RouteValueDictionary(new { key = "customvalue" }) + }; + context.Features.Set(customRouteValuesFeature); + // Can override DefaultHttpRequest.RouteValues with custom feature + Assert.Equal(customRouteValuesFeature.RouteValues, request.RouteValues); + + // Can clear feature + context.Features.Set(null); + Assert.Empty(request.RouteValues); + } - // Can clear feature - context.Features.Set(null); - Assert.Empty(request.RouteValues); - } + [Fact] + public void BodyReader_CanGet() + { + var context = new DefaultHttpContext(); + var bodyPipe = context.Request.BodyReader; + Assert.NotNull(bodyPipe); + } - [Fact] - public void BodyReader_CanGet() - { - var context = new DefaultHttpContext(); - var bodyPipe = context.Request.BodyReader; - Assert.NotNull(bodyPipe); - } + private class CustomRouteValuesFeature : IRouteValuesFeature + { + public RouteValueDictionary RouteValues { get; set; } + } - private class CustomRouteValuesFeature : IRouteValuesFeature - { - public RouteValueDictionary RouteValues { get; set; } - } + private static HttpRequest CreateRequest(IHeaderDictionary headers) + { + var context = new DefaultHttpContext(); + context.Features.Get().Headers = headers; + return context.Request; + } - private static HttpRequest CreateRequest(IHeaderDictionary headers) - { - var context = new DefaultHttpContext(); - context.Features.Get().Headers = headers; - return context.Request; - } + private static HttpRequest GetRequestWithContentLength(string contentLength = null) + { + return GetRequestWithHeader("Content-Length", contentLength); + } - private static HttpRequest GetRequestWithContentLength(string contentLength = null) - { - return GetRequestWithHeader("Content-Length", contentLength); - } + private static HttpRequest GetRequestWithContentType(string contentType = null) + { + return GetRequestWithHeader("Content-Type", contentType); + } - private static HttpRequest GetRequestWithContentType(string contentType = null) - { - return GetRequestWithHeader("Content-Type", contentType); - } + private static HttpRequest GetRequestWithAcceptHeader(string acceptHeader = null) + { + return GetRequestWithHeader("Accept", acceptHeader); + } - private static HttpRequest GetRequestWithAcceptHeader(string acceptHeader = null) - { - return GetRequestWithHeader("Accept", acceptHeader); - } + private static HttpRequest GetRequestWithAcceptCharsetHeader(string acceptCharset = null) + { + return GetRequestWithHeader("Accept-Charset", acceptCharset); + } - private static HttpRequest GetRequestWithAcceptCharsetHeader(string acceptCharset = null) + private static HttpRequest GetRequestWithHeader(string headerName, string headerValue) + { + var headers = new HeaderDictionary(); + if (headerValue != null) { - return GetRequestWithHeader("Accept-Charset", acceptCharset); + headers.Add(headerName, headerValue); } - private static HttpRequest GetRequestWithHeader(string headerName, string headerValue) - { - var headers = new HeaderDictionary(); - if (headerValue != null) - { - headers.Add(headerName, headerValue); - } - - return CreateRequest(headers); - } + return CreateRequest(headers); } } diff --git a/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs b/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs index f061e46153..a576de6794 100644 --- a/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs +++ b/src/Http/Http/test/Internal/DefaultHttpResponseTests.cs @@ -13,273 +13,272 @@ using Microsoft.Extensions.Primitives; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +public class DefaultHttpResponseTests { - public class DefaultHttpResponseTests + [Theory] + [InlineData(0)] + [InlineData(9001)] + [InlineData(65535)] + public void GetContentLength_ReturnsParsedHeader(long value) { - [Theory] - [InlineData(0)] - [InlineData(9001)] - [InlineData(65535)] - public void GetContentLength_ReturnsParsedHeader(long value) - { - // Arrange - var response = GetResponseWithContentLength(value.ToString(CultureInfo.InvariantCulture)); - - // Act and Assert - Assert.Equal(value, response.ContentLength); - } - - [Fact] - public void GetContentLength_ReturnsNullIfHeaderDoesNotExist() - { - // Arrange - var response = GetResponseWithContentLength(contentLength: null); + // Arrange + var response = GetResponseWithContentLength(value.ToString(CultureInfo.InvariantCulture)); - // Act and Assert - Assert.Null(response.ContentLength); - } + // Act and Assert + Assert.Equal(value, response.ContentLength); + } - [Theory] - [InlineData("cant-parse-this")] - [InlineData("-1000")] - [InlineData("1000.00")] - [InlineData("100/5")] - public void GetContentLength_ReturnsNullIfHeaderCannotBeParsed(string contentLength) - { - // Arrange - var response = GetResponseWithContentLength(contentLength); + [Fact] + public void GetContentLength_ReturnsNullIfHeaderDoesNotExist() + { + // Arrange + var response = GetResponseWithContentLength(contentLength: null); - // Act and Assert - Assert.Null(response.ContentLength); - } + // Act and Assert + Assert.Null(response.ContentLength); + } - [Fact] - public void GetContentType_ReturnsNullIfHeaderDoesNotExist() - { - // Arrange - var response = GetResponseWithContentType(contentType: null); + [Theory] + [InlineData("cant-parse-this")] + [InlineData("-1000")] + [InlineData("1000.00")] + [InlineData("100/5")] + public void GetContentLength_ReturnsNullIfHeaderCannotBeParsed(string contentLength) + { + // Arrange + var response = GetResponseWithContentLength(contentLength); - // Act and Assert - Assert.Null(response.ContentType); - } + // Act and Assert + Assert.Null(response.ContentLength); + } - [Fact] - public void BodyWriter_CanGet() - { - var response = new DefaultHttpContext(); - var bodyPipe = response.Response.BodyWriter; + [Fact] + public void GetContentType_ReturnsNullIfHeaderDoesNotExist() + { + // Arrange + var response = GetResponseWithContentType(contentType: null); - Assert.NotNull(bodyPipe); - } + // Act and Assert + Assert.Null(response.ContentType); + } - [Fact] - public void ReplacingResponseBody_DoesNotCreateOnCompletedRegistration() - { - var features = new FeatureCollection(); + [Fact] + public void BodyWriter_CanGet() + { + var response = new DefaultHttpContext(); + var bodyPipe = response.Response.BodyWriter; - var originalStream = new FlushAsyncCheckStream(); - var replacementStream = new FlushAsyncCheckStream(); + Assert.NotNull(bodyPipe); + } - var responseBodyMock = new Mock(); - responseBodyMock.Setup(o => o.Stream).Returns(originalStream); - features.Set(responseBodyMock.Object); + [Fact] + public void ReplacingResponseBody_DoesNotCreateOnCompletedRegistration() + { + var features = new FeatureCollection(); - var responseMock = new Mock(); - features.Set(responseMock.Object); + var originalStream = new FlushAsyncCheckStream(); + var replacementStream = new FlushAsyncCheckStream(); - var context = new DefaultHttpContext(features); + var responseBodyMock = new Mock(); + responseBodyMock.Setup(o => o.Stream).Returns(originalStream); + features.Set(responseBodyMock.Object); - Assert.Same(originalStream, context.Response.Body); - Assert.Same(responseBodyMock.Object, context.Features.Get()); + var responseMock = new Mock(); + features.Set(responseMock.Object); - context.Response.Body = replacementStream; + var context = new DefaultHttpContext(features); - Assert.Same(replacementStream, context.Response.Body); - Assert.NotSame(responseBodyMock.Object, context.Features.Get()); + Assert.Same(originalStream, context.Response.Body); + Assert.Same(responseBodyMock.Object, context.Features.Get()); - context.Response.Body = originalStream; + context.Response.Body = replacementStream; - Assert.Same(originalStream, context.Response.Body); - Assert.Same(responseBodyMock.Object, context.Features.Get()); + Assert.Same(replacementStream, context.Response.Body); + Assert.NotSame(responseBodyMock.Object, context.Features.Get()); - // The real issue was not that an OnCompleted registration existed, but that it would previously flush - // the original response body in the OnCompleted callback after the response body was disposed. - // However, since now there's no longer an OnCompleted registration at all, it's easier to verify that. - // https://github.com/dotnet/aspnetcore/issues/25342 - responseMock.Verify(m => m.OnCompleted(It.IsAny>(), It.IsAny()), Times.Never); - } + context.Response.Body = originalStream; - [Fact] - public async Task ResponseStart_CallsFeatureIfSet() - { - var features = new FeatureCollection(); - var mock = new Mock(); - mock.Setup(o => o.StartAsync(It.IsAny())).Returns(Task.CompletedTask); - features.Set(mock.Object); + Assert.Same(originalStream, context.Response.Body); + Assert.Same(responseBodyMock.Object, context.Features.Get()); - var responseMock = new Mock(); - responseMock.Setup(o => o.HasStarted).Returns(false); - features.Set(responseMock.Object); + // The real issue was not that an OnCompleted registration existed, but that it would previously flush + // the original response body in the OnCompleted callback after the response body was disposed. + // However, since now there's no longer an OnCompleted registration at all, it's easier to verify that. + // https://github.com/dotnet/aspnetcore/issues/25342 + responseMock.Verify(m => m.OnCompleted(It.IsAny>(), It.IsAny()), Times.Never); + } - var context = new DefaultHttpContext(features); - await context.Response.StartAsync(); + [Fact] + public async Task ResponseStart_CallsFeatureIfSet() + { + var features = new FeatureCollection(); + var mock = new Mock(); + mock.Setup(o => o.StartAsync(It.IsAny())).Returns(Task.CompletedTask); + features.Set(mock.Object); - mock.Verify(m => m.StartAsync(default), Times.Once()); - } + var responseMock = new Mock(); + responseMock.Setup(o => o.HasStarted).Returns(false); + features.Set(responseMock.Object); - [Fact] - public async Task ResponseStart_CallsFeatureIfSetWithProvidedCancellationToken() - { - var features = new FeatureCollection(); + var context = new DefaultHttpContext(features); + await context.Response.StartAsync(); - var mock = new Mock(); - var ct = new CancellationToken(); - mock.Setup(o => o.StartAsync(It.Is((localCt) => localCt.Equals(ct)))).Returns(Task.CompletedTask); - features.Set(mock.Object); + mock.Verify(m => m.StartAsync(default), Times.Once()); + } - var responseMock = new Mock(); - responseMock.Setup(o => o.HasStarted).Returns(false); - features.Set(responseMock.Object); + [Fact] + public async Task ResponseStart_CallsFeatureIfSetWithProvidedCancellationToken() + { + var features = new FeatureCollection(); - var context = new DefaultHttpContext(features); - await context.Response.StartAsync(ct); + var mock = new Mock(); + var ct = new CancellationToken(); + mock.Setup(o => o.StartAsync(It.Is((localCt) => localCt.Equals(ct)))).Returns(Task.CompletedTask); + features.Set(mock.Object); - mock.Verify(m => m.StartAsync(default), Times.Once()); - } + var responseMock = new Mock(); + responseMock.Setup(o => o.HasStarted).Returns(false); + features.Set(responseMock.Object); - [Fact] - public async Task ResponseStart_DoesNotCallStartIfHasStartedIsTrue() - { - var features = new FeatureCollection(); + var context = new DefaultHttpContext(features); + await context.Response.StartAsync(ct); - var startMock = new Mock(); - startMock.Setup(o => o.StartAsync(It.IsAny())).Returns(Task.CompletedTask); - features.Set(startMock.Object); + mock.Verify(m => m.StartAsync(default), Times.Once()); + } - var responseMock = new Mock(); - responseMock.Setup(o => o.HasStarted).Returns(true); - features.Set(responseMock.Object); + [Fact] + public async Task ResponseStart_DoesNotCallStartIfHasStartedIsTrue() + { + var features = new FeatureCollection(); - var context = new DefaultHttpContext(features); - await context.Response.StartAsync(); + var startMock = new Mock(); + startMock.Setup(o => o.StartAsync(It.IsAny())).Returns(Task.CompletedTask); + features.Set(startMock.Object); - startMock.Verify(m => m.StartAsync(default), Times.Never()); - } + var responseMock = new Mock(); + responseMock.Setup(o => o.HasStarted).Returns(true); + features.Set(responseMock.Object); - [Fact] - public async Task ResponseStart_CallsResponseBodyFlushIfNotSet() - { - var context = new DefaultHttpContext(); - var mock = new FlushAsyncCheckStream(); - context.Response.Body = mock; + var context = new DefaultHttpContext(features); + await context.Response.StartAsync(); - await context.Response.StartAsync(default); + startMock.Verify(m => m.StartAsync(default), Times.Never()); + } - Assert.True(mock.IsCalled); - } + [Fact] + public async Task ResponseStart_CallsResponseBodyFlushIfNotSet() + { + var context = new DefaultHttpContext(); + var mock = new FlushAsyncCheckStream(); + context.Response.Body = mock; - [Fact] - public async Task RegisterForDisposeHandlesDisposeAsyncIfObjectImplementsIAsyncDisposable() - { - var features = new FeatureCollection(); - var response = new ResponseFeature(); - features.Set(response); + await context.Response.StartAsync(default); - var context = new DefaultHttpContext(features); - var instance = new DisposableClass(); - context.Response.RegisterForDispose(instance); + Assert.True(mock.IsCalled); + } - await response.ExecuteOnCompletedCallbacks(); + [Fact] + public async Task RegisterForDisposeHandlesDisposeAsyncIfObjectImplementsIAsyncDisposable() + { + var features = new FeatureCollection(); + var response = new ResponseFeature(); + features.Set(response); - Assert.True(instance.DisposeAsyncCalled); - Assert.False(instance.DisposeCalled); - } + var context = new DefaultHttpContext(features); + var instance = new DisposableClass(); + context.Response.RegisterForDispose(instance); - public class ResponseFeature : IHttpResponseFeature - { - private readonly List<(Func, object)> _callbacks = new(); - public int StatusCode { get; set; } - public string ReasonPhrase { get; set; } - public IHeaderDictionary Headers { get; set; } - public Stream Body { get; set; } + await response.ExecuteOnCompletedCallbacks(); - public bool HasStarted => false; + Assert.True(instance.DisposeAsyncCalled); + Assert.False(instance.DisposeCalled); + } - public void OnCompleted(Func callback, object state) - { - _callbacks.Add((callback, state)); - } + public class ResponseFeature : IHttpResponseFeature + { + private readonly List<(Func, object)> _callbacks = new(); + public int StatusCode { get; set; } + public string ReasonPhrase { get; set; } + public IHeaderDictionary Headers { get; set; } + public Stream Body { get; set; } - public void OnStarting(Func callback, object state) - { - throw new NotImplementedException(); - } + public bool HasStarted => false; - public async Task ExecuteOnCompletedCallbacks() - { - foreach (var (callback, state) in _callbacks) - { - await callback(state); - } - } + public void OnCompleted(Func callback, object state) + { + _callbacks.Add((callback, state)); } - public class DisposableClass : IDisposable, IAsyncDisposable + public void OnStarting(Func callback, object state) { - public bool DisposeCalled { get; set; } - - public bool DisposeAsyncCalled { get; set; } - - public void Dispose() - { - DisposeCalled = true; - } + throw new NotImplementedException(); + } - public ValueTask DisposeAsync() + public async Task ExecuteOnCompletedCallbacks() + { + foreach (var (callback, state) in _callbacks) { - DisposeAsyncCalled = true; - return ValueTask.CompletedTask; + await callback(state); } } + } + + public class DisposableClass : IDisposable, IAsyncDisposable + { + public bool DisposeCalled { get; set; } - private static HttpResponse CreateResponse(IHeaderDictionary headers) + public bool DisposeAsyncCalled { get; set; } + + public void Dispose() { - var context = new DefaultHttpContext(); - context.Features.Get().Headers = headers; - return context.Response; + DisposeCalled = true; } - private static HttpResponse GetResponseWithContentLength(string contentLength = null) + public ValueTask DisposeAsync() { - return GetResponseWithHeader("Content-Length", contentLength); + DisposeAsyncCalled = true; + return ValueTask.CompletedTask; } + } + + private static HttpResponse CreateResponse(IHeaderDictionary headers) + { + var context = new DefaultHttpContext(); + context.Features.Get().Headers = headers; + return context.Response; + } + + private static HttpResponse GetResponseWithContentLength(string contentLength = null) + { + return GetResponseWithHeader("Content-Length", contentLength); + } + + private static HttpResponse GetResponseWithContentType(string contentType = null) + { + return GetResponseWithHeader("Content-Type", contentType); + } - private static HttpResponse GetResponseWithContentType(string contentType = null) + private static HttpResponse GetResponseWithHeader(string headerName, string headerValue) + { + var headers = new HeaderDictionary(); + if (headerValue != null) { - return GetResponseWithHeader("Content-Type", contentType); + headers.Add(headerName, headerValue); } - private static HttpResponse GetResponseWithHeader(string headerName, string headerValue) - { - var headers = new HeaderDictionary(); - if (headerValue != null) - { - headers.Add(headerName, headerValue); - } + return CreateResponse(headers); + } - return CreateResponse(headers); - } + private class FlushAsyncCheckStream : MemoryStream + { + public bool IsCalled { get; private set; } - private class FlushAsyncCheckStream : MemoryStream + public override Task FlushAsync(CancellationToken cancellationToken) { - public bool IsCalled { get; private set; } - - public override Task FlushAsync(CancellationToken cancellationToken) - { - IsCalled = true; - return base.FlushAsync(cancellationToken); - } + IsCalled = true; + return base.FlushAsync(cancellationToken); } } } diff --git a/src/Http/Http/test/Internal/ItemsDictionaryTests.cs b/src/Http/Http/test/Internal/ItemsDictionaryTests.cs index 2ae8b61852..816adc8f1a 100644 --- a/src/Http/Http/test/Internal/ItemsDictionaryTests.cs +++ b/src/Http/Http/test/Internal/ItemsDictionaryTests.cs @@ -12,30 +12,30 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Primitives; using Xunit; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +public class ItemsDictionaryTests { - public class ItemsDictionaryTests + [Fact] + public void GetEnumerator_ShouldResolveWithoutNullReferenceException() { - [Fact] - public void GetEnumerator_ShouldResolveWithoutNullReferenceException() - { - // Arrange - var dict = new ItemsDictionary(); + // Arrange + var dict = new ItemsDictionary(); - // Act and Assert - IEnumerable en = (IEnumerable) dict; - Assert.NotNull(en.GetEnumerator()); - } + // Act and Assert + IEnumerable en = (IEnumerable)dict; + Assert.NotNull(en.GetEnumerator()); + } - [Fact] - public void CopyTo_ShouldCopyItemsWithoutNullReferenceException() { - // Arrange - var dict = new ItemsDictionary(); - var pairs = new KeyValuePair[] { new KeyValuePair("first", "value") }; + [Fact] + public void CopyTo_ShouldCopyItemsWithoutNullReferenceException() + { + // Arrange + var dict = new ItemsDictionary(); + var pairs = new KeyValuePair[] { new KeyValuePair("first", "value") }; - // Act and Assert - ICollection> cl = (ICollection>) dict; - cl.CopyTo(pairs, 0); - } + // Act and Assert + ICollection> cl = (ICollection>)dict; + cl.CopyTo(pairs, 0); } } diff --git a/src/Http/Http/test/Internal/ReferenceReadStreamTests.cs b/src/Http/Http/test/Internal/ReferenceReadStreamTests.cs index 256cdeb81d..59538a29ac 100644 --- a/src/Http/Http/test/Internal/ReferenceReadStreamTests.cs +++ b/src/Http/Http/test/Internal/ReferenceReadStreamTests.cs @@ -7,71 +7,70 @@ using System.Threading.Tasks; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +public class ReferenceReadStreamTests { - public class ReferenceReadStreamTests + [Fact] + public void CanRead_ReturnsTrue() { - [Fact] - public void CanRead_ReturnsTrue() - { - var stream = new ReferenceReadStream(Mock.Of(), 0, 1); - Assert.True(stream.CanRead); - } + var stream = new ReferenceReadStream(Mock.Of(), 0, 1); + Assert.True(stream.CanRead); + } - [Fact] - public void CanSeek_ReturnsFalse() - { - var stream = new ReferenceReadStream(Mock.Of(), 0, 1); - Assert.False(stream.CanSeek); - } + [Fact] + public void CanSeek_ReturnsFalse() + { + var stream = new ReferenceReadStream(Mock.Of(), 0, 1); + Assert.False(stream.CanSeek); + } - [Fact] - public void CanWrite_ReturnsFalse() - { - var stream = new ReferenceReadStream(Mock.Of(), 0, 1); - Assert.False(stream.CanWrite); - } + [Fact] + public void CanWrite_ReturnsFalse() + { + var stream = new ReferenceReadStream(Mock.Of(), 0, 1); + Assert.False(stream.CanWrite); + } - [Fact] - public void SetLength_Throws() - { - var stream = new ReferenceReadStream(Mock.Of(), 0, 1); - Assert.Throws(() => stream.SetLength(0)); - } + [Fact] + public void SetLength_Throws() + { + var stream = new ReferenceReadStream(Mock.Of(), 0, 1); + Assert.Throws(() => stream.SetLength(0)); + } - [Fact] - public void Write_Throws() - { - var stream = new ReferenceReadStream(Mock.Of(), 0, 1); - Assert.Throws(() => stream.Write(new byte[1], 0, 1)); - } + [Fact] + public void Write_Throws() + { + var stream = new ReferenceReadStream(Mock.Of(), 0, 1); + Assert.Throws(() => stream.Write(new byte[1], 0, 1)); + } - [Fact] - public void WriteByte_Throws() - { - var stream = new ReferenceReadStream(Mock.Of(), 0, 1); - Assert.Throws(() => stream.WriteByte(0)); - } + [Fact] + public void WriteByte_Throws() + { + var stream = new ReferenceReadStream(Mock.Of(), 0, 1); + Assert.Throws(() => stream.WriteByte(0)); + } - [Fact] - public async Task WriteAsync_Throws() - { - var stream = new ReferenceReadStream(Mock.Of(), 0, 1); - await Assert.ThrowsAsync(() => stream.WriteAsync(new byte[1], 0, 1)); - } + [Fact] + public async Task WriteAsync_Throws() + { + var stream = new ReferenceReadStream(Mock.Of(), 0, 1); + await Assert.ThrowsAsync(() => stream.WriteAsync(new byte[1], 0, 1)); + } - [Fact] - public void Flush_DoesNotThrow() - { - var stream = new ReferenceReadStream(Mock.Of(), 0, 1); - stream.Flush(); - } + [Fact] + public void Flush_DoesNotThrow() + { + var stream = new ReferenceReadStream(Mock.Of(), 0, 1); + stream.Flush(); + } - [Fact] - public async Task FlushAsync_DoesNotThrow() - { - var stream = new ReferenceReadStream(Mock.Of(), 0, 1); - await stream.FlushAsync(); - } + [Fact] + public async Task FlushAsync_DoesNotThrow() + { + var stream = new ReferenceReadStream(Mock.Of(), 0, 1); + await stream.FlushAsync(); } } diff --git a/src/Http/Http/test/RequestCookiesCollectionTests.cs b/src/Http/Http/test/RequestCookiesCollectionTests.cs index 0b90f13120..0804c4b102 100644 --- a/src/Http/Http/test/RequestCookiesCollectionTests.cs +++ b/src/Http/Http/test/RequestCookiesCollectionTests.cs @@ -5,46 +5,45 @@ using System.Linq; using Microsoft.Extensions.Primitives; using Xunit; -namespace Microsoft.AspNetCore.Http.Tests +namespace Microsoft.AspNetCore.Http.Tests; + +public class RequestCookiesCollectionTests { - public class RequestCookiesCollectionTests + [Theory] + [InlineData("key=value", "key", "value")] + [InlineData("key%2C=%21value", "key%2C", "!value")] + [InlineData("ke%23y%2C=val%5Eue", "ke%23y%2C", "val^ue")] + [InlineData("base64=QUI%2BREU%2FRw%3D%3D", "base64", "QUI+REU/Rw==")] + [InlineData("base64=QUI+REU/Rw==", "base64", "QUI+REU/Rw==")] + public void UnEscapesValues(string input, string expectedKey, string expectedValue) + { + var cookies = RequestCookieCollection.Parse(new StringValues(input)); + + Assert.Equal(1, cookies.Count); + Assert.Equal(expectedKey, cookies.Keys.Single()); + Assert.Equal(expectedValue, cookies[expectedKey]); + } + + [Theory] + [InlineData("key=value", "key", "value")] + [InlineData("key%2C=%21value", "key,", "!value")] + [InlineData("ke%23y%2C=val%5Eue", "ke#y,", "val^ue")] + [InlineData("base64=QUI%2BREU%2FRw%3D%3D", "base64", "QUI+REU/Rw==")] + [InlineData("base64=QUI+REU/Rw==", "base64", "QUI+REU/Rw==")] + public void AppContextSwitchUnEscapesKeysAndValues(string input, string expectedKey, string expectedValue) { - [Theory] - [InlineData("key=value", "key", "value")] - [InlineData("key%2C=%21value", "key%2C", "!value")] - [InlineData("ke%23y%2C=val%5Eue", "ke%23y%2C", "val^ue")] - [InlineData("base64=QUI%2BREU%2FRw%3D%3D", "base64", "QUI+REU/Rw==")] - [InlineData("base64=QUI+REU/Rw==", "base64", "QUI+REU/Rw==")] - public void UnEscapesValues(string input, string expectedKey, string expectedValue) - { - var cookies = RequestCookieCollection.Parse(new StringValues(input)); - - Assert.Equal(1, cookies.Count); - Assert.Equal(expectedKey, cookies.Keys.Single()); - Assert.Equal(expectedValue, cookies[expectedKey]); - } - - [Theory] - [InlineData("key=value", "key", "value")] - [InlineData("key%2C=%21value", "key,", "!value")] - [InlineData("ke%23y%2C=val%5Eue", "ke#y,", "val^ue")] - [InlineData("base64=QUI%2BREU%2FRw%3D%3D", "base64", "QUI+REU/Rw==")] - [InlineData("base64=QUI+REU/Rw==", "base64", "QUI+REU/Rw==")] - public void AppContextSwitchUnEscapesKeysAndValues(string input, string expectedKey, string expectedValue) - { - var cookies = RequestCookieCollection.ParseInternal(new StringValues(input), enableCookieNameEncoding: true); - - Assert.Equal(1, cookies.Count); - Assert.Equal(expectedKey, cookies.Keys.Single()); - Assert.Equal(expectedValue, cookies[expectedKey]); - } - - [Fact] - public void ParseManyCookies() - { - var cookies = RequestCookieCollection.Parse(new StringValues(new[] { "a=a", "b=b", "c=c", "d=d", "e=e", "f=f", "g=g", "h=h", "i=i", "j=j", "k=k", "l=l" })); - - Assert.Equal(12, cookies.Count); - } + var cookies = RequestCookieCollection.ParseInternal(new StringValues(input), enableCookieNameEncoding: true); + + Assert.Equal(1, cookies.Count); + Assert.Equal(expectedKey, cookies.Keys.Single()); + Assert.Equal(expectedValue, cookies[expectedKey]); + } + + [Fact] + public void ParseManyCookies() + { + var cookies = RequestCookieCollection.Parse(new StringValues(new[] { "a=a", "b=b", "c=c", "d=d", "e=e", "f=f", "g=g", "h=h", "i=i", "j=j", "k=k", "l=l" })); + + Assert.Equal(12, cookies.Count); } } diff --git a/src/Http/Http/test/ResponseCookiesTest.cs b/src/Http/Http/test/ResponseCookiesTest.cs index b29657567b..d1b1fd8274 100644 --- a/src/Http/Http/test/ResponseCookiesTest.cs +++ b/src/Http/Http/test/ResponseCookiesTest.cs @@ -10,242 +10,241 @@ using Microsoft.Extensions.Logging.Testing; using Microsoft.Net.Http.Headers; using Xunit; -namespace Microsoft.AspNetCore.Http.Tests +namespace Microsoft.AspNetCore.Http.Tests; + +public class ResponseCookiesTest { - public class ResponseCookiesTest + private IFeatureCollection MakeFeatures(IHeaderDictionary headers) { - private IFeatureCollection MakeFeatures(IHeaderDictionary headers) + var responseFeature = new HttpResponseFeature() { - var responseFeature = new HttpResponseFeature() - { - Headers = headers - }; - var features = new FeatureCollection(); - features.Set(responseFeature); - return features; - } + Headers = headers + }; + var features = new FeatureCollection(); + features.Set(responseFeature); + return features; + } - [Fact] - public void AppendSameSiteNoneWithoutSecureLogsWarning() - { - var headers = (IHeaderDictionary)new HeaderDictionary(); - var features = MakeFeatures(headers); - var services = new ServiceCollection(); - - var sink = new TestSink(TestSink.EnableWithTypeName); - var loggerFactory = new TestLoggerFactory(sink, enabled: true); - services.AddLogging(); - services.AddSingleton(loggerFactory); - - features.Set(new ServiceProvidersFeature() { RequestServices = services.BuildServiceProvider() }); - - var cookies = new ResponseCookies(features); - var testCookie = "TestCookie"; - - cookies.Append(testCookie, "value", new CookieOptions() - { - SameSite = SameSiteMode.None, - }); - - var cookieHeaderValues = headers.SetCookie; - Assert.Single(cookieHeaderValues); - Assert.StartsWith(testCookie, cookieHeaderValues[0]); - Assert.Contains("path=/", cookieHeaderValues[0]); - Assert.Contains("samesite=none", cookieHeaderValues[0]); - Assert.DoesNotContain("secure", cookieHeaderValues[0]); - - var writeContext = Assert.Single(sink.Writes); - Assert.Equal("The cookie 'TestCookie' has set 'SameSite=None' and must also set 'Secure'.", writeContext.Message); - } + [Fact] + public void AppendSameSiteNoneWithoutSecureLogsWarning() + { + var headers = (IHeaderDictionary)new HeaderDictionary(); + var features = MakeFeatures(headers); + var services = new ServiceCollection(); - [Fact] - public void DeleteCookieShouldSetDefaultPath() - { - var headers = (IHeaderDictionary)new HeaderDictionary(); - var features = MakeFeatures(headers); - var cookies = new ResponseCookies(features); - var testCookie = "TestCookie"; - - cookies.Delete(testCookie); - - var cookieHeaderValues = headers.SetCookie; - Assert.Single(cookieHeaderValues); - Assert.StartsWith(testCookie, cookieHeaderValues[0]); - Assert.Contains("path=/", cookieHeaderValues[0]); - Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]); - } + var sink = new TestSink(TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + services.AddLogging(); + services.AddSingleton(loggerFactory); + + features.Set(new ServiceProvidersFeature() { RequestServices = services.BuildServiceProvider() }); - [Fact] - public void DeleteCookieWithDomainAndPathDeletesPriorMatchingCookies() + var cookies = new ResponseCookies(features); + var testCookie = "TestCookie"; + + cookies.Append(testCookie, "value", new CookieOptions() { - var headers = (IHeaderDictionary)new HeaderDictionary(); - var features = MakeFeatures(headers); - var responseCookies = new ResponseCookies(features); + SameSite = SameSiteMode.None, + }); + + var cookieHeaderValues = headers.SetCookie; + Assert.Single(cookieHeaderValues); + Assert.StartsWith(testCookie, cookieHeaderValues[0]); + Assert.Contains("path=/", cookieHeaderValues[0]); + Assert.Contains("samesite=none", cookieHeaderValues[0]); + Assert.DoesNotContain("secure", cookieHeaderValues[0]); + + var writeContext = Assert.Single(sink.Writes); + Assert.Equal("The cookie 'TestCookie' has set 'SameSite=None' and must also set 'Secure'.", writeContext.Message); + } + + [Fact] + public void DeleteCookieShouldSetDefaultPath() + { + var headers = (IHeaderDictionary)new HeaderDictionary(); + var features = MakeFeatures(headers); + var cookies = new ResponseCookies(features); + var testCookie = "TestCookie"; + + cookies.Delete(testCookie); + + var cookieHeaderValues = headers.SetCookie; + Assert.Single(cookieHeaderValues); + Assert.StartsWith(testCookie, cookieHeaderValues[0]); + Assert.Contains("path=/", cookieHeaderValues[0]); + Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]); + } + + [Fact] + public void DeleteCookieWithDomainAndPathDeletesPriorMatchingCookies() + { + var headers = (IHeaderDictionary)new HeaderDictionary(); + var features = MakeFeatures(headers); + var responseCookies = new ResponseCookies(features); - var testCookies = new (string Key, string Path, string Domain)[] - { + var testCookies = new (string Key, string Path, string Domain)[] + { new ("key1", "/path1/", null), new ("key1", "/path2/", null), new ("key2", "/path1/", "localhost"), new ("key2", "/path2/", "localhost"), - }; + }; + + foreach (var cookie in testCookies) + { + responseCookies.Delete(cookie.Key, new CookieOptions() { Domain = cookie.Domain, Path = cookie.Path }); + } - foreach (var cookie in testCookies) - { - responseCookies.Delete(cookie.Key, new CookieOptions() { Domain = cookie.Domain, Path = cookie.Path }); - } + var deletedCookies = headers.SetCookie.ToArray(); + Assert.Equal(testCookies.Length, deletedCookies.Length); - var deletedCookies = headers.SetCookie.ToArray(); - Assert.Equal(testCookies.Length, deletedCookies.Length); + Assert.Single(deletedCookies, cookie => cookie.StartsWith("key1", StringComparison.InvariantCulture) && cookie.Contains("path=/path1/")); + Assert.Single(deletedCookies, cookie => cookie.StartsWith("key1", StringComparison.InvariantCulture) && cookie.Contains("path=/path2/")); + Assert.Single(deletedCookies, cookie => cookie.StartsWith("key2", StringComparison.InvariantCulture) && cookie.Contains("path=/path1/") && cookie.Contains("domain=localhost")); + Assert.Single(deletedCookies, cookie => cookie.StartsWith("key2", StringComparison.InvariantCulture) && cookie.Contains("path=/path2/") && cookie.Contains("domain=localhost")); + Assert.All(deletedCookies, cookie => Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookie)); + } - Assert.Single(deletedCookies, cookie => cookie.StartsWith("key1", StringComparison.InvariantCulture) && cookie.Contains("path=/path1/")); - Assert.Single(deletedCookies, cookie => cookie.StartsWith("key1", StringComparison.InvariantCulture) && cookie.Contains("path=/path2/")); - Assert.Single(deletedCookies, cookie => cookie.StartsWith("key2", StringComparison.InvariantCulture) && cookie.Contains("path=/path1/") && cookie.Contains("domain=localhost")); - Assert.Single(deletedCookies, cookie => cookie.StartsWith("key2", StringComparison.InvariantCulture) && cookie.Contains("path=/path2/") && cookie.Contains("domain=localhost")); - Assert.All(deletedCookies, cookie => Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookie)); - } + [Fact] + public void DeleteRemovesCookieWithDomainAndPathCreatedByAdd() + { + var headers = (IHeaderDictionary)new HeaderDictionary(); + var features = MakeFeatures(headers); + var responseCookies = new ResponseCookies(features); - [Fact] - public void DeleteRemovesCookieWithDomainAndPathCreatedByAdd() + var testCookies = new (string Key, string Path, string Domain)[] { - var headers = (IHeaderDictionary)new HeaderDictionary(); - var features = MakeFeatures(headers); - var responseCookies = new ResponseCookies(features); - - var testCookies = new (string Key, string Path, string Domain)[] - { new ("key1", "/path1/", null), new ("key1", "/path1/", null), new ("key2", "/path1/", "localhost"), new ("key2", "/path1/", "localhost"), - }; - - foreach (var cookie in testCookies) - { - responseCookies.Append(cookie.Key, cookie.Key, new CookieOptions() { Domain = cookie.Domain, Path = cookie.Path }); - responseCookies.Delete(cookie.Key, new CookieOptions() { Domain = cookie.Domain, Path = cookie.Path }); - } - - var deletedCookies = headers.SetCookie.ToArray(); - Assert.Equal(2, deletedCookies.Length); - Assert.Single(deletedCookies, cookie => cookie.StartsWith("key1", StringComparison.InvariantCulture) && cookie.Contains("path=/path1/")); - Assert.Single(deletedCookies, cookie => cookie.StartsWith("key2", StringComparison.InvariantCulture) && cookie.Contains("path=/path1/") && cookie.Contains("domain=localhost")); - Assert.All(deletedCookies, cookie => Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookie)); - } + }; - [Fact] - public void DeleteCookieWithCookieOptionsShouldKeepPropertiesOfCookieOptions() + foreach (var cookie in testCookies) { - var headers = (IHeaderDictionary)new HeaderDictionary(); - var features = MakeFeatures(headers); - var cookies = new ResponseCookies(features); - var testCookie = "TestCookie"; - var time = new DateTimeOffset(2000, 1, 1, 1, 1, 1, 1, TimeSpan.Zero); - var options = new CookieOptions - { - Secure = true, - HttpOnly = true, - Path = "/", - Expires = time, - Domain = "example.com", - SameSite = SameSiteMode.Lax - }; - - cookies.Delete(testCookie, options); - - var cookieHeaderValues = headers.SetCookie; - Assert.Single(cookieHeaderValues); - Assert.StartsWith(testCookie, cookieHeaderValues[0]); - Assert.Contains("path=/", cookieHeaderValues[0]); - Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]); - Assert.Contains("secure", cookieHeaderValues[0]); - Assert.Contains("httponly", cookieHeaderValues[0]); - Assert.Contains("samesite", cookieHeaderValues[0]); + responseCookies.Append(cookie.Key, cookie.Key, new CookieOptions() { Domain = cookie.Domain, Path = cookie.Path }); + responseCookies.Delete(cookie.Key, new CookieOptions() { Domain = cookie.Domain, Path = cookie.Path }); } - [Fact] - public void NoParamsDeleteRemovesCookieCreatedByAdd() - { - var headers = (IHeaderDictionary)new HeaderDictionary(); - var features = MakeFeatures(headers); - var cookies = new ResponseCookies(features); - var testCookie = "TestCookie"; - - cookies.Append(testCookie, testCookie); - cookies.Delete(testCookie); - - var cookieHeaderValues = headers.SetCookie; - Assert.Single(cookieHeaderValues); - Assert.StartsWith(testCookie, cookieHeaderValues[0]); - Assert.Contains("path=/", cookieHeaderValues[0]); - Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]); - } + var deletedCookies = headers.SetCookie.ToArray(); + Assert.Equal(2, deletedCookies.Length); + Assert.Single(deletedCookies, cookie => cookie.StartsWith("key1", StringComparison.InvariantCulture) && cookie.Contains("path=/path1/")); + Assert.Single(deletedCookies, cookie => cookie.StartsWith("key2", StringComparison.InvariantCulture) && cookie.Contains("path=/path1/") && cookie.Contains("domain=localhost")); + Assert.All(deletedCookies, cookie => Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookie)); + } - [Fact] - public void ProvidesMaxAgeWithCookieOptionsArgumentExpectMaxAgeToBeSet() + [Fact] + public void DeleteCookieWithCookieOptionsShouldKeepPropertiesOfCookieOptions() + { + var headers = (IHeaderDictionary)new HeaderDictionary(); + var features = MakeFeatures(headers); + var cookies = new ResponseCookies(features); + var testCookie = "TestCookie"; + var time = new DateTimeOffset(2000, 1, 1, 1, 1, 1, 1, TimeSpan.Zero); + var options = new CookieOptions { - var headers = (IHeaderDictionary)new HeaderDictionary(); - var features = MakeFeatures(headers); - var cookies = new ResponseCookies(features); - var cookieOptions = new CookieOptions(); - var maxAgeTime = TimeSpan.FromHours(1); - cookieOptions.MaxAge = TimeSpan.FromHours(1); - var testCookie = "TestCookie"; - - cookies.Append(testCookie, testCookie, cookieOptions); - - var cookieHeaderValues = headers.SetCookie; - Assert.Single(cookieHeaderValues); - Assert.Contains($"max-age={maxAgeTime.TotalSeconds}", cookieHeaderValues[0]); - } + Secure = true, + HttpOnly = true, + Path = "/", + Expires = time, + Domain = "example.com", + SameSite = SameSiteMode.Lax + }; + + cookies.Delete(testCookie, options); + + var cookieHeaderValues = headers.SetCookie; + Assert.Single(cookieHeaderValues); + Assert.StartsWith(testCookie, cookieHeaderValues[0]); + Assert.Contains("path=/", cookieHeaderValues[0]); + Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]); + Assert.Contains("secure", cookieHeaderValues[0]); + Assert.Contains("httponly", cookieHeaderValues[0]); + Assert.Contains("samesite", cookieHeaderValues[0]); + } - [Theory] - [InlineData("value", "key=value")] - [InlineData("!value", "key=%21value")] - [InlineData("val^ue", "key=val%5Eue")] - [InlineData("QUI+REU/Rw==", "key=QUI%2BREU%2FRw%3D%3D")] - public void EscapesValuesBeforeSettingCookie(string value, string expected) - { - var headers = (IHeaderDictionary)new HeaderDictionary(); - var features = MakeFeatures(headers); - var cookies = new ResponseCookies(features); + [Fact] + public void NoParamsDeleteRemovesCookieCreatedByAdd() + { + var headers = (IHeaderDictionary)new HeaderDictionary(); + var features = MakeFeatures(headers); + var cookies = new ResponseCookies(features); + var testCookie = "TestCookie"; + + cookies.Append(testCookie, testCookie); + cookies.Delete(testCookie); + + var cookieHeaderValues = headers.SetCookie; + Assert.Single(cookieHeaderValues); + Assert.StartsWith(testCookie, cookieHeaderValues[0]); + Assert.Contains("path=/", cookieHeaderValues[0]); + Assert.Contains("expires=Thu, 01 Jan 1970 00:00:00 GMT", cookieHeaderValues[0]); + } + + [Fact] + public void ProvidesMaxAgeWithCookieOptionsArgumentExpectMaxAgeToBeSet() + { + var headers = (IHeaderDictionary)new HeaderDictionary(); + var features = MakeFeatures(headers); + var cookies = new ResponseCookies(features); + var cookieOptions = new CookieOptions(); + var maxAgeTime = TimeSpan.FromHours(1); + cookieOptions.MaxAge = TimeSpan.FromHours(1); + var testCookie = "TestCookie"; + + cookies.Append(testCookie, testCookie, cookieOptions); + + var cookieHeaderValues = headers.SetCookie; + Assert.Single(cookieHeaderValues); + Assert.Contains($"max-age={maxAgeTime.TotalSeconds}", cookieHeaderValues[0]); + } - cookies.Append("key", value); + [Theory] + [InlineData("value", "key=value")] + [InlineData("!value", "key=%21value")] + [InlineData("val^ue", "key=val%5Eue")] + [InlineData("QUI+REU/Rw==", "key=QUI%2BREU%2FRw%3D%3D")] + public void EscapesValuesBeforeSettingCookie(string value, string expected) + { + var headers = (IHeaderDictionary)new HeaderDictionary(); + var features = MakeFeatures(headers); + var cookies = new ResponseCookies(features); - var cookieHeaderValues = headers.SetCookie; - Assert.Single(cookieHeaderValues); - Assert.StartsWith(expected, cookieHeaderValues[0]); - } + cookies.Append("key", value); - [Theory] - [InlineData("key,")] - [InlineData("ke@y")] - public void InvalidKeysThrow(string key) - { - var headers = new HeaderDictionary(); - var features = MakeFeatures(headers); - var cookies = new ResponseCookies(features); + var cookieHeaderValues = headers.SetCookie; + Assert.Single(cookieHeaderValues); + Assert.StartsWith(expected, cookieHeaderValues[0]); + } - Assert.Throws(() => cookies.Append(key, "1")); - } + [Theory] + [InlineData("key,")] + [InlineData("ke@y")] + public void InvalidKeysThrow(string key) + { + var headers = new HeaderDictionary(); + var features = MakeFeatures(headers); + var cookies = new ResponseCookies(features); - [Theory] - [InlineData("key", "value", "key=value")] - [InlineData("key,", "!value", "key%2C=%21value")] - [InlineData("ke#y,", "val^ue", "ke%23y%2C=val%5Eue")] - [InlineData("base64", "QUI+REU/Rw==", "base64=QUI%2BREU%2FRw%3D%3D")] - public void AppContextSwitchEscapesKeysAndValuesBeforeSettingCookie(string key, string value, string expected) - { - var headers = (IHeaderDictionary)new HeaderDictionary(); - var features = MakeFeatures(headers); - var cookies = new ResponseCookies(features); - cookies._enableCookieNameEncoding = true; + Assert.Throws(() => cookies.Append(key, "1")); + } - cookies.Append(key, value); + [Theory] + [InlineData("key", "value", "key=value")] + [InlineData("key,", "!value", "key%2C=%21value")] + [InlineData("ke#y,", "val^ue", "ke%23y%2C=val%5Eue")] + [InlineData("base64", "QUI+REU/Rw==", "base64=QUI%2BREU%2FRw%3D%3D")] + public void AppContextSwitchEscapesKeysAndValuesBeforeSettingCookie(string key, string value, string expected) + { + var headers = (IHeaderDictionary)new HeaderDictionary(); + var features = MakeFeatures(headers); + var cookies = new ResponseCookies(features); + cookies._enableCookieNameEncoding = true; - var cookieHeaderValues = headers.SetCookie; - Assert.Single(cookieHeaderValues); - Assert.StartsWith(expected, cookieHeaderValues[0]); - } + cookies.Append(key, value); + + var cookieHeaderValues = headers.SetCookie; + Assert.Single(cookieHeaderValues); + Assert.StartsWith(expected, cookieHeaderValues[0]); } } diff --git a/src/Http/Metadata/src/IAllowAnonymous.cs b/src/Http/Metadata/src/IAllowAnonymous.cs index ab48cb3dbf..7d62876aea 100644 --- a/src/Http/Metadata/src/IAllowAnonymous.cs +++ b/src/Http/Metadata/src/IAllowAnonymous.cs @@ -1,12 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Authorization +namespace Microsoft.AspNetCore.Authorization; + +/// +/// Marker interface to allow access to anonymous users. +/// +public interface IAllowAnonymous { - /// - /// Marker interface to allow access to anonymous users. - /// - public interface IAllowAnonymous - { - } } diff --git a/src/Http/Metadata/src/IAuthorizeData.cs b/src/Http/Metadata/src/IAuthorizeData.cs index 13b586f9f5..d3c3e9effc 100644 --- a/src/Http/Metadata/src/IAuthorizeData.cs +++ b/src/Http/Metadata/src/IAuthorizeData.cs @@ -1,26 +1,25 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Authorization +namespace Microsoft.AspNetCore.Authorization; + +/// +/// Defines the set of data required to apply authorization rules to a resource. +/// +public interface IAuthorizeData { /// - /// Defines the set of data required to apply authorization rules to a resource. + /// Gets or sets the policy name that determines access to the resource. /// - public interface IAuthorizeData - { - /// - /// Gets or sets the policy name that determines access to the resource. - /// - string? Policy { get; set; } + string? Policy { get; set; } - /// - /// Gets or sets a comma delimited list of roles that are allowed to access the resource. - /// - string? Roles { get; set; } + /// + /// Gets or sets a comma delimited list of roles that are allowed to access the resource. + /// + string? Roles { get; set; } - /// - /// Gets or sets a comma delimited list of schemes from which user information is constructed. - /// - string? AuthenticationSchemes { get; set; } - } + /// + /// Gets or sets a comma delimited list of schemes from which user information is constructed. + /// + string? AuthenticationSchemes { get; set; } } diff --git a/src/Http/Owin/src/DictionaryStringArrayWrapper.cs b/src/Http/Owin/src/DictionaryStringArrayWrapper.cs index 732078d517..8798003848 100644 --- a/src/Http/Owin/src/DictionaryStringArrayWrapper.cs +++ b/src/Http/Owin/src/DictionaryStringArrayWrapper.cs @@ -7,75 +7,74 @@ using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Owin +namespace Microsoft.AspNetCore.Owin; + +internal class DictionaryStringArrayWrapper : IDictionary { - internal class DictionaryStringArrayWrapper : IDictionary + public DictionaryStringArrayWrapper(IHeaderDictionary inner) { - public DictionaryStringArrayWrapper(IHeaderDictionary inner) - { - Inner = inner; - } + Inner = inner; + } - public readonly IHeaderDictionary Inner; + public readonly IHeaderDictionary Inner; - private KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); + private KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); - private KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); + private KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); - private StringValues Convert(string[] item) => item; + private StringValues Convert(string[] item) => item; - private string[] Convert(StringValues item) => item; + private string[] Convert(StringValues item) => item; - string[] IDictionary.this[string key] - { - get { return ((IDictionary)Inner)[key]; } - set { Inner[key] = value; } - } + string[] IDictionary.this[string key] + { + get { return ((IDictionary)Inner)[key]; } + set { Inner[key] = value; } + } - int ICollection>.Count => Inner.Count; + int ICollection>.Count => Inner.Count; - bool ICollection>.IsReadOnly => Inner.IsReadOnly; + bool ICollection>.IsReadOnly => Inner.IsReadOnly; - ICollection IDictionary.Keys => Inner.Keys; + ICollection IDictionary.Keys => Inner.Keys; - ICollection IDictionary.Values => Inner.Values.Select(Convert).ToList(); + ICollection IDictionary.Values => Inner.Values.Select(Convert).ToList(); - void ICollection>.Add(KeyValuePair item) => Inner.Add(Convert(item)); + void ICollection>.Add(KeyValuePair item) => Inner.Add(Convert(item)); - void IDictionary.Add(string key, string[] value) => Inner.Add(key, value); + void IDictionary.Add(string key, string[] value) => Inner.Add(key, value); - void ICollection>.Clear() => Inner.Clear(); + void ICollection>.Clear() => Inner.Clear(); - bool ICollection>.Contains(KeyValuePair item) => Inner.Contains(Convert(item)); + bool ICollection>.Contains(KeyValuePair item) => Inner.Contains(Convert(item)); - bool IDictionary.ContainsKey(string key) => Inner.ContainsKey(key); + bool IDictionary.ContainsKey(string key) => Inner.ContainsKey(key); - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + foreach (var kv in Inner) { - foreach(var kv in Inner) - { - array[arrayIndex++] = Convert(kv); - } + array[arrayIndex++] = Convert(kv); } + } - IEnumerator IEnumerable.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); - IEnumerator> IEnumerable>.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + IEnumerator> IEnumerable>.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); - bool ICollection>.Remove(KeyValuePair item) => Inner.Remove(Convert(item)); + bool ICollection>.Remove(KeyValuePair item) => Inner.Remove(Convert(item)); - bool IDictionary.Remove(string key) => Inner.Remove(key); + bool IDictionary.Remove(string key) => Inner.Remove(key); - bool IDictionary.TryGetValue(string key, out string[] value) + bool IDictionary.TryGetValue(string key, out string[] value) + { + StringValues temp; + if (Inner.TryGetValue(key, out temp)) { - StringValues temp; - if (Inner.TryGetValue(key, out temp)) - { - value = temp; - return true; - } - value = default(StringValues); - return false; + value = temp; + return true; } + value = default(StringValues); + return false; } } diff --git a/src/Http/Owin/src/DictionaryStringValuesWrapper.cs b/src/Http/Owin/src/DictionaryStringValuesWrapper.cs index 6c3ae81209..673e41b784 100644 --- a/src/Http/Owin/src/DictionaryStringValuesWrapper.cs +++ b/src/Http/Owin/src/DictionaryStringValuesWrapper.cs @@ -8,119 +8,118 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Owin +namespace Microsoft.AspNetCore.Owin; + +internal class DictionaryStringValuesWrapper : IHeaderDictionary { - internal class DictionaryStringValuesWrapper : IHeaderDictionary + public DictionaryStringValuesWrapper(IDictionary inner) { - public DictionaryStringValuesWrapper(IDictionary inner) - { - Inner = inner; - } + Inner = inner; + } - public readonly IDictionary Inner; + public readonly IDictionary Inner; - private KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); + private KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); - private KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); + private KeyValuePair Convert(KeyValuePair item) => new KeyValuePair(item.Key, item.Value); - private StringValues Convert(string[] item) => item; + private StringValues Convert(string[] item) => item; - private string[] Convert(StringValues item) => item; + private string[] Convert(StringValues item) => item; - StringValues IHeaderDictionary.this[string key] + StringValues IHeaderDictionary.this[string key] + { + get { - get - { - string[] values; - return Inner.TryGetValue(key, out values) ? values : null; - } - set { Inner[key] = value; } + string[] values; + return Inner.TryGetValue(key, out values) ? values : null; } + set { Inner[key] = value; } + } - StringValues IDictionary.this[string key] - { - get { return Inner[key]; } - set { Inner[key] = value; } - } + StringValues IDictionary.this[string key] + { + get { return Inner[key]; } + set { Inner[key] = value; } + } - public long? ContentLength + public long? ContentLength + { + get { - get - { - long value; + long value; - string[] rawValue; - if (!Inner.TryGetValue(HeaderNames.ContentLength, out rawValue)) - { - return null; - } + string[] rawValue; + if (!Inner.TryGetValue(HeaderNames.ContentLength, out rawValue)) + { + return null; + } - if (rawValue.Length == 1 && - !string.IsNullOrEmpty(rawValue[0]) && - HeaderUtilities.TryParseNonNegativeInt64(new StringSegment(rawValue[0]).Trim(), out value)) - { - return value; - } + if (rawValue.Length == 1 && + !string.IsNullOrEmpty(rawValue[0]) && + HeaderUtilities.TryParseNonNegativeInt64(new StringSegment(rawValue[0]).Trim(), out value)) + { + return value; + } - return null; + return null; + } + set + { + if (value.HasValue) + { + Inner[HeaderNames.ContentLength] = (StringValues)HeaderUtilities.FormatNonNegativeInt64(value.GetValueOrDefault()); } - set + else { - if (value.HasValue) - { - Inner[HeaderNames.ContentLength] = (StringValues)HeaderUtilities.FormatNonNegativeInt64(value.GetValueOrDefault()); - } - else - { - Inner.Remove(HeaderNames.ContentLength); - } + Inner.Remove(HeaderNames.ContentLength); } } + } - int ICollection>.Count => Inner.Count; + int ICollection>.Count => Inner.Count; - bool ICollection>.IsReadOnly => Inner.IsReadOnly; + bool ICollection>.IsReadOnly => Inner.IsReadOnly; - ICollection IDictionary.Keys => Inner.Keys; + ICollection IDictionary.Keys => Inner.Keys; - ICollection IDictionary.Values => Inner.Values.Select(Convert).ToList(); + ICollection IDictionary.Values => Inner.Values.Select(Convert).ToList(); - void ICollection>.Add(KeyValuePair item) => Inner.Add(Convert(item)); + void ICollection>.Add(KeyValuePair item) => Inner.Add(Convert(item)); - void IDictionary.Add(string key, StringValues value) => Inner.Add(key, value); + void IDictionary.Add(string key, StringValues value) => Inner.Add(key, value); - void ICollection>.Clear() => Inner.Clear(); + void ICollection>.Clear() => Inner.Clear(); - bool ICollection>.Contains(KeyValuePair item) => Inner.Contains(Convert(item)); + bool ICollection>.Contains(KeyValuePair item) => Inner.Contains(Convert(item)); - bool IDictionary.ContainsKey(string key) => Inner.ContainsKey(key); + bool IDictionary.ContainsKey(string key) => Inner.ContainsKey(key); - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + foreach (var kv in Inner) { - foreach (var kv in Inner) - { - array[arrayIndex++] = Convert(kv); - } + array[arrayIndex++] = Convert(kv); } + } - IEnumerator IEnumerable.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); - IEnumerator> IEnumerable>.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); + IEnumerator> IEnumerable>.GetEnumerator() => Inner.Select(Convert).GetEnumerator(); - bool ICollection>.Remove(KeyValuePair item) => Inner.Remove(Convert(item)); + bool ICollection>.Remove(KeyValuePair item) => Inner.Remove(Convert(item)); - bool IDictionary.Remove(string key) => Inner.Remove(key); + bool IDictionary.Remove(string key) => Inner.Remove(key); - bool IDictionary.TryGetValue(string key, out StringValues value) + bool IDictionary.TryGetValue(string key, out StringValues value) + { + string[] temp; + if (Inner.TryGetValue(key, out temp)) { - string[] temp; - if (Inner.TryGetValue(key, out temp)) - { - value = temp; - return true; - } - value = default(StringValues); - return false; + value = temp; + return true; } + value = default(StringValues); + return false; } } diff --git a/src/Http/Owin/src/IOwinEnvironmentFeature.cs b/src/Http/Owin/src/IOwinEnvironmentFeature.cs index 0d33392a8f..8eb6a816fc 100644 --- a/src/Http/Owin/src/IOwinEnvironmentFeature.cs +++ b/src/Http/Owin/src/IOwinEnvironmentFeature.cs @@ -3,16 +3,15 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.Owin +namespace Microsoft.AspNetCore.Owin; + +/// +/// A feature interface for an OWIN environment. +/// +public interface IOwinEnvironmentFeature { /// - /// A feature interface for an OWIN environment. + /// Gets or sets the environment values. /// - public interface IOwinEnvironmentFeature - { - /// - /// Gets or sets the environment values. - /// - IDictionary Environment { get; set; } - } + IDictionary Environment { get; set; } } diff --git a/src/Http/Owin/src/OwinConstants.cs b/src/Http/Owin/src/OwinConstants.cs index 1c6aa5167c..b00d5a73d7 100644 --- a/src/Http/Owin/src/OwinConstants.cs +++ b/src/Http/Owin/src/OwinConstants.cs @@ -1,177 +1,176 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Owin -{ - internal static class OwinConstants - { - #region OWIN v1.0.0 - 3.2.1. Request Data +namespace Microsoft.AspNetCore.Owin; - // http://owin.org/spec/spec/owin-1.0.0.html +internal static class OwinConstants +{ + #region OWIN v1.0.0 - 3.2.1. Request Data - public const string RequestScheme = "owin.RequestScheme"; - public const string RequestMethod = "owin.RequestMethod"; - public const string RequestPathBase = "owin.RequestPathBase"; - public const string RequestPath = "owin.RequestPath"; - public const string RequestQueryString = "owin.RequestQueryString"; - public const string RequestProtocol = "owin.RequestProtocol"; - public const string RequestHeaders = "owin.RequestHeaders"; - public const string RequestBody = "owin.RequestBody"; + // http://owin.org/spec/spec/owin-1.0.0.html - #endregion + public const string RequestScheme = "owin.RequestScheme"; + public const string RequestMethod = "owin.RequestMethod"; + public const string RequestPathBase = "owin.RequestPathBase"; + public const string RequestPath = "owin.RequestPath"; + public const string RequestQueryString = "owin.RequestQueryString"; + public const string RequestProtocol = "owin.RequestProtocol"; + public const string RequestHeaders = "owin.RequestHeaders"; + public const string RequestBody = "owin.RequestBody"; - #region OWIN v1.0.1 - 3.2.1 Request Data + #endregion - // OWIN 1.0.1 http://owin.org/html/owin.html + #region OWIN v1.0.1 - 3.2.1 Request Data - public const string RequestId = "owin.RequestId"; - public const string RequestUser = "owin.RequestUser"; + // OWIN 1.0.1 http://owin.org/html/owin.html - #endregion + public const string RequestId = "owin.RequestId"; + public const string RequestUser = "owin.RequestUser"; - #region OWIN v1.0.0 - 3.2.2. Response Data + #endregion - // http://owin.org/spec/spec/owin-1.0.0.html + #region OWIN v1.0.0 - 3.2.2. Response Data - public const string ResponseStatusCode = "owin.ResponseStatusCode"; - public const string ResponseReasonPhrase = "owin.ResponseReasonPhrase"; - public const string ResponseProtocol = "owin.ResponseProtocol"; - public const string ResponseHeaders = "owin.ResponseHeaders"; - public const string ResponseBody = "owin.ResponseBody"; + // http://owin.org/spec/spec/owin-1.0.0.html - #endregion + public const string ResponseStatusCode = "owin.ResponseStatusCode"; + public const string ResponseReasonPhrase = "owin.ResponseReasonPhrase"; + public const string ResponseProtocol = "owin.ResponseProtocol"; + public const string ResponseHeaders = "owin.ResponseHeaders"; + public const string ResponseBody = "owin.ResponseBody"; - #region OWIN v1.0.0 - 3.2.3. Other Data + #endregion - // http://owin.org/spec/spec/owin-1.0.0.html + #region OWIN v1.0.0 - 3.2.3. Other Data - public const string CallCancelled = "owin.CallCancelled"; + // http://owin.org/spec/spec/owin-1.0.0.html - public const string OwinVersion = "owin.Version"; + public const string CallCancelled = "owin.CallCancelled"; - #endregion + public const string OwinVersion = "owin.Version"; - #region OWIN Keys for IAppBuilder.Properties + #endregion - internal static class Builder - { - public const string AddSignatureConversion = "builder.AddSignatureConversion"; - public const string DefaultApp = "builder.DefaultApp"; - } + #region OWIN Keys for IAppBuilder.Properties - #endregion + internal static class Builder + { + public const string AddSignatureConversion = "builder.AddSignatureConversion"; + public const string DefaultApp = "builder.DefaultApp"; + } - #region OWIN Key Guidelines and Common Keys - 6. Common keys + #endregion - // http://owin.org/spec/spec/CommonKeys.html + #region OWIN Key Guidelines and Common Keys - 6. Common keys - internal static class CommonKeys - { - public const string ClientCertificate = "ssl.ClientCertificate"; - public const string LoadClientCertAsync = "ssl.LoadClientCertAsync"; - public const string RemoteIpAddress = "server.RemoteIpAddress"; - public const string RemotePort = "server.RemotePort"; - public const string LocalIpAddress = "server.LocalIpAddress"; - public const string LocalPort = "server.LocalPort"; - public const string ConnectionId = "server.ConnectionId"; - public const string TraceOutput = "host.TraceOutput"; - public const string Addresses = "host.Addresses"; - public const string AppName = "host.AppName"; - public const string Capabilities = "server.Capabilities"; - public const string OnSendingHeaders = "server.OnSendingHeaders"; - public const string OnAppDisposing = "host.OnAppDisposing"; - public const string Scheme = "scheme"; - public const string Host = "host"; - public const string Port = "port"; - public const string Path = "path"; - } + // http://owin.org/spec/spec/CommonKeys.html - #endregion + internal static class CommonKeys + { + public const string ClientCertificate = "ssl.ClientCertificate"; + public const string LoadClientCertAsync = "ssl.LoadClientCertAsync"; + public const string RemoteIpAddress = "server.RemoteIpAddress"; + public const string RemotePort = "server.RemotePort"; + public const string LocalIpAddress = "server.LocalIpAddress"; + public const string LocalPort = "server.LocalPort"; + public const string ConnectionId = "server.ConnectionId"; + public const string TraceOutput = "host.TraceOutput"; + public const string Addresses = "host.Addresses"; + public const string AppName = "host.AppName"; + public const string Capabilities = "server.Capabilities"; + public const string OnSendingHeaders = "server.OnSendingHeaders"; + public const string OnAppDisposing = "host.OnAppDisposing"; + public const string Scheme = "scheme"; + public const string Host = "host"; + public const string Port = "port"; + public const string Path = "path"; + } - #region SendFiles v0.3.0 + #endregion - // http://owin.org/spec/extensions/owin-SendFile-Extension-v0.3.0.htm + #region SendFiles v0.3.0 - internal static class SendFiles - { - // 3.1. Startup + // http://owin.org/spec/extensions/owin-SendFile-Extension-v0.3.0.htm - public const string Version = "sendfile.Version"; - public const string Support = "sendfile.Support"; - public const string Concurrency = "sendfile.Concurrency"; + internal static class SendFiles + { + // 3.1. Startup - // 3.2. Per Request + public const string Version = "sendfile.Version"; + public const string Support = "sendfile.Support"; + public const string Concurrency = "sendfile.Concurrency"; - public const string SendAsync = "sendfile.SendAsync"; - } + // 3.2. Per Request - #endregion + public const string SendAsync = "sendfile.SendAsync"; + } - #region Opaque v0.3.0 + #endregion - // http://owin.org/spec/extensions/owin-OpaqueStream-Extension-v0.3.0.htm + #region Opaque v0.3.0 - internal static class OpaqueConstants - { - // 3.1. Startup + // http://owin.org/spec/extensions/owin-OpaqueStream-Extension-v0.3.0.htm - public const string Version = "opaque.Version"; + internal static class OpaqueConstants + { + // 3.1. Startup - // 3.2. Per Request + public const string Version = "opaque.Version"; - public const string Upgrade = "opaque.Upgrade"; + // 3.2. Per Request - // 5. Consumption + public const string Upgrade = "opaque.Upgrade"; - public const string Stream = "opaque.Stream"; - // public const string Version = "opaque.Version"; // redundant, declared above - public const string CallCancelled = "opaque.CallCancelled"; - } + // 5. Consumption - #endregion + public const string Stream = "opaque.Stream"; + // public const string Version = "opaque.Version"; // redundant, declared above + public const string CallCancelled = "opaque.CallCancelled"; + } - #region WebSocket v0.4.0 + #endregion - // http://owin.org/spec/extensions/owin-OpaqueStream-Extension-v0.3.0.htm + #region WebSocket v0.4.0 - internal static class WebSocket - { - // 3.1. Startup + // http://owin.org/spec/extensions/owin-OpaqueStream-Extension-v0.3.0.htm - public const string Version = "websocket.Version"; - public const string VersionValue = "1.0"; + internal static class WebSocket + { + // 3.1. Startup - // 3.2. Per Request + public const string Version = "websocket.Version"; + public const string VersionValue = "1.0"; - public const string Accept = "websocket.Accept"; - public const string AcceptAlt = "websocket.AcceptAlt"; // Non-spec + // 3.2. Per Request - // 4. Accept + public const string Accept = "websocket.Accept"; + public const string AcceptAlt = "websocket.AcceptAlt"; // Non-spec - public const string SubProtocol = "websocket.SubProtocol"; + // 4. Accept - // 5. Consumption + public const string SubProtocol = "websocket.SubProtocol"; - public const string SendAsync = "websocket.SendAsync"; - public const string ReceiveAsync = "websocket.ReceiveAsync"; - public const string CloseAsync = "websocket.CloseAsync"; - // public const string Version = "websocket.Version"; // redundant, declared above - public const string CallCancelled = "websocket.CallCancelled"; - public const string ClientCloseStatus = "websocket.ClientCloseStatus"; - public const string ClientCloseDescription = "websocket.ClientCloseDescription"; - } + // 5. Consumption - #endregion + public const string SendAsync = "websocket.SendAsync"; + public const string ReceiveAsync = "websocket.ReceiveAsync"; + public const string CloseAsync = "websocket.CloseAsync"; + // public const string Version = "websocket.Version"; // redundant, declared above + public const string CallCancelled = "websocket.CallCancelled"; + public const string ClientCloseStatus = "websocket.ClientCloseStatus"; + public const string ClientCloseDescription = "websocket.ClientCloseDescription"; + } - #region Security v0.1.0 + #endregion - internal static class Security - { - // 3.2. Per Request + #region Security v0.1.0 - public const string User = "server.User"; - } + internal static class Security + { + // 3.2. Per Request - #endregion + public const string User = "server.User"; } + + #endregion } diff --git a/src/Http/Owin/src/OwinEnvironment.cs b/src/Http/Owin/src/OwinEnvironment.cs index 1edaad5819..70f9fab224 100644 --- a/src/Http/Owin/src/OwinEnvironment.cs +++ b/src/Http/Owin/src/OwinEnvironment.cs @@ -18,41 +18,41 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features.Authentication; -namespace Microsoft.AspNetCore.Owin +namespace Microsoft.AspNetCore.Owin; + +using SendFileFunc = Func; +using WebSocketAcceptAlt = + Func + < + WebSocketAcceptContext, // WebSocket Accept parameters + Task + >; + +/// +/// A loosely-typed OWIN environment wrapper over an . +/// +public class OwinEnvironment : IDictionary { - using SendFileFunc = Func; - using WebSocketAcceptAlt = - Func - < - WebSocketAcceptContext, // WebSocket Accept parameters - Task - >; + private readonly HttpContext _context; + private readonly IDictionary _entries; /// - /// A loosely-typed OWIN environment wrapper over an . + /// Initializes a new instance of . /// - public class OwinEnvironment : IDictionary + /// The request context. + public OwinEnvironment(HttpContext context) { - private readonly HttpContext _context; - private readonly IDictionary _entries; - - /// - /// Initializes a new instance of . - /// - /// The request context. - public OwinEnvironment(HttpContext context) + if (context.Features.Get() == null) { - if (context.Features.Get() == null) - { - throw new ArgumentException("Missing required feature: " + nameof(IHttpRequestFeature) + ".", nameof(context)); - } - if (context.Features.Get() == null) - { - throw new ArgumentException("Missing required feature: " + nameof(IHttpResponseFeature) + ".", nameof(context)); - } + throw new ArgumentException("Missing required feature: " + nameof(IHttpRequestFeature) + ".", nameof(context)); + } + if (context.Features.Get() == null) + { + throw new ArgumentException("Missing required feature: " + nameof(IHttpResponseFeature) + ".", nameof(context)); + } - _context = context; - _entries = new Dictionary() + _context = context; + _entries = new Dictionary() { { OwinConstants.RequestProtocol, new FeatureMap(feature => feature.Protocol, () => string.Empty, (feature, value) => feature.Protocol = Convert.ToString(value, CultureInfo.InvariantCulture)) }, { OwinConstants.RequestScheme, new FeatureMap(feature => feature.Scheme, () => string.Empty, (feature, value) => feature.Scheme = Convert.ToString(value, CultureInfo.InvariantCulture)) }, @@ -105,374 +105,373 @@ namespace Microsoft.AspNetCore.Owin } }; - // owin.CallCancelled is required but the feature may not be present. - if (context.Features.Get() != null) - { - _entries[OwinConstants.CallCancelled] = new FeatureMap(feature => feature.RequestAborted); - } - else if (!_context.Items.ContainsKey(OwinConstants.CallCancelled)) - { - _context.Items[OwinConstants.CallCancelled] = CancellationToken.None; - } - - // owin.Version is required. - if (!context.Items.ContainsKey(OwinConstants.OwinVersion)) - { - _context.Items[OwinConstants.OwinVersion] = "1.0"; - } - - if (context.Request.IsHttps) - { - _entries.Add(OwinConstants.CommonKeys.ClientCertificate, new FeatureMap(feature => feature.ClientCertificate, - (feature, value) => feature.ClientCertificate = (X509Certificate2)value)); - _entries.Add(OwinConstants.CommonKeys.LoadClientCertAsync, new FeatureMap( - feature => new Func(() => feature.GetClientCertificateAsync(CancellationToken.None)))); - } + // owin.CallCancelled is required but the feature may not be present. + if (context.Features.Get() != null) + { + _entries[OwinConstants.CallCancelled] = new FeatureMap(feature => feature.RequestAborted); + } + else if (!_context.Items.ContainsKey(OwinConstants.CallCancelled)) + { + _context.Items[OwinConstants.CallCancelled] = CancellationToken.None; + } - if (context.WebSockets.IsWebSocketRequest) - { - _entries.Add(OwinConstants.WebSocket.AcceptAlt, new FeatureMap(feature => new WebSocketAcceptAlt(feature.AcceptAsync))); - } + // owin.Version is required. + if (!context.Items.ContainsKey(OwinConstants.OwinVersion)) + { + _context.Items[OwinConstants.OwinVersion] = "1.0"; + } - _context.Items[typeof(HttpContext).FullName] = _context; // Store for lookup when we transition back out of OWIN + if (context.Request.IsHttps) + { + _entries.Add(OwinConstants.CommonKeys.ClientCertificate, new FeatureMap(feature => feature.ClientCertificate, + (feature, value) => feature.ClientCertificate = (X509Certificate2)value)); + _entries.Add(OwinConstants.CommonKeys.LoadClientCertAsync, new FeatureMap( + feature => new Func(() => feature.GetClientCertificateAsync(CancellationToken.None)))); } - // Public in case there's a new/custom feature interface that needs to be added. - /// - /// Get the environment's feature maps. - /// - public IDictionary FeatureMaps + if (context.WebSockets.IsWebSocketRequest) { - get { return _entries; } + _entries.Add(OwinConstants.WebSocket.AcceptAlt, new FeatureMap(feature => new WebSocketAcceptAlt(feature.AcceptAsync))); } - void IDictionary.Add(string key, object value) + _context.Items[typeof(HttpContext).FullName] = _context; // Store for lookup when we transition back out of OWIN + } + + // Public in case there's a new/custom feature interface that needs to be added. + /// + /// Get the environment's feature maps. + /// + public IDictionary FeatureMaps + { + get { return _entries; } + } + + void IDictionary.Add(string key, object value) + { + if (_entries.ContainsKey(key)) { - if (_entries.ContainsKey(key)) - { - throw new InvalidOperationException("Key already present"); - } - _context.Items.Add(key, value); + throw new InvalidOperationException("Key already present"); } + _context.Items.Add(key, value); + } - bool IDictionary.ContainsKey(string key) + bool IDictionary.ContainsKey(string key) + { + object value; + return ((IDictionary)this).TryGetValue(key, out value); + } + + ICollection IDictionary.Keys + { + get { object value; - return ((IDictionary)this).TryGetValue(key, out value); + return _entries.Where(pair => pair.Value.TryGet(_context, out value)) + .Select(pair => pair.Key).Concat(_context.Items.Keys.Select(key => Convert.ToString(key, CultureInfo.InvariantCulture))).ToList(); } + } - ICollection IDictionary.Keys + bool IDictionary.Remove(string key) + { + if (_entries.Remove(key)) { - get - { - object value; - return _entries.Where(pair => pair.Value.TryGet(_context, out value)) - .Select(pair => pair.Key).Concat(_context.Items.Keys.Select(key => Convert.ToString(key, CultureInfo.InvariantCulture))).ToList(); - } + return true; } + return _context.Items.Remove(key); + } - bool IDictionary.Remove(string key) + bool IDictionary.TryGetValue(string key, out object value) + { + FeatureMap entry; + if (_entries.TryGetValue(key, out entry) && entry.TryGet(_context, out value)) { - if (_entries.Remove(key)) - { - return true; - } - return _context.Items.Remove(key); + return true; } + return _context.Items.TryGetValue(key, out value); + } + + ICollection IDictionary.Values + { + get { throw new NotImplementedException(); } + } - bool IDictionary.TryGetValue(string key, out object value) + object IDictionary.this[string key] + { + get { FeatureMap entry; + object value; if (_entries.TryGetValue(key, out entry) && entry.TryGet(_context, out value)) { - return true; + return value; } - return _context.Items.TryGetValue(key, out value); - } - - ICollection IDictionary.Values - { - get { throw new NotImplementedException(); } + if (_context.Items.TryGetValue(key, out value)) + { + return value; + } + throw new KeyNotFoundException(key); } - - object IDictionary.this[string key] + set { - get + FeatureMap entry; + if (_entries.TryGetValue(key, out entry)) { - FeatureMap entry; - object value; - if (_entries.TryGetValue(key, out entry) && entry.TryGet(_context, out value)) + if (entry.CanSet) { - return value; + entry.Set(_context, value); } - if (_context.Items.TryGetValue(key, out value)) + else { - return value; + _entries.Remove(key); + if (value != null) + { + _context.Items[key] = value; + } } - throw new KeyNotFoundException(key); } - set + else { - FeatureMap entry; - if (_entries.TryGetValue(key, out entry)) + if (value == null) { - if (entry.CanSet) - { - entry.Set(_context, value); - } - else - { - _entries.Remove(key); - if (value != null) - { - _context.Items[key] = value; - } - } + _context.Items.Remove(key); } else { - if (value == null) - { - _context.Items.Remove(key); - } - else - { - _context.Items[key] = value; - } + _context.Items[key] = value; } } } + } - void ICollection>.Add(KeyValuePair item) - { - throw new NotImplementedException(); - } + void ICollection>.Add(KeyValuePair item) + { + throw new NotImplementedException(); + } - void ICollection>.Clear() - { - _entries.Clear(); - _context.Items.Clear(); - } + void ICollection>.Clear() + { + _entries.Clear(); + _context.Items.Clear(); + } - bool ICollection>.Contains(KeyValuePair item) - { - throw new NotImplementedException(); - } + bool ICollection>.Contains(KeyValuePair item) + { + throw new NotImplementedException(); + } - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + int ICollection>.Count + { + get { return _entries.Count + _context.Items.Count; } + } + + bool ICollection>.IsReadOnly + { + get { return false; } + } + + bool ICollection>.Remove(KeyValuePair item) + { + throw new NotImplementedException(); + } + + /// + public IEnumerator> GetEnumerator() + { + foreach (var entryPair in _entries) { - throw new NotImplementedException(); + object value; + if (entryPair.Value.TryGet(_context, out value)) + { + yield return new KeyValuePair(entryPair.Key, value); + } } - - int ICollection>.Count + foreach (var entryPair in _context.Items) { - get { return _entries.Count + _context.Items.Count; } + yield return new KeyValuePair(Convert.ToString(entryPair.Key, CultureInfo.InvariantCulture), entryPair.Value); } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } - bool ICollection>.IsReadOnly + /// + /// Maps OWIN keys to ASP.NET Core features. + /// + public class FeatureMap + { + /// + /// Create a for the specified feature interface type. + /// + /// The feature interface type. + /// Value getter. + public FeatureMap(Type featureInterface, Func getter) + : this(featureInterface, getter, defaultFactory: null) { - get { return false; } } - bool ICollection>.Remove(KeyValuePair item) + /// + /// Initializes a new instance of for the specified feature interface type. + /// + /// The feature interface type. + /// Value getter delegate. + /// Default value factory delegate. + public FeatureMap(Type featureInterface, Func getter, Func defaultFactory) + : this(featureInterface, getter, defaultFactory, setter: null) { - throw new NotImplementedException(); } - /// - public IEnumerator> GetEnumerator() + /// + /// Initializes a new instance of for the specified feature interface type. + /// + /// The feature interface type. + /// Value getter delegate. + /// Value setter delegate. + public FeatureMap(Type featureInterface, Func getter, Action setter) + : this(featureInterface, getter, defaultFactory: null, setter: setter) { - foreach (var entryPair in _entries) - { - object value; - if (entryPair.Value.TryGet(_context, out value)) - { - yield return new KeyValuePair(entryPair.Key, value); - } - } - foreach (var entryPair in _context.Items) - { - yield return new KeyValuePair(Convert.ToString(entryPair.Key, CultureInfo.InvariantCulture), entryPair.Value); - } } - IEnumerator IEnumerable.GetEnumerator() + /// + /// Initializes a new instance of for the specified feature interface type. + /// + /// The feature interface type. + /// Value getter delegate. + /// Default value factory delegate. + /// Value setter delegate. + public FeatureMap(Type featureInterface, Func getter, Func defaultFactory, Action setter) + : this(featureInterface, getter, defaultFactory, setter, featureFactory: null) { - return GetEnumerator(); } /// - /// Maps OWIN keys to ASP.NET Core features. + /// Initializes a new instance of for the specified feature interface type. /// - public class FeatureMap + /// The feature interface type. + /// Value getter delegate. + /// Default value factory delegate. + /// Value setter delegate. + /// Feature factory delegate. + public FeatureMap(Type featureInterface, Func getter, Func defaultFactory, Action setter, Func featureFactory) { - /// - /// Create a for the specified feature interface type. - /// - /// The feature interface type. - /// Value getter. - public FeatureMap(Type featureInterface, Func getter) - : this(featureInterface, getter, defaultFactory: null) - { - } - - /// - /// Initializes a new instance of for the specified feature interface type. - /// - /// The feature interface type. - /// Value getter delegate. - /// Default value factory delegate. - public FeatureMap(Type featureInterface, Func getter, Func defaultFactory) - : this(featureInterface, getter, defaultFactory, setter: null) - { - } + FeatureInterface = featureInterface; + Getter = getter; + Setter = setter; + DefaultFactory = defaultFactory; + FeatureFactory = featureFactory; + } - /// - /// Initializes a new instance of for the specified feature interface type. - /// - /// The feature interface type. - /// Value getter delegate. - /// Value setter delegate. - public FeatureMap(Type featureInterface, Func getter, Action setter) - : this(featureInterface, getter, defaultFactory: null, setter: setter) - { - } + private Type FeatureInterface { get; set; } + private Func Getter { get; set; } + private Action Setter { get; set; } + private Func DefaultFactory { get; set; } + private Func FeatureFactory { get; set; } - /// - /// Initializes a new instance of for the specified feature interface type. - /// - /// The feature interface type. - /// Value getter delegate. - /// Default value factory delegate. - /// Value setter delegate. - public FeatureMap(Type featureInterface, Func getter, Func defaultFactory, Action setter) - : this(featureInterface, getter, defaultFactory, setter, featureFactory: null) - { - } + /// + /// Gets a value indicating whether the feature map is settable. + /// + public bool CanSet + { + get { return Setter != null; } + } - /// - /// Initializes a new instance of for the specified feature interface type. - /// - /// The feature interface type. - /// Value getter delegate. - /// Default value factory delegate. - /// Value setter delegate. - /// Feature factory delegate. - public FeatureMap(Type featureInterface, Func getter, Func defaultFactory, Action setter, Func featureFactory) + internal bool TryGet(HttpContext context, out object value) + { + object featureInstance = context.Features[FeatureInterface]; + if (featureInstance == null) { - FeatureInterface = featureInterface; - Getter = getter; - Setter = setter; - DefaultFactory = defaultFactory; - FeatureFactory = featureFactory; + value = null; + return false; } - - private Type FeatureInterface { get; set; } - private Func Getter { get; set; } - private Action Setter { get; set; } - private Func DefaultFactory { get; set; } - private Func FeatureFactory { get; set; } - - /// - /// Gets a value indicating whether the feature map is settable. - /// - public bool CanSet + value = Getter(featureInstance); + if (value == null && DefaultFactory != null) { - get { return Setter != null; } + value = DefaultFactory(); } + return true; + } - internal bool TryGet(HttpContext context, out object value) + internal void Set(HttpContext context, object value) + { + var feature = context.Features[FeatureInterface]; + if (feature == null) { - object featureInstance = context.Features[FeatureInterface]; - if (featureInstance == null) - { - value = null; - return false; - } - value = Getter(featureInstance); - if (value == null && DefaultFactory != null) + if (FeatureFactory == null) { - value = DefaultFactory(); + throw new InvalidOperationException("Missing feature: " + FeatureInterface.FullName); // TODO: LOC } - return true; - } - - internal void Set(HttpContext context, object value) - { - var feature = context.Features[FeatureInterface]; - if (feature == null) + else { - if (FeatureFactory == null) - { - throw new InvalidOperationException("Missing feature: " + FeatureInterface.FullName); // TODO: LOC - } - else - { - feature = FeatureFactory(); - context.Features[FeatureInterface] = feature; - } + feature = FeatureFactory(); + context.Features[FeatureInterface] = feature; } - Setter(feature, value); } + Setter(feature, value); } + } + /// + /// Maps OWIN keys to ASP.NET Core features. + /// + /// Feature interface type. + public class FeatureMap : FeatureMap + { /// - /// Maps OWIN keys to ASP.NET Core features. + /// Initializes a new instance of for the specified feature interface type. /// - /// Feature interface type. - public class FeatureMap : FeatureMap + /// Value getter. + public FeatureMap(Func getter) + : base(typeof(TFeature), feature => getter((TFeature)feature)) { - /// - /// Initializes a new instance of for the specified feature interface type. - /// - /// Value getter. - public FeatureMap(Func getter) - : base(typeof(TFeature), feature => getter((TFeature)feature)) - { - } + } - /// - /// Initializes a new instance of for the specified feature interface type. - /// - /// Value getter delegate. - /// Default value factory delegate. - public FeatureMap(Func getter, Func defaultFactory) - : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory) - { - } + /// + /// Initializes a new instance of for the specified feature interface type. + /// + /// Value getter delegate. + /// Default value factory delegate. + public FeatureMap(Func getter, Func defaultFactory) + : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory) + { + } - /// - /// Initializes a new instance of for the specified feature interface type. - /// - /// Value getter delegate. - /// Value setter delegate. - public FeatureMap(Func getter, Action setter) - : base(typeof(TFeature), feature => getter((TFeature)feature), (feature, value) => setter((TFeature)feature, value)) - { - } + /// + /// Initializes a new instance of for the specified feature interface type. + /// + /// Value getter delegate. + /// Value setter delegate. + public FeatureMap(Func getter, Action setter) + : base(typeof(TFeature), feature => getter((TFeature)feature), (feature, value) => setter((TFeature)feature, value)) + { + } - /// - /// Initializes a new instance of for the specified feature interface type. - /// - /// Value getter delegate. - /// Default value factory delegate. - /// Value setter delegate. - public FeatureMap(Func getter, Func defaultFactory, Action setter) - : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory, (feature, value) => setter((TFeature)feature, value)) - { - } + /// + /// Initializes a new instance of for the specified feature interface type. + /// + /// Value getter delegate. + /// Default value factory delegate. + /// Value setter delegate. + public FeatureMap(Func getter, Func defaultFactory, Action setter) + : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory, (feature, value) => setter((TFeature)feature, value)) + { + } - /// - /// Initializes a new instance of for the specified feature interface type. - /// - /// Value getter delegate. - /// Default value factory delegate. - /// Value setter delegate. - /// Feature factory delegate. - public FeatureMap(Func getter, Func defaultFactory, Action setter, Func featureFactory) - : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory, (feature, value) => setter((TFeature)feature, value), () => featureFactory()) - { - } + /// + /// Initializes a new instance of for the specified feature interface type. + /// + /// Value getter delegate. + /// Default value factory delegate. + /// Value setter delegate. + /// Feature factory delegate. + public FeatureMap(Func getter, Func defaultFactory, Action setter, Func featureFactory) + : base(typeof(TFeature), feature => getter((TFeature)feature), defaultFactory, (feature, value) => setter((TFeature)feature, value), () => featureFactory()) + { } } } diff --git a/src/Http/Owin/src/OwinEnvironmentFeature.cs b/src/Http/Owin/src/OwinEnvironmentFeature.cs index 9419f30b04..8960d3e26f 100644 --- a/src/Http/Owin/src/OwinEnvironmentFeature.cs +++ b/src/Http/Owin/src/OwinEnvironmentFeature.cs @@ -3,14 +3,13 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.Owin +namespace Microsoft.AspNetCore.Owin; + +/// +/// Default implementation of . +/// +public class OwinEnvironmentFeature : IOwinEnvironmentFeature { - /// - /// Default implementation of . - /// - public class OwinEnvironmentFeature : IOwinEnvironmentFeature - { - /// - public IDictionary Environment { get; set; } - } + /// + public IDictionary Environment { get; set; } } diff --git a/src/Http/Owin/src/OwinExtensions.cs b/src/Http/Owin/src/OwinExtensions.cs index ffafb19911..5ebc288b50 100644 --- a/src/Http/Owin/src/OwinExtensions.cs +++ b/src/Http/Owin/src/OwinExtensions.cs @@ -8,205 +8,204 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Owin; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +using AddMiddleware = Action, Task>, + Func, Task> + >>; +using AppFunc = Func, Task>; +using CreateMiddleware = Func< + Func, Task>, + Func, Task> + >; + +/// +/// Extension methods to add OWIN to an HTTP application pipeline. +/// +public static class OwinExtensions { - using AddMiddleware = Action, Task>, - Func, Task> - >>; - using AppFunc = Func, Task>; - using CreateMiddleware = Func< - Func, Task>, - Func, Task> - >; - /// - /// Extension methods to add OWIN to an HTTP application pipeline. + /// Adds an OWIN pipeline to the specified . /// - public static class OwinExtensions + /// The to add the pipeline to. + /// An action used to create the OWIN pipeline. + public static AddMiddleware UseOwin(this IApplicationBuilder builder) { - /// - /// Adds an OWIN pipeline to the specified . - /// - /// The to add the pipeline to. - /// An action used to create the OWIN pipeline. - public static AddMiddleware UseOwin(this IApplicationBuilder builder) + if (builder == null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + throw new ArgumentNullException(nameof(builder)); + } - AddMiddleware add = middleware => + AddMiddleware add = middleware => + { + Func middleware1 = next1 => { - Func middleware1 = next1 => + AppFunc exitMiddleware = env => + { + return next1((HttpContext)env[typeof(HttpContext).FullName]); + }; + var app = middleware(exitMiddleware); + return httpContext => { - AppFunc exitMiddleware = env => - { - return next1((HttpContext)env[typeof(HttpContext).FullName]); - }; - var app = middleware(exitMiddleware); - return httpContext => - { // Use the existing OWIN env if there is one. IDictionary env; - var owinEnvFeature = httpContext.Features.Get(); - if (owinEnvFeature != null) - { - env = owinEnvFeature.Environment; - env[typeof(HttpContext).FullName] = httpContext; - } - else - { - env = new OwinEnvironment(httpContext); - } - return app.Invoke(env); - }; + var owinEnvFeature = httpContext.Features.Get(); + if (owinEnvFeature != null) + { + env = owinEnvFeature.Environment; + env[typeof(HttpContext).FullName] = httpContext; + } + else + { + env = new OwinEnvironment(httpContext); + } + return app.Invoke(env); }; - builder.Use(middleware1); }; - // Adapt WebSockets by default. - add(WebSocketAcceptAdapter.AdaptWebSockets); - return add; - } + builder.Use(middleware1); + }; + // Adapt WebSockets by default. + add(WebSocketAcceptAdapter.AdaptWebSockets); + return add; + } - /// - /// Adds OWIN middleware pipeline to the specified . - /// - /// The to add the middleware to. - /// A delegate which can specify the OWIN pipeline. - /// The original . - public static IApplicationBuilder UseOwin(this IApplicationBuilder builder, Action pipeline) + /// + /// Adds OWIN middleware pipeline to the specified . + /// + /// The to add the middleware to. + /// A delegate which can specify the OWIN pipeline. + /// The original . + public static IApplicationBuilder UseOwin(this IApplicationBuilder builder, Action pipeline) + { + if (builder == null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - if (pipeline == null) - { - throw new ArgumentNullException(nameof(pipeline)); - } - - pipeline(builder.UseOwin()); - return builder; + throw new ArgumentNullException(nameof(builder)); } - - /// - /// Creates an for an OWIN pipeline. - /// - /// The OWIN pipeline. - /// An - public static IApplicationBuilder UseBuilder(this AddMiddleware app) + if (pipeline == null) { - return app.UseBuilder(serviceProvider: null); + throw new ArgumentNullException(nameof(pipeline)); } - /// - /// Creates an for an OWIN pipeline. - /// - /// The OWIN pipeline. - /// A service provider for . - /// An . - public static IApplicationBuilder UseBuilder(this AddMiddleware app, IServiceProvider serviceProvider) + pipeline(builder.UseOwin()); + return builder; + } + + /// + /// Creates an for an OWIN pipeline. + /// + /// The OWIN pipeline. + /// An + public static IApplicationBuilder UseBuilder(this AddMiddleware app) + { + return app.UseBuilder(serviceProvider: null); + } + + /// + /// Creates an for an OWIN pipeline. + /// + /// The OWIN pipeline. + /// A service provider for . + /// An . + public static IApplicationBuilder UseBuilder(this AddMiddleware app, IServiceProvider serviceProvider) + { + if (app == null) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } + throw new ArgumentNullException(nameof(app)); + } - // Do not set ApplicationBuilder.ApplicationServices to null. May fail later due to missing services but - // at least that results in a more useful Exception than a NRE. - if (serviceProvider == null) - { - serviceProvider = new EmptyProvider(); - } + // Do not set ApplicationBuilder.ApplicationServices to null. May fail later due to missing services but + // at least that results in a more useful Exception than a NRE. + if (serviceProvider == null) + { + serviceProvider = new EmptyProvider(); + } - // Adapt WebSockets by default. - app(OwinWebSocketAcceptAdapter.AdaptWebSockets); - var builder = new ApplicationBuilder(serviceProvider: serviceProvider); + // Adapt WebSockets by default. + app(OwinWebSocketAcceptAdapter.AdaptWebSockets); + var builder = new ApplicationBuilder(serviceProvider: serviceProvider); - var middleware = CreateMiddlewareFactory(exit => - { - builder.Use(ignored => exit); - return builder.Build(); - }, builder.ApplicationServices); + var middleware = CreateMiddlewareFactory(exit => + { + builder.Use(ignored => exit); + return builder.Build(); + }, builder.ApplicationServices); - app(middleware); - return builder; - } + app(middleware); + return builder; + } - private static CreateMiddleware CreateMiddlewareFactory(Func middleware, IServiceProvider services) + private static CreateMiddleware CreateMiddlewareFactory(Func middleware, IServiceProvider services) + { + return next => { - return next => + var app = middleware(httpContext => { - var app = middleware(httpContext => - { - return next(httpContext.Features.Get().Environment); - }); + return next(httpContext.Features.Get().Environment); + }); - return env => - { + return env => + { // Use the existing HttpContext if there is one. HttpContext context; - object obj; - if (env.TryGetValue(typeof(HttpContext).FullName, out obj)) - { - context = (HttpContext)obj; - context.Features.Set(new OwinEnvironmentFeature() { Environment = env }); - } - else - { - context = new DefaultHttpContext( - new FeatureCollection( - new OwinFeatureCollection(env))); - context.RequestServices = services; - } + object obj; + if (env.TryGetValue(typeof(HttpContext).FullName, out obj)) + { + context = (HttpContext)obj; + context.Features.Set(new OwinEnvironmentFeature() { Environment = env }); + } + else + { + context = new DefaultHttpContext( + new FeatureCollection( + new OwinFeatureCollection(env))); + context.RequestServices = services; + } - return app.Invoke(context); - }; + return app.Invoke(context); }; - } + }; + } - /// - /// Creates an for an OWIN pipeline. - /// - /// The OWIN pipeline. - /// A delegate used to configure a middleware pipeline. - /// An . - public static AddMiddleware UseBuilder(this AddMiddleware app, Action pipeline) + /// + /// Creates an for an OWIN pipeline. + /// + /// The OWIN pipeline. + /// A delegate used to configure a middleware pipeline. + /// An . + public static AddMiddleware UseBuilder(this AddMiddleware app, Action pipeline) + { + return app.UseBuilder(pipeline, serviceProvider: null); + } + + /// + /// Creates an for an OWIN pipeline. + /// + /// The OWIN pipeline. + /// A delegate used to configure a middleware pipeline. + /// A service provider for . + /// An . + public static AddMiddleware UseBuilder(this AddMiddleware app, Action pipeline, IServiceProvider serviceProvider) + { + if (app == null) { - return app.UseBuilder(pipeline, serviceProvider: null); + throw new ArgumentNullException(nameof(app)); } - - /// - /// Creates an for an OWIN pipeline. - /// - /// The OWIN pipeline. - /// A delegate used to configure a middleware pipeline. - /// A service provider for . - /// An . - public static AddMiddleware UseBuilder(this AddMiddleware app, Action pipeline, IServiceProvider serviceProvider) + if (pipeline == null) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - if (pipeline == null) - { - throw new ArgumentNullException(nameof(pipeline)); - } - - var builder = app.UseBuilder(serviceProvider); - pipeline(builder); - return app; + throw new ArgumentNullException(nameof(pipeline)); } - private class EmptyProvider : IServiceProvider + var builder = app.UseBuilder(serviceProvider); + pipeline(builder); + return app; + } + + private class EmptyProvider : IServiceProvider + { + public object GetService(Type serviceType) { - public object GetService(Type serviceType) - { - return null; - } + return null; } } } diff --git a/src/Http/Owin/src/OwinFeatureCollection.cs b/src/Http/Owin/src/OwinFeatureCollection.cs index 2bfba79232..ee1193b471 100644 --- a/src/Http/Owin/src/OwinFeatureCollection.cs +++ b/src/Http/Owin/src/OwinFeatureCollection.cs @@ -19,440 +19,439 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features.Authentication; -namespace Microsoft.AspNetCore.Owin +namespace Microsoft.AspNetCore.Owin; + +using SendFileFunc = Func; + +/// +/// OWIN feature collection. +/// +public class OwinFeatureCollection : + IFeatureCollection, + IHttpRequestFeature, + IHttpResponseFeature, + IHttpResponseBodyFeature, + IHttpConnectionFeature, + ITlsConnectionFeature, + IHttpRequestIdentifierFeature, + IHttpRequestLifetimeFeature, + IHttpAuthenticationFeature, + IHttpWebSocketFeature, + IOwinEnvironmentFeature { - using SendFileFunc = Func; - /// - /// OWIN feature collection. + /// Gets or sets OWIN environment values. /// - public class OwinFeatureCollection : - IFeatureCollection, - IHttpRequestFeature, - IHttpResponseFeature, - IHttpResponseBodyFeature, - IHttpConnectionFeature, - ITlsConnectionFeature, - IHttpRequestIdentifierFeature, - IHttpRequestLifetimeFeature, - IHttpAuthenticationFeature, - IHttpWebSocketFeature, - IOwinEnvironmentFeature - { - /// - /// Gets or sets OWIN environment values. - /// - public IDictionary Environment { get; set; } - private PipeWriter _responseBodyWrapper; - private bool _headersSent; - - /// - /// Initializes a new instance of . - /// - /// The environment values. - public OwinFeatureCollection(IDictionary environment) - { - Environment = environment; - SupportsWebSockets = true; + public IDictionary Environment { get; set; } + private PipeWriter _responseBodyWrapper; + private bool _headersSent; - var register = Prop, object>>(OwinConstants.CommonKeys.OnSendingHeaders); - register?.Invoke(state => - { - var collection = (OwinFeatureCollection)state; - collection._headersSent = true; - }, this); - } + /// + /// Initializes a new instance of . + /// + /// The environment values. + public OwinFeatureCollection(IDictionary environment) + { + Environment = environment; + SupportsWebSockets = true; - T Prop(string key) + var register = Prop, object>>(OwinConstants.CommonKeys.OnSendingHeaders); + register?.Invoke(state => { - object value; - if (Environment.TryGetValue(key, out value) && value is T) - { - return (T)value; - } - return default(T); - } + var collection = (OwinFeatureCollection)state; + collection._headersSent = true; + }, this); + } - void Prop(string key, object value) + T Prop(string key) + { + object value; + if (Environment.TryGetValue(key, out value) && value is T) { - Environment[key] = value; + return (T)value; } + return default(T); + } - string IHttpRequestFeature.Protocol - { - get { return Prop(OwinConstants.RequestProtocol); } - set { Prop(OwinConstants.RequestProtocol, value); } - } + void Prop(string key, object value) + { + Environment[key] = value; + } - string IHttpRequestFeature.Scheme - { - get { return Prop(OwinConstants.RequestScheme); } - set { Prop(OwinConstants.RequestScheme, value); } - } + string IHttpRequestFeature.Protocol + { + get { return Prop(OwinConstants.RequestProtocol); } + set { Prop(OwinConstants.RequestProtocol, value); } + } - string IHttpRequestFeature.Method - { - get { return Prop(OwinConstants.RequestMethod); } - set { Prop(OwinConstants.RequestMethod, value); } - } + string IHttpRequestFeature.Scheme + { + get { return Prop(OwinConstants.RequestScheme); } + set { Prop(OwinConstants.RequestScheme, value); } + } - string IHttpRequestFeature.PathBase - { - get { return Prop(OwinConstants.RequestPathBase); } - set { Prop(OwinConstants.RequestPathBase, value); } - } + string IHttpRequestFeature.Method + { + get { return Prop(OwinConstants.RequestMethod); } + set { Prop(OwinConstants.RequestMethod, value); } + } - string IHttpRequestFeature.Path - { - get { return Prop(OwinConstants.RequestPath); } - set { Prop(OwinConstants.RequestPath, value); } - } + string IHttpRequestFeature.PathBase + { + get { return Prop(OwinConstants.RequestPathBase); } + set { Prop(OwinConstants.RequestPathBase, value); } + } - string IHttpRequestFeature.QueryString - { - get { return Utilities.AddQuestionMark(Prop(OwinConstants.RequestQueryString)); } - set { Prop(OwinConstants.RequestQueryString, Utilities.RemoveQuestionMark(value)); } - } + string IHttpRequestFeature.Path + { + get { return Prop(OwinConstants.RequestPath); } + set { Prop(OwinConstants.RequestPath, value); } + } - string IHttpRequestFeature.RawTarget - { - get { return string.Empty; } - set { throw new NotSupportedException(); } - } + string IHttpRequestFeature.QueryString + { + get { return Utilities.AddQuestionMark(Prop(OwinConstants.RequestQueryString)); } + set { Prop(OwinConstants.RequestQueryString, Utilities.RemoveQuestionMark(value)); } + } - IHeaderDictionary IHttpRequestFeature.Headers - { - get { return Utilities.MakeHeaderDictionary(Prop>(OwinConstants.RequestHeaders)); } - set { Prop(OwinConstants.RequestHeaders, Utilities.MakeDictionaryStringArray(value)); } - } + string IHttpRequestFeature.RawTarget + { + get { return string.Empty; } + set { throw new NotSupportedException(); } + } - string IHttpRequestIdentifierFeature.TraceIdentifier - { - get { return Prop(OwinConstants.RequestId); } - set { Prop(OwinConstants.RequestId, value); } - } + IHeaderDictionary IHttpRequestFeature.Headers + { + get { return Utilities.MakeHeaderDictionary(Prop>(OwinConstants.RequestHeaders)); } + set { Prop(OwinConstants.RequestHeaders, Utilities.MakeDictionaryStringArray(value)); } + } - Stream IHttpRequestFeature.Body - { - get { return Prop(OwinConstants.RequestBody); } - set { Prop(OwinConstants.RequestBody, value); } - } + string IHttpRequestIdentifierFeature.TraceIdentifier + { + get { return Prop(OwinConstants.RequestId); } + set { Prop(OwinConstants.RequestId, value); } + } - int IHttpResponseFeature.StatusCode - { - get { return Prop(OwinConstants.ResponseStatusCode); } - set { Prop(OwinConstants.ResponseStatusCode, value); } - } + Stream IHttpRequestFeature.Body + { + get { return Prop(OwinConstants.RequestBody); } + set { Prop(OwinConstants.RequestBody, value); } + } - string IHttpResponseFeature.ReasonPhrase - { - get { return Prop(OwinConstants.ResponseReasonPhrase); } - set { Prop(OwinConstants.ResponseReasonPhrase, value); } - } + int IHttpResponseFeature.StatusCode + { + get { return Prop(OwinConstants.ResponseStatusCode); } + set { Prop(OwinConstants.ResponseStatusCode, value); } + } - IHeaderDictionary IHttpResponseFeature.Headers - { - get { return Utilities.MakeHeaderDictionary(Prop>(OwinConstants.ResponseHeaders)); } - set { Prop(OwinConstants.ResponseHeaders, Utilities.MakeDictionaryStringArray(value)); } - } + string IHttpResponseFeature.ReasonPhrase + { + get { return Prop(OwinConstants.ResponseReasonPhrase); } + set { Prop(OwinConstants.ResponseReasonPhrase, value); } + } - Stream IHttpResponseFeature.Body - { - get { return Prop(OwinConstants.ResponseBody); } - set { Prop(OwinConstants.ResponseBody, value); } - } + IHeaderDictionary IHttpResponseFeature.Headers + { + get { return Utilities.MakeHeaderDictionary(Prop>(OwinConstants.ResponseHeaders)); } + set { Prop(OwinConstants.ResponseHeaders, Utilities.MakeDictionaryStringArray(value)); } + } - Stream IHttpResponseBodyFeature.Stream - { - get { return Prop(OwinConstants.ResponseBody); } - } + Stream IHttpResponseFeature.Body + { + get { return Prop(OwinConstants.ResponseBody); } + set { Prop(OwinConstants.ResponseBody, value); } + } + + Stream IHttpResponseBodyFeature.Stream + { + get { return Prop(OwinConstants.ResponseBody); } + } - PipeWriter IHttpResponseBodyFeature.Writer + PipeWriter IHttpResponseBodyFeature.Writer + { + get { - get + if (_responseBodyWrapper == null) { - if (_responseBodyWrapper == null) - { - _responseBodyWrapper = PipeWriter.Create(Prop(OwinConstants.ResponseBody), new StreamPipeWriterOptions(leaveOpen: true)); - } - - return _responseBodyWrapper; + _responseBodyWrapper = PipeWriter.Create(Prop(OwinConstants.ResponseBody), new StreamPipeWriterOptions(leaveOpen: true)); } + + return _responseBodyWrapper; } + } + + bool IHttpResponseFeature.HasStarted + { + get { return _headersSent; } + } - bool IHttpResponseFeature.HasStarted + void IHttpResponseFeature.OnStarting(Func callback, object state) + { + var register = Prop, object>>(OwinConstants.CommonKeys.OnSendingHeaders); + if (register == null) { - get { return _headersSent; } + throw new NotSupportedException(OwinConstants.CommonKeys.OnSendingHeaders); } - void IHttpResponseFeature.OnStarting(Func callback, object state) - { - var register = Prop, object>>(OwinConstants.CommonKeys.OnSendingHeaders); - if (register == null) - { - throw new NotSupportedException(OwinConstants.CommonKeys.OnSendingHeaders); - } + // Need to block on the callback since we can't change the OWIN signature to be async + register(s => callback(s).GetAwaiter().GetResult(), state); + } - // Need to block on the callback since we can't change the OWIN signature to be async - register(s => callback(s).GetAwaiter().GetResult(), state); - } + void IHttpResponseFeature.OnCompleted(Func callback, object state) + { + throw new NotSupportedException(); + } - void IHttpResponseFeature.OnCompleted(Func callback, object state) - { - throw new NotSupportedException(); - } + IPAddress IHttpConnectionFeature.RemoteIpAddress + { + get { return IPAddress.Parse(Prop(OwinConstants.CommonKeys.RemoteIpAddress)); } + set { Prop(OwinConstants.CommonKeys.RemoteIpAddress, value.ToString()); } + } - IPAddress IHttpConnectionFeature.RemoteIpAddress - { - get { return IPAddress.Parse(Prop(OwinConstants.CommonKeys.RemoteIpAddress)); } - set { Prop(OwinConstants.CommonKeys.RemoteIpAddress, value.ToString()); } - } + IPAddress IHttpConnectionFeature.LocalIpAddress + { + get { return IPAddress.Parse(Prop(OwinConstants.CommonKeys.LocalIpAddress)); } + set { Prop(OwinConstants.CommonKeys.LocalIpAddress, value.ToString()); } + } - IPAddress IHttpConnectionFeature.LocalIpAddress - { - get { return IPAddress.Parse(Prop(OwinConstants.CommonKeys.LocalIpAddress)); } - set { Prop(OwinConstants.CommonKeys.LocalIpAddress, value.ToString()); } - } + int IHttpConnectionFeature.RemotePort + { + get { return int.Parse(Prop(OwinConstants.CommonKeys.RemotePort), CultureInfo.InvariantCulture); } + set { Prop(OwinConstants.CommonKeys.RemotePort, value.ToString(CultureInfo.InvariantCulture)); } + } - int IHttpConnectionFeature.RemotePort - { - get { return int.Parse(Prop(OwinConstants.CommonKeys.RemotePort), CultureInfo.InvariantCulture); } - set { Prop(OwinConstants.CommonKeys.RemotePort, value.ToString(CultureInfo.InvariantCulture)); } - } + int IHttpConnectionFeature.LocalPort + { + get { return int.Parse(Prop(OwinConstants.CommonKeys.LocalPort), CultureInfo.InvariantCulture); } + set { Prop(OwinConstants.CommonKeys.LocalPort, value.ToString(CultureInfo.InvariantCulture)); } + } - int IHttpConnectionFeature.LocalPort - { - get { return int.Parse(Prop(OwinConstants.CommonKeys.LocalPort), CultureInfo.InvariantCulture); } - set { Prop(OwinConstants.CommonKeys.LocalPort, value.ToString(CultureInfo.InvariantCulture)); } - } + string IHttpConnectionFeature.ConnectionId + { + get { return Prop(OwinConstants.CommonKeys.ConnectionId); } + set { Prop(OwinConstants.CommonKeys.ConnectionId, value); } + } - string IHttpConnectionFeature.ConnectionId + Task IHttpResponseBodyFeature.SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) + { + object obj; + if (Environment.TryGetValue(OwinConstants.SendFiles.SendAsync, out obj)) { - get { return Prop(OwinConstants.CommonKeys.ConnectionId); } - set { Prop(OwinConstants.CommonKeys.ConnectionId, value); } + var func = (SendFileFunc)obj; + return func(path, offset, length, cancellation); } + throw new NotSupportedException(OwinConstants.SendFiles.SendAsync); + } - Task IHttpResponseBodyFeature.SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) + private bool SupportsClientCerts + { + get { object obj; - if (Environment.TryGetValue(OwinConstants.SendFiles.SendAsync, out obj)) + if (string.Equals("https", ((IHttpRequestFeature)this).Scheme, StringComparison.OrdinalIgnoreCase) + && (Environment.TryGetValue(OwinConstants.CommonKeys.LoadClientCertAsync, out obj) + || Environment.TryGetValue(OwinConstants.CommonKeys.ClientCertificate, out obj)) + && obj != null) { - var func = (SendFileFunc)obj; - return func(path, offset, length, cancellation); + return true; } - throw new NotSupportedException(OwinConstants.SendFiles.SendAsync); + return false; } + } - private bool SupportsClientCerts - { - get - { - object obj; - if (string.Equals("https", ((IHttpRequestFeature)this).Scheme, StringComparison.OrdinalIgnoreCase) - && (Environment.TryGetValue(OwinConstants.CommonKeys.LoadClientCertAsync, out obj) - || Environment.TryGetValue(OwinConstants.CommonKeys.ClientCertificate, out obj)) - && obj != null) - { - return true; - } - return false; - } - } + X509Certificate2 ITlsConnectionFeature.ClientCertificate + { + get { return Prop(OwinConstants.CommonKeys.ClientCertificate); } + set { Prop(OwinConstants.CommonKeys.ClientCertificate, value); } + } - X509Certificate2 ITlsConnectionFeature.ClientCertificate + async Task ITlsConnectionFeature.GetClientCertificateAsync(CancellationToken cancellationToken) + { + var loadAsync = Prop>(OwinConstants.CommonKeys.LoadClientCertAsync); + if (loadAsync != null) { - get { return Prop(OwinConstants.CommonKeys.ClientCertificate); } - set { Prop(OwinConstants.CommonKeys.ClientCertificate, value); } + await loadAsync(); } + return Prop(OwinConstants.CommonKeys.ClientCertificate); + } - async Task ITlsConnectionFeature.GetClientCertificateAsync(CancellationToken cancellationToken) - { - var loadAsync = Prop>(OwinConstants.CommonKeys.LoadClientCertAsync); - if (loadAsync != null) - { - await loadAsync(); - } - return Prop(OwinConstants.CommonKeys.ClientCertificate); - } + CancellationToken IHttpRequestLifetimeFeature.RequestAborted + { + get { return Prop(OwinConstants.CallCancelled); } + set { Prop(OwinConstants.CallCancelled, value); } + } - CancellationToken IHttpRequestLifetimeFeature.RequestAborted - { - get { return Prop(OwinConstants.CallCancelled); } - set { Prop(OwinConstants.CallCancelled, value); } - } + void IHttpRequestLifetimeFeature.Abort() + { + throw new NotImplementedException(); + } - void IHttpRequestLifetimeFeature.Abort() + ClaimsPrincipal IHttpAuthenticationFeature.User + { + get { - throw new NotImplementedException(); + return Prop(OwinConstants.RequestUser) + ?? Utilities.MakeClaimsPrincipal(Prop(OwinConstants.Security.User)); } - - ClaimsPrincipal IHttpAuthenticationFeature.User + set { - get - { - return Prop(OwinConstants.RequestUser) - ?? Utilities.MakeClaimsPrincipal(Prop(OwinConstants.Security.User)); - } - set - { - Prop(OwinConstants.RequestUser, value); - Prop(OwinConstants.Security.User, value); - } + Prop(OwinConstants.RequestUser, value); + Prop(OwinConstants.Security.User, value); } + } - /// - /// Gets or sets if the underlying server supports WebSockets. This is enabled by default. - /// The value should be consistent across requests. - /// - public bool SupportsWebSockets { get; set; } + /// + /// Gets or sets if the underlying server supports WebSockets. This is enabled by default. + /// The value should be consistent across requests. + /// + public bool SupportsWebSockets { get; set; } - bool IHttpWebSocketFeature.IsWebSocketRequest + bool IHttpWebSocketFeature.IsWebSocketRequest + { + get { - get - { - object obj; - return Environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out obj); - } + object obj; + return Environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out obj); } + } - Task IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context) + Task IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context) + { + object obj; + if (!Environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out obj)) { - object obj; - if (!Environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out obj)) - { - throw new NotSupportedException("WebSockets are not supported"); // TODO: LOC - } - var accept = (Func>)obj; - return accept(context); + throw new NotSupportedException("WebSockets are not supported"); // TODO: LOC } + var accept = (Func>)obj; + return accept(context); + } - // IFeatureCollection + // IFeatureCollection - /// - public int Revision - { - get { return 0; } // Not modifiable - } + /// + public int Revision + { + get { return 0; } // Not modifiable + } - /// - public bool IsReadOnly - { - get { return true; } - } + /// + public bool IsReadOnly + { + get { return true; } + } - /// - public object this[Type key] - { - get { return Get(key); } - set { throw new NotSupportedException(); } - } + /// + public object this[Type key] + { + get { return Get(key); } + set { throw new NotSupportedException(); } + } - private bool SupportsInterface(Type key) + private bool SupportsInterface(Type key) + { + // Does this type implement the requested interface? + if (key.IsAssignableFrom(GetType())) { - // Does this type implement the requested interface? - if (key.IsAssignableFrom(GetType())) + // Check for conditional features + if (key == typeof(ITlsConnectionFeature)) { - // Check for conditional features - if (key == typeof(ITlsConnectionFeature)) - { - return SupportsClientCerts; - } - else if (key == typeof(IHttpWebSocketFeature)) - { - return SupportsWebSockets; - } - - // The rest of the features are always supported. - return true; + return SupportsClientCerts; } - return false; - } - - /// - public object Get(Type key) - { - if (SupportsInterface(key)) + else if (key == typeof(IHttpWebSocketFeature)) { - return this; + return SupportsWebSockets; } - return null; - } - /// - public void Set(Type key, object value) - { - throw new NotSupportedException(); + // The rest of the features are always supported. + return true; } + return false; + } - /// - public TFeature Get() + /// + public object Get(Type key) + { + if (SupportsInterface(key)) { - return (TFeature)this[typeof(TFeature)]; + return this; } + return null; + } - /// - public void Set(TFeature instance) - { - this[typeof(TFeature)] = instance; - } + /// + public void Set(Type key, object value) + { + throw new NotSupportedException(); + } - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + /// + public TFeature Get() + { + return (TFeature)this[typeof(TFeature)]; + } - /// - public IEnumerator> GetEnumerator() - { - yield return new KeyValuePair(typeof(IHttpRequestFeature), this); - yield return new KeyValuePair(typeof(IHttpResponseFeature), this); - yield return new KeyValuePair(typeof(IHttpResponseBodyFeature), this); - yield return new KeyValuePair(typeof(IHttpConnectionFeature), this); - yield return new KeyValuePair(typeof(IHttpRequestIdentifierFeature), this); - yield return new KeyValuePair(typeof(IHttpRequestLifetimeFeature), this); - yield return new KeyValuePair(typeof(IHttpAuthenticationFeature), this); - yield return new KeyValuePair(typeof(IOwinEnvironmentFeature), this); + /// + public void Set(TFeature instance) + { + this[typeof(TFeature)] = instance; + } - // Check for conditional features - if (SupportsClientCerts) - { - yield return new KeyValuePair(typeof(ITlsConnectionFeature), this); - } - if (SupportsWebSockets) - { - yield return new KeyValuePair(typeof(IHttpWebSocketFeature), this); - } - } + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + public IEnumerator> GetEnumerator() + { + yield return new KeyValuePair(typeof(IHttpRequestFeature), this); + yield return new KeyValuePair(typeof(IHttpResponseFeature), this); + yield return new KeyValuePair(typeof(IHttpResponseBodyFeature), this); + yield return new KeyValuePair(typeof(IHttpConnectionFeature), this); + yield return new KeyValuePair(typeof(IHttpRequestIdentifierFeature), this); + yield return new KeyValuePair(typeof(IHttpRequestLifetimeFeature), this); + yield return new KeyValuePair(typeof(IHttpAuthenticationFeature), this); + yield return new KeyValuePair(typeof(IOwinEnvironmentFeature), this); - void IHttpResponseBodyFeature.DisableBuffering() + // Check for conditional features + if (SupportsClientCerts) { + yield return new KeyValuePair(typeof(ITlsConnectionFeature), this); } - - async Task IHttpResponseBodyFeature.StartAsync(CancellationToken cancellationToken) + if (SupportsWebSockets) { - if (_responseBodyWrapper != null) - { - await _responseBodyWrapper.FlushAsync(cancellationToken); - } - - // The pipe may or may not have flushed the stream. Make sure the stream gets flushed to trigger response start. - await Prop(OwinConstants.ResponseBody).FlushAsync(cancellationToken); + yield return new KeyValuePair(typeof(IHttpWebSocketFeature), this); } + } - Task IHttpResponseBodyFeature.CompleteAsync() - { - if (_responseBodyWrapper != null) - { - return _responseBodyWrapper.FlushAsync().AsTask(); - } + void IHttpResponseBodyFeature.DisableBuffering() + { + } - return Task.CompletedTask; + async Task IHttpResponseBodyFeature.StartAsync(CancellationToken cancellationToken) + { + if (_responseBodyWrapper != null) + { + await _responseBodyWrapper.FlushAsync(cancellationToken); } - /// - public void Dispose() + // The pipe may or may not have flushed the stream. Make sure the stream gets flushed to trigger response start. + await Prop(OwinConstants.ResponseBody).FlushAsync(cancellationToken); + } + + Task IHttpResponseBodyFeature.CompleteAsync() + { + if (_responseBodyWrapper != null) { + return _responseBodyWrapper.FlushAsync().AsTask(); } + + return Task.CompletedTask; + } + + /// + public void Dispose() + { } } diff --git a/src/Http/Owin/src/Utilities.cs b/src/Http/Owin/src/Utilities.cs index 6a1b268faf..ffb4862e2c 100644 --- a/src/Http/Owin/src/Utilities.cs +++ b/src/Http/Owin/src/Utilities.cs @@ -8,62 +8,61 @@ using System.Security.Principal; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Owin +namespace Microsoft.AspNetCore.Owin; + +internal static class Utilities { - internal static class Utilities + internal static string RemoveQuestionMark(string queryString) { - internal static string RemoveQuestionMark(string queryString) + if (!string.IsNullOrEmpty(queryString)) { - if (!string.IsNullOrEmpty(queryString)) + if (queryString[0] == '?') { - if (queryString[0] == '?') - { - return queryString.Substring(1); - } + return queryString.Substring(1); } - return queryString; } + return queryString; + } - internal static string AddQuestionMark(string queryString) + internal static string AddQuestionMark(string queryString) + { + if (!string.IsNullOrEmpty(queryString)) { - if (!string.IsNullOrEmpty(queryString)) - { - return '?' + queryString; - } - return queryString; + return '?' + queryString; } + return queryString; + } - internal static ClaimsPrincipal MakeClaimsPrincipal(IPrincipal principal) + internal static ClaimsPrincipal MakeClaimsPrincipal(IPrincipal principal) + { + if (principal == null) { - if (principal == null) - { - return null; - } - if (principal is ClaimsPrincipal) - { - return principal as ClaimsPrincipal; - } - return new ClaimsPrincipal(principal); + return null; } + if (principal is ClaimsPrincipal) + { + return principal as ClaimsPrincipal; + } + return new ClaimsPrincipal(principal); + } - internal static IHeaderDictionary MakeHeaderDictionary(IDictionary dictionary) + internal static IHeaderDictionary MakeHeaderDictionary(IDictionary dictionary) + { + var wrapper = dictionary as DictionaryStringArrayWrapper; + if (wrapper != null) { - var wrapper = dictionary as DictionaryStringArrayWrapper; - if (wrapper != null) - { - return wrapper.Inner; - } - return new DictionaryStringValuesWrapper(dictionary); + return wrapper.Inner; } + return new DictionaryStringValuesWrapper(dictionary); + } - internal static IDictionary MakeDictionaryStringArray(IHeaderDictionary dictionary) + internal static IDictionary MakeDictionaryStringArray(IHeaderDictionary dictionary) + { + var wrapper = dictionary as DictionaryStringValuesWrapper; + if (wrapper != null) { - var wrapper = dictionary as DictionaryStringValuesWrapper; - if (wrapper != null) - { - return wrapper.Inner; - } - return new DictionaryStringArrayWrapper(dictionary); + return wrapper.Inner; } + return new DictionaryStringArrayWrapper(dictionary); } } diff --git a/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptAdapter.cs b/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptAdapter.cs index 16a1916ecb..03311884d4 100644 --- a/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptAdapter.cs +++ b/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptAdapter.cs @@ -8,143 +8,142 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Owin -{ - using AppFunc = Func, Task>; - using WebSocketAccept = - Action - < - IDictionary, // WebSocket Accept parameters - Func // WebSocketFunc callback - < - IDictionary, // WebSocket environment - Task // Complete - > - >; - using WebSocketAcceptAlt = - Func +namespace Microsoft.AspNetCore.Owin; + +using AppFunc = Func, Task>; +using WebSocketAccept = + Action + < + IDictionary, // WebSocket Accept parameters + Func // WebSocketFunc callback < - WebSocketAcceptContext, // WebSocket Accept parameters - Task - >; + IDictionary, // WebSocket environment + Task // Complete + > + >; +using WebSocketAcceptAlt = + Func + < + WebSocketAcceptContext, // WebSocket Accept parameters + Task + >; - /// - /// This adapts the OWIN WebSocket accept flow to match the ASP.NET Core WebSocket Accept flow. - /// This enables ASP.NET Core components to use WebSockets on OWIN based servers. - /// - public class OwinWebSocketAcceptAdapter +/// +/// This adapts the OWIN WebSocket accept flow to match the ASP.NET Core WebSocket Accept flow. +/// This enables ASP.NET Core components to use WebSockets on OWIN based servers. +/// +public class OwinWebSocketAcceptAdapter +{ + private readonly WebSocketAccept _owinWebSocketAccept; + private readonly TaskCompletionSource _requestTcs = new TaskCompletionSource(); + private readonly TaskCompletionSource _acceptTcs = new TaskCompletionSource(); + private readonly TaskCompletionSource _upstreamWentAsync = new TaskCompletionSource(); + private string _subProtocol; + + private OwinWebSocketAcceptAdapter(WebSocketAccept owinWebSocketAccept) { - private readonly WebSocketAccept _owinWebSocketAccept; - private readonly TaskCompletionSource _requestTcs = new TaskCompletionSource(); - private readonly TaskCompletionSource _acceptTcs = new TaskCompletionSource(); - private readonly TaskCompletionSource _upstreamWentAsync = new TaskCompletionSource(); - private string _subProtocol; + _owinWebSocketAccept = owinWebSocketAccept; + } + + private Task RequestTask { get { return _requestTcs.Task; } } + private Task UpstreamTask { get; set; } + private TaskCompletionSource UpstreamWentAsyncTcs { get { return _upstreamWentAsync; } } - private OwinWebSocketAcceptAdapter(WebSocketAccept owinWebSocketAccept) + private async Task AcceptWebSocketAsync(WebSocketAcceptContext context) + { + IDictionary options = null; + if (context is OwinWebSocketAcceptContext) { - _owinWebSocketAccept = owinWebSocketAccept; + var acceptContext = context as OwinWebSocketAcceptContext; + options = acceptContext.Options; + _subProtocol = acceptContext.SubProtocol; } - - private Task RequestTask { get { return _requestTcs.Task; } } - private Task UpstreamTask { get; set; } - private TaskCompletionSource UpstreamWentAsyncTcs { get { return _upstreamWentAsync; } } - - private async Task AcceptWebSocketAsync(WebSocketAcceptContext context) + else if (context?.SubProtocol != null) { - IDictionary options = null; - if (context is OwinWebSocketAcceptContext) - { - var acceptContext = context as OwinWebSocketAcceptContext; - options = acceptContext.Options; - _subProtocol = acceptContext.SubProtocol; - } - else if (context?.SubProtocol != null) - { - options = new Dictionary(1) + options = new Dictionary(1) { { OwinConstants.WebSocket.SubProtocol, context.SubProtocol } }; - _subProtocol = context.SubProtocol; - } + _subProtocol = context.SubProtocol; + } - // Accept may have been called synchronously on the original request thread, we might not have a task yet. Go async. - await _upstreamWentAsync.Task; + // Accept may have been called synchronously on the original request thread, we might not have a task yet. Go async. + await _upstreamWentAsync.Task; - _owinWebSocketAccept(options, OwinAcceptCallback); - _requestTcs.TrySetResult(0); // Let the pipeline unwind. + _owinWebSocketAccept(options, OwinAcceptCallback); + _requestTcs.TrySetResult(0); // Let the pipeline unwind. - return await _acceptTcs.Task; - } + return await _acceptTcs.Task; + } - private Task OwinAcceptCallback(IDictionary webSocketContext) + private Task OwinAcceptCallback(IDictionary webSocketContext) + { + _acceptTcs.TrySetResult(new OwinWebSocketAdapter(webSocketContext, _subProtocol)); + return UpstreamTask; + } + + // Make sure declined websocket requests complete. This is a no-op for accepted websocket requests. + private void EnsureCompleted(Task task) + { + if (task.IsCanceled) { - _acceptTcs.TrySetResult(new OwinWebSocketAdapter(webSocketContext, _subProtocol)); - return UpstreamTask; + _requestTcs.TrySetCanceled(); } - - // Make sure declined websocket requests complete. This is a no-op for accepted websocket requests. - private void EnsureCompleted(Task task) + else if (task.IsFaulted) { - if (task.IsCanceled) - { - _requestTcs.TrySetCanceled(); - } - else if (task.IsFaulted) - { - _requestTcs.TrySetException(task.Exception); - } - else - { - _requestTcs.TrySetResult(0); - } + _requestTcs.TrySetException(task.Exception); } + else + { + _requestTcs.TrySetResult(0); + } + } - // Order of operations: - // 1. A WebSocket handshake request is received by the middleware. - // 2. The middleware inserts an alternate Accept signature into the OWIN environment. - // 3. The middleware invokes Next and stores Next's Task locally. It then returns an alternate Task to the server. - // 4. The OwinFeatureCollection adapts the alternate Accept signature to IHttpWebSocketFeature.AcceptAsync. - // 5. A component later in the pipeline invokes IHttpWebSocketFeature.AcceptAsync (mapped to AcceptWebSocketAsync). - // 6. The middleware calls the OWIN Accept, providing a local callback, and returns an incomplete Task. - // 7. The middleware completes the alternate Task it returned from Invoke, telling the server that the request pipeline has completed. - // 8. The server invokes the middleware's callback, which creates a WebSocket adapter and completes the original Accept Task with it. - // 9. The middleware waits while the application uses the WebSocket, where the end is signaled by the Next's Task completion. - // - /// - /// Adapt web sockets to OWIN. - /// - /// The next OWIN app delegate. - /// An OWIN app delegate. - public static AppFunc AdaptWebSockets(AppFunc next) + // Order of operations: + // 1. A WebSocket handshake request is received by the middleware. + // 2. The middleware inserts an alternate Accept signature into the OWIN environment. + // 3. The middleware invokes Next and stores Next's Task locally. It then returns an alternate Task to the server. + // 4. The OwinFeatureCollection adapts the alternate Accept signature to IHttpWebSocketFeature.AcceptAsync. + // 5. A component later in the pipeline invokes IHttpWebSocketFeature.AcceptAsync (mapped to AcceptWebSocketAsync). + // 6. The middleware calls the OWIN Accept, providing a local callback, and returns an incomplete Task. + // 7. The middleware completes the alternate Task it returned from Invoke, telling the server that the request pipeline has completed. + // 8. The server invokes the middleware's callback, which creates a WebSocket adapter and completes the original Accept Task with it. + // 9. The middleware waits while the application uses the WebSocket, where the end is signaled by the Next's Task completion. + // + /// + /// Adapt web sockets to OWIN. + /// + /// The next OWIN app delegate. + /// An OWIN app delegate. + public static AppFunc AdaptWebSockets(AppFunc next) + { + return environment => { - return environment => + object accept; + if (environment.TryGetValue(OwinConstants.WebSocket.Accept, out accept) && accept is WebSocketAccept) { - object accept; - if (environment.TryGetValue(OwinConstants.WebSocket.Accept, out accept) && accept is WebSocketAccept) + var adapter = new OwinWebSocketAcceptAdapter((WebSocketAccept)accept); + + environment[OwinConstants.WebSocket.AcceptAlt] = new WebSocketAcceptAlt(adapter.AcceptWebSocketAsync); + + try { - var adapter = new OwinWebSocketAcceptAdapter((WebSocketAccept)accept); - - environment[OwinConstants.WebSocket.AcceptAlt] = new WebSocketAcceptAlt(adapter.AcceptWebSocketAsync); - - try - { - adapter.UpstreamTask = next(environment); - adapter.UpstreamWentAsyncTcs.TrySetResult(0); - adapter.UpstreamTask.ContinueWith(adapter.EnsureCompleted, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - } - catch (Exception ex) - { - adapter.UpstreamWentAsyncTcs.TrySetException(ex); - throw; - } - - return adapter.RequestTask; + adapter.UpstreamTask = next(environment); + adapter.UpstreamWentAsyncTcs.TrySetResult(0); + adapter.UpstreamTask.ContinueWith(adapter.EnsureCompleted, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); } - else + catch (Exception ex) { - return next(environment); + adapter.UpstreamWentAsyncTcs.TrySetException(ex); + throw; } - }; - } + + return adapter.RequestTask; + } + else + { + return next(environment); + } + }; } } diff --git a/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptContext.cs b/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptContext.cs index c255a4cfc9..9ccf5ef8f9 100644 --- a/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptContext.cs +++ b/src/Http/Owin/src/WebSockets/OwinWebSocketAcceptContext.cs @@ -4,59 +4,58 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Owin +namespace Microsoft.AspNetCore.Owin; + +/// +/// OWIN WebSocket accept context. +/// +public class OwinWebSocketAcceptContext : WebSocketAcceptContext { + private IDictionary _options; + /// - /// OWIN WebSocket accept context. + /// Initializes a new instance of . /// - public class OwinWebSocketAcceptContext : WebSocketAcceptContext + public OwinWebSocketAcceptContext() : this(new Dictionary(1)) { - private IDictionary _options; - - /// - /// Initializes a new instance of . - /// - public OwinWebSocketAcceptContext() : this(new Dictionary(1)) - { - } + } - /// - /// Initializes a new instance of . - /// - /// OWIN WebSocket options. - public OwinWebSocketAcceptContext(IDictionary options) - { - _options = options; - } + /// + /// Initializes a new instance of . + /// + /// OWIN WebSocket options. + public OwinWebSocketAcceptContext(IDictionary options) + { + _options = options; + } - /// - public override string SubProtocol + /// + public override string SubProtocol + { + get { - get + object obj; + if (_options != null && _options.TryGetValue(OwinConstants.WebSocket.SubProtocol, out obj)) { - object obj; - if (_options != null && _options.TryGetValue(OwinConstants.WebSocket.SubProtocol, out obj)) - { - return (string)obj; - } - return null; + return (string)obj; } - set + return null; + } + set + { + if (_options == null) { - if (_options == null) - { - _options = new Dictionary(1); - } - _options[OwinConstants.WebSocket.SubProtocol] = value; + _options = new Dictionary(1); } + _options[OwinConstants.WebSocket.SubProtocol] = value; } + } - /// - /// Gets OWIN WebSocket options. - /// - public IDictionary Options - { - get { return _options; } - } + /// + /// Gets OWIN WebSocket options. + /// + public IDictionary Options + { + get { return _options; } } } diff --git a/src/Http/Owin/src/WebSockets/OwinWebSocketAdapter.cs b/src/Http/Owin/src/WebSockets/OwinWebSocketAdapter.cs index 0adb7cdce0..ace67ba590 100644 --- a/src/Http/Owin/src/WebSockets/OwinWebSocketAdapter.cs +++ b/src/Http/Owin/src/WebSockets/OwinWebSocketAdapter.cs @@ -4,215 +4,214 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Net.WebSockets; using System.Threading; using System.Threading.Tasks; -using System.Net.WebSockets; -namespace Microsoft.AspNetCore.Owin -{ - // http://owin.org/extensions/owin-WebSocket-Extension-v0.4.0.htm - using WebSocketCloseAsync = - Func; - using WebSocketReceiveAsync = - Func /* data */, - CancellationToken /* cancel */, - Task>>; - using WebSocketSendAsync = - Func /* data */, - int /* messageType */, +namespace Microsoft.AspNetCore.Owin; + +using RawWebSocketReceiveResult = Tuple; // count +// http://owin.org/extensions/owin-WebSocket-Extension-v0.4.0.htm +using WebSocketCloseAsync = + Func; +using WebSocketReceiveAsync = + Func /* data */, + CancellationToken /* cancel */, + Task; - using RawWebSocketReceiveResult = Tuple; // count + int /* count */>>>; +using WebSocketSendAsync = + Func /* data */, + int /* messageType */, + bool /* endOfMessage */, + CancellationToken /* cancel */, + Task>; + +/// +/// OWIN WebSocket adapter. +/// +public class OwinWebSocketAdapter : WebSocket +{ + private const int _rentedBufferSize = 1024; + private readonly IDictionary _websocketContext; + private readonly WebSocketSendAsync _sendAsync; + private readonly WebSocketReceiveAsync _receiveAsync; + private readonly WebSocketCloseAsync _closeAsync; + private WebSocketState _state; + private readonly string _subProtocol; /// - /// OWIN WebSocket adapter. + /// Initializes a new instance of . /// - public class OwinWebSocketAdapter : WebSocket + /// WebSocket context options. + /// The WebSocket subprotocol. + public OwinWebSocketAdapter(IDictionary websocketContext, string subProtocol) { - private const int _rentedBufferSize = 1024; - private readonly IDictionary _websocketContext; - private readonly WebSocketSendAsync _sendAsync; - private readonly WebSocketReceiveAsync _receiveAsync; - private readonly WebSocketCloseAsync _closeAsync; - private WebSocketState _state; - private readonly string _subProtocol; - - /// - /// Initializes a new instance of . - /// - /// WebSocket context options. - /// The WebSocket subprotocol. - public OwinWebSocketAdapter(IDictionary websocketContext, string subProtocol) - { - _websocketContext = websocketContext; - _sendAsync = (WebSocketSendAsync)websocketContext[OwinConstants.WebSocket.SendAsync]; - _receiveAsync = (WebSocketReceiveAsync)websocketContext[OwinConstants.WebSocket.ReceiveAsync]; - _closeAsync = (WebSocketCloseAsync)websocketContext[OwinConstants.WebSocket.CloseAsync]; - _state = WebSocketState.Open; - _subProtocol = subProtocol; - } + _websocketContext = websocketContext; + _sendAsync = (WebSocketSendAsync)websocketContext[OwinConstants.WebSocket.SendAsync]; + _receiveAsync = (WebSocketReceiveAsync)websocketContext[OwinConstants.WebSocket.ReceiveAsync]; + _closeAsync = (WebSocketCloseAsync)websocketContext[OwinConstants.WebSocket.CloseAsync]; + _state = WebSocketState.Open; + _subProtocol = subProtocol; + } - /// - public override WebSocketCloseStatus? CloseStatus + /// + public override WebSocketCloseStatus? CloseStatus + { + get { - get + object obj; + if (_websocketContext.TryGetValue(OwinConstants.WebSocket.ClientCloseStatus, out obj)) { - object obj; - if (_websocketContext.TryGetValue(OwinConstants.WebSocket.ClientCloseStatus, out obj)) - { - return (WebSocketCloseStatus)obj; - } - return null; + return (WebSocketCloseStatus)obj; } + return null; } + } - /// - public override string CloseStatusDescription + /// + public override string CloseStatusDescription + { + get { - get + object obj; + if (_websocketContext.TryGetValue(OwinConstants.WebSocket.ClientCloseDescription, out obj)) { - object obj; - if (_websocketContext.TryGetValue(OwinConstants.WebSocket.ClientCloseDescription, out obj)) - { - return (string)obj; - } - return null; + return (string)obj; } + return null; } + } - /// - public override string SubProtocol + /// + public override string SubProtocol + { + get { - get - { - return _subProtocol; - } + return _subProtocol; } + } - /// - public override WebSocketState State + /// + public override WebSocketState State + { + get { - get - { - return _state; - } + return _state; } + } - /// - public override async Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) + /// + public override async Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) + { + var rawResult = await _receiveAsync(buffer, cancellationToken); + var messageType = OpCodeToEnum(rawResult.Item1); + if (messageType == WebSocketMessageType.Close) { - var rawResult = await _receiveAsync(buffer, cancellationToken); - var messageType = OpCodeToEnum(rawResult.Item1); - if (messageType == WebSocketMessageType.Close) + if (State == WebSocketState.Open) { - if (State == WebSocketState.Open) - { - _state = WebSocketState.CloseReceived; - } - else if (State == WebSocketState.CloseSent) - { - _state = WebSocketState.Closed; - } - return new WebSocketReceiveResult(rawResult.Item3, messageType, rawResult.Item2, CloseStatus, CloseStatusDescription); + _state = WebSocketState.CloseReceived; } - else + else if (State == WebSocketState.CloseSent) { - return new WebSocketReceiveResult(rawResult.Item3, messageType, rawResult.Item2); + _state = WebSocketState.Closed; } + return new WebSocketReceiveResult(rawResult.Item3, messageType, rawResult.Item2, CloseStatus, CloseStatusDescription); } - - /// - public override Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) + else { - return _sendAsync(buffer, EnumToOpCode(messageType), endOfMessage, cancellationToken); + return new WebSocketReceiveResult(rawResult.Item3, messageType, rawResult.Item2); } + } - /// - public override async Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) - { - if (State == WebSocketState.Open || State == WebSocketState.CloseReceived) - { - await CloseOutputAsync(closeStatus, statusDescription, cancellationToken); - } + /// + public override Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) + { + return _sendAsync(buffer, EnumToOpCode(messageType), endOfMessage, cancellationToken); + } - var buffer = ArrayPool.Shared.Rent(_rentedBufferSize); - try - { - while (State == WebSocketState.CloseSent) - { - // Drain until close received - await ReceiveAsync(new ArraySegment(buffer), cancellationToken); - } - } - finally - { - ArrayPool.Shared.Return(buffer); - } + /// + public override async Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + if (State == WebSocketState.Open || State == WebSocketState.CloseReceived) + { + await CloseOutputAsync(closeStatus, statusDescription, cancellationToken); } - /// - public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + var buffer = ArrayPool.Shared.Rent(_rentedBufferSize); + try { - // TODO: Validate state - if (State == WebSocketState.Open) + while (State == WebSocketState.CloseSent) { - _state = WebSocketState.CloseSent; + // Drain until close received + await ReceiveAsync(new ArraySegment(buffer), cancellationToken); } - else if (State == WebSocketState.CloseReceived) - { - _state = WebSocketState.Closed; - } - return _closeAsync((int)closeStatus, statusDescription, cancellationToken); } - - /// - public override void Abort() + finally { - _state = WebSocketState.Aborted; + ArrayPool.Shared.Return(buffer); } + } - /// - public override void Dispose() + /// + public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + // TODO: Validate state + if (State == WebSocketState.Open) + { + _state = WebSocketState.CloseSent; + } + else if (State == WebSocketState.CloseReceived) { _state = WebSocketState.Closed; } + return _closeAsync((int)closeStatus, statusDescription, cancellationToken); + } + + /// + public override void Abort() + { + _state = WebSocketState.Aborted; + } + + /// + public override void Dispose() + { + _state = WebSocketState.Closed; + } - private static WebSocketMessageType OpCodeToEnum(int messageType) + private static WebSocketMessageType OpCodeToEnum(int messageType) + { + switch (messageType) { - switch (messageType) - { - case 0x1: - return WebSocketMessageType.Text; - case 0x2: - return WebSocketMessageType.Binary; - case 0x8: - return WebSocketMessageType.Close; - default: - throw new ArgumentOutOfRangeException(nameof(messageType), messageType, string.Empty); - } + case 0x1: + return WebSocketMessageType.Text; + case 0x2: + return WebSocketMessageType.Binary; + case 0x8: + return WebSocketMessageType.Close; + default: + throw new ArgumentOutOfRangeException(nameof(messageType), messageType, string.Empty); } + } - private static int EnumToOpCode(WebSocketMessageType webSocketMessageType) + private static int EnumToOpCode(WebSocketMessageType webSocketMessageType) + { + switch (webSocketMessageType) { - switch (webSocketMessageType) - { - case WebSocketMessageType.Text: - return 0x1; - case WebSocketMessageType.Binary: - return 0x2; - case WebSocketMessageType.Close: - return 0x8; - default: - throw new ArgumentOutOfRangeException(nameof(webSocketMessageType), webSocketMessageType, string.Empty); - } + case WebSocketMessageType.Text: + return 0x1; + case WebSocketMessageType.Binary: + return 0x2; + case WebSocketMessageType.Close: + return 0x8; + default: + throw new ArgumentOutOfRangeException(nameof(webSocketMessageType), webSocketMessageType, string.Empty); } } } diff --git a/src/Http/Owin/src/WebSockets/WebSocketAcceptAdapter.cs b/src/Http/Owin/src/WebSockets/WebSocketAcceptAdapter.cs index f167313f10..f60f159f56 100644 --- a/src/Http/Owin/src/WebSockets/WebSocketAcceptAdapter.cs +++ b/src/Http/Owin/src/WebSockets/WebSocketAcceptAdapter.cs @@ -8,95 +8,94 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Owin -{ - using AppFunc = Func, Task>; - using WebSocketAccept = - Action - < - IDictionary, // WebSocket Accept parameters - Func // WebSocketFunc callback - < - IDictionary, // WebSocket environment - Task // Complete - > - >; - using WebSocketAcceptAlt = - Func +namespace Microsoft.AspNetCore.Owin; + +using AppFunc = Func, Task>; +using WebSocketAccept = + Action + < + IDictionary, // WebSocket Accept parameters + Func // WebSocketFunc callback < - WebSocketAcceptContext, // WebSocket Accept parameters - Task - >; + IDictionary, // WebSocket environment + Task // Complete + > + >; +using WebSocketAcceptAlt = + Func + < + WebSocketAcceptContext, // WebSocket Accept parameters + Task + >; + +/// +/// This adapts the ASP.NET Core WebSocket Accept flow to match the OWIN WebSocket accept flow. +/// This enables OWIN based components to use WebSockets on ASP.NET Core servers. +/// +public class WebSocketAcceptAdapter +{ + private readonly IDictionary _env; + private readonly WebSocketAcceptAlt _accept; + private AppFunc _callback; + private IDictionary _options; /// - /// This adapts the ASP.NET Core WebSocket Accept flow to match the OWIN WebSocket accept flow. - /// This enables OWIN based components to use WebSockets on ASP.NET Core servers. + /// Initializes a new instance of for an OWIN environment. /// - public class WebSocketAcceptAdapter + /// The OWIN environment. + /// WebSocket accept delegate. + public WebSocketAcceptAdapter(IDictionary env, WebSocketAcceptAlt accept) { - private readonly IDictionary _env; - private readonly WebSocketAcceptAlt _accept; - private AppFunc _callback; - private IDictionary _options; - - /// - /// Initializes a new instance of for an OWIN environment. - /// - /// The OWIN environment. - /// WebSocket accept delegate. - public WebSocketAcceptAdapter(IDictionary env, WebSocketAcceptAlt accept) - { - _env = env; - _accept = accept; - } + _env = env; + _accept = accept; + } - private void AcceptWebSocket(IDictionary options, AppFunc callback) - { - _options = options; - _callback = callback; - _env[OwinConstants.ResponseStatusCode] = 101; - } + private void AcceptWebSocket(IDictionary options, AppFunc callback) + { + _options = options; + _callback = callback; + _env[OwinConstants.ResponseStatusCode] = 101; + } - /// - /// Adapt web sockets to OWIN. - /// - /// The next OWIN app delegate. - /// An OWIN app delegate. - public static AppFunc AdaptWebSockets(AppFunc next) + /// + /// Adapt web sockets to OWIN. + /// + /// The next OWIN app delegate. + /// An OWIN app delegate. + public static AppFunc AdaptWebSockets(AppFunc next) + { + return async environment => { - return async environment => + object accept; + if (environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out accept) && accept is WebSocketAcceptAlt) { - object accept; - if (environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out accept) && accept is WebSocketAcceptAlt) - { - var adapter = new WebSocketAcceptAdapter(environment, (WebSocketAcceptAlt)accept); + var adapter = new WebSocketAcceptAdapter(environment, (WebSocketAcceptAlt)accept); - environment[OwinConstants.WebSocket.Accept] = new WebSocketAccept(adapter.AcceptWebSocket); - await next(environment); - if ((int)environment[OwinConstants.ResponseStatusCode] == 101 && adapter._callback != null) + environment[OwinConstants.WebSocket.Accept] = new WebSocketAccept(adapter.AcceptWebSocket); + await next(environment); + if ((int)environment[OwinConstants.ResponseStatusCode] == 101 && adapter._callback != null) + { + WebSocketAcceptContext acceptContext = null; + object obj; + if (adapter._options != null && adapter._options.TryGetValue(typeof(WebSocketAcceptContext).FullName, out obj)) { - WebSocketAcceptContext acceptContext = null; - object obj; - if (adapter._options != null && adapter._options.TryGetValue(typeof(WebSocketAcceptContext).FullName, out obj)) - { - acceptContext = obj as WebSocketAcceptContext; - } - else if (adapter._options != null) - { - acceptContext = new OwinWebSocketAcceptContext(adapter._options); - } - - var webSocket = await adapter._accept(acceptContext); - var webSocketAdapter = new WebSocketAdapter(webSocket, (CancellationToken)environment[OwinConstants.CallCancelled]); - await adapter._callback(webSocketAdapter.Environment); - await webSocketAdapter.CleanupAsync(); + acceptContext = obj as WebSocketAcceptContext; } + else if (adapter._options != null) + { + acceptContext = new OwinWebSocketAcceptContext(adapter._options); + } + + var webSocket = await adapter._accept(acceptContext); + var webSocketAdapter = new WebSocketAdapter(webSocket, (CancellationToken)environment[OwinConstants.CallCancelled]); + await adapter._callback(webSocketAdapter.Environment); + await webSocketAdapter.CleanupAsync(); } - else - { - await next(environment); - } - }; - } + } + else + { + await next(environment); + } + }; } } diff --git a/src/Http/Owin/src/WebSockets/WebSocketAdapter.cs b/src/Http/Owin/src/WebSockets/WebSocketAdapter.cs index 792aa83b29..3c2dfe4192 100644 --- a/src/Http/Owin/src/WebSockets/WebSocketAdapter.cs +++ b/src/Http/Owin/src/WebSockets/WebSocketAdapter.cs @@ -8,167 +8,166 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Owin -{ - using WebSocketCloseAsync = - Func; - using WebSocketReceiveAsync = - Func /* data */, - CancellationToken /* cancel */, - Task>>; - using WebSocketReceiveTuple = - Tuple; - using WebSocketSendAsync = - Func /* data */, - int /* messageType */, +namespace Microsoft.AspNetCore.Owin; + +using WebSocketCloseAsync = + Func; +using WebSocketReceiveAsync = + Func /* data */, + CancellationToken /* cancel */, + Task; + int /* count */>>>; +using WebSocketReceiveTuple = + Tuple; +using WebSocketSendAsync = + Func /* data */, + int /* messageType */, + bool /* endOfMessage */, + CancellationToken /* cancel */, + Task>; + +/// +/// WebSocket adapter. +/// +public class WebSocketAdapter +{ + private readonly WebSocket _webSocket; + private readonly IDictionary _environment; + private readonly CancellationToken _cancellationToken; - /// - /// WebSocket adapter. - /// - public class WebSocketAdapter + internal WebSocketAdapter(WebSocket webSocket, CancellationToken ct) { - private readonly WebSocket _webSocket; - private readonly IDictionary _environment; - private readonly CancellationToken _cancellationToken; + _webSocket = webSocket; + _cancellationToken = ct; - internal WebSocketAdapter(WebSocket webSocket, CancellationToken ct) - { - _webSocket = webSocket; - _cancellationToken = ct; + _environment = new Dictionary(); + _environment[OwinConstants.WebSocket.SendAsync] = new WebSocketSendAsync(SendAsync); + _environment[OwinConstants.WebSocket.ReceiveAsync] = new WebSocketReceiveAsync(ReceiveAsync); + _environment[OwinConstants.WebSocket.CloseAsync] = new WebSocketCloseAsync(CloseAsync); + _environment[OwinConstants.WebSocket.CallCancelled] = ct; + _environment[OwinConstants.WebSocket.Version] = OwinConstants.WebSocket.VersionValue; - _environment = new Dictionary(); - _environment[OwinConstants.WebSocket.SendAsync] = new WebSocketSendAsync(SendAsync); - _environment[OwinConstants.WebSocket.ReceiveAsync] = new WebSocketReceiveAsync(ReceiveAsync); - _environment[OwinConstants.WebSocket.CloseAsync] = new WebSocketCloseAsync(CloseAsync); - _environment[OwinConstants.WebSocket.CallCancelled] = ct; - _environment[OwinConstants.WebSocket.Version] = OwinConstants.WebSocket.VersionValue; + _environment[typeof(WebSocket).FullName] = webSocket; + } - _environment[typeof(WebSocket).FullName] = webSocket; - } + internal IDictionary Environment + { + get { return _environment; } + } - internal IDictionary Environment + internal Task SendAsync(ArraySegment buffer, int messageType, bool endOfMessage, CancellationToken cancel) + { + // Remap close messages to CloseAsync. System.Net.WebSockets.WebSocket.SendAsync does not allow close messages. + if (messageType == 0x8) { - get { return _environment; } + return RedirectSendToCloseAsync(buffer, cancel); } - - internal Task SendAsync(ArraySegment buffer, int messageType, bool endOfMessage, CancellationToken cancel) + else if (messageType == 0x9 || messageType == 0xA) { - // Remap close messages to CloseAsync. System.Net.WebSockets.WebSocket.SendAsync does not allow close messages. - if (messageType == 0x8) - { - return RedirectSendToCloseAsync(buffer, cancel); - } - else if (messageType == 0x9 || messageType == 0xA) - { - // Ping & Pong, not allowed by the underlying APIs, silently discard. - return Task.CompletedTask; - } - - return _webSocket.SendAsync(buffer, OpCodeToEnum(messageType), endOfMessage, cancel); + // Ping & Pong, not allowed by the underlying APIs, silently discard. + return Task.CompletedTask; } - internal async Task ReceiveAsync(ArraySegment buffer, CancellationToken cancel) + return _webSocket.SendAsync(buffer, OpCodeToEnum(messageType), endOfMessage, cancel); + } + + internal async Task ReceiveAsync(ArraySegment buffer, CancellationToken cancel) + { + WebSocketReceiveResult nativeResult = await _webSocket.ReceiveAsync(buffer, cancel); + + if (nativeResult.MessageType == WebSocketMessageType.Close) { - WebSocketReceiveResult nativeResult = await _webSocket.ReceiveAsync(buffer, cancel); - - if (nativeResult.MessageType == WebSocketMessageType.Close) - { - _environment[OwinConstants.WebSocket.ClientCloseStatus] = (int)(nativeResult.CloseStatus ?? WebSocketCloseStatus.NormalClosure); - _environment[OwinConstants.WebSocket.ClientCloseDescription] = nativeResult.CloseStatusDescription ?? string.Empty; - } - - return new WebSocketReceiveTuple( - EnumToOpCode(nativeResult.MessageType), - nativeResult.EndOfMessage, - nativeResult.Count); + _environment[OwinConstants.WebSocket.ClientCloseStatus] = (int)(nativeResult.CloseStatus ?? WebSocketCloseStatus.NormalClosure); + _environment[OwinConstants.WebSocket.ClientCloseDescription] = nativeResult.CloseStatusDescription ?? string.Empty; } - internal Task CloseAsync(int status, string description, CancellationToken cancel) + return new WebSocketReceiveTuple( + EnumToOpCode(nativeResult.MessageType), + nativeResult.EndOfMessage, + nativeResult.Count); + } + + internal Task CloseAsync(int status, string description, CancellationToken cancel) + { + return _webSocket.CloseOutputAsync((WebSocketCloseStatus)status, description, cancel); + } + + private Task RedirectSendToCloseAsync(ArraySegment buffer, CancellationToken cancel) + { + if (buffer.Array == null || buffer.Count == 0) { - return _webSocket.CloseOutputAsync((WebSocketCloseStatus)status, description, cancel); + return CloseAsync(1000, string.Empty, cancel); } + else if (buffer.Count >= 2) + { + // Unpack the close message. + int statusCode = + (buffer.Array[buffer.Offset] << 8) + | buffer.Array[buffer.Offset + 1]; + string description = Encoding.UTF8.GetString(buffer.Array, buffer.Offset + 2, buffer.Count - 2); - private Task RedirectSendToCloseAsync(ArraySegment buffer, CancellationToken cancel) + return CloseAsync(statusCode, description, cancel); + } + else { - if (buffer.Array == null || buffer.Count == 0) - { - return CloseAsync(1000, string.Empty, cancel); - } - else if (buffer.Count >= 2) - { - // Unpack the close message. - int statusCode = - (buffer.Array[buffer.Offset] << 8) - | buffer.Array[buffer.Offset + 1]; - string description = Encoding.UTF8.GetString(buffer.Array, buffer.Offset + 2, buffer.Count - 2); - - return CloseAsync(statusCode, description, cancel); - } - else - { - throw new ArgumentOutOfRangeException(nameof(buffer)); - } + throw new ArgumentOutOfRangeException(nameof(buffer)); } + } - internal async Task CleanupAsync() + internal async Task CleanupAsync() + { + switch (_webSocket.State) { - switch (_webSocket.State) - { - case WebSocketState.Closed: // Closed gracefully, no action needed. - case WebSocketState.Aborted: // Closed abortively, no action needed. - break; - case WebSocketState.CloseReceived: - // Echo what the client said, if anything. - await _webSocket.CloseAsync(_webSocket.CloseStatus ?? WebSocketCloseStatus.NormalClosure, - _webSocket.CloseStatusDescription ?? string.Empty, _cancellationToken); - break; - case WebSocketState.Open: - case WebSocketState.CloseSent: // No close received, abort so we don't have to drain the pipe. - _webSocket.Abort(); - break; - default: - throw new NotSupportedException($"Unsupported {nameof(WebSocketState)} value: {_webSocket.State}."); - } + case WebSocketState.Closed: // Closed gracefully, no action needed. + case WebSocketState.Aborted: // Closed abortively, no action needed. + break; + case WebSocketState.CloseReceived: + // Echo what the client said, if anything. + await _webSocket.CloseAsync(_webSocket.CloseStatus ?? WebSocketCloseStatus.NormalClosure, + _webSocket.CloseStatusDescription ?? string.Empty, _cancellationToken); + break; + case WebSocketState.Open: + case WebSocketState.CloseSent: // No close received, abort so we don't have to drain the pipe. + _webSocket.Abort(); + break; + default: + throw new NotSupportedException($"Unsupported {nameof(WebSocketState)} value: {_webSocket.State}."); } + } - private static WebSocketMessageType OpCodeToEnum(int messageType) + private static WebSocketMessageType OpCodeToEnum(int messageType) + { + switch (messageType) { - switch (messageType) - { - case 0x1: - return WebSocketMessageType.Text; - case 0x2: - return WebSocketMessageType.Binary; - case 0x8: - return WebSocketMessageType.Close; - default: - throw new ArgumentOutOfRangeException(nameof(messageType), messageType, string.Empty); - } + case 0x1: + return WebSocketMessageType.Text; + case 0x2: + return WebSocketMessageType.Binary; + case 0x8: + return WebSocketMessageType.Close; + default: + throw new ArgumentOutOfRangeException(nameof(messageType), messageType, string.Empty); } + } - private static int EnumToOpCode(WebSocketMessageType webSocketMessageType) + private static int EnumToOpCode(WebSocketMessageType webSocketMessageType) + { + switch (webSocketMessageType) { - switch (webSocketMessageType) - { - case WebSocketMessageType.Text: - return 0x1; - case WebSocketMessageType.Binary: - return 0x2; - case WebSocketMessageType.Close: - return 0x8; - default: - throw new ArgumentOutOfRangeException(nameof(webSocketMessageType), webSocketMessageType, string.Empty); - } + case WebSocketMessageType.Text: + return 0x1; + case WebSocketMessageType.Binary: + return 0x2; + case WebSocketMessageType.Close: + return 0x8; + default: + throw new ArgumentOutOfRangeException(nameof(webSocketMessageType), webSocketMessageType, string.Empty); } } } diff --git a/src/Http/Owin/test/OwinEnvironmentTests.cs b/src/Http/Owin/test/OwinEnvironmentTests.cs index dcdeb847e1..6cb8e415d9 100644 --- a/src/Http/Owin/test/OwinEnvironmentTests.cs +++ b/src/Http/Owin/test/OwinEnvironmentTests.cs @@ -10,139 +10,138 @@ using System.Threading; using Microsoft.AspNetCore.Http; using Xunit; -namespace Microsoft.AspNetCore.Owin +namespace Microsoft.AspNetCore.Owin; + +public class OwinEnvironmentTests { - public class OwinEnvironmentTests + private T Get(IDictionary environment, string key) + { + object value; + return environment.TryGetValue(key, out value) ? (T)value : default(T); + } + + [Fact] + public void OwinEnvironmentCanBeCreated() + { + HttpContext context = CreateContext(); + context.Request.Method = "SomeMethod"; + context.User = new ClaimsPrincipal(new ClaimsIdentity("Foo")); + context.Request.Body = Stream.Null; + context.Request.Headers["CustomRequestHeader"] = "CustomRequestValue"; + context.Request.Path = new PathString("/path"); + context.Request.PathBase = new PathString("/pathBase"); + context.Request.Protocol = "http/1.0"; + context.Request.QueryString = new QueryString("?key=value"); + context.Request.Scheme = "http"; + context.Response.Body = Stream.Null; + context.Response.Headers["CustomResponseHeader"] = "CustomResponseValue"; + context.Response.StatusCode = 201; + + IDictionary env = new OwinEnvironment(context); + Assert.Equal("SomeMethod", Get(env, "owin.RequestMethod")); + // User property should set both server.User (non-standard) and owin.RequestUser. + Assert.Equal("Foo", Get(env, "server.User").Identity.AuthenticationType); + Assert.Equal("Foo", Get(env, "owin.RequestUser").Identity.AuthenticationType); + Assert.Same(Stream.Null, Get(env, "owin.RequestBody")); + var requestHeaders = Get>(env, "owin.RequestHeaders"); + Assert.NotNull(requestHeaders); + Assert.Equal("CustomRequestValue", requestHeaders["CustomRequestHeader"].First()); + Assert.Equal("/path", Get(env, "owin.RequestPath")); + Assert.Equal("/pathBase", Get(env, "owin.RequestPathBase")); + Assert.Equal("http/1.0", Get(env, "owin.RequestProtocol")); + Assert.Equal("key=value", Get(env, "owin.RequestQueryString")); + Assert.Equal("http", Get(env, "owin.RequestScheme")); + + Assert.Same(Stream.Null, Get(env, "owin.ResponseBody")); + var responseHeaders = Get>(env, "owin.ResponseHeaders"); + Assert.NotNull(responseHeaders); + Assert.Equal("CustomResponseValue", responseHeaders["CustomResponseHeader"].First()); + Assert.Equal(201, Get(env, "owin.ResponseStatusCode")); + } + + [Fact] + public void OwinEnvironmentCanBeModified() + { + HttpContext context = CreateContext(); + IDictionary env = new OwinEnvironment(context); + + env["owin.RequestMethod"] = "SomeMethod"; + env["server.User"] = new ClaimsPrincipal(new ClaimsIdentity("Foo")); + Assert.Equal("Foo", context.User.Identity.AuthenticationType); + // User property should fall back from owin.RequestUser to server.User. + env["owin.RequestUser"] = new ClaimsPrincipal(new ClaimsIdentity("Bar")); + Assert.Equal("Bar", context.User.Identity.AuthenticationType); + env["owin.RequestBody"] = Stream.Null; + var requestHeaders = Get>(env, "owin.RequestHeaders"); + Assert.NotNull(requestHeaders); + requestHeaders["CustomRequestHeader"] = new[] { "CustomRequestValue" }; + env["owin.RequestPath"] = "/path"; + env["owin.RequestPathBase"] = "/pathBase"; + env["owin.RequestProtocol"] = "http/1.0"; + env["owin.RequestQueryString"] = "key=value"; + env["owin.RequestScheme"] = "http"; + env["owin.ResponseBody"] = Stream.Null; + var responseHeaders = Get>(env, "owin.ResponseHeaders"); + Assert.NotNull(responseHeaders); + responseHeaders["CustomResponseHeader"] = new[] { "CustomResponseValue" }; + env["owin.ResponseStatusCode"] = 201; + + Assert.Equal("SomeMethod", context.Request.Method); + Assert.Same(Stream.Null, context.Request.Body); + Assert.Equal("CustomRequestValue", context.Request.Headers["CustomRequestHeader"]); + Assert.Equal("/path", context.Request.Path.Value); + Assert.Equal("/pathBase", context.Request.PathBase.Value); + Assert.Equal("http/1.0", context.Request.Protocol); + Assert.Equal("?key=value", context.Request.QueryString.Value); + Assert.Equal("http", context.Request.Scheme); + + Assert.Same(Stream.Null, context.Response.Body); + Assert.Equal("CustomResponseValue", context.Response.Headers["CustomResponseHeader"]); + Assert.Equal(201, context.Response.StatusCode); + } + + [Theory] + [InlineData("server.LocalPort")] + public void OwinEnvironmentDoesNotContainEntriesForMissingFeatures(string key) + { + HttpContext context = CreateContext(); + IDictionary env = new OwinEnvironment(context); + + object value; + Assert.False(env.TryGetValue(key, out value)); + + Assert.Throws(() => env[key]); + + Assert.False(env.Keys.Contains(key)); + Assert.False(env.ContainsKey(key)); + } + + [Fact] + public void OwinEnvironmentSuppliesDefaultsForMissingRequiredEntries() + { + HttpContext context = CreateContext(); + IDictionary env = new OwinEnvironment(context); + + object value; + Assert.True(env.TryGetValue("owin.CallCancelled", out value), "owin.CallCancelled"); + Assert.True(env.TryGetValue("owin.Version", out value), "owin.Version"); + + Assert.Equal(CancellationToken.None, env["owin.CallCancelled"]); + Assert.Equal("1.0", env["owin.Version"]); + } + + [Fact] + public void OwinEnvironmentImplementsGetEnumerator() + { + var owinEnvironment = new OwinEnvironment(CreateContext()); + + Assert.NotNull(owinEnvironment.GetEnumerator()); + Assert.NotNull(((IEnumerable)owinEnvironment).GetEnumerator()); + } + + private HttpContext CreateContext() { - private T Get(IDictionary environment, string key) - { - object value; - return environment.TryGetValue(key, out value) ? (T)value : default(T); - } - - [Fact] - public void OwinEnvironmentCanBeCreated() - { - HttpContext context = CreateContext(); - context.Request.Method = "SomeMethod"; - context.User = new ClaimsPrincipal(new ClaimsIdentity("Foo")); - context.Request.Body = Stream.Null; - context.Request.Headers["CustomRequestHeader"] = "CustomRequestValue"; - context.Request.Path = new PathString("/path"); - context.Request.PathBase = new PathString("/pathBase"); - context.Request.Protocol = "http/1.0"; - context.Request.QueryString = new QueryString("?key=value"); - context.Request.Scheme = "http"; - context.Response.Body = Stream.Null; - context.Response.Headers["CustomResponseHeader"] = "CustomResponseValue"; - context.Response.StatusCode = 201; - - IDictionary env = new OwinEnvironment(context); - Assert.Equal("SomeMethod", Get(env, "owin.RequestMethod")); - // User property should set both server.User (non-standard) and owin.RequestUser. - Assert.Equal("Foo", Get(env, "server.User").Identity.AuthenticationType); - Assert.Equal("Foo", Get(env, "owin.RequestUser").Identity.AuthenticationType); - Assert.Same(Stream.Null, Get(env, "owin.RequestBody")); - var requestHeaders = Get>(env, "owin.RequestHeaders"); - Assert.NotNull(requestHeaders); - Assert.Equal("CustomRequestValue", requestHeaders["CustomRequestHeader"].First()); - Assert.Equal("/path", Get(env, "owin.RequestPath")); - Assert.Equal("/pathBase", Get(env, "owin.RequestPathBase")); - Assert.Equal("http/1.0", Get(env, "owin.RequestProtocol")); - Assert.Equal("key=value", Get(env, "owin.RequestQueryString")); - Assert.Equal("http", Get(env, "owin.RequestScheme")); - - Assert.Same(Stream.Null, Get(env, "owin.ResponseBody")); - var responseHeaders = Get>(env, "owin.ResponseHeaders"); - Assert.NotNull(responseHeaders); - Assert.Equal("CustomResponseValue", responseHeaders["CustomResponseHeader"].First()); - Assert.Equal(201, Get(env, "owin.ResponseStatusCode")); - } - - [Fact] - public void OwinEnvironmentCanBeModified() - { - HttpContext context = CreateContext(); - IDictionary env = new OwinEnvironment(context); - - env["owin.RequestMethod"] = "SomeMethod"; - env["server.User"] = new ClaimsPrincipal(new ClaimsIdentity("Foo")); - Assert.Equal("Foo", context.User.Identity.AuthenticationType); - // User property should fall back from owin.RequestUser to server.User. - env["owin.RequestUser"] = new ClaimsPrincipal(new ClaimsIdentity("Bar")); - Assert.Equal("Bar", context.User.Identity.AuthenticationType); - env["owin.RequestBody"] = Stream.Null; - var requestHeaders = Get>(env, "owin.RequestHeaders"); - Assert.NotNull(requestHeaders); - requestHeaders["CustomRequestHeader"] = new[] { "CustomRequestValue" }; - env["owin.RequestPath"] = "/path"; - env["owin.RequestPathBase"] = "/pathBase"; - env["owin.RequestProtocol"] = "http/1.0"; - env["owin.RequestQueryString"] = "key=value"; - env["owin.RequestScheme"] = "http"; - env["owin.ResponseBody"] = Stream.Null; - var responseHeaders = Get>(env, "owin.ResponseHeaders"); - Assert.NotNull(responseHeaders); - responseHeaders["CustomResponseHeader"] = new[] { "CustomResponseValue" }; - env["owin.ResponseStatusCode"] = 201; - - Assert.Equal("SomeMethod", context.Request.Method); - Assert.Same(Stream.Null, context.Request.Body); - Assert.Equal("CustomRequestValue", context.Request.Headers["CustomRequestHeader"]); - Assert.Equal("/path", context.Request.Path.Value); - Assert.Equal("/pathBase", context.Request.PathBase.Value); - Assert.Equal("http/1.0", context.Request.Protocol); - Assert.Equal("?key=value", context.Request.QueryString.Value); - Assert.Equal("http", context.Request.Scheme); - - Assert.Same(Stream.Null, context.Response.Body); - Assert.Equal("CustomResponseValue", context.Response.Headers["CustomResponseHeader"]); - Assert.Equal(201, context.Response.StatusCode); - } - - [Theory] - [InlineData("server.LocalPort")] - public void OwinEnvironmentDoesNotContainEntriesForMissingFeatures(string key) - { - HttpContext context = CreateContext(); - IDictionary env = new OwinEnvironment(context); - - object value; - Assert.False(env.TryGetValue(key, out value)); - - Assert.Throws(() => env[key]); - - Assert.False(env.Keys.Contains(key)); - Assert.False(env.ContainsKey(key)); - } - - [Fact] - public void OwinEnvironmentSuppliesDefaultsForMissingRequiredEntries() - { - HttpContext context = CreateContext(); - IDictionary env = new OwinEnvironment(context); - - object value; - Assert.True(env.TryGetValue("owin.CallCancelled", out value), "owin.CallCancelled"); - Assert.True(env.TryGetValue("owin.Version", out value), "owin.Version"); - - Assert.Equal(CancellationToken.None, env["owin.CallCancelled"]); - Assert.Equal("1.0", env["owin.Version"]); - } - - [Fact] - public void OwinEnvironmentImplementsGetEnumerator() - { - var owinEnvironment = new OwinEnvironment(CreateContext()); - - Assert.NotNull(owinEnvironment.GetEnumerator()); - Assert.NotNull(((IEnumerable)owinEnvironment).GetEnumerator()); - } - - private HttpContext CreateContext() - { - var context = new DefaultHttpContext(); - return context; - } + var context = new DefaultHttpContext(); + return context; } } diff --git a/src/Http/Owin/test/OwinExtensionTests.cs b/src/Http/Owin/test/OwinExtensionTests.cs index 9bb1d3f038..ca6ea45ba8 100644 --- a/src/Http/Owin/test/OwinExtensionTests.cs +++ b/src/Http/Owin/test/OwinExtensionTests.cs @@ -10,154 +10,153 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Microsoft.AspNetCore.Owin +namespace Microsoft.AspNetCore.Owin; + +using AddMiddleware = Action, Task>, + Func, Task> + >>; +using AppFunc = Func, Task>; +using CreateMiddleware = Func< + Func, Task>, + Func, Task> + >; + +public class OwinExtensionTests { - using AddMiddleware = Action, Task>, - Func, Task> - >>; - using AppFunc = Func, Task>; - using CreateMiddleware = Func< - Func, Task>, - Func, Task> - >; - - public class OwinExtensionTests + static readonly AppFunc notFound = env => new Task(() => { env["owin.ResponseStatusCode"] = 404; }); + + [Fact] + public async Task OwinConfigureServiceProviderAddsServices() { - static readonly AppFunc notFound = env => new Task(() => { env["owin.ResponseStatusCode"] = 404; }); + var list = new List(); + AddMiddleware build = list.Add; + IServiceProvider serviceProvider = null; + FakeService fakeService = null; - [Fact] - public async Task OwinConfigureServiceProviderAddsServices() + var builder = build.UseBuilder(applicationBuilder => { - var list = new List(); - AddMiddleware build = list.Add; - IServiceProvider serviceProvider = null; - FakeService fakeService = null; - - var builder = build.UseBuilder(applicationBuilder => + serviceProvider = applicationBuilder.ApplicationServices; + applicationBuilder.Run(context => { - serviceProvider = applicationBuilder.ApplicationServices; - applicationBuilder.Run(context => - { - fakeService = context.RequestServices.GetService(); - return Task.FromResult(0); - }); - }, - new ServiceCollection().AddSingleton(new FakeService()).BuildServiceProvider()); - - list.Reverse(); - await list - .Aggregate(notFound, (next, middleware) => middleware(next)) - .Invoke(new Dictionary()); - - Assert.NotNull(serviceProvider); - Assert.NotNull(serviceProvider.GetService()); - Assert.NotNull(fakeService); - } - - [Fact] - public async Task OwinDefaultNoServices() + fakeService = context.RequestServices.GetService(); + return Task.FromResult(0); + }); + }, + new ServiceCollection().AddSingleton(new FakeService()).BuildServiceProvider()); + + list.Reverse(); + await list + .Aggregate(notFound, (next, middleware) => middleware(next)) + .Invoke(new Dictionary()); + + Assert.NotNull(serviceProvider); + Assert.NotNull(serviceProvider.GetService()); + Assert.NotNull(fakeService); + } + + [Fact] + public async Task OwinDefaultNoServices() + { + var list = new List(); + AddMiddleware build = list.Add; + IServiceProvider expectedServiceProvider = new ServiceCollection().BuildServiceProvider(); + IServiceProvider serviceProvider = null; + FakeService fakeService = null; + bool builderExecuted = false; + bool applicationExecuted = false; + + var builder = build.UseBuilder(applicationBuilder => { - var list = new List(); - AddMiddleware build = list.Add; - IServiceProvider expectedServiceProvider = new ServiceCollection().BuildServiceProvider(); - IServiceProvider serviceProvider = null; - FakeService fakeService = null; - bool builderExecuted = false; - bool applicationExecuted = false; - - var builder = build.UseBuilder(applicationBuilder => + builderExecuted = true; + serviceProvider = applicationBuilder.ApplicationServices; + applicationBuilder.Run(context => { - builderExecuted = true; - serviceProvider = applicationBuilder.ApplicationServices; - applicationBuilder.Run(context => - { - applicationExecuted = true; - fakeService = context.RequestServices.GetService(); - return Task.FromResult(0); - }); - }, - expectedServiceProvider); - - list.Reverse(); - await list - .Aggregate(notFound, (next, middleware) => middleware(next)) - .Invoke(new Dictionary()); - - Assert.True(builderExecuted); - Assert.Equal(expectedServiceProvider, serviceProvider); - Assert.True(applicationExecuted); - Assert.Null(fakeService); - } - - [Fact] - public async Task OwinDefaultNullServiceProvider() + applicationExecuted = true; + fakeService = context.RequestServices.GetService(); + return Task.FromResult(0); + }); + }, + expectedServiceProvider); + + list.Reverse(); + await list + .Aggregate(notFound, (next, middleware) => middleware(next)) + .Invoke(new Dictionary()); + + Assert.True(builderExecuted); + Assert.Equal(expectedServiceProvider, serviceProvider); + Assert.True(applicationExecuted); + Assert.Null(fakeService); + } + + [Fact] + public async Task OwinDefaultNullServiceProvider() + { + var list = new List(); + AddMiddleware build = list.Add; + IServiceProvider serviceProvider = null; + FakeService fakeService = null; + bool builderExecuted = false; + bool applicationExecuted = false; + + var builder = build.UseBuilder(applicationBuilder => { - var list = new List(); - AddMiddleware build = list.Add; - IServiceProvider serviceProvider = null; - FakeService fakeService = null; - bool builderExecuted = false; - bool applicationExecuted = false; - - var builder = build.UseBuilder(applicationBuilder => + builderExecuted = true; + serviceProvider = applicationBuilder.ApplicationServices; + applicationBuilder.Run(context => { - builderExecuted = true; - serviceProvider = applicationBuilder.ApplicationServices; - applicationBuilder.Run(context => - { - applicationExecuted = true; - fakeService = context.RequestServices.GetService(); - return Task.FromResult(0); - }); + applicationExecuted = true; + fakeService = context.RequestServices.GetService(); + return Task.FromResult(0); }); + }); - list.Reverse(); - await list - .Aggregate(notFound, (next, middleware) => middleware(next)) - .Invoke(new Dictionary()); + list.Reverse(); + await list + .Aggregate(notFound, (next, middleware) => middleware(next)) + .Invoke(new Dictionary()); - Assert.True(builderExecuted); - Assert.NotNull(serviceProvider); - Assert.True(applicationExecuted); - Assert.Null(fakeService); - } + Assert.True(builderExecuted); + Assert.NotNull(serviceProvider); + Assert.True(applicationExecuted); + Assert.Null(fakeService); + } - [Fact] - public async Task UseOwin() - { - var serviceProvider = new ServiceCollection().BuildServiceProvider(); - var builder = new ApplicationBuilder(serviceProvider); - IDictionary environment = null; - var context = new DefaultHttpContext(); + [Fact] + public async Task UseOwin() + { + var serviceProvider = new ServiceCollection().BuildServiceProvider(); + var builder = new ApplicationBuilder(serviceProvider); + IDictionary environment = null; + var context = new DefaultHttpContext(); - builder.UseOwin(addToPipeline => + builder.UseOwin(addToPipeline => + { + addToPipeline(next => { - addToPipeline(next => + Assert.NotNull(next); + return async env => { - Assert.NotNull(next); - return async env => - { - environment = env; - await next(env); - }; - }); + environment = env; + await next(env); + }; }); - await builder.Build().Invoke(context); - - // Dictionary contains context but does not contain "websocket.Accept" or "websocket.AcceptAlt" keys. - Assert.NotNull(environment); - var value = Assert.Single( - environment, - kvp => string.Equals(typeof(HttpContext).FullName, kvp.Key, StringComparison.Ordinal)) - .Value; - Assert.Equal(context, value); - Assert.False(environment.ContainsKey("websocket.Accept")); - Assert.False(environment.ContainsKey("websocket.AcceptAlt")); - } - - private class FakeService - { - } + }); + await builder.Build().Invoke(context); + + // Dictionary contains context but does not contain "websocket.Accept" or "websocket.AcceptAlt" keys. + Assert.NotNull(environment); + var value = Assert.Single( + environment, + kvp => string.Equals(typeof(HttpContext).FullName, kvp.Key, StringComparison.Ordinal)) + .Value; + Assert.Equal(context, value); + Assert.False(environment.ContainsKey("websocket.Accept")); + Assert.False(environment.ContainsKey("websocket.AcceptAlt")); + } + + private class FakeService + { } } diff --git a/src/Http/Owin/test/OwinFeatureCollectionTests.cs b/src/Http/Owin/test/OwinFeatureCollectionTests.cs index b956960d1c..e92dab8444 100644 --- a/src/Http/Owin/test/OwinFeatureCollectionTests.cs +++ b/src/Http/Owin/test/OwinFeatureCollectionTests.cs @@ -6,63 +6,62 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Xunit; -namespace Microsoft.AspNetCore.Owin +namespace Microsoft.AspNetCore.Owin; + +public class OwinHttpEnvironmentTests { - public class OwinHttpEnvironmentTests + private T Get(IFeatureCollection features) { - private T Get(IFeatureCollection features) - { - return (T)features[typeof(T)]; - } + return (T)features[typeof(T)]; + } - private T Get(IDictionary env, string key) - { - object value; - return env.TryGetValue(key, out value) ? (T)value : default(T); - } + private T Get(IDictionary env, string key) + { + object value; + return env.TryGetValue(key, out value) ? (T)value : default(T); + } - [Fact] - public void OwinHttpEnvironmentCanBeCreated() - { - var env = new Dictionary + [Fact] + public void OwinHttpEnvironmentCanBeCreated() + { + var env = new Dictionary { { "owin.RequestMethod", HttpMethods.Post }, { "owin.RequestPath", "/path" }, { "owin.RequestPathBase", "/pathBase" }, { "owin.RequestQueryString", "name=value" }, }; - var features = new OwinFeatureCollection(env); + var features = new OwinFeatureCollection(env); - var requestFeature = Get(features); - Assert.Equal(requestFeature.Method, HttpMethods.Post); - Assert.Equal("/path", requestFeature.Path); - Assert.Equal("/pathBase", requestFeature.PathBase); - Assert.Equal("?name=value", requestFeature.QueryString); - } + var requestFeature = Get(features); + Assert.Equal(requestFeature.Method, HttpMethods.Post); + Assert.Equal("/path", requestFeature.Path); + Assert.Equal("/pathBase", requestFeature.PathBase); + Assert.Equal("?name=value", requestFeature.QueryString); + } - [Fact] - public void OwinHttpEnvironmentCanBeModified() - { - var env = new Dictionary + [Fact] + public void OwinHttpEnvironmentCanBeModified() + { + var env = new Dictionary { { "owin.RequestMethod", HttpMethods.Post }, { "owin.RequestPath", "/path" }, { "owin.RequestPathBase", "/pathBase" }, { "owin.RequestQueryString", "name=value" }, }; - var features = new OwinFeatureCollection(env); + var features = new OwinFeatureCollection(env); - var requestFeature = Get(features); - requestFeature.Method = HttpMethods.Get; - requestFeature.Path = "/path2"; - requestFeature.PathBase = "/pathBase2"; - requestFeature.QueryString = "?name=value2"; + var requestFeature = Get(features); + requestFeature.Method = HttpMethods.Get; + requestFeature.Path = "/path2"; + requestFeature.PathBase = "/pathBase2"; + requestFeature.QueryString = "?name=value2"; - Assert.Equal(HttpMethods.Get, Get(env, "owin.RequestMethod")); - Assert.Equal("/path2", Get(env, "owin.RequestPath")); - Assert.Equal("/pathBase2", Get(env, "owin.RequestPathBase")); - Assert.Equal("name=value2", Get(env, "owin.RequestQueryString")); - } + Assert.Equal(HttpMethods.Get, Get(env, "owin.RequestMethod")); + Assert.Equal("/path2", Get(env, "owin.RequestPath")); + Assert.Equal("/pathBase2", Get(env, "owin.RequestPathBase")); + Assert.Equal("name=value2", Get(env, "owin.RequestQueryString")); } } diff --git a/src/Http/Routing.Abstractions/src/IOutboundParameterTransformer.cs b/src/Http/Routing.Abstractions/src/IOutboundParameterTransformer.cs index c882fac762..308c1f7e11 100644 --- a/src/Http/Routing.Abstractions/src/IOutboundParameterTransformer.cs +++ b/src/Http/Routing.Abstractions/src/IOutboundParameterTransformer.cs @@ -1,19 +1,18 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Defines the contract that a class must implement to transform route values while building +/// a URI. +/// +public interface IOutboundParameterTransformer : IParameterPolicy { /// - /// Defines the contract that a class must implement to transform route values while building - /// a URI. + /// Transforms the specified route value to a string for inclusion in a URI. /// - public interface IOutboundParameterTransformer : IParameterPolicy - { - /// - /// Transforms the specified route value to a string for inclusion in a URI. - /// - /// The route value to transform. - /// The transformed value. - string? TransformOutbound(object? value); - } + /// The route value to transform. + /// The transformed value. + string? TransformOutbound(object? value); } diff --git a/src/Http/Routing.Abstractions/src/IParameterPolicy.cs b/src/Http/Routing.Abstractions/src/IParameterPolicy.cs index 25a16dadd9..05fb05bc56 100644 --- a/src/Http/Routing.Abstractions/src/IParameterPolicy.cs +++ b/src/Http/Routing.Abstractions/src/IParameterPolicy.cs @@ -1,12 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// A marker interface for types that are associated with route parameters. +/// +public interface IParameterPolicy { - /// - /// A marker interface for types that are associated with route parameters. - /// - public interface IParameterPolicy - { - } } diff --git a/src/Http/Routing.Abstractions/src/IRouteConstraint.cs b/src/Http/Routing.Abstractions/src/IRouteConstraint.cs index a259a18410..1c390b318b 100644 --- a/src/Http/Routing.Abstractions/src/IRouteConstraint.cs +++ b/src/Http/Routing.Abstractions/src/IRouteConstraint.cs @@ -3,31 +3,30 @@ using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Defines the contract that a class must implement in order to check whether a URL parameter +/// value is valid for a constraint. +/// +public interface IRouteConstraint : IParameterPolicy { /// - /// Defines the contract that a class must implement in order to check whether a URL parameter - /// value is valid for a constraint. + /// Determines whether the URL parameter contains a valid value for this constraint. /// - public interface IRouteConstraint : IParameterPolicy - { - /// - /// Determines whether the URL parameter contains a valid value for this constraint. - /// - /// An object that encapsulates information about the HTTP request. - /// The router that this constraint belongs to. - /// The name of the parameter that is being checked. - /// A dictionary that contains the parameters for the URL. - /// - /// An object that indicates whether the constraint check is being performed - /// when an incoming request is being handled or when a URL is being generated. - /// - /// true if the URL parameter contains a valid value; otherwise, false. - bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection); - } + /// An object that encapsulates information about the HTTP request. + /// The router that this constraint belongs to. + /// The name of the parameter that is being checked. + /// A dictionary that contains the parameters for the URL. + /// + /// An object that indicates whether the constraint check is being performed + /// when an incoming request is being handled or when a URL is being generated. + /// + /// true if the URL parameter contains a valid value; otherwise, false. + bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection); } diff --git a/src/Http/Routing.Abstractions/src/IRouteHandler.cs b/src/Http/Routing.Abstractions/src/IRouteHandler.cs index 44d33eddfe..385fd4b8ff 100644 --- a/src/Http/Routing.Abstractions/src/IRouteHandler.cs +++ b/src/Http/Routing.Abstractions/src/IRouteHandler.cs @@ -3,22 +3,21 @@ using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Defines a contract for a handler of a route. +/// +public interface IRouteHandler { /// - /// Defines a contract for a handler of a route. + /// Gets a to handle the request, based on the provided + /// . /// - public interface IRouteHandler - { - /// - /// Gets a to handle the request, based on the provided - /// . - /// - /// The associated with the current request. - /// The associated with the current routing match. - /// - /// A , or null if the handler cannot handle this request. - /// - RequestDelegate GetRequestHandler(HttpContext httpContext, RouteData routeData); - } + /// The associated with the current request. + /// The associated with the current routing match. + /// + /// A , or null if the handler cannot handle this request. + /// + RequestDelegate GetRequestHandler(HttpContext httpContext, RouteData routeData); } diff --git a/src/Http/Routing.Abstractions/src/IRouter.cs b/src/Http/Routing.Abstractions/src/IRouter.cs index d86455546e..8a90c2dd86 100644 --- a/src/Http/Routing.Abstractions/src/IRouter.cs +++ b/src/Http/Routing.Abstractions/src/IRouter.cs @@ -3,24 +3,23 @@ using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Interface for implementing a router. +/// +public interface IRouter { /// - /// Interface for implementing a router. + /// Asynchronously routes based on the current . /// - public interface IRouter - { - /// - /// Asynchronously routes based on the current . - /// - /// A instance. - Task RouteAsync(RouteContext context); + /// A instance. + Task RouteAsync(RouteContext context); - /// - /// Returns the URL that is associated with the route details provided in - /// - /// A instance. - /// A object. Can be null. - VirtualPathData? GetVirtualPath(VirtualPathContext context); - } + /// + /// Returns the URL that is associated with the route details provided in + /// + /// A instance. + /// A object. Can be null. + VirtualPathData? GetVirtualPath(VirtualPathContext context); } diff --git a/src/Http/Routing.Abstractions/src/IRoutingFeature.cs b/src/Http/Routing.Abstractions/src/IRoutingFeature.cs index 9a4f84e56b..d2d69bb725 100644 --- a/src/Http/Routing.Abstractions/src/IRoutingFeature.cs +++ b/src/Http/Routing.Abstractions/src/IRoutingFeature.cs @@ -3,16 +3,15 @@ #nullable enable -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// A feature interface for routing functionality. +/// +public interface IRoutingFeature { /// - /// A feature interface for routing functionality. + /// Gets or sets the associated with the current request. /// - public interface IRoutingFeature - { - /// - /// Gets or sets the associated with the current request. - /// - RouteData? RouteData { get; set; } - } + RouteData? RouteData { get; set; } } diff --git a/src/Http/Routing.Abstractions/src/LinkGenerator.cs b/src/Http/Routing.Abstractions/src/LinkGenerator.cs index 88a4361ce9..1a67311653 100644 --- a/src/Http/Routing.Abstractions/src/LinkGenerator.cs +++ b/src/Http/Routing.Abstractions/src/LinkGenerator.cs @@ -4,153 +4,152 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Defines a contract to generate absolute and related URIs based on endpoint routing. +/// +/// +/// +/// Generating URIs in endpoint routing occurs in two phases. First, an address is bound to a list of +/// endpoints that match the address. Secondly, each endpoint's RoutePattern is evaluated, until +/// a route pattern that matches the supplied values is found. The resulting output is combined with +/// the other URI parts supplied to the link generator and returned. +/// +/// +/// The methods provided by the type are general infrastructure, and support +/// the standard link generator functionality for any type of address. The most convenient way to use +/// is through extension methods that perform operations for a specific +/// address type. +/// +/// +public abstract class LinkGenerator { /// - /// Defines a contract to generate absolute and related URIs based on endpoint routing. + /// Generates a URI with an absolute path based on the provided values and . + /// + /// The address type. + /// The associated with the current request. + /// The address value. Used to resolve endpoints. + /// The route values. Used to expand parameters in the route template. + /// The values associated with the current request. Optional. + /// + /// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of will be used. + /// + /// An optional URI fragment. Appended to the resulting URI. + /// + /// An optional . Settings on provided object override the settings with matching + /// names from RouteOptions. + /// + /// A URI with an absolute path, or null. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public abstract string? GetPathByAddress( + HttpContext httpContext, + TAddress address, + RouteValueDictionary values, + RouteValueDictionary? ambientValues = default, + PathString? pathBase = default, + FragmentString fragment = default, + LinkOptions? options = default); + + /// + /// Generates a URI with an absolute path based on the provided values. /// + /// The address type. + /// The address value. Used to resolve endpoints. + /// The route values. Used to expand parameters in the route template. + /// An optional URI path base. Prepended to the path in the resulting URI. + /// An optional URI fragment. Appended to the resulting URI. + /// + /// An optional . Settings on provided object override the settings with matching + /// names from RouteOptions. + /// + /// A URI with an absolute path, or null. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public abstract string? GetPathByAddress( + TAddress address, + RouteValueDictionary values, + PathString pathBase = default, + FragmentString fragment = default, + LinkOptions? options = default); + + /// + /// Generates an absolute URI based on the provided values and . + /// + /// The address type. + /// The associated with the current request. + /// The address value. Used to resolve endpoints. + /// The route values. Used to expand parameters in the route template. + /// The values associated with the current request. Optional. + /// + /// The URI scheme, applied to the resulting URI. Optional. If not provided, the value of will be used. + /// + /// + /// The URI host/authority, applied to the resulting URI. Optional. If not provided, the value will be used. + /// See the remarks section for details about the security implications of the . + /// + /// + /// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of will be used. + /// + /// An optional URI fragment. Appended to the resulting URI. + /// + /// An optional . Settings on provided object override the settings with matching + /// names from RouteOptions. + /// + /// A URI with an absolute path, or null. /// /// - /// Generating URIs in endpoint routing occurs in two phases. First, an address is bound to a list of - /// endpoints that match the address. Secondly, each endpoint's RoutePattern is evaluated, until - /// a route pattern that matches the supplied values is found. The resulting output is combined with - /// the other URI parts supplied to the link generator and returned. + /// The value of should be a trusted value. Relying on the value of the current request + /// can allow untrusted input to influence the resulting URI unless the Host header has been validated. + /// See the deployment documentation for instructions on how to properly validate the Host header in + /// your deployment environment. /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public abstract string? GetUriByAddress( + HttpContext httpContext, + TAddress address, + RouteValueDictionary values, + RouteValueDictionary? ambientValues = default, + string? scheme = default, + HostString? host = default, + PathString? pathBase = default, + FragmentString fragment = default, + LinkOptions? options = default); + + /// + /// Generates an absolute URI based on the provided values. + /// + /// The address type. + /// The address value. Used to resolve endpoints. + /// The route values. Used to expand parameters in the route template. + /// The URI scheme, applied to the resulting URI. + /// + /// The URI host/authority, applied to the resulting URI. + /// See the remarks section for details about the security implications of the . + /// + /// An optional URI path base. Prepended to the path in the resulting URI. + /// An optional URI fragment. Appended to the resulting URI. + /// + /// An optional . Settings on provided object override the settings with matching + /// names from RouteOptions. + /// + /// An absolute URI, or null. + /// /// - /// The methods provided by the type are general infrastructure, and support - /// the standard link generator functionality for any type of address. The most convenient way to use - /// is through extension methods that perform operations for a specific - /// address type. + /// The value of should be a trusted value. Relying on the value of the current request + /// can allow untrusted input to influence the resulting URI unless the Host header has been validated. + /// See the deployment documentation for instructions on how to properly validate the Host header in + /// your deployment environment. /// /// - public abstract class LinkGenerator - { - /// - /// Generates a URI with an absolute path based on the provided values and . - /// - /// The address type. - /// The associated with the current request. - /// The address value. Used to resolve endpoints. - /// The route values. Used to expand parameters in the route template. - /// The values associated with the current request. Optional. - /// - /// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of will be used. - /// - /// An optional URI fragment. Appended to the resulting URI. - /// - /// An optional . Settings on provided object override the settings with matching - /// names from RouteOptions. - /// - /// A URI with an absolute path, or null. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public abstract string? GetPathByAddress( - HttpContext httpContext, - TAddress address, - RouteValueDictionary values, - RouteValueDictionary? ambientValues = default, - PathString? pathBase = default, - FragmentString fragment = default, - LinkOptions? options = default); - - /// - /// Generates a URI with an absolute path based on the provided values. - /// - /// The address type. - /// The address value. Used to resolve endpoints. - /// The route values. Used to expand parameters in the route template. - /// An optional URI path base. Prepended to the path in the resulting URI. - /// An optional URI fragment. Appended to the resulting URI. - /// - /// An optional . Settings on provided object override the settings with matching - /// names from RouteOptions. - /// - /// A URI with an absolute path, or null. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public abstract string? GetPathByAddress( - TAddress address, - RouteValueDictionary values, - PathString pathBase = default, - FragmentString fragment = default, - LinkOptions? options = default); - - /// - /// Generates an absolute URI based on the provided values and . - /// - /// The address type. - /// The associated with the current request. - /// The address value. Used to resolve endpoints. - /// The route values. Used to expand parameters in the route template. - /// The values associated with the current request. Optional. - /// - /// The URI scheme, applied to the resulting URI. Optional. If not provided, the value of will be used. - /// - /// - /// The URI host/authority, applied to the resulting URI. Optional. If not provided, the value will be used. - /// See the remarks section for details about the security implications of the . - /// - /// - /// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of will be used. - /// - /// An optional URI fragment. Appended to the resulting URI. - /// - /// An optional . Settings on provided object override the settings with matching - /// names from RouteOptions. - /// - /// A URI with an absolute path, or null. - /// - /// - /// The value of should be a trusted value. Relying on the value of the current request - /// can allow untrusted input to influence the resulting URI unless the Host header has been validated. - /// See the deployment documentation for instructions on how to properly validate the Host header in - /// your deployment environment. - /// - /// - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public abstract string? GetUriByAddress( - HttpContext httpContext, - TAddress address, - RouteValueDictionary values, - RouteValueDictionary? ambientValues = default, - string? scheme = default, - HostString? host = default, - PathString? pathBase = default, - FragmentString fragment = default, - LinkOptions? options = default); - - /// - /// Generates an absolute URI based on the provided values. - /// - /// The address type. - /// The address value. Used to resolve endpoints. - /// The route values. Used to expand parameters in the route template. - /// The URI scheme, applied to the resulting URI. - /// - /// The URI host/authority, applied to the resulting URI. - /// See the remarks section for details about the security implications of the . - /// - /// An optional URI path base. Prepended to the path in the resulting URI. - /// An optional URI fragment. Appended to the resulting URI. - /// - /// An optional . Settings on provided object override the settings with matching - /// names from RouteOptions. - /// - /// An absolute URI, or null. - /// - /// - /// The value of should be a trusted value. Relying on the value of the current request - /// can allow untrusted input to influence the resulting URI unless the Host header has been validated. - /// See the deployment documentation for instructions on how to properly validate the Host header in - /// your deployment environment. - /// - /// - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public abstract string? GetUriByAddress( - TAddress address, - RouteValueDictionary values, - string? scheme, - HostString host, - PathString pathBase = default, - FragmentString fragment = default, - LinkOptions? options = default); - } + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public abstract string? GetUriByAddress( + TAddress address, + RouteValueDictionary values, + string? scheme, + HostString host, + PathString pathBase = default, + FragmentString fragment = default, + LinkOptions? options = default); } diff --git a/src/Http/Routing.Abstractions/src/LinkOptions.cs b/src/Http/Routing.Abstractions/src/LinkOptions.cs index 1c7e5f2165..83d5ec8062 100644 --- a/src/Http/Routing.Abstractions/src/LinkOptions.cs +++ b/src/Http/Routing.Abstractions/src/LinkOptions.cs @@ -1,28 +1,27 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Configures options for generated URLs. +/// +public class LinkOptions { /// - /// Configures options for generated URLs. + /// Gets or sets a value indicating whether all generated paths URLs are lowercase. + /// Use to configure the behavior for query strings. /// - public class LinkOptions - { - /// - /// Gets or sets a value indicating whether all generated paths URLs are lowercase. - /// Use to configure the behavior for query strings. - /// - public bool? LowercaseUrls { get; set; } + public bool? LowercaseUrls { get; set; } - /// - /// Gets or sets a value indicating whether a generated query strings are lowercase. - /// This property will be false unless is also true. - /// - public bool? LowercaseQueryStrings { get; set; } + /// + /// Gets or sets a value indicating whether a generated query strings are lowercase. + /// This property will be false unless is also true. + /// + public bool? LowercaseQueryStrings { get; set; } - /// - /// Gets or sets a value indicating whether a trailing slash should be appended to the generated URLs. - /// - public bool? AppendTrailingSlash { get; set; } - } + /// + /// Gets or sets a value indicating whether a trailing slash should be appended to the generated URLs. + /// + public bool? AppendTrailingSlash { get; set; } } diff --git a/src/Http/Routing.Abstractions/src/Properties/AssemblyInfo.cs b/src/Http/Routing.Abstractions/src/Properties/AssemblyInfo.cs index b0ee28d60e..bd79df6b37 100644 --- a/src/Http/Routing.Abstractions/src/Properties/AssemblyInfo.cs +++ b/src/Http/Routing.Abstractions/src/Properties/AssemblyInfo.cs @@ -2,9 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.CompilerServices; -using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing; [assembly: TypeForwardedTo(typeof(IEndpointFeature))] [assembly: TypeForwardedTo(typeof(IRouteValuesFeature))] diff --git a/src/Http/Routing.Abstractions/src/RouteContext.cs b/src/Http/Routing.Abstractions/src/RouteContext.cs index 5bf51a0a00..09a4c4dd0c 100644 --- a/src/Http/Routing.Abstractions/src/RouteContext.cs +++ b/src/Http/Routing.Abstractions/src/RouteContext.cs @@ -4,55 +4,54 @@ using System; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// A context object for . +/// +public class RouteContext { + private RouteData _routeData; + /// - /// A context object for . + /// Creates a new instance of for the provided . /// - public class RouteContext + /// The associated with the current request. + public RouteContext(HttpContext httpContext) { - private RouteData _routeData; + HttpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); - /// - /// Creates a new instance of for the provided . - /// - /// The associated with the current request. - public RouteContext(HttpContext httpContext) - { - HttpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); + RouteData = new RouteData(); + } - RouteData = new RouteData(); - } + /// + /// Gets or sets the handler for the request. An should set + /// when it matches. + /// + public RequestDelegate? Handler { get; set; } - /// - /// Gets or sets the handler for the request. An should set - /// when it matches. - /// - public RequestDelegate? Handler { get; set; } - - /// - /// Gets the associated with the current request. - /// - public HttpContext HttpContext { get; } - - /// - /// Gets or sets the associated with the current context. - /// - public RouteData RouteData + /// + /// Gets the associated with the current request. + /// + public HttpContext HttpContext { get; } + + /// + /// Gets or sets the associated with the current context. + /// + public RouteData RouteData + { + get { - get + return _routeData; + } + set + { + if (value == null) { - return _routeData; + throw new ArgumentNullException(nameof(RouteData)); } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(RouteData)); - } - _routeData = value; - } + _routeData = value; } } } diff --git a/src/Http/Routing.Abstractions/src/RouteData.cs b/src/Http/Routing.Abstractions/src/RouteData.cs index 3cd807f85f..cd1a4df2fe 100644 --- a/src/Http/Routing.Abstractions/src/RouteData.cs +++ b/src/Http/Routing.Abstractions/src/RouteData.cs @@ -7,307 +7,306 @@ using System; using System.Collections.Generic; using System.Diagnostics; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Information about the current routing path. +/// +public class RouteData { + private RouteValueDictionary? _dataTokens; + private List? _routers; + private RouteValueDictionary? _values; + /// - /// Information about the current routing path. + /// Creates a new instance of instance. /// - public class RouteData + public RouteData() { - private RouteValueDictionary? _dataTokens; - private List? _routers; - private RouteValueDictionary? _values; + // Perf: Avoid allocating collections unless needed. + } - /// - /// Creates a new instance of instance. - /// - public RouteData() + /// + /// Creates a new instance of instance with values copied from . + /// + /// The other instance to copy. + public RouteData(RouteData other) + { + if (other == null) { - // Perf: Avoid allocating collections unless needed. + throw new ArgumentNullException(nameof(other)); } - /// - /// Creates a new instance of instance with values copied from . - /// - /// The other instance to copy. - public RouteData(RouteData other) + // Perf: Avoid allocating collections unless we need to make a copy. + if (other._routers != null) { - if (other == null) - { - throw new ArgumentNullException(nameof(other)); - } + _routers = new List(other.Routers); + } - // Perf: Avoid allocating collections unless we need to make a copy. - if (other._routers != null) + if (other._dataTokens != null) + { + _dataTokens = new RouteValueDictionary(other._dataTokens); + } + + if (other._values != null) + { + _values = new RouteValueDictionary(other._values); + } + } + + /// + /// Creates a new instance of instance with the specified values. + /// + /// The values. + public RouteData(RouteValueDictionary values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + _values = values; + } + + /// + /// Gets the data tokens produced by routes on the current routing path. + /// + public RouteValueDictionary DataTokens + { + get + { + if (_dataTokens == null) { - _routers = new List(other.Routers); + _dataTokens = new RouteValueDictionary(); } - if (other._dataTokens != null) + return _dataTokens; + } + } + + /// + /// Gets the list of instances on the current routing path. + /// + public IList Routers + { + get + { + if (_routers == null) { - _dataTokens = new RouteValueDictionary(other._dataTokens); + _routers = new List(); } - if (other._values != null) + return _routers; + } + } + + /// + /// Gets the values produced by routes on the current routing path. + /// + public RouteValueDictionary Values + { + get + { + if (_values == null) { - _values = new RouteValueDictionary(other._values); + _values = new RouteValueDictionary(); } + + return _values; } + } - /// - /// Creates a new instance of instance with the specified values. - /// - /// The values. - public RouteData(RouteValueDictionary values) + /// + /// + /// Creates a snapshot of the current state of the before appending + /// to , merging into + /// , and merging into . + /// + /// + /// Call to restore the state of this + /// to the state at the time of calling + /// . + /// + /// + /// + /// An to append to . If null, then + /// will not be changed. + /// + /// + /// A to merge into . If null, then + /// will not be changed. + /// + /// + /// A to merge into . If null, then + /// will not be changed. + /// + /// A that captures the current state. + public RouteDataSnapshot PushState(IRouter? router, RouteValueDictionary? values, RouteValueDictionary? dataTokens) + { + // Perf: this is optimized for small list sizes, in particular to avoid overhead of a native call in + // Array.CopyTo inside the List(IEnumerable) constructor. + List? routers = null; + var count = _routers?.Count; + if (count > 0) { - if (values == null) + Debug.Assert(_routers != null); + + routers = new List(count.Value); + for (var i = 0; i < count.Value; i++) { - throw new ArgumentNullException(nameof(values)); + routers.Add(_routers[i]); } + } - _values = values; + var snapshot = new RouteDataSnapshot( + this, + _dataTokens?.Count > 0 ? new RouteValueDictionary(_dataTokens) : null, + routers, + _values?.Count > 0 ? new RouteValueDictionary(_values) : null); + + if (router != null) + { + Routers.Add(router); } - /// - /// Gets the data tokens produced by routes on the current routing path. - /// - public RouteValueDictionary DataTokens + if (values != null) { - get + foreach (var kvp in values) { - if (_dataTokens == null) + if (kvp.Value != null) { - _dataTokens = new RouteValueDictionary(); + Values[kvp.Key] = kvp.Value; } - - return _dataTokens; } } - /// - /// Gets the list of instances on the current routing path. - /// - public IList Routers + if (dataTokens != null) { - get + foreach (var kvp in dataTokens) { - if (_routers == null) - { - _routers = new List(); - } - - return _routers; + DataTokens[kvp.Key] = kvp.Value; } } + return snapshot; + } + + /// + /// A snapshot of the state of a instance. + /// + public readonly struct RouteDataSnapshot + { + private readonly RouteData _routeData; + private readonly RouteValueDictionary? _dataTokens; + private readonly IList? _routers; + private readonly RouteValueDictionary? _values; + /// - /// Gets the values produced by routes on the current routing path. + /// Creates a new instance of for . /// - public RouteValueDictionary Values + /// The . + /// The data tokens. + /// The routers. + /// The route values. + public RouteDataSnapshot( + RouteData routeData, + RouteValueDictionary? dataTokens, + IList? routers, + RouteValueDictionary? values) { - get + if (routeData == null) { - if (_values == null) - { - _values = new RouteValueDictionary(); - } - - return _values; + throw new ArgumentNullException(nameof(routeData)); } + + _routeData = routeData; + _dataTokens = dataTokens; + _routers = routers; + _values = values; } /// - /// - /// Creates a snapshot of the current state of the before appending - /// to , merging into - /// , and merging into . - /// - /// - /// Call to restore the state of this - /// to the state at the time of calling - /// . - /// + /// Restores the to the captured state. /// - /// - /// An to append to . If null, then - /// will not be changed. - /// - /// - /// A to merge into . If null, then - /// will not be changed. - /// - /// - /// A to merge into . If null, then - /// will not be changed. - /// - /// A that captures the current state. - public RouteDataSnapshot PushState(IRouter? router, RouteValueDictionary? values, RouteValueDictionary? dataTokens) + public void Restore() { - // Perf: this is optimized for small list sizes, in particular to avoid overhead of a native call in - // Array.CopyTo inside the List(IEnumerable) constructor. - List? routers = null; - var count = _routers?.Count; - if (count > 0) + if (_routeData._dataTokens == null && _dataTokens == null) { - Debug.Assert(_routers != null); - - routers = new List(count.Value); - for (var i = 0; i < count.Value; i++) - { - routers.Add(_routers[i]); - } + // Do nothing } - - var snapshot = new RouteDataSnapshot( - this, - _dataTokens?.Count > 0 ? new RouteValueDictionary(_dataTokens) : null, - routers, - _values?.Count > 0 ? new RouteValueDictionary(_values) : null); - - if (router != null) + else if (_dataTokens == null) { - Routers.Add(router); + _routeData._dataTokens!.Clear(); } - - if (values != null) + else { - foreach (var kvp in values) + _routeData._dataTokens!.Clear(); + + foreach (var kvp in _dataTokens) { - if (kvp.Value != null) - { - Values[kvp.Key] = kvp.Value; - } + _routeData._dataTokens.Add(kvp.Key, kvp.Value); } } - if (dataTokens != null) + if (_routeData._routers == null && _routers == null) { - foreach (var kvp in dataTokens) - { - DataTokens[kvp.Key] = kvp.Value; - } + // Do nothing } - - return snapshot; - } - - /// - /// A snapshot of the state of a instance. - /// - public readonly struct RouteDataSnapshot - { - private readonly RouteData _routeData; - private readonly RouteValueDictionary? _dataTokens; - private readonly IList? _routers; - private readonly RouteValueDictionary? _values; - - /// - /// Creates a new instance of for . - /// - /// The . - /// The data tokens. - /// The routers. - /// The route values. - public RouteDataSnapshot( - RouteData routeData, - RouteValueDictionary? dataTokens, - IList? routers, - RouteValueDictionary? values) + else if (_routers == null) { - if (routeData == null) + // Perf: this is optimized for small list sizes, in particular to avoid overhead of a native call in + // Array.Clear inside the List.Clear() method. + var routers = _routeData._routers!; + for (var i = routers.Count - 1; i >= 0; i--) { - throw new ArgumentNullException(nameof(routeData)); + routers.RemoveAt(i); } - - _routeData = routeData; - _dataTokens = dataTokens; - _routers = routers; - _values = values; } - - /// - /// Restores the to the captured state. - /// - public void Restore() + else { - if (_routeData._dataTokens == null && _dataTokens == null) - { - // Do nothing - } - else if (_dataTokens == null) + // Perf: this is optimized for small list sizes, in particular to avoid overhead of a native call in + // Array.Clear inside the List.Clear() method. + // + // We want to basically copy the contents of _routers in _routeData._routers - this change does + // that with the minimal number of reads/writes and without calling Clear(). + var routers = _routeData._routers!; + var snapshotRouters = _routers; + + // This is made more complicated by the fact that List[int] throws if i == Count, so we have + // to do two loops and call Add for those cases. + var i = 0; + for (; i < snapshotRouters.Count && i < routers.Count; i++) { - _routeData._dataTokens!.Clear(); + routers[i] = snapshotRouters[i]; } - else - { - _routeData._dataTokens!.Clear(); - foreach (var kvp in _dataTokens) - { - _routeData._dataTokens.Add(kvp.Key, kvp.Value); - } - } - - if (_routeData._routers == null && _routers == null) - { - // Do nothing - } - else if (_routers == null) - { - // Perf: this is optimized for small list sizes, in particular to avoid overhead of a native call in - // Array.Clear inside the List.Clear() method. - var routers = _routeData._routers!; - for (var i = routers.Count - 1; i >= 0 ; i--) - { - routers.RemoveAt(i); - } - } - else + for (; i < snapshotRouters.Count; i++) { - // Perf: this is optimized for small list sizes, in particular to avoid overhead of a native call in - // Array.Clear inside the List.Clear() method. - // - // We want to basically copy the contents of _routers in _routeData._routers - this change does - // that with the minimal number of reads/writes and without calling Clear(). - var routers = _routeData._routers!; - var snapshotRouters = _routers; - - // This is made more complicated by the fact that List[int] throws if i == Count, so we have - // to do two loops and call Add for those cases. - var i = 0; - for (; i < snapshotRouters.Count && i < routers.Count; i++) - { - routers[i] = snapshotRouters[i]; - } - - for (; i < snapshotRouters.Count; i++) - { - routers.Add(snapshotRouters[i]); - } - - // Trim excess - again avoiding RemoveRange because it uses native methods. - for (i = routers.Count - 1; i >= snapshotRouters.Count; i--) - { - routers.RemoveAt(i); - } + routers.Add(snapshotRouters[i]); } - if (_routeData._values == null && _values == null) + // Trim excess - again avoiding RemoveRange because it uses native methods. + for (i = routers.Count - 1; i >= snapshotRouters.Count; i--) { - // Do nothing + routers.RemoveAt(i); } - else if (_values == null) - { - _routeData._values!.Clear(); - } - else - { - _routeData._values!.Clear(); + } - foreach (var kvp in _values) - { - _routeData._values.Add(kvp.Key, kvp.Value); - } + if (_routeData._values == null && _values == null) + { + // Do nothing + } + else if (_values == null) + { + _routeData._values!.Clear(); + } + else + { + _routeData._values!.Clear(); + + foreach (var kvp in _values) + { + _routeData._values.Add(kvp.Key, kvp.Value); } } } diff --git a/src/Http/Routing.Abstractions/src/RouteDirection.cs b/src/Http/Routing.Abstractions/src/RouteDirection.cs index cc98d09d6b..bf5629c174 100644 --- a/src/Http/Routing.Abstractions/src/RouteDirection.cs +++ b/src/Http/Routing.Abstractions/src/RouteDirection.cs @@ -1,21 +1,20 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Indicates whether ASP.NET routing is processing a URL from an HTTP request or generating a URL. +/// +public enum RouteDirection { /// - /// Indicates whether ASP.NET routing is processing a URL from an HTTP request or generating a URL. + /// A URL from a client is being processed. /// - public enum RouteDirection - { - /// - /// A URL from a client is being processed. - /// - IncomingRequest, + IncomingRequest, - /// - /// A URL is being created based on the route definition. - /// - UrlGeneration, - } + /// + /// A URL is being created based on the route definition. + /// + UrlGeneration, } diff --git a/src/Http/Routing.Abstractions/src/RoutingHttpContextExtensions.cs b/src/Http/Routing.Abstractions/src/RoutingHttpContextExtensions.cs index 9c613e077d..c23cd467ec 100644 --- a/src/Http/Routing.Abstractions/src/RoutingHttpContextExtensions.cs +++ b/src/Http/Routing.Abstractions/src/RoutingHttpContextExtensions.cs @@ -7,49 +7,48 @@ using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Extension methods for related to routing. +/// +public static class RoutingHttpContextExtensions { /// - /// Extension methods for related to routing. + /// Gets the associated with the provided . /// - public static class RoutingHttpContextExtensions + /// The associated with the current request. + /// The . + public static RouteData GetRouteData(this HttpContext httpContext) { - /// - /// Gets the associated with the provided . - /// - /// The associated with the current request. - /// The . - public static RouteData GetRouteData(this HttpContext httpContext) + if (httpContext == null) { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - var routingFeature = httpContext.Features.Get(); - return routingFeature?.RouteData ?? new RouteData(httpContext.Request.RouteValues); + throw new ArgumentNullException(nameof(httpContext)); } - /// - /// Gets a route value from associated with the provided - /// . - /// - /// The associated with the current request. - /// The key of the route value. - /// The corresponding route value, or null. - public static object? GetRouteValue(this HttpContext httpContext, string key) - { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } + var routingFeature = httpContext.Features.Get(); + return routingFeature?.RouteData ?? new RouteData(httpContext.Request.RouteValues); + } - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } + /// + /// Gets a route value from associated with the provided + /// . + /// + /// The associated with the current request. + /// The key of the route value. + /// The corresponding route value, or null. + public static object? GetRouteValue(this HttpContext httpContext, string key) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } - return httpContext.Features.Get()?.RouteValues[key]; + if (key == null) + { + throw new ArgumentNullException(nameof(key)); } + + return httpContext.Features.Get()?.RouteValues[key]; } } diff --git a/src/Http/Routing.Abstractions/src/VirtualPathContext.cs b/src/Http/Routing.Abstractions/src/VirtualPathContext.cs index a296a048ce..5259f71042 100644 --- a/src/Http/Routing.Abstractions/src/VirtualPathContext.cs +++ b/src/Http/Routing.Abstractions/src/VirtualPathContext.cs @@ -3,64 +3,63 @@ using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// A context for virtual path generation operations. +/// +public class VirtualPathContext { /// - /// A context for virtual path generation operations. + /// Creates a new instance of . /// - public class VirtualPathContext + /// The associated with the current request. + /// The set of route values associated with the current request. + /// The set of new values provided for virtual path generation. + public VirtualPathContext( + HttpContext httpContext, + RouteValueDictionary ambientValues, + RouteValueDictionary values) + : this(httpContext, ambientValues, values, null) { - /// - /// Creates a new instance of . - /// - /// The associated with the current request. - /// The set of route values associated with the current request. - /// The set of new values provided for virtual path generation. - public VirtualPathContext( - HttpContext httpContext, - RouteValueDictionary ambientValues, - RouteValueDictionary values) - : this(httpContext, ambientValues, values, null) - { - } + } - /// - /// Creates a new instance of . - /// - /// The associated with the current request. - /// The set of route values associated with the current request. - /// The set of new values provided for virtual path generation. - /// The name of the route to use for virtual path generation. - public VirtualPathContext( - HttpContext httpContext, - RouteValueDictionary ambientValues, - RouteValueDictionary values, - string? routeName) - { - HttpContext = httpContext; - AmbientValues = ambientValues; - Values = values; - RouteName = routeName; - } + /// + /// Creates a new instance of . + /// + /// The associated with the current request. + /// The set of route values associated with the current request. + /// The set of new values provided for virtual path generation. + /// The name of the route to use for virtual path generation. + public VirtualPathContext( + HttpContext httpContext, + RouteValueDictionary ambientValues, + RouteValueDictionary values, + string? routeName) + { + HttpContext = httpContext; + AmbientValues = ambientValues; + Values = values; + RouteName = routeName; + } - /// - /// Gets the set of route values associated with the current request. - /// - public RouteValueDictionary AmbientValues { get; } + /// + /// Gets the set of route values associated with the current request. + /// + public RouteValueDictionary AmbientValues { get; } - /// - /// Gets the associated with the current request. - /// - public HttpContext HttpContext { get; } + /// + /// Gets the associated with the current request. + /// + public HttpContext HttpContext { get; } - /// - /// Gets the name of the route to use for virtual path generation. - /// - public string? RouteName { get; } + /// + /// Gets the name of the route to use for virtual path generation. + /// + public string? RouteName { get; } - /// - /// Gets or sets the set of new values provided for virtual path generation. - /// - public RouteValueDictionary Values { get; set; } - } + /// + /// Gets or sets the set of new values provided for virtual path generation. + /// + public RouteValueDictionary Values { get; set; } } diff --git a/src/Http/Routing.Abstractions/src/VirtualPathData.cs b/src/Http/Routing.Abstractions/src/VirtualPathData.cs index a2f042ce58..c1b3c97d55 100644 --- a/src/Http/Routing.Abstractions/src/VirtualPathData.cs +++ b/src/Http/Routing.Abstractions/src/VirtualPathData.cs @@ -3,97 +3,96 @@ using System; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Represents information about the route and virtual path that are the result of +/// generating a URL with the ASP.NET routing middleware. +/// +public class VirtualPathData { + private RouteValueDictionary _dataTokens; + private string _virtualPath; + /// - /// Represents information about the route and virtual path that are the result of - /// generating a URL with the ASP.NET routing middleware. + /// Initializes a new instance of the class. /// - public class VirtualPathData + /// The object that is used to generate the URL. + /// The generated URL. + public VirtualPathData(IRouter router, string virtualPath) + : this(router, virtualPath, dataTokens: null) { - private RouteValueDictionary _dataTokens; - private string _virtualPath; + } - /// - /// Initializes a new instance of the class. - /// - /// The object that is used to generate the URL. - /// The generated URL. - public VirtualPathData(IRouter router, string virtualPath) - : this(router, virtualPath, dataTokens: null) + /// + /// Initializes a new instance of the class. + /// + /// The object that is used to generate the URL. + /// The generated URL. + /// The collection of custom values. + public VirtualPathData( + IRouter router, + string virtualPath, + RouteValueDictionary dataTokens) + { + if (router == null) { + throw new ArgumentNullException(nameof(router)); } - /// - /// Initializes a new instance of the class. - /// - /// The object that is used to generate the URL. - /// The generated URL. - /// The collection of custom values. - public VirtualPathData( - IRouter router, - string virtualPath, - RouteValueDictionary dataTokens) + Router = router; + VirtualPath = virtualPath; + _dataTokens = dataTokens == null ? null : new RouteValueDictionary(dataTokens); + } + + /// + /// Gets the collection of custom values for the . + /// + public RouteValueDictionary DataTokens + { + get { - if (router == null) + if (_dataTokens == null) { - throw new ArgumentNullException(nameof(router)); + _dataTokens = new RouteValueDictionary(); } - Router = router; - VirtualPath = virtualPath; - _dataTokens = dataTokens == null ? null : new RouteValueDictionary(dataTokens); + return _dataTokens; } + } - /// - /// Gets the collection of custom values for the . - /// - public RouteValueDictionary DataTokens - { - get - { - if (_dataTokens == null) - { - _dataTokens = new RouteValueDictionary(); - } + /// + /// Gets or sets the that was used to generate the URL. + /// + public IRouter Router { get; set; } - return _dataTokens; - } + /// + /// Gets or sets the URL that was generated from the . + /// + public string VirtualPath + { + get + { + return _virtualPath; } - - /// - /// Gets or sets the that was used to generate the URL. - /// - public IRouter Router { get; set; } - - /// - /// Gets or sets the URL that was generated from the . - /// - public string VirtualPath + set { - get - { - return _virtualPath; - } - set - { - _virtualPath = NormalizePath(value); - } + _virtualPath = NormalizePath(value); } + } - private static string NormalizePath(string path) + private static string NormalizePath(string path) + { + if (string.IsNullOrEmpty(path)) { - if (string.IsNullOrEmpty(path)) - { - return string.Empty; - } - - if (!path.StartsWith("/", StringComparison.Ordinal)) - { - return "/" + path; - } + return string.Empty; + } - return path; + if (!path.StartsWith("/", StringComparison.Ordinal)) + { + return "/" + path; } + + return path; } -} \ No newline at end of file +} diff --git a/src/Http/Routing.Abstractions/test/RouteDataTest.cs b/src/Http/Routing.Abstractions/test/RouteDataTest.cs index 8b1da0edf8..825cb2ad90 100644 --- a/src/Http/Routing.Abstractions/test/RouteDataTest.cs +++ b/src/Http/Routing.Abstractions/test/RouteDataTest.cs @@ -4,154 +4,153 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class RouteDataTest { - public class RouteDataTest + [Fact] + public void RouteData_DefaultPropertyValues() + { + // Arrange & Act + var routeData = new RouteData(); + + // Assert + Assert.Empty(routeData.DataTokens); + Assert.Empty(routeData.Routers); + Assert.Empty(routeData.Values); + } + + [Fact] + public void RouteData_CopyConstructor() + { + // Arrange & Act + var original = new RouteData(); + + original.DataTokens.Add("data", "token"); + original.Routers.Add(Mock.Of()); + original.Values.Add("route", "value"); + + var routeData = new RouteData(original); + + // Assert + Assert.NotSame(routeData.DataTokens, original.DataTokens); + Assert.Equal(routeData.DataTokens, original.DataTokens); + Assert.NotSame(routeData.Routers, original.Routers); + Assert.Equal(routeData.Routers, original.Routers); + Assert.NotSame(routeData.Values, original.Values); + Assert.Equal(routeData.Values, original.Values); + } + + [Fact] + public void RouteData_PushStateAndRestore_NullValues() + { + // Arrange + var routeData = new RouteData(); + + // Act + var snapshot = routeData.PushState(null, null, null); + var copy = new RouteData(routeData); + snapshot.Restore(); + + // Assert + Assert.Equal(routeData.DataTokens, copy.DataTokens); + Assert.Equal(routeData.Routers, copy.Routers); + Assert.Equal(routeData.Values, copy.Values); + } + + [Fact] + public void RouteData_PushStateAndRestore_EmptyValues() + { + // Arrange + var routeData = new RouteData(); + + // Act + var snapshot = routeData.PushState(null, new RouteValueDictionary(), new RouteValueDictionary()); + var copy = new RouteData(routeData); + snapshot.Restore(); + + // Assert + Assert.Equal(routeData.DataTokens, copy.DataTokens); + Assert.Equal(routeData.Routers, copy.Routers); + Assert.Equal(routeData.Values, copy.Values); + } + + // This is an important semantic for catchall parameters. A null route value shouldn't be + // merged. + [Fact] + public void RouteData_PushStateAndRestore_NullRouteValueNotSet() + { + // Arrange + var original = new RouteData(); + original.Values.Add("bleh", "16"); + + var routeData = new RouteData(original); + + // Act + var snapshot = routeData.PushState( + null, + new RouteValueDictionary(new { bleh = (string)null }), + new RouteValueDictionary()); + snapshot.Restore(); + + // Assert + Assert.Equal(routeData.Values, original.Values); + } + + [Fact] + public void RouteData_PushStateAndThenModify() + { + // Arrange + var routeData = new RouteData(); + + // Act + var snapshot = routeData.PushState(null, null, null); + routeData.DataTokens.Add("data", "token"); + routeData.Routers.Add(Mock.Of()); + routeData.Values.Add("route", "value"); + + var copy = new RouteData(routeData); + snapshot.Restore(); + + // Assert + Assert.Empty(routeData.DataTokens); + Assert.NotEqual(routeData.DataTokens, copy.DataTokens); + Assert.Empty(routeData.Routers); + Assert.NotEqual(routeData.Routers, copy.Routers); + Assert.Empty(routeData.Values); + Assert.NotEqual(routeData.Values, copy.Values); + } + + [Fact] + public void RouteData_PushStateAndThenModify_WithInitialData() { - [Fact] - public void RouteData_DefaultPropertyValues() - { - // Arrange & Act - var routeData = new RouteData(); - - // Assert - Assert.Empty(routeData.DataTokens); - Assert.Empty(routeData.Routers); - Assert.Empty(routeData.Values); - } - - [Fact] - public void RouteData_CopyConstructor() - { - // Arrange & Act - var original = new RouteData(); - - original.DataTokens.Add("data", "token"); - original.Routers.Add(Mock.Of()); - original.Values.Add("route", "value"); - - var routeData = new RouteData(original); - - // Assert - Assert.NotSame(routeData.DataTokens, original.DataTokens); - Assert.Equal(routeData.DataTokens, original.DataTokens); - Assert.NotSame(routeData.Routers, original.Routers); - Assert.Equal(routeData.Routers, original.Routers); - Assert.NotSame(routeData.Values, original.Values); - Assert.Equal(routeData.Values, original.Values); - } - - [Fact] - public void RouteData_PushStateAndRestore_NullValues() - { - // Arrange - var routeData = new RouteData(); - - // Act - var snapshot = routeData.PushState(null, null, null); - var copy = new RouteData(routeData); - snapshot.Restore(); - - // Assert - Assert.Equal(routeData.DataTokens, copy.DataTokens); - Assert.Equal(routeData.Routers, copy.Routers); - Assert.Equal(routeData.Values, copy.Values); - } - - [Fact] - public void RouteData_PushStateAndRestore_EmptyValues() - { - // Arrange - var routeData = new RouteData(); - - // Act - var snapshot = routeData.PushState(null, new RouteValueDictionary(), new RouteValueDictionary()); - var copy = new RouteData(routeData); - snapshot.Restore(); - - // Assert - Assert.Equal(routeData.DataTokens, copy.DataTokens); - Assert.Equal(routeData.Routers, copy.Routers); - Assert.Equal(routeData.Values, copy.Values); - } - - // This is an important semantic for catchall parameters. A null route value shouldn't be - // merged. - [Fact] - public void RouteData_PushStateAndRestore_NullRouteValueNotSet() - { - // Arrange - var original = new RouteData(); - original.Values.Add("bleh", "16"); - - var routeData = new RouteData(original); - - // Act - var snapshot = routeData.PushState( - null, - new RouteValueDictionary(new { bleh = (string)null }), - new RouteValueDictionary()); - snapshot.Restore(); - - // Assert - Assert.Equal(routeData.Values, original.Values); - } - - [Fact] - public void RouteData_PushStateAndThenModify() - { - // Arrange - var routeData = new RouteData(); - - // Act - var snapshot = routeData.PushState(null, null, null); - routeData.DataTokens.Add("data", "token"); - routeData.Routers.Add(Mock.Of()); - routeData.Values.Add("route", "value"); - - var copy = new RouteData(routeData); - snapshot.Restore(); - - // Assert - Assert.Empty(routeData.DataTokens); - Assert.NotEqual(routeData.DataTokens, copy.DataTokens); - Assert.Empty(routeData.Routers); - Assert.NotEqual(routeData.Routers, copy.Routers); - Assert.Empty(routeData.Values); - Assert.NotEqual(routeData.Values, copy.Values); - } - - [Fact] - public void RouteData_PushStateAndThenModify_WithInitialData() - { - // Arrange - var original = new RouteData(); - original.DataTokens.Add("data", "token1"); - original.Routers.Add(Mock.Of()); - original.Values.Add("route", "value1"); - - var routeData = new RouteData(original); - - // Act - var snapshot = routeData.PushState( - Mock.Of(), - new RouteValueDictionary(new { route = "value2" }), - new RouteValueDictionary(new { data = "token2" })); - - routeData.DataTokens.Add("data2", "token"); - routeData.Routers.Add(Mock.Of()); - routeData.Values.Add("route2", "value"); - - var copy = new RouteData(routeData); - snapshot.Restore(); - - // Assert - Assert.Equal(original.DataTokens, routeData.DataTokens); - Assert.NotEqual(routeData.DataTokens, copy.DataTokens); - Assert.Equal(original.Routers, routeData.Routers); - Assert.NotEqual(routeData.Routers, copy.Routers); - Assert.Equal(original.Values, routeData.Values); - Assert.NotEqual(routeData.Values, copy.Values); - } + // Arrange + var original = new RouteData(); + original.DataTokens.Add("data", "token1"); + original.Routers.Add(Mock.Of()); + original.Values.Add("route", "value1"); + + var routeData = new RouteData(original); + + // Act + var snapshot = routeData.PushState( + Mock.Of(), + new RouteValueDictionary(new { route = "value2" }), + new RouteValueDictionary(new { data = "token2" })); + + routeData.DataTokens.Add("data2", "token"); + routeData.Routers.Add(Mock.Of()); + routeData.Values.Add("route2", "value"); + + var copy = new RouteData(routeData); + snapshot.Restore(); + + // Assert + Assert.Equal(original.DataTokens, routeData.DataTokens); + Assert.NotEqual(routeData.DataTokens, copy.DataTokens); + Assert.Equal(original.Routers, routeData.Routers); + Assert.NotEqual(routeData.Routers, copy.Routers); + Assert.Equal(original.Values, routeData.Values); + Assert.NotEqual(routeData.Values, copy.Values); } } diff --git a/src/Http/Routing.Abstractions/test/VirtualPathDataTests.cs b/src/Http/Routing.Abstractions/test/VirtualPathDataTests.cs index 5312fdd52e..ebbf2200d4 100644 --- a/src/Http/Routing.Abstractions/test/VirtualPathDataTests.cs +++ b/src/Http/Routing.Abstractions/test/VirtualPathDataTests.cs @@ -4,62 +4,61 @@ using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class VirtualPathDataTests { - public class VirtualPathDataTests + [Fact] + public void Constructor_CreatesEmptyDataTokensIfNull() { - [Fact] - public void Constructor_CreatesEmptyDataTokensIfNull() - { - // Arrange - var router = Mock.Of(); - var path = "/virtual path"; + // Arrange + var router = Mock.Of(); + var path = "/virtual path"; - // Act - var pathData = new VirtualPathData(router, path, null); + // Act + var pathData = new VirtualPathData(router, path, null); - // Assert - Assert.Same(router, pathData.Router); - Assert.Equal(path, pathData.VirtualPath); - Assert.NotNull(pathData.DataTokens); - Assert.Empty(pathData.DataTokens); - } + // Assert + Assert.Same(router, pathData.Router); + Assert.Equal(path, pathData.VirtualPath); + Assert.NotNull(pathData.DataTokens); + Assert.Empty(pathData.DataTokens); + } - [Fact] - public void Constructor_CopiesDataTokens() - { - // Arrange - var router = Mock.Of(); - var path = "/virtual path"; - var dataTokens = new RouteValueDictionary(); - dataTokens["TestKey"] = "TestValue"; + [Fact] + public void Constructor_CopiesDataTokens() + { + // Arrange + var router = Mock.Of(); + var path = "/virtual path"; + var dataTokens = new RouteValueDictionary(); + dataTokens["TestKey"] = "TestValue"; - // Act - var pathData = new VirtualPathData(router, path, dataTokens); + // Act + var pathData = new VirtualPathData(router, path, dataTokens); - // Assert - Assert.Same(router, pathData.Router); - Assert.Equal(path, pathData.VirtualPath); - Assert.NotNull(pathData.DataTokens); - Assert.Equal("TestValue", pathData.DataTokens["TestKey"]); - Assert.Single(pathData.DataTokens); - Assert.NotSame(dataTokens, pathData.DataTokens); - } + // Assert + Assert.Same(router, pathData.Router); + Assert.Equal(path, pathData.VirtualPath); + Assert.NotNull(pathData.DataTokens); + Assert.Equal("TestValue", pathData.DataTokens["TestKey"]); + Assert.Single(pathData.DataTokens); + Assert.NotSame(dataTokens, pathData.DataTokens); + } - [Fact] - public void VirtualPath_ReturnsEmptyStringIfNull() - { - // Arrange - var router = Mock.Of(); + [Fact] + public void VirtualPath_ReturnsEmptyStringIfNull() + { + // Arrange + var router = Mock.Of(); - // Act - var pathData = new VirtualPathData(router, virtualPath: null); + // Act + var pathData = new VirtualPathData(router, virtualPath: null); - // Assert - Assert.Same(router, pathData.Router); - Assert.Empty(pathData.VirtualPath); - Assert.NotNull(pathData.DataTokens); - Assert.Empty(pathData.DataTokens); - } + // Assert + Assert.Same(router, pathData.Router); + Assert.Empty(pathData.VirtualPath); + Assert.NotNull(pathData.DataTokens); + Assert.Empty(pathData.DataTokens); } } diff --git a/src/Http/Routing/perf/Microbenchmarks/EndpointMetadataCollectionBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/EndpointMetadataCollectionBenchmark.cs index 8c67f50f2e..0a5c918ea3 100644 --- a/src/Http/Routing/perf/Microbenchmarks/EndpointMetadataCollectionBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/EndpointMetadataCollectionBenchmark.cs @@ -5,21 +5,21 @@ using System; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class EndpointMetadataCollectionBenchmark { - public class EndpointMetadataCollectionBenchmark - { - private object[] _items; - private EndpointMetadataCollection _collection; + private object[] _items; + private EndpointMetadataCollection _collection; - [Params(3, 10, 25)] - public int Count { get; set; } + [Params(3, 10, 25)] + public int Count { get; set; } - [GlobalSetup] - public void Setup() + [GlobalSetup] + public void Setup() + { + var seeds = new Type[] { - var seeds = new Type[] - { typeof(Metadata1), typeof(Metadata2), typeof(Metadata3), @@ -29,100 +29,99 @@ namespace Microsoft.AspNetCore.Routing typeof(Metadata7), typeof(Metadata8), typeof(Metadata9), - }; + }; + + _items = new object[Count]; + for (var i = 0; i < _items.Length; i++) + { + _items[i] = seeds[i % seeds.Length]; + } + + _collection = new EndpointMetadataCollection(_items); + } + + // This is a synthetic baseline that visits each item and does an as-cast. + [Benchmark(Baseline = true, OperationsPerInvoke = 5)] + public void Baseline() + { + var items = _items; + for (var i = items.Length - 1; i >= 0; i--) + { + GC.KeepAlive(_items[i] as IMetadata1); + } + + for (var i = items.Length - 1; i >= 0; i--) + { + GC.KeepAlive(_items[i] as IMetadata2); + } + + for (var i = items.Length - 1; i >= 0; i--) + { + GC.KeepAlive(_items[i] as IMetadata3); + } + + for (var i = items.Length - 1; i >= 0; i--) + { + GC.KeepAlive(_items[i] as IMetadata4); + } + + for (var i = items.Length - 1; i >= 0; i--) + { + GC.KeepAlive(_items[i] as IMetadata5); + } + } - _items = new object[Count]; - for (var i = 0; i < _items.Length; i++) - { - _items[i] = seeds[i % seeds.Length]; - } + [Benchmark(OperationsPerInvoke = 5)] + public void GetMetadata() + { + GC.KeepAlive(_collection.GetMetadata()); + GC.KeepAlive(_collection.GetMetadata()); + GC.KeepAlive(_collection.GetMetadata()); + GC.KeepAlive(_collection.GetMetadata()); + GC.KeepAlive(_collection.GetMetadata()); + } - _collection = new EndpointMetadataCollection(_items); + [Benchmark(OperationsPerInvoke = 5)] + public void GetOrderedMetadata() + { + foreach (var item in _collection.GetOrderedMetadata()) + { + GC.KeepAlive(item); } - // This is a synthetic baseline that visits each item and does an as-cast. - [Benchmark(Baseline = true, OperationsPerInvoke = 5)] - public void Baseline() + foreach (var item in _collection.GetOrderedMetadata()) { - var items = _items; - for (var i = items.Length - 1; i >= 0; i--) - { - GC.KeepAlive(_items[i] as IMetadata1); - } - - for (var i = items.Length - 1; i >= 0; i--) - { - GC.KeepAlive(_items[i] as IMetadata2); - } - - for (var i = items.Length - 1; i >= 0; i--) - { - GC.KeepAlive(_items[i] as IMetadata3); - } - - for (var i = items.Length - 1; i >= 0; i--) - { - GC.KeepAlive(_items[i] as IMetadata4); - } - - for (var i = items.Length - 1; i >= 0; i--) - { - GC.KeepAlive(_items[i] as IMetadata5); - } + GC.KeepAlive(item); } - [Benchmark(OperationsPerInvoke = 5)] - public void GetMetadata() + foreach (var item in _collection.GetOrderedMetadata()) { - GC.KeepAlive(_collection.GetMetadata()); - GC.KeepAlive(_collection.GetMetadata()); - GC.KeepAlive(_collection.GetMetadata()); - GC.KeepAlive(_collection.GetMetadata()); - GC.KeepAlive(_collection.GetMetadata()); + GC.KeepAlive(item); } - [Benchmark(OperationsPerInvoke = 5)] - public void GetOrderedMetadata() + foreach (var item in _collection.GetOrderedMetadata()) { - foreach (var item in _collection.GetOrderedMetadata()) - { - GC.KeepAlive(item); - } - - foreach (var item in _collection.GetOrderedMetadata()) - { - GC.KeepAlive(item); - } - - foreach (var item in _collection.GetOrderedMetadata()) - { - GC.KeepAlive(item); - } - - foreach (var item in _collection.GetOrderedMetadata()) - { - GC.KeepAlive(item); - } - - foreach (var item in _collection.GetOrderedMetadata()) - { - GC.KeepAlive(item); - } + GC.KeepAlive(item); } - private interface IMetadata1 { } - private interface IMetadata2 { } - private interface IMetadata3 { } - private interface IMetadata4 { } - private interface IMetadata5 { } - private class Metadata1 : IMetadata1 { } - private class Metadata2 : IMetadata2 { } - private class Metadata3 : IMetadata3 { } - private class Metadata4 : IMetadata4 { } - private class Metadata5 : IMetadata5 { } - private class Metadata6 : IMetadata1, IMetadata2 { } - private class Metadata7 : IMetadata2, IMetadata3 { } - private class Metadata8 : IMetadata4, IMetadata5 { } - private class Metadata9 : IMetadata1, IMetadata2 { } + foreach (var item in _collection.GetOrderedMetadata()) + { + GC.KeepAlive(item); + } } + + private interface IMetadata1 { } + private interface IMetadata2 { } + private interface IMetadata3 { } + private interface IMetadata4 { } + private interface IMetadata5 { } + private class Metadata1 : IMetadata1 { } + private class Metadata2 : IMetadata2 { } + private class Metadata3 : IMetadata3 { } + private class Metadata4 : IMetadata4 { } + private class Metadata5 : IMetadata5 { } + private class Metadata6 : IMetadata1, IMetadata2 { } + private class Metadata7 : IMetadata2, IMetadata3 { } + private class Metadata8 : IMetadata4, IMetadata5 { } + private class Metadata9 : IMetadata1, IMetadata2 { } } diff --git a/src/Http/Routing/perf/Microbenchmarks/EndpointRoutingBenchmarkBase.cs b/src/Http/Routing/perf/Microbenchmarks/EndpointRoutingBenchmarkBase.cs index 55c15eef63..35b9a95544 100644 --- a/src/Http/Routing/perf/Microbenchmarks/EndpointRoutingBenchmarkBase.cs +++ b/src/Http/Routing/perf/Microbenchmarks/EndpointRoutingBenchmarkBase.cs @@ -14,138 +14,137 @@ using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public abstract class EndpointRoutingBenchmarkBase { - public abstract class EndpointRoutingBenchmarkBase + private protected RouteEndpoint[] Endpoints; + private protected HttpContext[] Requests; + + private protected void SetupEndpoints(params RouteEndpoint[] endpoints) { - private protected RouteEndpoint[] Endpoints; - private protected HttpContext[] Requests; + Endpoints = endpoints; + } - private protected void SetupEndpoints(params RouteEndpoint[] endpoints) - { - Endpoints = endpoints; - } + // The older routing implementations retrieve services when they first execute. + private protected IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddOptions(); + services.AddRouting(); - // The older routing implementations retrieve services when they first execute. - private protected IServiceProvider CreateServices() - { - var services = new ServiceCollection(); - services.AddLogging(); - services.AddOptions(); - services.AddRouting(); + services.TryAddEnumerable( + ServiceDescriptor.Singleton(new DefaultEndpointDataSource(Endpoints))); - services.TryAddEnumerable( - ServiceDescriptor.Singleton(new DefaultEndpointDataSource(Endpoints))); + return services.BuildServiceProvider(); + } - return services.BuildServiceProvider(); - } + private protected DfaMatcherBuilder CreateDfaMatcherBuilder() + { + return CreateServices().GetRequiredService(); + } - private protected DfaMatcherBuilder CreateDfaMatcherBuilder() + private protected static int[] SampleRequests(int endpointCount, int count) + { + // This isn't very high tech, but it's at least regular distribution. + // We sort the route templates by precedence, so this should result in + // an even distribution of the 'complexity' of the routes that are exercised. + var frequency = endpointCount / count; + if (frequency < 2) { - return CreateServices().GetRequiredService(); + throw new InvalidOperationException( + "The sample count is too high. This won't produce an accurate sampling" + + "of the request data."); } - private protected static int[] SampleRequests(int endpointCount, int count) + var samples = new int[count]; + for (var i = 0; i < samples.Length; i++) { - // This isn't very high tech, but it's at least regular distribution. - // We sort the route templates by precedence, so this should result in - // an even distribution of the 'complexity' of the routes that are exercised. - var frequency = endpointCount / count; - if (frequency < 2) - { - throw new InvalidOperationException( - "The sample count is too high. This won't produce an accurate sampling" + - "of the request data."); - } - - var samples = new int[count]; - for (var i = 0; i < samples.Length; i++) - { - samples[i] = i * frequency; - } - - return samples; + samples[i] = i * frequency; } - [MethodImpl(MethodImplOptions.NoInlining)] - private protected void Validate(HttpContext httpContext, Endpoint expected, Endpoint actual) - { - if (!object.ReferenceEquals(expected, actual)) - { - var message = new StringBuilder(); - message.AppendLine(FormattableString.Invariant($"Validation failed for request {Array.IndexOf(Requests, httpContext)}")); - message.AppendLine(FormattableString.Invariant($"{httpContext.Request.Method} {httpContext.Request.Path}")); - message.AppendLine(FormattableString.Invariant($"expected: '{((RouteEndpoint)expected)?.DisplayName ?? "null"}'")); - message.AppendLine(FormattableString.Invariant($"actual: '{((RouteEndpoint)actual)?.DisplayName ?? "null"}'")); - throw new InvalidOperationException(message.ToString()); - } - } + return samples; + } - protected void AssertUrl(string expectedUrl, string actualUrl) + [MethodImpl(MethodImplOptions.NoInlining)] + private protected void Validate(HttpContext httpContext, Endpoint expected, Endpoint actual) + { + if (!object.ReferenceEquals(expected, actual)) { - AssertUrl(expectedUrl, actualUrl, StringComparison.Ordinal); + var message = new StringBuilder(); + message.AppendLine(FormattableString.Invariant($"Validation failed for request {Array.IndexOf(Requests, httpContext)}")); + message.AppendLine(FormattableString.Invariant($"{httpContext.Request.Method} {httpContext.Request.Path}")); + message.AppendLine(FormattableString.Invariant($"expected: '{((RouteEndpoint)expected)?.DisplayName ?? "null"}'")); + message.AppendLine(FormattableString.Invariant($"actual: '{((RouteEndpoint)actual)?.DisplayName ?? "null"}'")); + throw new InvalidOperationException(message.ToString()); } + } - protected void AssertUrl(string expectedUrl, string actualUrl, StringComparison stringComparison) + protected void AssertUrl(string expectedUrl, string actualUrl) + { + AssertUrl(expectedUrl, actualUrl, StringComparison.Ordinal); + } + + protected void AssertUrl(string expectedUrl, string actualUrl, StringComparison stringComparison) + { + if (!string.Equals(expectedUrl, actualUrl, stringComparison)) { - if (!string.Equals(expectedUrl, actualUrl, stringComparison)) - { - throw new InvalidOperationException($"Expected: {expectedUrl}, Actual: {actualUrl}"); - } + throw new InvalidOperationException($"Expected: {expectedUrl}, Actual: {actualUrl}"); } + } - protected RouteEndpoint CreateEndpoint(string template, string httpMethod) + protected RouteEndpoint CreateEndpoint(string template, string httpMethod) + { + return CreateEndpoint(template, metadata: new object[] { - return CreateEndpoint(template, metadata: new object[] - { new HttpMethodMetadata(new string[]{ httpMethod, }), - }); - } + }); + } - protected RouteEndpoint CreateEndpoint( - string template, - object defaults = null, - object constraints = null, - object requiredValues = null, - int order = 0, - string displayName = null, - string routeName = null, - params object[] metadata) + protected RouteEndpoint CreateEndpoint( + string template, + object defaults = null, + object constraints = null, + object requiredValues = null, + int order = 0, + string displayName = null, + string routeName = null, + params object[] metadata) + { + var endpointMetadata = new List(metadata ?? Array.Empty()); + if (routeName != null) { - var endpointMetadata = new List(metadata ?? Array.Empty()); - if (routeName != null) - { - endpointMetadata.Add(new RouteNameMetadata(routeName)); - } - - return new RouteEndpoint( - (context) => Task.CompletedTask, - RoutePatternFactory.Parse(template, defaults, constraints, requiredValues), - order, - new EndpointMetadataCollection(endpointMetadata), - displayName); + endpointMetadata.Add(new RouteNameMetadata(routeName)); } - protected (HttpContext httpContext, RouteValueDictionary ambientValues) CreateCurrentRequestContext( - object ambientValues = null) - { - var context = new DefaultHttpContext(); - context.Request.RouteValues = new RouteValueDictionary(ambientValues); + return new RouteEndpoint( + (context) => Task.CompletedTask, + RoutePatternFactory.Parse(template, defaults, constraints, requiredValues), + order, + new EndpointMetadataCollection(endpointMetadata), + displayName); + } - return (context, context.Request.RouteValues); - } + protected (HttpContext httpContext, RouteValueDictionary ambientValues) CreateCurrentRequestContext( + object ambientValues = null) + { + var context = new DefaultHttpContext(); + context.Request.RouteValues = new RouteValueDictionary(ambientValues); - protected void CreateOutboundRouteEntry(TreeRouteBuilder treeRouteBuilder, RouteEndpoint endpoint) - { - treeRouteBuilder.MapOutbound( - NullRouter.Instance, - new RouteTemplate(RoutePatternFactory.Parse( - endpoint.RoutePattern.RawText, - defaults: endpoint.RoutePattern.Defaults, - parameterPolicies: null)), - requiredLinkValues: new RouteValueDictionary(endpoint.RoutePattern.RequiredValues), - routeName: null, - order: 0); - } + return (context, context.Request.RouteValues); + } + + protected void CreateOutboundRouteEntry(TreeRouteBuilder treeRouteBuilder, RouteEndpoint endpoint) + { + treeRouteBuilder.MapOutbound( + NullRouter.Instance, + new RouteTemplate(RoutePatternFactory.Parse( + endpoint.RoutePattern.RawText, + defaults: endpoint.RoutePattern.Defaults, + parameterPolicies: null)), + requiredLinkValues: new RouteValueDictionary(endpoint.RoutePattern.RequiredValues), + routeName: null, + order: 0); } } diff --git a/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/LinkGenerationGithubBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/LinkGenerationGithubBenchmark.cs index 826002361f..8df2a07ce4 100644 --- a/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/LinkGenerationGithubBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/LinkGenerationGithubBenchmark.cs @@ -6,70 +6,69 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Routing.LinkGeneration +namespace Microsoft.AspNetCore.Routing.LinkGeneration; + +public partial class LinkGenerationGithubBenchmark { - public partial class LinkGenerationGithubBenchmark - { - private LinkGenerator _linkGenerator; - private TreeRouter _treeRouter; - private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext; - private RouteValueDictionary _lookUpValues; + private LinkGenerator _linkGenerator; + private TreeRouter _treeRouter; + private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext; + private RouteValueDictionary _lookUpValues; - [GlobalSetup] - public void Setup() - { - SetupEndpoints(); + [GlobalSetup] + public void Setup() + { + SetupEndpoints(); - var services = CreateServices(); - _linkGenerator = services.GetRequiredService(); + var services = CreateServices(); + _linkGenerator = services.GetRequiredService(); - // Attribute routing related - var treeRouteBuilder = services.GetRequiredService(); - foreach (var endpoint in Endpoints) - { - CreateOutboundRouteEntry(treeRouteBuilder, endpoint); - } - _treeRouter = treeRouteBuilder.Build(); + // Attribute routing related + var treeRouteBuilder = services.GetRequiredService(); + foreach (var endpoint in Endpoints) + { + CreateOutboundRouteEntry(treeRouteBuilder, endpoint); + } + _treeRouter = treeRouteBuilder.Build(); - _requestContext = CreateCurrentRequestContext(); + _requestContext = CreateCurrentRequestContext(); - // Get the endpoint to test and pre-populate the lookup values with the defaults - // (as they are dynamically generated) and update with other required parameter values. - // /repos/{owner}/{repo}/issues/comments/{commentId} - var endpointToTest = Endpoints[176]; - _lookUpValues = new RouteValueDictionary(endpointToTest.RoutePattern.Defaults); - _lookUpValues["owner"] = "aspnet"; - _lookUpValues["repo"] = "routing"; - _lookUpValues["commentId"] = "20202"; - } + // Get the endpoint to test and pre-populate the lookup values with the defaults + // (as they are dynamically generated) and update with other required parameter values. + // /repos/{owner}/{repo}/issues/comments/{commentId} + var endpointToTest = Endpoints[176]; + _lookUpValues = new RouteValueDictionary(endpointToTest.RoutePattern.Defaults); + _lookUpValues["owner"] = "aspnet"; + _lookUpValues["repo"] = "routing"; + _lookUpValues["commentId"] = "20202"; + } - [Benchmark(Baseline = true)] - public void Baseline() - { - var url = $"/repos/{_lookUpValues["owner"]}/{_lookUpValues["repo"]}/issues/comments/{_lookUpValues["commentId"]}"; - AssertUrl("/repos/aspnet/routing/issues/comments/20202", url); - } + [Benchmark(Baseline = true)] + public void Baseline() + { + var url = $"/repos/{_lookUpValues["owner"]}/{_lookUpValues["repo"]}/issues/comments/{_lookUpValues["commentId"]}"; + AssertUrl("/repos/aspnet/routing/issues/comments/20202", url); + } - [Benchmark] - public void TreeRouter() - { - var virtualPathData = _treeRouter.GetVirtualPath(new VirtualPathContext( - _requestContext.HttpContext, - ambientValues: _requestContext.AmbientValues, - values: new RouteValueDictionary(_lookUpValues))); + [Benchmark] + public void TreeRouter() + { + var virtualPathData = _treeRouter.GetVirtualPath(new VirtualPathContext( + _requestContext.HttpContext, + ambientValues: _requestContext.AmbientValues, + values: new RouteValueDictionary(_lookUpValues))); - AssertUrl("/repos/aspnet/routing/issues/comments/20202", virtualPathData?.VirtualPath); - } + AssertUrl("/repos/aspnet/routing/issues/comments/20202", virtualPathData?.VirtualPath); + } - [Benchmark] - public void EndpointRouting() - { - var actualUrl = _linkGenerator.GetPathByRouteValues( - _requestContext.HttpContext, - routeName: null, - values: _lookUpValues); + [Benchmark] + public void EndpointRouting() + { + var actualUrl = _linkGenerator.GetPathByRouteValues( + _requestContext.HttpContext, + routeName: null, + values: _lookUpValues); - AssertUrl("/repos/aspnet/routing/issues/comments/20202", actualUrl); - } + AssertUrl("/repos/aspnet/routing/issues/comments/20202", actualUrl); } } diff --git a/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/SingleRouteRouteValuesAddressSchemeBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/SingleRouteRouteValuesAddressSchemeBenchmark.cs index 016780f1f4..448c294188 100644 --- a/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/SingleRouteRouteValuesAddressSchemeBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/SingleRouteRouteValuesAddressSchemeBenchmark.cs @@ -6,69 +6,68 @@ using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Routing.LinkGeneration +namespace Microsoft.AspNetCore.Routing.LinkGeneration; + +public class SingleRouteRouteValuesAddressSchemeBenchmark : EndpointRoutingBenchmarkBase { - public class SingleRouteRouteValuesAddressSchemeBenchmark : EndpointRoutingBenchmarkBase + private IEndpointAddressScheme _implementation; + private TestAddressScheme _baseline; + private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext; + + [GlobalSetup] + public void Setup() { - private IEndpointAddressScheme _implementation; - private TestAddressScheme _baseline; - private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext; + var template = "Products/Details"; + var defaults = new { controller = "Products", action = "Details" }; + var requiredValues = new { controller = "Products", action = "Details" }; - [GlobalSetup] - public void Setup() - { - var template = "Products/Details"; - var defaults = new { controller = "Products", action = "Details" }; - var requiredValues = new { controller = "Products", action = "Details" }; + SetupEndpoints(CreateEndpoint(template, defaults, requiredValues: requiredValues, routeName: "ProductDetails")); + var services = CreateServices(); + _implementation = services.GetRequiredService>(); + _baseline = new TestAddressScheme(Endpoints[0]); - SetupEndpoints(CreateEndpoint(template, defaults, requiredValues: requiredValues, routeName: "ProductDetails")); - var services = CreateServices(); - _implementation = services.GetRequiredService>(); - _baseline = new TestAddressScheme(Endpoints[0]); + _requestContext = CreateCurrentRequestContext(); + } - _requestContext = CreateCurrentRequestContext(); - } + [Benchmark(Baseline = true)] + public void Baseline() + { + var actual = _baseline.FindEndpoints(address: 0); + } - [Benchmark(Baseline = true)] - public void Baseline() + [Benchmark] + public void RouteValues() + { + var actual = _implementation.FindEndpoints(new RouteValuesAddress { - var actual = _baseline.FindEndpoints(address: 0); - } + AmbientValues = _requestContext.AmbientValues, + ExplicitValues = new RouteValueDictionary(new { controller = "Products", action = "Details" }), + RouteName = null + }); + } - [Benchmark] - public void RouteValues() + [Benchmark] + public void RouteName() + { + var actual = _implementation.FindEndpoints(new RouteValuesAddress { - var actual = _implementation.FindEndpoints(new RouteValuesAddress - { - AmbientValues = _requestContext.AmbientValues, - ExplicitValues = new RouteValueDictionary(new { controller = "Products", action = "Details" }), - RouteName = null - }); - } + AmbientValues = _requestContext.AmbientValues, + RouteName = "ProductDetails" + }); + } - [Benchmark] - public void RouteName() + private class TestAddressScheme : IEndpointAddressScheme + { + private readonly Endpoint _endpoint; + + public TestAddressScheme(Endpoint endpoint) { - var actual = _implementation.FindEndpoints(new RouteValuesAddress - { - AmbientValues = _requestContext.AmbientValues, - RouteName = "ProductDetails" - }); + _endpoint = endpoint; } - private class TestAddressScheme : IEndpointAddressScheme + public IEnumerable FindEndpoints(int address) { - private readonly Endpoint _endpoint; - - public TestAddressScheme(Endpoint endpoint) - { - _endpoint = endpoint; - } - - public IEnumerable FindEndpoints(int address) - { - return new[] { _endpoint }; - } + return new[] { _endpoint }; } } } diff --git a/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/SingleRouteWithConstraintsBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/SingleRouteWithConstraintsBenchmark.cs index 7c2f7a2273..925bc5159d 100644 --- a/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/SingleRouteWithConstraintsBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/SingleRouteWithConstraintsBenchmark.cs @@ -6,69 +6,68 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Routing.LinkGeneration -{ - public class SingleRouteWithConstraintsBenchmark : EndpointRoutingBenchmarkBase - { - private TreeRouter _treeRouter; - private LinkGenerator _linkGenerator; - private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext; - - [GlobalSetup] - public void Setup() - { - var template = "Customers/Details/{category}/{region}/{id:int}"; - var defaults = new { controller = "Customers", action = "Details" }; - var requiredValues = new { controller = "Customers", action = "Details" }; +namespace Microsoft.AspNetCore.Routing.LinkGeneration; - // Endpoint routing related - SetupEndpoints(CreateEndpoint(template, defaults, requiredValues: requiredValues)); - var services = CreateServices(); - _linkGenerator = services.GetRequiredService(); +public class SingleRouteWithConstraintsBenchmark : EndpointRoutingBenchmarkBase +{ + private TreeRouter _treeRouter; + private LinkGenerator _linkGenerator; + private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext; - // Attribute routing related - var treeRouteBuilder = services.GetRequiredService(); - CreateOutboundRouteEntry(treeRouteBuilder, Endpoints[0]); - _treeRouter = treeRouteBuilder.Build(); + [GlobalSetup] + public void Setup() + { + var template = "Customers/Details/{category}/{region}/{id:int}"; + var defaults = new { controller = "Customers", action = "Details" }; + var requiredValues = new { controller = "Customers", action = "Details" }; - _requestContext = CreateCurrentRequestContext(); - } + // Endpoint routing related + SetupEndpoints(CreateEndpoint(template, defaults, requiredValues: requiredValues)); + var services = CreateServices(); + _linkGenerator = services.GetRequiredService(); - [Benchmark(Baseline = true)] - public void TreeRouter() - { - var virtualPathData = _treeRouter.GetVirtualPath(new VirtualPathContext( - _requestContext.HttpContext, - ambientValues: _requestContext.AmbientValues, - values: new RouteValueDictionary( - new - { - controller = "Customers", - action = "Details", - category = "Administration", - region = "US", - id = 10 - }))); + // Attribute routing related + var treeRouteBuilder = services.GetRequiredService(); + CreateOutboundRouteEntry(treeRouteBuilder, Endpoints[0]); + _treeRouter = treeRouteBuilder.Build(); - AssertUrl("/Customers/Details/Administration/US/10", virtualPathData?.VirtualPath); - } + _requestContext = CreateCurrentRequestContext(); + } - [Benchmark] - public void EndpointRouting() - { - var actualUrl = _linkGenerator.GetPathByRouteValues( - _requestContext.HttpContext, - routeName: null, - values: new + [Benchmark(Baseline = true)] + public void TreeRouter() + { + var virtualPathData = _treeRouter.GetVirtualPath(new VirtualPathContext( + _requestContext.HttpContext, + ambientValues: _requestContext.AmbientValues, + values: new RouteValueDictionary( + new { controller = "Customers", action = "Details", category = "Administration", region = "US", id = 10 - }); + }))); + + AssertUrl("/Customers/Details/Administration/US/10", virtualPathData?.VirtualPath); + } + + [Benchmark] + public void EndpointRouting() + { + var actualUrl = _linkGenerator.GetPathByRouteValues( + _requestContext.HttpContext, + routeName: null, + values: new + { + controller = "Customers", + action = "Details", + category = "Administration", + region = "US", + id = 10 + }); - AssertUrl("/Customers/Details/Administration/US/10", actualUrl); - } + AssertUrl("/Customers/Details/Administration/US/10", actualUrl); } } diff --git a/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/SingleRouteWithNoParametersBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/SingleRouteWithNoParametersBenchmark.cs index a6f19e276c..d47b7a209b 100644 --- a/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/SingleRouteWithNoParametersBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/SingleRouteWithNoParametersBenchmark.cs @@ -6,63 +6,62 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Routing.LinkGeneration -{ - public class SingleRouteWithNoParametersBenchmark : EndpointRoutingBenchmarkBase - { - private TreeRouter _treeRouter; - private LinkGenerator _linkGenerator; - private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext; - - [GlobalSetup] - public void Setup() - { - var template = "Products/Details"; - var defaults = new { controller = "Products", action = "Details" }; - var requiredValues = new { controller = "Products", action = "Details" }; +namespace Microsoft.AspNetCore.Routing.LinkGeneration; - // Endpoint routing related - SetupEndpoints(CreateEndpoint(template, defaults, requiredValues: requiredValues)); - var services = CreateServices(); - _linkGenerator = services.GetRequiredService(); +public class SingleRouteWithNoParametersBenchmark : EndpointRoutingBenchmarkBase +{ + private TreeRouter _treeRouter; + private LinkGenerator _linkGenerator; + private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext; - // Attribute routing related - var treeRouteBuilder = services.GetRequiredService(); - CreateOutboundRouteEntry(treeRouteBuilder, Endpoints[0]); - _treeRouter = treeRouteBuilder.Build(); + [GlobalSetup] + public void Setup() + { + var template = "Products/Details"; + var defaults = new { controller = "Products", action = "Details" }; + var requiredValues = new { controller = "Products", action = "Details" }; - _requestContext = CreateCurrentRequestContext(); - } + // Endpoint routing related + SetupEndpoints(CreateEndpoint(template, defaults, requiredValues: requiredValues)); + var services = CreateServices(); + _linkGenerator = services.GetRequiredService(); - [Benchmark(Baseline = true)] - public void TreeRouter() - { - var virtualPathData = _treeRouter.GetVirtualPath(new VirtualPathContext( - _requestContext.HttpContext, - ambientValues: _requestContext.AmbientValues, - values: new RouteValueDictionary( - new - { - controller = "Products", - action = "Details", - }))); + // Attribute routing related + var treeRouteBuilder = services.GetRequiredService(); + CreateOutboundRouteEntry(treeRouteBuilder, Endpoints[0]); + _treeRouter = treeRouteBuilder.Build(); - AssertUrl("/Products/Details", virtualPathData?.VirtualPath); - } + _requestContext = CreateCurrentRequestContext(); + } - [Benchmark] - public void EndpointRouting() - { - var actualUrl = _linkGenerator.GetPathByRouteValues( - _requestContext.HttpContext, - routeName: null, - values: new + [Benchmark(Baseline = true)] + public void TreeRouter() + { + var virtualPathData = _treeRouter.GetVirtualPath(new VirtualPathContext( + _requestContext.HttpContext, + ambientValues: _requestContext.AmbientValues, + values: new RouteValueDictionary( + new { controller = "Products", action = "Details", - }); + }))); + + AssertUrl("/Products/Details", virtualPathData?.VirtualPath); + } + + [Benchmark] + public void EndpointRouting() + { + var actualUrl = _linkGenerator.GetPathByRouteValues( + _requestContext.HttpContext, + routeName: null, + values: new + { + controller = "Products", + action = "Details", + }); - AssertUrl("/Products/Details", actualUrl); - } + AssertUrl("/Products/Details", actualUrl); } } diff --git a/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/SingleRouteWithParametersBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/SingleRouteWithParametersBenchmark.cs index 4a7a0b5ee9..df3e44ce8e 100644 --- a/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/SingleRouteWithParametersBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/LinkGeneration/SingleRouteWithParametersBenchmark.cs @@ -6,69 +6,68 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Tree; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Routing.LinkGeneration -{ - public class SingleRouteWithParametersBenchmark : EndpointRoutingBenchmarkBase - { - private TreeRouter _treeRouter; - private LinkGenerator _linkGenerator; - private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext; - - [GlobalSetup] - public void Setup() - { - var template = "Customers/Details/{category}/{region}/{id}"; - var defaults = new { controller = "Customers", action = "Details" }; - var requiredValues = new { controller = "Customers", action = "Details" }; +namespace Microsoft.AspNetCore.Routing.LinkGeneration; - // Endpoint routing related - SetupEndpoints(CreateEndpoint(template, defaults, requiredValues: requiredValues)); - var services = CreateServices(); - _linkGenerator = services.GetRequiredService(); +public class SingleRouteWithParametersBenchmark : EndpointRoutingBenchmarkBase +{ + private TreeRouter _treeRouter; + private LinkGenerator _linkGenerator; + private (HttpContext HttpContext, RouteValueDictionary AmbientValues) _requestContext; - // Attribute routing related - var treeRouteBuilder = services.GetRequiredService(); - CreateOutboundRouteEntry(treeRouteBuilder, Endpoints[0]); - _treeRouter = treeRouteBuilder.Build(); + [GlobalSetup] + public void Setup() + { + var template = "Customers/Details/{category}/{region}/{id}"; + var defaults = new { controller = "Customers", action = "Details" }; + var requiredValues = new { controller = "Customers", action = "Details" }; - _requestContext = CreateCurrentRequestContext(); - } + // Endpoint routing related + SetupEndpoints(CreateEndpoint(template, defaults, requiredValues: requiredValues)); + var services = CreateServices(); + _linkGenerator = services.GetRequiredService(); - [Benchmark(Baseline = true)] - public void TreeRouter() - { - var virtualPathData = _treeRouter.GetVirtualPath(new VirtualPathContext( - _requestContext.HttpContext, - ambientValues: _requestContext.AmbientValues, - values: new RouteValueDictionary( - new - { - controller = "Customers", - action = "Details", - category = "Administration", - region = "US", - id = 10 - }))); + // Attribute routing related + var treeRouteBuilder = services.GetRequiredService(); + CreateOutboundRouteEntry(treeRouteBuilder, Endpoints[0]); + _treeRouter = treeRouteBuilder.Build(); - AssertUrl("/Customers/Details/Administration/US/10", virtualPathData?.VirtualPath); - } + _requestContext = CreateCurrentRequestContext(); + } - [Benchmark] - public void EndpointRouting() - { - var actualUrl = _linkGenerator.GetPathByRouteValues( - _requestContext.HttpContext, - routeName: null, - values: new + [Benchmark(Baseline = true)] + public void TreeRouter() + { + var virtualPathData = _treeRouter.GetVirtualPath(new VirtualPathContext( + _requestContext.HttpContext, + ambientValues: _requestContext.AmbientValues, + values: new RouteValueDictionary( + new { controller = "Customers", action = "Details", category = "Administration", region = "US", id = 10 - }); + }))); + + AssertUrl("/Customers/Details/Administration/US/10", virtualPathData?.VirtualPath); + } + + [Benchmark] + public void EndpointRouting() + { + var actualUrl = _linkGenerator.GetPathByRouteValues( + _requestContext.HttpContext, + routeName: null, + values: new + { + controller = "Customers", + action = "Details", + category = "Administration", + region = "US", + id = 10 + }); - AssertUrl("/Customers/Details/Administration/US/10", actualUrl); - } + AssertUrl("/Customers/Details/Administration/US/10", actualUrl); } } diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerBenchmarkBase.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerBenchmarkBase.cs index e70c950eda..f265f263c3 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerBenchmarkBase.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerBenchmarkBase.cs @@ -1,34 +1,33 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public abstract class FastPathTokenizerBenchmarkBase { - public abstract class FastPathTokenizerBenchmarkBase + internal unsafe void NaiveBaseline(string path, PathSegment* segments, int maxCount) { - internal unsafe void NaiveBaseline(string path, PathSegment* segments, int maxCount) + int count = 0; + int start = 1; // Paths always start with a leading / + int end; + while ((end = path.IndexOf('/', start)) >= 0 && count < maxCount) { - int count = 0; - int start = 1; // Paths always start with a leading / - int end; - while ((end = path.IndexOf('/', start)) >= 0 && count < maxCount) - { - segments[count++] = new PathSegment(start, end - start); - start = end + 1; // resume search after the current character - } - - // Residue - var length = path.Length - start; - if (length > 0 && count < maxCount) - { - segments[count++] = new PathSegment(start, length); - } + segments[count++] = new PathSegment(start, end - start); + start = end + 1; // resume search after the current character } - internal unsafe void MinimalBaseline(string path, PathSegment* segments, int maxCount) + // Residue + var length = path.Length - start; + if (length > 0 && count < maxCount) { - var start = 1; - var length = path.Length - start; - segments[0] = new PathSegment(start, length); + segments[count++] = new PathSegment(start, length); } } + + internal unsafe void MinimalBaseline(string path, PathSegment* segments, int maxCount) + { + var start = 1; + var length = path.Length - start; + segments[0] = new PathSegment(start, length); + } } diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerEmptyBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerEmptyBenchmark.cs index 8bb7b77779..4d6fe55a66 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerEmptyBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerEmptyBenchmark.cs @@ -4,30 +4,29 @@ using System; using BenchmarkDotNet.Attributes; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class FastPathTokenizerEmptyBenchmark : FastPathTokenizerBenchmarkBase { - public class FastPathTokenizerEmptyBenchmark : FastPathTokenizerBenchmarkBase - { - private const int MaxCount = 32; - private static readonly string Input = "/"; + private const int MaxCount = 32; + private static readonly string Input = "/"; - // This is super hardcoded implementation for comparison, we dont't expect to do better. - [Benchmark(Baseline = true)] - public unsafe void Baseline() - { - var path = Input; - var segments = stackalloc PathSegment[MaxCount]; + // This is super hardcoded implementation for comparison, we dont't expect to do better. + [Benchmark(Baseline = true)] + public unsafe void Baseline() + { + var path = Input; + var segments = stackalloc PathSegment[MaxCount]; - MinimalBaseline(path, segments, MaxCount); - } + MinimalBaseline(path, segments, MaxCount); + } - [Benchmark] - public void Implementation() - { - var path = Input; - Span segments = stackalloc PathSegment[MaxCount]; + [Benchmark] + public void Implementation() + { + var path = Input; + Span segments = stackalloc PathSegment[MaxCount]; - FastPathTokenizer.Tokenize(path, segments); - } + FastPathTokenizer.Tokenize(path, segments); } } diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerLargeBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerLargeBenchmark.cs index 96ec8683b8..085e150c94 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerLargeBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerLargeBenchmark.cs @@ -4,34 +4,33 @@ using System; using BenchmarkDotNet.Attributes; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class FastPathTokenizerLargeBenchmark : FastPathTokenizerBenchmarkBase { - public class FastPathTokenizerLargeBenchmark : FastPathTokenizerBenchmarkBase - { - private static readonly int MaxCount = 32; - private static readonly string Input = - "/heeeeeeeeeeyyyyyyyyyyy/this/is/a/string/with/lots/of/segments" + - "/hoooooooooooooooooooooooooooooooooow long/do you think it should be?/I think" + - "/like/32/segments/is /a/goood/number/dklfl/20303/dlflkf" + - "/Im/tired/of/thinking/of/more/things/to/so"; + private static readonly int MaxCount = 32; + private static readonly string Input = + "/heeeeeeeeeeyyyyyyyyyyy/this/is/a/string/with/lots/of/segments" + + "/hoooooooooooooooooooooooooooooooooow long/do you think it should be?/I think" + + "/like/32/segments/is /a/goood/number/dklfl/20303/dlflkf" + + "/Im/tired/of/thinking/of/more/things/to/so"; - // This is a naive reference implementation. We expect to do better. - [Benchmark(Baseline = true)] - public unsafe void Baseline() - { - var path = Input; - var segments = stackalloc PathSegment[MaxCount]; + // This is a naive reference implementation. We expect to do better. + [Benchmark(Baseline = true)] + public unsafe void Baseline() + { + var path = Input; + var segments = stackalloc PathSegment[MaxCount]; - NaiveBaseline(path, segments, MaxCount); - } + NaiveBaseline(path, segments, MaxCount); + } - [Benchmark] - public void Implementation() - { - var path = Input; - Span segments = stackalloc PathSegment[MaxCount]; + [Benchmark] + public void Implementation() + { + var path = Input; + Span segments = stackalloc PathSegment[MaxCount]; - FastPathTokenizer.Tokenize(path, segments); - } + FastPathTokenizer.Tokenize(path, segments); } } diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerPlaintextBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerPlaintextBenchmark.cs index df16357fec..fd1b881156 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerPlaintextBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerPlaintextBenchmark.cs @@ -4,30 +4,29 @@ using System; using BenchmarkDotNet.Attributes; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class FastPathTokenizerPlaintextBenchmark : FastPathTokenizerBenchmarkBase { - public class FastPathTokenizerPlaintextBenchmark : FastPathTokenizerBenchmarkBase - { - private const int MaxCount = 32; - private static readonly string Input = "/plaintext"; + private const int MaxCount = 32; + private static readonly string Input = "/plaintext"; - // This is super hardcoded implementation for comparison, we dont't expect to do better. - [Benchmark(Baseline = true)] - public unsafe void Baseline() - { - var path = Input; - var segments = stackalloc PathSegment[MaxCount]; + // This is super hardcoded implementation for comparison, we dont't expect to do better. + [Benchmark(Baseline = true)] + public unsafe void Baseline() + { + var path = Input; + var segments = stackalloc PathSegment[MaxCount]; - MinimalBaseline(path, segments, MaxCount); - } + MinimalBaseline(path, segments, MaxCount); + } - [Benchmark] - public void Implementation() - { - var path = Input; - Span segments = stackalloc PathSegment[MaxCount]; + [Benchmark] + public void Implementation() + { + var path = Input; + Span segments = stackalloc PathSegment[MaxCount]; - FastPathTokenizer.Tokenize(path, segments); - } + FastPathTokenizer.Tokenize(path, segments); } } diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerSmallBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerSmallBenchmark.cs index 4b56abc201..cf26edc3c5 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerSmallBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/FastPathTokenizerSmallBenchmark.cs @@ -4,30 +4,29 @@ using System; using BenchmarkDotNet.Attributes; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class FastPathTokenizerSmallBenchmark : FastPathTokenizerBenchmarkBase { - public class FastPathTokenizerSmallBenchmark : FastPathTokenizerBenchmarkBase - { - private const int MaxCount = 32; - private static readonly string Input = "/hello/world/cool"; + private const int MaxCount = 32; + private static readonly string Input = "/hello/world/cool"; - // This is a naive reference implementation. We expect to do better. - [Benchmark(Baseline = true)] - public unsafe void Baseline() - { - var path = Input; - var segments = stackalloc PathSegment[MaxCount]; + // This is a naive reference implementation. We expect to do better. + [Benchmark(Baseline = true)] + public unsafe void Baseline() + { + var path = Input; + var segments = stackalloc PathSegment[MaxCount]; - NaiveBaseline(path, segments, MaxCount); - } + NaiveBaseline(path, segments, MaxCount); + } - [Benchmark] - public void Implementation() - { - var path = Input; - Span segments = stackalloc PathSegment[MaxCount]; + [Benchmark] + public void Implementation() + { + var path = Input; + Span segments = stackalloc PathSegment[MaxCount]; - FastPathTokenizer.Tokenize(path, segments); - } + FastPathTokenizer.Tokenize(path, segments); } } diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/HttpMethodPolicyJumpTableBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/HttpMethodPolicyJumpTableBenchmark.cs index 40eb396587..21392a7d86 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/HttpMethodPolicyJumpTableBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/HttpMethodPolicyJumpTableBenchmark.cs @@ -5,50 +5,49 @@ using System.Collections.Generic; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class HttpMethodPolicyJumpTableBenchmark { - public class HttpMethodPolicyJumpTableBenchmark - { - private PolicyJumpTable _dictionaryJumptable; - private PolicyJumpTable _singleEntryJumptable; - private DefaultHttpContext _httpContext; + private PolicyJumpTable _dictionaryJumptable; + private PolicyJumpTable _singleEntryJumptable; + private DefaultHttpContext _httpContext; - [GlobalSetup] - public void Setup() - { - _dictionaryJumptable = new HttpMethodDictionaryPolicyJumpTable( - 0, - new Dictionary - { - [HttpMethods.Get] = 1 - }, - -1, - new Dictionary - { - [HttpMethods.Get] = 2 - }); - _singleEntryJumptable = new HttpMethodSingleEntryPolicyJumpTable( - 0, - HttpMethods.Get, - -1, - supportsCorsPreflight: true, - -1, - 2); + [GlobalSetup] + public void Setup() + { + _dictionaryJumptable = new HttpMethodDictionaryPolicyJumpTable( + 0, + new Dictionary + { + [HttpMethods.Get] = 1 + }, + -1, + new Dictionary + { + [HttpMethods.Get] = 2 + }); + _singleEntryJumptable = new HttpMethodSingleEntryPolicyJumpTable( + 0, + HttpMethods.Get, + -1, + supportsCorsPreflight: true, + -1, + 2); - _httpContext = new DefaultHttpContext(); - _httpContext.Request.Method = HttpMethods.Get; - } + _httpContext = new DefaultHttpContext(); + _httpContext.Request.Method = HttpMethods.Get; + } - [Benchmark] - public int DictionaryPolicyJumpTable() - { - return _dictionaryJumptable.GetDestination(_httpContext); - } + [Benchmark] + public int DictionaryPolicyJumpTable() + { + return _dictionaryJumptable.GetDestination(_httpContext); + } - [Benchmark] - public int SingleEntryPolicyJumpTable() - { - return _singleEntryJumptable.GetDestination(_httpContext); - } + [Benchmark] + public int SingleEntryPolicyJumpTable() + { + return _singleEntryJumptable.GetDestination(_httpContext); } } diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/JumpTableMultipleEntryBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/JumpTableMultipleEntryBenchmark.cs index e674028998..81f2520c6e 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/JumpTableMultipleEntryBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/JumpTableMultipleEntryBenchmark.cs @@ -5,173 +5,172 @@ using System; using System.Collections.Generic; using BenchmarkDotNet.Attributes; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class JumpTableMultipleEntryBenchmark { - public class JumpTableMultipleEntryBenchmark + private string[] _strings; + private PathSegment[] _segments; + + private JumpTable _linearSearch; + private JumpTable _dictionary; + private JumpTable _trie; + private JumpTable _vectorTrie; + + // All factors of 100 to support sampling + [Params(2, 5, 10, 25, 50, 100)] + public int Count; + + [GlobalSetup] + public void Setup() { - private string[] _strings; - private PathSegment[] _segments; + _strings = GetStrings(100); + _segments = new PathSegment[100]; - private JumpTable _linearSearch; - private JumpTable _dictionary; - private JumpTable _trie; - private JumpTable _vectorTrie; + for (var i = 0; i < _strings.Length; i++) + { + _segments[i] = new PathSegment(0, _strings[i].Length); + } - // All factors of 100 to support sampling - [Params(2, 5, 10, 25, 50, 100)] - public int Count; + var samples = new int[Count]; + for (var i = 0; i < samples.Length; i++) + { + samples[i] = i * (_strings.Length / Count); + } - [GlobalSetup] - public void Setup() + var entries = new List<(string text, int _)>(); + for (var i = 0; i < samples.Length; i++) { - _strings = GetStrings(100); - _segments = new PathSegment[100]; + entries.Add((_strings[samples[i]], i)); + } + + _linearSearch = new LinearSearchJumpTable(0, -1, entries.ToArray()); + _dictionary = new DictionaryJumpTable(0, -1, entries.ToArray()); + _trie = new ILEmitTrieJumpTable(0, -1, entries.ToArray(), vectorize: false, _dictionary); + _vectorTrie = new ILEmitTrieJumpTable(0, -1, entries.ToArray(), vectorize: true, _dictionary); + } + + // This baseline is similar to SingleEntryJumpTable. We just want + // something stable to compare against. + [Benchmark(Baseline = true, OperationsPerInvoke = 100)] + public int Baseline() + { + var strings = _strings; + var segments = _segments; - for (var i = 0; i < _strings.Length; i++) + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + var @string = strings[i]; + var segment = segments[i]; + + if (segment.Length == 0) { - _segments[i] = new PathSegment(0, _strings[i].Length); + destination = -1; } - - var samples = new int[Count]; - for (var i = 0; i < samples.Length; i++) + else if (segment.Length != @string.Length) { - samples[i] = i * (_strings.Length / Count); + destination = 1; } - - var entries = new List<(string text, int _)>(); - for (var i = 0; i < samples.Length; i++) + else { - entries.Add((_strings[samples[i]], i)); + destination = string.Compare( + @string, + segment.Start, + @string, + 0, + segment.Length, + StringComparison.OrdinalIgnoreCase); } - - _linearSearch = new LinearSearchJumpTable(0, -1, entries.ToArray()); - _dictionary = new DictionaryJumpTable(0, -1, entries.ToArray()); - _trie = new ILEmitTrieJumpTable(0, -1, entries.ToArray(), vectorize: false, _dictionary); - _vectorTrie = new ILEmitTrieJumpTable(0, -1, entries.ToArray(), vectorize: true, _dictionary); } - // This baseline is similar to SingleEntryJumpTable. We just want - // something stable to compare against. - [Benchmark(Baseline = true, OperationsPerInvoke = 100)] - public int Baseline() - { - var strings = _strings; - var segments = _segments; + return destination; + } - var destination = 0; - for (var i = 0; i < strings.Length; i++) - { - var @string = strings[i]; - var segment = segments[i]; - - if (segment.Length == 0) - { - destination = -1; - } - else if (segment.Length != @string.Length) - { - destination = 1; - } - else - { - destination = string.Compare( - @string, - segment.Start, - @string, - 0, - segment.Length, - StringComparison.OrdinalIgnoreCase); - } - } + [Benchmark(OperationsPerInvoke = 100)] + public int LinearSearch() + { + var strings = _strings; + var segments = _segments; - return destination; + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + destination = _linearSearch.GetDestination(strings[i], segments[i]); } - [Benchmark(OperationsPerInvoke = 100)] - public int LinearSearch() - { - var strings = _strings; - var segments = _segments; + return destination; + } - var destination = 0; - for (var i = 0; i < strings.Length; i++) - { - destination = _linearSearch.GetDestination(strings[i], segments[i]); - } + [Benchmark(OperationsPerInvoke = 100)] + public int Dictionary() + { + var strings = _strings; + var segments = _segments; - return destination; + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + destination = _dictionary.GetDestination(strings[i], segments[i]); } - [Benchmark(OperationsPerInvoke = 100)] - public int Dictionary() - { - var strings = _strings; - var segments = _segments; + return destination; + } - var destination = 0; - for (var i = 0; i < strings.Length; i++) - { - destination = _dictionary.GetDestination(strings[i], segments[i]); - } + [Benchmark(OperationsPerInvoke = 100)] + public int Trie() + { + var strings = _strings; + var segments = _segments; - return destination; + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + destination = _trie.GetDestination(strings[i], segments[i]); } - [Benchmark(OperationsPerInvoke = 100)] - public int Trie() - { - var strings = _strings; - var segments = _segments; + return destination; + } - var destination = 0; - for (var i = 0; i < strings.Length; i++) - { - destination = _trie.GetDestination(strings[i], segments[i]); - } + [Benchmark(OperationsPerInvoke = 100)] + public int VectorTrie() + { + var strings = _strings; + var segments = _segments; - return destination; + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + destination = _vectorTrie.GetDestination(strings[i], segments[i]); } - [Benchmark(OperationsPerInvoke = 100)] - public int VectorTrie() + return destination; + } + + private static string[] GetStrings(int count) + { + var strings = new string[count]; + for (var i = 0; i < count; i++) { - var strings = _strings; - var segments = _segments; + var guid = Guid.NewGuid().ToString(); - var destination = 0; - for (var i = 0; i < strings.Length; i++) + // Between 5 and 36 characters + var text = guid.Substring(0, Math.Max(5, Math.Min(i, 36))); + if (char.IsDigit(text[0])) { - destination = _vectorTrie.GetDestination(strings[i], segments[i]); + // Convert first character to a letter. + text = ((char)(text[0] + ('G' - '0'))) + text.Substring(1); } - return destination; - } - - private static string[] GetStrings(int count) - { - var strings = new string[count]; - for (var i = 0; i < count; i++) + if (i % 2 == 0) { - var guid = Guid.NewGuid().ToString(); - - // Between 5 and 36 characters - var text = guid.Substring(0, Math.Max(5, Math.Min(i, 36))); - if (char.IsDigit(text[0])) - { - // Convert first character to a letter. - text = ((char)(text[0] + ('G' - '0'))) + text.Substring(1); - } - - if (i % 2 == 0) - { - // Lowercase half of them - text = text.ToLowerInvariant(); - } - - strings[i] = text; + // Lowercase half of them + text = text.ToLowerInvariant(); } - return strings; + strings[i] = text; } + + return strings; } } diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/JumpTableSingleEntryBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/JumpTableSingleEntryBenchmark.cs index 9c1ce9842f..dee68914fb 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/JumpTableSingleEntryBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/JumpTableSingleEntryBenchmark.cs @@ -6,137 +6,136 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using BenchmarkDotNet.Attributes; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class JumpTableSingleEntryBenchmark { - public class JumpTableSingleEntryBenchmark - { - private JumpTable _default; - private JumpTable _trie; - private JumpTable _vectorTrie; - private JumpTable _ascii; + private JumpTable _default; + private JumpTable _trie; + private JumpTable _vectorTrie; + private JumpTable _ascii; - private string[] _strings; - private PathSegment[] _segments; + private string[] _strings; + private PathSegment[] _segments; - [GlobalSetup] - public void Setup() - { - _default = new SingleEntryJumpTable(0, -1, "hello-world", 1); - _trie = new ILEmitTrieJumpTable(0, -1, new[] { ("hello-world", 1), }, vectorize: false, _default); - _vectorTrie = new ILEmitTrieJumpTable(0, -1, new[] { ("hello-world", 1), }, vectorize: true, _default); - _ascii = new SingleEntryAsciiJumpTable(0, -1, "hello-world", 1); + [GlobalSetup] + public void Setup() + { + _default = new SingleEntryJumpTable(0, -1, "hello-world", 1); + _trie = new ILEmitTrieJumpTable(0, -1, new[] { ("hello-world", 1), }, vectorize: false, _default); + _vectorTrie = new ILEmitTrieJumpTable(0, -1, new[] { ("hello-world", 1), }, vectorize: true, _default); + _ascii = new SingleEntryAsciiJumpTable(0, -1, "hello-world", 1); - _strings = new string[] - { + _strings = new string[] + { "index/foo/2", "index/hello-world1/2", "index/hello-world/2", "index//2", "index/hillo-goodbye/2", - }; - _segments = new PathSegment[] - { + }; + _segments = new PathSegment[] + { new PathSegment(6, 3), new PathSegment(6, 12), new PathSegment(6, 11), new PathSegment(6, 0), new PathSegment(6, 13), - }; - } + }; + } - [Benchmark(Baseline = true, OperationsPerInvoke = 5)] - public int Baseline() + [Benchmark(Baseline = true, OperationsPerInvoke = 5)] + public int Baseline() + { + var strings = _strings; + var segments = _segments; + + int destination = 0; + for (var i = 0; i < strings.Length; i++) { - var strings = _strings; - var segments = _segments; + var @string = strings[i]; + var segment = segments[i]; - int destination = 0; - for (var i = 0; i < strings.Length; i++) + if (segment.Length == 0) { - var @string = strings[i]; - var segment = segments[i]; - - if (segment.Length == 0) - { - destination = -1; - } - else if (segment.Length != "hello-world".Length) - { - destination = 1; - } - else - { - destination = string.Compare( - @string, - segment.Start, - "hello-world", - 0, - segment.Length, - StringComparison.OrdinalIgnoreCase); - } + destination = -1; + } + else if (segment.Length != "hello-world".Length) + { + destination = 1; + } + else + { + destination = string.Compare( + @string, + segment.Start, + "hello-world", + 0, + segment.Length, + StringComparison.OrdinalIgnoreCase); } - - return destination; } - [Benchmark(OperationsPerInvoke = 5)] - public int Default() - { - var strings = _strings; - var segments = _segments; + return destination; + } - var destination = 0; - for (var i = 0; i < strings.Length; i++) - { - destination = _default.GetDestination(strings[i], segments[i]); - } + [Benchmark(OperationsPerInvoke = 5)] + public int Default() + { + var strings = _strings; + var segments = _segments; - return destination; + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + destination = _default.GetDestination(strings[i], segments[i]); } - [Benchmark(OperationsPerInvoke = 5)] - public int Ascii() - { - var strings = _strings; - var segments = _segments; + return destination; + } - var destination = 0; - for (var i = 0; i < strings.Length; i++) - { - destination = _ascii.GetDestination(strings[i], segments[i]); - } + [Benchmark(OperationsPerInvoke = 5)] + public int Ascii() + { + var strings = _strings; + var segments = _segments; - return destination; + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + destination = _ascii.GetDestination(strings[i], segments[i]); } - [Benchmark(OperationsPerInvoke = 5)] - public int Trie() - { - var strings = _strings; - var segments = _segments; + return destination; + } - var destination = 0; - for (var i = 0; i < strings.Length; i++) - { - destination = _trie.GetDestination(strings[i], segments[i]); - } + [Benchmark(OperationsPerInvoke = 5)] + public int Trie() + { + var strings = _strings; + var segments = _segments; - return destination; + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + destination = _trie.GetDestination(strings[i], segments[i]); } - [Benchmark(OperationsPerInvoke = 5)] - public int VectorTrie() - { - var strings = _strings; - var segments = _segments; + return destination; + } - var destination = 0; - for (var i = 0; i < strings.Length; i++) - { - destination = _vectorTrie.GetDestination(strings[i], segments[i]); - } + [Benchmark(OperationsPerInvoke = 5)] + public int VectorTrie() + { + var strings = _strings; + var segments = _segments; - return destination; + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + destination = _vectorTrie.GetDestination(strings[i], segments[i]); } + + return destination; } } diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/JumpTableZeroEntryBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/JumpTableZeroEntryBenchmark.cs index 4371f1c7c6..ece9820d77 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/JumpTableZeroEntryBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/JumpTableZeroEntryBenchmark.cs @@ -3,64 +3,63 @@ using BenchmarkDotNet.Attributes; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class JumpTableZeroEntryBenchmark { - public class JumpTableZeroEntryBenchmark - { - private JumpTable _table; - private string[] _strings; - private PathSegment[] _segments; + private JumpTable _table; + private string[] _strings; + private PathSegment[] _segments; - [GlobalSetup] - public void Setup() + [GlobalSetup] + public void Setup() + { + _table = new ZeroEntryJumpTable(0, -1); + _strings = new string[] { - _table = new ZeroEntryJumpTable(0, -1); - _strings = new string[] - { "index/foo/2", "index/hello-world1/2", "index/hello-world/2", "index//2", "index/hillo-goodbye/2", - }; - _segments = new PathSegment[] - { + }; + _segments = new PathSegment[] + { new PathSegment(6, 3), new PathSegment(6, 12), new PathSegment(6, 11), new PathSegment(6, 0), new PathSegment(6, 13), - }; - } - - [Benchmark(Baseline=true, OperationsPerInvoke = 5)] - public int Baseline() - { - var strings = _strings; - var segments = _segments; + }; + } - var destination = 0; - for (var i = 0; i < strings.Length; i++) - { - destination = segments[i].Length == 0 ? -1 : 0; - } + [Benchmark(Baseline = true, OperationsPerInvoke = 5)] + public int Baseline() + { + var strings = _strings; + var segments = _segments; - return destination; + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + destination = segments[i].Length == 0 ? -1 : 0; } - [Benchmark(OperationsPerInvoke = 5)] - public int Implementation() - { - var strings = _strings; - var segments = _segments; + return destination; + } - var destination = 0; - for (var i = 0; i < strings.Length; i++) - { - destination = _table.GetDestination(strings[i], segments[i]); - } + [Benchmark(OperationsPerInvoke = 5)] + public int Implementation() + { + var strings = _strings; + var segments = _segments; - return destination; + var destination = 0; + for (var i = 0; i < strings.Length; i++) + { + destination = _table.GetDestination(strings[i], segments[i]); } + + return destination; } } diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherAzureBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherAzureBenchmark.cs index 5d3b802be8..a7b45af5da 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherAzureBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherAzureBenchmark.cs @@ -5,55 +5,54 @@ using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// Generated from https://github.com/Azure/azure-rest-api-specs +public class MatcherAzureBenchmark : MatcherAzureBenchmarkBase { - // Generated from https://github.com/Azure/azure-rest-api-specs - public class MatcherAzureBenchmark : MatcherAzureBenchmarkBase - { - private const int SampleCount = 100; + private const int SampleCount = 100; - private BarebonesMatcher _baseline; - private Matcher _dfa; + private BarebonesMatcher _baseline; + private Matcher _dfa; - private int[] _samples; + private int[] _samples; - [GlobalSetup] - public void Setup() - { - SetupEndpoints(); + [GlobalSetup] + public void Setup() + { + SetupEndpoints(); - SetupRequests(); + SetupRequests(); - // The perf is kinda slow for these benchmarks, so we do some sampling - // of the request data. - _samples = SampleRequests(EndpointCount, SampleCount); + // The perf is kinda slow for these benchmarks, so we do some sampling + // of the request data. + _samples = SampleRequests(EndpointCount, SampleCount); - _baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder()); - _dfa = SetupMatcher(CreateDfaMatcherBuilder()); - } + _baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder()); + _dfa = SetupMatcher(CreateDfaMatcherBuilder()); + } - [Benchmark(Baseline = true, OperationsPerInvoke = SampleCount)] - public async Task Baseline() + [Benchmark(Baseline = true, OperationsPerInvoke = SampleCount)] + public async Task Baseline() + { + for (var i = 0; i < SampleCount; i++) { - for (var i = 0; i < SampleCount; i++) - { - var sample = _samples[i]; - var httpContext = Requests[sample]; - await _baseline.Matchers[sample].MatchAsync(httpContext); - Validate(httpContext, Endpoints[sample], httpContext.GetEndpoint()); - } + var sample = _samples[i]; + var httpContext = Requests[sample]; + await _baseline.Matchers[sample].MatchAsync(httpContext); + Validate(httpContext, Endpoints[sample], httpContext.GetEndpoint()); } + } - [Benchmark(OperationsPerInvoke = SampleCount)] - public async Task Dfa() + [Benchmark(OperationsPerInvoke = SampleCount)] + public async Task Dfa() + { + for (var i = 0; i < SampleCount; i++) { - for (var i = 0; i < SampleCount; i++) - { - var sample = _samples[i]; - var httpContext = Requests[sample]; - await _dfa.MatchAsync(httpContext); - Validate(httpContext, Endpoints[sample], httpContext.GetEndpoint()); - } + var sample = _samples[i]; + var httpContext = Requests[sample]; + await _dfa.MatchAsync(httpContext); + Validate(httpContext, Endpoints[sample], httpContext.GetEndpoint()); } } } diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherBuilderAzureBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherBuilderAzureBenchmark.cs index f6ed69f098..75656bd641 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherBuilderAzureBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherBuilderAzureBenchmark.cs @@ -5,27 +5,26 @@ using System; using BenchmarkDotNet.Attributes; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// Generated from https://github.com/APIs-guru/openapi-directory +// Use https://editor2.swagger.io/ to convert from yaml to json- +public class MatcherBuilderAzureBenchmark : MatcherAzureBenchmarkBase { - // Generated from https://github.com/APIs-guru/openapi-directory - // Use https://editor2.swagger.io/ to convert from yaml to json- - public class MatcherBuilderAzureBenchmark : MatcherAzureBenchmarkBase - { - private IServiceProvider _services; + private IServiceProvider _services; - [GlobalSetup] - public void Setup() - { - SetupEndpoints(); + [GlobalSetup] + public void Setup() + { + SetupEndpoints(); - _services = CreateServices(); - } + _services = CreateServices(); + } - [Benchmark] - public void Dfa() - { - var builder = _services.GetRequiredService(); - SetupMatcher(builder); - } + [Benchmark] + public void Dfa() + { + var builder = _services.GetRequiredService(); + SetupMatcher(builder); } -} \ No newline at end of file +} diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherBuilderGithubBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherBuilderGithubBenchmark.cs index 372c7ff2dd..43bc4f4a7f 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherBuilderGithubBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherBuilderGithubBenchmark.cs @@ -5,27 +5,26 @@ using System; using BenchmarkDotNet.Attributes; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// Generated from https://github.com/APIs-guru/openapi-directory +// Use https://editor2.swagger.io/ to convert from yaml to json- +public class MatcherBuilderGithubBenchmark : MatcherGithubBenchmarkBase { - // Generated from https://github.com/APIs-guru/openapi-directory - // Use https://editor2.swagger.io/ to convert from yaml to json- - public class MatcherBuilderGithubBenchmark : MatcherGithubBenchmarkBase - { - private IServiceProvider _services; + private IServiceProvider _services; - [GlobalSetup] - public void Setup() - { - SetupEndpoints(); + [GlobalSetup] + public void Setup() + { + SetupEndpoints(); - _services = CreateServices(); - } + _services = CreateServices(); + } - [Benchmark] - public void Dfa() - { - var builder = _services.GetRequiredService(); - SetupMatcher(builder); - } + [Benchmark] + public void Dfa() + { + var builder = _services.GetRequiredService(); + SetupMatcher(builder); } -} \ No newline at end of file +} diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherBuilderMultipleEntryBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherBuilderMultipleEntryBenchmark.cs index 2973af0e09..861cb2b8ca 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherBuilderMultipleEntryBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherBuilderMultipleEntryBenchmark.cs @@ -12,36 +12,36 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public partial class MatcherBuilderMultipleEntryBenchmark : EndpointRoutingBenchmarkBase { - public partial class MatcherBuilderMultipleEntryBenchmark : EndpointRoutingBenchmarkBase + private IServiceProvider _services; + private List _policies; + private ILoggerFactory _loggerFactory; + private DefaultEndpointSelector _selector; + private DefaultParameterPolicyFactory _parameterPolicyFactory; + + [GlobalSetup] + public void Setup() { - private IServiceProvider _services; - private List _policies; - private ILoggerFactory _loggerFactory; - private DefaultEndpointSelector _selector; - private DefaultParameterPolicyFactory _parameterPolicyFactory; - - [GlobalSetup] - public void Setup() - { - Endpoints = new RouteEndpoint[10]; - Endpoints[0] = CreateEndpoint("/product", "GET"); - Endpoints[1] = CreateEndpoint("/product/{id}", "GET"); - - Endpoints[2] = CreateEndpoint("/account", "GET"); - Endpoints[3] = CreateEndpoint("/account/{id}"); - Endpoints[4] = CreateEndpoint("/account/{id}", "POST"); - Endpoints[5] = CreateEndpoint("/account/{id}", "UPDATE"); - - Endpoints[6] = CreateEndpoint("/v2/account", "GET"); - Endpoints[7] = CreateEndpoint("/v2/account/{id}"); - Endpoints[8] = CreateEndpoint("/v2/account/{id}", "POST"); - Endpoints[9] = CreateEndpoint("/v2/account/{id}", "UPDATE"); - - // Define an unordered mixture of policies that implement INodeBuilderPolicy, - // IEndpointComparerPolicy and/or IEndpointSelectorPolicy - _policies = new List() + Endpoints = new RouteEndpoint[10]; + Endpoints[0] = CreateEndpoint("/product", "GET"); + Endpoints[1] = CreateEndpoint("/product/{id}", "GET"); + + Endpoints[2] = CreateEndpoint("/account", "GET"); + Endpoints[3] = CreateEndpoint("/account/{id}"); + Endpoints[4] = CreateEndpoint("/account/{id}", "POST"); + Endpoints[5] = CreateEndpoint("/account/{id}", "UPDATE"); + + Endpoints[6] = CreateEndpoint("/v2/account", "GET"); + Endpoints[7] = CreateEndpoint("/v2/account/{id}"); + Endpoints[8] = CreateEndpoint("/v2/account/{id}", "POST"); + Endpoints[9] = CreateEndpoint("/v2/account/{id}", "UPDATE"); + + // Define an unordered mixture of policies that implement INodeBuilderPolicy, + // IEndpointComparerPolicy and/or IEndpointSelectorPolicy + _policies = new List() { CreateNodeBuilderPolicy(4), CreateUberPolicy(2), @@ -55,154 +55,153 @@ namespace Microsoft.AspNetCore.Routing.Matching CreateUberPolicy(12), CreateEndpointComparerPolicy(11) }; - _loggerFactory = NullLoggerFactory.Instance; - _selector = new DefaultEndpointSelector(); - _parameterPolicyFactory = new DefaultParameterPolicyFactory(Options.Create(new RouteOptions()), new TestServiceProvider()); + _loggerFactory = NullLoggerFactory.Instance; + _selector = new DefaultEndpointSelector(); + _parameterPolicyFactory = new DefaultParameterPolicyFactory(Options.Create(new RouteOptions()), new TestServiceProvider()); - _services = CreateServices(); - } + _services = CreateServices(); + } - private Matcher SetupMatcher(MatcherBuilder builder) + private Matcher SetupMatcher(MatcherBuilder builder) + { + for (int i = 0; i < Endpoints.Length; i++) { - for (int i = 0; i < Endpoints.Length; i++) - { - builder.AddEndpoint(Endpoints[i]); - } - return builder.Build(); + builder.AddEndpoint(Endpoints[i]); } + return builder.Build(); + } + + [Benchmark] + public void Dfa() + { + var builder = _services.GetRequiredService(); + SetupMatcher(builder); + } + + [Benchmark] + public void Constructor_Policies() + { + new DfaMatcherBuilder(_loggerFactory, _parameterPolicyFactory, _selector, _policies); + } + + private static MatcherPolicy CreateNodeBuilderPolicy(int order) + { + return new TestNodeBuilderPolicy(order); + } + private static MatcherPolicy CreateEndpointComparerPolicy(int order) + { + return new TestEndpointComparerPolicy(order); + } - [Benchmark] - public void Dfa() + private static MatcherPolicy CreateEndpointSelectorPolicy(int order) + { + return new TestEndpointSelectorPolicy(order); + } + + private static MatcherPolicy CreateUberPolicy(int order) + { + return new TestUberPolicy(order); + } + + private class TestUberPolicy : TestMatcherPolicyBase, INodeBuilderPolicy, IEndpointComparerPolicy + { + public TestUberPolicy(int order) : base(order) { - var builder = _services.GetRequiredService(); - SetupMatcher(builder); } - [Benchmark] - public void Constructor_Policies() + public IComparer Comparer => new TestEndpointComparer(); + + public bool AppliesToEndpoints(IReadOnlyList endpoints) { - new DfaMatcherBuilder(_loggerFactory, _parameterPolicyFactory, _selector, _policies); + return false; } - private static MatcherPolicy CreateNodeBuilderPolicy(int order) + public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) { - return new TestNodeBuilderPolicy(order); + throw new NotImplementedException(); } - private static MatcherPolicy CreateEndpointComparerPolicy(int order) + + public IReadOnlyList GetEdges(IReadOnlyList endpoints) { - return new TestEndpointComparerPolicy(order); + throw new NotImplementedException(); } + } - private static MatcherPolicy CreateEndpointSelectorPolicy(int order) + private class TestNodeBuilderPolicy : TestMatcherPolicyBase, INodeBuilderPolicy + { + public TestNodeBuilderPolicy(int order) : base(order) { - return new TestEndpointSelectorPolicy(order); } - private static MatcherPolicy CreateUberPolicy(int order) + public bool AppliesToEndpoints(IReadOnlyList endpoints) { - return new TestUberPolicy(order); + return false; } - private class TestUberPolicy : TestMatcherPolicyBase, INodeBuilderPolicy, IEndpointComparerPolicy + public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) { - public TestUberPolicy(int order) : base(order) - { - } - - public IComparer Comparer => new TestEndpointComparer(); - - public bool AppliesToEndpoints(IReadOnlyList endpoints) - { - return false; - } - - public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) - { - throw new NotImplementedException(); - } - - public IReadOnlyList GetEdges(IReadOnlyList endpoints) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } - private class TestNodeBuilderPolicy : TestMatcherPolicyBase, INodeBuilderPolicy + public IReadOnlyList GetEdges(IReadOnlyList endpoints) { - public TestNodeBuilderPolicy(int order) : base(order) - { - } - - public bool AppliesToEndpoints(IReadOnlyList endpoints) - { - return false; - } - - public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) - { - throw new NotImplementedException(); - } - - public IReadOnlyList GetEdges(IReadOnlyList endpoints) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } + } - private class TestEndpointComparerPolicy : TestMatcherPolicyBase, IEndpointComparerPolicy + private class TestEndpointComparerPolicy : TestMatcherPolicyBase, IEndpointComparerPolicy + { + public TestEndpointComparerPolicy(int order) : base(order) { - public TestEndpointComparerPolicy(int order) : base(order) - { - } - - public IComparer Comparer => new TestEndpointComparer(); + } - public bool AppliesToEndpoints(IReadOnlyList endpoints) - { - return false; - } + public IComparer Comparer => new TestEndpointComparer(); - public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) - { - throw new NotImplementedException(); - } + public bool AppliesToEndpoints(IReadOnlyList endpoints) + { + return false; } - private class TestEndpointSelectorPolicy : TestMatcherPolicyBase, IEndpointSelectorPolicy + public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { - public TestEndpointSelectorPolicy(int order) : base(order) - { - } + throw new NotImplementedException(); + } + } - public bool AppliesToEndpoints(IReadOnlyList endpoints) - { - return false; - } + private class TestEndpointSelectorPolicy : TestMatcherPolicyBase, IEndpointSelectorPolicy + { + public TestEndpointSelectorPolicy(int order) : base(order) + { + } - public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) - { - throw new NotImplementedException(); - } + public bool AppliesToEndpoints(IReadOnlyList endpoints) + { + return false; } - private abstract class TestMatcherPolicyBase : MatcherPolicy + public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { - private readonly int _order; + throw new NotImplementedException(); + } + } - protected TestMatcherPolicyBase(int order) - { - _order = order; - } + private abstract class TestMatcherPolicyBase : MatcherPolicy + { + private readonly int _order; - public override int Order { get { return _order; } } + protected TestMatcherPolicyBase(int order) + { + _order = order; } - private class TestEndpointComparer : IComparer + public override int Order { get { return _order; } } + } + + private class TestEndpointComparer : IComparer + { + public int Compare(Endpoint x, Endpoint y) { - public int Compare(Endpoint x, Endpoint y) - { - return 0; - } + return 0; } } } diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherGithubBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherGithubBenchmark.cs index 6161a411fa..1afd6ba95e 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherGithubBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherGithubBenchmark.cs @@ -6,46 +6,45 @@ using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// Generated from https://github.com/APIs-guru/openapi-directory +// Use https://editor2.swagger.io/ to convert from yaml to json- +public class MatcherGithubBenchmark : MatcherGithubBenchmarkBase { - // Generated from https://github.com/APIs-guru/openapi-directory - // Use https://editor2.swagger.io/ to convert from yaml to json- - public class MatcherGithubBenchmark : MatcherGithubBenchmarkBase - { - private BarebonesMatcher _baseline; - private Matcher _dfa; + private BarebonesMatcher _baseline; + private Matcher _dfa; - [GlobalSetup] - public void Setup() - { - SetupEndpoints(); + [GlobalSetup] + public void Setup() + { + SetupEndpoints(); - SetupRequests(); + SetupRequests(); - _baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder()); - _dfa = SetupMatcher(CreateDfaMatcherBuilder()); - } + _baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder()); + _dfa = SetupMatcher(CreateDfaMatcherBuilder()); + } - [Benchmark(Baseline = true, OperationsPerInvoke = EndpointCount)] - public async Task Baseline() + [Benchmark(Baseline = true, OperationsPerInvoke = EndpointCount)] + public async Task Baseline() + { + for (var i = 0; i < EndpointCount; i++) { - for (var i = 0; i < EndpointCount; i++) - { - var httpContext = Requests[i]; - await _baseline.Matchers[i].MatchAsync(httpContext); - Validate(httpContext, Endpoints[i], httpContext.GetEndpoint()); - } + var httpContext = Requests[i]; + await _baseline.Matchers[i].MatchAsync(httpContext); + Validate(httpContext, Endpoints[i], httpContext.GetEndpoint()); } + } - [Benchmark( OperationsPerInvoke = EndpointCount)] - public async Task Dfa() + [Benchmark(OperationsPerInvoke = EndpointCount)] + public async Task Dfa() + { + for (var i = 0; i < EndpointCount; i++) { - for (var i = 0; i < EndpointCount; i++) - { - var httpContext = Requests[i]; - await _dfa.MatchAsync(httpContext); - Validate(httpContext, Endpoints[i], httpContext.GetEndpoint()); - } + var httpContext = Requests[i]; + await _dfa.MatchAsync(httpContext); + Validate(httpContext, Endpoints[i], httpContext.GetEndpoint()); } } } diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherSingleEntryBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherSingleEntryBenchmark.cs index 6ce2ccecaf..fcaefdf658 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherSingleEntryBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/MatcherSingleEntryBenchmark.cs @@ -6,73 +6,72 @@ using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// Just like TechEmpower Plaintext +public partial class MatcherSingleEntryBenchmark : EndpointRoutingBenchmarkBase { - // Just like TechEmpower Plaintext - public partial class MatcherSingleEntryBenchmark : EndpointRoutingBenchmarkBase - { - private BarebonesMatcher _baseline; - private Matcher _dfa; - private Matcher _route; - private Matcher _tree; + private BarebonesMatcher _baseline; + private Matcher _dfa; + private Matcher _route; + private Matcher _tree; - [GlobalSetup] - public void Setup() - { - Endpoints = new RouteEndpoint[1]; - Endpoints[0] = CreateEndpoint("/plaintext"); + [GlobalSetup] + public void Setup() + { + Endpoints = new RouteEndpoint[1]; + Endpoints[0] = CreateEndpoint("/plaintext"); - Requests = new HttpContext[1]; - Requests[0] = new DefaultHttpContext(); - Requests[0].RequestServices = CreateServices(); - Requests[0].Request.Path = "/plaintext"; + Requests = new HttpContext[1]; + Requests[0] = new DefaultHttpContext(); + Requests[0].RequestServices = CreateServices(); + Requests[0].Request.Path = "/plaintext"; - _baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder()); - _dfa = SetupMatcher(CreateDfaMatcherBuilder()); - _route = SetupMatcher(new RouteMatcherBuilder()); - _tree = SetupMatcher(new TreeRouterMatcherBuilder()); - } + _baseline = (BarebonesMatcher)SetupMatcher(new BarebonesMatcherBuilder()); + _dfa = SetupMatcher(CreateDfaMatcherBuilder()); + _route = SetupMatcher(new RouteMatcherBuilder()); + _tree = SetupMatcher(new TreeRouterMatcherBuilder()); + } - private Matcher SetupMatcher(MatcherBuilder builder) - { - builder.AddEndpoint(Endpoints[0]); - return builder.Build(); - } + private Matcher SetupMatcher(MatcherBuilder builder) + { + builder.AddEndpoint(Endpoints[0]); + return builder.Build(); + } - [Benchmark(Baseline = true)] - public async Task Baseline() - { - var httpContext = Requests[0]; + [Benchmark(Baseline = true)] + public async Task Baseline() + { + var httpContext = Requests[0]; - await _baseline.MatchAsync(httpContext); - Validate(httpContext, Endpoints[0], httpContext.GetEndpoint()); - } + await _baseline.MatchAsync(httpContext); + Validate(httpContext, Endpoints[0], httpContext.GetEndpoint()); + } - [Benchmark] - public async Task Dfa() - { - var httpContext = Requests[0]; + [Benchmark] + public async Task Dfa() + { + var httpContext = Requests[0]; - await _dfa.MatchAsync(httpContext); - Validate(httpContext, Endpoints[0], httpContext.GetEndpoint()); - } + await _dfa.MatchAsync(httpContext); + Validate(httpContext, Endpoints[0], httpContext.GetEndpoint()); + } - [Benchmark] - public async Task LegacyTreeRouter() - { - var httpContext = Requests[0]; + [Benchmark] + public async Task LegacyTreeRouter() + { + var httpContext = Requests[0]; - await _tree.MatchAsync(httpContext); - Validate(httpContext, Endpoints[0], httpContext.GetEndpoint()); - } + await _tree.MatchAsync(httpContext); + Validate(httpContext, Endpoints[0], httpContext.GetEndpoint()); + } - [Benchmark] - public async Task LegacyRouter() - { - var httpContext = Requests[0]; + [Benchmark] + public async Task LegacyRouter() + { + var httpContext = Requests[0]; - await _route.MatchAsync(httpContext); - Validate(httpContext, Endpoints[0], httpContext.GetEndpoint()); - } + await _route.MatchAsync(httpContext); + Validate(httpContext, Endpoints[0], httpContext.GetEndpoint()); } } diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/RouteEndpointAzureBenchmark.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/RouteEndpointAzureBenchmark.cs index 392d82070b..0652ba306b 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/RouteEndpointAzureBenchmark.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/RouteEndpointAzureBenchmark.cs @@ -3,14 +3,13 @@ using BenchmarkDotNet.Attributes; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class RouteEndpointAzureBenchmark : MatcherAzureBenchmarkBase { - public class RouteEndpointAzureBenchmark : MatcherAzureBenchmarkBase + [Benchmark] + public void CreateEndpoints() { - [Benchmark] - public void CreateEndpoints() - { - SetupEndpoints(); - } + SetupEndpoints(); } -} \ No newline at end of file +} diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/TrivialMatcher.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/TrivialMatcher.cs index 3543a2bb38..775e81c544 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/TrivialMatcher.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/TrivialMatcher.cs @@ -6,49 +6,48 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// A test-only matcher implementation - used as a baseline for simpler +// perf tests. The idea with this matcher is that we can cheat on the requirements +// to establish a lower bound for perf comparisons. +internal sealed class TrivialMatcher : Matcher { - // A test-only matcher implementation - used as a baseline for simpler - // perf tests. The idea with this matcher is that we can cheat on the requirements - // to establish a lower bound for perf comparisons. - internal sealed class TrivialMatcher : Matcher + private readonly RouteEndpoint _endpoint; + private readonly Candidate[] _candidates; + + public TrivialMatcher(RouteEndpoint endpoint) { - private readonly RouteEndpoint _endpoint; - private readonly Candidate[] _candidates; + _endpoint = endpoint; - public TrivialMatcher(RouteEndpoint endpoint) - { - _endpoint = endpoint; + _candidates = new Candidate[] { new Candidate(endpoint), }; + } - _candidates = new Candidate[] { new Candidate(endpoint), }; + public sealed override Task MatchAsync(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); } - public sealed override Task MatchAsync(HttpContext httpContext) + var path = httpContext.Request.Path.Value; + if (string.Equals(_endpoint.RoutePattern.RawText, path, StringComparison.OrdinalIgnoreCase)) { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - var path = httpContext.Request.Path.Value; - if (string.Equals(_endpoint.RoutePattern.RawText, path, StringComparison.OrdinalIgnoreCase)) - { - httpContext.SetEndpoint(_endpoint); - httpContext.Request.RouteValues = new RouteValueDictionary(); - } - - return Task.CompletedTask; + httpContext.SetEndpoint(_endpoint); + httpContext.Request.RouteValues = new RouteValueDictionary(); } - // This is here so this can be tested alongside DFA matcher. - internal Candidate[] FindCandidateSet(string path, ReadOnlySpan segments) - { - if (string.Equals(_endpoint.RoutePattern.RawText, path, StringComparison.OrdinalIgnoreCase)) - { - return _candidates; - } + return Task.CompletedTask; + } - return Array.Empty(); + // This is here so this can be tested alongside DFA matcher. + internal Candidate[] FindCandidateSet(string path, ReadOnlySpan segments) + { + if (string.Equals(_endpoint.RoutePattern.RawText, path, StringComparison.OrdinalIgnoreCase)) + { + return _candidates; } + + return Array.Empty(); } } diff --git a/src/Http/Routing/perf/Microbenchmarks/Matching/TrivialMatcherBuilder.cs b/src/Http/Routing/perf/Microbenchmarks/Matching/TrivialMatcherBuilder.cs index b8bac787a7..48bca301c0 100644 --- a/src/Http/Routing/perf/Microbenchmarks/Matching/TrivialMatcherBuilder.cs +++ b/src/Http/Routing/perf/Microbenchmarks/Matching/TrivialMatcherBuilder.cs @@ -4,20 +4,19 @@ using System.Collections.Generic; using System.Linq; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal class TrivialMatcherBuilder : MatcherBuilder { - internal class TrivialMatcherBuilder : MatcherBuilder - { - private readonly List _endpoints = new List(); + private readonly List _endpoints = new List(); - public override void AddEndpoint(RouteEndpoint endpoint) - { - _endpoints.Add(endpoint); - } + public override void AddEndpoint(RouteEndpoint endpoint) + { + _endpoints.Add(endpoint); + } - public override Matcher Build() - { - return new TrivialMatcher(_endpoints.Last()); - } + public override Matcher Build() + { + return new TrivialMatcher(_endpoints.Last()); } } diff --git a/src/Http/Routing/src/ArrayBuilder.cs b/src/Http/Routing/src/ArrayBuilder.cs index 21129fc800..225cefcf4a 100644 --- a/src/Http/Routing/src/ArrayBuilder.cs +++ b/src/Http/Routing/src/ArrayBuilder.cs @@ -10,160 +10,159 @@ using System; using System.Diagnostics; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Helper type for avoiding allocations while building arrays. +/// +/// The element type. +internal struct ArrayBuilder { + private const int DefaultCapacity = 4; + private const int MaxCoreClrArrayLength = 0x7fefffff; // For byte arrays the limit is slightly larger + + private T[] _array; // Starts out null, initialized on first Add. + private int _count; // Number of items into _array we're using. + /// - /// Helper type for avoiding allocations while building arrays. + /// Initializes the with a specified capacity. /// - /// The element type. - internal struct ArrayBuilder + /// The capacity of the array to allocate. + public ArrayBuilder(int capacity) : this() { - private const int DefaultCapacity = 4; - private const int MaxCoreClrArrayLength = 0x7fefffff; // For byte arrays the limit is slightly larger + Debug.Assert(capacity >= 0); + if (capacity > 0) + { + _array = new T[capacity]; + } + } - private T[] _array; // Starts out null, initialized on first Add. - private int _count; // Number of items into _array we're using. + /// + /// Gets the number of items this instance can store without re-allocating, + /// or 0 if the backing array is null. + /// + public int Capacity => _array?.Length ?? 0; + + /// Gets the current underlying array. + public T[] Buffer => _array; + + /// + /// Gets the number of items in the array currently in use. + /// + public int Count => _count; - /// - /// Initializes the with a specified capacity. - /// - /// The capacity of the array to allocate. - public ArrayBuilder(int capacity) : this() + /// + /// Gets or sets the item at a certain index in the array. + /// + /// The index into the array. + public T this[int index] + { + get { - Debug.Assert(capacity >= 0); - if (capacity > 0) - { - _array = new T[capacity]; - } + Debug.Assert(index >= 0 && index < _count); + return _array[index]; } + } - /// - /// Gets the number of items this instance can store without re-allocating, - /// or 0 if the backing array is null. - /// - public int Capacity => _array?.Length ?? 0; - - /// Gets the current underlying array. - public T[] Buffer => _array; - - /// - /// Gets the number of items in the array currently in use. - /// - public int Count => _count; - - /// - /// Gets or sets the item at a certain index in the array. - /// - /// The index into the array. - public T this[int index] + /// + /// Adds an item to the backing array, resizing it if necessary. + /// + /// The item to add. + public void Add(T item) + { + if (_count == Capacity) { - get - { - Debug.Assert(index >= 0 && index < _count); - return _array[index]; - } + EnsureCapacity(_count + 1); } - /// - /// Adds an item to the backing array, resizing it if necessary. - /// - /// The item to add. - public void Add(T item) - { - if (_count == Capacity) - { - EnsureCapacity(_count + 1); - } + UncheckedAdd(item); + } - UncheckedAdd(item); - } + /// + /// Gets the first item in this builder. + /// + public T First() + { + Debug.Assert(_count > 0); + return _array[0]; + } - /// - /// Gets the first item in this builder. - /// - public T First() - { - Debug.Assert(_count > 0); - return _array[0]; - } + /// + /// Gets the last item in this builder. + /// + public T Last() + { + Debug.Assert(_count > 0); + return _array[_count - 1]; + } - /// - /// Gets the last item in this builder. - /// - public T Last() + /// + /// Creates an array from the contents of this builder. + /// + /// + /// Do not call this method twice on the same builder. + /// + public T[] ToArray() + { + if (_count == 0) { - Debug.Assert(_count > 0); - return _array[_count - 1]; + return Array.Empty(); } - /// - /// Creates an array from the contents of this builder. - /// - /// - /// Do not call this method twice on the same builder. - /// - public T[] ToArray() + Debug.Assert(_array != null); // Nonzero _count should imply this + + T[] result = _array; + if (_count < result.Length) { - if (_count == 0) - { - return Array.Empty(); - } - - Debug.Assert(_array != null); // Nonzero _count should imply this - - T[] result = _array; - if (_count < result.Length) - { - // Avoid a bit of overhead (method call, some branches, extra codegen) - // which would be incurred by using Array.Resize - result = new T[_count]; - Array.Copy(_array, 0, result, 0, _count); - } + // Avoid a bit of overhead (method call, some branches, extra codegen) + // which would be incurred by using Array.Resize + result = new T[_count]; + Array.Copy(_array, 0, result, 0, _count); + } #if DEBUG - // Try to prevent callers from using the ArrayBuilder after ToArray, if _count != 0. - _count = -1; - _array = null; + // Try to prevent callers from using the ArrayBuilder after ToArray, if _count != 0. + _count = -1; + _array = null; #endif - return result; - } + return result; + } - /// - /// Adds an item to the backing array, without checking if there is room. - /// - /// The item to add. - /// - /// Use this method if you know there is enough space in the - /// for another item, and you are writing performance-sensitive code. - /// - public void UncheckedAdd(T item) - { - Debug.Assert(_count < Capacity); + /// + /// Adds an item to the backing array, without checking if there is room. + /// + /// The item to add. + /// + /// Use this method if you know there is enough space in the + /// for another item, and you are writing performance-sensitive code. + /// + public void UncheckedAdd(T item) + { + Debug.Assert(_count < Capacity); - _array[_count++] = item; - } + _array[_count++] = item; + } - private void EnsureCapacity(int minimum) - { - Debug.Assert(minimum > Capacity); + private void EnsureCapacity(int minimum) + { + Debug.Assert(minimum > Capacity); - int capacity = Capacity; - int nextCapacity = capacity == 0 ? DefaultCapacity : 2 * capacity; + int capacity = Capacity; + int nextCapacity = capacity == 0 ? DefaultCapacity : 2 * capacity; - if ((uint)nextCapacity > (uint)MaxCoreClrArrayLength) - { - nextCapacity = Math.Max(capacity + 1, MaxCoreClrArrayLength); - } + if ((uint)nextCapacity > (uint)MaxCoreClrArrayLength) + { + nextCapacity = Math.Max(capacity + 1, MaxCoreClrArrayLength); + } - nextCapacity = Math.Max(nextCapacity, minimum); + nextCapacity = Math.Max(nextCapacity, minimum); - T[] next = new T[nextCapacity]; - if (_count > 0) - { - Array.Copy(_array, 0, next, 0, _count); - } - _array = next; + T[] next = new T[nextCapacity]; + if (_count > 0) + { + Array.Copy(_array, 0, next, 0, _count); } + _array = next; } } diff --git a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs index e7f92e4354..e99250d89c 100644 --- a/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/EndpointRouteBuilderExtensions.cs @@ -11,535 +11,534 @@ using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Provides extension methods for to add endpoints. +/// +public static class EndpointRouteBuilderExtensions { + // Avoid creating a new array every call + private static readonly string[] GetVerb = new[] { "GET" }; + private static readonly string[] PostVerb = new[] { "POST" }; + private static readonly string[] PutVerb = new[] { "PUT" }; + private static readonly string[] DeleteVerb = new[] { "DELETE" }; + private static readonly string[] PatchVerb = new[] { "PATCH" }; + /// + /// Adds a to the that matches HTTP GET requests + /// for the specified pattern. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + public static IEndpointConventionBuilder MapGet( + this IEndpointRouteBuilder endpoints, + string pattern, + RequestDelegate requestDelegate) + { + return MapMethods(endpoints, pattern, GetVerb, requestDelegate); + } + + /// + /// Adds a to the that matches HTTP POST requests + /// for the specified pattern. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + public static IEndpointConventionBuilder MapPost( + this IEndpointRouteBuilder endpoints, + string pattern, + RequestDelegate requestDelegate) + { + return MapMethods(endpoints, pattern, PostVerb, requestDelegate); + } + + /// + /// Adds a to the that matches HTTP PUT requests + /// for the specified pattern. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + public static IEndpointConventionBuilder MapPut( + this IEndpointRouteBuilder endpoints, + string pattern, + RequestDelegate requestDelegate) + { + return MapMethods(endpoints, pattern, PutVerb, requestDelegate); + } + + /// + /// Adds a to the that matches HTTP DELETE requests + /// for the specified pattern. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + public static IEndpointConventionBuilder MapDelete( + this IEndpointRouteBuilder endpoints, + string pattern, + RequestDelegate requestDelegate) + { + return MapMethods(endpoints, pattern, DeleteVerb, requestDelegate); + } + + /// + /// Adds a to the that matches HTTP PATCH requests + /// for the specified pattern. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + public static IEndpointConventionBuilder MapPatch( + this IEndpointRouteBuilder endpoints, + string pattern, + RequestDelegate requestDelegate) + { + return MapMethods(endpoints, pattern, PatchVerb, requestDelegate); + } + /// - /// Provides extension methods for to add endpoints. + /// Adds a to the that matches HTTP requests + /// for the specified HTTP methods and pattern. /// - public static class EndpointRouteBuilderExtensions + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// HTTP methods that the endpoint will match. + /// A that can be used to further customize the endpoint. + public static IEndpointConventionBuilder MapMethods( + this IEndpointRouteBuilder endpoints, + string pattern, + IEnumerable httpMethods, + RequestDelegate requestDelegate) { - // Avoid creating a new array every call - private static readonly string[] GetVerb = new[] { "GET" }; - private static readonly string[] PostVerb = new[] { "POST" }; - private static readonly string[] PutVerb = new[] { "PUT" }; - private static readonly string[] DeleteVerb = new[] { "DELETE" }; - private static readonly string[] PatchVerb = new[] { "PATCH" }; - /// - /// Adds a to the that matches HTTP GET requests - /// for the specified pattern. - /// - /// The to add the route to. - /// The route pattern. - /// The delegate executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - public static IEndpointConventionBuilder MapGet( - this IEndpointRouteBuilder endpoints, - string pattern, - RequestDelegate requestDelegate) + if (httpMethods == null) { - return MapMethods(endpoints, pattern, GetVerb, requestDelegate); + throw new ArgumentNullException(nameof(httpMethods)); } - /// - /// Adds a to the that matches HTTP POST requests - /// for the specified pattern. - /// - /// The to add the route to. - /// The route pattern. - /// The delegate executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - public static IEndpointConventionBuilder MapPost( - this IEndpointRouteBuilder endpoints, - string pattern, - RequestDelegate requestDelegate) + var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), requestDelegate); + builder.WithDisplayName($"{pattern} HTTP: {string.Join(", ", httpMethods)}"); + builder.WithMetadata(new HttpMethodMetadata(httpMethods)); + return builder; + } + + /// + /// Adds a to the that matches HTTP requests + /// for the specified pattern. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + public static IEndpointConventionBuilder Map( + this IEndpointRouteBuilder endpoints, + string pattern, + RequestDelegate requestDelegate) + { + return Map(endpoints, RoutePatternFactory.Parse(pattern), requestDelegate); + } + + /// + /// Adds a to the that matches HTTP requests + /// for the specified pattern. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + public static IEndpointConventionBuilder Map( + this IEndpointRouteBuilder endpoints, + RoutePattern pattern, + RequestDelegate requestDelegate) + { + if (endpoints == null) { - return MapMethods(endpoints, pattern, PostVerb, requestDelegate); + throw new ArgumentNullException(nameof(endpoints)); } - /// - /// Adds a to the that matches HTTP PUT requests - /// for the specified pattern. - /// - /// The to add the route to. - /// The route pattern. - /// The delegate executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - public static IEndpointConventionBuilder MapPut( - this IEndpointRouteBuilder endpoints, - string pattern, - RequestDelegate requestDelegate) + if (pattern == null) { - return MapMethods(endpoints, pattern, PutVerb, requestDelegate); + throw new ArgumentNullException(nameof(pattern)); } - /// - /// Adds a to the that matches HTTP DELETE requests - /// for the specified pattern. - /// - /// The to add the route to. - /// The route pattern. - /// The delegate executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - public static IEndpointConventionBuilder MapDelete( - this IEndpointRouteBuilder endpoints, - string pattern, - RequestDelegate requestDelegate) + if (requestDelegate == null) { - return MapMethods(endpoints, pattern, DeleteVerb, requestDelegate); + throw new ArgumentNullException(nameof(requestDelegate)); } - /// - /// Adds a to the that matches HTTP PATCH requests - /// for the specified pattern. - /// - /// The to add the route to. - /// The route pattern. - /// The delegate executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - public static IEndpointConventionBuilder MapPatch( - this IEndpointRouteBuilder endpoints, - string pattern, - RequestDelegate requestDelegate) + const int defaultOrder = 0; + + var builder = new RouteEndpointBuilder( + requestDelegate, + pattern, + defaultOrder) { - return MapMethods(endpoints, pattern, PatchVerb, requestDelegate); - } + DisplayName = pattern.RawText ?? pattern.DebuggerToString(), + }; - /// - /// Adds a to the that matches HTTP requests - /// for the specified HTTP methods and pattern. - /// - /// The to add the route to. - /// The route pattern. - /// The delegate executed when the endpoint is matched. - /// HTTP methods that the endpoint will match. - /// A that can be used to further customize the endpoint. - public static IEndpointConventionBuilder MapMethods( - this IEndpointRouteBuilder endpoints, - string pattern, - IEnumerable httpMethods, - RequestDelegate requestDelegate) + // Add delegate attributes as metadata + var attributes = requestDelegate.Method.GetCustomAttributes(); + + // This can be null if the delegate is a dynamic method or compiled from an expression tree + if (attributes != null) { - if (httpMethods == null) + foreach (var attribute in attributes) { - throw new ArgumentNullException(nameof(httpMethods)); + builder.Metadata.Add(attribute); } - - var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), requestDelegate); - builder.WithDisplayName($"{pattern} HTTP: {string.Join(", ", httpMethods)}"); - builder.WithMetadata(new HttpMethodMetadata(httpMethods)); - return builder; } - /// - /// Adds a to the that matches HTTP requests - /// for the specified pattern. - /// - /// The to add the route to. - /// The route pattern. - /// The delegate executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - public static IEndpointConventionBuilder Map( - this IEndpointRouteBuilder endpoints, - string pattern, - RequestDelegate requestDelegate) + var dataSource = endpoints.DataSources.OfType().FirstOrDefault(); + if (dataSource == null) { - return Map(endpoints, RoutePatternFactory.Parse(pattern), requestDelegate); + dataSource = new ModelEndpointDataSource(); + endpoints.DataSources.Add(dataSource); } - /// - /// Adds a to the that matches HTTP requests - /// for the specified pattern. - /// - /// The to add the route to. - /// The route pattern. - /// The delegate executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - public static IEndpointConventionBuilder Map( - this IEndpointRouteBuilder endpoints, - RoutePattern pattern, - RequestDelegate requestDelegate) - { - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } - - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } - - if (requestDelegate == null) - { - throw new ArgumentNullException(nameof(requestDelegate)); - } + return dataSource.AddEndpointBuilder(builder); + } - const int defaultOrder = 0; - var builder = new RouteEndpointBuilder( - requestDelegate, - pattern, - defaultOrder) - { - DisplayName = pattern.RawText ?? pattern.DebuggerToString(), - }; + /// + /// Adds a to the that matches HTTP GET requests + /// for the specified pattern. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder MapGet( + this IEndpointRouteBuilder endpoints, + string pattern, + Delegate handler) + { + return MapMethods(endpoints, pattern, GetVerb, handler); + } - // Add delegate attributes as metadata - var attributes = requestDelegate.Method.GetCustomAttributes(); + /// + /// Adds a to the that matches HTTP POST requests + /// for the specified pattern. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder MapPost( + this IEndpointRouteBuilder endpoints, + string pattern, + Delegate handler) + { + return MapMethods(endpoints, pattern, PostVerb, handler); + } - // This can be null if the delegate is a dynamic method or compiled from an expression tree - if (attributes != null) - { - foreach (var attribute in attributes) - { - builder.Metadata.Add(attribute); - } - } + /// + /// Adds a to the that matches HTTP PUT requests + /// for the specified pattern. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder MapPut( + this IEndpointRouteBuilder endpoints, + string pattern, + Delegate handler) + { + return MapMethods(endpoints, pattern, PutVerb, handler); + } - var dataSource = endpoints.DataSources.OfType().FirstOrDefault(); - if (dataSource == null) - { - dataSource = new ModelEndpointDataSource(); - endpoints.DataSources.Add(dataSource); - } + /// + /// Adds a to the that matches HTTP DELETE requests + /// for the specified pattern. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder MapDelete( + this IEndpointRouteBuilder endpoints, + string pattern, + Delegate handler) + { + return MapMethods(endpoints, pattern, DeleteVerb, handler); + } - return dataSource.AddEndpointBuilder(builder); - } + /// + /// Adds a to the that matches HTTP PATCH requests + /// for the specified pattern. + /// + /// The to add the route to. + /// The route pattern. + /// The executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder MapPatch( + this IEndpointRouteBuilder endpoints, + string pattern, + Delegate handler) + { + return MapMethods(endpoints, pattern, PatchVerb, handler); + } - /// - /// Adds a to the that matches HTTP GET requests - /// for the specified pattern. - /// - /// The to add the route to. - /// The route pattern. - /// The delegate executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder MapGet( - this IEndpointRouteBuilder endpoints, - string pattern, - Delegate handler) + /// + /// Adds a to the that matches HTTP requests + /// for the specified HTTP methods and pattern. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// HTTP methods that the endpoint will match. + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder MapMethods( + this IEndpointRouteBuilder endpoints, + string pattern, + IEnumerable httpMethods, + Delegate handler) + { + if (httpMethods is null) { - return MapMethods(endpoints, pattern, GetVerb, handler); + throw new ArgumentNullException(nameof(httpMethods)); } - /// - /// Adds a to the that matches HTTP POST requests - /// for the specified pattern. - /// - /// The to add the route to. - /// The route pattern. - /// The delegate executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder MapPost( - this IEndpointRouteBuilder endpoints, - string pattern, - Delegate handler) + var disableInferredBody = false; + foreach (var method in httpMethods) { - return MapMethods(endpoints, pattern, PostVerb, handler); + disableInferredBody = ShouldDisableInferredBody(method); + if (disableInferredBody is true) + { + break; + } } - /// - /// Adds a to the that matches HTTP PUT requests - /// for the specified pattern. - /// - /// The to add the route to. - /// The route pattern. - /// The delegate executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder MapPut( - this IEndpointRouteBuilder endpoints, - string pattern, - Delegate handler) - { - return MapMethods(endpoints, pattern, PutVerb, handler); - } + var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), handler, disableInferredBody); + // Prepends the HTTP method to the DisplayName produced with pattern + method name + builder.Add(b => b.DisplayName = $"HTTP: {string.Join(", ", httpMethods)} {b.DisplayName}"); + builder.WithMetadata(new HttpMethodMetadata(httpMethods)); + return builder; - /// - /// Adds a to the that matches HTTP DELETE requests - /// for the specified pattern. - /// - /// The to add the route to. - /// The route pattern. - /// The delegate executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder MapDelete( - this IEndpointRouteBuilder endpoints, - string pattern, - Delegate handler) + static bool ShouldDisableInferredBody(string method) { - return MapMethods(endpoints, pattern, DeleteVerb, handler); + // GET, DELETE, HEAD, CONNECT, TRACE, and OPTIONS normally do not contain bodies + return method.Equals(HttpMethods.Get, StringComparison.Ordinal) || + method.Equals(HttpMethods.Delete, StringComparison.Ordinal) || + method.Equals(HttpMethods.Head, StringComparison.Ordinal) || + method.Equals(HttpMethods.Options, StringComparison.Ordinal) || + method.Equals(HttpMethods.Trace, StringComparison.Ordinal) || + method.Equals(HttpMethods.Connect, StringComparison.Ordinal); } + } + + /// + /// Adds a to the that matches HTTP requests + /// for the specified pattern. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder Map( + this IEndpointRouteBuilder endpoints, + string pattern, + Delegate handler) + { + return Map(endpoints, RoutePatternFactory.Parse(pattern), handler); + } + /// + /// Adds a to the that matches HTTP requests + /// for the specified pattern. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder Map( + this IEndpointRouteBuilder endpoints, + RoutePattern pattern, + Delegate handler) + { + return Map(endpoints, pattern, handler, disableInferBodyFromParameters: false); + } - /// - /// Adds a to the that matches HTTP PATCH requests - /// for the specified pattern. - /// - /// The to add the route to. - /// The route pattern. - /// The executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder MapPatch( - this IEndpointRouteBuilder endpoints, - string pattern, - Delegate handler) + /// + /// Adds a specialized to the that will match + /// requests for non-file-names with the lowest possible priority. + /// + /// The to add the route to. + /// The delegate executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + /// + /// + /// is intended to handle cases where URL path of + /// the request does not contain a file name, and no other endpoint has matched. This is convenient for routing + /// requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to + /// result in an HTTP 404. + /// + /// + /// registers an endpoint using the pattern + /// {*path:nonfile}. The order of the registered endpoint will be int.MaxValue. + /// + /// + public static RouteHandlerBuilder MapFallback(this IEndpointRouteBuilder endpoints, Delegate handler) + { + if (endpoints == null) { - return MapMethods(endpoints, pattern, PatchVerb, handler); + throw new ArgumentNullException(nameof(endpoints)); } - /// - /// Adds a to the that matches HTTP requests - /// for the specified HTTP methods and pattern. - /// - /// The to add the route to. - /// The route pattern. - /// The delegate executed when the endpoint is matched. - /// HTTP methods that the endpoint will match. - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder MapMethods( - this IEndpointRouteBuilder endpoints, - string pattern, - IEnumerable httpMethods, - Delegate handler) + if (handler == null) { - if (httpMethods is null) - { - throw new ArgumentNullException(nameof(httpMethods)); - } - - var disableInferredBody = false; - foreach (var method in httpMethods) - { - disableInferredBody = ShouldDisableInferredBody(method); - if (disableInferredBody is true) - { - break; - } - } - - var builder = endpoints.Map(RoutePatternFactory.Parse(pattern), handler, disableInferredBody); - // Prepends the HTTP method to the DisplayName produced with pattern + method name - builder.Add(b => b.DisplayName = $"HTTP: {string.Join(", ", httpMethods)} {b.DisplayName}"); - builder.WithMetadata(new HttpMethodMetadata(httpMethods)); - return builder; - - static bool ShouldDisableInferredBody(string method) - { - // GET, DELETE, HEAD, CONNECT, TRACE, and OPTIONS normally do not contain bodies - return method.Equals(HttpMethods.Get, StringComparison.Ordinal) || - method.Equals(HttpMethods.Delete, StringComparison.Ordinal) || - method.Equals(HttpMethods.Head, StringComparison.Ordinal) || - method.Equals(HttpMethods.Options, StringComparison.Ordinal) || - method.Equals(HttpMethods.Trace, StringComparison.Ordinal) || - method.Equals(HttpMethods.Connect, StringComparison.Ordinal); - } + throw new ArgumentNullException(nameof(handler)); } - /// - /// Adds a to the that matches HTTP requests - /// for the specified pattern. - /// - /// The to add the route to. - /// The route pattern. - /// The delegate executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder Map( - this IEndpointRouteBuilder endpoints, - string pattern, - Delegate handler) + return endpoints.MapFallback("{*path:nonfile}", handler); + } + + /// + /// Adds a specialized to the that will match + /// the provided pattern with the lowest possible priority. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + /// + /// + /// is intended to handle cases where no + /// other endpoint has matched. This is convenient for routing requests to a SPA framework. + /// + /// + /// The order of the registered endpoint will be int.MaxValue. + /// + /// + /// This overload will use the provided verbatim. Use the :nonfile route constraint + /// to exclude requests for static files. + /// + /// + public static RouteHandlerBuilder MapFallback( + this IEndpointRouteBuilder endpoints, + string pattern, + Delegate handler) + { + if (endpoints == null) { - return Map(endpoints, RoutePatternFactory.Parse(pattern), handler); + throw new ArgumentNullException(nameof(endpoints)); } - /// - /// Adds a to the that matches HTTP requests - /// for the specified pattern. - /// - /// The to add the route to. - /// The route pattern. - /// The delegate executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder Map( - this IEndpointRouteBuilder endpoints, - RoutePattern pattern, - Delegate handler) + if (pattern == null) { - return Map(endpoints, pattern, handler, disableInferBodyFromParameters: false); + throw new ArgumentNullException(nameof(pattern)); } - /// - /// Adds a specialized to the that will match - /// requests for non-file-names with the lowest possible priority. - /// - /// The to add the route to. - /// The delegate executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - /// - /// - /// is intended to handle cases where URL path of - /// the request does not contain a file name, and no other endpoint has matched. This is convenient for routing - /// requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to - /// result in an HTTP 404. - /// - /// - /// registers an endpoint using the pattern - /// {*path:nonfile}. The order of the registered endpoint will be int.MaxValue. - /// - /// - public static RouteHandlerBuilder MapFallback(this IEndpointRouteBuilder endpoints, Delegate handler) + if (handler == null) { - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } + throw new ArgumentNullException(nameof(handler)); + } - if (handler == null) - { - throw new ArgumentNullException(nameof(handler)); - } + var conventionBuilder = endpoints.Map(pattern, handler); + conventionBuilder.WithDisplayName("Fallback " + pattern); + conventionBuilder.Add(b => ((RouteEndpointBuilder)b).Order = int.MaxValue); + return conventionBuilder; + } - return endpoints.MapFallback("{*path:nonfile}", handler); + private static RouteHandlerBuilder Map( + this IEndpointRouteBuilder endpoints, + RoutePattern pattern, + Delegate handler, + bool disableInferBodyFromParameters) + { + if (endpoints is null) + { + throw new ArgumentNullException(nameof(endpoints)); } - /// - /// Adds a specialized to the that will match - /// the provided pattern with the lowest possible priority. - /// - /// The to add the route to. - /// The route pattern. - /// The delegate executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - /// - /// - /// is intended to handle cases where no - /// other endpoint has matched. This is convenient for routing requests to a SPA framework. - /// - /// - /// The order of the registered endpoint will be int.MaxValue. - /// - /// - /// This overload will use the provided verbatim. Use the :nonfile route constraint - /// to exclude requests for static files. - /// - /// - public static RouteHandlerBuilder MapFallback( - this IEndpointRouteBuilder endpoints, - string pattern, - Delegate handler) + if (pattern is null) { - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } - - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } - - if (handler == null) - { - throw new ArgumentNullException(nameof(handler)); - } - - var conventionBuilder = endpoints.Map(pattern, handler); - conventionBuilder.WithDisplayName("Fallback " + pattern); - conventionBuilder.Add(b => ((RouteEndpointBuilder)b).Order = int.MaxValue); - return conventionBuilder; + throw new ArgumentNullException(nameof(pattern)); } - private static RouteHandlerBuilder Map( - this IEndpointRouteBuilder endpoints, - RoutePattern pattern, - Delegate handler, - bool disableInferBodyFromParameters) + if (handler is null) { - if (endpoints is null) - { - throw new ArgumentNullException(nameof(endpoints)); - } - - if (pattern is null) - { - throw new ArgumentNullException(nameof(pattern)); - } - - if (handler is null) - { - throw new ArgumentNullException(nameof(handler)); - } + throw new ArgumentNullException(nameof(handler)); + } - const int defaultOrder = 0; + const int defaultOrder = 0; - var routeParams = new List(pattern.Parameters.Count); - foreach (var part in pattern.Parameters) - { - routeParams.Add(part.Name); - } - - var routeHandlerOptions = endpoints.ServiceProvider?.GetService>(); + var routeParams = new List(pattern.Parameters.Count); + foreach (var part in pattern.Parameters) + { + routeParams.Add(part.Name); + } - var options = new RequestDelegateFactoryOptions - { - ServiceProvider = endpoints.ServiceProvider, - RouteParameterNames = routeParams, - ThrowOnBadRequest = routeHandlerOptions?.Value.ThrowOnBadRequest ?? false, - DisableInferBodyFromParameters = disableInferBodyFromParameters, - }; - - var requestDelegateResult = RequestDelegateFactory.Create(handler, options); - - var builder = new RouteEndpointBuilder( - requestDelegateResult.RequestDelegate, - pattern, - defaultOrder) - { - DisplayName = pattern.RawText ?? pattern.DebuggerToString(), - }; + var routeHandlerOptions = endpoints.ServiceProvider?.GetService>(); - // REVIEW: Should we add an IActionMethodMetadata with just MethodInfo on it so we are - // explicit about the MethodInfo representing the "handler" and not the RequestDelegate? + var options = new RequestDelegateFactoryOptions + { + ServiceProvider = endpoints.ServiceProvider, + RouteParameterNames = routeParams, + ThrowOnBadRequest = routeHandlerOptions?.Value.ThrowOnBadRequest ?? false, + DisableInferBodyFromParameters = disableInferBodyFromParameters, + }; + + var requestDelegateResult = RequestDelegateFactory.Create(handler, options); + + var builder = new RouteEndpointBuilder( + requestDelegateResult.RequestDelegate, + pattern, + defaultOrder) + { + DisplayName = pattern.RawText ?? pattern.DebuggerToString(), + }; - // Add MethodInfo as metadata to assist with OpenAPI generation for the endpoint. - builder.Metadata.Add(handler.Method); + // REVIEW: Should we add an IActionMethodMetadata with just MethodInfo on it so we are + // explicit about the MethodInfo representing the "handler" and not the RequestDelegate? - // Methods defined in a top-level program are generated as statics so the delegate - // target will be null. Inline lambdas are compiler generated method so they can - // be filtered that way. - if (GeneratedNameParser.TryParseLocalFunctionName(handler.Method.Name, out var endpointName) - || !TypeHelper.IsCompilerGeneratedMethod(handler.Method)) - { - endpointName ??= handler.Method.Name; - builder.DisplayName = $"{builder.DisplayName} => {endpointName}"; - } + // Add MethodInfo as metadata to assist with OpenAPI generation for the endpoint. + builder.Metadata.Add(handler.Method); - // Add delegate attributes as metadata - var attributes = handler.Method.GetCustomAttributes(); + // Methods defined in a top-level program are generated as statics so the delegate + // target will be null. Inline lambdas are compiler generated method so they can + // be filtered that way. + if (GeneratedNameParser.TryParseLocalFunctionName(handler.Method.Name, out var endpointName) + || !TypeHelper.IsCompilerGeneratedMethod(handler.Method)) + { + endpointName ??= handler.Method.Name; + builder.DisplayName = $"{builder.DisplayName} => {endpointName}"; + } - // Add add request delegate metadata - foreach (var metadata in requestDelegateResult.EndpointMetadata) - { - builder.Metadata.Add(metadata); - } + // Add delegate attributes as metadata + var attributes = handler.Method.GetCustomAttributes(); - // This can be null if the delegate is a dynamic method or compiled from an expression tree - if (attributes is not null) - { - foreach (var attribute in attributes) - { - builder.Metadata.Add(attribute); - } - } + // Add add request delegate metadata + foreach (var metadata in requestDelegateResult.EndpointMetadata) + { + builder.Metadata.Add(metadata); + } - var dataSource = endpoints.DataSources.OfType().FirstOrDefault(); - if (dataSource is null) + // This can be null if the delegate is a dynamic method or compiled from an expression tree + if (attributes is not null) + { + foreach (var attribute in attributes) { - dataSource = new ModelEndpointDataSource(); - endpoints.DataSources.Add(dataSource); + builder.Metadata.Add(attribute); } + } - return new RouteHandlerBuilder(dataSource.AddEndpointBuilder(builder)); + var dataSource = endpoints.DataSources.OfType().FirstOrDefault(); + if (dataSource is null) + { + dataSource = new ModelEndpointDataSource(); + endpoints.DataSources.Add(dataSource); } + + return new RouteHandlerBuilder(dataSource.AddEndpointBuilder(builder)); } } diff --git a/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs b/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs index 8bde422846..15db1a6068 100644 --- a/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/EndpointRoutingApplicationBuilderExtensions.cs @@ -7,155 +7,154 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Constains extensions for configuring routing on an . +/// +public static class EndpointRoutingApplicationBuilderExtensions { + private const string EndpointRouteBuilder = "__EndpointRouteBuilder"; + private const string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder"; + /// - /// Constains extensions for configuring routing on an . + /// Adds a middleware to the specified . /// - public static class EndpointRoutingApplicationBuilderExtensions + /// The to add the middleware to. + /// A reference to this instance after the operation has completed. + /// + /// + /// A call to must be followed by a call to + /// for the same + /// instance. + /// + /// + /// The defines a point in the middleware pipeline where routing decisions are + /// made, and an is associated with the . The + /// defines a point in the middleware pipeline where the current is executed. Middleware between + /// the and may observe or change the + /// associated with the . + /// + /// + public static IApplicationBuilder UseRouting(this IApplicationBuilder builder) { - private const string EndpointRouteBuilder = "__EndpointRouteBuilder"; - private const string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder"; - - /// - /// Adds a middleware to the specified . - /// - /// The to add the middleware to. - /// A reference to this instance after the operation has completed. - /// - /// - /// A call to must be followed by a call to - /// for the same - /// instance. - /// - /// - /// The defines a point in the middleware pipeline where routing decisions are - /// made, and an is associated with the . The - /// defines a point in the middleware pipeline where the current is executed. Middleware between - /// the and may observe or change the - /// associated with the . - /// - /// - public static IApplicationBuilder UseRouting(this IApplicationBuilder builder) + if (builder == null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + throw new ArgumentNullException(nameof(builder)); + } - VerifyRoutingServicesAreRegistered(builder); + VerifyRoutingServicesAreRegistered(builder); - IEndpointRouteBuilder endpointRouteBuilder; - if (builder.Properties.TryGetValue(GlobalEndpointRouteBuilderKey, out var obj)) - { - endpointRouteBuilder = (IEndpointRouteBuilder)obj!; - // Let interested parties know if UseRouting() was called while a global route builder was set - builder.Properties[EndpointRouteBuilder] = endpointRouteBuilder; - } - else - { - endpointRouteBuilder = new DefaultEndpointRouteBuilder(builder); - builder.Properties[EndpointRouteBuilder] = endpointRouteBuilder; - } - - return builder.UseMiddleware(endpointRouteBuilder); + IEndpointRouteBuilder endpointRouteBuilder; + if (builder.Properties.TryGetValue(GlobalEndpointRouteBuilderKey, out var obj)) + { + endpointRouteBuilder = (IEndpointRouteBuilder)obj!; + // Let interested parties know if UseRouting() was called while a global route builder was set + builder.Properties[EndpointRouteBuilder] = endpointRouteBuilder; + } + else + { + endpointRouteBuilder = new DefaultEndpointRouteBuilder(builder); + builder.Properties[EndpointRouteBuilder] = endpointRouteBuilder; } - /// - /// Adds a middleware to the specified - /// with the instances built from configured . - /// The will execute the associated with the current - /// request. - /// - /// The to add the middleware to. - /// An to configure the provided . - /// A reference to this instance after the operation has completed. - /// - /// - /// A call to must be preceded by a call to - /// for the same - /// instance. - /// - /// - /// The defines a point in the middleware pipeline where routing decisions are - /// made, and an is associated with the . The - /// defines a point in the middleware pipeline where the current is executed. Middleware between - /// the and may observe or change the - /// associated with the . - /// - /// - public static IApplicationBuilder UseEndpoints(this IApplicationBuilder builder, Action configure) + return builder.UseMiddleware(endpointRouteBuilder); + } + + /// + /// Adds a middleware to the specified + /// with the instances built from configured . + /// The will execute the associated with the current + /// request. + /// + /// The to add the middleware to. + /// An to configure the provided . + /// A reference to this instance after the operation has completed. + /// + /// + /// A call to must be preceded by a call to + /// for the same + /// instance. + /// + /// + /// The defines a point in the middleware pipeline where routing decisions are + /// made, and an is associated with the . The + /// defines a point in the middleware pipeline where the current is executed. Middleware between + /// the and may observe or change the + /// associated with the . + /// + /// + public static IApplicationBuilder UseEndpoints(this IApplicationBuilder builder, Action configure) + { + if (builder == null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + throw new ArgumentNullException(nameof(builder)); + } - if (configure == null) - { - throw new ArgumentNullException(nameof(configure)); - } + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } - VerifyRoutingServicesAreRegistered(builder); + VerifyRoutingServicesAreRegistered(builder); - VerifyEndpointRoutingMiddlewareIsRegistered(builder, out var endpointRouteBuilder); + VerifyEndpointRoutingMiddlewareIsRegistered(builder, out var endpointRouteBuilder); - configure(endpointRouteBuilder); + configure(endpointRouteBuilder); - // Yes, this mutates an IOptions. We're registering data sources in a global collection which - // can be used for discovery of endpoints or URL generation. - // - // Each middleware gets its own collection of data sources, and all of those data sources also - // get added to a global collection. - var routeOptions = builder.ApplicationServices.GetRequiredService>(); - foreach (var dataSource in endpointRouteBuilder.DataSources) + // Yes, this mutates an IOptions. We're registering data sources in a global collection which + // can be used for discovery of endpoints or URL generation. + // + // Each middleware gets its own collection of data sources, and all of those data sources also + // get added to a global collection. + var routeOptions = builder.ApplicationServices.GetRequiredService>(); + foreach (var dataSource in endpointRouteBuilder.DataSources) + { + if (!routeOptions.Value.EndpointDataSources.Contains(dataSource)) { - if (!routeOptions.Value.EndpointDataSources.Contains(dataSource)) - { - routeOptions.Value.EndpointDataSources.Add(dataSource); - } + routeOptions.Value.EndpointDataSources.Add(dataSource); } - - return builder.UseMiddleware(); } - private static void VerifyRoutingServicesAreRegistered(IApplicationBuilder app) + return builder.UseMiddleware(); + } + + private static void VerifyRoutingServicesAreRegistered(IApplicationBuilder app) + { + // Verify if AddRouting was done before calling UseEndpointRouting/UseEndpoint + // We use the RoutingMarkerService to make sure if all the services were added. + if (app.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null) { - // Verify if AddRouting was done before calling UseEndpointRouting/UseEndpoint - // We use the RoutingMarkerService to make sure if all the services were added. - if (app.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null) - { - throw new InvalidOperationException(Resources.FormatUnableToFindServices( - nameof(IServiceCollection), - nameof(RoutingServiceCollectionExtensions.AddRouting), - "ConfigureServices(...)")); - } + throw new InvalidOperationException(Resources.FormatUnableToFindServices( + nameof(IServiceCollection), + nameof(RoutingServiceCollectionExtensions.AddRouting), + "ConfigureServices(...)")); } + } - private static void VerifyEndpointRoutingMiddlewareIsRegistered(IApplicationBuilder app, out IEndpointRouteBuilder endpointRouteBuilder) + private static void VerifyEndpointRoutingMiddlewareIsRegistered(IApplicationBuilder app, out IEndpointRouteBuilder endpointRouteBuilder) + { + if (!app.Properties.TryGetValue(EndpointRouteBuilder, out var obj)) { - if (!app.Properties.TryGetValue(EndpointRouteBuilder, out var obj)) - { - var message = - $"{nameof(EndpointRoutingMiddleware)} matches endpoints setup by {nameof(EndpointMiddleware)} and so must be added to the request " + - $"execution pipeline before {nameof(EndpointMiddleware)}. " + - $"Please add {nameof(EndpointRoutingMiddleware)} by calling '{nameof(IApplicationBuilder)}.{nameof(UseRouting)}' inside the call " + - $"to 'Configure(...)' in the application startup code."; - throw new InvalidOperationException(message); - } + var message = + $"{nameof(EndpointRoutingMiddleware)} matches endpoints setup by {nameof(EndpointMiddleware)} and so must be added to the request " + + $"execution pipeline before {nameof(EndpointMiddleware)}. " + + $"Please add {nameof(EndpointRoutingMiddleware)} by calling '{nameof(IApplicationBuilder)}.{nameof(UseRouting)}' inside the call " + + $"to 'Configure(...)' in the application startup code."; + throw new InvalidOperationException(message); + } - endpointRouteBuilder = (IEndpointRouteBuilder)obj!; + endpointRouteBuilder = (IEndpointRouteBuilder)obj!; - // This check handles the case where Map or something else that forks the pipeline is called between the two - // routing middleware. - if (endpointRouteBuilder is DefaultEndpointRouteBuilder defaultRouteBuilder && !object.ReferenceEquals(app, defaultRouteBuilder.ApplicationBuilder)) - { - var message = - $"The {nameof(EndpointRoutingMiddleware)} and {nameof(EndpointMiddleware)} must be added to the same {nameof(IApplicationBuilder)} instance. " + - $"To use Endpoint Routing with 'Map(...)', make sure to call '{nameof(IApplicationBuilder)}.{nameof(UseRouting)}' before " + - $"'{nameof(IApplicationBuilder)}.{nameof(UseEndpoints)}' for each branch of the middleware pipeline."; - throw new InvalidOperationException(message); - } + // This check handles the case where Map or something else that forks the pipeline is called between the two + // routing middleware. + if (endpointRouteBuilder is DefaultEndpointRouteBuilder defaultRouteBuilder && !object.ReferenceEquals(app, defaultRouteBuilder.ApplicationBuilder)) + { + var message = + $"The {nameof(EndpointRoutingMiddleware)} and {nameof(EndpointMiddleware)} must be added to the same {nameof(IApplicationBuilder)} instance. " + + $"To use Endpoint Routing with 'Map(...)', make sure to call '{nameof(IApplicationBuilder)}.{nameof(UseRouting)}' before " + + $"'{nameof(IApplicationBuilder)}.{nameof(UseEndpoints)}' for each branch of the middleware pipeline."; + throw new InvalidOperationException(message); } } } diff --git a/src/Http/Routing/src/Builder/FallbackEndpointRouteBuilderExtensions.cs b/src/Http/Routing/src/Builder/FallbackEndpointRouteBuilderExtensions.cs index ed146c36cb..8367d9a78d 100644 --- a/src/Http/Routing/src/Builder/FallbackEndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/FallbackEndpointRouteBuilderExtensions.cs @@ -5,97 +5,96 @@ using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Contains extension methods for . +/// +public static class FallbackEndpointRouteBuilderExtensions { /// - /// Contains extension methods for . + /// The default route pattern used by fallback routing. {*path:nonfile} /// - public static class FallbackEndpointRouteBuilderExtensions + public static readonly string DefaultPattern = "{*path:nonfile}"; + + /// + /// Adds a specialized to the that will match + /// requests for non-file-names with the lowest possible priority. + /// + /// The to add the route to. + /// The delegate executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + /// + /// + /// is intended to handle cases where URL path of + /// the request does not contain a file name, and no other endpoint has matched. This is convenient for routing + /// requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to + /// result in an HTTP 404. + /// + /// + /// registers an endpoint using the pattern + /// {*path:nonfile}. The order of the registered endpoint will be int.MaxValue. + /// + /// + public static IEndpointConventionBuilder MapFallback(this IEndpointRouteBuilder endpoints, RequestDelegate requestDelegate) { - /// - /// The default route pattern used by fallback routing. {*path:nonfile} - /// - public static readonly string DefaultPattern = "{*path:nonfile}"; + if (endpoints == null) + { + throw new ArgumentNullException(nameof(endpoints)); + } - /// - /// Adds a specialized to the that will match - /// requests for non-file-names with the lowest possible priority. - /// - /// The to add the route to. - /// The delegate executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - /// - /// - /// is intended to handle cases where URL path of - /// the request does not contain a file name, and no other endpoint has matched. This is convenient for routing - /// requests for dynamic content to a SPA framework, while also allowing requests for non-existent files to - /// result in an HTTP 404. - /// - /// - /// registers an endpoint using the pattern - /// {*path:nonfile}. The order of the registered endpoint will be int.MaxValue. - /// - /// - public static IEndpointConventionBuilder MapFallback(this IEndpointRouteBuilder endpoints, RequestDelegate requestDelegate) + if (requestDelegate == null) { - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } + throw new ArgumentNullException(nameof(requestDelegate)); + } - if (requestDelegate == null) - { - throw new ArgumentNullException(nameof(requestDelegate)); - } + return endpoints.MapFallback("{*path:nonfile}", requestDelegate); + } - return endpoints.MapFallback("{*path:nonfile}", requestDelegate); + /// + /// Adds a specialized to the that will match + /// the provided pattern with the lowest possible priority. + /// + /// The to add the route to. + /// The route pattern. + /// The delegate executed when the endpoint is matched. + /// A that can be used to further customize the endpoint. + /// + /// + /// is intended to handle cases where no + /// other endpoint has matched. This is convenient for routing requests to a SPA framework. + /// + /// + /// The order of the registered endpoint will be int.MaxValue. + /// + /// + /// This overload will use the provided verbatim. Use the :nonfile route constraint + /// to exclude requests for static files. + /// + /// + public static IEndpointConventionBuilder MapFallback( + this IEndpointRouteBuilder endpoints, + string pattern, + RequestDelegate requestDelegate) + { + if (endpoints == null) + { + throw new ArgumentNullException(nameof(endpoints)); } - /// - /// Adds a specialized to the that will match - /// the provided pattern with the lowest possible priority. - /// - /// The to add the route to. - /// The route pattern. - /// The delegate executed when the endpoint is matched. - /// A that can be used to further customize the endpoint. - /// - /// - /// is intended to handle cases where no - /// other endpoint has matched. This is convenient for routing requests to a SPA framework. - /// - /// - /// The order of the registered endpoint will be int.MaxValue. - /// - /// - /// This overload will use the provided verbatim. Use the :nonfile route constraint - /// to exclude requests for static files. - /// - /// - public static IEndpointConventionBuilder MapFallback( - this IEndpointRouteBuilder endpoints, - string pattern, - RequestDelegate requestDelegate) + if (pattern == null) { - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } - - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } - - if (requestDelegate == null) - { - throw new ArgumentNullException(nameof(requestDelegate)); - } + throw new ArgumentNullException(nameof(pattern)); + } - var conventionBuilder = endpoints.Map(pattern, requestDelegate); - conventionBuilder.WithDisplayName("Fallback " + pattern); - conventionBuilder.Add(b => ((RouteEndpointBuilder)b).Order = int.MaxValue); - return conventionBuilder; + if (requestDelegate == null) + { + throw new ArgumentNullException(nameof(requestDelegate)); } + + var conventionBuilder = endpoints.Map(pattern, requestDelegate); + conventionBuilder.WithDisplayName("Fallback " + pattern); + conventionBuilder.Add(b => ((RouteEndpointBuilder)b).Order = int.MaxValue); + return conventionBuilder; } } diff --git a/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs b/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs index 34a9d897ad..46daeb65f7 100644 --- a/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/OpenApiRouteHandlerBuilderExtensions.cs @@ -6,221 +6,220 @@ using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +/// +/// Extension methods for adding that is +/// meant to be consumed by OpenAPI libraries. +/// +public static class OpenApiRouteHandlerBuilderExtensions { + private static readonly ExcludeFromDescriptionAttribute _excludeFromDescriptionMetadataAttribute = new(); + /// - /// Extension methods for adding that is - /// meant to be consumed by OpenAPI libraries. + /// Adds the to for all builders + /// produced by . /// - public static class OpenApiRouteHandlerBuilderExtensions + /// The . + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder ExcludeFromDescription(this RouteHandlerBuilder builder) { - private static readonly ExcludeFromDescriptionAttribute _excludeFromDescriptionMetadataAttribute = new(); - - /// - /// Adds the to for all builders - /// produced by . - /// - /// The . - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder ExcludeFromDescription(this RouteHandlerBuilder builder) - { - builder.WithMetadata(_excludeFromDescriptionMetadataAttribute); + builder.WithMetadata(_excludeFromDescriptionMetadataAttribute); - return builder; - } + return builder; + } - /// - /// Adds an to for all builders - /// produced by . - /// - /// The type of the response. - /// The . - /// The response status code. Defaults to . - /// The response content type. Defaults to "application/json". - /// Additional response content types the endpoint produces for the supplied status code. - /// A that can be used to further customize the endpoint. + /// + /// Adds an to for all builders + /// produced by . + /// + /// The type of the response. + /// The . + /// The response status code. Defaults to . + /// The response content type. Defaults to "application/json". + /// Additional response content types the endpoint produces for the supplied status code. + /// A that can be used to further customize the endpoint. #pragma warning disable RS0026 - public static RouteHandlerBuilder Produces(this RouteHandlerBuilder builder, + public static RouteHandlerBuilder Produces(this RouteHandlerBuilder builder, #pragma warning restore RS0026 int statusCode = StatusCodes.Status200OK, - string? contentType = null, - params string[] additionalContentTypes) - { - return Produces(builder, statusCode, typeof(TResponse), contentType, additionalContentTypes); - } + string? contentType = null, + params string[] additionalContentTypes) + { + return Produces(builder, statusCode, typeof(TResponse), contentType, additionalContentTypes); + } - /// - /// Adds an to for all builders - /// produced by . - /// - /// The . - /// The response status code. - /// The type of the response. Defaults to null. - /// The response content type. Defaults to "application/json" if responseType is not null, otherwise defaults to null. - /// Additional response content types the endpoint produces for the supplied status code. - /// A that can be used to further customize the endpoint. + /// + /// Adds an to for all builders + /// produced by . + /// + /// The . + /// The response status code. + /// The type of the response. Defaults to null. + /// The response content type. Defaults to "application/json" if responseType is not null, otherwise defaults to null. + /// Additional response content types the endpoint produces for the supplied status code. + /// A that can be used to further customize the endpoint. #pragma warning disable RS0026 - public static RouteHandlerBuilder Produces(this RouteHandlerBuilder builder, + public static RouteHandlerBuilder Produces(this RouteHandlerBuilder builder, #pragma warning restore RS0026 int statusCode, - Type? responseType = null, - string? contentType = null, - params string[] additionalContentTypes) + Type? responseType = null, + string? contentType = null, + params string[] additionalContentTypes) + { + if (responseType is Type && string.IsNullOrEmpty(contentType)) { - if (responseType is Type && string.IsNullOrEmpty(contentType)) - { - contentType = "application/json"; - } - - if (contentType is null) - { - builder.WithMetadata(new ProducesResponseTypeMetadata(responseType ?? typeof(void), statusCode)); - return builder; - } - - builder.WithMetadata(new ProducesResponseTypeMetadata(responseType ?? typeof(void), statusCode, contentType, additionalContentTypes)); + contentType = "application/json"; + } + if (contentType is null) + { + builder.WithMetadata(new ProducesResponseTypeMetadata(responseType ?? typeof(void), statusCode)); return builder; } - /// - /// Adds an with a type - /// to for all builders produced by . - /// - /// The . - /// The response status code. - /// The response content type. Defaults to "application/problem+json". - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder ProducesProblem(this RouteHandlerBuilder builder, - int statusCode, - string? contentType = null) - { - if (string.IsNullOrEmpty(contentType)) - { - contentType = "application/problem+json"; - } + builder.WithMetadata(new ProducesResponseTypeMetadata(responseType ?? typeof(void), statusCode, contentType, additionalContentTypes)); - return Produces(builder, statusCode, contentType); - } + return builder; + } - /// - /// Adds an with a type - /// to for all builders produced by . - /// - /// The . - /// The response status code. Defaults to . - /// The response content type. Defaults to "application/problem+json". - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder ProducesValidationProblem(this RouteHandlerBuilder builder, - int statusCode = StatusCodes.Status400BadRequest, - string? contentType = null) + /// + /// Adds an with a type + /// to for all builders produced by . + /// + /// The . + /// The response status code. + /// The response content type. Defaults to "application/problem+json". + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder ProducesProblem(this RouteHandlerBuilder builder, + int statusCode, + string? contentType = null) + { + if (string.IsNullOrEmpty(contentType)) { - if (string.IsNullOrEmpty(contentType)) - { - contentType = "application/problem+json"; - } - - return Produces(builder, statusCode, contentType); + contentType = "application/problem+json"; } - /// - /// Adds the to for all builders - /// produced by . - /// - /// - /// The OpenAPI specification supports a tags classification to categorize operations - /// into related groups. These tags are typically included in the generated specification - /// and are typically used to group operations by tags in the UI. - /// - /// The . - /// A collection of tags to be associated with the endpoint. - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder WithTags(this RouteHandlerBuilder builder, params string[] tags) + return Produces(builder, statusCode, contentType); + } + + /// + /// Adds an with a type + /// to for all builders produced by . + /// + /// The . + /// The response status code. Defaults to . + /// The response content type. Defaults to "application/problem+json". + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder ProducesValidationProblem(this RouteHandlerBuilder builder, + int statusCode = StatusCodes.Status400BadRequest, + string? contentType = null) + { + if (string.IsNullOrEmpty(contentType)) { - builder.WithMetadata(new TagsAttribute(tags)); - return builder; + contentType = "application/problem+json"; } - /// - /// Adds to for all builders - /// produced by . - /// - /// The type of the request body. - /// The . - /// The request content type that the endpoint accepts. - /// The list of additional request content types that the endpoint accepts. - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder, - string contentType, params string[] additionalContentTypes) where TRequest : notnull - { - Accepts(builder, typeof(TRequest), contentType, additionalContentTypes); + return Produces(builder, statusCode, contentType); + } - return builder; - } + /// + /// Adds the to for all builders + /// produced by . + /// + /// + /// The OpenAPI specification supports a tags classification to categorize operations + /// into related groups. These tags are typically included in the generated specification + /// and are typically used to group operations by tags in the UI. + /// + /// The . + /// A collection of tags to be associated with the endpoint. + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder WithTags(this RouteHandlerBuilder builder, params string[] tags) + { + builder.WithMetadata(new TagsAttribute(tags)); + return builder; + } - /// - /// Adds to for all builders - /// produced by . - /// - /// The type of the request body. - /// The . - /// Sets a value that determines if the request body is optional. - /// The request content type that the endpoint accepts. - /// The list of additional request content types that the endpoint accepts. - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder, - bool isOptional, string contentType, params string[] additionalContentTypes) where TRequest : notnull - { - Accepts(builder, typeof(TRequest), isOptional, contentType, additionalContentTypes); + /// + /// Adds to for all builders + /// produced by . + /// + /// The type of the request body. + /// The . + /// The request content type that the endpoint accepts. + /// The list of additional request content types that the endpoint accepts. + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder, + string contentType, params string[] additionalContentTypes) where TRequest : notnull + { + Accepts(builder, typeof(TRequest), contentType, additionalContentTypes); - return builder; - } + return builder; + } - /// - /// Adds to for all builders - /// produced by . - /// - /// The . - /// The type of the request body. - /// The request content type that the endpoint accepts. - /// The list of additional request content types that the endpoint accepts. - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder, - Type requestType, string contentType, params string[] additionalContentTypes) - { - builder.WithMetadata(new AcceptsMetadata(requestType, false, GetAllContentTypes(contentType, additionalContentTypes))); - return builder; - } + /// + /// Adds to for all builders + /// produced by . + /// + /// The type of the request body. + /// The . + /// Sets a value that determines if the request body is optional. + /// The request content type that the endpoint accepts. + /// The list of additional request content types that the endpoint accepts. + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder, + bool isOptional, string contentType, params string[] additionalContentTypes) where TRequest : notnull + { + Accepts(builder, typeof(TRequest), isOptional, contentType, additionalContentTypes); + return builder; + } - /// - /// Adds to for all builders - /// produced by . - /// - /// The . - /// The type of the request body. - /// Sets a value that determines if the request body is optional. - /// The request content type that the endpoint accepts. - /// The list of additional request content types that the endpoint accepts. - /// A that can be used to further customize the endpoint. - public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder, - Type requestType, bool isOptional, string contentType, params string[] additionalContentTypes) - { - builder.WithMetadata(new AcceptsMetadata(requestType, isOptional, GetAllContentTypes(contentType, additionalContentTypes))); - return builder; - } + /// + /// Adds to for all builders + /// produced by . + /// + /// The . + /// The type of the request body. + /// The request content type that the endpoint accepts. + /// The list of additional request content types that the endpoint accepts. + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder, + Type requestType, string contentType, params string[] additionalContentTypes) + { + builder.WithMetadata(new AcceptsMetadata(requestType, false, GetAllContentTypes(contentType, additionalContentTypes))); + return builder; + } - private static string[] GetAllContentTypes(string contentType, string[] additionalContentTypes) - { - var allContentTypes = new string[additionalContentTypes.Length + 1]; - allContentTypes[0] = contentType; - for (var i = 0; i < additionalContentTypes.Length; i++) - { - allContentTypes[i + 1] = additionalContentTypes[i]; - } + /// + /// Adds to for all builders + /// produced by . + /// + /// The . + /// The type of the request body. + /// Sets a value that determines if the request body is optional. + /// The request content type that the endpoint accepts. + /// The list of additional request content types that the endpoint accepts. + /// A that can be used to further customize the endpoint. + public static RouteHandlerBuilder Accepts(this RouteHandlerBuilder builder, + Type requestType, bool isOptional, string contentType, params string[] additionalContentTypes) + { + builder.WithMetadata(new AcceptsMetadata(requestType, isOptional, GetAllContentTypes(contentType, additionalContentTypes))); + return builder; + } - return allContentTypes; + private static string[] GetAllContentTypes(string contentType, string[] additionalContentTypes) + { + var allContentTypes = new string[additionalContentTypes.Length + 1]; + allContentTypes[0] = contentType; + + for (var i = 0; i < additionalContentTypes.Length; i++) + { + allContentTypes[i + 1] = additionalContentTypes[i]; } + + return allContentTypes; } } diff --git a/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs b/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs index d1e778d99d..8879fe39c1 100644 --- a/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs +++ b/src/Http/Routing/src/Builder/RouteHandlerBuilder.cs @@ -4,52 +4,51 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Builds conventions that will be used for customization of MapAction instances. +/// +public sealed class RouteHandlerBuilder : IEndpointConventionBuilder { + private readonly IEnumerable? _endpointConventionBuilders; + private readonly IEndpointConventionBuilder? _endpointConventionBuilder; + /// - /// Builds conventions that will be used for customization of MapAction instances. + /// Instantiates a new given a single + /// . /// - public sealed class RouteHandlerBuilder : IEndpointConventionBuilder + /// The to instantiate with. + internal RouteHandlerBuilder(IEndpointConventionBuilder endpointConventionBuilder) { - private readonly IEnumerable? _endpointConventionBuilders; - private readonly IEndpointConventionBuilder? _endpointConventionBuilder; + _endpointConventionBuilder = endpointConventionBuilder; + } - /// - /// Instantiates a new given a single - /// . - /// - /// The to instantiate with. - internal RouteHandlerBuilder(IEndpointConventionBuilder endpointConventionBuilder) - { - _endpointConventionBuilder = endpointConventionBuilder; - } + /// + /// Instantiates a new given multiple + /// instances. + /// + /// A sequence of instances. + public RouteHandlerBuilder(IEnumerable endpointConventionBuilders) + { + _endpointConventionBuilders = endpointConventionBuilders; + } - /// - /// Instantiates a new given multiple - /// instances. - /// - /// A sequence of instances. - public RouteHandlerBuilder(IEnumerable endpointConventionBuilders) + /// + /// Adds the specified convention to the builder. Conventions are used to customize instances. + /// + /// The convention to add to the builder. + public void Add(Action convention) + { + if (_endpointConventionBuilder != null) { - _endpointConventionBuilders = endpointConventionBuilders; + _endpointConventionBuilder.Add(convention); } - - /// - /// Adds the specified convention to the builder. Conventions are used to customize instances. - /// - /// The convention to add to the builder. - public void Add(Action convention) + else { - if (_endpointConventionBuilder != null) - { - _endpointConventionBuilder.Add(convention); - } - else + foreach (var endpointConventionBuilder in _endpointConventionBuilders!) { - foreach (var endpointConventionBuilder in _endpointConventionBuilders!) - { - endpointConventionBuilder.Add(convention); - } + endpointConventionBuilder.Add(convention); } } } diff --git a/src/Http/Routing/src/Builder/RoutingBuilderExtensions.cs b/src/Http/Routing/src/Builder/RoutingBuilderExtensions.cs index edf3adf18e..2722752758 100644 --- a/src/Http/Routing/src/Builder/RoutingBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/RoutingBuilderExtensions.cs @@ -5,73 +5,72 @@ using System; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extension methods for adding the middleware to an . +/// +public static class RoutingBuilderExtensions { /// - /// Extension methods for adding the middleware to an . + /// Adds a middleware to the specified with the specified . /// - public static class RoutingBuilderExtensions + /// The to add the middleware to. + /// The to use for routing requests. + /// A reference to this instance after the operation has completed. + public static IApplicationBuilder UseRouter(this IApplicationBuilder builder, IRouter router) { - /// - /// Adds a middleware to the specified with the specified . - /// - /// The to add the middleware to. - /// The to use for routing requests. - /// A reference to this instance after the operation has completed. - public static IApplicationBuilder UseRouter(this IApplicationBuilder builder, IRouter router) + if (builder == null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (router == null) - { - throw new ArgumentNullException(nameof(router)); - } - - if (builder.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null) - { - throw new InvalidOperationException(Resources.FormatUnableToFindServices( - nameof(IServiceCollection), - nameof(RoutingServiceCollectionExtensions.AddRouting), - "ConfigureServices(...)")); - } + throw new ArgumentNullException(nameof(builder)); + } - return builder.UseMiddleware(router); + if (router == null) + { + throw new ArgumentNullException(nameof(router)); } - /// - /// Adds a middleware to the specified - /// with the built from configured . - /// - /// The to add the middleware to. - /// An to configure the provided . - /// A reference to this instance after the operation has completed. - public static IApplicationBuilder UseRouter(this IApplicationBuilder builder, Action action) + if (builder.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + throw new InvalidOperationException(Resources.FormatUnableToFindServices( + nameof(IServiceCollection), + nameof(RoutingServiceCollectionExtensions.AddRouting), + "ConfigureServices(...)")); + } - if (action == null) - { - throw new ArgumentNullException(nameof(action)); - } + return builder.UseMiddleware(router); + } - if (builder.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null) - { - throw new InvalidOperationException(Resources.FormatUnableToFindServices( - nameof(IServiceCollection), - nameof(RoutingServiceCollectionExtensions.AddRouting), - "ConfigureServices(...)")); - } + /// + /// Adds a middleware to the specified + /// with the built from configured . + /// + /// The to add the middleware to. + /// An to configure the provided . + /// A reference to this instance after the operation has completed. + public static IApplicationBuilder UseRouter(this IApplicationBuilder builder, Action action) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } - var routeBuilder = new RouteBuilder(builder); - action(routeBuilder); + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } - return builder.UseRouter(routeBuilder.Build()); + if (builder.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null) + { + throw new InvalidOperationException(Resources.FormatUnableToFindServices( + nameof(IServiceCollection), + nameof(RoutingServiceCollectionExtensions.AddRouting), + "ConfigureServices(...)")); } + + var routeBuilder = new RouteBuilder(builder); + action(routeBuilder); + + return builder.UseRouter(routeBuilder.Build()); } } diff --git a/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs b/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs index c99ae45d88..2901f44491 100644 --- a/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs +++ b/src/Http/Routing/src/Builder/RoutingEndpointConventionBuilderExtensions.cs @@ -4,149 +4,148 @@ using System; using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extension methods for adding routing metadata to endpoint instances using . +/// +public static class RoutingEndpointConventionBuilderExtensions { /// - /// Extension methods for adding routing metadata to endpoint instances using . + /// Requires that endpoints match one of the specified hosts during routing. /// - public static class RoutingEndpointConventionBuilderExtensions + /// The to add the metadata to. + /// + /// The hosts used during routing. + /// Hosts should be Unicode rather than punycode, and may have a port. + /// An empty collection means any host will be accepted. + /// + /// A reference to this instance after the operation has completed. + public static TBuilder RequireHost(this TBuilder builder, params string[] hosts) where TBuilder : IEndpointConventionBuilder { - /// - /// Requires that endpoints match one of the specified hosts during routing. - /// - /// The to add the metadata to. - /// - /// The hosts used during routing. - /// Hosts should be Unicode rather than punycode, and may have a port. - /// An empty collection means any host will be accepted. - /// - /// A reference to this instance after the operation has completed. - public static TBuilder RequireHost(this TBuilder builder, params string[] hosts) where TBuilder : IEndpointConventionBuilder + if (builder == null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (hosts == null) - { - throw new ArgumentNullException(nameof(hosts)); - } - - builder.Add(endpointBuilder => - { - endpointBuilder.Metadata.Add(new HostAttribute(hosts)); - }); - return builder; + throw new ArgumentNullException(nameof(builder)); } - /// - /// Sets the to the provided for all - /// builders created by . - /// - /// The . - /// The display name. - /// The . - public static TBuilder WithDisplayName(this TBuilder builder, string displayName) where TBuilder : IEndpointConventionBuilder + if (hosts == null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + throw new ArgumentNullException(nameof(hosts)); + } - builder.Add(b => - { - b.DisplayName = displayName; - }); + builder.Add(endpointBuilder => + { + endpointBuilder.Metadata.Add(new HostAttribute(hosts)); + }); + return builder; + } - return builder; + /// + /// Sets the to the provided for all + /// builders created by . + /// + /// The . + /// The display name. + /// The . + public static TBuilder WithDisplayName(this TBuilder builder, string displayName) where TBuilder : IEndpointConventionBuilder + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); } - /// - /// Sets the using the provided for all - /// builders created by . - /// - /// The . - /// A delegate that produces the display name for each . - /// The . - public static TBuilder WithDisplayName(this TBuilder builder, Func func) where TBuilder : IEndpointConventionBuilder + builder.Add(b => { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (func == null) - { - throw new ArgumentNullException(nameof(func)); - } + b.DisplayName = displayName; + }); - builder.Add(b => - { - b.DisplayName = func(b); - }); + return builder; + } - return builder; + /// + /// Sets the using the provided for all + /// builders created by . + /// + /// The . + /// A delegate that produces the display name for each . + /// The . + public static TBuilder WithDisplayName(this TBuilder builder, Func func) where TBuilder : IEndpointConventionBuilder + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); } - /// - /// Adds the provided metadata to for all builders - /// produced by . - /// - /// The . - /// A collection of metadata items. - /// The . - public static TBuilder WithMetadata(this TBuilder builder, params object[] items) where TBuilder : IEndpointConventionBuilder + if (func == null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + throw new ArgumentNullException(nameof(func)); + } - if (items == null) - { - throw new ArgumentNullException(nameof(items)); - } + builder.Add(b => + { + b.DisplayName = func(b); + }); - builder.Add(b => - { - foreach (var item in items) - { - b.Metadata.Add(item); - } - }); + return builder; + } - return builder; + /// + /// Adds the provided metadata to for all builders + /// produced by . + /// + /// The . + /// A collection of metadata items. + /// The . + public static TBuilder WithMetadata(this TBuilder builder, params object[] items) where TBuilder : IEndpointConventionBuilder + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); } - /// - /// Sets the for all endpoints produced - /// on the target given the . - /// The on the endpoint is used for link generation and - /// is treated as the operation ID in the given endpoint's OpenAPI specification. - /// - /// The . - /// The endpoint name. - /// The . - public static TBuilder WithName(this TBuilder builder, string endpointName) where TBuilder : IEndpointConventionBuilder + if (items == null) { - builder.WithMetadata(new EndpointNameMetadata(endpointName), new RouteNameMetadata(endpointName)); - return builder; + throw new ArgumentNullException(nameof(items)); } - /// - /// Sets the for all endpoints produced - /// on the target given the . - /// The on the endpoint is used to set the endpoint's - /// GroupName in the OpenAPI specification. - /// - /// The . - /// The endpoint group name. - /// The . - public static TBuilder WithGroupName(this TBuilder builder, string endpointGroupName) where TBuilder : IEndpointConventionBuilder + builder.Add(b => { - builder.WithMetadata(new EndpointGroupNameAttribute(endpointGroupName)); - return builder; - } + foreach (var item in items) + { + b.Metadata.Add(item); + } + }); + + return builder; + } + + /// + /// Sets the for all endpoints produced + /// on the target given the . + /// The on the endpoint is used for link generation and + /// is treated as the operation ID in the given endpoint's OpenAPI specification. + /// + /// The . + /// The endpoint name. + /// The . + public static TBuilder WithName(this TBuilder builder, string endpointName) where TBuilder : IEndpointConventionBuilder + { + builder.WithMetadata(new EndpointNameMetadata(endpointName), new RouteNameMetadata(endpointName)); + return builder; + } + + /// + /// Sets the for all endpoints produced + /// on the target given the . + /// The on the endpoint is used to set the endpoint's + /// GroupName in the OpenAPI specification. + /// + /// The . + /// The endpoint group name. + /// The . + public static TBuilder WithGroupName(this TBuilder builder, string endpointGroupName) where TBuilder : IEndpointConventionBuilder + { + builder.WithMetadata(new EndpointGroupNameAttribute(endpointGroupName)); + return builder; } } diff --git a/src/Http/Routing/src/CompositeEndpointDataSource.cs b/src/Http/Routing/src/CompositeEndpointDataSource.cs index e38b22be24..fa9f30c9a1 100644 --- a/src/Http/Routing/src/CompositeEndpointDataSource.cs +++ b/src/Http/Routing/src/CompositeEndpointDataSource.cs @@ -13,215 +13,214 @@ using System.Threading; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Represents an whose values come from a collection of instances. +/// +[DebuggerDisplay("{DebuggerDisplayString,nq}")] +public sealed class CompositeEndpointDataSource : EndpointDataSource { - /// - /// Represents an whose values come from a collection of instances. - /// - [DebuggerDisplay("{DebuggerDisplayString,nq}")] - public sealed class CompositeEndpointDataSource : EndpointDataSource + private readonly object _lock; + private readonly ICollection _dataSources = default!; + private IReadOnlyList _endpoints = default!; + private IChangeToken _consumerChangeToken; + private CancellationTokenSource _cts; + + private CompositeEndpointDataSource() { - private readonly object _lock; - private readonly ICollection _dataSources = default!; - private IReadOnlyList _endpoints = default!; - private IChangeToken _consumerChangeToken; - private CancellationTokenSource _cts; + CreateChangeToken(); + _lock = new object(); + } - private CompositeEndpointDataSource() - { - CreateChangeToken(); - _lock = new object(); - } + internal CompositeEndpointDataSource(ObservableCollection dataSources) : this() + { + dataSources.CollectionChanged += OnDataSourcesChanged; - internal CompositeEndpointDataSource(ObservableCollection dataSources) : this() - { - dataSources.CollectionChanged += OnDataSourcesChanged; + _dataSources = dataSources; + } - _dataSources = dataSources; - } + /// + /// Instantiates a object from . + /// + /// An collection of objects. + /// A + public CompositeEndpointDataSource(IEnumerable endpointDataSources) : this() + { + _dataSources = new List(); - /// - /// Instantiates a object from . - /// - /// An collection of objects. - /// A - public CompositeEndpointDataSource(IEnumerable endpointDataSources) : this() + foreach (var dataSource in endpointDataSources) { - _dataSources = new List(); - - foreach (var dataSource in endpointDataSources) - { - _dataSources.Add(dataSource); - } + _dataSources.Add(dataSource); } + } - private void OnDataSourcesChanged(object? sender, NotifyCollectionChangedEventArgs e) + private void OnDataSourcesChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + lock (_lock) { - lock (_lock) + // Only trigger changes if composite data source has already initialized endpoints + if (_endpoints != null) { - // Only trigger changes if composite data source has already initialized endpoints - if (_endpoints != null) - { - HandleChange(); - } + HandleChange(); } } + } + + /// + /// Returns the collection of instances associated with the object. + /// + public IEnumerable DataSources => _dataSources; + + /// + /// Gets a used to signal invalidation of cached + /// instances. + /// + /// The . + public override IChangeToken GetChangeToken() + { + EnsureInitialized(); + return _consumerChangeToken; + } - /// - /// Returns the collection of instances associated with the object. - /// - public IEnumerable DataSources => _dataSources; - - /// - /// Gets a used to signal invalidation of cached - /// instances. - /// - /// The . - public override IChangeToken GetChangeToken() + /// + /// Returns a read-only collection of instances. + /// + public override IReadOnlyList Endpoints + { + get { EnsureInitialized(); - return _consumerChangeToken; + return _endpoints; } + } - /// - /// Returns a read-only collection of instances. - /// - public override IReadOnlyList Endpoints + // Defer initialization to avoid doing lots of reflection on startup. + private void EnsureInitialized() + { + if (_endpoints == null) { - get - { - EnsureInitialized(); - return _endpoints; - } + Initialize(); } + } - // Defer initialization to avoid doing lots of reflection on startup. - private void EnsureInitialized() + // Note: we can't use DataSourceDependentCache here because we also need to handle a list of change + // tokens, which is a complication most of our code doesn't have. + private void Initialize() + { + lock (_lock) { if (_endpoints == null) { - Initialize(); - } - } + _endpoints = _dataSources.SelectMany(d => d.Endpoints).ToArray(); - // Note: we can't use DataSourceDependentCache here because we also need to handle a list of change - // tokens, which is a complication most of our code doesn't have. - private void Initialize() - { - lock (_lock) - { - if (_endpoints == null) + foreach (var dataSource in _dataSources) { - _endpoints = _dataSources.SelectMany(d => d.Endpoints).ToArray(); - - foreach (var dataSource in _dataSources) - { - ChangeToken.OnChange( - dataSource.GetChangeToken, - HandleChange); - } + ChangeToken.OnChange( + dataSource.GetChangeToken, + HandleChange); } } } + } - private void HandleChange() + private void HandleChange() + { + lock (_lock) { - lock (_lock) - { - // Refresh the endpoints from datasource so that callbacks can get the latest endpoints - _endpoints = _dataSources.SelectMany(d => d.Endpoints).ToArray(); + // Refresh the endpoints from datasource so that callbacks can get the latest endpoints + _endpoints = _dataSources.SelectMany(d => d.Endpoints).ToArray(); + + // Prevent consumers from re-registering callback to inflight events as that can + // cause a stackoverflow + // Example: + // 1. B registers A + // 2. A fires event causing B's callback to get called + // 3. B executes some code in its callback, but needs to re-register callback + // in the same callback + var oldTokenSource = _cts; + var oldToken = _consumerChangeToken; - // Prevent consumers from re-registering callback to inflight events as that can - // cause a stackoverflow - // Example: - // 1. B registers A - // 2. A fires event causing B's callback to get called - // 3. B executes some code in its callback, but needs to re-register callback - // in the same callback - var oldTokenSource = _cts; - var oldToken = _consumerChangeToken; - - CreateChangeToken(); - - // Raise consumer callbacks. Any new callback registration would happen on the new token - // created in earlier step. - oldTokenSource.Cancel(); - } - } + CreateChangeToken(); - [MemberNotNull(nameof(_cts), nameof(_consumerChangeToken))] - private void CreateChangeToken() - { - _cts = new CancellationTokenSource(); - _consumerChangeToken = new CancellationChangeToken(_cts.Token); + // Raise consumer callbacks. Any new callback registration would happen on the new token + // created in earlier step. + oldTokenSource.Cancel(); } + } + + [MemberNotNull(nameof(_cts), nameof(_consumerChangeToken))] + private void CreateChangeToken() + { + _cts = new CancellationTokenSource(); + _consumerChangeToken = new CancellationChangeToken(_cts.Token); + } - private string DebuggerDisplayString + private string DebuggerDisplayString + { + get { - get + // Try using private variable '_endpoints' to avoid initialization + if (_endpoints == null) { - // Try using private variable '_endpoints' to avoid initialization - if (_endpoints == null) - { - return "No endpoints"; - } + return "No endpoints"; + } - var sb = new StringBuilder(); - foreach (var endpoint in _endpoints) + var sb = new StringBuilder(); + foreach (var endpoint in _endpoints) + { + if (endpoint is RouteEndpoint routeEndpoint) { - if (endpoint is RouteEndpoint routeEndpoint) + var template = routeEndpoint.RoutePattern.RawText; + template = string.IsNullOrEmpty(template) ? "\"\"" : template; + sb.Append(template); + sb.Append(", Defaults: new { "); + sb.AppendJoin(", ", FormatValues(routeEndpoint.RoutePattern.Defaults)); + sb.Append(" }"); + var routeNameMetadata = routeEndpoint.Metadata.GetMetadata(); + sb.Append(", Route Name: "); + sb.Append(routeNameMetadata?.RouteName); + var routeValues = routeEndpoint.RoutePattern.RequiredValues; + if (routeValues.Count > 0) { - var template = routeEndpoint.RoutePattern.RawText; - template = string.IsNullOrEmpty(template) ? "\"\"" : template; - sb.Append(template); - sb.Append(", Defaults: new { "); - sb.AppendJoin(", ", FormatValues(routeEndpoint.RoutePattern.Defaults)); + sb.Append(", Required Values: new { "); + sb.AppendJoin(", ", FormatValues(routeValues)); sb.Append(" }"); - var routeNameMetadata = routeEndpoint.Metadata.GetMetadata(); - sb.Append(", Route Name: "); - sb.Append(routeNameMetadata?.RouteName); - var routeValues = routeEndpoint.RoutePattern.RequiredValues; - if (routeValues.Count > 0) - { - sb.Append(", Required Values: new { "); - sb.AppendJoin(", ", FormatValues(routeValues)); - sb.Append(" }"); - } - sb.Append(", Order: "); - sb.Append(routeEndpoint.Order); - - var httpMethodMetadata = routeEndpoint.Metadata.GetMetadata(); - if (httpMethodMetadata != null) - { - sb.Append(", Http Methods: "); - sb.AppendJoin(", ", httpMethodMetadata.HttpMethods); - } - sb.Append(", Display Name: "); - sb.Append(routeEndpoint.DisplayName); - sb.AppendLine(); } - else + sb.Append(", Order: "); + sb.Append(routeEndpoint.Order); + + var httpMethodMetadata = routeEndpoint.Metadata.GetMetadata(); + if (httpMethodMetadata != null) { - sb.Append("Non-RouteEndpoint. DisplayName:"); - sb.AppendLine(endpoint.DisplayName); + sb.Append(", Http Methods: "); + sb.AppendJoin(", ", httpMethodMetadata.HttpMethods); } + sb.Append(", Display Name: "); + sb.Append(routeEndpoint.DisplayName); + sb.AppendLine(); } - return sb.ToString(); - - IEnumerable FormatValues(IEnumerable> values) + else { - return values.Select( - kvp => - { - var value = "null"; - if (kvp.Value != null) - { - value = "\"" + kvp.Value.ToString() + "\""; - } - return kvp.Key + " = " + value; - }); + sb.Append("Non-RouteEndpoint. DisplayName:"); + sb.AppendLine(endpoint.DisplayName); } } + return sb.ToString(); + + IEnumerable FormatValues(IEnumerable> values) + { + return values.Select( + kvp => + { + var value = "null"; + if (kvp.Value != null) + { + value = "\"" + kvp.Value.ToString() + "\""; + } + return kvp.Key + " = " + value; + }); + } } } } diff --git a/src/Http/Routing/src/ConfigureRouteHandlerOptions.cs b/src/Http/Routing/src/ConfigureRouteHandlerOptions.cs index a3781ca7e5..a88bd37dc4 100644 --- a/src/Http/Routing/src/ConfigureRouteHandlerOptions.cs +++ b/src/Http/Routing/src/ConfigureRouteHandlerOptions.cs @@ -9,23 +9,22 @@ using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +internal sealed class ConfigureRouteHandlerOptions : IConfigureOptions { - internal sealed class ConfigureRouteHandlerOptions : IConfigureOptions - { - private readonly IHostEnvironment _environment; + private readonly IHostEnvironment _environment; - public ConfigureRouteHandlerOptions(IHostEnvironment environment) - { - _environment = environment; - } + public ConfigureRouteHandlerOptions(IHostEnvironment environment) + { + _environment = environment; + } - public void Configure(RouteHandlerOptions options) + public void Configure(RouteHandlerOptions options) + { + if (_environment.IsDevelopment()) { - if (_environment.IsDevelopment()) - { - options.ThrowOnBadRequest = true; - } + options.ThrowOnBadRequest = true; } } } diff --git a/src/Http/Routing/src/ConfigureRouteOptions.cs b/src/Http/Routing/src/ConfigureRouteOptions.cs index eb89e526f4..e8a4b96aa9 100644 --- a/src/Http/Routing/src/ConfigureRouteOptions.cs +++ b/src/Http/Routing/src/ConfigureRouteOptions.cs @@ -7,30 +7,29 @@ using System.Collections.ObjectModel; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +internal class ConfigureRouteOptions : IConfigureOptions { - internal class ConfigureRouteOptions : IConfigureOptions - { - private readonly ICollection _dataSources; + private readonly ICollection _dataSources; - public ConfigureRouteOptions(ICollection dataSources) + public ConfigureRouteOptions(ICollection dataSources) + { + if (dataSources == null) { - if (dataSources == null) - { - throw new ArgumentNullException(nameof(dataSources)); - } - - _dataSources = dataSources; + throw new ArgumentNullException(nameof(dataSources)); } - public void Configure(RouteOptions options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } + _dataSources = dataSources; + } - options.EndpointDataSources = _dataSources; + public void Configure(RouteOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); } + + options.EndpointDataSources = _dataSources; } -} \ No newline at end of file +} diff --git a/src/Http/Routing/src/Constraints/AlphaRouteConstraint.cs b/src/Http/Routing/src/Constraints/AlphaRouteConstraint.cs index 23758a95d0..abb5031396 100644 --- a/src/Http/Routing/src/Constraints/AlphaRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/AlphaRouteConstraint.cs @@ -1,18 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to contain only lowercase or uppercase letters A through Z in the English alphabet. +/// +public class AlphaRouteConstraint : RegexRouteConstraint { /// - /// Constrains a route parameter to contain only lowercase or uppercase letters A through Z in the English alphabet. + /// Initializes a new instance of the class. /// - public class AlphaRouteConstraint : RegexRouteConstraint + public AlphaRouteConstraint() : base(@"^[a-z]*$") { - /// - /// Initializes a new instance of the class. - /// - public AlphaRouteConstraint() : base(@"^[a-z]*$") - { - } } -} \ No newline at end of file +} diff --git a/src/Http/Routing/src/Constraints/BoolRouteConstraint.cs b/src/Http/Routing/src/Constraints/BoolRouteConstraint.cs index 99e70d4721..8abe828f88 100644 --- a/src/Http/Routing/src/Constraints/BoolRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/BoolRouteConstraint.cs @@ -6,53 +6,52 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to represent only Boolean values. +/// +public class BoolRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { - /// - /// Constrains a route parameter to represent only Boolean values. - /// - public class BoolRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) { - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + if (routeKey == null) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } + throw new ArgumentNullException(nameof(routeKey)); + } - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } - if (values.TryGetValue(routeKey, out var value) && value != null) + if (values.TryGetValue(routeKey, out var value) && value != null) + { + if (value is bool) { - if (value is bool) - { - return true; - } - - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - return CheckConstraintCore(valueString); + return true; } - return false; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return CheckConstraintCore(valueString); } - private static bool CheckConstraintCore(string? valueString) - { - return bool.TryParse(valueString, out _); - } + return false; + } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) - { - return CheckConstraintCore(literal); - } + private static bool CheckConstraintCore(string? valueString) + { + return bool.TryParse(valueString, out _); + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return CheckConstraintCore(literal); } } diff --git a/src/Http/Routing/src/Constraints/CompositeRouteConstraint.cs b/src/Http/Routing/src/Constraints/CompositeRouteConstraint.cs index 0b141ee191..94a0589889 100644 --- a/src/Http/Routing/src/Constraints/CompositeRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/CompositeRouteConstraint.cs @@ -6,72 +6,71 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route by several child constraints. +/// +public class CompositeRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { /// - /// Constrains a route by several child constraints. + /// Initializes a new instance of the class. /// - public class CompositeRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// The child constraints that must match for this constraint to match. + public CompositeRouteConstraint(IEnumerable constraints) { - /// - /// Initializes a new instance of the class. - /// - /// The child constraints that must match for this constraint to match. - public CompositeRouteConstraint(IEnumerable constraints) + if (constraints == null) { - if (constraints == null) - { - throw new ArgumentNullException(nameof(constraints)); - } - - Constraints = constraints; + throw new ArgumentNullException(nameof(constraints)); } - /// - /// Gets the child constraints that must match for this constraint to match. - /// - public IEnumerable Constraints { get; private set; } + Constraints = constraints; + } + + /// + /// Gets the child constraints that must match for this constraint to match. + /// + public IEnumerable Constraints { get; private set; } - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (routeKey == null) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } + throw new ArgumentNullException(nameof(routeKey)); + } - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } - foreach (var constraint in Constraints) + foreach (var constraint in Constraints) + { + if (!constraint.Match(httpContext, route, routeKey, values, routeDirection)) { - if (!constraint.Match(httpContext, route, routeKey, values, routeDirection)) - { - return false; - } + return false; } - - return true; } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + return true; + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + foreach (var constraint in Constraints) { - foreach (var constraint in Constraints) + if (constraint is IParameterLiteralNodeMatchingPolicy literalConstraint && !literalConstraint.MatchesLiteral(parameterName, literal)) { - if (constraint is IParameterLiteralNodeMatchingPolicy literalConstraint && !literalConstraint.MatchesLiteral(parameterName, literal)) - { - return false; - } + return false; } - - return true; } + + return true; } } diff --git a/src/Http/Routing/src/Constraints/DateTimeRouteConstraint.cs b/src/Http/Routing/src/Constraints/DateTimeRouteConstraint.cs index 0f7b53dbfb..9d012719dc 100644 --- a/src/Http/Routing/src/Constraints/DateTimeRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/DateTimeRouteConstraint.cs @@ -6,59 +6,58 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to represent only values. +/// +/// +/// This constraint tries to parse strings by using all of the formats returned by the +/// CultureInfo.InvariantCulture.DateTimeFormat.GetAllDateTimePatterns() method. +/// For a sample on how to list all formats which are considered, please visit +/// http://msdn.microsoft.com/en-us/library/aszyst2c(v=vs.110).aspx +/// +public class DateTimeRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { - /// - /// Constrains a route parameter to represent only values. - /// - /// - /// This constraint tries to parse strings by using all of the formats returned by the - /// CultureInfo.InvariantCulture.DateTimeFormat.GetAllDateTimePatterns() method. - /// For a sample on how to list all formats which are considered, please visit - /// http://msdn.microsoft.com/en-us/library/aszyst2c(v=vs.110).aspx - /// - public class DateTimeRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) { - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + if (routeKey == null) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } + throw new ArgumentNullException(nameof(routeKey)); + } - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } - if (values.TryGetValue(routeKey, out var value) && value != null) + if (values.TryGetValue(routeKey, out var value) && value != null) + { + if (value is DateTime) { - if (value is DateTime) - { - return true; - } - - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - return CheckConstraintCore(valueString); + return true; } - return false; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return CheckConstraintCore(valueString); } - private static bool CheckConstraintCore(string? valueString) - { - return DateTime.TryParse(valueString, CultureInfo.InvariantCulture, DateTimeStyles.None, out _); - } + return false; + } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) - { - return CheckConstraintCore(literal); - } + private static bool CheckConstraintCore(string? valueString) + { + return DateTime.TryParse(valueString, CultureInfo.InvariantCulture, DateTimeStyles.None, out _); + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return CheckConstraintCore(literal); } } diff --git a/src/Http/Routing/src/Constraints/DecimalRouteConstraint.cs b/src/Http/Routing/src/Constraints/DecimalRouteConstraint.cs index ff142ba9d9..d5d2b3f24f 100644 --- a/src/Http/Routing/src/Constraints/DecimalRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/DecimalRouteConstraint.cs @@ -6,53 +6,52 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to represent only decimal values. +/// +public class DecimalRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { - /// - /// Constrains a route parameter to represent only decimal values. - /// - public class DecimalRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) { - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + if (routeKey == null) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } + throw new ArgumentNullException(nameof(routeKey)); + } - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } - if (values.TryGetValue(routeKey, out var value) && value != null) + if (values.TryGetValue(routeKey, out var value) && value != null) + { + if (value is decimal) { - if (value is decimal) - { - return true; - } - - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - return CheckConstraintCore(valueString); + return true; } - return false; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return CheckConstraintCore(valueString); } - private static bool CheckConstraintCore(string? valueString) - { - return decimal.TryParse(valueString, NumberStyles.Number, CultureInfo.InvariantCulture, out _); - } + return false; + } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) - { - return CheckConstraintCore(literal); - } + private static bool CheckConstraintCore(string? valueString) + { + return decimal.TryParse(valueString, NumberStyles.Number, CultureInfo.InvariantCulture, out _); + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return CheckConstraintCore(literal); } } diff --git a/src/Http/Routing/src/Constraints/DoubleRouteConstraint.cs b/src/Http/Routing/src/Constraints/DoubleRouteConstraint.cs index 8fd6d6b5b0..fc27b421d0 100644 --- a/src/Http/Routing/src/Constraints/DoubleRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/DoubleRouteConstraint.cs @@ -6,57 +6,56 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to represent only 64-bit floating-point values. +/// +public class DoubleRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { - /// - /// Constrains a route parameter to represent only 64-bit floating-point values. - /// - public class DoubleRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) { - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + if (routeKey == null) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } + throw new ArgumentNullException(nameof(routeKey)); + } - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } - if (values.TryGetValue(routeKey, out var value) && value != null) + if (values.TryGetValue(routeKey, out var value) && value != null) + { + if (value is double) { - if (value is double) - { - return true; - } - - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - return CheckConstraintCore(valueString); + return true; } - return false; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return CheckConstraintCore(valueString); } - private static bool CheckConstraintCore(string? valueString) - { - return double.TryParse( - valueString, - NumberStyles.Float | NumberStyles.AllowThousands, - CultureInfo.InvariantCulture, - out _); - } + return false; + } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) - { - return CheckConstraintCore(literal); - } + private static bool CheckConstraintCore(string? valueString) + { + return double.TryParse( + valueString, + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out _); + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return CheckConstraintCore(literal); } } diff --git a/src/Http/Routing/src/Constraints/FileNameRouteConstraint.cs b/src/Http/Routing/src/Constraints/FileNameRouteConstraint.cs index f716ae6b05..49c50fbb62 100644 --- a/src/Http/Routing/src/Constraints/FileNameRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/FileNameRouteConstraint.cs @@ -6,149 +6,148 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to represent only file name values. Does not validate that +/// the route value contains valid file system characters, or that the value represents +/// an actual file on disk. +/// +/// +/// +/// This constraint can be used to disambiguate requests for static files versus dynamic +/// content served from the application. +/// +/// +/// This constraint determines whether a route value represents a file name by examining +/// the last URL Path segment of the value (delimited by /). The last segment +/// must contain the dot (.) character followed by one or more non-(.) characters. +/// +/// +/// If the route value does not contain a / then the entire value will be interpreted +/// as the last segment. +/// +/// +/// The does not attempt to validate that the value contains +/// a legal file name for the current operating system. +/// +/// +/// The does not attempt to validate that the value represents +/// an actual file on disk. +/// +/// +/// +/// +/// Examples of route values that will be matched as file names +/// description +/// +/// +/// /a/b/c.txt +/// Final segment contains a . followed by other characters. +/// +/// +/// /hello.world.txt +/// Final segment contains a . followed by other characters. +/// +/// +/// hello.world.txt +/// Final segment contains a . followed by other characters. +/// +/// +/// .gitignore +/// Final segment contains a . followed by other characters. +/// +/// +/// +/// +/// Examples of route values that will be rejected as non-file-names +/// description +/// +/// +/// /a/b/c +/// Final segment does not contain a .. +/// +/// +/// /a/b.d/c +/// Final segment does not contain a .. +/// +/// +/// /a/b.d/c/ +/// Final segment is empty. +/// +/// +/// +/// Value is empty +/// +/// +/// +/// +public class FileNameRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { - /// - /// Constrains a route parameter to represent only file name values. Does not validate that - /// the route value contains valid file system characters, or that the value represents - /// an actual file on disk. - /// - /// - /// - /// This constraint can be used to disambiguate requests for static files versus dynamic - /// content served from the application. - /// - /// - /// This constraint determines whether a route value represents a file name by examining - /// the last URL Path segment of the value (delimited by /). The last segment - /// must contain the dot (.) character followed by one or more non-(.) characters. - /// - /// - /// If the route value does not contain a / then the entire value will be interpreted - /// as the last segment. - /// - /// - /// The does not attempt to validate that the value contains - /// a legal file name for the current operating system. - /// - /// - /// The does not attempt to validate that the value represents - /// an actual file on disk. - /// - /// - /// - /// - /// Examples of route values that will be matched as file names - /// description - /// - /// - /// /a/b/c.txt - /// Final segment contains a . followed by other characters. - /// - /// - /// /hello.world.txt - /// Final segment contains a . followed by other characters. - /// - /// - /// hello.world.txt - /// Final segment contains a . followed by other characters. - /// - /// - /// .gitignore - /// Final segment contains a . followed by other characters. - /// - /// - /// - /// - /// Examples of route values that will be rejected as non-file-names - /// description - /// - /// - /// /a/b/c - /// Final segment does not contain a .. - /// - /// - /// /a/b.d/c - /// Final segment does not contain a .. - /// - /// - /// /a/b.d/c/ - /// Final segment is empty. - /// - /// - /// - /// Value is empty - /// - /// - /// - /// - public class FileNameRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) { - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + if (routeKey == null) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - if (values.TryGetValue(routeKey, out var obj) && obj != null) - { - var value = Convert.ToString(obj, CultureInfo.InvariantCulture); - return IsFileName(value); - } + throw new ArgumentNullException(nameof(routeKey)); + } - // No value or null value. - return false; + if (values == null) + { + throw new ArgumentNullException(nameof(values)); } - // This is used both here and in NonFileNameRouteConstraint - // Any changes to this logic need to update the docs in those places. - internal static bool IsFileName(ReadOnlySpan value) + if (values.TryGetValue(routeKey, out var obj) && obj != null) { - if (value.Length == 0) - { - // Not a file name because empty. - return false; - } + var value = Convert.ToString(obj, CultureInfo.InvariantCulture); + return IsFileName(value); + } - var lastSlashIndex = value.LastIndexOf('/'); - if (lastSlashIndex >= 0) - { - value = value.Slice(lastSlashIndex + 1); - } + // No value or null value. + return false; + } - var dotIndex = value.IndexOf('.'); - if (dotIndex == -1) - { - // No dot. - return false; - } + // This is used both here and in NonFileNameRouteConstraint + // Any changes to this logic need to update the docs in those places. + internal static bool IsFileName(ReadOnlySpan value) + { + if (value.Length == 0) + { + // Not a file name because empty. + return false; + } - for (var i = dotIndex + 1; i < value.Length; i++) - { - if (value[i] != '.') - { - return true; - } - } + var lastSlashIndex = value.LastIndexOf('/'); + if (lastSlashIndex >= 0) + { + value = value.Slice(lastSlashIndex + 1); + } + var dotIndex = value.IndexOf('.'); + if (dotIndex == -1) + { + // No dot. return false; } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + for (var i = dotIndex + 1; i < value.Length; i++) { - return IsFileName(literal); + if (value[i] != '.') + { + return true; + } } + + return false; + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return IsFileName(literal); } } diff --git a/src/Http/Routing/src/Constraints/FloatRouteConstraint.cs b/src/Http/Routing/src/Constraints/FloatRouteConstraint.cs index 6cb7fc9b60..5e0b7a8722 100644 --- a/src/Http/Routing/src/Constraints/FloatRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/FloatRouteConstraint.cs @@ -6,57 +6,56 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to represent only 32-bit floating-point values. +/// +public class FloatRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { - /// - /// Constrains a route parameter to represent only 32-bit floating-point values. - /// - public class FloatRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) { - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + if (routeKey == null) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } + throw new ArgumentNullException(nameof(routeKey)); + } - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } - if (values.TryGetValue(routeKey, out var value) && value != null) + if (values.TryGetValue(routeKey, out var value) && value != null) + { + if (value is float) { - if (value is float) - { - return true; - } - - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - return CheckConstraintCore(valueString); + return true; } - return false; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return CheckConstraintCore(valueString); } - private static bool CheckConstraintCore(string? valueString) - { - return float.TryParse( - valueString, - NumberStyles.Float | NumberStyles.AllowThousands, - CultureInfo.InvariantCulture, - out _); - } + return false; + } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) - { - return CheckConstraintCore(literal); - } + private static bool CheckConstraintCore(string? valueString) + { + return float.TryParse( + valueString, + NumberStyles.Float | NumberStyles.AllowThousands, + CultureInfo.InvariantCulture, + out _); + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return CheckConstraintCore(literal); } } diff --git a/src/Http/Routing/src/Constraints/GuidRouteConstraint.cs b/src/Http/Routing/src/Constraints/GuidRouteConstraint.cs index 56678018cb..7abab4891e 100644 --- a/src/Http/Routing/src/Constraints/GuidRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/GuidRouteConstraint.cs @@ -6,55 +6,54 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to represent only values. +/// Matches values specified in any of the five formats "N", "D", "B", "P", or "X", +/// supported by Guid.ToString(string) and Guid.ToString(String, IFormatProvider) methods. +/// +public class GuidRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { - /// - /// Constrains a route parameter to represent only values. - /// Matches values specified in any of the five formats "N", "D", "B", "P", or "X", - /// supported by Guid.ToString(string) and Guid.ToString(String, IFormatProvider) methods. - /// - public class GuidRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) { - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + if (routeKey == null) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } + throw new ArgumentNullException(nameof(routeKey)); + } - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } - if (values.TryGetValue(routeKey, out var value) && value != null) + if (values.TryGetValue(routeKey, out var value) && value != null) + { + if (value is Guid) { - if (value is Guid) - { - return true; - } - - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - return CheckConstraintCore(valueString); + return true; } - return false; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return CheckConstraintCore(valueString); } - private static bool CheckConstraintCore(string? valueString) - { - return Guid.TryParse(valueString, out _); - } + return false; + } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) - { - return CheckConstraintCore(literal); - } + private static bool CheckConstraintCore(string? valueString) + { + return Guid.TryParse(valueString, out _); + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return CheckConstraintCore(literal); } } diff --git a/src/Http/Routing/src/Constraints/HttpMethodRouteConstraint.cs b/src/Http/Routing/src/Constraints/HttpMethodRouteConstraint.cs index 8c76a6172c..827947f7f0 100644 --- a/src/Http/Routing/src/Constraints/HttpMethodRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/HttpMethodRouteConstraint.cs @@ -7,86 +7,85 @@ using System.Globalization; using System.Linq; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains the HTTP method of request or a route. +/// +public class HttpMethodRouteConstraint : IRouteConstraint { /// - /// Constrains the HTTP method of request or a route. + /// Creates a new instance of that accepts the HTTP methods specified + /// by . /// - public class HttpMethodRouteConstraint : IRouteConstraint + /// The allowed HTTP methods. + public HttpMethodRouteConstraint(params string[] allowedMethods) { - /// - /// Creates a new instance of that accepts the HTTP methods specified - /// by . - /// - /// The allowed HTTP methods. - public HttpMethodRouteConstraint(params string[] allowedMethods) + if (allowedMethods == null) { - if (allowedMethods == null) - { - throw new ArgumentNullException(nameof(allowedMethods)); - } - - AllowedMethods = new List(allowedMethods); + throw new ArgumentNullException(nameof(allowedMethods)); } - /// - /// Gets the HTTP methods allowed by the constraint. - /// - public IList AllowedMethods { get; } + AllowedMethods = new List(allowedMethods); + } + + /// + /// Gets the HTTP methods allowed by the constraint. + /// + public IList AllowedMethods { get; } - /// - public virtual bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + /// + public virtual bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (routeKey == null) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } + throw new ArgumentNullException(nameof(routeKey)); + } - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } - switch (routeDirection) - { - case RouteDirection.IncomingRequest: - // Only required for constraining incoming requests - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } + switch (routeDirection) + { + case RouteDirection.IncomingRequest: + // Only required for constraining incoming requests + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } - return AllowedMethods.Contains(httpContext.Request.Method, StringComparer.OrdinalIgnoreCase); + return AllowedMethods.Contains(httpContext.Request.Method, StringComparer.OrdinalIgnoreCase); - case RouteDirection.UrlGeneration: - // We need to see if the user specified the HTTP method explicitly. Consider these two routes: - // - // a) Route: template = "/{foo}", Constraints = { httpMethod = new HttpMethodRouteConstraint("GET") } - // b) Route: template = "/{foo}", Constraints = { httpMethod = new HttpMethodRouteConstraint("POST") } - // - // A user might know ahead of time that a URI he/she is generating might be used with a particular HTTP - // method. If a URI will be used for an HTTP POST but we match on (a) while generating the URI, then - // the HTTP GET-specific route will be used for URI generation, which might have undesired behavior. - // - // To prevent this, a user might call GetVirtualPath(..., { httpMethod = "POST" }) to - // signal that they are generating a URI that will be used for an HTTP POST, so they want the URI - // generation to be performed by the (b) route instead of the (a) route, consistent with what would - // happen on incoming requests. - if (!values.TryGetValue(routeKey, out var obj)) - { - return true; - } + case RouteDirection.UrlGeneration: + // We need to see if the user specified the HTTP method explicitly. Consider these two routes: + // + // a) Route: template = "/{foo}", Constraints = { httpMethod = new HttpMethodRouteConstraint("GET") } + // b) Route: template = "/{foo}", Constraints = { httpMethod = new HttpMethodRouteConstraint("POST") } + // + // A user might know ahead of time that a URI he/she is generating might be used with a particular HTTP + // method. If a URI will be used for an HTTP POST but we match on (a) while generating the URI, then + // the HTTP GET-specific route will be used for URI generation, which might have undesired behavior. + // + // To prevent this, a user might call GetVirtualPath(..., { httpMethod = "POST" }) to + // signal that they are generating a URI that will be used for an HTTP POST, so they want the URI + // generation to be performed by the (b) route instead of the (a) route, consistent with what would + // happen on incoming requests. + if (!values.TryGetValue(routeKey, out var obj)) + { + return true; + } - return AllowedMethods.Contains(Convert.ToString(obj, CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase); + return AllowedMethods.Contains(Convert.ToString(obj, CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase); - default: - throw new ArgumentOutOfRangeException(nameof(routeDirection)); - } + default: + throw new ArgumentOutOfRangeException(nameof(routeDirection)); } } } diff --git a/src/Http/Routing/src/Constraints/IntRouteConstraint.cs b/src/Http/Routing/src/Constraints/IntRouteConstraint.cs index 42d9ae707a..a26f8d1fc1 100644 --- a/src/Http/Routing/src/Constraints/IntRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/IntRouteConstraint.cs @@ -6,53 +6,52 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to represent only 32-bit integer values. +/// +public class IntRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { - /// - /// Constrains a route parameter to represent only 32-bit integer values. - /// - public class IntRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) { - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + if (routeKey == null) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } + throw new ArgumentNullException(nameof(routeKey)); + } - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } - if (values.TryGetValue(routeKey, out var value) && value != null) + if (values.TryGetValue(routeKey, out var value) && value != null) + { + if (value is int) { - if (value is int) - { - return true; - } - - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - return valueString is not null && CheckConstraintCore(valueString); + return true; } - return false; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return valueString is not null && CheckConstraintCore(valueString); } - private static bool CheckConstraintCore(string valueString) - { - return int.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out _); - } + return false; + } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) - { - return CheckConstraintCore(literal); - } + private static bool CheckConstraintCore(string valueString) + { + return int.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out _); + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return CheckConstraintCore(literal); } } diff --git a/src/Http/Routing/src/Constraints/LengthRouteConstraint.cs b/src/Http/Routing/src/Constraints/LengthRouteConstraint.cs index 65dc364f0a..2946754e45 100644 --- a/src/Http/Routing/src/Constraints/LengthRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/LengthRouteConstraint.cs @@ -6,106 +6,105 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to be a string of a given length or within a given range of lengths. +/// +public class LengthRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { /// - /// Constrains a route parameter to be a string of a given length or within a given range of lengths. + /// Initializes a new instance of the class that constrains + /// a route parameter to be a string of a given length. /// - public class LengthRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// The length of the route parameter. + public LengthRouteConstraint(int length) { - /// - /// Initializes a new instance of the class that constrains - /// a route parameter to be a string of a given length. - /// - /// The length of the route parameter. - public LengthRouteConstraint(int length) + if (length < 0) { - if (length < 0) - { - var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0); - throw new ArgumentOutOfRangeException(nameof(length), length, errorMessage); - } + var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0); + throw new ArgumentOutOfRangeException(nameof(length), length, errorMessage); + } + + MinLength = MaxLength = length; + } - MinLength = MaxLength = length; + /// + /// Initializes a new instance of the class that constrains + /// a route parameter to be a string of a given length. + /// + /// The minimum length allowed for the route parameter. + /// The maximum length allowed for the route parameter. + public LengthRouteConstraint(int minLength, int maxLength) + { + if (minLength < 0) + { + var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0); + throw new ArgumentOutOfRangeException(nameof(minLength), minLength, errorMessage); } - /// - /// Initializes a new instance of the class that constrains - /// a route parameter to be a string of a given length. - /// - /// The minimum length allowed for the route parameter. - /// The maximum length allowed for the route parameter. - public LengthRouteConstraint(int minLength, int maxLength) + if (maxLength < 0) { - if (minLength < 0) - { - var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0); - throw new ArgumentOutOfRangeException(nameof(minLength), minLength, errorMessage); - } - - if (maxLength < 0) - { - var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0); - throw new ArgumentOutOfRangeException(nameof(maxLength), maxLength, errorMessage); - } - - if (minLength > maxLength) - { - var errorMessage = - Resources.FormatRangeConstraint_MinShouldBeLessThanOrEqualToMax("minLength", "maxLength"); - throw new ArgumentOutOfRangeException(nameof(minLength), minLength, errorMessage); - } - - MinLength = minLength; - MaxLength = maxLength; + var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0); + throw new ArgumentOutOfRangeException(nameof(maxLength), maxLength, errorMessage); } - /// - /// Gets the minimum length allowed for the route parameter. - /// - public int MinLength { get; } - - /// - /// Gets the maximum length allowed for the route parameter. - /// - public int MaxLength { get; } - - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + if (minLength > maxLength) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - if (values.TryGetValue(routeKey, out var value) && value != null) - { - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture)!; - return CheckConstraintCore(valueString); - } - - return false; + var errorMessage = + Resources.FormatRangeConstraint_MinShouldBeLessThanOrEqualToMax("minLength", "maxLength"); + throw new ArgumentOutOfRangeException(nameof(minLength), minLength, errorMessage); } - private bool CheckConstraintCore(string valueString) + MinLength = minLength; + MaxLength = maxLength; + } + + /// + /// Gets the minimum length allowed for the route parameter. + /// + public int MinLength { get; } + + /// + /// Gets the maximum length allowed for the route parameter. + /// + public int MaxLength { get; } + + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (routeKey == null) { - var length = valueString.Length; - return length >= MinLength && length <= MaxLength; + throw new ArgumentNullException(nameof(routeKey)); } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + if (values == null) { - return CheckConstraintCore(literal); + throw new ArgumentNullException(nameof(values)); } + + if (values.TryGetValue(routeKey, out var value) && value != null) + { + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture)!; + return CheckConstraintCore(valueString); + } + + return false; + } + + private bool CheckConstraintCore(string valueString) + { + var length = valueString.Length; + return length >= MinLength && length <= MaxLength; + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return CheckConstraintCore(literal); } } diff --git a/src/Http/Routing/src/Constraints/LongRouteConstraint.cs b/src/Http/Routing/src/Constraints/LongRouteConstraint.cs index 431240809e..2a146bd95e 100644 --- a/src/Http/Routing/src/Constraints/LongRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/LongRouteConstraint.cs @@ -6,53 +6,52 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to represent only 64-bit integer values. +/// +public class LongRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { - /// - /// Constrains a route parameter to represent only 64-bit integer values. - /// - public class LongRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) { - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + if (routeKey == null) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } + throw new ArgumentNullException(nameof(routeKey)); + } - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } - if (values.TryGetValue(routeKey, out var value) && value != null) + if (values.TryGetValue(routeKey, out var value) && value != null) + { + if (value is long) { - if (value is long) - { - return true; - } - - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - return CheckConstraintCore(valueString); + return true; } - return false; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return CheckConstraintCore(valueString); } - private static bool CheckConstraintCore(string? valueString) - { - return long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out _); - } + return false; + } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) - { - return CheckConstraintCore(literal); - } + private static bool CheckConstraintCore(string? valueString) + { + return long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out _); + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return CheckConstraintCore(literal); } } diff --git a/src/Http/Routing/src/Constraints/MaxLengthRouteConstraint.cs b/src/Http/Routing/src/Constraints/MaxLengthRouteConstraint.cs index 971627b96b..0d23f0ddce 100644 --- a/src/Http/Routing/src/Constraints/MaxLengthRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/MaxLengthRouteConstraint.cs @@ -6,68 +6,67 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to be a string with a maximum length. +/// +public class MaxLengthRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { /// - /// Constrains a route parameter to be a string with a maximum length. + /// Initializes a new instance of the class. /// - public class MaxLengthRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// The maximum length allowed for the route parameter. + public MaxLengthRouteConstraint(int maxLength) { - /// - /// Initializes a new instance of the class. - /// - /// The maximum length allowed for the route parameter. - public MaxLengthRouteConstraint(int maxLength) + if (maxLength < 0) { - if (maxLength < 0) - { - var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0); - throw new ArgumentOutOfRangeException(nameof(maxLength), maxLength, errorMessage); - } - - MaxLength = maxLength; + var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0); + throw new ArgumentOutOfRangeException(nameof(maxLength), maxLength, errorMessage); } - /// - /// Gets the maximum length allowed for the route parameter. - /// - public int MaxLength { get; } - - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) - { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } + MaxLength = maxLength; + } - if (values.TryGetValue(routeKey, out var value) && value != null) - { - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture)!; - return CheckConstraintCore(valueString); - } + /// + /// Gets the maximum length allowed for the route parameter. + /// + public int MaxLength { get; } - return false; + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); } - private bool CheckConstraintCore(string valueString) + if (values == null) { - return valueString.Length <= MaxLength; + throw new ArgumentNullException(nameof(values)); } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + if (values.TryGetValue(routeKey, out var value) && value != null) { - return CheckConstraintCore(literal); + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture)!; + return CheckConstraintCore(valueString); } + + return false; + } + + private bool CheckConstraintCore(string valueString) + { + return valueString.Length <= MaxLength; + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return CheckConstraintCore(literal); } } diff --git a/src/Http/Routing/src/Constraints/MaxRouteConstraint.cs b/src/Http/Routing/src/Constraints/MaxRouteConstraint.cs index 8c05a1b7fa..ede450dce7 100644 --- a/src/Http/Routing/src/Constraints/MaxRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/MaxRouteConstraint.cs @@ -6,66 +6,65 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to be an integer with a maximum value. +/// +public class MaxRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { /// - /// Constrains a route parameter to be an integer with a maximum value. + /// Initializes a new instance of the class. /// - public class MaxRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// The maximum value allowed for the route parameter. + public MaxRouteConstraint(long max) { - /// - /// Initializes a new instance of the class. - /// - /// The maximum value allowed for the route parameter. - public MaxRouteConstraint(long max) - { - Max = max; - } + Max = max; + } - /// - /// Gets the maximum allowed value of the route parameter. - /// - public long Max { get; private set; } + /// + /// Gets the maximum allowed value of the route parameter. + /// + public long Max { get; private set; } - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (routeKey == null) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - if (values.TryGetValue(routeKey, out var value) && value != null) - { - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - return CheckConstraintCore(valueString); - } + throw new ArgumentNullException(nameof(routeKey)); + } - return false; + if (values == null) + { + throw new ArgumentNullException(nameof(values)); } - private bool CheckConstraintCore(string? valueString) + if (values.TryGetValue(routeKey, out var value) && value != null) { - if (long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue)) - { - return longValue <= Max; - } - return false; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return CheckConstraintCore(valueString); } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + return false; + } + + private bool CheckConstraintCore(string? valueString) + { + if (long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue)) { - return CheckConstraintCore(literal); + return longValue <= Max; } + return false; + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return CheckConstraintCore(literal); } } diff --git a/src/Http/Routing/src/Constraints/MinLengthRouteConstraint.cs b/src/Http/Routing/src/Constraints/MinLengthRouteConstraint.cs index 7cb782d6d5..ecb4b09f8f 100644 --- a/src/Http/Routing/src/Constraints/MinLengthRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/MinLengthRouteConstraint.cs @@ -6,68 +6,67 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to be a string with a minimum length. +/// +public class MinLengthRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { /// - /// Constrains a route parameter to be a string with a minimum length. + /// Initializes a new instance of the class. /// - public class MinLengthRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// The minimum length allowed for the route parameter. + public MinLengthRouteConstraint(int minLength) { - /// - /// Initializes a new instance of the class. - /// - /// The minimum length allowed for the route parameter. - public MinLengthRouteConstraint(int minLength) + if (minLength < 0) { - if (minLength < 0) - { - var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0); - throw new ArgumentOutOfRangeException(nameof(minLength), minLength, errorMessage); - } - - MinLength = minLength; + var errorMessage = Resources.FormatArgumentMustBeGreaterThanOrEqualTo(0); + throw new ArgumentOutOfRangeException(nameof(minLength), minLength, errorMessage); } - /// - /// Gets the minimum length allowed for the route parameter. - /// - public int MinLength { get; private set; } - - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) - { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } + MinLength = minLength; + } - if (values.TryGetValue(routeKey, out var value) && value != null) - { - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture)!; - return CheckConstraintCore(valueString); - } + /// + /// Gets the minimum length allowed for the route parameter. + /// + public int MinLength { get; private set; } - return false; + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); } - private bool CheckConstraintCore(string valueString) + if (values == null) { - return valueString.Length >= MinLength; + throw new ArgumentNullException(nameof(values)); } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + if (values.TryGetValue(routeKey, out var value) && value != null) { - return CheckConstraintCore(literal); + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture)!; + return CheckConstraintCore(valueString); } + + return false; + } + + private bool CheckConstraintCore(string valueString) + { + return valueString.Length >= MinLength; + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return CheckConstraintCore(literal); } } diff --git a/src/Http/Routing/src/Constraints/MinRouteConstraint.cs b/src/Http/Routing/src/Constraints/MinRouteConstraint.cs index 1b45b93233..e7438ee22c 100644 --- a/src/Http/Routing/src/Constraints/MinRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/MinRouteConstraint.cs @@ -6,66 +6,65 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to be a long with a minimum value. +/// +public class MinRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { /// - /// Constrains a route parameter to be a long with a minimum value. + /// Initializes a new instance of the class. /// - public class MinRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// The minimum value allowed for the route parameter. + public MinRouteConstraint(long min) { - /// - /// Initializes a new instance of the class. - /// - /// The minimum value allowed for the route parameter. - public MinRouteConstraint(long min) - { - Min = min; - } + Min = min; + } - /// - /// Gets the minimum allowed value of the route parameter. - /// - public long Min { get; } + /// + /// Gets the minimum allowed value of the route parameter. + /// + public long Min { get; } - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (routeKey == null) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - if (values.TryGetValue(routeKey, out var value) && value != null) - { - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - return CheckConstraintCore(valueString); - } + throw new ArgumentNullException(nameof(routeKey)); + } - return false; + if (values == null) + { + throw new ArgumentNullException(nameof(values)); } - private bool CheckConstraintCore(string? valueString) + if (values.TryGetValue(routeKey, out var value) && value != null) { - if (long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue)) - { - return longValue >= Min; - } - return false; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return CheckConstraintCore(valueString); } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + return false; + } + + private bool CheckConstraintCore(string? valueString) + { + if (long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue)) { - return CheckConstraintCore(literal); + return longValue >= Min; } + return false; + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return CheckConstraintCore(literal); } } diff --git a/src/Http/Routing/src/Constraints/NonFileNameRouteConstraint.cs b/src/Http/Routing/src/Constraints/NonFileNameRouteConstraint.cs index 5bc0d8121e..0ebc693f1c 100644 --- a/src/Http/Routing/src/Constraints/NonFileNameRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/NonFileNameRouteConstraint.cs @@ -6,115 +6,114 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to represent only non-file-name values. Does not validate that +/// the route value contains valid file system characters, or that the value represents +/// an actual file on disk. +/// +/// +/// +/// This constraint can be used to disambiguate requests for dynamic content versus +/// static files served from the application. +/// +/// +/// This constraint determines whether a route value represents a file name by examining +/// the last URL Path segment of the value (delimited by /). The last segment +/// must contain the dot (.) character followed by one or more non-(.) characters. +/// +/// +/// If the route value does not contain a / then the entire value will be interpreted +/// as a the last segment. +/// +/// +/// The does not attempt to validate that the value contains +/// a legal file name for the current operating system. +/// +/// +/// +/// +/// Examples of route values that will be matched as non-file-names +/// description +/// +/// +/// /a/b/c +/// Final segment does not contain a .. +/// +/// +/// /a/b.d/c +/// Final segment does not contain a .. +/// +/// +/// /a/b.d/c/ +/// Final segment is empty. +/// +/// +/// +/// Value is empty +/// +/// +/// +/// +/// Examples of route values that will be rejected as file names +/// description +/// +/// +/// /a/b/c.txt +/// Final segment contains a . followed by other characters. +/// +/// +/// /hello.world.txt +/// Final segment contains a . followed by other characters. +/// +/// +/// hello.world.txt +/// Final segment contains a . followed by other characters. +/// +/// +/// .gitignore +/// Final segment contains a . followed by other characters. +/// +/// +/// +/// +public class NonFileNameRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { - /// - /// Constrains a route parameter to represent only non-file-name values. Does not validate that - /// the route value contains valid file system characters, or that the value represents - /// an actual file on disk. - /// - /// - /// - /// This constraint can be used to disambiguate requests for dynamic content versus - /// static files served from the application. - /// - /// - /// This constraint determines whether a route value represents a file name by examining - /// the last URL Path segment of the value (delimited by /). The last segment - /// must contain the dot (.) character followed by one or more non-(.) characters. - /// - /// - /// If the route value does not contain a / then the entire value will be interpreted - /// as a the last segment. - /// - /// - /// The does not attempt to validate that the value contains - /// a legal file name for the current operating system. - /// - /// - /// - /// - /// Examples of route values that will be matched as non-file-names - /// description - /// - /// - /// /a/b/c - /// Final segment does not contain a .. - /// - /// - /// /a/b.d/c - /// Final segment does not contain a .. - /// - /// - /// /a/b.d/c/ - /// Final segment is empty. - /// - /// - /// - /// Value is empty - /// - /// - /// - /// - /// Examples of route values that will be rejected as file names - /// description - /// - /// - /// /a/b/c.txt - /// Final segment contains a . followed by other characters. - /// - /// - /// /hello.world.txt - /// Final segment contains a . followed by other characters. - /// - /// - /// hello.world.txt - /// Final segment contains a . followed by other characters. - /// - /// - /// .gitignore - /// Final segment contains a . followed by other characters. - /// - /// - /// - /// - public class NonFileNameRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) { - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + if (routeKey == null) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - if (values.TryGetValue(routeKey, out var obj) && obj != null) - { - var value = Convert.ToString(obj, CultureInfo.InvariantCulture); - return !FileNameRouteConstraint.IsFileName(value); - } + throw new ArgumentNullException(nameof(routeKey)); + } - // No value or null value. - // - // We want to return true here because the core use-case of the constraint is to *exclude* - // things that look like file names. There's nothing here that looks like a file name, so - // let it through. - return true; + if (values == null) + { + throw new ArgumentNullException(nameof(values)); } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + if (values.TryGetValue(routeKey, out var obj) && obj != null) { - return !FileNameRouteConstraint.IsFileName(literal); + var value = Convert.ToString(obj, CultureInfo.InvariantCulture); + return !FileNameRouteConstraint.IsFileName(value); } + + // No value or null value. + // + // We want to return true here because the core use-case of the constraint is to *exclude* + // things that look like file names. There's nothing here that looks like a file name, so + // let it through. + return true; + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return !FileNameRouteConstraint.IsFileName(literal); } } diff --git a/src/Http/Routing/src/Constraints/NullRouteConstraint.cs b/src/Http/Routing/src/Constraints/NullRouteConstraint.cs index 1ec7e77f2b..8e80ad4c4b 100644 --- a/src/Http/Routing/src/Constraints/NullRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/NullRouteConstraint.cs @@ -3,19 +3,18 @@ using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +internal class NullRouteConstraint : IRouteConstraint { - internal class NullRouteConstraint : IRouteConstraint - { - public static readonly NullRouteConstraint Instance = new NullRouteConstraint(); + public static readonly NullRouteConstraint Instance = new NullRouteConstraint(); - private NullRouteConstraint() - { - } + private NullRouteConstraint() + { + } - public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) - { - return true; - } + public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) + { + return true; } } diff --git a/src/Http/Routing/src/Constraints/OptionalRouteConstraint.cs b/src/Http/Routing/src/Constraints/OptionalRouteConstraint.cs index 29170be117..799ae4d316 100644 --- a/src/Http/Routing/src/Constraints/OptionalRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/OptionalRouteConstraint.cs @@ -4,60 +4,59 @@ using System; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Defines a constraint on an optional parameter. If the parameter is present, then it is constrained by InnerConstraint. +/// +public class OptionalRouteConstraint : IRouteConstraint { /// - /// Defines a constraint on an optional parameter. If the parameter is present, then it is constrained by InnerConstraint. + /// Creates a new instance given the . + /// + /// + public OptionalRouteConstraint(IRouteConstraint innerConstraint) + { + if (innerConstraint == null) + { + throw new ArgumentNullException(nameof(innerConstraint)); + } + + InnerConstraint = innerConstraint; + } + + /// + /// Gets the associated with the optional parameter. /// - public class OptionalRouteConstraint : IRouteConstraint + public IRouteConstraint InnerConstraint { get; } + + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) { - /// - /// Creates a new instance given the . - /// - /// - public OptionalRouteConstraint(IRouteConstraint innerConstraint) + if (routeKey == null) { - if (innerConstraint == null) - { - throw new ArgumentNullException(nameof(innerConstraint)); - } + throw new ArgumentNullException(nameof(routeKey)); + } - InnerConstraint = innerConstraint; + if (values == null) + { + throw new ArgumentNullException(nameof(values)); } - /// - /// Gets the associated with the optional parameter. - /// - public IRouteConstraint InnerConstraint { get; } - - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + if (values.TryGetValue(routeKey, out var value)) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - if (values.TryGetValue(routeKey, out var value)) - { - return InnerConstraint.Match(httpContext, - route, - routeKey, - values, - routeDirection); - } - - return true; + return InnerConstraint.Match(httpContext, + route, + routeKey, + values, + routeDirection); } + + return true; } } diff --git a/src/Http/Routing/src/Constraints/RangeRouteConstraint.cs b/src/Http/Routing/src/Constraints/RangeRouteConstraint.cs index 0f8012e539..01004df0f4 100644 --- a/src/Http/Routing/src/Constraints/RangeRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/RangeRouteConstraint.cs @@ -6,80 +6,79 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constraints a route parameter to be an integer within a given range of values. +/// +public class RangeRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { /// - /// Constraints a route parameter to be an integer within a given range of values. + /// Initializes a new instance of the class. /// - public class RangeRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// The minimum value. + /// The maximum value. + /// The minimum value should be less than or equal to the maximum value. + public RangeRouteConstraint(long min, long max) { - /// - /// Initializes a new instance of the class. - /// - /// The minimum value. - /// The maximum value. - /// The minimum value should be less than or equal to the maximum value. - public RangeRouteConstraint(long min, long max) + if (min > max) { - if (min > max) - { - var errorMessage = Resources.FormatRangeConstraint_MinShouldBeLessThanOrEqualToMax("min", "max"); - throw new ArgumentOutOfRangeException(nameof(min), min, errorMessage); - } - - Min = min; - Max = max; + var errorMessage = Resources.FormatRangeConstraint_MinShouldBeLessThanOrEqualToMax("min", "max"); + throw new ArgumentOutOfRangeException(nameof(min), min, errorMessage); } - /// - /// Gets the minimum allowed value of the route parameter. - /// - public long Min { get; private set; } - - /// - /// Gets the maximum allowed value of the route parameter. - /// - public long Max { get; private set; } + Min = min; + Max = max; + } - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) - { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } + /// + /// Gets the minimum allowed value of the route parameter. + /// + public long Min { get; private set; } - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } + /// + /// Gets the maximum allowed value of the route parameter. + /// + public long Max { get; private set; } - if (values.TryGetValue(routeKey, out var value) && value != null) - { - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - return CheckConstraintCore(valueString); - } + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } - return false; + if (values == null) + { + throw new ArgumentNullException(nameof(values)); } - private bool CheckConstraintCore(string? valueString) + if (values.TryGetValue(routeKey, out var value) && value != null) { - if (long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue)) - { - return longValue >= Min && longValue <= Max; - } - return false; + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return CheckConstraintCore(valueString); } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + return false; + } + + private bool CheckConstraintCore(string? valueString) + { + if (long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue)) { - return CheckConstraintCore(literal); + return longValue >= Min && longValue <= Max; } + return false; + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return CheckConstraintCore(literal); } } diff --git a/src/Http/Routing/src/Constraints/RegexInlineRouteConstraint.cs b/src/Http/Routing/src/Constraints/RegexInlineRouteConstraint.cs index e0e82c3f66..465e0a9f9c 100644 --- a/src/Http/Routing/src/Constraints/RegexInlineRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/RegexInlineRouteConstraint.cs @@ -1,20 +1,19 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Represents a regex constraint which can be used as an inlineConstraint. +/// +public class RegexInlineRouteConstraint : RegexRouteConstraint { /// - /// Represents a regex constraint which can be used as an inlineConstraint. + /// Initializes a new instance of the class. /// - public class RegexInlineRouteConstraint : RegexRouteConstraint + /// The regular expression pattern to match. + public RegexInlineRouteConstraint(string regexPattern) + : base(regexPattern) { - /// - /// Initializes a new instance of the class. - /// - /// The regular expression pattern to match. - public RegexInlineRouteConstraint(string regexPattern) - : base(regexPattern) - { - } } } diff --git a/src/Http/Routing/src/Constraints/RegexRouteConstraint.cs b/src/Http/Routing/src/Constraints/RegexRouteConstraint.cs index 54f78a08fd..de7f28961d 100644 --- a/src/Http/Routing/src/Constraints/RegexRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/RegexRouteConstraint.cs @@ -7,83 +7,82 @@ using System.Text.RegularExpressions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to match a regular expression. +/// +public class RegexRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { + private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10); + /// - /// Constrains a route parameter to match a regular expression. + /// Constructor for a given a . /// - public class RegexRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// A instance to use as a constraint. + public RegexRouteConstraint(Regex regex) { - private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10); - - /// - /// Constructor for a given a . - /// - /// A instance to use as a constraint. - public RegexRouteConstraint(Regex regex) + if (regex == null) { - if (regex == null) - { - throw new ArgumentNullException(nameof(regex)); - } - - Constraint = regex; + throw new ArgumentNullException(nameof(regex)); } - /// - /// Constructor for a given a . - /// - /// A string containing the regex pattern. - public RegexRouteConstraint(string regexPattern) - { - if (regexPattern == null) - { - throw new ArgumentNullException(nameof(regexPattern)); - } - - Constraint = new Regex( - regexPattern, - RegexOptions.CultureInvariant | RegexOptions.IgnoreCase, - RegexMatchTimeout); - } - - /// - /// Gets the regular expression used in the route constraint. - /// - public Regex Constraint { get; private set; } + Constraint = regex; + } - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + /// + /// Constructor for a given a . + /// + /// A string containing the regex pattern. + public RegexRouteConstraint(string regexPattern) + { + if (regexPattern == null) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } + throw new ArgumentNullException(nameof(regexPattern)); + } - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } + Constraint = new Regex( + regexPattern, + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase, + RegexMatchTimeout); + } - if (values.TryGetValue(routeKey, out var routeValue) - && routeValue != null) - { - var parameterValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture)!; + /// + /// Gets the regular expression used in the route constraint. + /// + public Regex Constraint { get; private set; } - return Constraint.IsMatch(parameterValueString); - } + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); + } - return false; + if (values == null) + { + throw new ArgumentNullException(nameof(values)); } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + if (values.TryGetValue(routeKey, out var routeValue) + && routeValue != null) { - return Constraint.IsMatch(literal); + var parameterValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture)!; + + return Constraint.IsMatch(parameterValueString); } + + return false; + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return Constraint.IsMatch(literal); } } diff --git a/src/Http/Routing/src/Constraints/RequiredRouteConstraint.cs b/src/Http/Routing/src/Constraints/RequiredRouteConstraint.cs index 79423bffc9..d53d4a4c5c 100644 --- a/src/Http/Routing/src/Constraints/RequiredRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/RequiredRouteConstraint.cs @@ -5,43 +5,42 @@ using System; using System.Globalization; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constraints a route parameter that must have a value. +/// +/// +/// This constraint is primarily used to enforce that a non-parameter value is present during +/// URL generation. +/// +public class RequiredRouteConstraint : IRouteConstraint { - /// - /// Constraints a route parameter that must have a value. - /// - /// - /// This constraint is primarily used to enforce that a non-parameter value is present during - /// URL generation. - /// - public class RequiredRouteConstraint : IRouteConstraint + /// + public bool Match( + HttpContext? httpContext, + IRouter? route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) { - /// - public bool Match( - HttpContext? httpContext, - IRouter? route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) + if (routeKey == null) { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } + throw new ArgumentNullException(nameof(routeKey)); + } - if (values.TryGetValue(routeKey, out var value) && value != null) - { - // In routing the empty string is equivalent to null, which is equivalent to an unset value. - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - return !string.IsNullOrEmpty(valueString); - } + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } - return false; + if (values.TryGetValue(routeKey, out var value) && value != null) + { + // In routing the empty string is equivalent to null, which is equivalent to an unset value. + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + return !string.IsNullOrEmpty(valueString); } + + return false; } } diff --git a/src/Http/Routing/src/Constraints/StringRouteConstraint.cs b/src/Http/Routing/src/Constraints/StringRouteConstraint.cs index c638283274..60afff66cd 100644 --- a/src/Http/Routing/src/Constraints/StringRouteConstraint.cs +++ b/src/Http/Routing/src/Constraints/StringRouteConstraint.cs @@ -6,60 +6,59 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +/// +/// Constrains a route parameter to contain only a specified string. +/// +public class StringRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy { + private readonly string _value; + /// - /// Constrains a route parameter to contain only a specified string. + /// Initializes a new instance of the class. /// - public class StringRouteConstraint : IRouteConstraint, IParameterLiteralNodeMatchingPolicy + /// The constraint value to match. + public StringRouteConstraint(string value) { - private readonly string _value; - - /// - /// Initializes a new instance of the class. - /// - /// The constraint value to match. - public StringRouteConstraint(string value) + if (value == null) { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - - _value = value; + throw new ArgumentNullException(nameof(value)); } - /// - public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) - { - if (routeKey == null) - { - throw new ArgumentNullException(nameof(routeKey)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - if (values.TryGetValue(routeKey, out var routeValue) - && routeValue != null) - { - var parameterValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture)!; - return CheckConstraintCore(parameterValueString); - } + _value = value; + } - return false; + /// + public bool Match(HttpContext? httpContext, IRouter? route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) + { + if (routeKey == null) + { + throw new ArgumentNullException(nameof(routeKey)); } - private bool CheckConstraintCore(string parameterValueString) + if (values == null) { - return parameterValueString.Equals(_value, StringComparison.OrdinalIgnoreCase); + throw new ArgumentNullException(nameof(values)); } - bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + if (values.TryGetValue(routeKey, out var routeValue) + && routeValue != null) { - return CheckConstraintCore(literal); + var parameterValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture)!; + return CheckConstraintCore(parameterValueString); } + + return false; + } + + private bool CheckConstraintCore(string parameterValueString) + { + return parameterValueString.Equals(_value, StringComparison.OrdinalIgnoreCase); + } + + bool IParameterLiteralNodeMatchingPolicy.MatchesLiteral(string parameterName, string literal) + { + return CheckConstraintCore(literal); } } diff --git a/src/Http/Routing/src/DataSourceDependentCache.cs b/src/Http/Routing/src/DataSourceDependentCache.cs index a6241036f8..3c56316b92 100644 --- a/src/Http/Routing/src/DataSourceDependentCache.cs +++ b/src/Http/Routing/src/DataSourceDependentCache.cs @@ -9,88 +9,87 @@ using System.Diagnostics.CodeAnalysis; using System.Threading; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +// FYI: This class is also linked into MVC. If you make changes to the API you will +// also need to change MVC's usage. +internal sealed class DataSourceDependentCache : IDisposable where T : class { - // FYI: This class is also linked into MVC. If you make changes to the API you will - // also need to change MVC's usage. - internal sealed class DataSourceDependentCache : IDisposable where T : class - { - private readonly EndpointDataSource _dataSource; - private readonly Func, T> _initializeCore; - private readonly Func _initializer; - private readonly Action _initializerWithState; + private readonly EndpointDataSource _dataSource; + private readonly Func, T> _initializeCore; + private readonly Func _initializer; + private readonly Action _initializerWithState; - private object _lock; - private bool _initialized; - private T? _value; + private object _lock; + private bool _initialized; + private T? _value; - private IDisposable? _disposable; - private bool _disposed; + private IDisposable? _disposable; + private bool _disposed; - public DataSourceDependentCache(EndpointDataSource dataSource, Func, T> initialize) + public DataSourceDependentCache(EndpointDataSource dataSource, Func, T> initialize) + { + if (dataSource == null) { - if (dataSource == null) - { - throw new ArgumentNullException(nameof(dataSource)); - } + throw new ArgumentNullException(nameof(dataSource)); + } - if (initialize == null) - { - throw new ArgumentNullException(nameof(initialize)); - } + if (initialize == null) + { + throw new ArgumentNullException(nameof(initialize)); + } - _dataSource = dataSource; - _initializeCore = initialize; + _dataSource = dataSource; + _initializeCore = initialize; - _initializer = Initialize; - _initializerWithState = (state) => Initialize(); - _lock = new object(); - } + _initializer = Initialize; + _initializerWithState = (state) => Initialize(); + _lock = new object(); + } - // Note that we don't lock here, and think about that in the context of a 'push'. So when data gets 'pushed' - // we start computing a new state, but we're still able to perform operations on the old state until we've - // processed the update. - [NotNullIfNotNull(nameof(_value))] - public T? Value => _value; + // Note that we don't lock here, and think about that in the context of a 'push'. So when data gets 'pushed' + // we start computing a new state, but we're still able to perform operations on the old state until we've + // processed the update. + [NotNullIfNotNull(nameof(_value))] + public T? Value => _value; - [MemberNotNull(nameof(_value))] - public T EnsureInitialized() - { - return LazyInitializer.EnsureInitialized(ref _value, ref _initialized, ref _lock, _initializer); - } + [MemberNotNull(nameof(_value))] + public T EnsureInitialized() + { + return LazyInitializer.EnsureInitialized(ref _value, ref _initialized, ref _lock, _initializer); + } - private T Initialize() + private T Initialize() + { + lock (_lock) { - lock (_lock) - { - var changeToken = _dataSource.GetChangeToken(); - _value = _initializeCore(_dataSource.Endpoints); + var changeToken = _dataSource.GetChangeToken(); + _value = _initializeCore(_dataSource.Endpoints); - // Don't resubscribe if we're already disposed. - if (_disposed) - { - return _value; - } - - _disposable = changeToken.RegisterChangeCallback(_initializerWithState, null); + // Don't resubscribe if we're already disposed. + if (_disposed) + { return _value; } + + _disposable = changeToken.RegisterChangeCallback(_initializerWithState, null); + return _value; } + } - public void Dispose() + public void Dispose() + { + lock (_lock) { - lock (_lock) + if (!_disposed) { - if (!_disposed) - { - _disposable?.Dispose(); - _disposable = null; - - // Tracking whether we're disposed or not prevents a race-condition - // between disposal and Initialize(). If a change callback fires after - // we dispose, then we don't want to reregister. - _disposed = true; - } + _disposable?.Dispose(); + _disposable = null; + + // Tracking whether we're disposed or not prevents a race-condition + // between disposal and Initialize(). If a change callback fires after + // we dispose, then we don't want to reregister. + _disposed = true; } } } diff --git a/src/Http/Routing/src/DataTokensMetadata.cs b/src/Http/Routing/src/DataTokensMetadata.cs index a0568f35ae..6859473704 100644 --- a/src/Http/Routing/src/DataTokensMetadata.cs +++ b/src/Http/Routing/src/DataTokensMetadata.cs @@ -7,27 +7,26 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Metadata that defines data tokens for an . This metadata +/// type provides data tokens value for associated +/// with an endpoint. +/// +public sealed class DataTokensMetadata : IDataTokensMetadata { /// - /// Metadata that defines data tokens for an . This metadata - /// type provides data tokens value for associated - /// with an endpoint. + /// Constructor for a new given . /// - public sealed class DataTokensMetadata : IDataTokensMetadata + /// The data tokens. + public DataTokensMetadata(IReadOnlyDictionary dataTokens) { - /// - /// Constructor for a new given . - /// - /// The data tokens. - public DataTokensMetadata(IReadOnlyDictionary dataTokens) - { - DataTokens = dataTokens ?? throw new ArgumentNullException(nameof(dataTokens)); - } - - /// - /// Get the data tokens. - /// - public IReadOnlyDictionary DataTokens { get; } + DataTokens = dataTokens ?? throw new ArgumentNullException(nameof(dataTokens)); } + + /// + /// Get the data tokens. + /// + public IReadOnlyDictionary DataTokens { get; } } diff --git a/src/Http/Routing/src/DecisionTree/DecisionCriterion.cs b/src/Http/Routing/src/DecisionTree/DecisionCriterion.cs index 88619b0739..8056288463 100644 --- a/src/Http/Routing/src/DecisionTree/DecisionCriterion.cs +++ b/src/Http/Routing/src/DecisionTree/DecisionCriterion.cs @@ -5,12 +5,11 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.Routing.DecisionTree +namespace Microsoft.AspNetCore.Routing.DecisionTree; + +internal class DecisionCriterion { - internal class DecisionCriterion - { - public string Key { get; set; } + public string Key { get; set; } - public Dictionary> Branches { get; set; } - } + public Dictionary> Branches { get; set; } } diff --git a/src/Http/Routing/src/DecisionTree/DecisionCriterionValue.cs b/src/Http/Routing/src/DecisionTree/DecisionCriterionValue.cs index c58fbb564a..52061cd760 100644 --- a/src/Http/Routing/src/DecisionTree/DecisionCriterionValue.cs +++ b/src/Http/Routing/src/DecisionTree/DecisionCriterionValue.cs @@ -1,20 +1,19 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.DecisionTree +namespace Microsoft.AspNetCore.Routing.DecisionTree; + +internal readonly struct DecisionCriterionValue { - internal readonly struct DecisionCriterionValue - { - private readonly object _value; + private readonly object _value; - public DecisionCriterionValue(object value) - { - _value = value; - } + public DecisionCriterionValue(object value) + { + _value = value; + } - public object Value - { - get { return _value; } - } + public object Value + { + get { return _value; } } } diff --git a/src/Http/Routing/src/DecisionTree/DecisionCriterionValueEqualityComparer.cs b/src/Http/Routing/src/DecisionTree/DecisionCriterionValueEqualityComparer.cs index 054115badf..0e4d2cea88 100644 --- a/src/Http/Routing/src/DecisionTree/DecisionCriterionValueEqualityComparer.cs +++ b/src/Http/Routing/src/DecisionTree/DecisionCriterionValueEqualityComparer.cs @@ -3,25 +3,24 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.Routing.DecisionTree +namespace Microsoft.AspNetCore.Routing.DecisionTree; + +internal class DecisionCriterionValueEqualityComparer : IEqualityComparer { - internal class DecisionCriterionValueEqualityComparer : IEqualityComparer + public DecisionCriterionValueEqualityComparer(IEqualityComparer innerComparer) { - public DecisionCriterionValueEqualityComparer(IEqualityComparer innerComparer) - { - InnerComparer = innerComparer; - } + InnerComparer = innerComparer; + } - public IEqualityComparer InnerComparer { get; private set; } + public IEqualityComparer InnerComparer { get; private set; } - public bool Equals(DecisionCriterionValue x, DecisionCriterionValue y) - { - return InnerComparer.Equals(x.Value, y.Value); - } + public bool Equals(DecisionCriterionValue x, DecisionCriterionValue y) + { + return InnerComparer.Equals(x.Value, y.Value); + } - public int GetHashCode(DecisionCriterionValue obj) - { - return InnerComparer.GetHashCode(obj.Value); - } + public int GetHashCode(DecisionCriterionValue obj) + { + return InnerComparer.GetHashCode(obj.Value); } -} \ No newline at end of file +} diff --git a/src/Http/Routing/src/DecisionTree/DecisionTreeBuilder.cs b/src/Http/Routing/src/DecisionTree/DecisionTreeBuilder.cs index 20229ed733..86aac66162 100644 --- a/src/Http/Routing/src/DecisionTree/DecisionTreeBuilder.cs +++ b/src/Http/Routing/src/DecisionTree/DecisionTreeBuilder.cs @@ -5,222 +5,221 @@ using System; using System.Collections.Generic; using System.Linq; -namespace Microsoft.AspNetCore.Routing.DecisionTree +namespace Microsoft.AspNetCore.Routing.DecisionTree; + +// This code generates a minimal tree of decision criteria that map known categorical data +// (key-value-pairs) to a set of inputs. Action Selection is the best example of how this +// can be used, so the comments here will describe the process from the point-of-view, +// though the decision tree is generally applicable to like-problems. +// +// Care has been taken here to keep the performance of building the data-structure at a +// reasonable level, as this has an impact on startup cost for action selection. Additionally +// we want to hold on to the minimal amount of memory needed once we've built the tree. +// +// Ex: +// Given actions like the following, create a decision tree that will help action +// selection work efficiently. +// +// Given any set of route data it should be possible to traverse the tree using the +// presence our route data keys (like action), and whether or not they match any of +// the known values for that route data key, to find the set of actions that match +// the route data. +// +// Actions: +// +// { controller = "Home", action = "Index" } +// { controller = "Products", action = "Index" } +// { controller = "Products", action = "Buy" } +// { area = "Admin", controller = "Users", action = "AddUser" } +// +// The generated tree looks like this (json-like-notation): +// +// { +// action : { +// "AddUser" : { +// controller : { +// "Users" : { +// area : { +// "Admin" : match { area = "Admin", controller = "Users", action = "AddUser" } +// } +// } +// } +// }, +// "Buy" : { +// controller : { +// "Products" : { +// area : { +// null : match { controller = "Products", action = "Buy" } +// } +// } +// } +// }, +// "Index" : { +// controller : { +// "Home" : { +// area : { +// null : match { controller = "Home", action = "Index" } +// } +// } +// "Products" : { +// area : { +// "null" : match { controller = "Products", action = "Index" } +// } +// } +// } +// } +// } +// } +internal static class DecisionTreeBuilder { - // This code generates a minimal tree of decision criteria that map known categorical data - // (key-value-pairs) to a set of inputs. Action Selection is the best example of how this - // can be used, so the comments here will describe the process from the point-of-view, - // though the decision tree is generally applicable to like-problems. - // - // Care has been taken here to keep the performance of building the data-structure at a - // reasonable level, as this has an impact on startup cost for action selection. Additionally - // we want to hold on to the minimal amount of memory needed once we've built the tree. - // - // Ex: - // Given actions like the following, create a decision tree that will help action - // selection work efficiently. - // - // Given any set of route data it should be possible to traverse the tree using the - // presence our route data keys (like action), and whether or not they match any of - // the known values for that route data key, to find the set of actions that match - // the route data. - // - // Actions: - // - // { controller = "Home", action = "Index" } - // { controller = "Products", action = "Index" } - // { controller = "Products", action = "Buy" } - // { area = "Admin", controller = "Users", action = "AddUser" } - // - // The generated tree looks like this (json-like-notation): - // - // { - // action : { - // "AddUser" : { - // controller : { - // "Users" : { - // area : { - // "Admin" : match { area = "Admin", controller = "Users", action = "AddUser" } - // } - // } - // } - // }, - // "Buy" : { - // controller : { - // "Products" : { - // area : { - // null : match { controller = "Products", action = "Buy" } - // } - // } - // } - // }, - // "Index" : { - // controller : { - // "Home" : { - // area : { - // null : match { controller = "Home", action = "Index" } - // } - // } - // "Products" : { - // area : { - // "null" : match { controller = "Products", action = "Index" } - // } - // } - // } - // } - // } - // } - internal static class DecisionTreeBuilder + public static DecisionTreeNode GenerateTree(IReadOnlyList items, IClassifier classifier) { - public static DecisionTreeNode GenerateTree(IReadOnlyList items, IClassifier classifier) + var itemCount = items.Count; + var itemDescriptors = new List>(itemCount); + for (var i = 0; i < itemCount; i++) { - var itemCount = items.Count; - var itemDescriptors = new List>(itemCount); - for (var i = 0; i < itemCount; i++) + var item = items[i]; + itemDescriptors.Add(new ItemDescriptor() { - var item = items[i]; - itemDescriptors.Add(new ItemDescriptor() - { - Criteria = classifier.GetCriteria(item), - Index = i, - Item = item, - }); - } - - var comparer = new DecisionCriterionValueEqualityComparer(classifier.ValueComparer); - return GenerateNode( - new TreeBuilderContext(), - comparer, - itemDescriptors); + Criteria = classifier.GetCriteria(item), + Index = i, + Item = item, + }); } - private static DecisionTreeNode GenerateNode( - TreeBuilderContext context, - DecisionCriterionValueEqualityComparer comparer, - List> items) + var comparer = new DecisionCriterionValueEqualityComparer(classifier.ValueComparer); + return GenerateNode( + new TreeBuilderContext(), + comparer, + itemDescriptors); + } + + private static DecisionTreeNode GenerateNode( + TreeBuilderContext context, + DecisionCriterionValueEqualityComparer comparer, + List> items) + { + // The extreme use of generics here is intended to reduce the number of intermediate + // allocations of wrapper classes. Performance testing found that building these trees allocates + // significant memory that we can avoid and that it has a real impact on startup. + var criteria = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Matches are items that have no remaining criteria - at this point in the tree + // they are considered accepted. + var matches = new List(); + + // For each item in the working set, we want to map it to it's possible criteria-branch + // pairings, then reduce that tree to the minimal set. + foreach (var item in items) { - // The extreme use of generics here is intended to reduce the number of intermediate - // allocations of wrapper classes. Performance testing found that building these trees allocates - // significant memory that we can avoid and that it has a real impact on startup. - var criteria = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // Matches are items that have no remaining criteria - at this point in the tree - // they are considered accepted. - var matches = new List(); - - // For each item in the working set, we want to map it to it's possible criteria-branch - // pairings, then reduce that tree to the minimal set. - foreach (var item in items) - { - var unsatisfiedCriteria = 0; + var unsatisfiedCriteria = 0; - foreach (var kvp in item.Criteria) + foreach (var kvp in item.Criteria) + { + // context.CurrentCriteria is the logical 'stack' of criteria that we've already processed + // on this branch of the tree. + if (context.CurrentCriteria.Contains(kvp.Key)) { - // context.CurrentCriteria is the logical 'stack' of criteria that we've already processed - // on this branch of the tree. - if (context.CurrentCriteria.Contains(kvp.Key)) - { - continue; - } - - unsatisfiedCriteria++; - - if (!criteria.TryGetValue(kvp.Key, out var criterion)) - { - criterion = new Criterion(comparer); - criteria.Add(kvp.Key, criterion); - } + continue; + } - if (!criterion.TryGetValue(kvp.Value, out var branch)) - { - branch = new List>(); - criterion.Add(kvp.Value, branch); - } + unsatisfiedCriteria++; - branch.Add(item); + if (!criteria.TryGetValue(kvp.Key, out var criterion)) + { + criterion = new Criterion(comparer); + criteria.Add(kvp.Key, criterion); } - // If all of the criteria on item are satisfied by the 'stack' then this item is a match. - if (unsatisfiedCriteria == 0) + if (!criterion.TryGetValue(kvp.Value, out var branch)) { - matches.Add(item.Item); + branch = new List>(); + criterion.Add(kvp.Value, branch); } + + branch.Add(item); } - // Iterate criteria in order of branchiness to determine which one to explore next. If a criterion - // has no 'new' matches under it then we can just eliminate that part of the tree. - var reducedCriteria = new List>(); - foreach (var criterion in criteria.OrderByDescending(c => c.Value.Count)) + // If all of the criteria on item are satisfied by the 'stack' then this item is a match. + if (unsatisfiedCriteria == 0) { - var reducedBranches = new Dictionary>(comparer.InnerComparer); + matches.Add(item.Item); + } + } - foreach (var branch in criterion.Value) - { - bool hasReducedItems = false; + // Iterate criteria in order of branchiness to determine which one to explore next. If a criterion + // has no 'new' matches under it then we can just eliminate that part of the tree. + var reducedCriteria = new List>(); + foreach (var criterion in criteria.OrderByDescending(c => c.Value.Count)) + { + var reducedBranches = new Dictionary>(comparer.InnerComparer); - foreach (var item in branch.Value) - { - if (context.MatchedItems.Add(item)) - { - hasReducedItems = true; - } - } + foreach (var branch in criterion.Value) + { + bool hasReducedItems = false; - if (hasReducedItems) + foreach (var item in branch.Value) + { + if (context.MatchedItems.Add(item)) { - var childContext = new TreeBuilderContext(context); - childContext.CurrentCriteria.Add(criterion.Key); - - var newBranch = GenerateNode(childContext, comparer, branch.Value); - reducedBranches.Add(branch.Key.Value, newBranch); + hasReducedItems = true; } } - if (reducedBranches.Count > 0) + if (hasReducedItems) { - var newCriterion = new DecisionCriterion() - { - Key = criterion.Key, - Branches = reducedBranches, - }; + var childContext = new TreeBuilderContext(context); + childContext.CurrentCriteria.Add(criterion.Key); - reducedCriteria.Add(newCriterion); + var newBranch = GenerateNode(childContext, comparer, branch.Value); + reducedBranches.Add(branch.Key.Value, newBranch); } } - return new DecisionTreeNode() + if (reducedBranches.Count > 0) { - Criteria = reducedCriteria, - Matches = matches, - }; - } + var newCriterion = new DecisionCriterion() + { + Key = criterion.Key, + Branches = reducedBranches, + }; - private class TreeBuilderContext - { - public TreeBuilderContext() - { - CurrentCriteria = new HashSet(StringComparer.OrdinalIgnoreCase); - MatchedItems = new HashSet>(); + reducedCriteria.Add(newCriterion); } + } - public TreeBuilderContext(TreeBuilderContext other) - { - CurrentCriteria = new HashSet(other.CurrentCriteria, StringComparer.OrdinalIgnoreCase); - MatchedItems = new HashSet>(); - } + return new DecisionTreeNode() + { + Criteria = reducedCriteria, + Matches = matches, + }; + } - public HashSet CurrentCriteria { get; private set; } + private class TreeBuilderContext + { + public TreeBuilderContext() + { + CurrentCriteria = new HashSet(StringComparer.OrdinalIgnoreCase); + MatchedItems = new HashSet>(); + } - public HashSet> MatchedItems { get; private set; } + public TreeBuilderContext(TreeBuilderContext other) + { + CurrentCriteria = new HashSet(other.CurrentCriteria, StringComparer.OrdinalIgnoreCase); + MatchedItems = new HashSet>(); } - // Subclass just to give a logical name to a mess of generics - private class Criterion : Dictionary>> + public HashSet CurrentCriteria { get; private set; } + + public HashSet> MatchedItems { get; private set; } + } + + // Subclass just to give a logical name to a mess of generics + private class Criterion : Dictionary>> + { + public Criterion(DecisionCriterionValueEqualityComparer comparer) + : base(comparer) { - public Criterion(DecisionCriterionValueEqualityComparer comparer) - : base(comparer) - { - } } } } diff --git a/src/Http/Routing/src/DecisionTree/DecisionTreeNode.cs b/src/Http/Routing/src/DecisionTree/DecisionTreeNode.cs index eca4006483..b4776a0b55 100644 --- a/src/Http/Routing/src/DecisionTree/DecisionTreeNode.cs +++ b/src/Http/Routing/src/DecisionTree/DecisionTreeNode.cs @@ -5,18 +5,17 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.Routing.DecisionTree +namespace Microsoft.AspNetCore.Routing.DecisionTree; + +// Data structure representing a node in a decision tree. These are created in DecisionTreeBuilder +// and walked to find a set of items matching some input criteria. +internal class DecisionTreeNode { - // Data structure representing a node in a decision tree. These are created in DecisionTreeBuilder - // and walked to find a set of items matching some input criteria. - internal class DecisionTreeNode - { - // The list of matches for the current node. This represents a set of items that have had all - // of their criteria matched if control gets to this point in the tree. - public IList Matches { get; set; } + // The list of matches for the current node. This represents a set of items that have had all + // of their criteria matched if control gets to this point in the tree. + public IList Matches { get; set; } - // Additional criteria that further branch out from this node. Walk these to fine more items - // matching the input data. - public IList> Criteria { get; set; } - } + // Additional criteria that further branch out from this node. Walk these to fine more items + // matching the input data. + public IList> Criteria { get; set; } } diff --git a/src/Http/Routing/src/DecisionTree/IClassifier.cs b/src/Http/Routing/src/DecisionTree/IClassifier.cs index 6ce92a8673..1571bee942 100644 --- a/src/Http/Routing/src/DecisionTree/IClassifier.cs +++ b/src/Http/Routing/src/DecisionTree/IClassifier.cs @@ -3,12 +3,11 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.Routing.DecisionTree +namespace Microsoft.AspNetCore.Routing.DecisionTree; + +internal interface IClassifier { - internal interface IClassifier - { - IDictionary GetCriteria(TItem item); + IDictionary GetCriteria(TItem item); - IEqualityComparer ValueComparer { get; } - } -} \ No newline at end of file + IEqualityComparer ValueComparer { get; } +} diff --git a/src/Http/Routing/src/DecisionTree/ItemDescriptor.cs b/src/Http/Routing/src/DecisionTree/ItemDescriptor.cs index 4b49217883..ce35ddc5d1 100644 --- a/src/Http/Routing/src/DecisionTree/ItemDescriptor.cs +++ b/src/Http/Routing/src/DecisionTree/ItemDescriptor.cs @@ -5,14 +5,13 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.Routing.DecisionTree +namespace Microsoft.AspNetCore.Routing.DecisionTree; + +internal class ItemDescriptor { - internal class ItemDescriptor - { - public IDictionary Criteria { get; set; } + public IDictionary Criteria { get; set; } - public int Index { get; set; } + public int Index { get; set; } - public TItem Item { get; set; } - } + public TItem Item { get; set; } } diff --git a/src/Http/Routing/src/DefaultEndpointConventionBuilder.cs b/src/Http/Routing/src/DefaultEndpointConventionBuilder.cs index 992e23a551..c80b20856e 100644 --- a/src/Http/Routing/src/DefaultEndpointConventionBuilder.cs +++ b/src/Http/Routing/src/DefaultEndpointConventionBuilder.cs @@ -6,46 +6,45 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +internal class DefaultEndpointConventionBuilder : IEndpointConventionBuilder { - internal class DefaultEndpointConventionBuilder : IEndpointConventionBuilder + internal EndpointBuilder EndpointBuilder { get; } + + private List>? _conventions; + + public DefaultEndpointConventionBuilder(EndpointBuilder endpointBuilder) { - internal EndpointBuilder EndpointBuilder { get; } + EndpointBuilder = endpointBuilder; + _conventions = new(); + } - private List>? _conventions; + public void Add(Action convention) + { + var conventions = _conventions; - public DefaultEndpointConventionBuilder(EndpointBuilder endpointBuilder) + if (conventions is null) { - EndpointBuilder = endpointBuilder; - _conventions = new(); + throw new InvalidOperationException("Conventions cannot be added after building the endpoint"); } - public void Add(Action convention) - { - var conventions = _conventions; - - if (conventions is null) - { - throw new InvalidOperationException("Conventions cannot be added after building the endpoint"); - } + conventions.Add(convention); + } - conventions.Add(convention); - } + public Endpoint Build() + { + // Only apply the conventions once + var conventions = Interlocked.Exchange(ref _conventions, null); - public Endpoint Build() + if (conventions is not null) { - // Only apply the conventions once - var conventions = Interlocked.Exchange(ref _conventions, null); - - if (conventions is not null) + foreach (var convention in conventions) { - foreach (var convention in conventions) - { - convention(EndpointBuilder); - } + convention(EndpointBuilder); } - - return EndpointBuilder.Build(); } + + return EndpointBuilder.Build(); } } diff --git a/src/Http/Routing/src/DefaultEndpointDataSource.cs b/src/Http/Routing/src/DefaultEndpointDataSource.cs index 1793c32b20..2728f1647f 100644 --- a/src/Http/Routing/src/DefaultEndpointDataSource.cs +++ b/src/Http/Routing/src/DefaultEndpointDataSource.cs @@ -7,53 +7,52 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Provides a collection of instances. +/// +public sealed class DefaultEndpointDataSource : EndpointDataSource { + private readonly IReadOnlyList _endpoints; + /// - /// Provides a collection of instances. + /// Initializes a new instance of the class. /// - public sealed class DefaultEndpointDataSource : EndpointDataSource + /// The instances that the data source will return. + public DefaultEndpointDataSource(params Endpoint[] endpoints) { - private readonly IReadOnlyList _endpoints; - - /// - /// Initializes a new instance of the class. - /// - /// The instances that the data source will return. - public DefaultEndpointDataSource(params Endpoint[] endpoints) + if (endpoints == null) { - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } - - _endpoints = (Endpoint[])endpoints.Clone(); + throw new ArgumentNullException(nameof(endpoints)); } - /// - /// Initializes a new instance of the class. - /// - /// The instances that the data source will return. - public DefaultEndpointDataSource(IEnumerable endpoints) - { - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } + _endpoints = (Endpoint[])endpoints.Clone(); + } - _endpoints = new List(endpoints); + /// + /// Initializes a new instance of the class. + /// + /// The instances that the data source will return. + public DefaultEndpointDataSource(IEnumerable endpoints) + { + if (endpoints == null) + { + throw new ArgumentNullException(nameof(endpoints)); } - /// - /// Gets a used to signal invalidation of cached - /// instances. - /// - /// The . - public override IChangeToken GetChangeToken() => NullChangeToken.Singleton; - - /// - /// Returns a read-only collection of instances. - /// - public override IReadOnlyList Endpoints => _endpoints; + _endpoints = new List(endpoints); } + + /// + /// Gets a used to signal invalidation of cached + /// instances. + /// + /// The . + public override IChangeToken GetChangeToken() => NullChangeToken.Singleton; + + /// + /// Returns a read-only collection of instances. + /// + public override IReadOnlyList Endpoints => _endpoints; } diff --git a/src/Http/Routing/src/DefaultEndpointRouteBuilder.cs b/src/Http/Routing/src/DefaultEndpointRouteBuilder.cs index e87e44d7ea..1d747f14a6 100644 --- a/src/Http/Routing/src/DefaultEndpointRouteBuilder.cs +++ b/src/Http/Routing/src/DefaultEndpointRouteBuilder.cs @@ -5,22 +5,21 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Builder; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +internal class DefaultEndpointRouteBuilder : IEndpointRouteBuilder { - internal class DefaultEndpointRouteBuilder : IEndpointRouteBuilder + public DefaultEndpointRouteBuilder(IApplicationBuilder applicationBuilder) { - public DefaultEndpointRouteBuilder(IApplicationBuilder applicationBuilder) - { - ApplicationBuilder = applicationBuilder ?? throw new ArgumentNullException(nameof(applicationBuilder)); - DataSources = new List(); - } + ApplicationBuilder = applicationBuilder ?? throw new ArgumentNullException(nameof(applicationBuilder)); + DataSources = new List(); + } - public IApplicationBuilder ApplicationBuilder { get; } + public IApplicationBuilder ApplicationBuilder { get; } - public IApplicationBuilder CreateApplicationBuilder() => ApplicationBuilder.New(); + public IApplicationBuilder CreateApplicationBuilder() => ApplicationBuilder.New(); - public ICollection DataSources { get; } + public ICollection DataSources { get; } - public IServiceProvider ServiceProvider => ApplicationBuilder.ApplicationServices; - } + public IServiceProvider ServiceProvider => ApplicationBuilder.ApplicationServices; } diff --git a/src/Http/Routing/src/DefaultInlineConstraintResolver.cs b/src/Http/Routing/src/DefaultInlineConstraintResolver.cs index 4f58fceb3d..ad13f5ba1a 100644 --- a/src/Http/Routing/src/DefaultInlineConstraintResolver.cs +++ b/src/Http/Routing/src/DefaultInlineConstraintResolver.cs @@ -7,60 +7,59 @@ using System; using System.Collections.Generic; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// The default implementation of . Resolves constraints by parsing +/// a constraint key and constraint arguments, using a map to resolve the constraint type, and calling an +/// appropriate constructor for the constraint type. +/// +public class DefaultInlineConstraintResolver : IInlineConstraintResolver { + private readonly IDictionary _inlineConstraintMap; + private readonly IServiceProvider _serviceProvider; + /// - /// The default implementation of . Resolves constraints by parsing - /// a constraint key and constraint arguments, using a map to resolve the constraint type, and calling an - /// appropriate constructor for the constraint type. + /// Initializes a new instance of the class. /// - public class DefaultInlineConstraintResolver : IInlineConstraintResolver + /// Accessor for containing the constraints of interest. + /// The to get service arguments from. + public DefaultInlineConstraintResolver(IOptions routeOptions, IServiceProvider serviceProvider) { - private readonly IDictionary _inlineConstraintMap; - private readonly IServiceProvider _serviceProvider; - - /// - /// Initializes a new instance of the class. - /// - /// Accessor for containing the constraints of interest. - /// The to get service arguments from. - public DefaultInlineConstraintResolver(IOptions routeOptions, IServiceProvider serviceProvider) + if (routeOptions == null) { - if (routeOptions == null) - { - throw new ArgumentNullException(nameof(routeOptions)); - } - - if (serviceProvider == null) - { - throw new ArgumentNullException(nameof(serviceProvider)); - } - - _inlineConstraintMap = routeOptions.Value.ConstraintMap; - _serviceProvider = serviceProvider; + throw new ArgumentNullException(nameof(routeOptions)); } - /// - /// - /// A typical constraint looks like the following - /// "exampleConstraint(arg1, arg2, 12)". - /// Here if the type registered for exampleConstraint has a single constructor with one argument, - /// The entire string "arg1, arg2, 12" will be treated as a single argument. - /// In all other cases arguments are split at comma. - /// - public virtual IRouteConstraint? ResolveConstraint(string inlineConstraint) + if (serviceProvider == null) { - if (inlineConstraint == null) - { - throw new ArgumentNullException(nameof(inlineConstraint)); - } + throw new ArgumentNullException(nameof(serviceProvider)); + } - // This will return null if the text resolves to a non-IRouteConstraint - return ParameterPolicyActivator.ResolveParameterPolicy( - _inlineConstraintMap, - _serviceProvider, - inlineConstraint, - out _); + _inlineConstraintMap = routeOptions.Value.ConstraintMap; + _serviceProvider = serviceProvider; + } + + /// + /// + /// A typical constraint looks like the following + /// "exampleConstraint(arg1, arg2, 12)". + /// Here if the type registered for exampleConstraint has a single constructor with one argument, + /// The entire string "arg1, arg2, 12" will be treated as a single argument. + /// In all other cases arguments are split at comma. + /// + public virtual IRouteConstraint? ResolveConstraint(string inlineConstraint) + { + if (inlineConstraint == null) + { + throw new ArgumentNullException(nameof(inlineConstraint)); } + + // This will return null if the text resolves to a non-IRouteConstraint + return ParameterPolicyActivator.ResolveParameterPolicy( + _inlineConstraintMap, + _serviceProvider, + inlineConstraint, + out _); } } diff --git a/src/Http/Routing/src/DefaultLinkGenerator.cs b/src/Http/Routing/src/DefaultLinkGenerator.cs index 72f3a68cbc..7b39a9e140 100644 --- a/src/Http/Routing/src/DefaultLinkGenerator.cs +++ b/src/Http/Routing/src/DefaultLinkGenerator.cs @@ -16,456 +16,455 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +internal sealed partial class DefaultLinkGenerator : LinkGenerator, IDisposable { - internal sealed partial class DefaultLinkGenerator : LinkGenerator, IDisposable + private readonly TemplateBinderFactory _binderFactory; + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + + // A LinkOptions object initialized with the values from RouteOptions + // Used when the user didn't specify something more global. + private readonly LinkOptions _globalLinkOptions; + + // Caches TemplateBinder instances + private readonly DataSourceDependentCache> _cache; + + // Used to initialize TemplateBinder instances + private readonly Func _createTemplateBinder; + + public DefaultLinkGenerator( + ParameterPolicyFactory parameterPolicyFactory, + TemplateBinderFactory binderFactory, + EndpointDataSource dataSource, + IOptions routeOptions, + ILogger logger, + IServiceProvider serviceProvider) { - private readonly TemplateBinderFactory _binderFactory; - private readonly ILogger _logger; - private readonly IServiceProvider _serviceProvider; - - // A LinkOptions object initialized with the values from RouteOptions - // Used when the user didn't specify something more global. - private readonly LinkOptions _globalLinkOptions; - - // Caches TemplateBinder instances - private readonly DataSourceDependentCache> _cache; - - // Used to initialize TemplateBinder instances - private readonly Func _createTemplateBinder; - - public DefaultLinkGenerator( - ParameterPolicyFactory parameterPolicyFactory, - TemplateBinderFactory binderFactory, - EndpointDataSource dataSource, - IOptions routeOptions, - ILogger logger, - IServiceProvider serviceProvider) - { - _binderFactory = binderFactory; - _logger = logger; - _serviceProvider = serviceProvider; + _binderFactory = binderFactory; + _logger = logger; + _serviceProvider = serviceProvider; - // We cache TemplateBinder instances per-Endpoint for performance, but we want to wipe out - // that cache is the endpoints change so that we don't allow unbounded memory growth. - _cache = new DataSourceDependentCache>(dataSource, (_) => - { + // We cache TemplateBinder instances per-Endpoint for performance, but we want to wipe out + // that cache is the endpoints change so that we don't allow unbounded memory growth. + _cache = new DataSourceDependentCache>(dataSource, (_) => + { // We don't eagerly fill this cache because there's no real reason to. Unlike URL matching, we don't // need to build a big data structure up front to be correct. return new ConcurrentDictionary(); - }); + }); - // Cached to avoid per-call allocation of a delegate on lookup. - _createTemplateBinder = CreateTemplateBinder; - - _globalLinkOptions = new LinkOptions() - { - AppendTrailingSlash = routeOptions.Value.AppendTrailingSlash, - LowercaseQueryStrings = routeOptions.Value.LowercaseQueryStrings, - LowercaseUrls = routeOptions.Value.LowercaseUrls, - }; - } + // Cached to avoid per-call allocation of a delegate on lookup. + _createTemplateBinder = CreateTemplateBinder; - public override string? GetPathByAddress( - HttpContext httpContext, - TAddress address, - RouteValueDictionary values, - RouteValueDictionary? ambientValues = default, - PathString? pathBase = default, - FragmentString fragment = default, - LinkOptions? options = null) + _globalLinkOptions = new LinkOptions() { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - var endpoints = GetEndpoints(address); - if (endpoints.Count == 0) - { - return null; - } + AppendTrailingSlash = routeOptions.Value.AppendTrailingSlash, + LowercaseQueryStrings = routeOptions.Value.LowercaseQueryStrings, + LowercaseUrls = routeOptions.Value.LowercaseUrls, + }; + } - return GetPathByEndpoints( - httpContext, - endpoints, - values, - ambientValues, - pathBase ?? httpContext.Request.PathBase, - fragment, - options); + public override string? GetPathByAddress( + HttpContext httpContext, + TAddress address, + RouteValueDictionary values, + RouteValueDictionary? ambientValues = default, + PathString? pathBase = default, + FragmentString fragment = default, + LinkOptions? options = null) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); } - public override string? GetPathByAddress( - TAddress address, - RouteValueDictionary values, - PathString pathBase = default, - FragmentString fragment = default, - LinkOptions? options = null) + var endpoints = GetEndpoints(address); + if (endpoints.Count == 0) { - var endpoints = GetEndpoints(address); - if (endpoints.Count == 0) - { - return null; - } - - return GetPathByEndpoints( - httpContext: null, - endpoints, - values, - ambientValues: null, - pathBase: pathBase, - fragment: fragment, - options: options); + return null; } - public override string? GetUriByAddress( - HttpContext httpContext, - TAddress address, - RouteValueDictionary values, - RouteValueDictionary? ambientValues = default, - string? scheme = default, - HostString? host = default, - PathString? pathBase = default, - FragmentString fragment = default, - LinkOptions? options = null) + return GetPathByEndpoints( + httpContext, + endpoints, + values, + ambientValues, + pathBase ?? httpContext.Request.PathBase, + fragment, + options); + } + + public override string? GetPathByAddress( + TAddress address, + RouteValueDictionary values, + PathString pathBase = default, + FragmentString fragment = default, + LinkOptions? options = null) + { + var endpoints = GetEndpoints(address); + if (endpoints.Count == 0) { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } + return null; + } - var endpoints = GetEndpoints(address); - if (endpoints.Count == 0) - { - return null; - } + return GetPathByEndpoints( + httpContext: null, + endpoints, + values, + ambientValues: null, + pathBase: pathBase, + fragment: fragment, + options: options); + } - return GetUriByEndpoints( - endpoints, - values, - ambientValues, - scheme ?? httpContext.Request.Scheme, - host ?? httpContext.Request.Host, - pathBase ?? httpContext.Request.PathBase, - fragment, - options); + public override string? GetUriByAddress( + HttpContext httpContext, + TAddress address, + RouteValueDictionary values, + RouteValueDictionary? ambientValues = default, + string? scheme = default, + HostString? host = default, + PathString? pathBase = default, + FragmentString fragment = default, + LinkOptions? options = null) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); } - public override string? GetUriByAddress( - TAddress address, - RouteValueDictionary values, - string? scheme, - HostString host, - PathString pathBase = default, - FragmentString fragment = default, - LinkOptions? options = null) + var endpoints = GetEndpoints(address); + if (endpoints.Count == 0) { - if (string.IsNullOrEmpty(scheme)) - { - throw new ArgumentException("A scheme must be provided.", nameof(scheme)); - } - - if (!host.HasValue) - { - throw new ArgumentException("A host must be provided.", nameof(host)); - } + return null; + } - var endpoints = GetEndpoints(address); - if (endpoints.Count == 0) - { - return null; - } + return GetUriByEndpoints( + endpoints, + values, + ambientValues, + scheme ?? httpContext.Request.Scheme, + host ?? httpContext.Request.Host, + pathBase ?? httpContext.Request.PathBase, + fragment, + options); + } - return GetUriByEndpoints( - endpoints, - values, - ambientValues: null, - scheme: scheme, - host: host, - pathBase: pathBase, - fragment: fragment, - options: options); + public override string? GetUriByAddress( + TAddress address, + RouteValueDictionary values, + string? scheme, + HostString host, + PathString pathBase = default, + FragmentString fragment = default, + LinkOptions? options = null) + { + if (string.IsNullOrEmpty(scheme)) + { + throw new ArgumentException("A scheme must be provided.", nameof(scheme)); } - private List GetEndpoints(TAddress address) + if (!host.HasValue) { - var addressingScheme = _serviceProvider.GetRequiredService>(); - var endpoints = addressingScheme.FindEndpoints(address).OfType().ToList(); - - if (endpoints.Count == 0) - { - Log.EndpointsNotFound(_logger, address); - } - else - { - Log.EndpointsFound(_logger, address, endpoints); - } - - return endpoints; + throw new ArgumentException("A host must be provided.", nameof(host)); } - private string? GetPathByEndpoints( - HttpContext? httpContext, - List endpoints, - RouteValueDictionary values, - RouteValueDictionary? ambientValues, - PathString pathBase, - FragmentString fragment, - LinkOptions? options) + var endpoints = GetEndpoints(address); + if (endpoints.Count == 0) { - for (var i = 0; i < endpoints.Count; i++) - { - var endpoint = endpoints[i]; - if (TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: values, - ambientValues: ambientValues, - options: options, - result: out var result)) - { - var uri = UriHelper.BuildRelative( - pathBase, - result.path, - result.query, - fragment); - Log.LinkGenerationSucceeded(_logger, endpoints, uri); - return uri; - } - } - - Log.LinkGenerationFailed(_logger, endpoints); return null; } - // Also called from DefaultLinkGenerationTemplate - public string? GetUriByEndpoints( - List endpoints, - RouteValueDictionary values, - RouteValueDictionary? ambientValues, - string scheme, - HostString host, - PathString pathBase, - FragmentString fragment, - LinkOptions? options) - { - for (var i = 0; i < endpoints.Count; i++) - { - var endpoint = endpoints[i]; - if (TryProcessTemplate( - httpContext: null, - endpoint: endpoint, - values: values, - ambientValues: ambientValues, - options: options, - result: out var result)) - { - var uri = UriHelper.BuildAbsolute( - scheme, - host, - pathBase, - result.path, - result.query, - fragment); - Log.LinkGenerationSucceeded(_logger, endpoints, uri); - return uri; - } - } + return GetUriByEndpoints( + endpoints, + values, + ambientValues: null, + scheme: scheme, + host: host, + pathBase: pathBase, + fragment: fragment, + options: options); + } - Log.LinkGenerationFailed(_logger, endpoints); - return null; - } + private List GetEndpoints(TAddress address) + { + var addressingScheme = _serviceProvider.GetRequiredService>(); + var endpoints = addressingScheme.FindEndpoints(address).OfType().ToList(); - private TemplateBinder CreateTemplateBinder(RouteEndpoint endpoint) + if (endpoints.Count == 0) { - return _binderFactory.Create(endpoint.RoutePattern); + Log.EndpointsNotFound(_logger, address); } - - // Internal for testing - internal TemplateBinder GetTemplateBinder(RouteEndpoint endpoint) => _cache.EnsureInitialized().GetOrAdd(endpoint, _createTemplateBinder); - - // Internal for testing - internal bool TryProcessTemplate( - HttpContext? httpContext, - RouteEndpoint endpoint, - RouteValueDictionary values, - RouteValueDictionary? ambientValues, - LinkOptions? options, - out (PathString path, QueryString query) result) + else { - var templateBinder = GetTemplateBinder(endpoint); + Log.EndpointsFound(_logger, address, endpoints); + } - var templateValuesResult = templateBinder.GetValues(ambientValues, values); - if (templateValuesResult == null) - { - // We're missing one of the required values for this route. - result = default; - Log.TemplateFailedRequiredValues(_logger, endpoint, ambientValues, values); - return false; - } + return endpoints; + } - if (!templateBinder.TryProcessConstraints(httpContext, templateValuesResult.CombinedValues, out var parameterName, out var constraint)) + private string? GetPathByEndpoints( + HttpContext? httpContext, + List endpoints, + RouteValueDictionary values, + RouteValueDictionary? ambientValues, + PathString pathBase, + FragmentString fragment, + LinkOptions? options) + { + for (var i = 0; i < endpoints.Count; i++) + { + var endpoint = endpoints[i]; + if (TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: values, + ambientValues: ambientValues, + options: options, + result: out var result)) { - result = default; - Log.TemplateFailedConstraint(_logger, endpoint, parameterName, constraint, templateValuesResult.CombinedValues); - return false; + var uri = UriHelper.BuildRelative( + pathBase, + result.path, + result.query, + fragment); + Log.LinkGenerationSucceeded(_logger, endpoints, uri); + return uri; } + } + + Log.LinkGenerationFailed(_logger, endpoints); + return null; + } - if (!templateBinder.TryBindValues(templateValuesResult.AcceptedValues, options, _globalLinkOptions, out result)) + // Also called from DefaultLinkGenerationTemplate + public string? GetUriByEndpoints( + List endpoints, + RouteValueDictionary values, + RouteValueDictionary? ambientValues, + string scheme, + HostString host, + PathString pathBase, + FragmentString fragment, + LinkOptions? options) + { + for (var i = 0; i < endpoints.Count; i++) + { + var endpoint = endpoints[i]; + if (TryProcessTemplate( + httpContext: null, + endpoint: endpoint, + values: values, + ambientValues: ambientValues, + options: options, + result: out var result)) { - Log.TemplateFailedExpansion(_logger, endpoint, templateValuesResult.AcceptedValues); - return false; + var uri = UriHelper.BuildAbsolute( + scheme, + host, + pathBase, + result.path, + result.query, + fragment); + Log.LinkGenerationSucceeded(_logger, endpoints, uri); + return uri; } + } + + Log.LinkGenerationFailed(_logger, endpoints); + return null; + } - Log.TemplateSucceeded(_logger, endpoint, result.path, result.query); - return true; + private TemplateBinder CreateTemplateBinder(RouteEndpoint endpoint) + { + return _binderFactory.Create(endpoint.RoutePattern); + } + + // Internal for testing + internal TemplateBinder GetTemplateBinder(RouteEndpoint endpoint) => _cache.EnsureInitialized().GetOrAdd(endpoint, _createTemplateBinder); + + // Internal for testing + internal bool TryProcessTemplate( + HttpContext? httpContext, + RouteEndpoint endpoint, + RouteValueDictionary values, + RouteValueDictionary? ambientValues, + LinkOptions? options, + out (PathString path, QueryString query) result) + { + var templateBinder = GetTemplateBinder(endpoint); + + var templateValuesResult = templateBinder.GetValues(ambientValues, values); + if (templateValuesResult == null) + { + // We're missing one of the required values for this route. + result = default; + Log.TemplateFailedRequiredValues(_logger, endpoint, ambientValues, values); + return false; } - // Also called from DefaultLinkGenerationTemplate - public static RouteValueDictionary? GetAmbientValues(HttpContext? httpContext) + if (!templateBinder.TryProcessConstraints(httpContext, templateValuesResult.CombinedValues, out var parameterName, out var constraint)) { - return httpContext?.Features.Get()?.RouteValues; + result = default; + Log.TemplateFailedConstraint(_logger, endpoint, parameterName, constraint, templateValuesResult.CombinedValues); + return false; } - public void Dispose() + if (!templateBinder.TryBindValues(templateValuesResult.AcceptedValues, options, _globalLinkOptions, out result)) { - _cache.Dispose(); + Log.TemplateFailedExpansion(_logger, endpoint, templateValuesResult.AcceptedValues); + return false; } - private static partial class Log + Log.TemplateSucceeded(_logger, endpoint, result.path, result.query); + return true; + } + + // Also called from DefaultLinkGenerationTemplate + public static RouteValueDictionary? GetAmbientValues(HttpContext? httpContext) + { + return httpContext?.Features.Get()?.RouteValues; + } + + public void Dispose() + { + _cache.Dispose(); + } + + private static partial class Log + { + public static void EndpointsFound(ILogger logger, object? address, IEnumerable endpoints) { - public static void EndpointsFound(ILogger logger, object? address, IEnumerable endpoints) + // Checking level again to avoid allocation on the common path + if (logger.IsEnabled(LogLevel.Debug)) { - // Checking level again to avoid allocation on the common path - if (logger.IsEnabled(LogLevel.Debug)) - { - EndpointsFound(logger, endpoints.Select(e => e.DisplayName), address); - } + EndpointsFound(logger, endpoints.Select(e => e.DisplayName), address); } + } - [LoggerMessage(100, LogLevel.Debug, "Found the endpoints {Endpoints} for address {Address}", EventName = "EndpointsFound", SkipEnabledCheck = true)] - private static partial void EndpointsFound(ILogger logger, IEnumerable endpoints, object? address); + [LoggerMessage(100, LogLevel.Debug, "Found the endpoints {Endpoints} for address {Address}", EventName = "EndpointsFound", SkipEnabledCheck = true)] + private static partial void EndpointsFound(ILogger logger, IEnumerable endpoints, object? address); - [LoggerMessage(101, LogLevel.Debug, "No endpoints found for address {Address}", EventName = "EndpointsNotFound")] - public static partial void EndpointsNotFound(ILogger logger, object? address); + [LoggerMessage(101, LogLevel.Debug, "No endpoints found for address {Address}", EventName = "EndpointsNotFound")] + public static partial void EndpointsNotFound(ILogger logger, object? address); - public static void TemplateSucceeded(ILogger logger, RouteEndpoint endpoint, PathString path, QueryString query) - => TemplateSucceeded(logger, endpoint.RoutePattern.RawText, endpoint.DisplayName, path.Value, query.Value); + public static void TemplateSucceeded(ILogger logger, RouteEndpoint endpoint, PathString path, QueryString query) + => TemplateSucceeded(logger, endpoint.RoutePattern.RawText, endpoint.DisplayName, path.Value, query.Value); - [LoggerMessage(102, LogLevel.Debug, - "Successfully processed template {Template} for {Endpoint} resulting in {Path} and {Query}", - EventName = "TemplateSucceeded")] - private static partial void TemplateSucceeded(ILogger logger, string? template, string? endpoint, string? path, string? query); + [LoggerMessage(102, LogLevel.Debug, + "Successfully processed template {Template} for {Endpoint} resulting in {Path} and {Query}", + EventName = "TemplateSucceeded")] + private static partial void TemplateSucceeded(ILogger logger, string? template, string? endpoint, string? path, string? query); - public static void TemplateFailedRequiredValues(ILogger logger, RouteEndpoint endpoint, RouteValueDictionary? ambientValues, RouteValueDictionary values) + public static void TemplateFailedRequiredValues(ILogger logger, RouteEndpoint endpoint, RouteValueDictionary? ambientValues, RouteValueDictionary values) + { + // Checking level again to avoid allocation on the common path + if (logger.IsEnabled(LogLevel.Debug)) { - // Checking level again to avoid allocation on the common path - if (logger.IsEnabled(LogLevel.Debug)) - { - TemplateFailedRequiredValues(logger, endpoint.RoutePattern.RawText, endpoint.DisplayName, FormatRouteValues(ambientValues), FormatRouteValues(values), FormatRouteValues(endpoint.RoutePattern.Defaults)); - } + TemplateFailedRequiredValues(logger, endpoint.RoutePattern.RawText, endpoint.DisplayName, FormatRouteValues(ambientValues), FormatRouteValues(values), FormatRouteValues(endpoint.RoutePattern.Defaults)); } + } - [LoggerMessage(103, LogLevel.Debug, - "Failed to process the template {Template} for {Endpoint}. " + - "A required route value is missing, or has a different value from the required default values. " + - "Supplied ambient values {AmbientValues} and {Values} with default values {Defaults}", - EventName = "TemplateFailedRequiredValues", - SkipEnabledCheck = true)] - private static partial void TemplateFailedRequiredValues(ILogger logger, string? template, string? endpoint, string ambientValues, string values, string defaults); + [LoggerMessage(103, LogLevel.Debug, + "Failed to process the template {Template} for {Endpoint}. " + + "A required route value is missing, or has a different value from the required default values. " + + "Supplied ambient values {AmbientValues} and {Values} with default values {Defaults}", + EventName = "TemplateFailedRequiredValues", + SkipEnabledCheck = true)] + private static partial void TemplateFailedRequiredValues(ILogger logger, string? template, string? endpoint, string ambientValues, string values, string defaults); - public static void TemplateFailedConstraint(ILogger logger, RouteEndpoint endpoint, string? parameterName, IRouteConstraint? constraint, RouteValueDictionary values) + public static void TemplateFailedConstraint(ILogger logger, RouteEndpoint endpoint, string? parameterName, IRouteConstraint? constraint, RouteValueDictionary values) + { + // Checking level again to avoid allocation on the common path + if (logger.IsEnabled(LogLevel.Debug)) { - // Checking level again to avoid allocation on the common path - if (logger.IsEnabled(LogLevel.Debug)) - { - TemplateFailedConstraint(logger, endpoint.RoutePattern.RawText, endpoint.DisplayName, constraint, parameterName, FormatRouteValues(values)); - } + TemplateFailedConstraint(logger, endpoint.RoutePattern.RawText, endpoint.DisplayName, constraint, parameterName, FormatRouteValues(values)); } + } - [LoggerMessage(107, LogLevel.Debug, - "Failed to process the template {Template} for {Endpoint}. " + - "The constraint {Constraint} for parameter {ParameterName} failed with values {Values}", - EventName = "TemplateFailedConstraint", - SkipEnabledCheck = true)] - private static partial void TemplateFailedConstraint(ILogger logger, string? template, string? endpoint, IRouteConstraint? constraint, string? parameterName, string values); + [LoggerMessage(107, LogLevel.Debug, + "Failed to process the template {Template} for {Endpoint}. " + + "The constraint {Constraint} for parameter {ParameterName} failed with values {Values}", + EventName = "TemplateFailedConstraint", + SkipEnabledCheck = true)] + private static partial void TemplateFailedConstraint(ILogger logger, string? template, string? endpoint, IRouteConstraint? constraint, string? parameterName, string values); - public static void TemplateFailedExpansion(ILogger logger, RouteEndpoint endpoint, RouteValueDictionary values) + public static void TemplateFailedExpansion(ILogger logger, RouteEndpoint endpoint, RouteValueDictionary values) + { + // Checking level again to avoid allocation on the common path + if (logger.IsEnabled(LogLevel.Debug)) { - // Checking level again to avoid allocation on the common path - if (logger.IsEnabled(LogLevel.Debug)) - { - TemplateFailedExpansion(logger, endpoint.RoutePattern.RawText, endpoint.DisplayName, FormatRouteValues(values)); - } + TemplateFailedExpansion(logger, endpoint.RoutePattern.RawText, endpoint.DisplayName, FormatRouteValues(values)); } + } - [LoggerMessage(104, LogLevel.Debug, - "Failed to process the template {Template} for {Endpoint}. " + - "The failure occurred while expanding the template with values {Values} " + - "This is usually due to a missing or empty value in a complex segment", - EventName = "TemplateFailedExpansion", - SkipEnabledCheck = true)] - private static partial void TemplateFailedExpansion(ILogger logger, string? template, string? endpoint, string values); + [LoggerMessage(104, LogLevel.Debug, + "Failed to process the template {Template} for {Endpoint}. " + + "The failure occurred while expanding the template with values {Values} " + + "This is usually due to a missing or empty value in a complex segment", + EventName = "TemplateFailedExpansion", + SkipEnabledCheck = true)] + private static partial void TemplateFailedExpansion(ILogger logger, string? template, string? endpoint, string values); - public static void LinkGenerationSucceeded(ILogger logger, IEnumerable endpoints, string uri) + public static void LinkGenerationSucceeded(ILogger logger, IEnumerable endpoints, string uri) + { + // Checking level again to avoid allocation on the common path + if (logger.IsEnabled(LogLevel.Debug)) { - // Checking level again to avoid allocation on the common path - if (logger.IsEnabled(LogLevel.Debug)) - { - LinkGenerationSucceeded(logger, endpoints.Select(e => e.DisplayName), uri); - } + LinkGenerationSucceeded(logger, endpoints.Select(e => e.DisplayName), uri); } + } - [LoggerMessage(105, LogLevel.Debug, - "Link generation succeeded for endpoints {Endpoints} with result {URI}", - EventName = "LinkGenerationSucceeded", - SkipEnabledCheck = true)] - private static partial void LinkGenerationSucceeded(ILogger logger, IEnumerable endpoints, string uri); + [LoggerMessage(105, LogLevel.Debug, + "Link generation succeeded for endpoints {Endpoints} with result {URI}", + EventName = "LinkGenerationSucceeded", + SkipEnabledCheck = true)] + private static partial void LinkGenerationSucceeded(ILogger logger, IEnumerable endpoints, string uri); - public static void LinkGenerationFailed(ILogger logger, IEnumerable endpoints) + public static void LinkGenerationFailed(ILogger logger, IEnumerable endpoints) + { + // Checking level again to avoid allocation on the common path + if (logger.IsEnabled(LogLevel.Debug)) { - // Checking level again to avoid allocation on the common path - if (logger.IsEnabled(LogLevel.Debug)) - { - LinkGenerationFailed(logger, endpoints.Select(e => e.DisplayName)); - } + LinkGenerationFailed(logger, endpoints.Select(e => e.DisplayName)); } + } - [LoggerMessage(106, LogLevel.Debug, "Link generation failed for endpoints {Endpoints}", EventName = "LinkGenerationFailed", SkipEnabledCheck = true)] - private static partial void LinkGenerationFailed(ILogger logger, IEnumerable endpoints); + [LoggerMessage(106, LogLevel.Debug, "Link generation failed for endpoints {Endpoints}", EventName = "LinkGenerationFailed", SkipEnabledCheck = true)] + private static partial void LinkGenerationFailed(ILogger logger, IEnumerable endpoints); - // EXPENSIVE: should only be used at Debug and higher levels of logging. - private static string FormatRouteValues(IReadOnlyDictionary? values) + // EXPENSIVE: should only be used at Debug and higher levels of logging. + private static string FormatRouteValues(IReadOnlyDictionary? values) + { + if (values == null || values.Count == 0) { - if (values == null || values.Count == 0) - { - return "{ }"; - } - - var builder = new StringBuilder(); - builder.Append("{ "); - - foreach (var kvp in values.OrderBy(kvp => kvp.Key)) - { - builder.Append('"'); - builder.Append(kvp.Key); - builder.Append('"'); - builder.Append(':'); - builder.Append(' '); - builder.Append('"'); - builder.Append(kvp.Value); - builder.Append('"'); - builder.Append(", "); - } - - // Trim trailing ", " - builder.Remove(builder.Length - 2, 2); - - builder.Append(" }"); - - return builder.ToString(); + return "{ }"; } + + var builder = new StringBuilder(); + builder.Append("{ "); + + foreach (var kvp in values.OrderBy(kvp => kvp.Key)) + { + builder.Append('"'); + builder.Append(kvp.Key); + builder.Append('"'); + builder.Append(':'); + builder.Append(' '); + builder.Append('"'); + builder.Append(kvp.Value); + builder.Append('"'); + builder.Append(", "); + } + + // Trim trailing ", " + builder.Remove(builder.Length - 2, 2); + + builder.Append(" }"); + + return builder.ToString(); } } } diff --git a/src/Http/Routing/src/DefaultLinkParser.cs b/src/Http/Routing/src/DefaultLinkParser.cs index d7243a081b..64a8531c3a 100644 --- a/src/Http/Routing/src/DefaultLinkParser.cs +++ b/src/Http/Routing/src/DefaultLinkParser.cs @@ -10,205 +10,204 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +internal sealed partial class DefaultLinkParser : LinkParser, IDisposable { - internal sealed partial class DefaultLinkParser : LinkParser, IDisposable - { - private readonly ParameterPolicyFactory _parameterPolicyFactory; - private readonly ILogger _logger; - private readonly IServiceProvider _serviceProvider; + private readonly ParameterPolicyFactory _parameterPolicyFactory; + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; - // Caches RoutePatternMatcher instances - private readonly DataSourceDependentCache> _matcherCache; + // Caches RoutePatternMatcher instances + private readonly DataSourceDependentCache> _matcherCache; - // Used to initialize RoutePatternMatcher and constraint instances - private readonly Func _createMatcher; + // Used to initialize RoutePatternMatcher and constraint instances + private readonly Func _createMatcher; - public DefaultLinkParser( - ParameterPolicyFactory parameterPolicyFactory, - EndpointDataSource dataSource, - ILogger logger, - IServiceProvider serviceProvider) - { - _parameterPolicyFactory = parameterPolicyFactory; - _logger = logger; - _serviceProvider = serviceProvider; + public DefaultLinkParser( + ParameterPolicyFactory parameterPolicyFactory, + EndpointDataSource dataSource, + ILogger logger, + IServiceProvider serviceProvider) + { + _parameterPolicyFactory = parameterPolicyFactory; + _logger = logger; + _serviceProvider = serviceProvider; - // We cache RoutePatternMatcher instances per-Endpoint for performance, but we want to wipe out - // that cache is the endpoints change so that we don't allow unbounded memory growth. - _matcherCache = new DataSourceDependentCache>(dataSource, (_) => - { + // We cache RoutePatternMatcher instances per-Endpoint for performance, but we want to wipe out + // that cache is the endpoints change so that we don't allow unbounded memory growth. + _matcherCache = new DataSourceDependentCache>(dataSource, (_) => + { // We don't eagerly fill this cache because there's no real reason to. Unlike URL matching, we don't // need to build a big data structure up front to be correct. return new ConcurrentDictionary(); - }); + }); + + // Cached to avoid per-call allocation of a delegate on lookup. + _createMatcher = CreateRoutePatternMatcher; + } - // Cached to avoid per-call allocation of a delegate on lookup. - _createMatcher = CreateRoutePatternMatcher; + public override RouteValueDictionary? ParsePathByAddress(TAddress address, PathString path) + { + var endpoints = GetEndpoints(address); + if (endpoints.Count == 0) + { + return null; } - public override RouteValueDictionary? ParsePathByAddress(TAddress address, PathString path) + for (var i = 0; i < endpoints.Count; i++) { - var endpoints = GetEndpoints(address); - if (endpoints.Count == 0) + var endpoint = endpoints[i]; + if (TryParse(endpoint, path, out var values)) { - return null; + Log.PathParsingSucceeded(_logger, path, endpoint); + return values; } + } - for (var i = 0; i < endpoints.Count; i++) - { - var endpoint = endpoints[i]; - if (TryParse(endpoint, path, out var values)) - { - Log.PathParsingSucceeded(_logger, path, endpoint); - return values; - } - } + Log.PathParsingFailed(_logger, path, endpoints); + return null; + } - Log.PathParsingFailed(_logger, path, endpoints); - return null; - } + private List GetEndpoints(TAddress address) + { + var addressingScheme = _serviceProvider.GetRequiredService>(); + var endpoints = addressingScheme.FindEndpoints(address).OfType().ToList(); - private List GetEndpoints(TAddress address) + if (endpoints.Count == 0) + { + Log.EndpointsNotFound(_logger, address); + } + else { - var addressingScheme = _serviceProvider.GetRequiredService>(); - var endpoints = addressingScheme.FindEndpoints(address).OfType().ToList(); + Log.EndpointsFound(_logger, address, endpoints); + } - if (endpoints.Count == 0) - { - Log.EndpointsNotFound(_logger, address); - } - else - { - Log.EndpointsFound(_logger, address, endpoints); - } + return endpoints; + } - return endpoints; - } + private MatcherState CreateRoutePatternMatcher(RouteEndpoint endpoint) + { + var constraints = new Dictionary>(StringComparer.OrdinalIgnoreCase); - private MatcherState CreateRoutePatternMatcher(RouteEndpoint endpoint) + var policies = endpoint.RoutePattern.ParameterPolicies; + foreach (var kvp in policies) { - var constraints = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - var policies = endpoint.RoutePattern.ParameterPolicies; - foreach (var kvp in policies) + var constraintsForParameter = new List(); + var parameter = endpoint.RoutePattern.GetParameter(kvp.Key); + for (var i = 0; i < kvp.Value.Count; i++) { - var constraintsForParameter = new List(); - var parameter = endpoint.RoutePattern.GetParameter(kvp.Key); - for (var i = 0; i < kvp.Value.Count; i++) + var policy = _parameterPolicyFactory.Create(parameter, kvp.Value[i]); + if (policy is IRouteConstraint constraint) { - var policy = _parameterPolicyFactory.Create(parameter, kvp.Value[i]); - if (policy is IRouteConstraint constraint) - { - constraintsForParameter.Add(constraint); - } - } - - if (constraintsForParameter.Count > 0) - { - constraints.Add(kvp.Key, constraintsForParameter); + constraintsForParameter.Add(constraint); } } - var matcher = new RoutePatternMatcher(endpoint.RoutePattern, new RouteValueDictionary(endpoint.RoutePattern.Defaults)); - return new MatcherState(matcher, constraints); + if (constraintsForParameter.Count > 0) + { + constraints.Add(kvp.Key, constraintsForParameter); + } } - // Internal for testing - internal MatcherState GetMatcherState(RouteEndpoint endpoint) => _matcherCache.EnsureInitialized().GetOrAdd(endpoint, _createMatcher); + var matcher = new RoutePatternMatcher(endpoint.RoutePattern, new RouteValueDictionary(endpoint.RoutePattern.Defaults)); + return new MatcherState(matcher, constraints); + } - // Internal for testing - internal bool TryParse(RouteEndpoint endpoint, PathString path, [NotNullWhen(true)] out RouteValueDictionary? values) - { - var (matcher, constraints) = GetMatcherState(endpoint); + // Internal for testing + internal MatcherState GetMatcherState(RouteEndpoint endpoint) => _matcherCache.EnsureInitialized().GetOrAdd(endpoint, _createMatcher); - values = new RouteValueDictionary(); - if (!matcher.TryMatch(path, values)) - { - values = null; - return false; - } + // Internal for testing + internal bool TryParse(RouteEndpoint endpoint, PathString path, [NotNullWhen(true)] out RouteValueDictionary? values) + { + var (matcher, constraints) = GetMatcherState(endpoint); - foreach (var kvp in constraints) + values = new RouteValueDictionary(); + if (!matcher.TryMatch(path, values)) + { + values = null; + return false; + } + + foreach (var kvp in constraints) + { + for (var i = 0; i < kvp.Value.Count; i++) { - for (var i = 0; i < kvp.Value.Count; i++) + var constraint = kvp.Value[i]; + if (!constraint.Match(httpContext: null, NullRouter.Instance, kvp.Key, values, RouteDirection.IncomingRequest)) { - var constraint = kvp.Value[i]; - if (!constraint.Match(httpContext: null, NullRouter.Instance, kvp.Key, values, RouteDirection.IncomingRequest)) - { - values = null; - return false; - } + values = null; + return false; } } - - return true; } - public void Dispose() + return true; + } + + public void Dispose() + { + _matcherCache.Dispose(); + } + + // internal for testing + internal readonly struct MatcherState + { + public readonly RoutePatternMatcher Matcher; + public readonly Dictionary> Constraints; + + public MatcherState(RoutePatternMatcher matcher, Dictionary> constraints) { - _matcherCache.Dispose(); + Matcher = matcher; + Constraints = constraints; } - // internal for testing - internal readonly struct MatcherState + public void Deconstruct(out RoutePatternMatcher matcher, out Dictionary> constraints) { - public readonly RoutePatternMatcher Matcher; - public readonly Dictionary> Constraints; - - public MatcherState(RoutePatternMatcher matcher, Dictionary> constraints) - { - Matcher = matcher; - Constraints = constraints; - } - - public void Deconstruct(out RoutePatternMatcher matcher, out Dictionary> constraints) - { - matcher = Matcher; - constraints = Constraints; - } + matcher = Matcher; + constraints = Constraints; } + } - private static partial class Log + private static partial class Log + { + public static void EndpointsFound(ILogger logger, object? address, IEnumerable endpoints) { - public static void EndpointsFound(ILogger logger, object? address, IEnumerable endpoints) + // Checking level again to avoid allocation on the common path + if (logger.IsEnabled(LogLevel.Debug)) { - // Checking level again to avoid allocation on the common path - if (logger.IsEnabled(LogLevel.Debug)) - { - EndpointsFound(logger, endpoints.Select(e => e.DisplayName), address); - } + EndpointsFound(logger, endpoints.Select(e => e.DisplayName), address); } + } - [LoggerMessage(100, LogLevel.Debug, "Found the endpoints {Endpoints} for address {Address}", EventName = "EndpointsFound", SkipEnabledCheck = true)] - private static partial void EndpointsFound(ILogger logger, IEnumerable endpoints, object? address); + [LoggerMessage(100, LogLevel.Debug, "Found the endpoints {Endpoints} for address {Address}", EventName = "EndpointsFound", SkipEnabledCheck = true)] + private static partial void EndpointsFound(ILogger logger, IEnumerable endpoints, object? address); - [LoggerMessage(101, LogLevel.Debug, "No endpoints found for address {Address}", EventName = "EndpointsNotFound")] - public static partial void EndpointsNotFound(ILogger logger, object? address); + [LoggerMessage(101, LogLevel.Debug, "No endpoints found for address {Address}", EventName = "EndpointsNotFound")] + public static partial void EndpointsNotFound(ILogger logger, object? address); - public static void PathParsingSucceeded(ILogger logger, PathString path, Endpoint endpoint) + public static void PathParsingSucceeded(ILogger logger, PathString path, Endpoint endpoint) + { + // Checking level again to avoid allocation on the common path + if (logger.IsEnabled(LogLevel.Debug)) { - // Checking level again to avoid allocation on the common path - if (logger.IsEnabled(LogLevel.Debug)) - { - PathParsingSucceeded(logger, endpoint.DisplayName, path.Value); - } + PathParsingSucceeded(logger, endpoint.DisplayName, path.Value); } + } - [LoggerMessage(102, LogLevel.Debug, "Path parsing succeeded for endpoint {Endpoint} and URI path {URI}", EventName = "PathParsingSucceeded", SkipEnabledCheck = true)] - private static partial void PathParsingSucceeded(ILogger logger, string? endpoint, string? uri); + [LoggerMessage(102, LogLevel.Debug, "Path parsing succeeded for endpoint {Endpoint} and URI path {URI}", EventName = "PathParsingSucceeded", SkipEnabledCheck = true)] + private static partial void PathParsingSucceeded(ILogger logger, string? endpoint, string? uri); - public static void PathParsingFailed(ILogger logger, PathString path, IEnumerable endpoints) + public static void PathParsingFailed(ILogger logger, PathString path, IEnumerable endpoints) + { + // Checking level again to avoid allocation on the common path + if (logger.IsEnabled(LogLevel.Debug)) { - // Checking level again to avoid allocation on the common path - if (logger.IsEnabled(LogLevel.Debug)) - { - PathParsingFailed(logger, endpoints.Select(e => e.DisplayName), path.Value); - } + PathParsingFailed(logger, endpoints.Select(e => e.DisplayName), path.Value); } - - [LoggerMessage(103, LogLevel.Debug, "Path parsing failed for endpoints {Endpoints} and URI path {URI}", EventName = "PathParsingFailed", SkipEnabledCheck = true)] - private static partial void PathParsingFailed(ILogger logger, IEnumerable endpoints, string? uri); } + + [LoggerMessage(103, LogLevel.Debug, "Path parsing failed for endpoints {Endpoints} and URI path {URI}", EventName = "PathParsingFailed", SkipEnabledCheck = true)] + private static partial void PathParsingFailed(ILogger logger, IEnumerable endpoints, string? uri); } } diff --git a/src/Http/Routing/src/DefaultParameterPolicyFactory.cs b/src/Http/Routing/src/DefaultParameterPolicyFactory.cs index 6ad731970b..ce90169896 100644 --- a/src/Http/Routing/src/DefaultParameterPolicyFactory.cs +++ b/src/Http/Routing/src/DefaultParameterPolicyFactory.cs @@ -6,75 +6,74 @@ using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +internal class DefaultParameterPolicyFactory : ParameterPolicyFactory { - internal class DefaultParameterPolicyFactory : ParameterPolicyFactory + private readonly RouteOptions _options; + private readonly IServiceProvider _serviceProvider; + + public DefaultParameterPolicyFactory( + IOptions options, + IServiceProvider serviceProvider) { - private readonly RouteOptions _options; - private readonly IServiceProvider _serviceProvider; + _options = options.Value; + _serviceProvider = serviceProvider; + } - public DefaultParameterPolicyFactory( - IOptions options, - IServiceProvider serviceProvider) + public override IParameterPolicy Create(RoutePatternParameterPart? parameter, IParameterPolicy parameterPolicy) + { + if (parameterPolicy == null) { - _options = options.Value; - _serviceProvider = serviceProvider; + throw new ArgumentNullException(nameof(parameterPolicy)); } - public override IParameterPolicy Create(RoutePatternParameterPart? parameter, IParameterPolicy parameterPolicy) + if (parameterPolicy is IRouteConstraint routeConstraint) { - if (parameterPolicy == null) - { - throw new ArgumentNullException(nameof(parameterPolicy)); - } - - if (parameterPolicy is IRouteConstraint routeConstraint) - { - return InitializeRouteConstraint(parameter?.IsOptional ?? false, routeConstraint); - } - - return parameterPolicy; + return InitializeRouteConstraint(parameter?.IsOptional ?? false, routeConstraint); } - public override IParameterPolicy Create(RoutePatternParameterPart? parameter, string inlineText) - { - if (inlineText == null) - { - throw new ArgumentNullException(nameof(inlineText)); - } - - var parameterPolicy = ParameterPolicyActivator.ResolveParameterPolicy( - _options.ConstraintMap, - _serviceProvider, - inlineText, - out var parameterPolicyKey); + return parameterPolicy; + } - if (parameterPolicy == null) - { - throw new InvalidOperationException(Resources.FormatRoutePattern_ConstraintReferenceNotFound( - parameterPolicyKey, - typeof(RouteOptions), - nameof(RouteOptions.ConstraintMap))); - } + public override IParameterPolicy Create(RoutePatternParameterPart? parameter, string inlineText) + { + if (inlineText == null) + { + throw new ArgumentNullException(nameof(inlineText)); + } - if (parameterPolicy is IRouteConstraint constraint) - { - return InitializeRouteConstraint(parameter?.IsOptional ?? false, constraint); - } + var parameterPolicy = ParameterPolicyActivator.ResolveParameterPolicy( + _options.ConstraintMap, + _serviceProvider, + inlineText, + out var parameterPolicyKey); - return parameterPolicy; + if (parameterPolicy == null) + { + throw new InvalidOperationException(Resources.FormatRoutePattern_ConstraintReferenceNotFound( + parameterPolicyKey, + typeof(RouteOptions), + nameof(RouteOptions.ConstraintMap))); } - private static IParameterPolicy InitializeRouteConstraint( - bool optional, - IRouteConstraint routeConstraint) + if (parameterPolicy is IRouteConstraint constraint) { - if (optional) - { - routeConstraint = new OptionalRouteConstraint(routeConstraint); - } + return InitializeRouteConstraint(parameter?.IsOptional ?? false, constraint); + } - return routeConstraint; + return parameterPolicy; + } + + private static IParameterPolicy InitializeRouteConstraint( + bool optional, + IRouteConstraint routeConstraint) + { + if (optional) + { + routeConstraint = new OptionalRouteConstraint(routeConstraint); } + + return routeConstraint; } } diff --git a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs index f11f13628b..2c225672e5 100644 --- a/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs +++ b/src/Http/Routing/src/DependencyInjection/RoutingServiceCollectionExtensions.cs @@ -14,121 +14,120 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Contains extension methods to . +/// +public static class RoutingServiceCollectionExtensions { /// - /// Contains extension methods to . + /// Adds services required for routing requests. /// - public static class RoutingServiceCollectionExtensions + /// The to add the services to. + /// The so that additional calls can be chained. + public static IServiceCollection AddRouting(this IServiceCollection services) { - /// - /// Adds services required for routing requests. - /// - /// The to add the services to. - /// The so that additional calls can be chained. - public static IServiceCollection AddRouting(this IServiceCollection services) + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.TryAddTransient(); + services.TryAddTransient(); + services.TryAddSingleton>(s => + { + var provider = s.GetRequiredService(); + return provider.Create(new UriBuilderContextPooledObjectPolicy()); + }); + + // The TreeRouteBuilder is a builder for creating routes, it should stay transient because it's + // stateful. + services.TryAdd(ServiceDescriptor.Transient(s => + { + var loggerFactory = s.GetRequiredService(); + var objectPool = s.GetRequiredService>(); + var constraintResolver = s.GetRequiredService(); + return new TreeRouteBuilder(loggerFactory, objectPool, constraintResolver); + })); + + services.TryAddSingleton(typeof(RoutingMarkerService)); + + // Setup global collection of endpoint data sources + var dataSources = new ObservableCollection(); + services.TryAddEnumerable(ServiceDescriptor.Transient, ConfigureRouteOptions>( + serviceProvider => new ConfigureRouteOptions(dataSources))); + + // Allow global access to the list of endpoints. + services.TryAddSingleton(s => { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - services.TryAddTransient(); - services.TryAddTransient(); - services.TryAddSingleton>(s => - { - var provider = s.GetRequiredService(); - return provider.Create(new UriBuilderContextPooledObjectPolicy()); - }); - - // The TreeRouteBuilder is a builder for creating routes, it should stay transient because it's - // stateful. - services.TryAdd(ServiceDescriptor.Transient(s => - { - var loggerFactory = s.GetRequiredService(); - var objectPool = s.GetRequiredService>(); - var constraintResolver = s.GetRequiredService(); - return new TreeRouteBuilder(loggerFactory, objectPool, constraintResolver); - })); - - services.TryAddSingleton(typeof(RoutingMarkerService)); - - // Setup global collection of endpoint data sources - var dataSources = new ObservableCollection(); - services.TryAddEnumerable(ServiceDescriptor.Transient, ConfigureRouteOptions>( - serviceProvider => new ConfigureRouteOptions(dataSources))); - - // Allow global access to the list of endpoints. - services.TryAddSingleton(s => - { // Call internal ctor and pass global collection return new CompositeEndpointDataSource(dataSources); - }); - - // - // Default matcher implementation - // - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddTransient(); - services.TryAddSingleton(); - services.TryAddTransient(); - services.TryAddSingleton(services => - { + }); + + // + // Default matcher implementation + // + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddTransient(); + services.TryAddSingleton(); + services.TryAddTransient(); + services.TryAddSingleton(services => + { // This has no public constructor. return new EndpointMetadataComparer(services); - }); - - // Link generation related services - services.TryAddSingleton(); - services.TryAddSingleton, EndpointNameAddressScheme>(); - services.TryAddSingleton, RouteValuesAddressScheme>(); - services.TryAddSingleton(); - - // - // Endpoint Selection - // - services.TryAddSingleton(); - services.TryAddEnumerable(ServiceDescriptor.Singleton()); - services.TryAddEnumerable(ServiceDescriptor.Singleton()); - services.TryAddEnumerable(ServiceDescriptor.Singleton()); - - // - // Misc infrastructure - // - services.TryAddSingleton(); - services.TryAddSingleton(); - - // Set RouteHandlerOptions.ThrowOnBadRequest in development - services.TryAddEnumerable(ServiceDescriptor.Transient, ConfigureRouteHandlerOptions>()); - - return services; - } + }); + + // Link generation related services + services.TryAddSingleton(); + services.TryAddSingleton, EndpointNameAddressScheme>(); + services.TryAddSingleton, RouteValuesAddressScheme>(); + services.TryAddSingleton(); + + // + // Endpoint Selection + // + services.TryAddSingleton(); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + + // + // Misc infrastructure + // + services.TryAddSingleton(); + services.TryAddSingleton(); + + // Set RouteHandlerOptions.ThrowOnBadRequest in development + services.TryAddEnumerable(ServiceDescriptor.Transient, ConfigureRouteHandlerOptions>()); + + return services; + } - /// - /// Adds services required for routing requests. - /// - /// The to add the services to. - /// The routing options to configure the middleware with. - /// The so that additional calls can be chained. - public static IServiceCollection AddRouting( - this IServiceCollection services, - Action configureOptions) + /// + /// Adds services required for routing requests. + /// + /// The to add the services to. + /// The routing options to configure the middleware with. + /// The so that additional calls can be chained. + public static IServiceCollection AddRouting( + this IServiceCollection services, + Action configureOptions) + { + if (services == null) { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } + throw new ArgumentNullException(nameof(services)); + } - if (configureOptions == null) - { - throw new ArgumentNullException(nameof(configureOptions)); - } + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } - services.Configure(configureOptions); - services.AddRouting(); + services.Configure(configureOptions); + services.AddRouting(); - return services; - } + return services; } } diff --git a/src/Http/Routing/src/EndpointDataSource.cs b/src/Http/Routing/src/EndpointDataSource.cs index 9f714fd6ea..4e83b57314 100644 --- a/src/Http/Routing/src/EndpointDataSource.cs +++ b/src/Http/Routing/src/EndpointDataSource.cs @@ -5,23 +5,22 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Provides a collection of instances. +/// +public abstract class EndpointDataSource { /// - /// Provides a collection of instances. + /// Gets a used to signal invalidation of cached + /// instances. /// - public abstract class EndpointDataSource - { - /// - /// Gets a used to signal invalidation of cached - /// instances. - /// - /// The . - public abstract IChangeToken GetChangeToken(); + /// The . + public abstract IChangeToken GetChangeToken(); - /// - /// Returns a read-only collection of instances. - /// - public abstract IReadOnlyList Endpoints { get; } - } + /// + /// Returns a read-only collection of instances. + /// + public abstract IReadOnlyList Endpoints { get; } } diff --git a/src/Http/Routing/src/EndpointGroupNameAttribute.cs b/src/Http/Routing/src/EndpointGroupNameAttribute.cs index 68511b6ca9..3709fd7839 100644 --- a/src/Http/Routing/src/EndpointGroupNameAttribute.cs +++ b/src/Http/Routing/src/EndpointGroupNameAttribute.cs @@ -4,29 +4,28 @@ using System; using Microsoft.AspNetCore.Http; - namespace Microsoft.AspNetCore.Routing - { +namespace Microsoft.AspNetCore.Routing; + +/// +/// Specifies the endpoint group name in . +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate | AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public sealed class EndpointGroupNameAttribute : Attribute, IEndpointGroupNameMetadata +{ /// - /// Specifies the endpoint group name in . + /// Initializes an instance of the . /// - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate | AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - public sealed class EndpointGroupNameAttribute : Attribute, IEndpointGroupNameMetadata + /// The endpoint group name. + public EndpointGroupNameAttribute(string endpointGroupName) { - /// - /// Initializes an instance of the . - /// - /// The endpoint group name. - public EndpointGroupNameAttribute(string endpointGroupName) + if (endpointGroupName == null) { - if (endpointGroupName == null) - { - throw new ArgumentNullException(nameof(endpointGroupName)); - } - - EndpointGroupName = endpointGroupName; + throw new ArgumentNullException(nameof(endpointGroupName)); } - /// - public string EndpointGroupName { get; } + EndpointGroupName = endpointGroupName; } - } \ No newline at end of file + + /// + public string EndpointGroupName { get; } +} diff --git a/src/Http/Routing/src/EndpointMiddleware.cs b/src/Http/Routing/src/EndpointMiddleware.cs index 41919ff00c..d04af317c0 100644 --- a/src/Http/Routing/src/EndpointMiddleware.cs +++ b/src/Http/Routing/src/EndpointMiddleware.cs @@ -7,105 +7,104 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +internal sealed partial class EndpointMiddleware { - internal sealed partial class EndpointMiddleware - { - internal const string AuthorizationMiddlewareInvokedKey = "__AuthorizationMiddlewareWithEndpointInvoked"; - internal const string CorsMiddlewareInvokedKey = "__CorsMiddlewareWithEndpointInvoked"; + internal const string AuthorizationMiddlewareInvokedKey = "__AuthorizationMiddlewareWithEndpointInvoked"; + internal const string CorsMiddlewareInvokedKey = "__CorsMiddlewareWithEndpointInvoked"; - private readonly ILogger _logger; - private readonly RequestDelegate _next; - private readonly RouteOptions _routeOptions; + private readonly ILogger _logger; + private readonly RequestDelegate _next; + private readonly RouteOptions _routeOptions; - public EndpointMiddleware( - ILogger logger, - RequestDelegate next, - IOptions routeOptions) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _next = next ?? throw new ArgumentNullException(nameof(next)); - _routeOptions = routeOptions?.Value ?? throw new ArgumentNullException(nameof(routeOptions)); - } + public EndpointMiddleware( + ILogger logger, + RequestDelegate next, + IOptions routeOptions) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _next = next ?? throw new ArgumentNullException(nameof(next)); + _routeOptions = routeOptions?.Value ?? throw new ArgumentNullException(nameof(routeOptions)); + } - public Task Invoke(HttpContext httpContext) + public Task Invoke(HttpContext httpContext) + { + var endpoint = httpContext.GetEndpoint(); + if (endpoint?.RequestDelegate != null) { - var endpoint = httpContext.GetEndpoint(); - if (endpoint?.RequestDelegate != null) + if (!_routeOptions.SuppressCheckForUnhandledSecurityMetadata) { - if (!_routeOptions.SuppressCheckForUnhandledSecurityMetadata) + if (endpoint.Metadata.GetMetadata() != null && + !httpContext.Items.ContainsKey(AuthorizationMiddlewareInvokedKey)) { - if (endpoint.Metadata.GetMetadata() != null && - !httpContext.Items.ContainsKey(AuthorizationMiddlewareInvokedKey)) - { - ThrowMissingAuthMiddlewareException(endpoint); - } - - if (endpoint.Metadata.GetMetadata() != null && - !httpContext.Items.ContainsKey(CorsMiddlewareInvokedKey)) - { - ThrowMissingCorsMiddlewareException(endpoint); - } + ThrowMissingAuthMiddlewareException(endpoint); } - Log.ExecutingEndpoint(_logger, endpoint); - - try + if (endpoint.Metadata.GetMetadata() != null && + !httpContext.Items.ContainsKey(CorsMiddlewareInvokedKey)) { - var requestTask = endpoint.RequestDelegate(httpContext); - if (!requestTask.IsCompletedSuccessfully) - { - return AwaitRequestTask(endpoint, requestTask, _logger); - } + ThrowMissingCorsMiddlewareException(endpoint); } - catch (Exception exception) - { - Log.ExecutedEndpoint(_logger, endpoint); - return Task.FromException(exception); - } - - Log.ExecutedEndpoint(_logger, endpoint); - return Task.CompletedTask; } - return _next(httpContext); + Log.ExecutingEndpoint(_logger, endpoint); - static async Task AwaitRequestTask(Endpoint endpoint, Task requestTask, ILogger logger) + try { - try - { - await requestTask; - } - finally + var requestTask = endpoint.RequestDelegate(httpContext); + if (!requestTask.IsCompletedSuccessfully) { - Log.ExecutedEndpoint(logger, endpoint); + return AwaitRequestTask(endpoint, requestTask, _logger); } } - } + catch (Exception exception) + { + Log.ExecutedEndpoint(_logger, endpoint); + return Task.FromException(exception); + } - private static void ThrowMissingAuthMiddlewareException(Endpoint endpoint) - { - throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains authorization metadata, " + - "but a middleware was not found that supports authorization." + - Environment.NewLine + - "Configure your application startup by adding app.UseAuthorization() in the application startup code. If there are calls to app.UseRouting() and app.UseEndpoints(...), the call to app.UseAuthorization() must go between them."); + Log.ExecutedEndpoint(_logger, endpoint); + return Task.CompletedTask; } - private static void ThrowMissingCorsMiddlewareException(Endpoint endpoint) + return _next(httpContext); + + static async Task AwaitRequestTask(Endpoint endpoint, Task requestTask, ILogger logger) { - throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains CORS metadata, " + - "but a middleware was not found that supports CORS." + - Environment.NewLine + - "Configure your application startup by adding app.UseCors() in the application startup code. If there are calls to app.UseRouting() and app.UseEndpoints(...), the call to app.UseCors() must go between them."); + try + { + await requestTask; + } + finally + { + Log.ExecutedEndpoint(logger, endpoint); + } } + } - private static partial class Log - { - [LoggerMessage(0, LogLevel.Information, "Executing endpoint '{EndpointName}'", EventName = "ExecutingEndpoint")] - public static partial void ExecutingEndpoint(ILogger logger, Endpoint endpointName); + private static void ThrowMissingAuthMiddlewareException(Endpoint endpoint) + { + throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains authorization metadata, " + + "but a middleware was not found that supports authorization." + + Environment.NewLine + + "Configure your application startup by adding app.UseAuthorization() in the application startup code. If there are calls to app.UseRouting() and app.UseEndpoints(...), the call to app.UseAuthorization() must go between them."); + } - [LoggerMessage(1, LogLevel.Information, "Executed endpoint '{EndpointName}'", EventName = "ExecutedEndpoint")] - public static partial void ExecutedEndpoint(ILogger logger, Endpoint endpointName); - } + private static void ThrowMissingCorsMiddlewareException(Endpoint endpoint) + { + throw new InvalidOperationException($"Endpoint {endpoint.DisplayName} contains CORS metadata, " + + "but a middleware was not found that supports CORS." + + Environment.NewLine + + "Configure your application startup by adding app.UseCors() in the application startup code. If there are calls to app.UseRouting() and app.UseEndpoints(...), the call to app.UseCors() must go between them."); + } + + private static partial class Log + { + [LoggerMessage(0, LogLevel.Information, "Executing endpoint '{EndpointName}'", EventName = "ExecutingEndpoint")] + public static partial void ExecutingEndpoint(ILogger logger, Endpoint endpointName); + + [LoggerMessage(1, LogLevel.Information, "Executed endpoint '{EndpointName}'", EventName = "ExecutedEndpoint")] + public static partial void ExecutedEndpoint(ILogger logger, Endpoint endpointName); } } diff --git a/src/Http/Routing/src/EndpointNameAddressScheme.cs b/src/Http/Routing/src/EndpointNameAddressScheme.cs index ef057acac1..c1fe9cdade 100644 --- a/src/Http/Routing/src/EndpointNameAddressScheme.cs +++ b/src/Http/Routing/src/EndpointNameAddressScheme.cs @@ -7,105 +7,104 @@ using System.Linq; using System.Text; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +internal sealed class EndpointNameAddressScheme : IEndpointAddressScheme, IDisposable { - internal sealed class EndpointNameAddressScheme : IEndpointAddressScheme, IDisposable + private readonly DataSourceDependentCache> _cache; + + public EndpointNameAddressScheme(EndpointDataSource dataSource) { - private readonly DataSourceDependentCache> _cache; + _cache = new DataSourceDependentCache>(dataSource, Initialize); + } + + // Internal for tests + internal Dictionary Entries => _cache.EnsureInitialized(); - public EndpointNameAddressScheme(EndpointDataSource dataSource) + public IEnumerable FindEndpoints(string address) + { + if (address == null) { - _cache = new DataSourceDependentCache>(dataSource, Initialize); + throw new ArgumentNullException(nameof(address)); } - // Internal for tests - internal Dictionary Entries => _cache.EnsureInitialized(); + // Capture the current value of the cache + var entries = Entries; - public IEnumerable FindEndpoints(string address) - { - if (address == null) - { - throw new ArgumentNullException(nameof(address)); - } - - // Capture the current value of the cache - var entries = Entries; + entries.TryGetValue(address, out var result); + return result ?? Array.Empty(); + } - entries.TryGetValue(address, out var result); - return result ?? Array.Empty(); - } + private static Dictionary Initialize(IReadOnlyList endpoints) + { + // Collect duplicates as we go, blow up on startup if we find any. + var hasDuplicates = false; - private static Dictionary Initialize(IReadOnlyList endpoints) + var entries = new Dictionary(StringComparer.Ordinal); + for (var i = 0; i < endpoints.Count; i++) { - // Collect duplicates as we go, blow up on startup if we find any. - var hasDuplicates = false; + var endpoint = endpoints[i]; - var entries = new Dictionary(StringComparer.Ordinal); - for (var i = 0; i < endpoints.Count; i++) + var endpointName = GetEndpointName(endpoint); + if (endpointName == null) { - var endpoint = endpoints[i]; - - var endpointName = GetEndpointName(endpoint); - if (endpointName == null) - { - continue; - } - - if (!entries.TryGetValue(endpointName, out var existing)) - { - // This isn't a duplicate (so far) - entries[endpointName] = new[] { endpoint }; - continue; - } - - // Ok this is a duplicate, because we have two endpoints with the same name. Bail out, because we - // are just going to throw, we don't need to finish collecting data. - hasDuplicates = true; - break; + continue; } - if (!hasDuplicates) + if (!entries.TryGetValue(endpointName, out var existing)) { - // No duplicates, success! - return entries; + // This isn't a duplicate (so far) + entries[endpointName] = new[] { endpoint }; + continue; } - // OK we need to report some duplicates. - var duplicates = endpoints - .GroupBy(e => GetEndpointName(e)) - .Where(g => g.Key != null && g.Count() > 1); + // Ok this is a duplicate, because we have two endpoints with the same name. Bail out, because we + // are just going to throw, we don't need to finish collecting data. + hasDuplicates = true; + break; + } - var builder = new StringBuilder(); - builder.AppendLine(Resources.DuplicateEndpointNameHeader); + if (!hasDuplicates) + { + // No duplicates, success! + return entries; + } - foreach (var group in duplicates) - { - builder.AppendLine(); - builder.AppendLine(Resources.FormatDuplicateEndpointNameEntry(group.Key)); + // OK we need to report some duplicates. + var duplicates = endpoints + .GroupBy(e => GetEndpointName(e)) + .Where(g => g.Key != null && g.Count() > 1); - foreach (var endpoint in group) - { - builder.AppendLine(endpoint.DisplayName); - } - } + var builder = new StringBuilder(); + builder.AppendLine(Resources.DuplicateEndpointNameHeader); - throw new InvalidOperationException(builder.ToString()); + foreach (var group in duplicates) + { + builder.AppendLine(); + builder.AppendLine(Resources.FormatDuplicateEndpointNameEntry(group.Key)); - string? GetEndpointName(Endpoint endpoint) + foreach (var endpoint in group) { - if (endpoint.Metadata.GetMetadata()?.SuppressLinkGeneration == true) - { - // Skip anything that's suppressed for linking. - return null; - } - - return endpoint.Metadata.GetMetadata()?.EndpointName; + builder.AppendLine(endpoint.DisplayName); } } - public void Dispose() + throw new InvalidOperationException(builder.ToString()); + + string? GetEndpointName(Endpoint endpoint) { - _cache.Dispose(); + if (endpoint.Metadata.GetMetadata()?.SuppressLinkGeneration == true) + { + // Skip anything that's suppressed for linking. + return null; + } + + return endpoint.Metadata.GetMetadata()?.EndpointName; } } + + public void Dispose() + { + _cache.Dispose(); + } } diff --git a/src/Http/Routing/src/EndpointNameAttribute.cs b/src/Http/Routing/src/EndpointNameAttribute.cs index 9692dc8321..fe1ac9a7cc 100644 --- a/src/Http/Routing/src/EndpointNameAttribute.cs +++ b/src/Http/Routing/src/EndpointNameAttribute.cs @@ -3,34 +3,33 @@ using System; using Microsoft.AspNetCore.Http; - - namespace Microsoft.AspNetCore.Routing - { + +namespace Microsoft.AspNetCore.Routing; + +/// +/// Specifies the endpoint name in . +/// +/// +/// Endpoint names must be unique within an application, and can be used to unambiguously +/// identify a desired endpoint for URI generation using +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate, Inherited = false, AllowMultiple = false)] +public sealed class EndpointNameAttribute : Attribute, IEndpointNameMetadata +{ /// - /// Specifies the endpoint name in . + /// Initializes an instance of the EndpointNameAttribute. /// - /// - /// Endpoint names must be unique within an application, and can be used to unambiguously - /// identify a desired endpoint for URI generation using - /// - [AttributeUsage(AttributeTargets.Method | AttributeTargets.Delegate, Inherited = false, AllowMultiple = false)] - public sealed class EndpointNameAttribute : Attribute, IEndpointNameMetadata + /// The endpoint name. + public EndpointNameAttribute(string endpointName) { - /// - /// Initializes an instance of the EndpointNameAttribute. - /// - /// The endpoint name. - public EndpointNameAttribute(string endpointName) + if (endpointName == null) { - if (endpointName == null) - { - throw new ArgumentNullException(nameof(endpointName)); - } - - EndpointName = endpointName; + throw new ArgumentNullException(nameof(endpointName)); } - /// - public string EndpointName { get; } + EndpointName = endpointName; } - } \ No newline at end of file + + /// + public string EndpointName { get; } +} diff --git a/src/Http/Routing/src/EndpointNameMetadata.cs b/src/Http/Routing/src/EndpointNameMetadata.cs index 7584675ad2..a33949e060 100644 --- a/src/Http/Routing/src/EndpointNameMetadata.cs +++ b/src/Http/Routing/src/EndpointNameMetadata.cs @@ -4,34 +4,33 @@ using System; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Specifies an endpoint name in . +/// +/// +/// Endpoint names must be unique within an application, and can be used to unambiguously +/// identify a desired endpoint for URI generation using . +/// +public class EndpointNameMetadata : IEndpointNameMetadata { /// - /// Specifies an endpoint name in . + /// Creates a new instance of with the provided endpoint name. /// - /// - /// Endpoint names must be unique within an application, and can be used to unambiguously - /// identify a desired endpoint for URI generation using . - /// - public class EndpointNameMetadata : IEndpointNameMetadata + /// The endpoint name. + public EndpointNameMetadata(string endpointName) { - /// - /// Creates a new instance of with the provided endpoint name. - /// - /// The endpoint name. - public EndpointNameMetadata(string endpointName) + if (endpointName == null) { - if (endpointName == null) - { - throw new ArgumentNullException(nameof(endpointName)); - } - - EndpointName = endpointName; + throw new ArgumentNullException(nameof(endpointName)); } - /// - /// Gets the endpoint name. - /// - public string EndpointName { get; } + EndpointName = endpointName; } + + /// + /// Gets the endpoint name. + /// + public string EndpointName { get; } } diff --git a/src/Http/Routing/src/EndpointRoutingMiddleware.cs b/src/Http/Routing/src/EndpointRoutingMiddleware.cs index 5bf49b7f54..772556e04f 100644 --- a/src/Http/Routing/src/EndpointRoutingMiddleware.cs +++ b/src/Http/Routing/src/EndpointRoutingMiddleware.cs @@ -10,172 +10,171 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +internal sealed partial class EndpointRoutingMiddleware { - internal sealed partial class EndpointRoutingMiddleware + private const string DiagnosticsEndpointMatchedKey = "Microsoft.AspNetCore.Routing.EndpointMatched"; + + private readonly MatcherFactory _matcherFactory; + private readonly ILogger _logger; + private readonly EndpointDataSource _endpointDataSource; + private readonly DiagnosticListener _diagnosticListener; + private readonly RequestDelegate _next; + + private Task? _initializationTask; + + public EndpointRoutingMiddleware( + MatcherFactory matcherFactory, + ILogger logger, + IEndpointRouteBuilder endpointRouteBuilder, + DiagnosticListener diagnosticListener, + RequestDelegate next) { - private const string DiagnosticsEndpointMatchedKey = "Microsoft.AspNetCore.Routing.EndpointMatched"; - - private readonly MatcherFactory _matcherFactory; - private readonly ILogger _logger; - private readonly EndpointDataSource _endpointDataSource; - private readonly DiagnosticListener _diagnosticListener; - private readonly RequestDelegate _next; - - private Task? _initializationTask; - - public EndpointRoutingMiddleware( - MatcherFactory matcherFactory, - ILogger logger, - IEndpointRouteBuilder endpointRouteBuilder, - DiagnosticListener diagnosticListener, - RequestDelegate next) + if (endpointRouteBuilder == null) { - if (endpointRouteBuilder == null) - { - throw new ArgumentNullException(nameof(endpointRouteBuilder)); - } + throw new ArgumentNullException(nameof(endpointRouteBuilder)); + } - _matcherFactory = matcherFactory ?? throw new ArgumentNullException(nameof(matcherFactory)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _diagnosticListener = diagnosticListener ?? throw new ArgumentNullException(nameof(diagnosticListener)); - _next = next ?? throw new ArgumentNullException(nameof(next)); + _matcherFactory = matcherFactory ?? throw new ArgumentNullException(nameof(matcherFactory)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _diagnosticListener = diagnosticListener ?? throw new ArgumentNullException(nameof(diagnosticListener)); + _next = next ?? throw new ArgumentNullException(nameof(next)); - _endpointDataSource = new CompositeEndpointDataSource(endpointRouteBuilder.DataSources); + _endpointDataSource = new CompositeEndpointDataSource(endpointRouteBuilder.DataSources); + } + + public Task Invoke(HttpContext httpContext) + { + // There's already an endpoint, skip matching completely + var endpoint = httpContext.GetEndpoint(); + if (endpoint != null) + { + Log.MatchSkipped(_logger, endpoint); + return _next(httpContext); } - public Task Invoke(HttpContext httpContext) + // There's an inherent race condition between waiting for init and accessing the matcher + // this is OK because once `_matcher` is initialized, it will not be set to null again. + var matcherTask = InitializeAsync(); + if (!matcherTask.IsCompletedSuccessfully) { - // There's already an endpoint, skip matching completely - var endpoint = httpContext.GetEndpoint(); - if (endpoint != null) - { - Log.MatchSkipped(_logger, endpoint); - return _next(httpContext); - } + return AwaitMatcher(this, httpContext, matcherTask); + } - // There's an inherent race condition between waiting for init and accessing the matcher - // this is OK because once `_matcher` is initialized, it will not be set to null again. - var matcherTask = InitializeAsync(); - if (!matcherTask.IsCompletedSuccessfully) - { - return AwaitMatcher(this, httpContext, matcherTask); - } + var matchTask = matcherTask.Result.MatchAsync(httpContext); + if (!matchTask.IsCompletedSuccessfully) + { + return AwaitMatch(this, httpContext, matchTask); + } - var matchTask = matcherTask.Result.MatchAsync(httpContext); - if (!matchTask.IsCompletedSuccessfully) - { - return AwaitMatch(this, httpContext, matchTask); - } + return SetRoutingAndContinue(httpContext); - return SetRoutingAndContinue(httpContext); + // Awaited fallbacks for when the Tasks do not synchronously complete + static async Task AwaitMatcher(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task matcherTask) + { + var matcher = await matcherTask; + await matcher.MatchAsync(httpContext); + await middleware.SetRoutingAndContinue(httpContext); + } - // Awaited fallbacks for when the Tasks do not synchronously complete - static async Task AwaitMatcher(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task matcherTask) - { - var matcher = await matcherTask; - await matcher.MatchAsync(httpContext); - await middleware.SetRoutingAndContinue(httpContext); - } + static async Task AwaitMatch(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task matchTask) + { + await matchTask; + await middleware.SetRoutingAndContinue(httpContext); + } - static async Task AwaitMatch(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task matchTask) - { - await matchTask; - await middleware.SetRoutingAndContinue(httpContext); - } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private Task SetRoutingAndContinue(HttpContext httpContext) + { + // If there was no mutation of the endpoint then log failure + var endpoint = httpContext.GetEndpoint(); + if (endpoint == null) + { + Log.MatchFailure(_logger); } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Task SetRoutingAndContinue(HttpContext httpContext) + else { - // If there was no mutation of the endpoint then log failure - var endpoint = httpContext.GetEndpoint(); - if (endpoint == null) - { - Log.MatchFailure(_logger); - } - else + // Raise an event if the route matched + if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled(DiagnosticsEndpointMatchedKey)) { - // Raise an event if the route matched - if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled(DiagnosticsEndpointMatchedKey)) - { - // We're just going to send the HttpContext since it has all of the relevant information - _diagnosticListener.Write(DiagnosticsEndpointMatchedKey, httpContext); - } - - Log.MatchSuccess(_logger, endpoint); + // We're just going to send the HttpContext since it has all of the relevant information + _diagnosticListener.Write(DiagnosticsEndpointMatchedKey, httpContext); } - return _next(httpContext); + Log.MatchSuccess(_logger, endpoint); } - // Initialization is async to avoid blocking threads while reflection and things - // of that nature take place. - // - // We've seen cases where startup is very slow if we allow multiple threads to race - // while initializing the set of endpoints/routes. Doing CPU intensive work is a - // blocking operation if you have a low core count and enough work to do. - private Task InitializeAsync() - { - var initializationTask = _initializationTask; - if (initializationTask != null) - { - return initializationTask; - } + return _next(httpContext); + } - return InitializeCoreAsync(); + // Initialization is async to avoid blocking threads while reflection and things + // of that nature take place. + // + // We've seen cases where startup is very slow if we allow multiple threads to race + // while initializing the set of endpoints/routes. Doing CPU intensive work is a + // blocking operation if you have a low core count and enough work to do. + private Task InitializeAsync() + { + var initializationTask = _initializationTask; + if (initializationTask != null) + { + return initializationTask; } - private Task InitializeCoreAsync() + return InitializeCoreAsync(); + } + + private Task InitializeCoreAsync() + { + var initialization = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var initializationTask = Interlocked.CompareExchange(ref _initializationTask, initialization.Task, null); + if (initializationTask != null) { - var initialization = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var initializationTask = Interlocked.CompareExchange(ref _initializationTask, initialization.Task, null); - if (initializationTask != null) - { - // This thread lost the race, join the existing task. - return initializationTask; - } + // This thread lost the race, join the existing task. + return initializationTask; + } - // This thread won the race, do the initialization. - try - { - var matcher = _matcherFactory.CreateMatcher(_endpointDataSource); + // This thread won the race, do the initialization. + try + { + var matcher = _matcherFactory.CreateMatcher(_endpointDataSource); - _initializationTask = Task.FromResult(matcher); + _initializationTask = Task.FromResult(matcher); - // Complete the task, this will unblock any requests that came in while initializing. - initialization.SetResult(matcher); - return initialization.Task; - } - catch (Exception ex) - { - // Allow initialization to occur again. Since DataSources can change, it's possible - // for the developer to correct the data causing the failure. - _initializationTask = null; + // Complete the task, this will unblock any requests that came in while initializing. + initialization.SetResult(matcher); + return initialization.Task; + } + catch (Exception ex) + { + // Allow initialization to occur again. Since DataSources can change, it's possible + // for the developer to correct the data causing the failure. + _initializationTask = null; - // Complete the task, this will throw for any requests that came in while initializing. - initialization.SetException(ex); - return initialization.Task; - } + // Complete the task, this will throw for any requests that came in while initializing. + initialization.SetException(ex); + return initialization.Task; } + } - private static partial class Log - { - public static void MatchSuccess(ILogger logger, Endpoint endpoint) - => MatchSuccess(logger, endpoint.DisplayName); + private static partial class Log + { + public static void MatchSuccess(ILogger logger, Endpoint endpoint) + => MatchSuccess(logger, endpoint.DisplayName); - [LoggerMessage(1, LogLevel.Debug, "Request matched endpoint '{EndpointName}'", EventName = "MatchSuccess")] - private static partial void MatchSuccess(ILogger logger, string? endpointName); + [LoggerMessage(1, LogLevel.Debug, "Request matched endpoint '{EndpointName}'", EventName = "MatchSuccess")] + private static partial void MatchSuccess(ILogger logger, string? endpointName); - [LoggerMessage(2, LogLevel.Debug, "Request did not match any endpoints", EventName = "MatchFailure")] - public static partial void MatchFailure(ILogger logger); + [LoggerMessage(2, LogLevel.Debug, "Request did not match any endpoints", EventName = "MatchFailure")] + public static partial void MatchFailure(ILogger logger); - public static void MatchSkipped(ILogger logger, Endpoint endpoint) - => MatchingSkipped(logger, endpoint.DisplayName); + public static void MatchSkipped(ILogger logger, Endpoint endpoint) + => MatchingSkipped(logger, endpoint.DisplayName); - [LoggerMessage(3, LogLevel.Debug, "Endpoint '{EndpointName}' already set, skipping route matching.", EventName = "MatchingSkipped")] - private static partial void MatchingSkipped(ILogger logger, string? endpointName); - } + [LoggerMessage(3, LogLevel.Debug, "Endpoint '{EndpointName}' already set, skipping route matching.", EventName = "MatchingSkipped")] + private static partial void MatchingSkipped(ILogger logger, string? endpointName); } } diff --git a/src/Http/Routing/src/ExcludeFromDescriptionAttribute.cs b/src/Http/Routing/src/ExcludeFromDescriptionAttribute.cs index 6aeb8426e5..0a4c87c441 100644 --- a/src/Http/Routing/src/ExcludeFromDescriptionAttribute.cs +++ b/src/Http/Routing/src/ExcludeFromDescriptionAttribute.cs @@ -4,15 +4,14 @@ using System; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Indicates that this should not be included in the generated API metadata. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate, AllowMultiple = false, Inherited = true)] +public sealed class ExcludeFromDescriptionAttribute : Attribute, IExcludeFromDescriptionMetadata { - /// - /// Indicates that this should not be included in the generated API metadata. - /// - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate, AllowMultiple = false, Inherited = true)] - public sealed class ExcludeFromDescriptionAttribute : Attribute, IExcludeFromDescriptionMetadata - { - /// - public bool ExcludeFromDescription => true; - } + /// + public bool ExcludeFromDescription => true; } diff --git a/src/Http/Routing/src/HostAttribute.cs b/src/Http/Routing/src/HostAttribute.cs index be4403eaa0..c9b2c8569c 100644 --- a/src/Http/Routing/src/HostAttribute.cs +++ b/src/Http/Routing/src/HostAttribute.cs @@ -6,62 +6,61 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Attribute for providing host metdata that is used during routing. +/// +[DebuggerDisplay("{DebuggerToString(),nq}")] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] +public sealed class HostAttribute : Attribute, IHostMetadata { /// - /// Attribute for providing host metdata that is used during routing. + /// Initializes a new instance of the class. /// - [DebuggerDisplay("{DebuggerToString(),nq}")] - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - public sealed class HostAttribute : Attribute, IHostMetadata + /// + /// The host used during routing. + /// Host should be Unicode rather than punycode, and may have a port. + /// + public HostAttribute(string host) : this(new[] { host }) { - /// - /// Initializes a new instance of the class. - /// - /// - /// The host used during routing. - /// Host should be Unicode rather than punycode, and may have a port. - /// - public HostAttribute(string host) : this(new[] { host }) + if (host == null) { - if (host == null) - { - throw new ArgumentNullException(nameof(host)); - } + throw new ArgumentNullException(nameof(host)); } + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The hosts used during routing. - /// Hosts should be Unicode rather than punycode, and may have a port. - /// An empty collection means any host will be accepted. - /// - public HostAttribute(params string[] hosts) + /// + /// Initializes a new instance of the class. + /// + /// + /// The hosts used during routing. + /// Hosts should be Unicode rather than punycode, and may have a port. + /// An empty collection means any host will be accepted. + /// + public HostAttribute(params string[] hosts) + { + if (hosts == null) { - if (hosts == null) - { - throw new ArgumentNullException(nameof(hosts)); - } - - Hosts = hosts.ToArray(); + throw new ArgumentNullException(nameof(hosts)); } - /// - /// Returns a read-only collection of hosts used during routing. - /// Hosts will be Unicode rather than punycode, and may have a port. - /// An empty collection means any host will be accepted. - /// - public IReadOnlyList Hosts { get; } + Hosts = hosts.ToArray(); + } - private string DebuggerToString() - { - var hostsDisplay = (Hosts.Count == 0) - ? "*:*" - : string.Join(",", Hosts.Select(h => h.Contains(':') ? h : h + ":*")); + /// + /// Returns a read-only collection of hosts used during routing. + /// Hosts will be Unicode rather than punycode, and may have a port. + /// An empty collection means any host will be accepted. + /// + public IReadOnlyList Hosts { get; } - return $"Hosts: {hostsDisplay}"; - } + private string DebuggerToString() + { + var hostsDisplay = (Hosts.Count == 0) + ? "*:*" + : string.Join(",", Hosts.Select(h => h.Contains(':') ? h : h + ":*")); + + return $"Hosts: {hostsDisplay}"; } } diff --git a/src/Http/Routing/src/HttpMethodMetadata.cs b/src/Http/Routing/src/HttpMethodMetadata.cs index a37701efa7..8586c5b744 100644 --- a/src/Http/Routing/src/HttpMethodMetadata.cs +++ b/src/Http/Routing/src/HttpMethodMetadata.cs @@ -7,59 +7,58 @@ using System.Diagnostics; using System.Linq; using static Microsoft.AspNetCore.Http.HttpMethods; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Represents HTTP method metadata used during routing. +/// +[DebuggerDisplay("{DebuggerToString(),nq}")] +public sealed class HttpMethodMetadata : IHttpMethodMetadata { /// - /// Represents HTTP method metadata used during routing. + /// Initializes a new instance of the class. /// - [DebuggerDisplay("{DebuggerToString(),nq}")] - public sealed class HttpMethodMetadata : IHttpMethodMetadata + /// + /// The HTTP methods used during routing. + /// An empty collection means any HTTP method will be accepted. + /// + public HttpMethodMetadata(IEnumerable httpMethods) + : this(httpMethods, acceptCorsPreflight: false) { - /// - /// Initializes a new instance of the class. - /// - /// - /// The HTTP methods used during routing. - /// An empty collection means any HTTP method will be accepted. - /// - public HttpMethodMetadata(IEnumerable httpMethods) - : this(httpMethods, acceptCorsPreflight: false) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The HTTP methods used during routing. - /// An empty collection means any HTTP method will be accepted. - /// - /// A value indicating whether routing accepts CORS preflight requests. - public HttpMethodMetadata(IEnumerable httpMethods, bool acceptCorsPreflight) + /// + /// Initializes a new instance of the class. + /// + /// + /// The HTTP methods used during routing. + /// An empty collection means any HTTP method will be accepted. + /// + /// A value indicating whether routing accepts CORS preflight requests. + public HttpMethodMetadata(IEnumerable httpMethods, bool acceptCorsPreflight) + { + if (httpMethods == null) { - if (httpMethods == null) - { - throw new ArgumentNullException(nameof(httpMethods)); - } - - HttpMethods = httpMethods.Select(GetCanonicalizedValue).ToArray(); - AcceptCorsPreflight = acceptCorsPreflight; + throw new ArgumentNullException(nameof(httpMethods)); } - /// - /// Returns a value indicating whether the associated endpoint should accept CORS preflight requests. - /// - public bool AcceptCorsPreflight { get; } + HttpMethods = httpMethods.Select(GetCanonicalizedValue).ToArray(); + AcceptCorsPreflight = acceptCorsPreflight; + } - /// - /// Returns a read-only collection of HTTP methods used during routing. - /// An empty collection means any HTTP method will be accepted. - /// - public IReadOnlyList HttpMethods { get; } + /// + /// Returns a value indicating whether the associated endpoint should accept CORS preflight requests. + /// + public bool AcceptCorsPreflight { get; } - private string DebuggerToString() - { - return $"HttpMethods: {string.Join(",", HttpMethods)} - Cors: {AcceptCorsPreflight}"; - } + /// + /// Returns a read-only collection of HTTP methods used during routing. + /// An empty collection means any HTTP method will be accepted. + /// + public IReadOnlyList HttpMethods { get; } + + private string DebuggerToString() + { + return $"HttpMethods: {string.Join(",", HttpMethods)} - Cors: {AcceptCorsPreflight}"; } } diff --git a/src/Http/Routing/src/IDataTokenMetadata.cs b/src/Http/Routing/src/IDataTokenMetadata.cs index 02a292d02b..6dbf0a2cb0 100644 --- a/src/Http/Routing/src/IDataTokenMetadata.cs +++ b/src/Http/Routing/src/IDataTokenMetadata.cs @@ -4,18 +4,17 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Metadata that defines data tokens for an . This metadata +/// type provides data tokens value for associated +/// with an endpoint. +/// +public interface IDataTokensMetadata { /// - /// Metadata that defines data tokens for an . This metadata - /// type provides data tokens value for associated - /// with an endpoint. + /// Get the data tokens. /// - public interface IDataTokensMetadata - { - /// - /// Get the data tokens. - /// - IReadOnlyDictionary DataTokens { get; } - } + IReadOnlyDictionary DataTokens { get; } } diff --git a/src/Http/Routing/src/IDynamicEndpointMetadata.cs b/src/Http/Routing/src/IDynamicEndpointMetadata.cs index 76d0698b60..501fe5b2ef 100644 --- a/src/Http/Routing/src/IDynamicEndpointMetadata.cs +++ b/src/Http/Routing/src/IDynamicEndpointMetadata.cs @@ -4,31 +4,30 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// A metadata interface that can be used to specify that the associated +/// will be dynamically replaced during matching. +/// +/// +/// +/// and related derived interfaces signal to +/// implementations that an has dynamic behavior +/// and thus cannot have its characteristics cached. +/// +/// +/// Using dynamic endpoints can be useful because the default matcher implementation does not +/// supply extensibility for how URLs are processed. Routing implementations that have dynamic +/// behavior can apply their dynamic logic after URL processing, by replacing a endpoints as +/// part of a . +/// +/// +public interface IDynamicEndpointMetadata { /// - /// A metadata interface that can be used to specify that the associated - /// will be dynamically replaced during matching. + /// Returns a value that indicates whether the associated endpoint has dynamic matching + /// behavior. /// - /// - /// - /// and related derived interfaces signal to - /// implementations that an has dynamic behavior - /// and thus cannot have its characteristics cached. - /// - /// - /// Using dynamic endpoints can be useful because the default matcher implementation does not - /// supply extensibility for how URLs are processed. Routing implementations that have dynamic - /// behavior can apply their dynamic logic after URL processing, by replacing a endpoints as - /// part of a . - /// - /// - public interface IDynamicEndpointMetadata - { - /// - /// Returns a value that indicates whether the associated endpoint has dynamic matching - /// behavior. - /// - bool IsDynamic { get; } - } + bool IsDynamic { get; } } diff --git a/src/Http/Routing/src/IEndpointAddressScheme.cs b/src/Http/Routing/src/IEndpointAddressScheme.cs index 09f503e84d..513ceda497 100644 --- a/src/Http/Routing/src/IEndpointAddressScheme.cs +++ b/src/Http/Routing/src/IEndpointAddressScheme.cs @@ -4,19 +4,18 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Defines a contract to find endpoints based on the provided address. +/// +/// The address type to look up endpoints. +public interface IEndpointAddressScheme { /// - /// Defines a contract to find endpoints based on the provided address. + /// Finds endpoints based on the provided . /// - /// The address type to look up endpoints. - public interface IEndpointAddressScheme - { - /// - /// Finds endpoints based on the provided . - /// - /// The information used to look up endpoints. - /// A collection of . - IEnumerable FindEndpoints(TAddress address); - } + /// The information used to look up endpoints. + /// A collection of . + IEnumerable FindEndpoints(TAddress address); } diff --git a/src/Http/Routing/src/IEndpointGroupNameMetadata.cs b/src/Http/Routing/src/IEndpointGroupNameMetadata.cs index 08d7fefc63..108d0f2a95 100644 --- a/src/Http/Routing/src/IEndpointGroupNameMetadata.cs +++ b/src/Http/Routing/src/IEndpointGroupNameMetadata.cs @@ -3,16 +3,15 @@ using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Defines a contract used to specify an endpoint group name in . +/// +public interface IEndpointGroupNameMetadata { /// - /// Defines a contract used to specify an endpoint group name in . + /// Gets the endpoint group name. /// - public interface IEndpointGroupNameMetadata - { - /// - /// Gets the endpoint group name. - /// - string EndpointGroupName { get; } - } + string EndpointGroupName { get; } } diff --git a/src/Http/Routing/src/IEndpointNameMetadata.cs b/src/Http/Routing/src/IEndpointNameMetadata.cs index 4b5f3a5236..79ddfa7a66 100644 --- a/src/Http/Routing/src/IEndpointNameMetadata.cs +++ b/src/Http/Routing/src/IEndpointNameMetadata.cs @@ -3,20 +3,19 @@ using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Defines a contract use to specify an endpoint name in . +/// +/// +/// Endpoint names must be unique within an application, and can be used to unambiguously +/// identify a desired endpoint for URI generation using . +/// +public interface IEndpointNameMetadata { /// - /// Defines a contract use to specify an endpoint name in . + /// Gets the endpoint name. /// - /// - /// Endpoint names must be unique within an application, and can be used to unambiguously - /// identify a desired endpoint for URI generation using . - /// - public interface IEndpointNameMetadata - { - /// - /// Gets the endpoint name. - /// - string EndpointName { get; } - } + string EndpointName { get; } } diff --git a/src/Http/Routing/src/IEndpointRouteBuilder.cs b/src/Http/Routing/src/IEndpointRouteBuilder.cs index 8810235be1..3840cbb3d1 100644 --- a/src/Http/Routing/src/IEndpointRouteBuilder.cs +++ b/src/Http/Routing/src/IEndpointRouteBuilder.cs @@ -5,28 +5,27 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Builder; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Defines a contract for a route builder in an application. A route builder specifies the routes for +/// an application. +/// +public interface IEndpointRouteBuilder { /// - /// Defines a contract for a route builder in an application. A route builder specifies the routes for - /// an application. + /// Creates a new . /// - public interface IEndpointRouteBuilder - { - /// - /// Creates a new . - /// - /// The new . - IApplicationBuilder CreateApplicationBuilder(); + /// The new . + IApplicationBuilder CreateApplicationBuilder(); - /// - /// Gets the sets the used to resolve services for routes. - /// - IServiceProvider ServiceProvider { get; } + /// + /// Gets the sets the used to resolve services for routes. + /// + IServiceProvider ServiceProvider { get; } - /// - /// Gets the endpoint data sources configured in the builder. - /// - ICollection DataSources { get; } - } + /// + /// Gets the endpoint data sources configured in the builder. + /// + ICollection DataSources { get; } } diff --git a/src/Http/Routing/src/IExcludeFromDescriptionMetadata.cs b/src/Http/Routing/src/IExcludeFromDescriptionMetadata.cs index 4e3c1eb997..ae308e14ac 100644 --- a/src/Http/Routing/src/IExcludeFromDescriptionMetadata.cs +++ b/src/Http/Routing/src/IExcludeFromDescriptionMetadata.cs @@ -4,18 +4,17 @@ using System; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Indicates whether or not that API explorer data should be emitted for this endpoint. +/// +public interface IExcludeFromDescriptionMetadata { /// - /// Indicates whether or not that API explorer data should be emitted for this endpoint. + /// Gets a value indicating whether OpenAPI + /// data should be excluded for this endpoint. If , + /// API metadata is not emitted. /// - public interface IExcludeFromDescriptionMetadata - { - /// - /// Gets a value indicating whether OpenAPI - /// data should be excluded for this endpoint. If , - /// API metadata is not emitted. - /// - bool ExcludeFromDescription { get; } - } + bool ExcludeFromDescription { get; } } diff --git a/src/Http/Routing/src/IHostMetadata.cs b/src/Http/Routing/src/IHostMetadata.cs index 8876ac770b..5975671e5f 100644 --- a/src/Http/Routing/src/IHostMetadata.cs +++ b/src/Http/Routing/src/IHostMetadata.cs @@ -3,18 +3,17 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Represents host metadata used during routing. +/// +public interface IHostMetadata { /// - /// Represents host metadata used during routing. + /// Returns a read-only collection of hosts used during routing. + /// Hosts will be Unicode rather than punycode, and may have a port. + /// An empty collection means any host will be accepted. /// - public interface IHostMetadata - { - /// - /// Returns a read-only collection of hosts used during routing. - /// Hosts will be Unicode rather than punycode, and may have a port. - /// An empty collection means any host will be accepted. - /// - IReadOnlyList Hosts { get; } - } + IReadOnlyList Hosts { get; } } diff --git a/src/Http/Routing/src/IHttpMethodMetadata.cs b/src/Http/Routing/src/IHttpMethodMetadata.cs index ca7e87d506..c32391b2d5 100644 --- a/src/Http/Routing/src/IHttpMethodMetadata.cs +++ b/src/Http/Routing/src/IHttpMethodMetadata.cs @@ -3,22 +3,21 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Represents HTTP method metadata used during routing. +/// +public interface IHttpMethodMetadata { /// - /// Represents HTTP method metadata used during routing. + /// Returns a value indicating whether the associated endpoint should accept CORS preflight requests. /// - public interface IHttpMethodMetadata - { - /// - /// Returns a value indicating whether the associated endpoint should accept CORS preflight requests. - /// - bool AcceptCorsPreflight { get; } + bool AcceptCorsPreflight { get; } - /// - /// Returns a read-only collection of HTTP methods used during routing. - /// An empty collection means any HTTP method will be accepted. - /// - IReadOnlyList HttpMethods { get; } - } + /// + /// Returns a read-only collection of HTTP methods used during routing. + /// An empty collection means any HTTP method will be accepted. + /// + IReadOnlyList HttpMethods { get; } } diff --git a/src/Http/Routing/src/IInlineConstraintResolver.cs b/src/Http/Routing/src/IInlineConstraintResolver.cs index 16e469cc4c..20537eefca 100644 --- a/src/Http/Routing/src/IInlineConstraintResolver.cs +++ b/src/Http/Routing/src/IInlineConstraintResolver.cs @@ -1,18 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Defines an abstraction for resolving inline constraints as instances of . +/// +public interface IInlineConstraintResolver { /// - /// Defines an abstraction for resolving inline constraints as instances of . + /// Resolves the inline constraint. /// - public interface IInlineConstraintResolver - { - /// - /// Resolves the inline constraint. - /// - /// The inline constraint to resolve. - /// The the inline constraint was resolved to. - IRouteConstraint? ResolveConstraint(string inlineConstraint); - } + /// The inline constraint to resolve. + /// The the inline constraint was resolved to. + IRouteConstraint? ResolveConstraint(string inlineConstraint); } diff --git a/src/Http/Routing/src/INamedRouter.cs b/src/Http/Routing/src/INamedRouter.cs index 71ed22bc41..be07a56222 100644 --- a/src/Http/Routing/src/INamedRouter.cs +++ b/src/Http/Routing/src/INamedRouter.cs @@ -1,16 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// An interface for an with a name. +/// +public interface INamedRouter : IRouter { /// - /// An interface for an with a name. + /// The name of the router. Can be null. /// - public interface INamedRouter : IRouter - { - /// - /// The name of the router. Can be null. - /// - string? Name { get; } - } + string? Name { get; } } diff --git a/src/Http/Routing/src/IRouteBuilder.cs b/src/Http/Routing/src/IRouteBuilder.cs index 4e460c0be2..7229d675f2 100644 --- a/src/Http/Routing/src/IRouteBuilder.cs +++ b/src/Http/Routing/src/IRouteBuilder.cs @@ -5,38 +5,37 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Builder; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Defines a contract for a route builder in an application. A route builder specifies the routes for +/// an application. +/// +public interface IRouteBuilder { /// - /// Defines a contract for a route builder in an application. A route builder specifies the routes for - /// an application. + /// Gets the . /// - public interface IRouteBuilder - { - /// - /// Gets the . - /// - IApplicationBuilder ApplicationBuilder { get; } + IApplicationBuilder ApplicationBuilder { get; } - /// - /// Gets or sets the default that is used as a handler if an - /// is added to the list of routes but does not specify its own. - /// - IRouter? DefaultHandler { get; set; } + /// + /// Gets or sets the default that is used as a handler if an + /// is added to the list of routes but does not specify its own. + /// + IRouter? DefaultHandler { get; set; } - /// - /// Gets the sets the used to resolve services for routes. - /// - IServiceProvider ServiceProvider { get; } + /// + /// Gets the sets the used to resolve services for routes. + /// + IServiceProvider ServiceProvider { get; } - /// - /// Gets the routes configured in the builder. - /// - IList Routes { get; } + /// + /// Gets the routes configured in the builder. + /// + IList Routes { get; } - /// - /// Builds an that routes the routes specified in the property. - /// - IRouter Build(); - } + /// + /// Builds an that routes the routes specified in the property. + /// + IRouter Build(); } diff --git a/src/Http/Routing/src/IRouteCollection.cs b/src/Http/Routing/src/IRouteCollection.cs index 06cfd4b4c8..8259b4bab5 100644 --- a/src/Http/Routing/src/IRouteCollection.cs +++ b/src/Http/Routing/src/IRouteCollection.cs @@ -1,17 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Interface for a router that supports appending new routes. +/// +public interface IRouteCollection : IRouter { /// - /// Interface for a router that supports appending new routes. + /// Appends the collection of routes defined in . /// - public interface IRouteCollection : IRouter - { - /// - /// Appends the collection of routes defined in . - /// - /// A instance. - void Add(IRouter router); - } + /// A instance. + void Add(IRouter router); } diff --git a/src/Http/Routing/src/IRouteNameMetadata.cs b/src/Http/Routing/src/IRouteNameMetadata.cs index e7198844c4..9a617cc3da 100644 --- a/src/Http/Routing/src/IRouteNameMetadata.cs +++ b/src/Http/Routing/src/IRouteNameMetadata.cs @@ -1,17 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Represents metadata used during link generation to find +/// the associated endpoint using route name. +/// +public interface IRouteNameMetadata { /// - /// Represents metadata used during link generation to find - /// the associated endpoint using route name. + /// Gets the route name. Can be . /// - public interface IRouteNameMetadata - { - /// - /// Gets the route name. Can be . - /// - string? RouteName { get; } - } + string? RouteName { get; } } diff --git a/src/Http/Routing/src/ISuppressLinkGenerationMetadata.cs b/src/Http/Routing/src/ISuppressLinkGenerationMetadata.cs index 45b2504ad8..3305e5e8ab 100644 --- a/src/Http/Routing/src/ISuppressLinkGenerationMetadata.cs +++ b/src/Http/Routing/src/ISuppressLinkGenerationMetadata.cs @@ -1,17 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Represents metadata used during link generation. If is true +/// the associated endpoint will not be used for link generation. +/// +public interface ISuppressLinkGenerationMetadata { /// - /// Represents metadata used during link generation. If is true - /// the associated endpoint will not be used for link generation. + /// Gets a value indicating whether the associated endpoint should be used for link generation. /// - public interface ISuppressLinkGenerationMetadata - { - /// - /// Gets a value indicating whether the associated endpoint should be used for link generation. - /// - bool SuppressLinkGeneration { get; } - } -} \ No newline at end of file + bool SuppressLinkGeneration { get; } +} diff --git a/src/Http/Routing/src/ISuppressMatchingMetadata.cs b/src/Http/Routing/src/ISuppressMatchingMetadata.cs index fbf5ac0f43..ca0a79e844 100644 --- a/src/Http/Routing/src/ISuppressMatchingMetadata.cs +++ b/src/Http/Routing/src/ISuppressMatchingMetadata.cs @@ -1,17 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Metadata used to prevent URL matching. If is true the +/// associated endpoint will not be considered for URL matching. +/// +public interface ISuppressMatchingMetadata { /// - /// Metadata used to prevent URL matching. If is true the - /// associated endpoint will not be considered for URL matching. + /// Gets a value indicating whether the associated endpoint should be used for URL matching. /// - public interface ISuppressMatchingMetadata - { - /// - /// Gets a value indicating whether the associated endpoint should be used for URL matching. - /// - bool SuppressMatching { get; } - } -} \ No newline at end of file + bool SuppressMatching { get; } +} diff --git a/src/Http/Routing/src/InlineRouteParameterParser.cs b/src/Http/Routing/src/InlineRouteParameterParser.cs index 416f395bf7..a68827a259 100644 --- a/src/Http/Routing/src/InlineRouteParameterParser.cs +++ b/src/Http/Routing/src/InlineRouteParameterParser.cs @@ -5,249 +5,248 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Routing.Template; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Contains methods for parsing processing constraints from a route definition. +/// +public static class InlineRouteParameterParser { /// - /// Contains methods for parsing processing constraints from a route definition. + /// Parses a string representing the provided into a . /// - public static class InlineRouteParameterParser + /// A string representation of the route parameter. + /// A instance. + public static TemplatePart ParseRouteParameter(string routeParameter) { - /// - /// Parses a string representing the provided into a . - /// - /// A string representation of the route parameter. - /// A instance. - public static TemplatePart ParseRouteParameter(string routeParameter) + if (routeParameter == null) { - if (routeParameter == null) - { - throw new ArgumentNullException(nameof(routeParameter)); - } - - if (routeParameter.Length == 0) - { - return TemplatePart.CreateParameter( - name: string.Empty, - isCatchAll: false, - isOptional: false, - defaultValue: null, - inlineConstraints: null); - } - - var startIndex = 0; - var endIndex = routeParameter.Length - 1; + throw new ArgumentNullException(nameof(routeParameter)); + } - var isCatchAll = false; - var isOptional = false; + if (routeParameter.Length == 0) + { + return TemplatePart.CreateParameter( + name: string.Empty, + isCatchAll: false, + isOptional: false, + defaultValue: null, + inlineConstraints: null); + } - if (routeParameter[0] == '*') - { - isCatchAll = true; - startIndex++; - } + var startIndex = 0; + var endIndex = routeParameter.Length - 1; - if (routeParameter[endIndex] == '?') - { - isOptional = true; - endIndex--; - } + var isCatchAll = false; + var isOptional = false; - var currentIndex = startIndex; + if (routeParameter[0] == '*') + { + isCatchAll = true; + startIndex++; + } - // Parse parameter name - var parameterName = string.Empty; + if (routeParameter[endIndex] == '?') + { + isOptional = true; + endIndex--; + } - while (currentIndex <= endIndex) - { - var currentChar = routeParameter[currentIndex]; + var currentIndex = startIndex; - if ((currentChar == ':' || currentChar == '=') && startIndex != currentIndex) - { - // Parameter names are allowed to start with delimiters used to denote constraints or default values. - // i.e. "=foo" or ":bar" would be treated as parameter names rather than default value or constraint - // specifications. - parameterName = routeParameter.Substring(startIndex, currentIndex - startIndex); + // Parse parameter name + var parameterName = string.Empty; - // Roll the index back and move to the constraint parsing stage. - currentIndex--; - break; - } - else if (currentIndex == endIndex) - { - parameterName = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); - } + while (currentIndex <= endIndex) + { + var currentChar = routeParameter[currentIndex]; - currentIndex++; + if ((currentChar == ':' || currentChar == '=') && startIndex != currentIndex) + { + // Parameter names are allowed to start with delimiters used to denote constraints or default values. + // i.e. "=foo" or ":bar" would be treated as parameter names rather than default value or constraint + // specifications. + parameterName = routeParameter.Substring(startIndex, currentIndex - startIndex); + + // Roll the index back and move to the constraint parsing stage. + currentIndex--; + break; } - - var parseResults = ParseConstraints(routeParameter, currentIndex, endIndex); - currentIndex = parseResults.CurrentIndex; - - string? defaultValue = null; - if (currentIndex <= endIndex && - routeParameter[currentIndex] == '=') + else if (currentIndex == endIndex) { - defaultValue = routeParameter.Substring(currentIndex + 1, endIndex - currentIndex); + parameterName = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); } - return TemplatePart.CreateParameter(parameterName, - isCatchAll, - isOptional, - defaultValue, - parseResults.Constraints); + currentIndex++; } - private static ConstraintParseResults ParseConstraints( - string routeParameter, - int currentIndex, - int endIndex) + var parseResults = ParseConstraints(routeParameter, currentIndex, endIndex); + currentIndex = parseResults.CurrentIndex; + + string? defaultValue = null; + if (currentIndex <= endIndex && + routeParameter[currentIndex] == '=') + { + defaultValue = routeParameter.Substring(currentIndex + 1, endIndex - currentIndex); + } + + return TemplatePart.CreateParameter(parameterName, + isCatchAll, + isOptional, + defaultValue, + parseResults.Constraints); + } + + private static ConstraintParseResults ParseConstraints( + string routeParameter, + int currentIndex, + int endIndex) + { + var inlineConstraints = new List(); + var state = ParseState.Start; + var startIndex = currentIndex; + do { - var inlineConstraints = new List(); - var state = ParseState.Start; - var startIndex = currentIndex; - do + var currentChar = currentIndex > endIndex ? null : (char?)routeParameter[currentIndex]; + switch (state) { - var currentChar = currentIndex > endIndex ? null : (char?)routeParameter[currentIndex]; - switch (state) - { - case ParseState.Start: - switch (currentChar) - { - case null: - state = ParseState.End; - break; - case ':': - state = ParseState.ParsingName; - startIndex = currentIndex + 1; - break; - case '(': - state = ParseState.InsideParenthesis; - break; - case '=': - state = ParseState.End; - currentIndex--; - break; - } - break; - case ParseState.InsideParenthesis: - switch (currentChar) - { - case null: - state = ParseState.End; - var constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); - inlineConstraints.Add(new InlineConstraint(constraintText)); - break; - case ')': - // Only consume a ')' token if - // (a) it is the last token - // (b) the next character is the start of the new constraint ':' - // (c) the next character is the start of the default value. - - var nextChar = currentIndex + 1 > endIndex ? null : (char?)routeParameter[currentIndex + 1]; - switch (nextChar) - { - case null: - state = ParseState.End; - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); - inlineConstraints.Add(new InlineConstraint(constraintText)); - break; - case ':': - state = ParseState.Start; - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); - inlineConstraints.Add(new InlineConstraint(constraintText)); - startIndex = currentIndex + 1; - break; - case '=': - state = ParseState.End; - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); - inlineConstraints.Add(new InlineConstraint(constraintText)); - break; - } - break; - case ':': - case '=': - // In the original implementation, the Regex would've backtracked if it encountered an - // unbalanced opening bracket followed by (not necessarily immediately) a delimiter. - // Simply verifying that the parantheses will eventually be closed should suffice to - // determine if the terminator needs to be consumed as part of the current constraint - // specification. - var indexOfClosingParantheses = routeParameter.IndexOf(')', currentIndex + 1); - if (indexOfClosingParantheses == -1) - { - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); + case ParseState.Start: + switch (currentChar) + { + case null: + state = ParseState.End; + break; + case ':': + state = ParseState.ParsingName; + startIndex = currentIndex + 1; + break; + case '(': + state = ParseState.InsideParenthesis; + break; + case '=': + state = ParseState.End; + currentIndex--; + break; + } + break; + case ParseState.InsideParenthesis: + switch (currentChar) + { + case null: + state = ParseState.End; + var constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); + inlineConstraints.Add(new InlineConstraint(constraintText)); + break; + case ')': + // Only consume a ')' token if + // (a) it is the last token + // (b) the next character is the start of the new constraint ':' + // (c) the next character is the start of the default value. + + var nextChar = currentIndex + 1 > endIndex ? null : (char?)routeParameter[currentIndex + 1]; + switch (nextChar) + { + case null: + state = ParseState.End; + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); + inlineConstraints.Add(new InlineConstraint(constraintText)); + break; + case ':': + state = ParseState.Start; + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); inlineConstraints.Add(new InlineConstraint(constraintText)); + startIndex = currentIndex + 1; + break; + case '=': + state = ParseState.End; + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex + 1); + inlineConstraints.Add(new InlineConstraint(constraintText)); + break; + } + break; + case ':': + case '=': + // In the original implementation, the Regex would've backtracked if it encountered an + // unbalanced opening bracket followed by (not necessarily immediately) a delimiter. + // Simply verifying that the parantheses will eventually be closed should suffice to + // determine if the terminator needs to be consumed as part of the current constraint + // specification. + var indexOfClosingParantheses = routeParameter.IndexOf(')', currentIndex + 1); + if (indexOfClosingParantheses == -1) + { + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); + inlineConstraints.Add(new InlineConstraint(constraintText)); - if (currentChar == ':') - { - state = ParseState.ParsingName; - startIndex = currentIndex + 1; - } - else - { - state = ParseState.End; - currentIndex--; - } + if (currentChar == ':') + { + state = ParseState.ParsingName; + startIndex = currentIndex + 1; } else { - currentIndex = indexOfClosingParantheses; + state = ParseState.End; + currentIndex--; } + } + else + { + currentIndex = indexOfClosingParantheses; + } + + break; + } + break; + case ParseState.ParsingName: + switch (currentChar) + { + case null: + state = ParseState.End; + var constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); + inlineConstraints.Add(new InlineConstraint(constraintText)); + break; + case ':': + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); + inlineConstraints.Add(new InlineConstraint(constraintText)); + startIndex = currentIndex + 1; + break; + case '(': + state = ParseState.InsideParenthesis; + break; + case '=': + state = ParseState.End; + constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); + inlineConstraints.Add(new InlineConstraint(constraintText)); + currentIndex--; + break; + } + break; + } - break; - } - break; - case ParseState.ParsingName: - switch (currentChar) - { - case null: - state = ParseState.End; - var constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); - inlineConstraints.Add(new InlineConstraint(constraintText)); - break; - case ':': - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); - inlineConstraints.Add(new InlineConstraint(constraintText)); - startIndex = currentIndex + 1; - break; - case '(': - state = ParseState.InsideParenthesis; - break; - case '=': - state = ParseState.End; - constraintText = routeParameter.Substring(startIndex, currentIndex - startIndex); - inlineConstraints.Add(new InlineConstraint(constraintText)); - currentIndex--; - break; - } - break; - } - - currentIndex++; + currentIndex++; - } while (state != ParseState.End); + } while (state != ParseState.End); - return new ConstraintParseResults(currentIndex, inlineConstraints); - } + return new ConstraintParseResults(currentIndex, inlineConstraints); + } - private enum ParseState - { - Start, - ParsingName, - InsideParenthesis, - End - } + private enum ParseState + { + Start, + ParsingName, + InsideParenthesis, + End + } - private readonly struct ConstraintParseResults - { - public readonly int CurrentIndex; + private readonly struct ConstraintParseResults + { + public readonly int CurrentIndex; - public readonly IEnumerable Constraints; + public readonly IEnumerable Constraints; - public ConstraintParseResults(int currentIndex, IEnumerable constraints) - { - CurrentIndex = currentIndex; - Constraints = constraints; - } + public ConstraintParseResults(int currentIndex, IEnumerable constraints) + { + CurrentIndex = currentIndex; + Constraints = constraints; } } } diff --git a/src/Http/Routing/src/Internal/DfaGraphWriter.cs b/src/Http/Routing/src/Internal/DfaGraphWriter.cs index dbe47d7c28..95f992d457 100644 --- a/src/Http/Routing/src/Internal/DfaGraphWriter.cs +++ b/src/Http/Routing/src/Internal/DfaGraphWriter.cs @@ -7,100 +7,99 @@ using System.IO; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Routing.Internal +namespace Microsoft.AspNetCore.Routing.Internal; + +/// +/// +/// A singleton service that can be used to write the route table as a state machine +/// in GraphViz DOT language https://www.graphviz.org/doc/info/lang.html +/// +/// +/// You can use http://www.webgraphviz.com/ to visualize the results. +/// +/// +/// This type has no support contract, and may be removed or changed at any time in +/// a future release. +/// +/// +public class DfaGraphWriter { + private readonly IServiceProvider _services; + /// - /// - /// A singleton service that can be used to write the route table as a state machine - /// in GraphViz DOT language https://www.graphviz.org/doc/info/lang.html - /// - /// - /// You can use http://www.webgraphviz.com/ to visualize the results. - /// - /// - /// This type has no support contract, and may be removed or changed at any time in - /// a future release. - /// + /// Constructor for a given . /// - public class DfaGraphWriter + /// The to add services to. + public DfaGraphWriter(IServiceProvider services) { - private readonly IServiceProvider _services; + _services = services; + } - /// - /// Constructor for a given . - /// - /// The to add services to. - public DfaGraphWriter(IServiceProvider services) - { - _services = services; - } + /// + /// Displays a graph representation of in DOT. + /// + /// The to extract routes from. + /// The to which the content is written. + public void Write(EndpointDataSource dataSource, TextWriter writer) + { + var builder = _services.GetRequiredService(); - /// - /// Displays a graph representation of in DOT. - /// - /// The to extract routes from. - /// The to which the content is written. - public void Write(EndpointDataSource dataSource, TextWriter writer) + var endpoints = dataSource.Endpoints; + for (var i = 0; i < endpoints.Count; i++) { - var builder = _services.GetRequiredService(); - - var endpoints = dataSource.Endpoints; - for (var i = 0; i < endpoints.Count; i++) + if (endpoints[i] is RouteEndpoint endpoint && (endpoint.Metadata.GetMetadata()?.SuppressMatching ?? false) == false) { - if (endpoints[i] is RouteEndpoint endpoint && (endpoint.Metadata.GetMetadata()?.SuppressMatching ?? false) == false) - { - builder.AddEndpoint(endpoint); - } + builder.AddEndpoint(endpoint); } + } - // Assign each node a sequential index. - var visited = new Dictionary(); + // Assign each node a sequential index. + var visited = new Dictionary(); - var tree = builder.BuildDfaTree(includeLabel: true); + var tree = builder.BuildDfaTree(includeLabel: true); - writer.WriteLine("digraph DFA {"); - tree.Visit(WriteNode); - writer.WriteLine("}"); + writer.WriteLine("digraph DFA {"); + tree.Visit(WriteNode); + writer.WriteLine("}"); - void WriteNode(DfaNode node) + void WriteNode(DfaNode node) + { + if (!visited.TryGetValue(node, out var label)) { - if (!visited.TryGetValue(node, out var label)) - { - label = visited.Count; - visited.Add(node, label); - } + label = visited.Count; + visited.Add(node, label); + } - // We can safely index into visited because this is a post-order traversal, - // all of the children of this node are already in the dictionary. + // We can safely index into visited because this is a post-order traversal, + // all of the children of this node are already in the dictionary. - if (node.Literals != null) + if (node.Literals != null) + { + foreach (var literal in node.Literals) { - foreach (var literal in node.Literals) - { - writer.WriteLine($"{label} -> {visited[literal.Value]} [label=\"/{literal.Key}\"]"); - } + writer.WriteLine($"{label} -> {visited[literal.Value]} [label=\"/{literal.Key}\"]"); } + } - if (node.Parameters != null) - { - writer.WriteLine($"{label} -> {visited[node.Parameters]} [label=\"/*\"]"); - } + if (node.Parameters != null) + { + writer.WriteLine($"{label} -> {visited[node.Parameters]} [label=\"/*\"]"); + } - if (node.CatchAll != null && node.Parameters != node.CatchAll) - { - writer.WriteLine($"{label} -> {visited[node.CatchAll]} [label=\"/**\"]"); - } + if (node.CatchAll != null && node.Parameters != node.CatchAll) + { + writer.WriteLine($"{label} -> {visited[node.CatchAll]} [label=\"/**\"]"); + } - if (node.PolicyEdges != null) + if (node.PolicyEdges != null) + { + foreach (var policy in node.PolicyEdges) { - foreach (var policy in node.PolicyEdges) - { - writer.WriteLine($"{label} -> {visited[policy.Value]} [label=\"{policy.Key}\"]"); - } + writer.WriteLine($"{label} -> {visited[policy.Value]} [label=\"{policy.Key}\"]"); } - - writer.WriteLine($"{label} [label=\"{node.Label}\"]"); } + + writer.WriteLine($"{label} [label=\"{node.Label}\"]"); } } } diff --git a/src/Http/Routing/src/LinkGeneratorEndpointNameAddressExtensions.cs b/src/Http/Routing/src/LinkGeneratorEndpointNameAddressExtensions.cs index 5a652f513f..8b556914d2 100644 --- a/src/Http/Routing/src/LinkGeneratorEndpointNameAddressExtensions.cs +++ b/src/Http/Routing/src/LinkGeneratorEndpointNameAddressExtensions.cs @@ -5,227 +5,226 @@ using System; using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Extension methods for using with and endpoint name. +/// +public static class LinkGeneratorEndpointNameAddressExtensions { /// - /// Extension methods for using with and endpoint name. + /// Generates a URI with an absolute path based on the provided values. /// - public static class LinkGeneratorEndpointNameAddressExtensions + /// The . + /// The associated with the current request. + /// The endpoint name. Used to resolve endpoints. + /// The route values. Used to expand parameters in the route template. Optional. + /// + /// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of will be used. + /// + /// An optional URI fragment. Appended to the resulting URI. + /// + /// An optional . Settings on provided object override the settings with matching + /// names from RouteOptions. + /// + /// A URI with an absolute path, or null. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static string? GetPathByName( + this LinkGenerator generator, + HttpContext httpContext, + string endpointName, + object? values, + PathString? pathBase = default, + FragmentString fragment = default, + LinkOptions? options = default) { - /// - /// Generates a URI with an absolute path based on the provided values. - /// - /// The . - /// The associated with the current request. - /// The endpoint name. Used to resolve endpoints. - /// The route values. Used to expand parameters in the route template. Optional. - /// - /// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of will be used. - /// - /// An optional URI fragment. Appended to the resulting URI. - /// - /// An optional . Settings on provided object override the settings with matching - /// names from RouteOptions. - /// - /// A URI with an absolute path, or null. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static string? GetPathByName( - this LinkGenerator generator, - HttpContext httpContext, - string endpointName, - object? values, - PathString? pathBase = default, - FragmentString fragment = default, - LinkOptions? options = default) + if (generator == null) { - if (generator == null) - { - throw new ArgumentNullException(nameof(generator)); - } - - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - if (endpointName == null) - { - throw new ArgumentNullException(nameof(endpointName)); - } - - return generator.GetPathByAddress( - httpContext, - endpointName, - new RouteValueDictionary(values), - ambientValues: null, - pathBase, - fragment, - options); + throw new ArgumentNullException(nameof(generator)); } - /// - /// Generates a URI with an absolute path based on the provided values. - /// - /// The . - /// The endpoint name. Used to resolve endpoints. - /// The route values. Used to expand parameters in the route template. Optional. - /// An optional URI path base. Prepended to the path in the resulting URI. - /// An optional URI fragment. Appended to the resulting URI. - /// - /// An optional . Settings on provided object override the settings with matching - /// names from RouteOptions. - /// - /// A URI with an absolute path, or null. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static string? GetPathByName( - this LinkGenerator generator, - string endpointName, - object? values, - PathString pathBase = default, - FragmentString fragment = default, - LinkOptions? options = default) + if (httpContext == null) { - if (generator == null) - { - throw new ArgumentNullException(nameof(generator)); - } + throw new ArgumentNullException(nameof(httpContext)); + } + + if (endpointName == null) + { + throw new ArgumentNullException(nameof(endpointName)); + } - if (endpointName == null) - { - throw new ArgumentNullException(nameof(endpointName)); - } + return generator.GetPathByAddress( + httpContext, + endpointName, + new RouteValueDictionary(values), + ambientValues: null, + pathBase, + fragment, + options); + } + + /// + /// Generates a URI with an absolute path based on the provided values. + /// + /// The . + /// The endpoint name. Used to resolve endpoints. + /// The route values. Used to expand parameters in the route template. Optional. + /// An optional URI path base. Prepended to the path in the resulting URI. + /// An optional URI fragment. Appended to the resulting URI. + /// + /// An optional . Settings on provided object override the settings with matching + /// names from RouteOptions. + /// + /// A URI with an absolute path, or null. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static string? GetPathByName( + this LinkGenerator generator, + string endpointName, + object? values, + PathString pathBase = default, + FragmentString fragment = default, + LinkOptions? options = default) + { + if (generator == null) + { + throw new ArgumentNullException(nameof(generator)); + } - return generator.GetPathByAddress(endpointName, new RouteValueDictionary(values), pathBase, fragment, options); + if (endpointName == null) + { + throw new ArgumentNullException(nameof(endpointName)); } - /// - /// Generates an absolute URI based on the provided values. - /// - /// The . - /// The associated with the current request. - /// The endpoint name. Used to resolve endpoints. - /// The route values. Used to expand parameters in the route template. Optional. - /// - /// The URI scheme, applied to the resulting URI. Optional. If not provided, the value of will be used. - /// - /// - /// The URI host/authority, applied to the resulting URI. Optional. If not provided, the value will be used. - /// See the remarks section for details about the security implications of the . - /// - /// - /// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of will be used. - /// - /// An optional URI fragment. Appended to the resulting URI. - /// - /// An optional . Settings on provided object override the settings with matching - /// names from RouteOptions. - /// - /// A URI with an absolute path, or null. - /// - /// - /// The value of should be a trusted value. Relying on the value of the current request - /// can allow untrusted input to influence the resulting URI unless the Host header has been validated. - /// See the deployment documentation for instructions on how to properly validate the Host header in - /// your deployment environment. - /// - /// - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static string? GetUriByName( - this LinkGenerator generator, - HttpContext httpContext, - string endpointName, - object? values, - string? scheme = default, - HostString? host = default, - PathString? pathBase = default, - FragmentString fragment = default, - LinkOptions? options = default) + return generator.GetPathByAddress(endpointName, new RouteValueDictionary(values), pathBase, fragment, options); + } + + /// + /// Generates an absolute URI based on the provided values. + /// + /// The . + /// The associated with the current request. + /// The endpoint name. Used to resolve endpoints. + /// The route values. Used to expand parameters in the route template. Optional. + /// + /// The URI scheme, applied to the resulting URI. Optional. If not provided, the value of will be used. + /// + /// + /// The URI host/authority, applied to the resulting URI. Optional. If not provided, the value will be used. + /// See the remarks section for details about the security implications of the . + /// + /// + /// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of will be used. + /// + /// An optional URI fragment. Appended to the resulting URI. + /// + /// An optional . Settings on provided object override the settings with matching + /// names from RouteOptions. + /// + /// A URI with an absolute path, or null. + /// + /// + /// The value of should be a trusted value. Relying on the value of the current request + /// can allow untrusted input to influence the resulting URI unless the Host header has been validated. + /// See the deployment documentation for instructions on how to properly validate the Host header in + /// your deployment environment. + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static string? GetUriByName( + this LinkGenerator generator, + HttpContext httpContext, + string endpointName, + object? values, + string? scheme = default, + HostString? host = default, + PathString? pathBase = default, + FragmentString fragment = default, + LinkOptions? options = default) + { + if (generator == null) { - if (generator == null) - { - throw new ArgumentNullException(nameof(generator)); - } - - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - if (endpointName == null) - { - throw new ArgumentNullException(nameof(endpointName)); - } - - return generator.GetUriByAddress( - httpContext, - endpointName, - new RouteValueDictionary(values), - ambientValues: null, - scheme, - host, - pathBase, - fragment, - options); + throw new ArgumentNullException(nameof(generator)); } - /// - /// Generates an absolute URI based on the provided values. - /// - /// The . - /// The endpoint name. Used to resolve endpoints. - /// The route values. Used to expand parameters in the route template. Optional. - /// The URI scheme, applied to the resulting URI. - /// - /// The URI host/authority, applied to the resulting URI. - /// See the remarks section for details about the security implications of the . - /// - /// An optional URI path base. Prepended to the path in the resulting URI. - /// An optional URI fragment. Appended to the resulting URI. - /// - /// An optional . Settings on provided object override the settings with matching - /// names from RouteOptions. - /// - /// An absolute URI, or null. - /// - /// - /// The value of should be a trusted value. Relying on the value of the current request - /// can allow untrusted input to influence the resulting URI unless the Host header has been validated. - /// See the deployment documentation for instructions on how to properly validate the Host header in - /// your deployment environment. - /// - /// - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static string? GetUriByName( - this LinkGenerator generator, - string endpointName, - object? values, - string scheme, - HostString host, - PathString pathBase = default, - FragmentString fragment = default, - LinkOptions? options = default) + if (httpContext == null) { - if (generator == null) - { - throw new ArgumentNullException(nameof(generator)); - } - - if (endpointName == null) - { - throw new ArgumentNullException(nameof(endpointName)); - } - - if (string.IsNullOrEmpty(scheme)) - { - throw new ArgumentException("A scheme must be provided.", nameof(scheme)); - } - - if (!host.HasValue) - { - throw new ArgumentException("A host must be provided.", nameof(host)); - } - - return generator.GetUriByAddress(endpointName, new RouteValueDictionary(values), scheme, host, pathBase, fragment, options); + throw new ArgumentNullException(nameof(httpContext)); } + + if (endpointName == null) + { + throw new ArgumentNullException(nameof(endpointName)); + } + + return generator.GetUriByAddress( + httpContext, + endpointName, + new RouteValueDictionary(values), + ambientValues: null, + scheme, + host, + pathBase, + fragment, + options); + } + + /// + /// Generates an absolute URI based on the provided values. + /// + /// The . + /// The endpoint name. Used to resolve endpoints. + /// The route values. Used to expand parameters in the route template. Optional. + /// The URI scheme, applied to the resulting URI. + /// + /// The URI host/authority, applied to the resulting URI. + /// See the remarks section for details about the security implications of the . + /// + /// An optional URI path base. Prepended to the path in the resulting URI. + /// An optional URI fragment. Appended to the resulting URI. + /// + /// An optional . Settings on provided object override the settings with matching + /// names from RouteOptions. + /// + /// An absolute URI, or null. + /// + /// + /// The value of should be a trusted value. Relying on the value of the current request + /// can allow untrusted input to influence the resulting URI unless the Host header has been validated. + /// See the deployment documentation for instructions on how to properly validate the Host header in + /// your deployment environment. + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static string? GetUriByName( + this LinkGenerator generator, + string endpointName, + object? values, + string scheme, + HostString host, + PathString pathBase = default, + FragmentString fragment = default, + LinkOptions? options = default) + { + if (generator == null) + { + throw new ArgumentNullException(nameof(generator)); + } + + if (endpointName == null) + { + throw new ArgumentNullException(nameof(endpointName)); + } + + if (string.IsNullOrEmpty(scheme)) + { + throw new ArgumentException("A scheme must be provided.", nameof(scheme)); + } + + if (!host.HasValue) + { + throw new ArgumentException("A host must be provided.", nameof(host)); + } + + return generator.GetUriByAddress(endpointName, new RouteValueDictionary(values), scheme, host, pathBase, fragment, options); } } diff --git a/src/Http/Routing/src/LinkGeneratorRouteValuesAddressExtensions.cs b/src/Http/Routing/src/LinkGeneratorRouteValuesAddressExtensions.cs index 444dc5c0d3..a1677a71ba 100644 --- a/src/Http/Routing/src/LinkGeneratorRouteValuesAddressExtensions.cs +++ b/src/Http/Routing/src/LinkGeneratorRouteValuesAddressExtensions.cs @@ -5,211 +5,210 @@ using System; using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Extension methods for using with . +/// +public static class LinkGeneratorRouteValuesAddressExtensions { /// - /// Extension methods for using with . + /// Generates a URI with an absolute path based on the provided values. /// - public static class LinkGeneratorRouteValuesAddressExtensions + /// The . + /// The associated with the current request. + /// The route name. Used to resolve endpoints. Optional. + /// The route values. Used to resolve endpoints and expand parameters in the route template. Optional. + /// + /// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of will be used. + /// + /// An optional URI fragment. Appended to the resulting URI. + /// + /// An optional . Settings on provided object override the settings with matching + /// names from RouteOptions. + /// + /// A URI with an absolute path, or null. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static string? GetPathByRouteValues( + this LinkGenerator generator, + HttpContext httpContext, + string? routeName, + object? values, + PathString? pathBase = default, + FragmentString fragment = default, + LinkOptions? options = default) { - /// - /// Generates a URI with an absolute path based on the provided values. - /// - /// The . - /// The associated with the current request. - /// The route name. Used to resolve endpoints. Optional. - /// The route values. Used to resolve endpoints and expand parameters in the route template. Optional. - /// - /// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of will be used. - /// - /// An optional URI fragment. Appended to the resulting URI. - /// - /// An optional . Settings on provided object override the settings with matching - /// names from RouteOptions. - /// - /// A URI with an absolute path, or null. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static string? GetPathByRouteValues( - this LinkGenerator generator, - HttpContext httpContext, - string? routeName, - object? values, - PathString? pathBase = default, - FragmentString fragment = default, - LinkOptions? options = default) + if (generator == null) { - if (generator == null) - { - throw new ArgumentNullException(nameof(generator)); - } - - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - var address = CreateAddress(httpContext, routeName, values); - return generator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues, - pathBase, - fragment, - options); + throw new ArgumentNullException(nameof(generator)); } - /// - /// Generates a URI with an absolute path based on the provided values. - /// - /// The . - /// The route name. Used to resolve endpoints. Optional. - /// The route values. Used to resolve endpoints and expand parameters in the route template. Optional. - /// An optional URI path base. Prepended to the path in the resulting URI. - /// An optional URI fragment. Appended to the resulting URI. - /// - /// An optional . Settings on provided object override the settings with matching - /// names from RouteOptions. - /// - /// A URI with an absolute path, or null. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static string? GetPathByRouteValues( - this LinkGenerator generator, - string? routeName, - object? values, - PathString pathBase = default, - FragmentString fragment = default, - LinkOptions? options = default) + if (httpContext == null) { - if (generator == null) - { - throw new ArgumentNullException(nameof(generator)); - } - - var address = CreateAddress(httpContext: null, routeName, values); - return generator.GetPathByAddress(address, address.ExplicitValues, pathBase, fragment, options); + throw new ArgumentNullException(nameof(httpContext)); } - /// - /// Generates an absolute URI based on the provided values. - /// - /// The . - /// The associated with the current request. - /// The route name. Used to resolve endpoints. Optional. - /// The route values. Used to resolve endpoints and expand parameters in the route template. Optional. - /// - /// The URI scheme, applied to the resulting URI. Optional. If not provided, the value of will be used. - /// - /// - /// The URI host/authority, applied to the resulting URI. Optional. If not provided, the value will be used. - /// See the remarks section for details about the security implications of the . - /// - /// - /// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of will be used. - /// - /// An optional URI fragment. Appended to the resulting URI. - /// - /// An optional . Settings on provided object override the settings with matching - /// names from RouteOptions. - /// - /// A URI with an absolute path, or null. - /// - /// - /// The value of should be a trusted value. Relying on the value of the current request - /// can allow untrusted input to influence the resulting URI unless the Host header has been validated. - /// See the deployment documentation for instructions on how to properly validate the Host header in - /// your deployment environment. - /// - /// - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static string? GetUriByRouteValues( - this LinkGenerator generator, - HttpContext httpContext, - string? routeName, - object? values, - string? scheme = default, - HostString? host = default, - PathString? pathBase = default, - FragmentString fragment = default, - LinkOptions? options = default) + var address = CreateAddress(httpContext, routeName, values); + return generator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues, + pathBase, + fragment, + options); + } + + /// + /// Generates a URI with an absolute path based on the provided values. + /// + /// The . + /// The route name. Used to resolve endpoints. Optional. + /// The route values. Used to resolve endpoints and expand parameters in the route template. Optional. + /// An optional URI path base. Prepended to the path in the resulting URI. + /// An optional URI fragment. Appended to the resulting URI. + /// + /// An optional . Settings on provided object override the settings with matching + /// names from RouteOptions. + /// + /// A URI with an absolute path, or null. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static string? GetPathByRouteValues( + this LinkGenerator generator, + string? routeName, + object? values, + PathString pathBase = default, + FragmentString fragment = default, + LinkOptions? options = default) + { + if (generator == null) { - if (generator == null) - { - throw new ArgumentNullException(nameof(generator)); - } + throw new ArgumentNullException(nameof(generator)); + } - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } + var address = CreateAddress(httpContext: null, routeName, values); + return generator.GetPathByAddress(address, address.ExplicitValues, pathBase, fragment, options); + } - var address = CreateAddress(httpContext, routeName, values); - return generator.GetUriByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues, - scheme, - host, - pathBase, - fragment, - options); + /// + /// Generates an absolute URI based on the provided values. + /// + /// The . + /// The associated with the current request. + /// The route name. Used to resolve endpoints. Optional. + /// The route values. Used to resolve endpoints and expand parameters in the route template. Optional. + /// + /// The URI scheme, applied to the resulting URI. Optional. If not provided, the value of will be used. + /// + /// + /// The URI host/authority, applied to the resulting URI. Optional. If not provided, the value will be used. + /// See the remarks section for details about the security implications of the . + /// + /// + /// An optional URI path base. Prepended to the path in the resulting URI. If not provided, the value of will be used. + /// + /// An optional URI fragment. Appended to the resulting URI. + /// + /// An optional . Settings on provided object override the settings with matching + /// names from RouteOptions. + /// + /// A URI with an absolute path, or null. + /// + /// + /// The value of should be a trusted value. Relying on the value of the current request + /// can allow untrusted input to influence the resulting URI unless the Host header has been validated. + /// See the deployment documentation for instructions on how to properly validate the Host header in + /// your deployment environment. + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static string? GetUriByRouteValues( + this LinkGenerator generator, + HttpContext httpContext, + string? routeName, + object? values, + string? scheme = default, + HostString? host = default, + PathString? pathBase = default, + FragmentString fragment = default, + LinkOptions? options = default) + { + if (generator == null) + { + throw new ArgumentNullException(nameof(generator)); } - /// - /// Generates an absolute URI based on the provided values. - /// - /// The . - /// The route name. Used to resolve endpoints. Optional. - /// The route values. Used to resolve endpoints and expand parameters in the route template. Optional. - /// The URI scheme, applied to the resulting URI. - /// - /// The URI host/authority, applied to the resulting URI. - /// See the remarks section for details about the security implications of the . - /// - /// An optional URI path base. Prepended to the path in the resulting URI. - /// An optional URI fragment. Appended to the resulting URI. - /// - /// An optional . Settings on provided object override the settings with matching - /// names from RouteOptions. - /// - /// An absolute URI, or null. - /// - /// - /// The value of should be a trusted value. Relying on the value of the current request - /// can allow untrusted input to influence the resulting URI unless the Host header has been validated. - /// See the deployment documentation for instructions on how to properly validate the Host header in - /// your deployment environment. - /// - /// - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public static string? GetUriByRouteValues( - this LinkGenerator generator, - string? routeName, - object? values, - string scheme, - HostString host, - PathString pathBase = default, - FragmentString fragment = default, - LinkOptions? options = default) + if (httpContext == null) { - if (generator == null) - { - throw new ArgumentNullException(nameof(generator)); - } - - var address = CreateAddress(httpContext: null, routeName, values); - return generator.GetUriByAddress(address, address.ExplicitValues, scheme, host, pathBase, fragment, options); + throw new ArgumentNullException(nameof(httpContext)); } - private static RouteValuesAddress CreateAddress(HttpContext? httpContext, string? routeName, object? values) + var address = CreateAddress(httpContext, routeName, values); + return generator.GetUriByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues, + scheme, + host, + pathBase, + fragment, + options); + } + + /// + /// Generates an absolute URI based on the provided values. + /// + /// The . + /// The route name. Used to resolve endpoints. Optional. + /// The route values. Used to resolve endpoints and expand parameters in the route template. Optional. + /// The URI scheme, applied to the resulting URI. + /// + /// The URI host/authority, applied to the resulting URI. + /// See the remarks section for details about the security implications of the . + /// + /// An optional URI path base. Prepended to the path in the resulting URI. + /// An optional URI fragment. Appended to the resulting URI. + /// + /// An optional . Settings on provided object override the settings with matching + /// names from RouteOptions. + /// + /// An absolute URI, or null. + /// + /// + /// The value of should be a trusted value. Relying on the value of the current request + /// can allow untrusted input to influence the resulting URI unless the Host header has been validated. + /// See the deployment documentation for instructions on how to properly validate the Host header in + /// your deployment environment. + /// + /// + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public static string? GetUriByRouteValues( + this LinkGenerator generator, + string? routeName, + object? values, + string scheme, + HostString host, + PathString pathBase = default, + FragmentString fragment = default, + LinkOptions? options = default) + { + if (generator == null) { - return new RouteValuesAddress() - { - AmbientValues = DefaultLinkGenerator.GetAmbientValues(httpContext), - ExplicitValues = new RouteValueDictionary(values), - RouteName = routeName, - }; + throw new ArgumentNullException(nameof(generator)); } + + var address = CreateAddress(httpContext: null, routeName, values); + return generator.GetUriByAddress(address, address.ExplicitValues, scheme, host, pathBase, fragment, options); + } + + private static RouteValuesAddress CreateAddress(HttpContext? httpContext, string? routeName, object? values) + { + return new RouteValuesAddress() + { + AmbientValues = DefaultLinkGenerator.GetAmbientValues(httpContext), + ExplicitValues = new RouteValueDictionary(values), + RouteName = routeName, + }; } } diff --git a/src/Http/Routing/src/LinkParser.cs b/src/Http/Routing/src/LinkParser.cs index a28d9283ea..68edc662a4 100644 --- a/src/Http/Routing/src/LinkParser.cs +++ b/src/Http/Routing/src/LinkParser.cs @@ -3,35 +3,34 @@ using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Defines a contract to parse URIs using information from routing. +/// +public abstract class LinkParser { /// - /// Defines a contract to parse URIs using information from routing. + /// Attempts to parse the provided using the route pattern + /// specified by the matching . /// - public abstract class LinkParser - { - /// - /// Attempts to parse the provided using the route pattern - /// specified by the matching . - /// - /// The address type. - /// The address value. Used to resolve endpoints. - /// The URI path to parse. - /// - /// A with the parsed values if parsing is successful; - /// otherwise null. - /// - /// - /// - /// will attempt to first resolve - /// instances that match and then use the route - /// pattern associated with each endpoint to parse the URL path. - /// - /// - /// The parsing operation will fail and return null if either no endpoints are found or none - /// of the route patterns match the provided URI path. - /// - /// - public abstract RouteValueDictionary? ParsePathByAddress(TAddress address, PathString path); - } + /// The address type. + /// The address value. Used to resolve endpoints. + /// The URI path to parse. + /// + /// A with the parsed values if parsing is successful; + /// otherwise null. + /// + /// + /// + /// will attempt to first resolve + /// instances that match and then use the route + /// pattern associated with each endpoint to parse the URL path. + /// + /// + /// The parsing operation will fail and return null if either no endpoints are found or none + /// of the route patterns match the provided URI path. + /// + /// + public abstract RouteValueDictionary? ParsePathByAddress(TAddress address, PathString path); } diff --git a/src/Http/Routing/src/LinkParserEndpointNameAddressExtensions.cs b/src/Http/Routing/src/LinkParserEndpointNameAddressExtensions.cs index 700961b1e8..2c72c3c38f 100644 --- a/src/Http/Routing/src/LinkParserEndpointNameAddressExtensions.cs +++ b/src/Http/Routing/src/LinkParserEndpointNameAddressExtensions.cs @@ -4,51 +4,50 @@ using System; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Extension methods for using with an endpoint name. +/// +public static class LinkParserEndpointNameAddressExtensions { /// - /// Extension methods for using with an endpoint name. + /// Attempts to parse the provided using the route pattern + /// specified by the matching . /// - public static class LinkParserEndpointNameAddressExtensions + /// The . + /// The endpoint name. Used to resolve endpoints. + /// The URI path to parse. + /// + /// A with the parsed values if parsing is successful; + /// otherwise null. + /// + /// + /// + /// will attempt to first resolve + /// instances that match and then use the route + /// pattern associated with each endpoint to parse the URL path. + /// + /// + /// The parsing operation will fail and return null if either no endpoints are found or none + /// of the route patterns match the provided URI path. + /// + /// + public static RouteValueDictionary? ParsePathByEndpointName( + this LinkParser parser, + string endpointName, + PathString path) { - /// - /// Attempts to parse the provided using the route pattern - /// specified by the matching . - /// - /// The . - /// The endpoint name. Used to resolve endpoints. - /// The URI path to parse. - /// - /// A with the parsed values if parsing is successful; - /// otherwise null. - /// - /// - /// - /// will attempt to first resolve - /// instances that match and then use the route - /// pattern associated with each endpoint to parse the URL path. - /// - /// - /// The parsing operation will fail and return null if either no endpoints are found or none - /// of the route patterns match the provided URI path. - /// - /// - public static RouteValueDictionary? ParsePathByEndpointName( - this LinkParser parser, - string endpointName, - PathString path) + if (parser == null) { - if (parser == null) - { - throw new ArgumentNullException(nameof(parser)); - } - - if (endpointName == null) - { - throw new ArgumentNullException(nameof(endpointName)); - } + throw new ArgumentNullException(nameof(parser)); + } - return parser.ParsePathByAddress(endpointName, path); + if (endpointName == null) + { + throw new ArgumentNullException(nameof(endpointName)); } + + return parser.ParsePathByAddress(endpointName, path); } } diff --git a/src/Http/Routing/src/MapRouteRouteBuilderExtensions.cs b/src/Http/Routing/src/MapRouteRouteBuilderExtensions.cs index 55c32b2b7b..8dd3e1de3b 100644 --- a/src/Http/Routing/src/MapRouteRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/MapRouteRouteBuilderExtensions.cs @@ -8,161 +8,160 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Provides extension methods for to add routes. +/// +public static class MapRouteRouteBuilderExtensions { /// - /// Provides extension methods for to add routes. + /// Adds a route to the with the specified name and template. /// - public static class MapRouteRouteBuilderExtensions + /// The to add the route to. + /// The name of the route. + /// The URL pattern of the route. + /// A reference to this instance after the operation has completed. + public static IRouteBuilder MapRoute( + this IRouteBuilder routeBuilder, + string? name, + string? template) { - /// - /// Adds a route to the with the specified name and template. - /// - /// The to add the route to. - /// The name of the route. - /// The URL pattern of the route. - /// A reference to this instance after the operation has completed. - public static IRouteBuilder MapRoute( - this IRouteBuilder routeBuilder, - string? name, - string? template) - { - MapRoute(routeBuilder, name, template, defaults: null); - return routeBuilder; - } + MapRoute(routeBuilder, name, template, defaults: null); + return routeBuilder; + } - /// - /// Adds a route to the with the specified name, template, and default values. - /// - /// The to add the route to. - /// The name of the route. - /// The URL pattern of the route. - /// - /// An object that contains default values for route parameters. The object's properties represent the names - /// and values of the default values. - /// - /// A reference to this instance after the operation has completed. - public static IRouteBuilder MapRoute( - this IRouteBuilder routeBuilder, - string? name, - string? template, - object? defaults) - { - return MapRoute(routeBuilder, name, template, defaults, constraints: null); - } + /// + /// Adds a route to the with the specified name, template, and default values. + /// + /// The to add the route to. + /// The name of the route. + /// The URL pattern of the route. + /// + /// An object that contains default values for route parameters. The object's properties represent the names + /// and values of the default values. + /// + /// A reference to this instance after the operation has completed. + public static IRouteBuilder MapRoute( + this IRouteBuilder routeBuilder, + string? name, + string? template, + object? defaults) + { + return MapRoute(routeBuilder, name, template, defaults, constraints: null); + } + + /// + /// Adds a route to the with the specified name, template, default values, and + /// constraints. + /// + /// The to add the route to. + /// The name of the route. + /// The URL pattern of the route. + /// + /// An object that contains default values for route parameters. The object's properties represent the names + /// and values of the default values. + /// + /// + /// An object that contains constraints for the route. The object's properties represent the names and values + /// of the constraints. + /// + /// A reference to this instance after the operation has completed. + public static IRouteBuilder MapRoute( + this IRouteBuilder routeBuilder, + string? name, + string? template, + object? defaults, + object? constraints) + { + return MapRoute(routeBuilder, name, template, defaults, constraints, dataTokens: null); + } - /// - /// Adds a route to the with the specified name, template, default values, and - /// constraints. - /// - /// The to add the route to. - /// The name of the route. - /// The URL pattern of the route. - /// - /// An object that contains default values for route parameters. The object's properties represent the names - /// and values of the default values. - /// - /// - /// An object that contains constraints for the route. The object's properties represent the names and values - /// of the constraints. - /// - /// A reference to this instance after the operation has completed. - public static IRouteBuilder MapRoute( - this IRouteBuilder routeBuilder, - string? name, - string? template, - object? defaults, - object? constraints) + /// + /// Adds a route to the with the specified name, template, default values, and + /// data tokens. + /// + /// The to add the route to. + /// The name of the route. + /// The URL pattern of the route. + /// + /// An object that contains default values for route parameters. The object's properties represent the names + /// and values of the default values. + /// + /// + /// An object that contains constraints for the route. The object's properties represent the names and values + /// of the constraints. + /// + /// + /// An object that contains data tokens for the route. The object's properties represent the names and values + /// of the data tokens. + /// + /// A reference to this instance after the operation has completed. + public static IRouteBuilder MapRoute( + this IRouteBuilder routeBuilder, + string? name, + string? template, + object? defaults, + object? constraints, + object? dataTokens) + { + if (routeBuilder.DefaultHandler == null) { - return MapRoute(routeBuilder, name, template, defaults, constraints, dataTokens: null); + throw new RouteCreationException(Resources.FormatDefaultHandler_MustBeSet(nameof(IRouteBuilder))); } - /// - /// Adds a route to the with the specified name, template, default values, and - /// data tokens. - /// - /// The to add the route to. - /// The name of the route. - /// The URL pattern of the route. - /// - /// An object that contains default values for route parameters. The object's properties represent the names - /// and values of the default values. - /// - /// - /// An object that contains constraints for the route. The object's properties represent the names and values - /// of the constraints. - /// - /// - /// An object that contains data tokens for the route. The object's properties represent the names and values - /// of the data tokens. - /// - /// A reference to this instance after the operation has completed. - public static IRouteBuilder MapRoute( - this IRouteBuilder routeBuilder, - string? name, - string? template, - object? defaults, - object? constraints, - object? dataTokens) - { - if (routeBuilder.DefaultHandler == null) - { - throw new RouteCreationException(Resources.FormatDefaultHandler_MustBeSet(nameof(IRouteBuilder))); - } + routeBuilder.Routes.Add(new Route( + routeBuilder.DefaultHandler, + name, + template, + new RouteValueDictionary(defaults), + new RouteValueDictionary(constraints)!, + new RouteValueDictionary(dataTokens), + CreateInlineConstraintResolver(routeBuilder.ServiceProvider))); - routeBuilder.Routes.Add(new Route( - routeBuilder.DefaultHandler, - name, - template, - new RouteValueDictionary(defaults), - new RouteValueDictionary(constraints)!, - new RouteValueDictionary(dataTokens), - CreateInlineConstraintResolver(routeBuilder.ServiceProvider))); + return routeBuilder; + } - return routeBuilder; - } + private static IInlineConstraintResolver CreateInlineConstraintResolver(IServiceProvider serviceProvider) + { + var inlineConstraintResolver = serviceProvider + .GetRequiredService(); - private static IInlineConstraintResolver CreateInlineConstraintResolver(IServiceProvider serviceProvider) - { - var inlineConstraintResolver = serviceProvider - .GetRequiredService(); + var parameterPolicyFactory = serviceProvider + .GetRequiredService(); - var parameterPolicyFactory = serviceProvider - .GetRequiredService(); + // This inline constraint resolver will return a null constraint for non-IRouteConstraint + // parameter policies so Route does not error + return new BackCompatInlineConstraintResolver(inlineConstraintResolver, parameterPolicyFactory); + } - // This inline constraint resolver will return a null constraint for non-IRouteConstraint - // parameter policies so Route does not error - return new BackCompatInlineConstraintResolver(inlineConstraintResolver, parameterPolicyFactory); - } + private class BackCompatInlineConstraintResolver : IInlineConstraintResolver + { + private readonly IInlineConstraintResolver _inner; + private readonly ParameterPolicyFactory _parameterPolicyFactory; - private class BackCompatInlineConstraintResolver : IInlineConstraintResolver + public BackCompatInlineConstraintResolver(IInlineConstraintResolver inner, ParameterPolicyFactory parameterPolicyFactory) { - private readonly IInlineConstraintResolver _inner; - private readonly ParameterPolicyFactory _parameterPolicyFactory; + _inner = inner; + _parameterPolicyFactory = parameterPolicyFactory; + } - public BackCompatInlineConstraintResolver(IInlineConstraintResolver inner, ParameterPolicyFactory parameterPolicyFactory) + public IRouteConstraint? ResolveConstraint(string inlineConstraint) + { + var routeConstraint = _inner.ResolveConstraint(inlineConstraint); + if (routeConstraint != null) { - _inner = inner; - _parameterPolicyFactory = parameterPolicyFactory; + return routeConstraint; } - public IRouteConstraint? ResolveConstraint(string inlineConstraint) + var parameterPolicy = _parameterPolicyFactory.Create(null!, inlineConstraint); + if (parameterPolicy != null) { - var routeConstraint = _inner.ResolveConstraint(inlineConstraint); - if (routeConstraint != null) - { - return routeConstraint; - } - - var parameterPolicy = _parameterPolicyFactory.Create(null!, inlineConstraint); - if (parameterPolicy != null) - { - // Logic inside Route will skip adding NullRouteConstraint - return NullRouteConstraint.Instance; - } - - return null; + // Logic inside Route will skip adding NullRouteConstraint + return NullRouteConstraint.Instance; } + + return null; } } } diff --git a/src/Http/Routing/src/Matching/AmbiguousMatchException.cs b/src/Http/Routing/src/Matching/AmbiguousMatchException.cs index 10d02711e6..8acbccc0f2 100644 --- a/src/Http/Routing/src/Matching/AmbiguousMatchException.cs +++ b/src/Http/Routing/src/Matching/AmbiguousMatchException.cs @@ -4,22 +4,21 @@ using System; using System.Runtime.Serialization; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +/// +/// An exception which indicates multiple matches in endpoint selection. +/// +[Serializable] +internal class AmbiguousMatchException : Exception { - /// - /// An exception which indicates multiple matches in endpoint selection. - /// - [Serializable] - internal class AmbiguousMatchException : Exception + public AmbiguousMatchException(string message) + : base(message) { - public AmbiguousMatchException(string message) - : base(message) - { - } + } - protected AmbiguousMatchException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } + protected AmbiguousMatchException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } -} \ No newline at end of file +} diff --git a/src/Http/Routing/src/Matching/Ascii.cs b/src/Http/Routing/src/Matching/Ascii.cs index 1820a3ce3b..69cb1e64e1 100644 --- a/src/Http/Routing/src/Matching/Ascii.cs +++ b/src/Http/Routing/src/Matching/Ascii.cs @@ -5,71 +5,70 @@ using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal static class Ascii { - internal static class Ascii + // case-sensitive equality comparison when we KNOW that 'a' is in the ASCII range + // and we know that the spans are the same length. + // + // Similar to https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Globalization/CompareInfo.cs#L549 + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AsciiIgnoreCaseEquals(ReadOnlySpan a, ReadOnlySpan b, int length) { - // case-sensitive equality comparison when we KNOW that 'a' is in the ASCII range - // and we know that the spans are the same length. - // - // Similar to https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Globalization/CompareInfo.cs#L549 - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool AsciiIgnoreCaseEquals(ReadOnlySpan a, ReadOnlySpan b, int length) + // The caller should have checked the length. We enforce that here by THROWING if the + // lengths are unequal. + if (a.Length < length || b.Length < length) { - // The caller should have checked the length. We enforce that here by THROWING if the - // lengths are unequal. - if (a.Length < length || b.Length < length) - { - // This should never happen, but we don't want to have undefined - // behavior if it does. - ThrowArgumentExceptionForLength(); - } + // This should never happen, but we don't want to have undefined + // behavior if it does. + ThrowArgumentExceptionForLength(); + } - ref var charA = ref MemoryMarshal.GetReference(a); - ref var charB = ref MemoryMarshal.GetReference(b); + ref var charA = ref MemoryMarshal.GetReference(a); + ref var charB = ref MemoryMarshal.GetReference(b); - // Iterates each span for the provided length and compares each character - // case-insensitively. This looks funky because we're using unsafe operations - // to elide bounds-checks. - while (length > 0 && AsciiIgnoreCaseEquals(charA, charB)) - { - charA = ref Unsafe.Add(ref charA, 1); - charB = ref Unsafe.Add(ref charB, 1); - length--; - } - - return length == 0; + // Iterates each span for the provided length and compares each character + // case-insensitively. This looks funky because we're using unsafe operations + // to elide bounds-checks. + while (length > 0 && AsciiIgnoreCaseEquals(charA, charB)) + { + charA = ref Unsafe.Add(ref charA, 1); + charB = ref Unsafe.Add(ref charB, 1); + length--; } - // case-insensitive equality comparison for characters in the ASCII range - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool AsciiIgnoreCaseEquals(char charA, char charB) - { - const uint AsciiToLower = 0x20; - return - // Equal when chars are exactly equal - charA == charB || + return length == 0; + } - // Equal when converted to-lower AND they are letters - ((charA | AsciiToLower) == (charB | AsciiToLower) && (uint)((charA | AsciiToLower) - 'a') <= (uint)('z' - 'a')); - } + // case-insensitive equality comparison for characters in the ASCII range + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool AsciiIgnoreCaseEquals(char charA, char charB) + { + const uint AsciiToLower = 0x20; + return + // Equal when chars are exactly equal + charA == charB || + + // Equal when converted to-lower AND they are letters + ((charA | AsciiToLower) == (charB | AsciiToLower) && (uint)((charA | AsciiToLower) - 'a') <= (uint)('z' - 'a')); + } - public static bool IsAscii(string text) + public static bool IsAscii(string text) + { + for (var i = 0; i < text.Length; i++) { - for (var i = 0; i < text.Length; i++) + if (text[i] > (char)0x7F) { - if (text[i] > (char)0x7F) - { - return false; - } + return false; } - - return true; } - private static void ThrowArgumentExceptionForLength() - { - throw new ArgumentException("length"); - } + return true; + } + + private static void ThrowArgumentExceptionForLength() + { + throw new ArgumentException("length"); } } diff --git a/src/Http/Routing/src/Matching/Candidate.cs b/src/Http/Routing/src/Matching/Candidate.cs index ab97dc6e0a..e906118104 100644 --- a/src/Http/Routing/src/Matching/Candidate.cs +++ b/src/Http/Routing/src/Matching/Candidate.cs @@ -6,120 +6,119 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Patterns; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal readonly struct Candidate { - internal readonly struct Candidate + public readonly Endpoint Endpoint; + + // Used to optimize out operations that modify route values. + public readonly CandidateFlags Flags; + + // Data for creating the RouteValueDictionary. We assign each key its own slot + // and we fill the values array with all of the default values. + // + // Then when we process parameters, we don't need to operate on the RouteValueDictionary + // we can just operate on an array, which is much much faster. + public readonly KeyValuePair[] Slots; + + // List of parameters to capture. Segment is the segment index, index is the + // index into the values array. + public readonly (string parameterName, int segmentIndex, int slotIndex)[] Captures; + + // Catchall parameter to capture (limit one per template). + public readonly (string parameterName, int segmentIndex, int slotIndex) CatchAll; + + // Complex segments are processed in a separate pass because they require a + // RouteValueDictionary. + public readonly (RoutePatternPathSegment pathSegment, int segmentIndex)[] ComplexSegments; + + public readonly KeyValuePair[] Constraints; + + // Score is a sequential integer value that in determines the priority of an Endpoint. + // Scores are computed within the context of candidate set, and are meaningless when + // applied to endpoints not in the set. + // + // The score concept boils down the system of comparisons done when ordering Endpoints + // to a single value that can be compared easily. This can be defeated by having + // int32.MaxValue + 1 endpoints in a single set, but you would have other problems by + // that point. + // + // Score is not part of the Endpoint itself, because it's contextual based on where + // the endpoint appears. An Endpoint is often be a member of multiple candidate sets. + public readonly int Score; + + // Used in tests. + public Candidate(Endpoint endpoint) { - public readonly Endpoint Endpoint; - - // Used to optimize out operations that modify route values. - public readonly CandidateFlags Flags; - - // Data for creating the RouteValueDictionary. We assign each key its own slot - // and we fill the values array with all of the default values. - // - // Then when we process parameters, we don't need to operate on the RouteValueDictionary - // we can just operate on an array, which is much much faster. - public readonly KeyValuePair[] Slots; - - // List of parameters to capture. Segment is the segment index, index is the - // index into the values array. - public readonly (string parameterName, int segmentIndex, int slotIndex)[] Captures; - - // Catchall parameter to capture (limit one per template). - public readonly (string parameterName, int segmentIndex, int slotIndex) CatchAll; - - // Complex segments are processed in a separate pass because they require a - // RouteValueDictionary. - public readonly (RoutePatternPathSegment pathSegment, int segmentIndex)[] ComplexSegments; - - public readonly KeyValuePair[] Constraints; - - // Score is a sequential integer value that in determines the priority of an Endpoint. - // Scores are computed within the context of candidate set, and are meaningless when - // applied to endpoints not in the set. - // - // The score concept boils down the system of comparisons done when ordering Endpoints - // to a single value that can be compared easily. This can be defeated by having - // int32.MaxValue + 1 endpoints in a single set, but you would have other problems by - // that point. - // - // Score is not part of the Endpoint itself, because it's contextual based on where - // the endpoint appears. An Endpoint is often be a member of multiple candidate sets. - public readonly int Score; - - // Used in tests. - public Candidate(Endpoint endpoint) - { - Endpoint = endpoint; + Endpoint = endpoint; - Slots = Array.Empty>(); - Captures = Array.Empty<(string parameterName, int segmentIndex, int slotIndex)>(); - CatchAll = default; - ComplexSegments = Array.Empty<(RoutePatternPathSegment pathSegment, int segmentIndex)>(); - Constraints = Array.Empty>(); - Score = 0; + Slots = Array.Empty>(); + Captures = Array.Empty<(string parameterName, int segmentIndex, int slotIndex)>(); + CatchAll = default; + ComplexSegments = Array.Empty<(RoutePatternPathSegment pathSegment, int segmentIndex)>(); + Constraints = Array.Empty>(); + Score = 0; - Flags = CandidateFlags.None; - } + Flags = CandidateFlags.None; + } - public Candidate( - Endpoint endpoint, - int score, - KeyValuePair[] slots, - (string parameterName, int segmentIndex, int slotIndex)[] captures, - in (string parameterName, int segmentIndex, int slotIndex) catchAll, - (RoutePatternPathSegment pathSegment, int segmentIndex)[] complexSegments, - KeyValuePair[] constraints) + public Candidate( + Endpoint endpoint, + int score, + KeyValuePair[] slots, + (string parameterName, int segmentIndex, int slotIndex)[] captures, + in (string parameterName, int segmentIndex, int slotIndex) catchAll, + (RoutePatternPathSegment pathSegment, int segmentIndex)[] complexSegments, + KeyValuePair[] constraints) + { + Endpoint = endpoint; + Score = score; + Slots = slots; + Captures = captures; + CatchAll = catchAll; + ComplexSegments = complexSegments; + Constraints = constraints; + + Flags = CandidateFlags.None; + for (var i = 0; i < slots.Length; i++) { - Endpoint = endpoint; - Score = score; - Slots = slots; - Captures = captures; - CatchAll = catchAll; - ComplexSegments = complexSegments; - Constraints = constraints; - - Flags = CandidateFlags.None; - for (var i = 0; i < slots.Length; i++) - { - if (slots[i].Key != null) - { - Flags |= CandidateFlags.HasDefaults; - } - } - - if (captures.Length > 0) + if (slots[i].Key != null) { - Flags |= CandidateFlags.HasCaptures; + Flags |= CandidateFlags.HasDefaults; } + } - if (catchAll.parameterName != null) - { - Flags |= CandidateFlags.HasCatchAll; - } + if (captures.Length > 0) + { + Flags |= CandidateFlags.HasCaptures; + } - if (complexSegments.Length > 0) - { - Flags |= CandidateFlags.HasComplexSegments; - } + if (catchAll.parameterName != null) + { + Flags |= CandidateFlags.HasCatchAll; + } - if (constraints.Length > 0) - { - Flags |= CandidateFlags.HasConstraints; - } + if (complexSegments.Length > 0) + { + Flags |= CandidateFlags.HasComplexSegments; } - [Flags] - public enum CandidateFlags + if (constraints.Length > 0) { - None = 0, - HasDefaults = 1, - HasCaptures = 2, - HasCatchAll = 4, - HasSlots = HasDefaults | HasCaptures | HasCatchAll, - HasComplexSegments = 8, - HasConstraints = 16, + Flags |= CandidateFlags.HasConstraints; } } + + [Flags] + public enum CandidateFlags + { + None = 0, + HasDefaults = 1, + HasCaptures = 2, + HasCatchAll = 4, + HasSlots = HasDefaults | HasCaptures | HasCatchAll, + HasComplexSegments = 8, + HasConstraints = 16, + } } diff --git a/src/Http/Routing/src/Matching/CandidateSet.cs b/src/Http/Routing/src/Matching/CandidateSet.cs index 9a7df16550..188e63329a 100644 --- a/src/Http/Routing/src/Matching/CandidateSet.cs +++ b/src/Http/Routing/src/Matching/CandidateSet.cs @@ -12,116 +12,95 @@ using System.Linq; using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +/// +/// Represents a set of candidates that have been matched +/// by the routing system. Used by implementations of +/// and . +/// +public sealed class CandidateSet { + internal CandidateState[] Candidates; + /// - /// Represents a set of candidates that have been matched - /// by the routing system. Used by implementations of + /// + /// Initializes a new instances of the class with the provided , + /// , and . + /// + /// + /// The constructor is provided to enable unit tests of implementations of /// and . + /// /// - public sealed class CandidateSet + /// The list of endpoints, sorted in descending priority order. + /// The list of instances. + /// The list of endpoint scores. . + public CandidateSet(Endpoint[] endpoints, RouteValueDictionary[] values, int[] scores) { - internal CandidateState[] Candidates; - - /// - /// - /// Initializes a new instances of the class with the provided , - /// , and . - /// - /// - /// The constructor is provided to enable unit tests of implementations of - /// and . - /// - /// - /// The list of endpoints, sorted in descending priority order. - /// The list of instances. - /// The list of endpoint scores. . - public CandidateSet(Endpoint[] endpoints, RouteValueDictionary[] values, int[] scores) + if (endpoints == null) { - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } - - if (values == null) - { - throw new ArgumentNullException(nameof(values)); - } - - if (scores == null) - { - throw new ArgumentNullException(nameof(scores)); - } + throw new ArgumentNullException(nameof(endpoints)); + } - if (endpoints.Length != values.Length || endpoints.Length != scores.Length) - { - throw new ArgumentException($"The provided {nameof(endpoints)}, {nameof(values)}, and {nameof(scores)} must have the same length."); - } + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } - Candidates = new CandidateState[endpoints.Length]; - for (var i = 0; i < endpoints.Length; i++) - { - Candidates[i] = new CandidateState(endpoints[i], values[i], scores[i]); - } + if (scores == null) + { + throw new ArgumentNullException(nameof(scores)); } - // Used in tests. - internal CandidateSet(Candidate[] candidates) + if (endpoints.Length != values.Length || endpoints.Length != scores.Length) { - Candidates = new CandidateState[candidates.Length]; - for (var i = 0; i < candidates.Length; i++) - { - Candidates[i] = new CandidateState(candidates[i].Endpoint, candidates[i].Score); - } + throw new ArgumentException($"The provided {nameof(endpoints)}, {nameof(values)}, and {nameof(scores)} must have the same length."); } - internal CandidateSet(CandidateState[] candidates) + Candidates = new CandidateState[endpoints.Length]; + for (var i = 0; i < endpoints.Length; i++) { - Candidates = candidates; + Candidates[i] = new CandidateState(endpoints[i], values[i], scores[i]); } + } - /// - /// Gets the count of candidates in the set. - /// - public int Count => Candidates.Length; - - /// - /// Gets the associated with the candidate - /// at . - /// - /// The candidate index. - /// - /// A reference to the . The result is returned by reference. - /// - public ref CandidateState this[int index] + // Used in tests. + internal CandidateSet(Candidate[] candidates) + { + Candidates = new CandidateState[candidates.Length]; + for (var i = 0; i < candidates.Length; i++) { - // Note that this is a ref-return because of performance. - // We don't want to copy these fat structs if it can be avoided. + Candidates[i] = new CandidateState(candidates[i].Endpoint, candidates[i].Score); + } + } - // PERF: Force inlining - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - // Friendliness for inlining - if ((uint)index >= Count) - { - ThrowIndexArgumentOutOfRangeException(); - } + internal CandidateSet(CandidateState[] candidates) + { + Candidates = candidates; + } - return ref Candidates[index]; - } - } + /// + /// Gets the count of candidates in the set. + /// + public int Count => Candidates.Length; + + /// + /// Gets the associated with the candidate + /// at . + /// + /// The candidate index. + /// + /// A reference to the . The result is returned by reference. + /// + public ref CandidateState this[int index] + { + // Note that this is a ref-return because of performance. + // We don't want to copy these fat structs if it can be avoided. - /// - /// Gets a value which indicates where the is considered - /// a valid candidate for the current request. - /// - /// The candidate index. - /// - /// true if the candidate at position is considered valid - /// for the current request, otherwise false. - /// - public bool IsValidCandidate(int index) + // PERF: Force inlining + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { // Friendliness for inlining if ((uint)index >= Count) @@ -129,256 +108,276 @@ namespace Microsoft.AspNetCore.Routing.Matching ThrowIndexArgumentOutOfRangeException(); } - return IsValidCandidate(ref Candidates[index]); + return ref Candidates[index]; } + } - internal static bool IsValidCandidate(ref CandidateState candidate) + /// + /// Gets a value which indicates where the is considered + /// a valid candidate for the current request. + /// + /// The candidate index. + /// + /// true if the candidate at position is considered valid + /// for the current request, otherwise false. + /// + public bool IsValidCandidate(int index) + { + // Friendliness for inlining + if ((uint)index >= Count) { - return candidate.Score >= 0; + ThrowIndexArgumentOutOfRangeException(); } - /// - /// Sets the validity of the candidate at the provided index. - /// - /// The candidate index. - /// - /// The value to set. If true the candidate is considered valid for the current request. - /// - public void SetValidity(int index, bool value) - { - // Friendliness for inlining - if ((uint)index >= Count) - { - ThrowIndexArgumentOutOfRangeException(); - } + return IsValidCandidate(ref Candidates[index]); + } - ref var original = ref Candidates[index]; - SetValidity(ref original, value); - } + internal static bool IsValidCandidate(ref CandidateState candidate) + { + return candidate.Score >= 0; + } - internal static void SetValidity(ref CandidateState candidate, bool value) + /// + /// Sets the validity of the candidate at the provided index. + /// + /// The candidate index. + /// + /// The value to set. If true the candidate is considered valid for the current request. + /// + public void SetValidity(int index, bool value) + { + // Friendliness for inlining + if ((uint)index >= Count) { - var originalScore = candidate.Score; - var score = originalScore >= 0 ^ value ? ~originalScore : originalScore; - candidate = new CandidateState(candidate.Endpoint, candidate.Values, score); + ThrowIndexArgumentOutOfRangeException(); } - /// - /// Replaces the at the provided with the - /// provided . - /// - /// The candidate index. - /// - /// The to replace the original at - /// the . If is null. the candidate will be marked - /// as invalid. - /// - /// - /// The to replace the original at - /// the . - /// - public void ReplaceEndpoint(int index, Endpoint? endpoint, RouteValueDictionary? values) + ref var original = ref Candidates[index]; + SetValidity(ref original, value); + } + + internal static void SetValidity(ref CandidateState candidate, bool value) + { + var originalScore = candidate.Score; + var score = originalScore >= 0 ^ value ? ~originalScore : originalScore; + candidate = new CandidateState(candidate.Endpoint, candidate.Values, score); + } + + /// + /// Replaces the at the provided with the + /// provided . + /// + /// The candidate index. + /// + /// The to replace the original at + /// the . If is null. the candidate will be marked + /// as invalid. + /// + /// + /// The to replace the original at + /// the . + /// + public void ReplaceEndpoint(int index, Endpoint? endpoint, RouteValueDictionary? values) + { + // Friendliness for inlining + if ((uint)index >= Count) { - // Friendliness for inlining - if ((uint)index >= Count) - { - ThrowIndexArgumentOutOfRangeException(); - } + ThrowIndexArgumentOutOfRangeException(); + } - // CandidateState allows a null-valued endpoint. However a validate candidate should never have a null endpoint - // We'll make lives easier for matcher policies by declaring it as non-null. - Candidates[index] = new CandidateState(endpoint!, values, Candidates[index].Score); + // CandidateState allows a null-valued endpoint. However a validate candidate should never have a null endpoint + // We'll make lives easier for matcher policies by declaring it as non-null. + Candidates[index] = new CandidateState(endpoint!, values, Candidates[index].Score); - if (endpoint == null) - { - SetValidity(index, false); - } + if (endpoint == null) + { + SetValidity(index, false); } + } - /// - /// Replaces the at the provided with the - /// provided . - /// - /// The candidate index. - /// - /// The list of endpoints to replace the original at - /// the . If is empty, the candidate will be marked - /// as invalid. - /// - /// - /// The endpoint comparer used to order the endpoints. Can be retrieved from the service provider as - /// type . - /// - /// - /// - /// This method supports replacing a dynamic endpoint with a collection of endpoints, and relying on - /// implementations to disambiguate further. - /// - /// - /// The endpoint being replace should have a unique score value. The score is the combination of route - /// patter precedence, order, and policy metadata evaluation. A dynamic endpoint will not function - /// correctly if other endpoints exist with the same score. - /// - /// - public void ExpandEndpoint(int index, IReadOnlyList endpoints, IComparer comparer) + /// + /// Replaces the at the provided with the + /// provided . + /// + /// The candidate index. + /// + /// The list of endpoints to replace the original at + /// the . If is empty, the candidate will be marked + /// as invalid. + /// + /// + /// The endpoint comparer used to order the endpoints. Can be retrieved from the service provider as + /// type . + /// + /// + /// + /// This method supports replacing a dynamic endpoint with a collection of endpoints, and relying on + /// implementations to disambiguate further. + /// + /// + /// The endpoint being replace should have a unique score value. The score is the combination of route + /// patter precedence, order, and policy metadata evaluation. A dynamic endpoint will not function + /// correctly if other endpoints exist with the same score. + /// + /// + public void ExpandEndpoint(int index, IReadOnlyList endpoints, IComparer comparer) + { + // Friendliness for inlining + if ((uint)index >= Count) { - // Friendliness for inlining - if ((uint)index >= Count) - { - ThrowIndexArgumentOutOfRangeException(); - } + ThrowIndexArgumentOutOfRangeException(); + } - if (endpoints == null) - { - ThrowArgumentNullException(nameof(endpoints)); - } + if (endpoints == null) + { + ThrowArgumentNullException(nameof(endpoints)); + } - if (comparer == null) - { - ThrowArgumentNullException(nameof(comparer)); - } + if (comparer == null) + { + ThrowArgumentNullException(nameof(comparer)); + } - // First we need to verify that the score of what we're replacing is unique. - ValidateUniqueScore(index); + // First we need to verify that the score of what we're replacing is unique. + ValidateUniqueScore(index); - switch (endpoints.Count) - { - case 0: - ReplaceEndpoint(index, null, null); - break; - - case 1: - ReplaceEndpoint(index, endpoints[0], Candidates[index].Values); - break; - - default: - - var score = GetOriginalScore(index); - var values = Candidates[index].Values; - - // Adding candidates requires expanding the array and computing new score values for the new candidates. - var original = Candidates; - var candidates = new CandidateState[original.Length - 1 + endpoints.Count]; - Candidates = candidates; - - // Since the new endpoints have an unknown ordering relationship to each other, we need to: - // - order them - // - assign scores - // - offset everything that comes after - // - // If the inputs look like: - // - // score 0: A1 - // score 0: A2 - // score 1: B - // score 2: C <-- being expanded - // score 3: D - // - // Then the result should look like: - // - // score 0: A1 - // score 0: A2 - // score 1: B - // score 2: `C1 - // score 3: `C2 - // score 4: D - - // Candidates before index can be copied unchanged. - for (var i = 0; i < index; i++) - { - candidates[i] = original[i]; - } + switch (endpoints.Count) + { + case 0: + ReplaceEndpoint(index, null, null); + break; + + case 1: + ReplaceEndpoint(index, endpoints[0], Candidates[index].Values); + break; + + default: + + var score = GetOriginalScore(index); + var values = Candidates[index].Values; + + // Adding candidates requires expanding the array and computing new score values for the new candidates. + var original = Candidates; + var candidates = new CandidateState[original.Length - 1 + endpoints.Count]; + Candidates = candidates; + + // Since the new endpoints have an unknown ordering relationship to each other, we need to: + // - order them + // - assign scores + // - offset everything that comes after + // + // If the inputs look like: + // + // score 0: A1 + // score 0: A2 + // score 1: B + // score 2: C <-- being expanded + // score 3: D + // + // Then the result should look like: + // + // score 0: A1 + // score 0: A2 + // score 1: B + // score 2: `C1 + // score 3: `C2 + // score 4: D + + // Candidates before index can be copied unchanged. + for (var i = 0; i < index; i++) + { + candidates[i] = original[i]; + } + + var buffer = endpoints.ToArray(); + Array.Sort(buffer, comparer); - var buffer = endpoints.ToArray(); - Array.Sort(buffer, comparer); + // Add the first new endpoint with the current score + candidates[index] = new CandidateState(buffer[0], values, score); - // Add the first new endpoint with the current score - candidates[index] = new CandidateState(buffer[0], values, score); + var scoreOffset = 0; + for (var i = 1; i < buffer.Length; i++) + { + var cmp = comparer.Compare(buffer[i - 1], buffer[i]); - var scoreOffset = 0; - for (var i = 1; i < buffer.Length; i++) + // This should not be possible. This would mean that sorting is wrong. + Debug.Assert(cmp <= 0); + if (cmp == 0) { - var cmp = comparer.Compare(buffer[i - 1], buffer[i]); - - // This should not be possible. This would mean that sorting is wrong. - Debug.Assert(cmp <= 0); - if (cmp == 0) - { - // Score is unchanged. - } - else if (cmp < 0) - { - // Endpoint is lower priority, higher score. - scoreOffset++; - } - - Candidates[i + index] = new CandidateState(buffer[i], values, score + scoreOffset); + // Score is unchanged. } - - for (var i = index + 1; i < original.Length; i++) + else if (cmp < 0) { - Candidates[i + endpoints.Count - 1] = new CandidateState(original[i].Endpoint, original[i].Values, original[i].Score + scoreOffset); + // Endpoint is lower priority, higher score. + scoreOffset++; } - break; + Candidates[i + index] = new CandidateState(buffer[i], values, score + scoreOffset); + } + + for (var i = index + 1; i < original.Length; i++) + { + Candidates[i + endpoints.Count - 1] = new CandidateState(original[i].Endpoint, original[i].Values, original[i].Score + scoreOffset); + } + + break; - } } + } + + // Returns the *positive* score value. Score is used to track valid/invalid which can cause it to be negative. + // + // This is the original score and used to determine if there are ambiguities. + private int GetOriginalScore(int index) + { + var score = Candidates[index].Score; + return score >= 0 ? score : ~score; + } - // Returns the *positive* score value. Score is used to track valid/invalid which can cause it to be negative. - // - // This is the original score and used to determine if there are ambiguities. - private int GetOriginalScore(int index) + private void ValidateUniqueScore(int index) + { + var score = GetOriginalScore(index); + + var count = 0; + var candidates = Candidates; + for (var i = 0; i < candidates.Length; i++) { - var score = Candidates[index].Score; - return score >= 0 ? score : ~score; + if (GetOriginalScore(i) == score) + { + count++; + } } - private void ValidateUniqueScore(int index) + Debug.Assert(count > 0); + if (count > 1) { - var score = GetOriginalScore(index); - - var count = 0; - var candidates = Candidates; + // Uh-oh. We don't allow duplicates with ExpandEndpoint because that will do unpredictable things. + var duplicates = new List(); for (var i = 0; i < candidates.Length; i++) { if (GetOriginalScore(i) == score) { - count++; + duplicates.Add(candidates[i].Endpoint!); } } - Debug.Assert(count > 0); - if (count > 1) - { - // Uh-oh. We don't allow duplicates with ExpandEndpoint because that will do unpredictable things. - var duplicates = new List(); - for (var i = 0; i < candidates.Length; i++) - { - if (GetOriginalScore(i) == score) - { - duplicates.Add(candidates[i].Endpoint!); - } - } - - var message = - $"Using {nameof(ExpandEndpoint)} requires that the replaced endpoint have a unique priority. " + - $"The following endpoints were found with the same priority:" + Environment.NewLine + - string.Join(Environment.NewLine, duplicates.Select(e => e.DisplayName)); - throw new InvalidOperationException(message); - } + var message = + $"Using {nameof(ExpandEndpoint)} requires that the replaced endpoint have a unique priority. " + + $"The following endpoints were found with the same priority:" + Environment.NewLine + + string.Join(Environment.NewLine, duplicates.Select(e => e.DisplayName)); + throw new InvalidOperationException(message); } + } - [DoesNotReturn] - private static void ThrowIndexArgumentOutOfRangeException() - { - throw new ArgumentOutOfRangeException("index"); - } + [DoesNotReturn] + private static void ThrowIndexArgumentOutOfRangeException() + { + throw new ArgumentOutOfRangeException("index"); + } - [DoesNotReturn] - private static void ThrowArgumentNullException(string parameter) - { - throw new ArgumentNullException(parameter); - } + [DoesNotReturn] + private static void ThrowArgumentNullException(string parameter) + { + throw new ArgumentNullException(parameter); } } diff --git a/src/Http/Routing/src/Matching/CandidateState.cs b/src/Http/Routing/src/Matching/CandidateState.cs index 1c9b0f3128..8eae3a22c3 100644 --- a/src/Http/Routing/src/Matching/CandidateState.cs +++ b/src/Http/Routing/src/Matching/CandidateState.cs @@ -3,53 +3,52 @@ using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +/// +/// The state associated with a candidate in a . +/// +public struct CandidateState { - /// - /// The state associated with a candidate in a . - /// - public struct CandidateState + internal CandidateState(Endpoint endpoint, int score) { - internal CandidateState(Endpoint endpoint, int score) - { - Endpoint = endpoint; - Score = score; - Values = null; - } + Endpoint = endpoint; + Score = score; + Values = null; + } - internal CandidateState(Endpoint endpoint, RouteValueDictionary? values, int score) - { - Endpoint = endpoint; - Values = values; - Score = score; - } + internal CandidateState(Endpoint endpoint, RouteValueDictionary? values, int score) + { + Endpoint = endpoint; + Values = values; + Score = score; + } - /// - /// Gets the . - /// - public Endpoint Endpoint { get; } + /// + /// Gets the . + /// + public Endpoint Endpoint { get; } - /// - /// Gets the score of the within the current - /// . - /// - /// - /// - /// Candidates within a set are ordered in priority order and then assigned a - /// sequential score value based on that ordering. Candiates with the same - /// score are considered to have equal priority. - /// - /// - /// The score values are used in the to determine - /// whether a set of matching candidates is an ambiguous match. - /// - /// - public int Score { get; } + /// + /// Gets the score of the within the current + /// . + /// + /// + /// + /// Candidates within a set are ordered in priority order and then assigned a + /// sequential score value based on that ordering. Candiates with the same + /// score are considered to have equal priority. + /// + /// + /// The score values are used in the to determine + /// whether a set of matching candidates is an ambiguous match. + /// + /// + public int Score { get; } - /// - /// Gets associated with the - /// and the current request. - /// - public RouteValueDictionary? Values { get; internal set; } - } + /// + /// Gets associated with the + /// and the current request. + /// + public RouteValueDictionary? Values { get; internal set; } } diff --git a/src/Http/Routing/src/Matching/DataSourceDependentMatcher.cs b/src/Http/Routing/src/Matching/DataSourceDependentMatcher.cs index 2906028a02..cc01324dea 100644 --- a/src/Http/Routing/src/Matching/DataSourceDependentMatcher.cs +++ b/src/Http/Routing/src/Matching/DataSourceDependentMatcher.cs @@ -8,105 +8,104 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal sealed class DataSourceDependentMatcher : Matcher { - internal sealed class DataSourceDependentMatcher : Matcher - { - private readonly Func _matcherBuilderFactory; - private readonly DataSourceDependentCache _cache; + private readonly Func _matcherBuilderFactory; + private readonly DataSourceDependentCache _cache; - public DataSourceDependentMatcher( - EndpointDataSource dataSource, - Lifetime lifetime, - Func matcherBuilderFactory) - { - _matcherBuilderFactory = matcherBuilderFactory; + public DataSourceDependentMatcher( + EndpointDataSource dataSource, + Lifetime lifetime, + Func matcherBuilderFactory) + { + _matcherBuilderFactory = matcherBuilderFactory; - _cache = new DataSourceDependentCache(dataSource, CreateMatcher); - _cache.EnsureInitialized(); + _cache = new DataSourceDependentCache(dataSource, CreateMatcher); + _cache.EnsureInitialized(); - // This will Dispose the cache when the lifetime is disposed, this allows - // the service provider to manage the lifetime of the cache. - lifetime.Cache = _cache; - } + // This will Dispose the cache when the lifetime is disposed, this allows + // the service provider to manage the lifetime of the cache. + lifetime.Cache = _cache; + } - // Used in tests - internal Matcher CurrentMatcher => _cache.Value!; + // Used in tests + internal Matcher CurrentMatcher => _cache.Value!; - public override Task MatchAsync(HttpContext httpContext) - { - return CurrentMatcher.MatchAsync(httpContext); - } + public override Task MatchAsync(HttpContext httpContext) + { + return CurrentMatcher.MatchAsync(httpContext); + } - private Matcher CreateMatcher(IReadOnlyList endpoints) + private Matcher CreateMatcher(IReadOnlyList endpoints) + { + var builder = _matcherBuilderFactory(); + var seenEndpointNames = new Dictionary(); + for (var i = 0; i < endpoints.Count; i++) { - var builder = _matcherBuilderFactory(); - var seenEndpointNames = new Dictionary(); - for (var i = 0; i < endpoints.Count; i++) + // By design we only look at RouteEndpoint here. It's possible to + // register other endpoint types, which are non-routable, and it's + // ok that we won't route to them. + if (endpoints[i] is RouteEndpoint endpoint) { - // By design we only look at RouteEndpoint here. It's possible to - // register other endpoint types, which are non-routable, and it's - // ok that we won't route to them. - if (endpoints[i] is RouteEndpoint endpoint) + // Validate that endpoint names are unique. + var endpointName = endpoint.Metadata.GetMetadata()?.EndpointName; + if (endpointName is not null) { - // Validate that endpoint names are unique. - var endpointName = endpoint.Metadata.GetMetadata()?.EndpointName; - if (endpointName is not null) + if (seenEndpointNames.TryGetValue(endpointName, out var existingEndpoint)) { - if (seenEndpointNames.TryGetValue(endpointName, out var existingEndpoint)) - { - throw new InvalidOperationException($"Duplicate endpoint name '{endpointName}' found on '{endpoint.DisplayName}' and '{existingEndpoint}'. Endpoint names must be globally unique."); - } - - seenEndpointNames.Add(endpointName, endpoint.DisplayName ?? endpoint.RoutePattern.RawText); + throw new InvalidOperationException($"Duplicate endpoint name '{endpointName}' found on '{endpoint.DisplayName}' and '{existingEndpoint}'. Endpoint names must be globally unique."); } - // We check for duplicate endpoint names on all endpoints regardless - // of whether they suppress matching because endpoint names can be - // used in OpenAPI specifications as well. - if (endpoint.Metadata.GetMetadata()?.SuppressMatching != true) - { - builder.AddEndpoint(endpoint); - } + seenEndpointNames.Add(endpointName, endpoint.DisplayName ?? endpoint.RoutePattern.RawText); } - } - return builder.Build(); + // We check for duplicate endpoint names on all endpoints regardless + // of whether they suppress matching because endpoint names can be + // used in OpenAPI specifications as well. + if (endpoint.Metadata.GetMetadata()?.SuppressMatching != true) + { + builder.AddEndpoint(endpoint); + } + } } - // Used to tie the lifetime of a DataSourceDependentCache to the service provider - public sealed class Lifetime : IDisposable - { - private readonly object _lock = new object(); - private DataSourceDependentCache? _cache; - private bool _disposed; + return builder.Build(); + } - public DataSourceDependentCache? Cache + // Used to tie the lifetime of a DataSourceDependentCache to the service provider + public sealed class Lifetime : IDisposable + { + private readonly object _lock = new object(); + private DataSourceDependentCache? _cache; + private bool _disposed; + + public DataSourceDependentCache? Cache + { + get => _cache; + set { - get => _cache; - set + lock (_lock) { - lock (_lock) + if (_disposed) { - if (_disposed) - { - value?.Dispose(); - } - - _cache = value; + value?.Dispose(); } + + _cache = value; } } + } - public void Dispose() + public void Dispose() + { + lock (_lock) { - lock (_lock) - { - _cache?.Dispose(); - _cache = null; + _cache?.Dispose(); + _cache = null; - _disposed = true; - } + _disposed = true; } } } diff --git a/src/Http/Routing/src/Matching/DefaultEndpointSelector.cs b/src/Http/Routing/src/Matching/DefaultEndpointSelector.cs index 9cb7684579..eddde5aec7 100644 --- a/src/Http/Routing/src/Matching/DefaultEndpointSelector.cs +++ b/src/Http/Routing/src/Matching/DefaultEndpointSelector.cs @@ -7,131 +7,130 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal sealed class DefaultEndpointSelector : EndpointSelector { - internal sealed class DefaultEndpointSelector : EndpointSelector + public override Task SelectAsync( + HttpContext httpContext, + CandidateSet candidateSet) { - public override Task SelectAsync( - HttpContext httpContext, - CandidateSet candidateSet) + if (httpContext == null) { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - if (candidateSet == null) - { - throw new ArgumentNullException(nameof(candidateSet)); - } - - Select(httpContext, candidateSet.Candidates); - return Task.CompletedTask; + throw new ArgumentNullException(nameof(httpContext)); } - internal static void Select(HttpContext httpContext, CandidateState[] candidateState) + if (candidateSet == null) { - // Fast path: We can specialize for trivial numbers of candidates since there can - // be no ambiguities - switch (candidateState.Length) - { - case 0: - { - // Do nothing - break; - } - - case 1: - { - ref var state = ref candidateState[0]; - if (CandidateSet.IsValidCandidate(ref state)) - { - httpContext.SetEndpoint(state.Endpoint); - httpContext.Request.RouteValues = state.Values!; - } - - break; - } - - default: - { - // Slow path: There's more than one candidate (to say nothing of validity) so we - // have to process for ambiguities. - ProcessFinalCandidates(httpContext, candidateState); - break; - } - } + throw new ArgumentNullException(nameof(candidateSet)); } - private static void ProcessFinalCandidates( - HttpContext httpContext, - CandidateState[] candidateState) + Select(httpContext, candidateSet.Candidates); + return Task.CompletedTask; + } + + internal static void Select(HttpContext httpContext, CandidateState[] candidateState) + { + // Fast path: We can specialize for trivial numbers of candidates since there can + // be no ambiguities + switch (candidateState.Length) { - Endpoint? endpoint = null; - RouteValueDictionary? values = null; - int? foundScore = null; - for (var i = 0; i < candidateState.Length; i++) - { - ref var state = ref candidateState[i]; - if (!CandidateSet.IsValidCandidate(ref state)) + case 0: { - continue; + // Do nothing + break; } - if (foundScore == null) + case 1: { - // This is the first match we've seen - speculatively assign it. - endpoint = state.Endpoint; - values = state.Values; - foundScore = state.Score; + ref var state = ref candidateState[0]; + if (CandidateSet.IsValidCandidate(ref state)) + { + httpContext.SetEndpoint(state.Endpoint); + httpContext.Request.RouteValues = state.Values!; + } + + break; } - else if (foundScore < state.Score) + + default: { - // This candidate is lower priority than the one we've seen - // so far, we can stop. - // - // Don't worry about the 'null < state.Score' case, it returns false. + // Slow path: There's more than one candidate (to say nothing of validity) so we + // have to process for ambiguities. + ProcessFinalCandidates(httpContext, candidateState); break; } - else if (foundScore == state.Score) - { - // This is the second match we've found of the same score, so there - // must be an ambiguity. - // - // Don't worry about the 'null == state.Score' case, it returns false. - - ReportAmbiguity(candidateState); + } + } - // Unreachable, ReportAmbiguity always throws. - throw new NotSupportedException(); - } + private static void ProcessFinalCandidates( + HttpContext httpContext, + CandidateState[] candidateState) + { + Endpoint? endpoint = null; + RouteValueDictionary? values = null; + int? foundScore = null; + for (var i = 0; i < candidateState.Length; i++) + { + ref var state = ref candidateState[i]; + if (!CandidateSet.IsValidCandidate(ref state)) + { + continue; } - if (endpoint != null) + if (foundScore == null) { - httpContext.SetEndpoint(endpoint); - httpContext.Request.RouteValues = values!; + // This is the first match we've seen - speculatively assign it. + endpoint = state.Endpoint; + values = state.Values; + foundScore = state.Score; } + else if (foundScore < state.Score) + { + // This candidate is lower priority than the one we've seen + // so far, we can stop. + // + // Don't worry about the 'null < state.Score' case, it returns false. + break; + } + else if (foundScore == state.Score) + { + // This is the second match we've found of the same score, so there + // must be an ambiguity. + // + // Don't worry about the 'null == state.Score' case, it returns false. + + ReportAmbiguity(candidateState); + + // Unreachable, ReportAmbiguity always throws. + throw new NotSupportedException(); + } + } + + if (endpoint != null) + { + httpContext.SetEndpoint(endpoint); + httpContext.Request.RouteValues = values!; } + } - private static void ReportAmbiguity(CandidateState[] candidateState) + private static void ReportAmbiguity(CandidateState[] candidateState) + { + // If we get here it's the result of an ambiguity - we're OK with this + // being a littler slower and more allocatey. + var matches = new List(); + for (var i = 0; i < candidateState.Length; i++) { - // If we get here it's the result of an ambiguity - we're OK with this - // being a littler slower and more allocatey. - var matches = new List(); - for (var i = 0; i < candidateState.Length; i++) + ref var state = ref candidateState[i]; + if (CandidateSet.IsValidCandidate(ref state)) { - ref var state = ref candidateState[i]; - if (CandidateSet.IsValidCandidate(ref state)) - { - matches.Add(state.Endpoint); - } + matches.Add(state.Endpoint); } - - var message = Resources.FormatAmbiguousEndpoints( - Environment.NewLine, - string.Join(Environment.NewLine, matches.Select(e => e.DisplayName))); - throw new AmbiguousMatchException(message); } + + var message = Resources.FormatAmbiguousEndpoints( + Environment.NewLine, + string.Join(Environment.NewLine, matches.Select(e => e.DisplayName))); + throw new AmbiguousMatchException(message); } } diff --git a/src/Http/Routing/src/Matching/DfaMatcher.cs b/src/Http/Routing/src/Matching/DfaMatcher.cs index f789d50627..4f30c8df69 100644 --- a/src/Http/Routing/src/Matching/DfaMatcher.cs +++ b/src/Http/Routing/src/Matching/DfaMatcher.cs @@ -9,405 +9,404 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal sealed partial class DfaMatcher : Matcher { - internal sealed partial class DfaMatcher : Matcher + private readonly ILogger _logger; + private readonly EndpointSelector _selector; + private readonly DfaState[] _states; + private readonly int _maxSegmentCount; + private readonly bool _isDefaultEndpointSelector; + + public DfaMatcher(ILogger logger, EndpointSelector selector, DfaState[] states, int maxSegmentCount) { - private readonly ILogger _logger; - private readonly EndpointSelector _selector; - private readonly DfaState[] _states; - private readonly int _maxSegmentCount; - private readonly bool _isDefaultEndpointSelector; + _logger = logger; + _selector = selector; + _states = states; + _maxSegmentCount = maxSegmentCount; + _isDefaultEndpointSelector = selector is DefaultEndpointSelector; + } - public DfaMatcher(ILogger logger, EndpointSelector selector, DfaState[] states, int maxSegmentCount) + [SkipLocalsInit] + public sealed override Task MatchAsync(HttpContext httpContext) + { + if (httpContext == null) { - _logger = logger; - _selector = selector; - _states = states; - _maxSegmentCount = maxSegmentCount; - _isDefaultEndpointSelector = selector is DefaultEndpointSelector; + throw new ArgumentNullException(nameof(httpContext)); } - [SkipLocalsInit] - public sealed override Task MatchAsync(HttpContext httpContext) - { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - // All of the logging we do here is at level debug, so we can get away with doing a single check. - var log = _logger.IsEnabled(LogLevel.Debug); + // All of the logging we do here is at level debug, so we can get away with doing a single check. + var log = _logger.IsEnabled(LogLevel.Debug); - // The sequence of actions we take is optimized to avoid doing expensive work - // like creating substrings, creating route value dictionaries, and calling - // into policies like versioning. - var path = httpContext.Request.Path.Value!; + // The sequence of actions we take is optimized to avoid doing expensive work + // like creating substrings, creating route value dictionaries, and calling + // into policies like versioning. + var path = httpContext.Request.Path.Value!; - // First tokenize the path into series of segments. - Span buffer = stackalloc PathSegment[_maxSegmentCount]; - var count = FastPathTokenizer.Tokenize(path, buffer); - var segments = buffer.Slice(0, count); - - // FindCandidateSet will process the DFA and return a candidate set. This does - // some preliminary matching of the URL (mostly the literal segments). - var (candidates, policies) = FindCandidateSet(httpContext, path, segments); - var candidateCount = candidates.Length; - if (candidateCount == 0) - { - if (log) - { - Log.CandidatesNotFound(_logger, path); - } - - return Task.CompletedTask; - } + // First tokenize the path into series of segments. + Span buffer = stackalloc PathSegment[_maxSegmentCount]; + var count = FastPathTokenizer.Tokenize(path, buffer); + var segments = buffer.Slice(0, count); + // FindCandidateSet will process the DFA and return a candidate set. This does + // some preliminary matching of the URL (mostly the literal segments). + var (candidates, policies) = FindCandidateSet(httpContext, path, segments); + var candidateCount = candidates.Length; + if (candidateCount == 0) + { if (log) { - Log.CandidatesFound(_logger, path, candidates); + Log.CandidatesNotFound(_logger, path); } - var policyCount = policies.Length; + return Task.CompletedTask; + } - // This is a fast path for single candidate, 0 policies and default selector - if (candidateCount == 1 && policyCount == 0 && _isDefaultEndpointSelector) - { - ref readonly var candidate = ref candidates[0]; + if (log) + { + Log.CandidatesFound(_logger, path, candidates); + } - // Just strict path matching (no route values) - if (candidate.Flags == Candidate.CandidateFlags.None) - { - httpContext.SetEndpoint(candidate.Endpoint); + var policyCount = policies.Length; - // We're done - return Task.CompletedTask; - } + // This is a fast path for single candidate, 0 policies and default selector + if (candidateCount == 1 && policyCount == 0 && _isDefaultEndpointSelector) + { + ref readonly var candidate = ref candidates[0]; + + // Just strict path matching (no route values) + if (candidate.Flags == Candidate.CandidateFlags.None) + { + httpContext.SetEndpoint(candidate.Endpoint); + + // We're done + return Task.CompletedTask; } + } - // At this point we have a candidate set, defined as a list of endpoints in - // priority order. - // - // We don't yet know that any candidate can be considered a match, because - // we haven't processed things like route constraints and complex segments. + // At this point we have a candidate set, defined as a list of endpoints in + // priority order. + // + // We don't yet know that any candidate can be considered a match, because + // we haven't processed things like route constraints and complex segments. + // + // Now we'll iterate each endpoint to capture route values, process constraints, + // and process complex segments. + + // `candidates` has all of our internal state that we use to process the + // set of endpoints before we call the EndpointSelector. + // + // `candidateSet` is the mutable state that we pass to the EndpointSelector. + var candidateState = new CandidateState[candidateCount]; + + for (var i = 0; i < candidateCount; i++) + { + // PERF: using ref here to avoid copying around big structs. // - // Now we'll iterate each endpoint to capture route values, process constraints, - // and process complex segments. + // Reminder! + // candidate: readonly data about the endpoint and how to match + // state: mutable storarge for our processing + ref readonly var candidate = ref candidates[i]; + ref var state = ref candidateState[i]; + state = new CandidateState(candidate.Endpoint, candidate.Score); - // `candidates` has all of our internal state that we use to process the - // set of endpoints before we call the EndpointSelector. - // - // `candidateSet` is the mutable state that we pass to the EndpointSelector. - var candidateState = new CandidateState[candidateCount]; + var flags = candidate.Flags; - for (var i = 0; i < candidateCount; i++) + // First process all of the parameters and defaults. + if ((flags & Candidate.CandidateFlags.HasSlots) != 0) { - // PERF: using ref here to avoid copying around big structs. + // The Slots array has the default values of the route values in it. // - // Reminder! - // candidate: readonly data about the endpoint and how to match - // state: mutable storarge for our processing - ref readonly var candidate = ref candidates[i]; - ref var state = ref candidateState[i]; - state = new CandidateState(candidate.Endpoint, candidate.Score); + // We want to create a new array for the route values based on Slots + // as a prototype. + var prototype = candidate.Slots; + var slots = new KeyValuePair[prototype.Length]; - var flags = candidate.Flags; - - // First process all of the parameters and defaults. - if ((flags & Candidate.CandidateFlags.HasSlots) != 0) + if ((flags & Candidate.CandidateFlags.HasDefaults) != 0) { - // The Slots array has the default values of the route values in it. - // - // We want to create a new array for the route values based on Slots - // as a prototype. - var prototype = candidate.Slots; - var slots = new KeyValuePair[prototype.Length]; - - if ((flags & Candidate.CandidateFlags.HasDefaults) != 0) - { - Array.Copy(prototype, 0, slots, 0, prototype.Length); - } - - if ((flags & Candidate.CandidateFlags.HasCaptures) != 0) - { - ProcessCaptures(slots, candidate.Captures, path, segments); - } - - if ((flags & Candidate.CandidateFlags.HasCatchAll) != 0) - { - ProcessCatchAll(slots, candidate.CatchAll, path, segments); - } - - state.Values = RouteValueDictionary.FromArray(slots); + Array.Copy(prototype, 0, slots, 0, prototype.Length); } - // Now that we have the route values, we need to process complex segments. - // Complex segments go through an old API that requires a fully-materialized - // route value dictionary. - var isMatch = true; - if ((flags & Candidate.CandidateFlags.HasComplexSegments) != 0) + if ((flags & Candidate.CandidateFlags.HasCaptures) != 0) { - state.Values ??= new RouteValueDictionary(); - if (!ProcessComplexSegments(candidate.Endpoint, candidate.ComplexSegments, path, segments, state.Values)) - { - CandidateSet.SetValidity(ref state, false); - isMatch = false; - } + ProcessCaptures(slots, candidate.Captures, path, segments); } - if ((flags & Candidate.CandidateFlags.HasConstraints) != 0) + if ((flags & Candidate.CandidateFlags.HasCatchAll) != 0) { - state.Values ??= new RouteValueDictionary(); - if (!ProcessConstraints(candidate.Endpoint, candidate.Constraints, httpContext, state.Values)) - { - CandidateSet.SetValidity(ref state, false); - isMatch = false; - } + ProcessCatchAll(slots, candidate.CatchAll, path, segments); } - if (log) + state.Values = RouteValueDictionary.FromArray(slots); + } + + // Now that we have the route values, we need to process complex segments. + // Complex segments go through an old API that requires a fully-materialized + // route value dictionary. + var isMatch = true; + if ((flags & Candidate.CandidateFlags.HasComplexSegments) != 0) + { + state.Values ??= new RouteValueDictionary(); + if (!ProcessComplexSegments(candidate.Endpoint, candidate.ComplexSegments, path, segments, state.Values)) { - if (isMatch) - { - Log.CandidateValid(_logger, path, candidate.Endpoint); - } - else - { - Log.CandidateNotValid(_logger, path, candidate.Endpoint); - } + CandidateSet.SetValidity(ref state, false); + isMatch = false; } } - if (policyCount == 0 && _isDefaultEndpointSelector) + if ((flags & Candidate.CandidateFlags.HasConstraints) != 0) { - // Fast path that avoids allocating the candidate set. - // - // We can use this when there are no policies and we're using the default selector. - DefaultEndpointSelector.Select(httpContext, candidateState); - return Task.CompletedTask; + state.Values ??= new RouteValueDictionary(); + if (!ProcessConstraints(candidate.Endpoint, candidate.Constraints, httpContext, state.Values)) + { + CandidateSet.SetValidity(ref state, false); + isMatch = false; + } } - else if (policyCount == 0) + + if (log) { - // Fast path that avoids a state machine. - // - // We can use this when there are no policies and a non-default selector. - return _selector.SelectAsync(httpContext, new CandidateSet(candidateState)); + if (isMatch) + { + Log.CandidateValid(_logger, path, candidate.Endpoint); + } + else + { + Log.CandidateNotValid(_logger, path, candidate.Endpoint); + } } - - return SelectEndpointWithPoliciesAsync(httpContext, policies, new CandidateSet(candidateState)); } - internal (Candidate[] candidates, IEndpointSelectorPolicy[] policies) FindCandidateSet( - HttpContext httpContext, - string path, - ReadOnlySpan segments) + if (policyCount == 0 && _isDefaultEndpointSelector) { - var states = _states; - - // Process each path segment - var destination = 0; - for (var i = 0; i < segments.Length; i++) - { - destination = states[destination].PathTransitions.GetDestination(path, segments[i]); - } + // Fast path that avoids allocating the candidate set. + // + // We can use this when there are no policies and we're using the default selector. + DefaultEndpointSelector.Select(httpContext, candidateState); + return Task.CompletedTask; + } + else if (policyCount == 0) + { + // Fast path that avoids a state machine. + // + // We can use this when there are no policies and a non-default selector. + return _selector.SelectAsync(httpContext, new CandidateSet(candidateState)); + } - // Process an arbitrary number of policy-based decisions - var policyTransitions = states[destination].PolicyTransitions; - while (policyTransitions != null) - { - destination = policyTransitions.GetDestination(httpContext); - policyTransitions = states[destination].PolicyTransitions; - } + return SelectEndpointWithPoliciesAsync(httpContext, policies, new CandidateSet(candidateState)); + } - return (states[destination].Candidates, states[destination].Policies); - } + internal (Candidate[] candidates, IEndpointSelectorPolicy[] policies) FindCandidateSet( + HttpContext httpContext, + string path, + ReadOnlySpan segments) + { + var states = _states; - private static void ProcessCaptures( - KeyValuePair[] slots, - (string parameterName, int segmentIndex, int slotIndex)[] captures, - string path, - ReadOnlySpan segments) + // Process each path segment + var destination = 0; + for (var i = 0; i < segments.Length; i++) { - for (var i = 0; i < captures.Length; i++) - { - (var parameterName, var segmentIndex, var slotIndex) = captures[i]; - - if ((uint)segmentIndex < (uint)segments.Length) - { - var segment = segments[segmentIndex]; - if (parameterName != null && segment.Length > 0) - { - slots[slotIndex] = new KeyValuePair( - parameterName, - path.Substring(segment.Start, segment.Length)); - } - } - } + destination = states[destination].PathTransitions.GetDestination(path, segments[i]); } - private static void ProcessCatchAll( - KeyValuePair[] slots, - in (string parameterName, int segmentIndex, int slotIndex) catchAll, - string path, - ReadOnlySpan segments) + // Process an arbitrary number of policy-based decisions + var policyTransitions = states[destination].PolicyTransitions; + while (policyTransitions != null) { - // Read segmentIndex to local both to skip double read from stack value - // and to use the same in-bounds validated variable to access the array. - var segmentIndex = catchAll.segmentIndex; - if ((uint)segmentIndex < (uint)segments.Length) - { - var segment = segments[segmentIndex]; - slots[catchAll.slotIndex] = new KeyValuePair( - catchAll.parameterName, - path.Substring(segment.Start)); - } + destination = policyTransitions.GetDestination(httpContext); + policyTransitions = states[destination].PolicyTransitions; } - private bool ProcessComplexSegments( - Endpoint endpoint, - (RoutePatternPathSegment pathSegment, int segmentIndex)[] complexSegments, - string path, - ReadOnlySpan segments, - RouteValueDictionary values) + return (states[destination].Candidates, states[destination].Policies); + } + + private static void ProcessCaptures( + KeyValuePair[] slots, + (string parameterName, int segmentIndex, int slotIndex)[] captures, + string path, + ReadOnlySpan segments) + { + for (var i = 0; i < captures.Length; i++) { - for (var i = 0; i < complexSegments.Length; i++) + (var parameterName, var segmentIndex, var slotIndex) = captures[i]; + + if ((uint)segmentIndex < (uint)segments.Length) { - (var complexSegment, var segmentIndex) = complexSegments[i]; var segment = segments[segmentIndex]; - var text = path.AsSpan(segment.Start, segment.Length); - if (!RoutePatternMatcher.MatchComplexSegment(complexSegment, text, values)) + if (parameterName != null && segment.Length > 0) { - Log.CandidateRejectedByComplexSegment(_logger, path, endpoint, complexSegment); - return false; + slots[slotIndex] = new KeyValuePair( + parameterName, + path.Substring(segment.Start, segment.Length)); } } + } + } - return true; + private static void ProcessCatchAll( + KeyValuePair[] slots, + in (string parameterName, int segmentIndex, int slotIndex) catchAll, + string path, + ReadOnlySpan segments) + { + // Read segmentIndex to local both to skip double read from stack value + // and to use the same in-bounds validated variable to access the array. + var segmentIndex = catchAll.segmentIndex; + if ((uint)segmentIndex < (uint)segments.Length) + { + var segment = segments[segmentIndex]; + slots[catchAll.slotIndex] = new KeyValuePair( + catchAll.parameterName, + path.Substring(segment.Start)); } + } - private bool ProcessConstraints( - Endpoint endpoint, - KeyValuePair[] constraints, - HttpContext httpContext, - RouteValueDictionary values) + private bool ProcessComplexSegments( + Endpoint endpoint, + (RoutePatternPathSegment pathSegment, int segmentIndex)[] complexSegments, + string path, + ReadOnlySpan segments, + RouteValueDictionary values) + { + for (var i = 0; i < complexSegments.Length; i++) { - for (var i = 0; i < constraints.Length; i++) + (var complexSegment, var segmentIndex) = complexSegments[i]; + var segment = segments[segmentIndex]; + var text = path.AsSpan(segment.Start, segment.Length); + if (!RoutePatternMatcher.MatchComplexSegment(complexSegment, text, values)) { - var constraint = constraints[i]; - if (!constraint.Value.Match(httpContext, NullRouter.Instance, constraint.Key, values, RouteDirection.IncomingRequest)) - { - Log.CandidateRejectedByConstraint(_logger, httpContext.Request.Path, endpoint, constraint.Key, constraint.Value, values[constraint.Key]); - return false; - } + Log.CandidateRejectedByComplexSegment(_logger, path, endpoint, complexSegment); + return false; } - - return true; } - private async Task SelectEndpointWithPoliciesAsync( - HttpContext httpContext, - IEndpointSelectorPolicy[] policies, - CandidateSet candidateSet) + return true; + } + + private bool ProcessConstraints( + Endpoint endpoint, + KeyValuePair[] constraints, + HttpContext httpContext, + RouteValueDictionary values) + { + for (var i = 0; i < constraints.Length; i++) { - for (var i = 0; i < policies.Length; i++) + var constraint = constraints[i]; + if (!constraint.Value.Match(httpContext, NullRouter.Instance, constraint.Key, values, RouteDirection.IncomingRequest)) { - var policy = policies[i]; - await policy.ApplyAsync(httpContext, candidateSet); - if (httpContext.GetEndpoint() != null) - { - // This is a short circuit, the selector chose an endpoint. - return; - } + Log.CandidateRejectedByConstraint(_logger, httpContext.Request.Path, endpoint, constraint.Key, constraint.Value, values[constraint.Key]); + return false; } - - await _selector.SelectAsync(httpContext, candidateSet); } - private static partial class Log + return true; + } + + private async Task SelectEndpointWithPoliciesAsync( + HttpContext httpContext, + IEndpointSelectorPolicy[] policies, + CandidateSet candidateSet) + { + for (var i = 0; i < policies.Length; i++) { - [LoggerMessage(1000, LogLevel.Debug, - "No candidates found for the request path '{Path}'", - EventName = "CandidatesNotFound", - SkipEnabledCheck = true)] - public static partial void CandidatesNotFound(ILogger logger, string path); - - public static void CandidatesFound(ILogger logger, string path, Candidate[] candidates) - => CandidatesFound(logger, candidates.Length, path); - - [LoggerMessage(1001, LogLevel.Debug, - "{CandidateCount} candidate(s) found for the request path '{Path}'", - EventName = "CandidatesFound", - SkipEnabledCheck = true)] - private static partial void CandidatesFound(ILogger logger, int candidateCount, string path); - - public static void CandidateRejectedByComplexSegment(ILogger logger, string path, Endpoint endpoint, RoutePatternPathSegment segment) + var policy = policies[i]; + await policy.ApplyAsync(httpContext, candidateSet); + if (httpContext.GetEndpoint() != null) { - // This should return a real pattern since we're processing complex segments.... but just in case. - if (logger.IsEnabled(LogLevel.Debug)) - { - var routePattern = GetRoutePattern(endpoint); - CandidateRejectedByComplexSegment(logger, endpoint.DisplayName, routePattern, segment.DebuggerToString(), path); - } + // This is a short circuit, the selector chose an endpoint. + return; } + } - [LoggerMessage(1002, LogLevel.Debug, - "Endpoint '{Endpoint}' with route pattern '{RoutePattern}' was rejected by complex segment '{Segment}' for the request path '{Path}'", - EventName = "CandidateRejectedByComplexSegment", - SkipEnabledCheck = true)] - private static partial void CandidateRejectedByComplexSegment(ILogger logger, string? endpoint, string routePattern, string segment, string path); + await _selector.SelectAsync(httpContext, candidateSet); + } - public static void CandidateRejectedByConstraint(ILogger logger, string path, Endpoint endpoint, string constraintName, IRouteConstraint constraint, object? value) + private static partial class Log + { + [LoggerMessage(1000, LogLevel.Debug, + "No candidates found for the request path '{Path}'", + EventName = "CandidatesNotFound", + SkipEnabledCheck = true)] + public static partial void CandidatesNotFound(ILogger logger, string path); + + public static void CandidatesFound(ILogger logger, string path, Candidate[] candidates) + => CandidatesFound(logger, candidates.Length, path); + + [LoggerMessage(1001, LogLevel.Debug, + "{CandidateCount} candidate(s) found for the request path '{Path}'", + EventName = "CandidatesFound", + SkipEnabledCheck = true)] + private static partial void CandidatesFound(ILogger logger, int candidateCount, string path); + + public static void CandidateRejectedByComplexSegment(ILogger logger, string path, Endpoint endpoint, RoutePatternPathSegment segment) + { + // This should return a real pattern since we're processing complex segments.... but just in case. + if (logger.IsEnabled(LogLevel.Debug)) { - // This should return a real pattern since we're processing constraints.... but just in case. - if (logger.IsEnabled(LogLevel.Debug)) - { - var routePattern = GetRoutePattern(endpoint); - CandidateRejectedByConstraint(logger, endpoint.DisplayName, routePattern, constraintName, constraint.ToString(), value, path); - } + var routePattern = GetRoutePattern(endpoint); + CandidateRejectedByComplexSegment(logger, endpoint.DisplayName, routePattern, segment.DebuggerToString(), path); } + } - [LoggerMessage(1003, LogLevel.Debug, - "Endpoint '{Endpoint}' with route pattern '{RoutePattern}' was rejected by constraint '{ConstraintName}':'{Constraint}' with value '{RouteValue}' for the request path '{Path}'", - EventName = "CandidateRejectedByConstraint", - SkipEnabledCheck = true)] - private static partial void CandidateRejectedByConstraint(ILogger logger, string? endpoint, string routePattern, string constraintName, string? constraint, object? routeValue, string path); + [LoggerMessage(1002, LogLevel.Debug, + "Endpoint '{Endpoint}' with route pattern '{RoutePattern}' was rejected by complex segment '{Segment}' for the request path '{Path}'", + EventName = "CandidateRejectedByComplexSegment", + SkipEnabledCheck = true)] + private static partial void CandidateRejectedByComplexSegment(ILogger logger, string? endpoint, string routePattern, string segment, string path); - public static void CandidateNotValid(ILogger logger, string path, Endpoint endpoint) + public static void CandidateRejectedByConstraint(ILogger logger, string path, Endpoint endpoint, string constraintName, IRouteConstraint constraint, object? value) + { + // This should return a real pattern since we're processing constraints.... but just in case. + if (logger.IsEnabled(LogLevel.Debug)) { - // This can be the fallback value because it really might not be a route endpoint - if (logger.IsEnabled(LogLevel.Debug)) - { - var routePattern = GetRoutePattern(endpoint); - CandidateNotValid(logger, endpoint.DisplayName, routePattern, path); - } + var routePattern = GetRoutePattern(endpoint); + CandidateRejectedByConstraint(logger, endpoint.DisplayName, routePattern, constraintName, constraint.ToString(), value, path); } + } - [LoggerMessage(1004, LogLevel.Debug, - "Endpoint '{Endpoint}' with route pattern '{RoutePattern}' is not valid for the request path '{Path}'", - EventName = "CandidateNotValid", - SkipEnabledCheck = true)] - private static partial void CandidateNotValid(ILogger logger, string? endpoint, string routePattern, string path); + [LoggerMessage(1003, LogLevel.Debug, + "Endpoint '{Endpoint}' with route pattern '{RoutePattern}' was rejected by constraint '{ConstraintName}':'{Constraint}' with value '{RouteValue}' for the request path '{Path}'", + EventName = "CandidateRejectedByConstraint", + SkipEnabledCheck = true)] + private static partial void CandidateRejectedByConstraint(ILogger logger, string? endpoint, string routePattern, string constraintName, string? constraint, object? routeValue, string path); - public static void CandidateValid(ILogger logger, string path, Endpoint endpoint) + public static void CandidateNotValid(ILogger logger, string path, Endpoint endpoint) + { + // This can be the fallback value because it really might not be a route endpoint + if (logger.IsEnabled(LogLevel.Debug)) { - // This can be the fallback value because it really might not be a route endpoint - if (logger.IsEnabled(LogLevel.Debug)) - { - var routePattern = GetRoutePattern(endpoint); - CandidateValid(logger, endpoint.DisplayName, routePattern, path); - } + var routePattern = GetRoutePattern(endpoint); + CandidateNotValid(logger, endpoint.DisplayName, routePattern, path); } + } - [LoggerMessage(1005, LogLevel.Debug, - "Endpoint '{Endpoint}' with route pattern '{RoutePattern}' is valid for the request path '{Path}'", - EventName = "CandidateValid", - SkipEnabledCheck = true)] - private static partial void CandidateValid(ILogger logger, string? endpoint, string routePattern, string path); + [LoggerMessage(1004, LogLevel.Debug, + "Endpoint '{Endpoint}' with route pattern '{RoutePattern}' is not valid for the request path '{Path}'", + EventName = "CandidateNotValid", + SkipEnabledCheck = true)] + private static partial void CandidateNotValid(ILogger logger, string? endpoint, string routePattern, string path); - private static string GetRoutePattern(Endpoint endpoint) + public static void CandidateValid(ILogger logger, string path, Endpoint endpoint) + { + // This can be the fallback value because it really might not be a route endpoint + if (logger.IsEnabled(LogLevel.Debug)) { - return (endpoint as RouteEndpoint)?.RoutePattern?.RawText ?? "(none)"; + var routePattern = GetRoutePattern(endpoint); + CandidateValid(logger, endpoint.DisplayName, routePattern, path); } } + + [LoggerMessage(1005, LogLevel.Debug, + "Endpoint '{Endpoint}' with route pattern '{RoutePattern}' is valid for the request path '{Path}'", + EventName = "CandidateValid", + SkipEnabledCheck = true)] + private static partial void CandidateValid(ILogger logger, string? endpoint, string routePattern, string path); + + private static string GetRoutePattern(Endpoint endpoint) + { + return (endpoint as RouteEndpoint)?.RoutePattern?.RawText ?? "(none)"; + } } } diff --git a/src/Http/Routing/src/Matching/DfaMatcherBuilder.cs b/src/Http/Routing/src/Matching/DfaMatcherBuilder.cs index 2ceeb93d37..56b3b93d00 100644 --- a/src/Http/Routing/src/Matching/DfaMatcherBuilder.cs +++ b/src/Http/Routing/src/Matching/DfaMatcherBuilder.cs @@ -12,937 +12,936 @@ using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal class DfaMatcherBuilder : MatcherBuilder { - internal class DfaMatcherBuilder : MatcherBuilder + private readonly List _endpoints = new List(); + + private readonly ILoggerFactory _loggerFactory; + private readonly ParameterPolicyFactory _parameterPolicyFactory; + private readonly EndpointSelector _selector; + private readonly IEndpointSelectorPolicy[] _endpointSelectorPolicies; + private readonly INodeBuilderPolicy[] _nodeBuilders; + private readonly EndpointComparer _comparer; + + // These collections are reused when building candidates + private readonly Dictionary _assignments; + private readonly List> _slots; + private readonly List<(string parameterName, int segmentIndex, int slotIndex)> _captures; + private readonly List<(RoutePatternPathSegment pathSegment, int segmentIndex)> _complexSegments; + private readonly List> _constraints; + + private int _stateIndex; + + public DfaMatcherBuilder( + ILoggerFactory loggerFactory, + ParameterPolicyFactory parameterPolicyFactory, + EndpointSelector selector, + IEnumerable policies) { - private readonly List _endpoints = new List(); + _loggerFactory = loggerFactory; + _parameterPolicyFactory = parameterPolicyFactory; + _selector = selector; + + var (nodeBuilderPolicies, endpointComparerPolicies, endpointSelectorPolicies) = ExtractPolicies(policies.OrderBy(p => p.Order)); + _endpointSelectorPolicies = endpointSelectorPolicies; + _nodeBuilders = nodeBuilderPolicies; + _comparer = new EndpointComparer(endpointComparerPolicies); + + _assignments = new Dictionary(StringComparer.OrdinalIgnoreCase); + _slots = new List>(); + _captures = new List<(string parameterName, int segmentIndex, int slotIndex)>(); + _complexSegments = new List<(RoutePatternPathSegment pathSegment, int segmentIndex)>(); + _constraints = new List>(); + } - private readonly ILoggerFactory _loggerFactory; - private readonly ParameterPolicyFactory _parameterPolicyFactory; - private readonly EndpointSelector _selector; - private readonly IEndpointSelectorPolicy[] _endpointSelectorPolicies; - private readonly INodeBuilderPolicy[] _nodeBuilders; - private readonly EndpointComparer _comparer; - - // These collections are reused when building candidates - private readonly Dictionary _assignments; - private readonly List> _slots; - private readonly List<(string parameterName, int segmentIndex, int slotIndex)> _captures; - private readonly List<(RoutePatternPathSegment pathSegment, int segmentIndex)> _complexSegments; - private readonly List> _constraints; - - private int _stateIndex; - - public DfaMatcherBuilder( - ILoggerFactory loggerFactory, - ParameterPolicyFactory parameterPolicyFactory, - EndpointSelector selector, - IEnumerable policies) - { - _loggerFactory = loggerFactory; - _parameterPolicyFactory = parameterPolicyFactory; - _selector = selector; - - var (nodeBuilderPolicies, endpointComparerPolicies, endpointSelectorPolicies) = ExtractPolicies(policies.OrderBy(p => p.Order)); - _endpointSelectorPolicies = endpointSelectorPolicies; - _nodeBuilders = nodeBuilderPolicies; - _comparer = new EndpointComparer(endpointComparerPolicies); - - _assignments = new Dictionary(StringComparer.OrdinalIgnoreCase); - _slots = new List>(); - _captures = new List<(string parameterName, int segmentIndex, int slotIndex)>(); - _complexSegments = new List<(RoutePatternPathSegment pathSegment, int segmentIndex)>(); - _constraints = new List>(); - } + // Used in tests + internal EndpointComparer Comparer => _comparer; - // Used in tests - internal EndpointComparer Comparer => _comparer; + public override void AddEndpoint(RouteEndpoint endpoint) + { + _endpoints.Add(endpoint); + } + + public DfaNode BuildDfaTree(bool includeLabel = false) + { + // Since we're doing a BFS we will process each 'level' of the tree in stages + // this list will hold the set of items we need to process at the current + // stage. + var work = new List(_endpoints.Count); - public override void AddEndpoint(RouteEndpoint endpoint) + var root = new DfaNode() { PathDepth = 0, Label = includeLabel ? "/" : null }; + + // To prepare for this we need to compute the max depth, as well as + // a seed list of items to process (entry, root). + var maxDepth = 0; + for (var i = 0; i < _endpoints.Count; i++) { - _endpoints.Add(endpoint); + var endpoint = _endpoints[i]; + var precedenceDigit = GetPrecedenceDigitAtDepth(endpoint, depth: 0); + work.Add(new DfaBuilderWorkerWorkItem(endpoint, precedenceDigit, new List() { root, })); + maxDepth = Math.Max(maxDepth, endpoint.RoutePattern.PathSegments.Count); } - public DfaNode BuildDfaTree(bool includeLabel = false) + // Sort work at each level by *PRECEDENCE OF THE CURRENT SEGMENT*. + // + // We build the tree by doing a BFS over the list of entries. This is important + // because a 'parameter' node can also traverse the same paths that literal nodes + // traverse. This means that we need to order the entries first, or else we will + // miss possible edges in the DFA. + // + // We'll sort the matches again later using the *real* comparer once building the + // precedence part of the DFA is over. + var precedenceDigitComparer = Comparer.Create((x, y) => { - // Since we're doing a BFS we will process each 'level' of the tree in stages - // this list will hold the set of items we need to process at the current - // stage. - var work = new List(_endpoints.Count); - - var root = new DfaNode() { PathDepth = 0, Label = includeLabel ? "/" : null }; + return x.PrecedenceDigit.CompareTo(y.PrecedenceDigit); + }); - // To prepare for this we need to compute the max depth, as well as - // a seed list of items to process (entry, root). - var maxDepth = 0; - for (var i = 0; i < _endpoints.Count; i++) - { - var endpoint = _endpoints[i]; - var precedenceDigit = GetPrecedenceDigitAtDepth(endpoint, depth: 0); - work.Add(new DfaBuilderWorkerWorkItem(endpoint, precedenceDigit, new List() { root, })); - maxDepth = Math.Max(maxDepth, endpoint.RoutePattern.PathSegments.Count); - } + var dfaWorker = new DfaBuilderWorker(work, precedenceDigitComparer, includeLabel, _parameterPolicyFactory); - // Sort work at each level by *PRECEDENCE OF THE CURRENT SEGMENT*. - // - // We build the tree by doing a BFS over the list of entries. This is important - // because a 'parameter' node can also traverse the same paths that literal nodes - // traverse. This means that we need to order the entries first, or else we will - // miss possible edges in the DFA. - // - // We'll sort the matches again later using the *real* comparer once building the - // precedence part of the DFA is over. - var precedenceDigitComparer = Comparer.Create((x, y) => - { - return x.PrecedenceDigit.CompareTo(y.PrecedenceDigit); - }); + // Now we process the entries a level at a time. + for (var depth = 0; depth <= maxDepth; depth++) + { + dfaWorker.ProcessLevel(depth); + } - var dfaWorker = new DfaBuilderWorker(work, precedenceDigitComparer, includeLabel, _parameterPolicyFactory); + // Build the trees of policy nodes (like HTTP methods). Post-order traversal + // means that we won't have infinite recursion. + root.Visit(ApplyPolicies); - // Now we process the entries a level at a time. - for (var depth = 0; depth <= maxDepth; depth++) - { - dfaWorker.ProcessLevel(depth); - } + return root; + } - // Build the trees of policy nodes (like HTTP methods). Post-order traversal - // means that we won't have infinite recursion. - root.Visit(ApplyPolicies); + private class DfaBuilderWorker + { + private List _previousWork; + private List _work; + private int _workCount; + private readonly Comparer _precedenceDigitComparer; + private readonly bool _includeLabel; + private readonly ParameterPolicyFactory _parameterPolicyFactory; - return root; + public DfaBuilderWorker( + List work, + Comparer precedenceDigitComparer, + bool includeLabel, + ParameterPolicyFactory parameterPolicyFactory) + { + _work = work; + _previousWork = new List(); + _workCount = work.Count; + _precedenceDigitComparer = precedenceDigitComparer; + _includeLabel = includeLabel; + _parameterPolicyFactory = parameterPolicyFactory; } - private class DfaBuilderWorker + // Each time we process a level of the DFA we keep a list of work items consisting on the nodes we need to evaluate + // their precendence and their parent nodes. We sort nodes by precedence on each level, which means that nodes are + // evaluated in the following order: (literals, constrained parameters/complex segments, parameters, constrainted catch-alls and catch-alls) + // When we process a stage we build a list of the next set of workitems we need to evaluate. We also keep around the + // list of workitems from the previous level so that we can reuse all the nested lists while we are evaluating the current level. + internal void ProcessLevel(int depth) { - private List _previousWork; - private List _work; - private int _workCount; - private readonly Comparer _precedenceDigitComparer; - private readonly bool _includeLabel; - private readonly ParameterPolicyFactory _parameterPolicyFactory; + // As we process items, collect the next set of items. + var nextWork = _previousWork; + var nextWorkCount = 0; - public DfaBuilderWorker( - List work, - Comparer precedenceDigitComparer, - bool includeLabel, - ParameterPolicyFactory parameterPolicyFactory) - { - _work = work; - _previousWork = new List(); - _workCount = work.Count; - _precedenceDigitComparer = precedenceDigitComparer; - _includeLabel = includeLabel; - _parameterPolicyFactory = parameterPolicyFactory; - } + // See comments on precedenceDigitComparer + _work.Sort(0, _workCount, _precedenceDigitComparer); - // Each time we process a level of the DFA we keep a list of work items consisting on the nodes we need to evaluate - // their precendence and their parent nodes. We sort nodes by precedence on each level, which means that nodes are - // evaluated in the following order: (literals, constrained parameters/complex segments, parameters, constrainted catch-alls and catch-alls) - // When we process a stage we build a list of the next set of workitems we need to evaluate. We also keep around the - // list of workitems from the previous level so that we can reuse all the nested lists while we are evaluating the current level. - internal void ProcessLevel(int depth) + for (var i = 0; i < _workCount; i++) { - // As we process items, collect the next set of items. - var nextWork = _previousWork; - var nextWorkCount = 0; + var (endpoint, _, parents) = _work[i]; - // See comments on precedenceDigitComparer - _work.Sort(0, _workCount, _precedenceDigitComparer); - - for (var i = 0; i < _workCount; i++) + if (!HasAdditionalRequiredSegments(endpoint, depth)) { - var (endpoint, _, parents) = _work[i]; - - if (!HasAdditionalRequiredSegments(endpoint, depth)) + for (var j = 0; j < parents.Count; j++) { - for (var j = 0; j < parents.Count; j++) - { - var parent = parents[j]; - parent.AddMatch(endpoint); - } + var parent = parents[j]; + parent.AddMatch(endpoint); } + } - // Find the parents of this edge at the current depth - List nextParents; - if (nextWorkCount < nextWork.Count) - { - nextParents = nextWork[nextWorkCount].Parents; - nextParents.Clear(); + // Find the parents of this edge at the current depth + List nextParents; + if (nextWorkCount < nextWork.Count) + { + nextParents = nextWork[nextWorkCount].Parents; + nextParents.Clear(); - var nextPrecedenceDigit = GetPrecedenceDigitAtDepth(endpoint, depth + 1); - nextWork[nextWorkCount] = new DfaBuilderWorkerWorkItem(endpoint, nextPrecedenceDigit, nextParents); - } - else - { - nextParents = new List(); + var nextPrecedenceDigit = GetPrecedenceDigitAtDepth(endpoint, depth + 1); + nextWork[nextWorkCount] = new DfaBuilderWorkerWorkItem(endpoint, nextPrecedenceDigit, nextParents); + } + else + { + nextParents = new List(); - // Add to the next set of work now so the list will be reused - // even if there are no parents - var nextPrecedenceDigit = GetPrecedenceDigitAtDepth(endpoint, depth + 1); - nextWork.Add(new DfaBuilderWorkerWorkItem(endpoint, nextPrecedenceDigit, nextParents)); - } + // Add to the next set of work now so the list will be reused + // even if there are no parents + var nextPrecedenceDigit = GetPrecedenceDigitAtDepth(endpoint, depth + 1); + nextWork.Add(new DfaBuilderWorkerWorkItem(endpoint, nextPrecedenceDigit, nextParents)); + } - var segment = GetCurrentSegment(endpoint, depth); - if (segment == null) - { - continue; - } + var segment = GetCurrentSegment(endpoint, depth); + if (segment == null) + { + continue; + } - ProcessSegment(endpoint, parents, nextParents, segment); + ProcessSegment(endpoint, parents, nextParents, segment); - if (nextParents.Count > 0) - { - nextWorkCount++; - } + if (nextParents.Count > 0) + { + nextWorkCount++; } - - // Prepare to process the next stage. - _previousWork = _work; - _work = nextWork; - _workCount = nextWorkCount; } - private void ProcessSegment( - RouteEndpoint endpoint, - List parents, - List nextParents, - RoutePatternPathSegment segment) + // Prepare to process the next stage. + _previousWork = _work; + _work = nextWork; + _workCount = nextWorkCount; + } + + private void ProcessSegment( + RouteEndpoint endpoint, + List parents, + List nextParents, + RoutePatternPathSegment segment) + { + for (var i = 0; i < parents.Count; i++) { - for (var i = 0; i < parents.Count; i++) + var parent = parents[i]; + var part = segment.Parts[0]; + var parameterPart = part as RoutePatternParameterPart; + if (segment.IsSimple && part is RoutePatternLiteralPart literalPart) + { + AddLiteralNode(_includeLabel, nextParents, parent, literalPart.Content); + } + else if (segment.IsSimple && parameterPart != null && parameterPart.IsCatchAll) { - var parent = parents[i]; - var part = segment.Parts[0]; - var parameterPart = part as RoutePatternParameterPart; - if (segment.IsSimple && part is RoutePatternLiteralPart literalPart) + // A catch all should traverse all literal nodes as well as parameter nodes + // we don't need to create the parameter node here because of ordering + // all catchalls will be processed after all parameters. + if (parent.Literals != null) { - AddLiteralNode(_includeLabel, nextParents, parent, literalPart.Content); + nextParents.AddRange(parent.Literals.Values); } - else if (segment.IsSimple && parameterPart != null && parameterPart.IsCatchAll) + if (parent.Parameters != null) { - // A catch all should traverse all literal nodes as well as parameter nodes - // we don't need to create the parameter node here because of ordering - // all catchalls will be processed after all parameters. - if (parent.Literals != null) - { - nextParents.AddRange(parent.Literals.Values); - } - if (parent.Parameters != null) - { - nextParents.Add(parent.Parameters); - } + nextParents.Add(parent.Parameters); + } - // We also create a 'catchall' here. We don't do further traversals - // on the catchall node because only catchalls can end up here. The - // catchall node allows us to capture an unlimited amount of segments - // and also to match a zero-length segment, which a parameter node - // doesn't allow. - if (parent.CatchAll == null) + // We also create a 'catchall' here. We don't do further traversals + // on the catchall node because only catchalls can end up here. The + // catchall node allows us to capture an unlimited amount of segments + // and also to match a zero-length segment, which a parameter node + // doesn't allow. + if (parent.CatchAll == null) + { + parent.CatchAll = new DfaNode() { - parent.CatchAll = new DfaNode() - { - PathDepth = parent.PathDepth + 1, - Label = _includeLabel ? parent.Label + "{*...}/" : null, - }; - - // The catchall node just loops. - parent.CatchAll.Parameters = parent.CatchAll; - parent.CatchAll.CatchAll = parent.CatchAll; - } + PathDepth = parent.PathDepth + 1, + Label = _includeLabel ? parent.Label + "{*...}/" : null, + }; - parent.CatchAll.AddMatch(endpoint); + // The catchall node just loops. + parent.CatchAll.Parameters = parent.CatchAll; + parent.CatchAll.CatchAll = parent.CatchAll; } - else if (segment.IsSimple && parameterPart != null && TryGetRequiredValue(endpoint.RoutePattern, parameterPart, out var requiredValue)) - { - // If the parameter has a matching required value, replace the parameter with the required value - // as a literal. This should use the parameter's transformer (if present) - // e.g. Template: Home/{action}, Required values: { action = "Index" }, Result: Home/Index - AddRequiredLiteralValue(endpoint, nextParents, parent, parameterPart, requiredValue); - } - else if (segment.IsSimple && parameterPart != null) + parent.CatchAll.AddMatch(endpoint); + } + else if (segment.IsSimple && parameterPart != null && TryGetRequiredValue(endpoint.RoutePattern, parameterPart, out var requiredValue)) + { + // If the parameter has a matching required value, replace the parameter with the required value + // as a literal. This should use the parameter's transformer (if present) + // e.g. Template: Home/{action}, Required values: { action = "Index" }, Result: Home/Index + + AddRequiredLiteralValue(endpoint, nextParents, parent, parameterPart, requiredValue); + } + else if (segment.IsSimple && parameterPart != null) + { + if (parent.Parameters == null) { - if (parent.Parameters == null) + parent.Parameters = new DfaNode() { - parent.Parameters = new DfaNode() - { - PathDepth = parent.PathDepth + 1, - Label = _includeLabel ? parent.Label + "{...}/" : null, - }; - } + PathDepth = parent.PathDepth + 1, + Label = _includeLabel ? parent.Label + "{...}/" : null, + }; + } - if (parent.Literals != null) + if (parent.Literals != null) + { + // If the parameter contains constraints, we can be smarter about it and evaluate them while we build the tree. + // If the literal doesn't match any of the constraints, we can prune the branch. + // For example, for a parameter in a route {lang:length(2)} and a parent literal "ABC", we can check that "ABC" + // doesn't meet the parameter constraint (length(2)) when building the tree, and avoid the extra nodes. + if (endpoint.RoutePattern.ParameterPolicies.TryGetValue(parameterPart.Name, out var parameterPolicyReferences)) { - // If the parameter contains constraints, we can be smarter about it and evaluate them while we build the tree. - // If the literal doesn't match any of the constraints, we can prune the branch. - // For example, for a parameter in a route {lang:length(2)} and a parent literal "ABC", we can check that "ABC" - // doesn't meet the parameter constraint (length(2)) when building the tree, and avoid the extra nodes. - if (endpoint.RoutePattern.ParameterPolicies.TryGetValue(parameterPart.Name, out var parameterPolicyReferences)) - { - // We filter out sibling literals that don't match one of the constraints in the segment to avoid adding nodes to the DFA - // that will never match a route and which will result in a much higher memory usage. - AddParentsWithMatchingLiteralConstraints(nextParents, parent, parameterPart, parameterPolicyReferences); - } - else - { - // This means the current parameter we are evaluating doesn't contain any constraint, so we need to - // traverse all literal nodes as well as the parameter node. - nextParents.AddRange(parent.Literals.Values); - } + // We filter out sibling literals that don't match one of the constraints in the segment to avoid adding nodes to the DFA + // that will never match a route and which will result in a much higher memory usage. + AddParentsWithMatchingLiteralConstraints(nextParents, parent, parameterPart, parameterPolicyReferences); } - - nextParents.Add(parent.Parameters); - } - else - { - // Complex segment - we treat these are parameters here and do the - // expensive processing later. We don't want to spend time processing - // complex segments unless they are the best match, and treating them - // like parameters in the DFA allows us to do just that. - if (parent.Parameters == null) + else { - parent.Parameters = new DfaNode() - { - PathDepth = parent.PathDepth + 1, - Label = _includeLabel ? parent.Label + "{...}/" : null, - }; + // This means the current parameter we are evaluating doesn't contain any constraint, so we need to + // traverse all literal nodes as well as the parameter node. + nextParents.AddRange(parent.Literals.Values); } + } - if (parent.Literals != null) + nextParents.Add(parent.Parameters); + } + else + { + // Complex segment - we treat these are parameters here and do the + // expensive processing later. We don't want to spend time processing + // complex segments unless they are the best match, and treating them + // like parameters in the DFA allows us to do just that. + if (parent.Parameters == null) + { + parent.Parameters = new DfaNode() { - // For a complex segment like this, we can evaluate the literals and avoid adding extra nodes to - // the tree on cases where the literal won't ever be able to match the complex parameter. - // For example, if we have a complex parameter {a}-{b}.{c?} and a literal "Hello" we can guarantee - // that it will never be a match. + PathDepth = parent.PathDepth + 1, + Label = _includeLabel ? parent.Label + "{...}/" : null, + }; + } - // We filter out sibling literals that don't match the complex parameter segment to avoid adding nodes to the DFA - // that will never match a route and which will result in a much higher memory usage. - AddParentsMatchingComplexSegment(endpoint, nextParents, segment, parent, parameterPart); - } - nextParents.Add(parent.Parameters); + if (parent.Literals != null) + { + // For a complex segment like this, we can evaluate the literals and avoid adding extra nodes to + // the tree on cases where the literal won't ever be able to match the complex parameter. + // For example, if we have a complex parameter {a}-{b}.{c?} and a literal "Hello" we can guarantee + // that it will never be a match. + + // We filter out sibling literals that don't match the complex parameter segment to avoid adding nodes to the DFA + // that will never match a route and which will result in a much higher memory usage. + AddParentsMatchingComplexSegment(endpoint, nextParents, segment, parent, parameterPart); } + nextParents.Add(parent.Parameters); } } + } - private void AddParentsMatchingComplexSegment(RouteEndpoint endpoint, List nextParents, RoutePatternPathSegment segment, DfaNode parent, RoutePatternParameterPart parameterPart) + private void AddParentsMatchingComplexSegment(RouteEndpoint endpoint, List nextParents, RoutePatternPathSegment segment, DfaNode parent, RoutePatternParameterPart parameterPart) + { + var routeValues = new RouteValueDictionary(); + foreach (var literal in parent.Literals.Keys) { - var routeValues = new RouteValueDictionary(); - foreach (var literal in parent.Literals.Keys) + if (RoutePatternMatcher.MatchComplexSegment(segment, literal, routeValues)) { - if (RoutePatternMatcher.MatchComplexSegment(segment, literal, routeValues)) + // If we got here (rare) it means that the literal matches the complex segment (for example the literal is something A-B) + // there is another thing we can try here, which is to evaluate the policies for the parts in case they have one (for example {a:length(4)}-{b:regex(\d+)}) + // so that even if it maps closely to a complex parameter we have a chance to discard it and avoid adding the extra branches. + var passedAllPolicies = true; + for (var i = 0; i < segment.Parts.Count; i++) { - // If we got here (rare) it means that the literal matches the complex segment (for example the literal is something A-B) - // there is another thing we can try here, which is to evaluate the policies for the parts in case they have one (for example {a:length(4)}-{b:regex(\d+)}) - // so that even if it maps closely to a complex parameter we have a chance to discard it and avoid adding the extra branches. - var passedAllPolicies = true; - for (var i = 0; i < segment.Parts.Count; i++) + var segmentPart = segment.Parts[i]; + if (segmentPart is not RoutePatternParameterPart partParameter) { - var segmentPart = segment.Parts[i]; - if (segmentPart is not RoutePatternParameterPart partParameter) - { - // We skip over the literals and the separator since we already checked against them - continue; - } + // We skip over the literals and the separator since we already checked against them + continue; + } - if (!routeValues.TryGetValue(partParameter.Name, out var parameterValue)) - { - // We have a pattern like {a}-{b}.{part?} and a literal "a-b". Since we've matched the complex segment it means that the optional - // parameter was not specified, so we skip it. - Debug.Assert(i == segment.Parts.Count - 1 && partParameter.IsOptional); - continue; - } + if (!routeValues.TryGetValue(partParameter.Name, out var parameterValue)) + { + // We have a pattern like {a}-{b}.{part?} and a literal "a-b". Since we've matched the complex segment it means that the optional + // parameter was not specified, so we skip it. + Debug.Assert(i == segment.Parts.Count - 1 && partParameter.IsOptional); + continue; + } - if (endpoint.RoutePattern.ParameterPolicies.TryGetValue(partParameter.Name, out var parameterPolicyReferences)) + if (endpoint.RoutePattern.ParameterPolicies.TryGetValue(partParameter.Name, out var parameterPolicyReferences)) + { + for (var j = 0; j < parameterPolicyReferences.Count; j++) { - for (var j = 0; j < parameterPolicyReferences.Count; j++) + var reference = parameterPolicyReferences[j]; + var parameterPolicy = _parameterPolicyFactory.Create(parameterPart, reference); + if (parameterPolicy is IParameterLiteralNodeMatchingPolicy constraint && !constraint.MatchesLiteral(partParameter.Name, (string)parameterValue)) { - var reference = parameterPolicyReferences[j]; - var parameterPolicy = _parameterPolicyFactory.Create(parameterPart, reference); - if (parameterPolicy is IParameterLiteralNodeMatchingPolicy constraint && !constraint.MatchesLiteral(partParameter.Name, (string)parameterValue)) - { - passedAllPolicies = false; - break; - } + passedAllPolicies = false; + break; } } } - - if (passedAllPolicies) - { - nextParents.Add(parent.Literals[literal]); - } } - routeValues.Clear(); + if (passedAllPolicies) + { + nextParents.Add(parent.Literals[literal]); + } } + + routeValues.Clear(); } + } - private void AddParentsWithMatchingLiteralConstraints(List nextParents, DfaNode parent, RoutePatternParameterPart parameterPart, IReadOnlyList parameterPolicyReferences) - { - // The list of parameters that fail to meet at least one IParameterLiteralNodeMatchingPolicy. - var hasFailingPolicy = parent.Literals.Keys.Count < 32 ? - (stackalloc bool[32]).Slice(0, parent.Literals.Keys.Count) : - new bool[parent.Literals.Keys.Count]; + private void AddParentsWithMatchingLiteralConstraints(List nextParents, DfaNode parent, RoutePatternParameterPart parameterPart, IReadOnlyList parameterPolicyReferences) + { + // The list of parameters that fail to meet at least one IParameterLiteralNodeMatchingPolicy. + var hasFailingPolicy = parent.Literals.Keys.Count < 32 ? + (stackalloc bool[32]).Slice(0, parent.Literals.Keys.Count) : + new bool[parent.Literals.Keys.Count]; - // Whether or not all parameters have failed to meet at least one constraint. - for (var i = 0; i < parameterPolicyReferences.Count; i++) + // Whether or not all parameters have failed to meet at least one constraint. + for (var i = 0; i < parameterPolicyReferences.Count; i++) + { + var reference = parameterPolicyReferences[i]; + var parameterPolicy = _parameterPolicyFactory.Create(parameterPart, reference); + if (parameterPolicy is IParameterLiteralNodeMatchingPolicy constraint) { - var reference = parameterPolicyReferences[i]; - var parameterPolicy = _parameterPolicyFactory.Create(parameterPart, reference); - if (parameterPolicy is IParameterLiteralNodeMatchingPolicy constraint) + var literalIndex = 0; + var allFailed = true; + foreach (var literal in parent.Literals.Keys) { - var literalIndex = 0; - var allFailed = true; - foreach (var literal in parent.Literals.Keys) + if (!hasFailingPolicy[literalIndex] && !constraint.MatchesLiteral(parameterPart.Name, literal)) { - if (!hasFailingPolicy[literalIndex] && !constraint.MatchesLiteral(parameterPart.Name, literal)) - { - hasFailingPolicy[literalIndex] = true; - } - - allFailed &= hasFailingPolicy[literalIndex]; - - literalIndex++; + hasFailingPolicy[literalIndex] = true; } - if (allFailed) - { - // If we get here it means that all literals have failed at least one policy, which means we can skip checking policies - // and return early. This will be a very common case when your constraints are things like "int,length or a regex". - return; - } + allFailed &= hasFailingPolicy[literalIndex]; + + literalIndex++; } - } - var k = 0; - foreach (var literal in parent.Literals.Values) - { - if (!hasFailingPolicy[k]) + if (allFailed) { - nextParents.Add(literal); + // If we get here it means that all literals have failed at least one policy, which means we can skip checking policies + // and return early. This will be a very common case when your constraints are things like "int,length or a regex". + return; } - k++; } } - private void AddRequiredLiteralValue(RouteEndpoint endpoint, List nextParents, DfaNode parent, RoutePatternParameterPart parameterPart, object requiredValue) + var k = 0; + foreach (var literal in parent.Literals.Values) { - if (endpoint.RoutePattern.ParameterPolicies.TryGetValue(parameterPart.Name, out var parameterPolicyReferences)) + if (!hasFailingPolicy[k]) { - for (var k = 0; k < parameterPolicyReferences.Count; k++) - { - var reference = parameterPolicyReferences[k]; - var parameterPolicy = _parameterPolicyFactory.Create(parameterPart, reference); - if (parameterPolicy is IOutboundParameterTransformer parameterTransformer) - { - requiredValue = parameterTransformer.TransformOutbound(requiredValue); - break; - } - } + nextParents.Add(literal); } - - var literalValue = requiredValue?.ToString() ?? throw new InvalidOperationException($"Required value for literal '{parameterPart.Name}' must evaluate to a non-null string."); - - AddLiteralNode(_includeLabel, nextParents, parent, literalValue); + k++; } } - private static void AddLiteralNode(bool includeLabel, List nextParents, DfaNode parent, string literal) + private void AddRequiredLiteralValue(RouteEndpoint endpoint, List nextParents, DfaNode parent, RoutePatternParameterPart parameterPart, object requiredValue) { - DfaNode next = null; - if (parent.Literals == null || - !parent.Literals.TryGetValue(literal, out next)) + if (endpoint.RoutePattern.ParameterPolicies.TryGetValue(parameterPart.Name, out var parameterPolicyReferences)) { - next = new DfaNode() + for (var k = 0; k < parameterPolicyReferences.Count; k++) { - PathDepth = parent.PathDepth + 1, - Label = includeLabel ? parent.Label + literal + "/" : null, - }; - parent.AddLiteral(literal, next); + var reference = parameterPolicyReferences[k]; + var parameterPolicy = _parameterPolicyFactory.Create(parameterPart, reference); + if (parameterPolicy is IOutboundParameterTransformer parameterTransformer) + { + requiredValue = parameterTransformer.TransformOutbound(requiredValue); + break; + } + } } - nextParents.Add(next); + var literalValue = requiredValue?.ToString() ?? throw new InvalidOperationException($"Required value for literal '{parameterPart.Name}' must evaluate to a non-null string."); + + AddLiteralNode(_includeLabel, nextParents, parent, literalValue); } + } - private static RoutePatternPathSegment GetCurrentSegment(RouteEndpoint endpoint, int depth) + private static void AddLiteralNode(bool includeLabel, List nextParents, DfaNode parent, string literal) + { + DfaNode next = null; + if (parent.Literals == null || + !parent.Literals.TryGetValue(literal, out next)) { - if (depth < endpoint.RoutePattern.PathSegments.Count) + next = new DfaNode() { - return endpoint.RoutePattern.PathSegments[depth]; - } + PathDepth = parent.PathDepth + 1, + Label = includeLabel ? parent.Label + literal + "/" : null, + }; + parent.AddLiteral(literal, next); + } - if (endpoint.RoutePattern.PathSegments.Count == 0) - { - return null; - } + nextParents.Add(next); + } - var lastSegment = endpoint.RoutePattern.PathSegments[endpoint.RoutePattern.PathSegments.Count - 1]; - if (lastSegment.IsSimple && lastSegment.Parts[0] is RoutePatternParameterPart parameterPart && parameterPart.IsCatchAll) - { - return lastSegment; - } + private static RoutePatternPathSegment GetCurrentSegment(RouteEndpoint endpoint, int depth) + { + if (depth < endpoint.RoutePattern.PathSegments.Count) + { + return endpoint.RoutePattern.PathSegments[depth]; + } + if (endpoint.RoutePattern.PathSegments.Count == 0) + { return null; } - private static int GetPrecedenceDigitAtDepth(RouteEndpoint endpoint, int depth) + var lastSegment = endpoint.RoutePattern.PathSegments[endpoint.RoutePattern.PathSegments.Count - 1]; + if (lastSegment.IsSimple && lastSegment.Parts[0] is RoutePatternParameterPart parameterPart && parameterPart.IsCatchAll) { - var segment = GetCurrentSegment(endpoint, depth); - if (segment is null) - { - // Treat "no segment" as high priority. it won't effect the algorithm, but we need to define a sort-order. - return 0; - } - - return RoutePrecedence.ComputeInboundPrecedenceDigit(endpoint.RoutePattern, segment); + return lastSegment; } - public override Matcher Build() + return null; + } + + private static int GetPrecedenceDigitAtDepth(RouteEndpoint endpoint, int depth) + { + var segment = GetCurrentSegment(endpoint, depth); + if (segment is null) { + // Treat "no segment" as high priority. it won't effect the algorithm, but we need to define a sort-order. + return 0; + } + + return RoutePrecedence.ComputeInboundPrecedenceDigit(endpoint.RoutePattern, segment); + } + + public override Matcher Build() + { #if DEBUG - var includeLabel = true; + var includeLabel = true; #else var includeLabel = false; #endif - var root = BuildDfaTree(includeLabel); - - // State count is the number of nodes plus an exit state - var stateCount = 1; - var maxSegmentCount = 0; - root.Visit((node) => - { - stateCount++; - maxSegmentCount = Math.Max(maxSegmentCount, node.PathDepth); - }); - _stateIndex = 0; - - // The max segment count is the maximum path-node-depth +1. We need - // the +1 to capture any additional content after the 'last' segment. - maxSegmentCount++; - - var states = new DfaState[stateCount]; - var exitDestination = stateCount - 1; - AddNode(root, states, exitDestination); - - // The root state only has a jump table. - states[exitDestination] = new DfaState( - Array.Empty(), - Array.Empty(), - JumpTableBuilder.Build(exitDestination, exitDestination, null), - null); - - return new DfaMatcher(_loggerFactory.CreateLogger(), _selector, states, maxSegmentCount); - } + var root = BuildDfaTree(includeLabel); - private int AddNode( - DfaNode node, - DfaState[] states, - int exitDestination) + // State count is the number of nodes plus an exit state + var stateCount = 1; + var maxSegmentCount = 0; + root.Visit((node) => { - node.Matches?.Sort(_comparer); + stateCount++; + maxSegmentCount = Math.Max(maxSegmentCount, node.PathDepth); + }); + _stateIndex = 0; + + // The max segment count is the maximum path-node-depth +1. We need + // the +1 to capture any additional content after the 'last' segment. + maxSegmentCount++; + + var states = new DfaState[stateCount]; + var exitDestination = stateCount - 1; + AddNode(root, states, exitDestination); + + // The root state only has a jump table. + states[exitDestination] = new DfaState( + Array.Empty(), + Array.Empty(), + JumpTableBuilder.Build(exitDestination, exitDestination, null), + null); + + return new DfaMatcher(_loggerFactory.CreateLogger(), _selector, states, maxSegmentCount); + } - var currentStateIndex = _stateIndex; + private int AddNode( + DfaNode node, + DfaState[] states, + int exitDestination) + { + node.Matches?.Sort(_comparer); - var currentDefaultDestination = exitDestination; - var currentExitDestination = exitDestination; - (string text, int destination)[] pathEntries = null; - PolicyJumpTableEdge[] policyEntries = null; + var currentStateIndex = _stateIndex; - if (node.Literals != null) - { - pathEntries = new (string text, int destination)[node.Literals.Count]; + var currentDefaultDestination = exitDestination; + var currentExitDestination = exitDestination; + (string text, int destination)[] pathEntries = null; + PolicyJumpTableEdge[] policyEntries = null; - var index = 0; - foreach (var kvp in node.Literals) - { - var transition = Transition(kvp.Value); - pathEntries[index++] = (kvp.Key, transition); - } - } + if (node.Literals != null) + { + pathEntries = new (string text, int destination)[node.Literals.Count]; - if (node.Parameters != null && - node.CatchAll != null && - ReferenceEquals(node.Parameters, node.CatchAll)) + var index = 0; + foreach (var kvp in node.Literals) { - // This node has a single transition to but it should accept zero-width segments - // this can happen when a node only has catchall parameters. - currentExitDestination = currentDefaultDestination = Transition(node.Parameters); - } - else if (node.Parameters != null && node.CatchAll != null) - { - // This node has a separate transition for zero-width segments - // this can happen when a node has both parameters and catchall parameters. - currentDefaultDestination = Transition(node.Parameters); - currentExitDestination = Transition(node.CatchAll); - } - else if (node.Parameters != null) - { - // This node has parameters but no catchall. - currentDefaultDestination = Transition(node.Parameters); - } - else if (node.CatchAll != null) - { - // This node has a catchall but no parameters - currentExitDestination = currentDefaultDestination = Transition(node.CatchAll); + var transition = Transition(kvp.Value); + pathEntries[index++] = (kvp.Key, transition); } + } - if (node.PolicyEdges != null && node.PolicyEdges.Count > 0) - { - policyEntries = new PolicyJumpTableEdge[node.PolicyEdges.Count]; + if (node.Parameters != null && + node.CatchAll != null && + ReferenceEquals(node.Parameters, node.CatchAll)) + { + // This node has a single transition to but it should accept zero-width segments + // this can happen when a node only has catchall parameters. + currentExitDestination = currentDefaultDestination = Transition(node.Parameters); + } + else if (node.Parameters != null && node.CatchAll != null) + { + // This node has a separate transition for zero-width segments + // this can happen when a node has both parameters and catchall parameters. + currentDefaultDestination = Transition(node.Parameters); + currentExitDestination = Transition(node.CatchAll); + } + else if (node.Parameters != null) + { + // This node has parameters but no catchall. + currentDefaultDestination = Transition(node.Parameters); + } + else if (node.CatchAll != null) + { + // This node has a catchall but no parameters + currentExitDestination = currentDefaultDestination = Transition(node.CatchAll); + } - var index = 0; - foreach (var kvp in node.PolicyEdges) - { - policyEntries[index++] = new PolicyJumpTableEdge(kvp.Key, Transition(kvp.Value)); - } + if (node.PolicyEdges != null && node.PolicyEdges.Count > 0) + { + policyEntries = new PolicyJumpTableEdge[node.PolicyEdges.Count]; + + var index = 0; + foreach (var kvp in node.PolicyEdges) + { + policyEntries[index++] = new PolicyJumpTableEdge(kvp.Key, Transition(kvp.Value)); } + } - var candidates = CreateCandidates(node.Matches); + var candidates = CreateCandidates(node.Matches); - // Perf: most of the time there aren't any endpoint selector policies, create - // this lazily. - List endpointSelectorPolicies = null; - if (node.Matches?.Count > 0) + // Perf: most of the time there aren't any endpoint selector policies, create + // this lazily. + List endpointSelectorPolicies = null; + if (node.Matches?.Count > 0) + { + for (var i = 0; i < _endpointSelectorPolicies.Length; i++) { - for (var i = 0; i < _endpointSelectorPolicies.Length; i++) + var endpointSelectorPolicy = _endpointSelectorPolicies[i]; + if (endpointSelectorPolicy.AppliesToEndpoints(node.Matches)) { - var endpointSelectorPolicy = _endpointSelectorPolicies[i]; - if (endpointSelectorPolicy.AppliesToEndpoints(node.Matches)) + if (endpointSelectorPolicies == null) { - if (endpointSelectorPolicies == null) - { - endpointSelectorPolicies = new List(); - } - - endpointSelectorPolicies.Add(endpointSelectorPolicy); + endpointSelectorPolicies = new List(); } + + endpointSelectorPolicies.Add(endpointSelectorPolicy); } } + } - states[currentStateIndex] = new DfaState( - candidates, - endpointSelectorPolicies?.ToArray() ?? Array.Empty(), - JumpTableBuilder.Build(currentDefaultDestination, currentExitDestination, pathEntries), - // Use the final exit destination when building the policy state. - // We don't want to use either of the current destinations because they refer routing states, - // and a policy state should never transition back to a routing state. - BuildPolicy(exitDestination, node.NodeBuilder, policyEntries)); + states[currentStateIndex] = new DfaState( + candidates, + endpointSelectorPolicies?.ToArray() ?? Array.Empty(), + JumpTableBuilder.Build(currentDefaultDestination, currentExitDestination, pathEntries), + // Use the final exit destination when building the policy state. + // We don't want to use either of the current destinations because they refer routing states, + // and a policy state should never transition back to a routing state. + BuildPolicy(exitDestination, node.NodeBuilder, policyEntries)); - return currentStateIndex; + return currentStateIndex; - int Transition(DfaNode next) + int Transition(DfaNode next) + { + // Break cycles + if (ReferenceEquals(node, next)) { - // Break cycles - if (ReferenceEquals(node, next)) - { - return _stateIndex; - } - else - { - _stateIndex++; - return AddNode(next, states, exitDestination); - } + return _stateIndex; } - } - - private static PolicyJumpTable BuildPolicy(int exitDestination, INodeBuilderPolicy nodeBuilder, PolicyJumpTableEdge[] policyEntries) - { - if (policyEntries == null) + else { - return null; + _stateIndex++; + return AddNode(next, states, exitDestination); } + } + } - return nodeBuilder.BuildJumpTable(exitDestination, policyEntries); + private static PolicyJumpTable BuildPolicy(int exitDestination, INodeBuilderPolicy nodeBuilder, PolicyJumpTableEdge[] policyEntries) + { + if (policyEntries == null) + { + return null; } - // Builds an array of candidates for a node, assigns a 'score' for each - // endpoint. - internal Candidate[] CreateCandidates(IReadOnlyList endpoints) + return nodeBuilder.BuildJumpTable(exitDestination, policyEntries); + } + + // Builds an array of candidates for a node, assigns a 'score' for each + // endpoint. + internal Candidate[] CreateCandidates(IReadOnlyList endpoints) + { + if (endpoints == null || endpoints.Count == 0) { - if (endpoints == null || endpoints.Count == 0) - { - return Array.Empty(); - } + return Array.Empty(); + } - var candiates = new Candidate[endpoints.Count]; + var candiates = new Candidate[endpoints.Count]; - var score = 0; - var examplar = endpoints[0]; - candiates[0] = CreateCandidate(examplar, score); + var score = 0; + var examplar = endpoints[0]; + candiates[0] = CreateCandidate(examplar, score); - for (var i = 1; i < endpoints.Count; i++) + for (var i = 1; i < endpoints.Count; i++) + { + var endpoint = endpoints[i]; + if (!_comparer.Equals(examplar, endpoint)) { - var endpoint = endpoints[i]; - if (!_comparer.Equals(examplar, endpoint)) - { - // This endpoint doesn't have the same priority. - examplar = endpoint; - score++; - } - - candiates[i] = CreateCandidate(endpoint, score); + // This endpoint doesn't have the same priority. + examplar = endpoint; + score++; } - return candiates; + candiates[i] = CreateCandidate(endpoint, score); } - // internal for tests - internal Candidate CreateCandidate(Endpoint endpoint, int score) + return candiates; + } + + // internal for tests + internal Candidate CreateCandidate(Endpoint endpoint, int score) + { + (string parameterName, int segmentIndex, int slotIndex) catchAll = default; + + if (endpoint is RouteEndpoint routeEndpoint) { - (string parameterName, int segmentIndex, int slotIndex) catchAll = default; + _assignments.Clear(); + _slots.Clear(); + _captures.Clear(); + _complexSegments.Clear(); + _constraints.Clear(); - if (endpoint is RouteEndpoint routeEndpoint) + foreach (var kvp in routeEndpoint.RoutePattern.Defaults) { - _assignments.Clear(); - _slots.Clear(); - _captures.Clear(); - _complexSegments.Clear(); - _constraints.Clear(); + _assignments.Add(kvp.Key, _assignments.Count); + _slots.Add(kvp); + } - foreach (var kvp in routeEndpoint.RoutePattern.Defaults) + for (var i = 0; i < routeEndpoint.RoutePattern.PathSegments.Count; i++) + { + var segment = routeEndpoint.RoutePattern.PathSegments[i]; + if (!segment.IsSimple) { - _assignments.Add(kvp.Key, _assignments.Count); - _slots.Add(kvp); + continue; } - for (var i = 0; i < routeEndpoint.RoutePattern.PathSegments.Count; i++) + var parameterPart = segment.Parts[0] as RoutePatternParameterPart; + if (parameterPart == null) { - var segment = routeEndpoint.RoutePattern.PathSegments[i]; - if (!segment.IsSimple) - { - continue; - } - - var parameterPart = segment.Parts[0] as RoutePatternParameterPart; - if (parameterPart == null) - { - continue; - } - - if (!_assignments.TryGetValue(parameterPart.Name, out var slotIndex)) - { - slotIndex = _assignments.Count; - _assignments.Add(parameterPart.Name, slotIndex); + continue; + } - // A parameter can have a required value, default value/catch all, or be a normal parameter - // Add the required value or default value as the slot's initial value - if (TryGetRequiredValue(routeEndpoint.RoutePattern, parameterPart, out var requiredValue)) - { - _slots.Add(new KeyValuePair(parameterPart.Name, requiredValue)); - } - else - { - var hasDefaultValue = parameterPart.Default != null || parameterPart.IsCatchAll; - _slots.Add(hasDefaultValue ? new KeyValuePair(parameterPart.Name, parameterPart.Default) : default); - } - } + if (!_assignments.TryGetValue(parameterPart.Name, out var slotIndex)) + { + slotIndex = _assignments.Count; + _assignments.Add(parameterPart.Name, slotIndex); - if (TryGetRequiredValue(routeEndpoint.RoutePattern, parameterPart, out _)) - { - // Don't capture a parameter if it has a required value - // There is no need because a parameter with a required value is matched as a literal - } - else if (parameterPart.IsCatchAll) + // A parameter can have a required value, default value/catch all, or be a normal parameter + // Add the required value or default value as the slot's initial value + if (TryGetRequiredValue(routeEndpoint.RoutePattern, parameterPart, out var requiredValue)) { - catchAll = (parameterPart.Name, i, slotIndex); + _slots.Add(new KeyValuePair(parameterPart.Name, requiredValue)); } else { - _captures.Add((parameterPart.Name, i, slotIndex)); + var hasDefaultValue = parameterPart.Default != null || parameterPart.IsCatchAll; + _slots.Add(hasDefaultValue ? new KeyValuePair(parameterPart.Name, parameterPart.Default) : default); } } - for (var i = 0; i < routeEndpoint.RoutePattern.PathSegments.Count; i++) + if (TryGetRequiredValue(routeEndpoint.RoutePattern, parameterPart, out _)) { - var segment = routeEndpoint.RoutePattern.PathSegments[i]; - if (segment.IsSimple) - { - continue; - } - - _complexSegments.Add((segment, i)); + // Don't capture a parameter if it has a required value + // There is no need because a parameter with a required value is matched as a literal } - - foreach (var kvp in routeEndpoint.RoutePattern.ParameterPolicies) + else if (parameterPart.IsCatchAll) { - var parameter = routeEndpoint.RoutePattern.GetParameter(kvp.Key); // may be null, that's ok - var parameterPolicyReferences = kvp.Value; - for (var i = 0; i < parameterPolicyReferences.Count; i++) - { - var reference = parameterPolicyReferences[i]; - var parameterPolicy = _parameterPolicyFactory.Create(parameter, reference); - if (parameterPolicy is IRouteConstraint routeConstraint) - { - _constraints.Add(new KeyValuePair(kvp.Key, routeConstraint)); - } - } + catchAll = (parameterPart.Name, i, slotIndex); + } + else + { + _captures.Add((parameterPart.Name, i, slotIndex)); } - - return new Candidate( - endpoint, - score, - _slots.ToArray(), - _captures.ToArray(), - catchAll, - _complexSegments.ToArray(), - _constraints.ToArray()); - } - else - { - return new Candidate( - endpoint, - score, - Array.Empty>(), - Array.Empty<(string parameterName, int segmentIndex, int slotIndex)>(), - catchAll, - Array.Empty<(RoutePatternPathSegment pathSegment, int segmentIndex)>(), - Array.Empty>()); } - } - private static bool HasAdditionalRequiredSegments(RouteEndpoint endpoint, int depth) - { - for (var i = depth; i < endpoint.RoutePattern.PathSegments.Count; i++) + for (var i = 0; i < routeEndpoint.RoutePattern.PathSegments.Count; i++) { - var segment = endpoint.RoutePattern.PathSegments[i]; - if (!segment.IsSimple) + var segment = routeEndpoint.RoutePattern.PathSegments[i]; + if (segment.IsSimple) { - // Complex segments always require more processing - return true; + continue; } - var parameterPart = segment.Parts[0] as RoutePatternParameterPart; - if (parameterPart == null) - { - // It's a literal - return true; - } + _complexSegments.Add((segment, i)); + } - if (!parameterPart.IsOptional && - !parameterPart.IsCatchAll && - parameterPart.Default == null) + foreach (var kvp in routeEndpoint.RoutePattern.ParameterPolicies) + { + var parameter = routeEndpoint.RoutePattern.GetParameter(kvp.Key); // may be null, that's ok + var parameterPolicyReferences = kvp.Value; + for (var i = 0; i < parameterPolicyReferences.Count; i++) { - return true; + var reference = parameterPolicyReferences[i]; + var parameterPolicy = _parameterPolicyFactory.Create(parameter, reference); + if (parameterPolicy is IRouteConstraint routeConstraint) + { + _constraints.Add(new KeyValuePair(kvp.Key, routeConstraint)); + } } } - return false; + return new Candidate( + endpoint, + score, + _slots.ToArray(), + _captures.ToArray(), + catchAll, + _complexSegments.ToArray(), + _constraints.ToArray()); + } + else + { + return new Candidate( + endpoint, + score, + Array.Empty>(), + Array.Empty<(string parameterName, int segmentIndex, int slotIndex)>(), + catchAll, + Array.Empty<(RoutePatternPathSegment pathSegment, int segmentIndex)>(), + Array.Empty>()); } + } - private void ApplyPolicies(DfaNode node) + private static bool HasAdditionalRequiredSegments(RouteEndpoint endpoint, int depth) + { + for (var i = depth; i < endpoint.RoutePattern.PathSegments.Count; i++) { - if (node.Matches == null || node.Matches.Count == 0) + var segment = endpoint.RoutePattern.PathSegments[i]; + if (!segment.IsSimple) { - return; + // Complex segments always require more processing + return true; } - // We're done with the precedence based work. Sort the endpoints - // before applying policies for simplicity in policy-related code. - node.Matches.Sort(_comparer); + var parameterPart = segment.Parts[0] as RoutePatternParameterPart; + if (parameterPart == null) + { + // It's a literal + return true; + } - // Start with the current node as the root. - var work = new List() { node, }; - List previousWork = null; - for (var i = 0; i < _nodeBuilders.Length; i++) + if (!parameterPart.IsOptional && + !parameterPart.IsCatchAll && + parameterPart.Default == null) { - var nodeBuilder = _nodeBuilders[i]; + return true; + } + } - // Build a list of each - List nextWork; - if (previousWork == null) - { - nextWork = new List(); - } - else + return false; + } + + private void ApplyPolicies(DfaNode node) + { + if (node.Matches == null || node.Matches.Count == 0) + { + return; + } + + // We're done with the precedence based work. Sort the endpoints + // before applying policies for simplicity in policy-related code. + node.Matches.Sort(_comparer); + + // Start with the current node as the root. + var work = new List() { node, }; + List previousWork = null; + for (var i = 0; i < _nodeBuilders.Length; i++) + { + var nodeBuilder = _nodeBuilders[i]; + + // Build a list of each + List nextWork; + if (previousWork == null) + { + nextWork = new List(); + } + else + { + // Reuse previous collection for the next collection + previousWork.Clear(); + nextWork = previousWork; + } + + for (var j = 0; j < work.Count; j++) + { + var parent = work[j]; + if (!nodeBuilder.AppliesToEndpoints(parent.Matches ?? (IReadOnlyList)Array.Empty())) { - // Reuse previous collection for the next collection - previousWork.Clear(); - nextWork = previousWork; + // This node-builder doesn't care about this node, so add it to the list + // to be processed by the next node-builder. + nextWork.Add(parent); + continue; } - for (var j = 0; j < work.Count; j++) + // This node-builder does apply to this node, so we need to create new nodes for each edge, + // and then attach them to the parent. + var edges = nodeBuilder.GetEdges(parent.Matches ?? (IReadOnlyList)Array.Empty()); + for (var k = 0; k < edges.Count; k++) { - var parent = work[j]; - if (!nodeBuilder.AppliesToEndpoints(parent.Matches ?? (IReadOnlyList)Array.Empty())) - { - // This node-builder doesn't care about this node, so add it to the list - // to be processed by the next node-builder. - nextWork.Add(parent); - continue; - } + var edge = edges[k]; - // This node-builder does apply to this node, so we need to create new nodes for each edge, - // and then attach them to the parent. - var edges = nodeBuilder.GetEdges(parent.Matches ?? (IReadOnlyList)Array.Empty()); - for (var k = 0; k < edges.Count; k++) + var next = new DfaNode() { - var edge = edges[k]; - - var next = new DfaNode() - { - // If parent label is null then labels are not being included - Label = (parent.Label != null) ? parent.Label + " " + edge.State.ToString() : null, - }; - - if (edge.Endpoints.Count > 0) - { - next.AddMatches(edge.Endpoints); - } - nextWork.Add(next); + // If parent label is null then labels are not being included + Label = (parent.Label != null) ? parent.Label + " " + edge.State.ToString() : null, + }; - parent.AddPolicyEdge(edge.State, next); + if (edge.Endpoints.Count > 0) + { + next.AddMatches(edge.Endpoints); } + nextWork.Add(next); - // Associate the node-builder so we can build a jump table later. - parent.NodeBuilder = nodeBuilder; - - // The parent no longer has matches, it's not considered a terminal node. - parent.Matches?.Clear(); + parent.AddPolicyEdge(edge.State, next); } - previousWork = work; - work = nextWork; + // Associate the node-builder so we can build a jump table later. + parent.NodeBuilder = nodeBuilder; + + // The parent no longer has matches, it's not considered a terminal node. + parent.Matches?.Clear(); } + + previousWork = work; + work = nextWork; } + } - private static (INodeBuilderPolicy[] nodeBuilderPolicies, IEndpointComparerPolicy[] endpointComparerPolicies, IEndpointSelectorPolicy[] endpointSelectorPolicies) ExtractPolicies(IEnumerable policies) - { - var nodeBuilderPolicies = new List(); - var endpointComparerPolicies = new List(); - var endpointSelectorPolicies = new List(); + private static (INodeBuilderPolicy[] nodeBuilderPolicies, IEndpointComparerPolicy[] endpointComparerPolicies, IEndpointSelectorPolicy[] endpointSelectorPolicies) ExtractPolicies(IEnumerable policies) + { + var nodeBuilderPolicies = new List(); + var endpointComparerPolicies = new List(); + var endpointSelectorPolicies = new List(); - foreach (var policy in policies) + foreach (var policy in policies) + { + if (policy is INodeBuilderPolicy nodeBuilderPolicy) { - if (policy is INodeBuilderPolicy nodeBuilderPolicy) - { - nodeBuilderPolicies.Add(nodeBuilderPolicy); - } - - if (policy is IEndpointComparerPolicy endpointComparerPolicy) - { - endpointComparerPolicies.Add(endpointComparerPolicy); - } - - if (policy is IEndpointSelectorPolicy endpointSelectorPolicy) - { - endpointSelectorPolicies.Add(endpointSelectorPolicy); - } + nodeBuilderPolicies.Add(nodeBuilderPolicy); } - return (nodeBuilderPolicies.ToArray(), endpointComparerPolicies.ToArray(), endpointSelectorPolicies.ToArray()); - } + if (policy is IEndpointComparerPolicy endpointComparerPolicy) + { + endpointComparerPolicies.Add(endpointComparerPolicy); + } - private static bool TryGetRequiredValue(RoutePattern routePattern, RoutePatternParameterPart parameterPart, out object value) - { - if (!routePattern.RequiredValues.TryGetValue(parameterPart.Name, out value)) + if (policy is IEndpointSelectorPolicy endpointSelectorPolicy) { - return false; + endpointSelectorPolicies.Add(endpointSelectorPolicy); } + } + + return (nodeBuilderPolicies.ToArray(), endpointComparerPolicies.ToArray(), endpointSelectorPolicies.ToArray()); + } - return !RouteValueEqualityComparer.Default.Equals(value, string.Empty); + private static bool TryGetRequiredValue(RoutePattern routePattern, RoutePatternParameterPart parameterPart, out object value) + { + if (!routePattern.RequiredValues.TryGetValue(parameterPart.Name, out value)) + { + return false; } - private record struct DfaBuilderWorkerWorkItem(RouteEndpoint Endpoint, int PrecedenceDigit, List Parents); + return !RouteValueEqualityComparer.Default.Equals(value, string.Empty); } + + private record struct DfaBuilderWorkerWorkItem(RouteEndpoint Endpoint, int PrecedenceDigit, List Parents); } diff --git a/src/Http/Routing/src/Matching/DfaMatcherFactory.cs b/src/Http/Routing/src/Matching/DfaMatcherFactory.cs index 9f6587b0c0..0e7298e407 100644 --- a/src/Http/Routing/src/Matching/DfaMatcherFactory.cs +++ b/src/Http/Routing/src/Matching/DfaMatcherFactory.cs @@ -4,39 +4,38 @@ using System; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal class DfaMatcherFactory : MatcherFactory { - internal class DfaMatcherFactory : MatcherFactory - { - private readonly IServiceProvider _services; + private readonly IServiceProvider _services; - // Using the service provider here so we can avoid coupling to the dependencies - // of DfaMatcherBuilder. - public DfaMatcherFactory(IServiceProvider services) + // Using the service provider here so we can avoid coupling to the dependencies + // of DfaMatcherBuilder. + public DfaMatcherFactory(IServiceProvider services) + { + if (services == null) { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - _services = services; + throw new ArgumentNullException(nameof(services)); } - public override Matcher CreateMatcher(EndpointDataSource dataSource) + _services = services; + } + + public override Matcher CreateMatcher(EndpointDataSource dataSource) + { + if (dataSource == null) { - if (dataSource == null) - { - throw new ArgumentNullException(nameof(dataSource)); - } - - // Creates a tracking entry in DI to stop listening for change events - // when the services are disposed. - var lifetime = _services.GetRequiredService(); - - return new DataSourceDependentMatcher(dataSource, lifetime, () => - { - return _services.GetRequiredService(); - }); + throw new ArgumentNullException(nameof(dataSource)); } + + // Creates a tracking entry in DI to stop listening for change events + // when the services are disposed. + var lifetime = _services.GetRequiredService(); + + return new DataSourceDependentMatcher(dataSource, lifetime, () => + { + return _services.GetRequiredService(); + }); } } diff --git a/src/Http/Routing/src/Matching/DfaNode.cs b/src/Http/Routing/src/Matching/DfaNode.cs index eec7170625..286a4f3e2c 100644 --- a/src/Http/Routing/src/Matching/DfaNode.cs +++ b/src/Http/Routing/src/Matching/DfaNode.cs @@ -10,128 +10,127 @@ using System.Linq; using System.Text; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// Intermediate data structure used to build the DFA. Not used at runtime. +[DebuggerDisplay("{DebuggerToString(),nq}")] +internal class DfaNode { - // Intermediate data structure used to build the DFA. Not used at runtime. - [DebuggerDisplay("{DebuggerToString(),nq}")] - internal class DfaNode - { - // The depth of the node. The depth indicates the number of segments - // that must be processed to arrive at this node. - // - // This value is not computed for Policy nodes and will be set to -1. - public int PathDepth { get; set; } = -1; + // The depth of the node. The depth indicates the number of segments + // that must be processed to arrive at this node. + // + // This value is not computed for Policy nodes and will be set to -1. + public int PathDepth { get; set; } = -1; - // Just for diagnostics and debugging - public string Label { get; set; } + // Just for diagnostics and debugging + public string Label { get; set; } - public List Matches { get; private set; } + public List Matches { get; private set; } - public Dictionary Literals { get; private set; } + public Dictionary Literals { get; private set; } - public DfaNode Parameters { get; set; } + public DfaNode Parameters { get; set; } - public DfaNode CatchAll { get; set; } + public DfaNode CatchAll { get; set; } - public INodeBuilderPolicy NodeBuilder { get; set; } + public INodeBuilderPolicy NodeBuilder { get; set; } - public Dictionary PolicyEdges { get; private set; } + public Dictionary PolicyEdges { get; private set; } - public void AddPolicyEdge(object state, DfaNode node) + public void AddPolicyEdge(object state, DfaNode node) + { + if (PolicyEdges == null) { - if (PolicyEdges == null) - { - PolicyEdges = new Dictionary(); - } - - PolicyEdges.Add(state, node); + PolicyEdges = new Dictionary(); } - public void AddLiteral(string literal, DfaNode node) - { - if (Literals == null) - { - Literals = new Dictionary(StringComparer.OrdinalIgnoreCase); - } + PolicyEdges.Add(state, node); + } - Literals.Add(literal, node); + public void AddLiteral(string literal, DfaNode node) + { + if (Literals == null) + { + Literals = new Dictionary(StringComparer.OrdinalIgnoreCase); } - public void AddMatch(Endpoint endpoint) + Literals.Add(literal, node); + } + + public void AddMatch(Endpoint endpoint) + { + if (Matches == null) { - if (Matches == null) - { - Matches = new List(); - } + Matches = new List(); + } - Matches.Add(endpoint); + Matches.Add(endpoint); + } + + public void AddMatches(IEnumerable endpoints) + { + if (Matches == null) + { + Matches = new List(endpoints); + } + else + { + Matches.AddRange(endpoints); } + } - public void AddMatches(IEnumerable endpoints) + public void Visit(Action visitor) + { + if (Literals != null) { - if (Matches == null) - { - Matches = new List(endpoints); - } - else + foreach (var kvp in Literals) { - Matches.AddRange(endpoints); + kvp.Value.Visit(visitor); } } - public void Visit(Action visitor) + // Break cycles + if (Parameters != null && !ReferenceEquals(this, Parameters)) { - if (Literals != null) - { - foreach (var kvp in Literals) - { - kvp.Value.Visit(visitor); - } - } + Parameters.Visit(visitor); + } - // Break cycles - if (Parameters != null && !ReferenceEquals(this, Parameters)) - { - Parameters.Visit(visitor); - } + // Break cycles + if (CatchAll != null && !ReferenceEquals(this, CatchAll)) + { + CatchAll.Visit(visitor); + } - // Break cycles - if (CatchAll != null && !ReferenceEquals(this, CatchAll)) + if (PolicyEdges != null) + { + foreach (var kvp in PolicyEdges) { - CatchAll.Visit(visitor); + kvp.Value.Visit(visitor); } + } - if (PolicyEdges != null) - { - foreach (var kvp in PolicyEdges) - { - kvp.Value.Visit(visitor); - } - } + visitor(this); + } - visitor(this); + private string DebuggerToString() + { + var builder = new StringBuilder(); + builder.Append(Label); + builder.Append(" d:"); + builder.Append(PathDepth); + builder.Append(" m:"); + builder.Append(Matches?.Count ?? 0); + builder.Append(" c: "); + if (Literals != null) + { + builder.AppendJoin(", ", Literals.Select(kvp => $"{kvp.Key}->({FormatNode(kvp.Value)})")); } + return builder.ToString(); - private string DebuggerToString() + // DfaNodes can be self-referential, don't traverse cycles. + string FormatNode(DfaNode other) { - var builder = new StringBuilder(); - builder.Append(Label); - builder.Append(" d:"); - builder.Append(PathDepth); - builder.Append(" m:"); - builder.Append(Matches?.Count ?? 0); - builder.Append(" c: "); - if (Literals != null) - { - builder.AppendJoin(", ", Literals.Select(kvp => $"{kvp.Key}->({FormatNode(kvp.Value)})")); - } - return builder.ToString(); - - // DfaNodes can be self-referential, don't traverse cycles. - string FormatNode(DfaNode other) - { - return ReferenceEquals(this, other) ? "this" : other.DebuggerToString(); - } + return ReferenceEquals(this, other) ? "this" : other.DebuggerToString(); } } } diff --git a/src/Http/Routing/src/Matching/DfaState.cs b/src/Http/Routing/src/Matching/DfaState.cs index 04ec059c33..a33a47cbc8 100644 --- a/src/Http/Routing/src/Matching/DfaState.cs +++ b/src/Http/Routing/src/Matching/DfaState.cs @@ -3,34 +3,33 @@ using System.Diagnostics; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +[DebuggerDisplay("{DebuggerToString(),nq}")] +internal readonly struct DfaState { - [DebuggerDisplay("{DebuggerToString(),nq}")] - internal readonly struct DfaState - { - public readonly Candidate[] Candidates; - public readonly IEndpointSelectorPolicy[] Policies; - public readonly JumpTable PathTransitions; - public readonly PolicyJumpTable PolicyTransitions; + public readonly Candidate[] Candidates; + public readonly IEndpointSelectorPolicy[] Policies; + public readonly JumpTable PathTransitions; + public readonly PolicyJumpTable PolicyTransitions; - public DfaState( - Candidate[] candidates, - IEndpointSelectorPolicy[] policies, - JumpTable pathTransitions, - PolicyJumpTable policyTransitions) - { - Candidates = candidates; - Policies = policies; - PathTransitions = pathTransitions; - PolicyTransitions = policyTransitions; - } + public DfaState( + Candidate[] candidates, + IEndpointSelectorPolicy[] policies, + JumpTable pathTransitions, + PolicyJumpTable policyTransitions) + { + Candidates = candidates; + Policies = policies; + PathTransitions = pathTransitions; + PolicyTransitions = policyTransitions; + } - public string DebuggerToString() - { - return - $"matches: {Candidates?.Length ?? 0}, " + - $"path: ({PathTransitions?.DebuggerToString()}), " + - $"policy: ({PolicyTransitions?.DebuggerToString()})"; - } + public string DebuggerToString() + { + return + $"matches: {Candidates?.Length ?? 0}, " + + $"path: ({PathTransitions?.DebuggerToString()}), " + + $"policy: ({PolicyTransitions?.DebuggerToString()})"; } } diff --git a/src/Http/Routing/src/Matching/DictionaryJumpTable.cs b/src/Http/Routing/src/Matching/DictionaryJumpTable.cs index b4649d18d8..8efe718ea0 100644 --- a/src/Http/Routing/src/Matching/DictionaryJumpTable.cs +++ b/src/Http/Routing/src/Matching/DictionaryJumpTable.cs @@ -6,63 +6,62 @@ using System.Collections.Generic; using System.Linq; using System.Text; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal class DictionaryJumpTable : JumpTable { - internal class DictionaryJumpTable : JumpTable + private readonly int _defaultDestination; + private readonly int _exitDestination; + private readonly Dictionary _dictionary; + + public DictionaryJumpTable( + int defaultDestination, + int exitDestination, + (string text, int destination)[] entries) { - private readonly int _defaultDestination; - private readonly int _exitDestination; - private readonly Dictionary _dictionary; + _defaultDestination = defaultDestination; + _exitDestination = exitDestination; - public DictionaryJumpTable( - int defaultDestination, - int exitDestination, - (string text, int destination)[] entries) + _dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < entries.Length; i++) { - _defaultDestination = defaultDestination; - _exitDestination = exitDestination; - - _dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); - for (var i = 0; i < entries.Length; i++) - { - _dictionary.Add(entries[i].text, entries[i].destination); - } + _dictionary.Add(entries[i].text, entries[i].destination); } + } - public override int GetDestination(string path, PathSegment segment) + public override int GetDestination(string path, PathSegment segment) + { + if (segment.Length == 0) { - if (segment.Length == 0) - { - return _exitDestination; - } - - var text = path.Substring(segment.Start, segment.Length); - if (_dictionary.TryGetValue(text, out var destination)) - { - return destination; - } - - return _defaultDestination; + return _exitDestination; } - public override string DebuggerToString() + var text = path.Substring(segment.Start, segment.Length); + if (_dictionary.TryGetValue(text, out var destination)) { - var builder = new StringBuilder(); - builder.Append("{ "); + return destination; + } + + return _defaultDestination; + } - builder.AppendJoin(", ", _dictionary.Select(kvp => $"{kvp.Key}: {kvp.Value}")); + public override string DebuggerToString() + { + var builder = new StringBuilder(); + builder.Append("{ "); - builder.Append("$+: "); - builder.Append(_defaultDestination); - builder.Append(", "); + builder.AppendJoin(", ", _dictionary.Select(kvp => $"{kvp.Key}: {kvp.Value}")); - builder.Append("$0: "); - builder.Append(_defaultDestination); + builder.Append("$+: "); + builder.Append(_defaultDestination); + builder.Append(", "); - builder.Append(" }"); + builder.Append("$0: "); + builder.Append(_defaultDestination); + builder.Append(" }"); - return builder.ToString(); - } + + return builder.ToString(); } } diff --git a/src/Http/Routing/src/Matching/EndpointComparer.cs b/src/Http/Routing/src/Matching/EndpointComparer.cs index 664a0de9c9..5371ae2cac 100644 --- a/src/Http/Routing/src/Matching/EndpointComparer.cs +++ b/src/Http/Routing/src/Matching/EndpointComparer.cs @@ -6,54 +6,111 @@ using System.Collections.Generic; using System.Diagnostics; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// Use to sort and group Endpoints. RouteEndpoints are sorted before other implementations. +// +// NOTE: +// When ordering endpoints, we compare the route templates as an absolute last resort. +// This is used as a factor to ensure that we always have a predictable ordering +// for tests, errors, etc. +// +// When we group endpoints we don't consider the route template, because we're trying +// to group endpoints not separate them. +// +// TLDR: +// IComparer implementation considers the template string as a tiebreaker. +// IEqualityComparer implementation does not. +// This is cool and good. +internal class EndpointComparer : IComparer, IEqualityComparer { - // Use to sort and group Endpoints. RouteEndpoints are sorted before other implementations. - // - // NOTE: - // When ordering endpoints, we compare the route templates as an absolute last resort. - // This is used as a factor to ensure that we always have a predictable ordering - // for tests, errors, etc. - // - // When we group endpoints we don't consider the route template, because we're trying - // to group endpoints not separate them. - // - // TLDR: - // IComparer implementation considers the template string as a tiebreaker. - // IEqualityComparer implementation does not. - // This is cool and good. - internal class EndpointComparer : IComparer, IEqualityComparer + private readonly IComparer[] _comparers; + + public EndpointComparer(IEndpointComparerPolicy[] policies) + { + // Order, Precedence, (others)... + _comparers = new IComparer[2 + policies.Length]; + _comparers[0] = OrderComparer.Instance; + _comparers[1] = PrecedenceComparer.Instance; + for (var i = 0; i < policies.Length; i++) + { + _comparers[i + 2] = policies[i].Comparer; + } + } + + public int Compare(Endpoint? x, Endpoint? y) { - private readonly IComparer[] _comparers; + // We don't expose this publicly, and we should never call it on + // a null endpoint. + Debug.Assert(x != null); + Debug.Assert(y != null); + + var compare = CompareCore(x, y); - public EndpointComparer(IEndpointComparerPolicy[] policies) + // Since we're sorting, use the route template as a last resort. + return compare == 0 ? ComparePattern(x, y) : compare; + } + + private static int ComparePattern(Endpoint x, Endpoint y) + { + // A RouteEndpoint always comes before a non-RouteEndpoint, regardless of its RawText value + var routeEndpointX = x as RouteEndpoint; + var routeEndpointY = y as RouteEndpoint; + + if (routeEndpointX != null) { - // Order, Precedence, (others)... - _comparers = new IComparer[2 + policies.Length]; - _comparers[0] = OrderComparer.Instance; - _comparers[1] = PrecedenceComparer.Instance; - for (var i = 0; i < policies.Length; i++) + if (routeEndpointY != null) { - _comparers[i + 2] = policies[i].Comparer; + return string.Compare(routeEndpointX.RoutePattern.RawText, routeEndpointY.RoutePattern.RawText, StringComparison.OrdinalIgnoreCase); } - } - public int Compare(Endpoint? x, Endpoint? y) + return 1; + } + else if (routeEndpointY != null) { - // We don't expose this publicly, and we should never call it on - // a null endpoint. - Debug.Assert(x != null); - Debug.Assert(y != null); + return -1; + } - var compare = CompareCore(x, y); + return 0; + } + + public bool Equals(Endpoint? x, Endpoint? y) + { + // We don't expose this publicly, and we should never call it on + // a null endpoint. + Debug.Assert(x != null); + Debug.Assert(y != null); - // Since we're sorting, use the route template as a last resort. - return compare == 0 ? ComparePattern(x, y) : compare; + return CompareCore(x, y) == 0; + } + + public int GetHashCode(Endpoint obj) + { + // This should not be possible to call publicly. + Debug.Fail("We don't expect this to be called."); + throw new System.NotImplementedException(); + } + + private int CompareCore(Endpoint x, Endpoint y) + { + for (var i = 0; i < _comparers.Length; i++) + { + var compare = _comparers[i].Compare(x, y); + if (compare != 0) + { + return compare; + } } - private static int ComparePattern(Endpoint x, Endpoint y) + return 0; + } + + private class OrderComparer : IComparer + { + public static readonly IComparer Instance = new OrderComparer(); + + public int Compare(Endpoint? x, Endpoint? y) { - // A RouteEndpoint always comes before a non-RouteEndpoint, regardless of its RawText value var routeEndpointX = x as RouteEndpoint; var routeEndpointY = y as RouteEndpoint; @@ -61,7 +118,7 @@ namespace Microsoft.AspNetCore.Routing.Matching { if (routeEndpointY != null) { - return string.Compare(routeEndpointX.RoutePattern.RawText, routeEndpointY.RoutePattern.RawText, StringComparison.OrdinalIgnoreCase); + return routeEndpointX.Order.CompareTo(routeEndpointY.Order); } return 1; @@ -73,91 +130,33 @@ namespace Microsoft.AspNetCore.Routing.Matching return 0; } + } - public bool Equals(Endpoint? x, Endpoint? y) - { - // We don't expose this publicly, and we should never call it on - // a null endpoint. - Debug.Assert(x != null); - Debug.Assert(y != null); - - return CompareCore(x, y) == 0; - } - - public int GetHashCode(Endpoint obj) - { - // This should not be possible to call publicly. - Debug.Fail("We don't expect this to be called."); - throw new System.NotImplementedException(); - } - - private int CompareCore(Endpoint x, Endpoint y) - { - for (var i = 0; i < _comparers.Length; i++) - { - var compare = _comparers[i].Compare(x, y); - if (compare != 0) - { - return compare; - } - } - - return 0; - } + private class PrecedenceComparer : IComparer + { + public static readonly IComparer Instance = new PrecedenceComparer(); - private class OrderComparer : IComparer + public int Compare(Endpoint? x, Endpoint? y) { - public static readonly IComparer Instance = new OrderComparer(); + var routeEndpointX = x as RouteEndpoint; + var routeEndpointY = y as RouteEndpoint; - public int Compare(Endpoint? x, Endpoint? y) + if (routeEndpointX != null) { - var routeEndpointX = x as RouteEndpoint; - var routeEndpointY = y as RouteEndpoint; - - if (routeEndpointX != null) - { - if (routeEndpointY != null) - { - return routeEndpointX.Order.CompareTo(routeEndpointY.Order); - } - - return 1; - } - else if (routeEndpointY != null) + if (routeEndpointY != null) { - return -1; + return routeEndpointX.RoutePattern.InboundPrecedence + .CompareTo(routeEndpointY.RoutePattern.InboundPrecedence); } - return 0; + return 1; } - } - - private class PrecedenceComparer : IComparer - { - public static readonly IComparer Instance = new PrecedenceComparer(); - - public int Compare(Endpoint? x, Endpoint? y) + else if (routeEndpointY != null) { - var routeEndpointX = x as RouteEndpoint; - var routeEndpointY = y as RouteEndpoint; - - if (routeEndpointX != null) - { - if (routeEndpointY != null) - { - return routeEndpointX.RoutePattern.InboundPrecedence - .CompareTo(routeEndpointY.RoutePattern.InboundPrecedence); - } - - return 1; - } - else if (routeEndpointY != null) - { - return -1; - } - - return 0; + return -1; } + + return 0; } } } diff --git a/src/Http/Routing/src/Matching/EndpointMetadataComparer.cs b/src/Http/Routing/src/Matching/EndpointMetadataComparer.cs index 2380cd8197..ce74508e3f 100644 --- a/src/Http/Routing/src/Matching/EndpointMetadataComparer.cs +++ b/src/Http/Routing/src/Matching/EndpointMetadataComparer.cs @@ -9,165 +9,164 @@ using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +/// +/// A comparer that can order instances based on implementations of +/// . The implementation can be retrieved from the service +/// provider and provided to . +/// +public sealed class EndpointMetadataComparer : IComparer { - /// - /// A comparer that can order instances based on implementations of - /// . The implementation can be retrieved from the service - /// provider and provided to . - /// - public sealed class EndpointMetadataComparer : IComparer + private readonly IServiceProvider _services; + private IComparer[]? _comparers; + + // This type is **INTENDED** for use in MatcherPolicy instances yet is also needs the MatcherPolicy instances. + // using IServiceProvider to break the cycle. + internal EndpointMetadataComparer(IServiceProvider services) { - private readonly IServiceProvider _services; - private IComparer[]? _comparers; + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + _services = services; + } - // This type is **INTENDED** for use in MatcherPolicy instances yet is also needs the MatcherPolicy instances. - // using IServiceProvider to break the cycle. - internal EndpointMetadataComparer(IServiceProvider services) + private IComparer[] Comparers + { + get { - if (services == null) + if (_comparers == null) { - throw new ArgumentNullException(nameof(services)); + _comparers = _services.GetServices() + .OrderBy(p => p.Order) + .OfType() + .Select(p => p.Comparer) + .ToArray(); } - _services = services; + return _comparers; } + } - private IComparer[] Comparers + int IComparer.Compare(Endpoint? x, Endpoint? y) + { + if (x == null) { - get - { - if (_comparers == null) - { - _comparers = _services.GetServices() - .OrderBy(p => p.Order) - .OfType() - .Select(p => p.Comparer) - .ToArray(); - } - - return _comparers; - } + throw new ArgumentNullException(nameof(x)); } - int IComparer.Compare(Endpoint? x, Endpoint? y) + if (y == null) { - if (x == null) - { - throw new ArgumentNullException(nameof(x)); - } - - if (y == null) - { - throw new ArgumentNullException(nameof(y)); - } + throw new ArgumentNullException(nameof(y)); + } - var comparers = Comparers; - for (var i = 0; i < comparers.Length; i++) + var comparers = Comparers; + for (var i = 0; i < comparers.Length; i++) + { + var compare = comparers[i].Compare(x, y); + if (compare != 0) { - var compare = comparers[i].Compare(x, y); - if (compare != 0) - { - return compare; - } + return compare; } - - return 0; } + + return 0; } +} + +/// +/// A base class for implementations that use +/// a specific type of metadata from for comparison. +/// Useful for implementing . +/// +/// +/// The type of metadata to compare. Typically this is a type of metadata related +/// to the application concern being handled. +/// +public abstract class EndpointMetadataComparer : IComparer where TMetadata : class +{ + /// + /// A default instance of the . + /// + public static readonly EndpointMetadataComparer Default = new DefaultComparer(); /// - /// A base class for implementations that use - /// a specific type of metadata from for comparison. - /// Useful for implementing . + /// Compares two objects and returns a value indicating whether one is less than, equal to, + /// or greater than the other. /// - /// - /// The type of metadata to compare. Typically this is a type of metadata related - /// to the application concern being handled. - /// - public abstract class EndpointMetadataComparer : IComparer where TMetadata : class + /// The first object to compare. + /// The second object to compare. + /// + /// An implementation of this method must return a value less than zero if + /// x is less than y, zero if x is equal to y, or a value greater than zero if x is + /// greater than y. + /// + public int Compare(Endpoint? x, Endpoint? y) { - /// - /// A default instance of the . - /// - public static readonly EndpointMetadataComparer Default = new DefaultComparer(); - - /// - /// Compares two objects and returns a value indicating whether one is less than, equal to, - /// or greater than the other. - /// - /// The first object to compare. - /// The second object to compare. - /// - /// An implementation of this method must return a value less than zero if - /// x is less than y, zero if x is equal to y, or a value greater than zero if x is - /// greater than y. - /// - public int Compare(Endpoint? x, Endpoint? y) + if (x == null) { - if (x == null) - { - throw new ArgumentNullException(nameof(x)); - } - - if (y == null) - { - throw new ArgumentNullException(nameof(y)); - } - - return CompareMetadata(GetMetadata(x), GetMetadata(y)); + throw new ArgumentNullException(nameof(x)); } - /// - /// Gets the metadata of type from the provided endpoint. - /// - /// The . - /// The instance or null. - protected virtual TMetadata? GetMetadata(Endpoint endpoint) + if (y == null) { - return endpoint.Metadata.GetMetadata(); + throw new ArgumentNullException(nameof(y)); } - /// - /// Compares two instances. - /// - /// The first object to compare. - /// The second object to compare. - /// - /// An implementation of this method must return a value less than zero if - /// x is less than y, zero if x is equal to y, or a value greater than zero if x is - /// greater than y. - /// - /// - /// The base-class implementation of this method will compare metadata based on whether - /// or not they are null. The effect of this is that when endpoints are being - /// compared, the endpoint that defines an instance of - /// will be considered higher priority. - /// - protected virtual int CompareMetadata(TMetadata? x, TMetadata? y) - { - // The default policy is that if x endpoint defines TMetadata, and - // y endpoint does not, then x is *more specific* than y. We return - // -1 for this case so that x will come first in the sort order. + return CompareMetadata(GetMetadata(x), GetMetadata(y)); + } - if (x == null && y != null) - { - // y is more specific - return 1; - } - else if (x != null && y == null) - { - // x is more specific - return -1; - } + /// + /// Gets the metadata of type from the provided endpoint. + /// + /// The . + /// The instance or null. + protected virtual TMetadata? GetMetadata(Endpoint endpoint) + { + return endpoint.Metadata.GetMetadata(); + } - // both endpoints have this metadata, or both do not have it, they have - // the same specificity. - return 0; - } + /// + /// Compares two instances. + /// + /// The first object to compare. + /// The second object to compare. + /// + /// An implementation of this method must return a value less than zero if + /// x is less than y, zero if x is equal to y, or a value greater than zero if x is + /// greater than y. + /// + /// + /// The base-class implementation of this method will compare metadata based on whether + /// or not they are null. The effect of this is that when endpoints are being + /// compared, the endpoint that defines an instance of + /// will be considered higher priority. + /// + protected virtual int CompareMetadata(TMetadata? x, TMetadata? y) + { + // The default policy is that if x endpoint defines TMetadata, and + // y endpoint does not, then x is *more specific* than y. We return + // -1 for this case so that x will come first in the sort order. - private class DefaultComparer : EndpointMetadataComparer where T : class + if (x == null && y != null) { + // y is more specific + return 1; } + else if (x != null && y == null) + { + // x is more specific + return -1; + } + + // both endpoints have this metadata, or both do not have it, they have + // the same specificity. + return 0; + } + + private class DefaultComparer : EndpointMetadataComparer where T : class + { } } diff --git a/src/Http/Routing/src/Matching/EndpointSelector.cs b/src/Http/Routing/src/Matching/EndpointSelector.cs index a2cd1f7d57..5b520e62bb 100644 --- a/src/Http/Routing/src/Matching/EndpointSelector.cs +++ b/src/Http/Routing/src/Matching/EndpointSelector.cs @@ -4,26 +4,25 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +/// +/// A service that is responsible for the final selection +/// decision. To use a custom register an implementation +/// of in the dependency injection container as a singleton. +/// +public abstract class EndpointSelector { /// - /// A service that is responsible for the final selection - /// decision. To use a custom register an implementation - /// of in the dependency injection container as a singleton. + /// Asynchronously selects an from the . /// - public abstract class EndpointSelector - { - /// - /// Asynchronously selects an from the . - /// - /// The associated with the current request. - /// The . - /// A that completes asynchronously once endpoint selection is complete. - /// - /// An should assign the endpoint by calling - /// - /// and setting once an endpoint is selected. - /// - public abstract Task SelectAsync(HttpContext httpContext, CandidateSet candidates); - } + /// The associated with the current request. + /// The . + /// A that completes asynchronously once endpoint selection is complete. + /// + /// An should assign the endpoint by calling + /// + /// and setting once an endpoint is selected. + /// + public abstract Task SelectAsync(HttpContext httpContext, CandidateSet candidates); } diff --git a/src/Http/Routing/src/Matching/FastPathTokenizer.cs b/src/Http/Routing/src/Matching/FastPathTokenizer.cs index 73f59c77fd..29ba5982b0 100644 --- a/src/Http/Routing/src/Matching/FastPathTokenizer.cs +++ b/src/Http/Routing/src/Matching/FastPathTokenizer.cs @@ -3,44 +3,43 @@ using System; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// Low level implementation of our path tokenization algorithm. Alternative +// to PathTokenizer. +internal static class FastPathTokenizer { - // Low level implementation of our path tokenization algorithm. Alternative - // to PathTokenizer. - internal static class FastPathTokenizer + // This section tokenizes the path by marking the sequence of slashes, and their + // and the length of the text between them. + // + // If there is residue (text after last slash) then the length of the segment will + // computed based on the string length. + public static int Tokenize(string path, Span segments) { - // This section tokenizes the path by marking the sequence of slashes, and their - // and the length of the text between them. - // - // If there is residue (text after last slash) then the length of the segment will - // computed based on the string length. - public static int Tokenize(string path, Span segments) + // This can happen in test scenarios. + if (string.IsNullOrEmpty(path)) { - // This can happen in test scenarios. - if (string.IsNullOrEmpty(path)) - { - return 0; - } - - int count = 0; - int start = 1; // Paths always start with a leading / - int end; - var span = path.AsSpan(start); - while ((end = span.IndexOf('/')) >= 0 && count < segments.Length) - { - segments[count++] = new PathSegment(start, end); - start += end + 1; // resume search after the current character - span = path.AsSpan(start); - } + return 0; + } - // Residue - var length = span.Length; - if (length > 0 && count < segments.Length) - { - segments[count++] = new PathSegment(start, length); - } + int count = 0; + int start = 1; // Paths always start with a leading / + int end; + var span = path.AsSpan(start); + while ((end = span.IndexOf('/')) >= 0 && count < segments.Length) + { + segments[count++] = new PathSegment(start, end); + start += end + 1; // resume search after the current character + span = path.AsSpan(start); + } - return count; + // Residue + var length = span.Length; + if (length > 0 && count < segments.Length) + { + segments[count++] = new PathSegment(start, length); } + + return count; } } diff --git a/src/Http/Routing/src/Matching/HostMatcherPolicy.cs b/src/Http/Routing/src/Matching/HostMatcherPolicy.cs index a41d0a24e5..a9c9b790e9 100644 --- a/src/Http/Routing/src/Matching/HostMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HostMatcherPolicy.cs @@ -8,472 +8,471 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +/// +/// A that implements filtering and selection by +/// the host header of a request. +/// +public sealed class HostMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy, IEndpointSelectorPolicy { - /// - /// A that implements filtering and selection by - /// the host header of a request. - /// - public sealed class HostMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy, IEndpointSelectorPolicy - { - private const string WildcardHost = "*"; - private const string WildcardPrefix = "*."; + private const string WildcardHost = "*"; + private const string WildcardPrefix = "*."; - // Run after HTTP methods, but before 'default'. - /// - public override int Order { get; } = -100; + // Run after HTTP methods, but before 'default'. + /// + public override int Order { get; } = -100; - /// - public IComparer Comparer { get; } = new HostMetadataEndpointComparer(); + /// + public IComparer Comparer { get; } = new HostMetadataEndpointComparer(); - bool INodeBuilderPolicy.AppliesToEndpoints(IReadOnlyList endpoints) + bool INodeBuilderPolicy.AppliesToEndpoints(IReadOnlyList endpoints) + { + if (endpoints == null) { - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } - - return !ContainsDynamicEndpoints(endpoints) && AppliesToEndpointsCore(endpoints); + throw new ArgumentNullException(nameof(endpoints)); } - bool IEndpointSelectorPolicy.AppliesToEndpoints(IReadOnlyList endpoints) - { - // When the node contains dynamic endpoints we can't make any assumptions. - var applies = ContainsDynamicEndpoints(endpoints); - if (applies) - { - // Run for the side-effect of validating metadata. - AppliesToEndpointsCore(endpoints); - } + return !ContainsDynamicEndpoints(endpoints) && AppliesToEndpointsCore(endpoints); + } - return applies; + bool IEndpointSelectorPolicy.AppliesToEndpoints(IReadOnlyList endpoints) + { + // When the node contains dynamic endpoints we can't make any assumptions. + var applies = ContainsDynamicEndpoints(endpoints); + if (applies) + { + // Run for the side-effect of validating metadata. + AppliesToEndpointsCore(endpoints); } - private static bool AppliesToEndpointsCore(IReadOnlyList endpoints) + return applies; + } + + private static bool AppliesToEndpointsCore(IReadOnlyList endpoints) + { + return endpoints.Any(e => { - return endpoints.Any(e => + var hosts = e.Metadata.GetMetadata()?.Hosts; + if (hosts == null || hosts.Count == 0) { - var hosts = e.Metadata.GetMetadata()?.Hosts; - if (hosts == null || hosts.Count == 0) - { - return false; - } + return false; + } - foreach (var host in hosts) - { + foreach (var host in hosts) + { // Don't run policy on endpoints that match everything var key = CreateEdgeKey(host); - if (!key.MatchesAll) - { - return true; - } + if (!key.MatchesAll) + { + return true; } + } - return false; - }); + return false; + }); + } + + /// + public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); } - /// - public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) + if (candidates == null) { - if (httpContext == null) + throw new ArgumentNullException(nameof(candidates)); + } + + for (var i = 0; i < candidates.Count; i++) + { + if (!candidates.IsValidCandidate(i)) { - throw new ArgumentNullException(nameof(httpContext)); + continue; } - if (candidates == null) + var hosts = candidates[i].Endpoint.Metadata.GetMetadata()?.Hosts; + if (hosts == null || hosts.Count == 0) { - throw new ArgumentNullException(nameof(candidates)); + // Can match any host. + continue; } - for (var i = 0; i < candidates.Count; i++) + var matched = false; + var (requestHost, requestPort) = GetHostAndPort(httpContext); + for (var j = 0; j < hosts.Count; j++) { - if (!candidates.IsValidCandidate(i)) + var host = hosts[j].AsSpan(); + var port = ReadOnlySpan.Empty; + + // Split into host and port + var pivot = host.IndexOf(':'); + if (pivot >= 0) { - continue; + port = host.Slice(pivot + 1); + host = host.Slice(0, pivot); } - var hosts = candidates[i].Endpoint.Metadata.GetMetadata()?.Hosts; - if (hosts == null || hosts.Count == 0) + if (host == null || MemoryExtensions.Equals(host, WildcardHost, StringComparison.OrdinalIgnoreCase)) { - // Can match any host. - continue; + // Can match any host } + else if ( + host.StartsWith(WildcardPrefix) && - var matched = false; - var (requestHost, requestPort) = GetHostAndPort(httpContext); - for (var j = 0; j < hosts.Count; j++) + // Note that we only slice off the `*`. We want to match the leading `.` also. + MemoryExtensions.EndsWith(requestHost, host.Slice(WildcardHost.Length), StringComparison.OrdinalIgnoreCase)) { - var host = hosts[j].AsSpan(); - var port = ReadOnlySpan.Empty; - - // Split into host and port - var pivot = host.IndexOf(':'); - if (pivot >= 0) - { - port = host.Slice(pivot + 1); - host = host.Slice(0, pivot); - } - - if (host == null || MemoryExtensions.Equals(host, WildcardHost, StringComparison.OrdinalIgnoreCase)) - { - // Can match any host - } - else if ( - host.StartsWith(WildcardPrefix) && - - // Note that we only slice off the `*`. We want to match the leading `.` also. - MemoryExtensions.EndsWith(requestHost, host.Slice(WildcardHost.Length), StringComparison.OrdinalIgnoreCase)) - { - // Matches a suffix wildcard. - } - else if (MemoryExtensions.Equals(requestHost, host, StringComparison.OrdinalIgnoreCase)) - { - // Matches exactly - } - else - { - // If we get here then the host doesn't match. - continue; - } - - if (MemoryExtensions.Equals(port, WildcardHost, StringComparison.OrdinalIgnoreCase)) - { - // Port is a wildcard, we allow any port. - } - else if (port.Length > 0 && (!int.TryParse(port, out var parsed) || parsed != requestPort)) - { - // If we get here then the port doesn't match. - continue; - } - - matched = true; - break; + // Matches a suffix wildcard. + } + else if (MemoryExtensions.Equals(requestHost, host, StringComparison.OrdinalIgnoreCase)) + { + // Matches exactly + } + else + { + // If we get here then the host doesn't match. + continue; } - if (!matched) + if (MemoryExtensions.Equals(port, WildcardHost, StringComparison.OrdinalIgnoreCase)) + { + // Port is a wildcard, we allow any port. + } + else if (port.Length > 0 && (!int.TryParse(port, out var parsed) || parsed != requestPort)) { - candidates.SetValidity(i, false); + // If we get here then the port doesn't match. + continue; } + + matched = true; + break; } - return Task.CompletedTask; + if (!matched) + { + candidates.SetValidity(i, false); + } + } + + return Task.CompletedTask; + } + + private static EdgeKey CreateEdgeKey(string host) + { + if (host == null) + { + return EdgeKey.WildcardEdgeKey; } - private static EdgeKey CreateEdgeKey(string host) + var hostParts = host.Split(':'); + if (hostParts.Length == 1) { - if (host == null) + if (!string.IsNullOrEmpty(hostParts[0])) { - return EdgeKey.WildcardEdgeKey; + return new EdgeKey(hostParts[0], null); } - - var hostParts = host.Split(':'); - if (hostParts.Length == 1) + } + if (hostParts.Length == 2) + { + if (!string.IsNullOrEmpty(hostParts[0])) { - if (!string.IsNullOrEmpty(hostParts[0])) + if (int.TryParse(hostParts[1], out var port)) { - return new EdgeKey(hostParts[0], null); + return new EdgeKey(hostParts[0], port); } - } - if (hostParts.Length == 2) - { - if (!string.IsNullOrEmpty(hostParts[0])) + else if (string.Equals(hostParts[1], WildcardHost, StringComparison.Ordinal)) { - if (int.TryParse(hostParts[1], out var port)) - { - return new EdgeKey(hostParts[0], port); - } - else if (string.Equals(hostParts[1], WildcardHost, StringComparison.Ordinal)) - { - return new EdgeKey(hostParts[0], null); - } + return new EdgeKey(hostParts[0], null); } } + } + + throw new InvalidOperationException($"Could not parse host: {host}"); + } - throw new InvalidOperationException($"Could not parse host: {host}"); + /// + public IReadOnlyList GetEdges(IReadOnlyList endpoints) + { + if (endpoints == null) + { + throw new ArgumentNullException(nameof(endpoints)); } - /// - public IReadOnlyList GetEdges(IReadOnlyList endpoints) + // The algorithm here is designed to be preserve the order of the endpoints + // while also being relatively simple. Preserving order is important. + + // First, build a dictionary of all of the hosts that are included + // at this node. + // + // For now we're just building up the set of keys. We don't add any endpoints + // to lists now because we don't want ordering problems. + var edges = new Dictionary>(); + for (var i = 0; i < endpoints.Count; i++) { - if (endpoints == null) + var endpoint = endpoints[i]; + var hosts = endpoint.Metadata.GetMetadata()?.Hosts.Select(h => CreateEdgeKey(h)).ToArray(); + if (hosts == null || hosts.Length == 0) { - throw new ArgumentNullException(nameof(endpoints)); + hosts = new[] { EdgeKey.WildcardEdgeKey }; } - // The algorithm here is designed to be preserve the order of the endpoints - // while also being relatively simple. Preserving order is important. - - // First, build a dictionary of all of the hosts that are included - // at this node. - // - // For now we're just building up the set of keys. We don't add any endpoints - // to lists now because we don't want ordering problems. - var edges = new Dictionary>(); - for (var i = 0; i < endpoints.Count; i++) + for (var j = 0; j < hosts.Length; j++) { - var endpoint = endpoints[i]; - var hosts = endpoint.Metadata.GetMetadata()?.Hosts.Select(h => CreateEdgeKey(h)).ToArray(); - if (hosts == null || hosts.Length == 0) + var host = hosts[j]; + if (!edges.ContainsKey(host)) { - hosts = new[] { EdgeKey.WildcardEdgeKey }; - } - - for (var j = 0; j < hosts.Length; j++) - { - var host = hosts[j]; - if (!edges.ContainsKey(host)) - { - edges.Add(host, new List()); - } + edges.Add(host, new List()); } } + } - // Now in a second loop, add endpoints to these lists. We've enumerated all of - // the states, so we want to see which states this endpoint matches. - for (var i = 0; i < endpoints.Count; i++) - { - var endpoint = endpoints[i]; + // Now in a second loop, add endpoints to these lists. We've enumerated all of + // the states, so we want to see which states this endpoint matches. + for (var i = 0; i < endpoints.Count; i++) + { + var endpoint = endpoints[i]; - var endpointKeys = endpoint.Metadata.GetMetadata()?.Hosts.Select(h => CreateEdgeKey(h)).ToArray() ?? Array.Empty(); - if (endpointKeys.Length == 0) + var endpointKeys = endpoint.Metadata.GetMetadata()?.Hosts.Select(h => CreateEdgeKey(h)).ToArray() ?? Array.Empty(); + if (endpointKeys.Length == 0) + { + // OK this means that this endpoint matches *all* hosts. + // So, loop and add it to all states. + foreach (var kvp in edges) { - // OK this means that this endpoint matches *all* hosts. - // So, loop and add it to all states. - foreach (var kvp in edges) - { - kvp.Value.Add(endpoint); - } + kvp.Value.Add(endpoint); } - else + } + else + { + // OK this endpoint matches specific hosts + foreach (var kvp in edges) { - // OK this endpoint matches specific hosts - foreach (var kvp in edges) + // The edgeKey maps to a possible request header value + var edgeKey = kvp.Key; + + for (var j = 0; j < endpointKeys.Length; j++) { - // The edgeKey maps to a possible request header value - var edgeKey = kvp.Key; + var endpointKey = endpointKeys[j]; - for (var j = 0; j < endpointKeys.Length; j++) + if (edgeKey.Equals(endpointKey)) + { + kvp.Value.Add(endpoint); + break; + } + else if (edgeKey.HasHostWildcard && endpointKey.HasHostWildcard && + edgeKey.Port == endpointKey.Port && edgeKey.MatchHost(endpointKey.Host)) { - var endpointKey = endpointKeys[j]; - - if (edgeKey.Equals(endpointKey)) - { - kvp.Value.Add(endpoint); - break; - } - else if (edgeKey.HasHostWildcard && endpointKey.HasHostWildcard && - edgeKey.Port == endpointKey.Port && edgeKey.MatchHost(endpointKey.Host)) - { - kvp.Value.Add(endpoint); - break; - } + kvp.Value.Add(endpoint); + break; } } } } - - return edges - .Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value)) - .ToArray(); } - /// - public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) + return edges + .Select(kvp => new PolicyNodeEdge(kvp.Key, kvp.Value)) + .ToArray(); + } + + /// + public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) + { + if (edges == null) { - if (edges == null) - { - throw new ArgumentNullException(nameof(edges)); - } + throw new ArgumentNullException(nameof(edges)); + } - // Since our 'edges' can have wildcards, we do a sort based on how wildcard-ey they - // are then then execute them in linear order. - var ordered = edges - .Select(e => (host: (EdgeKey)e.State, destination: e.Destination)) - .OrderBy(e => GetScore(e.host)) - .ToArray(); + // Since our 'edges' can have wildcards, we do a sort based on how wildcard-ey they + // are then then execute them in linear order. + var ordered = edges + .Select(e => (host: (EdgeKey)e.State, destination: e.Destination)) + .OrderBy(e => GetScore(e.host)) + .ToArray(); - return new HostPolicyJumpTable(exitDestination, ordered); - } + return new HostPolicyJumpTable(exitDestination, ordered); + } - private static int GetScore(in EdgeKey key) + private static int GetScore(in EdgeKey key) + { + // Higher score == lower priority. + if (key.MatchesHost && !key.HasHostWildcard && key.MatchesPort) { - // Higher score == lower priority. - if (key.MatchesHost && !key.HasHostWildcard && key.MatchesPort) - { - return 1; // Has host AND port, e.g. www.consoto.com:8080 - } - else if (key.MatchesHost && !key.HasHostWildcard) - { - return 2; // Has host, e.g. www.consoto.com - } - else if (key.MatchesHost && key.MatchesPort) - { - return 3; // Has wildcard host AND port, e.g. *.consoto.com:8080 - } - else if (key.MatchesHost) - { - return 4; // Has wildcard host, e.g. *.consoto.com - } - else if (key.MatchesPort) - { - return 5; // Has port, e.g. *:8080 - } - else - { - return 6; // Has neither, e.g. *:* (or no metadata) - } + return 1; // Has host AND port, e.g. www.consoto.com:8080 } + else if (key.MatchesHost && !key.HasHostWildcard) + { + return 2; // Has host, e.g. www.consoto.com + } + else if (key.MatchesHost && key.MatchesPort) + { + return 3; // Has wildcard host AND port, e.g. *.consoto.com:8080 + } + else if (key.MatchesHost) + { + return 4; // Has wildcard host, e.g. *.consoto.com + } + else if (key.MatchesPort) + { + return 5; // Has port, e.g. *:8080 + } + else + { + return 6; // Has neither, e.g. *:* (or no metadata) + } + } - private static (string host, int? port) GetHostAndPort(HttpContext httpContext) + private static (string host, int? port) GetHostAndPort(HttpContext httpContext) + { + var hostString = httpContext.Request.Host; + if (hostString.Port != null) { - var hostString = httpContext.Request.Host; - if (hostString.Port != null) - { - return (hostString.Host, hostString.Port); - } - else if (string.Equals("https", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase)) - { - return (hostString.Host, 443); - } - else if (string.Equals("http", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase)) - { - return (hostString.Host, 80); - } - else - { - return (hostString.Host, null); - } + return (hostString.Host, hostString.Port); } + else if (string.Equals("https", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase)) + { + return (hostString.Host, 443); + } + else if (string.Equals("http", httpContext.Request.Scheme, StringComparison.OrdinalIgnoreCase)) + { + return (hostString.Host, 80); + } + else + { + return (hostString.Host, null); + } + } - private class HostMetadataEndpointComparer : EndpointMetadataComparer + private class HostMetadataEndpointComparer : EndpointMetadataComparer + { + protected override int CompareMetadata(IHostMetadata? x, IHostMetadata? y) { - protected override int CompareMetadata(IHostMetadata? x, IHostMetadata? y) - { - // Ignore the metadata if it has an empty list of hosts. - return base.CompareMetadata( - x?.Hosts.Count > 0 ? x : null, - y?.Hosts.Count > 0 ? y : null); - } + // Ignore the metadata if it has an empty list of hosts. + return base.CompareMetadata( + x?.Hosts.Count > 0 ? x : null, + y?.Hosts.Count > 0 ? y : null); } + } + + private class HostPolicyJumpTable : PolicyJumpTable + { + private readonly (EdgeKey host, int destination)[] _destinations; + private readonly int _exitDestination; - private class HostPolicyJumpTable : PolicyJumpTable + public HostPolicyJumpTable(int exitDestination, (EdgeKey host, int destination)[] destinations) { - private readonly (EdgeKey host, int destination)[] _destinations; - private readonly int _exitDestination; + _exitDestination = exitDestination; + _destinations = destinations; + } - public HostPolicyJumpTable(int exitDestination, (EdgeKey host, int destination)[] destinations) - { - _exitDestination = exitDestination; - _destinations = destinations; - } + public override int GetDestination(HttpContext httpContext) + { + // HostString can allocate when accessing the host or port + // Store host and port locally and reuse + var (host, port) = GetHostAndPort(httpContext); - public override int GetDestination(HttpContext httpContext) + var destinations = _destinations; + for (var i = 0; i < destinations.Length; i++) { - // HostString can allocate when accessing the host or port - // Store host and port locally and reuse - var (host, port) = GetHostAndPort(httpContext); + var destination = destinations[i]; - var destinations = _destinations; - for (var i = 0; i < destinations.Length; i++) + if ((!destination.host.MatchesPort || destination.host.Port == port) && + destination.host.MatchHost(host)) { - var destination = destinations[i]; - - if ((!destination.host.MatchesPort || destination.host.Port == port) && - destination.host.MatchHost(host)) - { - return destination.destination; - } + return destination.destination; } - - return _exitDestination; } + + return _exitDestination; } + } - private readonly struct EdgeKey : IEquatable, IComparable, IComparable - { - internal static readonly EdgeKey WildcardEdgeKey = new EdgeKey(null, null); + private readonly struct EdgeKey : IEquatable, IComparable, IComparable + { + internal static readonly EdgeKey WildcardEdgeKey = new EdgeKey(null, null); - public readonly int? Port; - public readonly string Host; + public readonly int? Port; + public readonly string Host; - private readonly string? _wildcardEndsWith; + private readonly string? _wildcardEndsWith; - public EdgeKey(string? host, int? port) - { - Host = host ?? WildcardHost; - Port = port; + public EdgeKey(string? host, int? port) + { + Host = host ?? WildcardHost; + Port = port; - HasHostWildcard = Host.StartsWith(WildcardPrefix, StringComparison.Ordinal); - _wildcardEndsWith = HasHostWildcard ? Host.Substring(1) : null; - } + HasHostWildcard = Host.StartsWith(WildcardPrefix, StringComparison.Ordinal); + _wildcardEndsWith = HasHostWildcard ? Host.Substring(1) : null; + } - public bool HasHostWildcard { get; } + public bool HasHostWildcard { get; } - public bool MatchesHost => !string.Equals(Host, WildcardHost, StringComparison.Ordinal); + public bool MatchesHost => !string.Equals(Host, WildcardHost, StringComparison.Ordinal); - public bool MatchesPort => Port != null; + public bool MatchesPort => Port != null; - public bool MatchesAll => !MatchesHost && !MatchesPort; + public bool MatchesAll => !MatchesHost && !MatchesPort; - public int CompareTo(EdgeKey other) + public int CompareTo(EdgeKey other) + { + var result = Comparer.Default.Compare(Host, other.Host); + if (result != 0) { - var result = Comparer.Default.Compare(Host, other.Host); - if (result != 0) - { - return result; - } - - return Comparer.Default.Compare(Port, other.Port); + return result; } - public int CompareTo(object? obj) - { - return CompareTo((EdgeKey)obj!); - } + return Comparer.Default.Compare(Port, other.Port); + } - public bool Equals(EdgeKey other) - { - return string.Equals(Host, other.Host, StringComparison.Ordinal) && Port == other.Port; - } + public int CompareTo(object? obj) + { + return CompareTo((EdgeKey)obj!); + } - public bool MatchHost(string host) + public bool Equals(EdgeKey other) + { + return string.Equals(Host, other.Host, StringComparison.Ordinal) && Port == other.Port; + } + + public bool MatchHost(string host) + { + if (MatchesHost) { - if (MatchesHost) + if (HasHostWildcard) { - if (HasHostWildcard) - { - return host.EndsWith(_wildcardEndsWith!, StringComparison.OrdinalIgnoreCase); - } - else - { - return string.Equals(host, Host, StringComparison.OrdinalIgnoreCase); - } + return host.EndsWith(_wildcardEndsWith!, StringComparison.OrdinalIgnoreCase); + } + else + { + return string.Equals(host, Host, StringComparison.OrdinalIgnoreCase); } - - return true; } + return true; + } - public override int GetHashCode() - { - return (Host?.GetHashCode() ?? 0) ^ (Port?.GetHashCode() ?? 0); - } - - public override bool Equals(object? obj) - { - if (obj is EdgeKey key) - { - return Equals(key); - } - return false; - } + public override int GetHashCode() + { + return (Host?.GetHashCode() ?? 0) ^ (Port?.GetHashCode() ?? 0); + } - public override string ToString() + public override bool Equals(object? obj) + { + if (obj is EdgeKey key) { - return $"{Host}:{Port?.ToString(CultureInfo.InvariantCulture) ?? WildcardHost}"; + return Equals(key); } + + return false; + } + + public override string ToString() + { + return $"{Host}:{Port?.ToString(CultureInfo.InvariantCulture) ?? WildcardHost}"; } } } diff --git a/src/Http/Routing/src/Matching/HttpMethodDictionaryPolicyJumpTable.cs b/src/Http/Routing/src/Matching/HttpMethodDictionaryPolicyJumpTable.cs index 31b4e58f7c..52d969f1c9 100644 --- a/src/Http/Routing/src/Matching/HttpMethodDictionaryPolicyJumpTable.cs +++ b/src/Http/Routing/src/Matching/HttpMethodDictionaryPolicyJumpTable.cs @@ -4,45 +4,44 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal sealed class HttpMethodDictionaryPolicyJumpTable : PolicyJumpTable { - internal sealed class HttpMethodDictionaryPolicyJumpTable : PolicyJumpTable + private readonly int _exitDestination; + private readonly Dictionary? _destinations; + private readonly int _corsPreflightExitDestination; + private readonly Dictionary? _corsPreflightDestinations; + + private readonly bool _supportsCorsPreflight; + + public HttpMethodDictionaryPolicyJumpTable( + int exitDestination, + Dictionary? destinations, + int corsPreflightExitDestination, + Dictionary? corsPreflightDestinations) { - private readonly int _exitDestination; - private readonly Dictionary? _destinations; - private readonly int _corsPreflightExitDestination; - private readonly Dictionary? _corsPreflightDestinations; - - private readonly bool _supportsCorsPreflight; - - public HttpMethodDictionaryPolicyJumpTable( - int exitDestination, - Dictionary? destinations, - int corsPreflightExitDestination, - Dictionary? corsPreflightDestinations) - { - _exitDestination = exitDestination; - _destinations = destinations; - _corsPreflightExitDestination = corsPreflightExitDestination; - _corsPreflightDestinations = corsPreflightDestinations; + _exitDestination = exitDestination; + _destinations = destinations; + _corsPreflightExitDestination = corsPreflightExitDestination; + _corsPreflightDestinations = corsPreflightDestinations; - _supportsCorsPreflight = _corsPreflightDestinations != null && _corsPreflightDestinations.Count > 0; - } + _supportsCorsPreflight = _corsPreflightDestinations != null && _corsPreflightDestinations.Count > 0; + } - public override int GetDestination(HttpContext httpContext) + public override int GetDestination(HttpContext httpContext) + { + int destination; + + var httpMethod = httpContext.Request.Method; + if (_supportsCorsPreflight && HttpMethodMatcherPolicy.IsCorsPreflightRequest(httpContext, httpMethod, out var accessControlRequestMethod)) { - int destination; - - var httpMethod = httpContext.Request.Method; - if (_supportsCorsPreflight && HttpMethodMatcherPolicy.IsCorsPreflightRequest(httpContext, httpMethod, out var accessControlRequestMethod)) - { - return _corsPreflightDestinations!.TryGetValue(accessControlRequestMethod.ToString(), out destination) - ? destination - : _corsPreflightExitDestination; - } - - return _destinations != null && - _destinations.TryGetValue(httpMethod, out destination) ? destination : _exitDestination; + return _corsPreflightDestinations!.TryGetValue(accessControlRequestMethod.ToString(), out destination) + ? destination + : _corsPreflightExitDestination; } + + return _destinations != null && + _destinations.TryGetValue(httpMethod, out destination) ? destination : _exitDestination; } } diff --git a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs index 199a9a9bb2..6bee324925 100644 --- a/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/HttpMethodMatcherPolicy.cs @@ -11,517 +11,516 @@ using Microsoft.Extensions.Internal; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +/// +/// An that implements filtering and selection by +/// the HTTP method of a request. +/// +public sealed class HttpMethodMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy, IEndpointSelectorPolicy { + // Used in tests + internal static readonly string PreflightHttpMethod = HttpMethods.Options; + + // Used in tests + internal const string Http405EndpointDisplayName = "405 HTTP Method Not Supported"; + + // Used in tests + internal const string AnyMethod = "*"; + + /// + /// For framework use only. + /// + public IComparer Comparer => new HttpMethodMetadataEndpointComparer(); + + // The order value is chosen to be less than 0, so that it comes before naively + // written policies. /// - /// An that implements filtering and selection by - /// the HTTP method of a request. + /// For framework use only. /// - public sealed class HttpMethodMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy, IEndpointSelectorPolicy + public override int Order => -1000; + + bool INodeBuilderPolicy.AppliesToEndpoints(IReadOnlyList endpoints) { - // Used in tests - internal static readonly string PreflightHttpMethod = HttpMethods.Options; + if (endpoints == null) + { + throw new ArgumentNullException(nameof(endpoints)); + } - // Used in tests - internal const string Http405EndpointDisplayName = "405 HTTP Method Not Supported"; + if (ContainsDynamicEndpoints(endpoints)) + { + return false; + } - // Used in tests - internal const string AnyMethod = "*"; + return AppliesToEndpointsCore(endpoints); + } - /// - /// For framework use only. - /// - public IComparer Comparer => new HttpMethodMetadataEndpointComparer(); + bool IEndpointSelectorPolicy.AppliesToEndpoints(IReadOnlyList endpoints) + { + if (endpoints == null) + { + throw new ArgumentNullException(nameof(endpoints)); + } - // The order value is chosen to be less than 0, so that it comes before naively - // written policies. - /// - /// For framework use only. - /// - public override int Order => -1000; + // When the node contains dynamic endpoints we can't make any assumptions. + return ContainsDynamicEndpoints(endpoints); + } - bool INodeBuilderPolicy.AppliesToEndpoints(IReadOnlyList endpoints) + private static bool AppliesToEndpointsCore(IReadOnlyList endpoints) + { + for (var i = 0; i < endpoints.Count; i++) { - if (endpoints == null) + if (endpoints[i].Metadata.GetMetadata()?.HttpMethods.Count > 0) { - throw new ArgumentNullException(nameof(endpoints)); + return true; } + } - if (ContainsDynamicEndpoints(endpoints)) - { - return false; - } + return false; + } - return AppliesToEndpointsCore(endpoints); + /// + /// For framework use only. + /// + /// + /// + /// + public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); } - bool IEndpointSelectorPolicy.AppliesToEndpoints(IReadOnlyList endpoints) + if (candidates == null) { - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } - - // When the node contains dynamic endpoints we can't make any assumptions. - return ContainsDynamicEndpoints(endpoints); + throw new ArgumentNullException(nameof(candidates)); } - private static bool AppliesToEndpointsCore(IReadOnlyList endpoints) + // Returning a 405 here requires us to return keep track of all 'seen' HTTP methods. We allocate to + // keep track of this because we either need to keep track of the HTTP methods or keep track of the + // endpoints - both allocate. + // + // Those code only runs in the presence of dynamic endpoints anyway. + // + // We want to return a 405 iff we eliminated ALL of the currently valid endpoints due to HTTP method + // mismatch. + bool? needs405Endpoint = null; + HashSet? methods = null; + + for (var i = 0; i < candidates.Count; i++) { - for (var i = 0; i < endpoints.Count; i++) + // We do this check first for consistency with how 405 is implemented for the graph version + // of this code. We still want to know if any endpoints in this set require an HTTP method + // even if those endpoints are already invalid - hence the null-check. + var metadata = candidates[i].Endpoint?.Metadata.GetMetadata(); + if (metadata == null || metadata.HttpMethods.Count == 0) { - if (endpoints[i].Metadata.GetMetadata()?.HttpMethods.Count > 0) - { - return true; - } + // Can match any method. + needs405Endpoint = false; + continue; } - return false; - } + // Saw a valid endpoint. + needs405Endpoint = needs405Endpoint ?? true; - /// - /// For framework use only. - /// - /// - /// - /// - public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) - { - if (httpContext == null) + if (!candidates.IsValidCandidate(i)) { - throw new ArgumentNullException(nameof(httpContext)); + continue; } - if (candidates == null) + var httpMethod = httpContext.Request.Method; + var headers = httpContext.Request.Headers; + if (metadata.AcceptCorsPreflight && + HttpMethods.Equals(httpMethod, PreflightHttpMethod) && + headers.ContainsKey(HeaderNames.Origin) && + headers.TryGetValue(HeaderNames.AccessControlRequestMethod, out var accessControlRequestMethod) && + !StringValues.IsNullOrEmpty(accessControlRequestMethod)) { - throw new ArgumentNullException(nameof(candidates)); + needs405Endpoint = false; // We don't return a 405 for a CORS preflight request when the endpoints accept CORS preflight. + httpMethod = accessControlRequestMethod.ToString(); } - // Returning a 405 here requires us to return keep track of all 'seen' HTTP methods. We allocate to - // keep track of this because we either need to keep track of the HTTP methods or keep track of the - // endpoints - both allocate. - // - // Those code only runs in the presence of dynamic endpoints anyway. - // - // We want to return a 405 iff we eliminated ALL of the currently valid endpoints due to HTTP method - // mismatch. - bool? needs405Endpoint = null; - HashSet? methods = null; - - for (var i = 0; i < candidates.Count; i++) + var matched = false; + for (var j = 0; j < metadata.HttpMethods.Count; j++) { - // We do this check first for consistency with how 405 is implemented for the graph version - // of this code. We still want to know if any endpoints in this set require an HTTP method - // even if those endpoints are already invalid - hence the null-check. - var metadata = candidates[i].Endpoint?.Metadata.GetMetadata(); - if (metadata == null || metadata.HttpMethods.Count == 0) + var candidateMethod = metadata.HttpMethods[j]; + if (!HttpMethods.Equals(httpMethod, candidateMethod)) { - // Can match any method. - needs405Endpoint = false; + methods = methods ?? new HashSet(StringComparer.OrdinalIgnoreCase); + methods.Add(candidateMethod); continue; } - // Saw a valid endpoint. - needs405Endpoint = needs405Endpoint ?? true; - - if (!candidates.IsValidCandidate(i)) - { - continue; - } - - var httpMethod = httpContext.Request.Method; - var headers = httpContext.Request.Headers; - if (metadata.AcceptCorsPreflight && - HttpMethods.Equals(httpMethod, PreflightHttpMethod) && - headers.ContainsKey(HeaderNames.Origin) && - headers.TryGetValue(HeaderNames.AccessControlRequestMethod, out var accessControlRequestMethod) && - !StringValues.IsNullOrEmpty(accessControlRequestMethod)) - { - needs405Endpoint = false; // We don't return a 405 for a CORS preflight request when the endpoints accept CORS preflight. - httpMethod = accessControlRequestMethod.ToString(); - } - - var matched = false; - for (var j = 0; j < metadata.HttpMethods.Count; j++) - { - var candidateMethod = metadata.HttpMethods[j]; - if (!HttpMethods.Equals(httpMethod, candidateMethod)) - { - methods = methods ?? new HashSet(StringComparer.OrdinalIgnoreCase); - methods.Add(candidateMethod); - continue; - } - - matched = true; - needs405Endpoint = false; - break; - } - - if (!matched) - { - candidates.SetValidity(i, false); - } + matched = true; + needs405Endpoint = false; + break; } - if (needs405Endpoint == true) + if (!matched) { - // We saw some endpoints coming in, and we eliminated them all. - httpContext.SetEndpoint(CreateRejectionEndpoint(methods!.OrderBy(m => m, StringComparer.OrdinalIgnoreCase))); - httpContext.Request.RouteValues = null!; + candidates.SetValidity(i, false); } + } - return Task.CompletedTask; + if (needs405Endpoint == true) + { + // We saw some endpoints coming in, and we eliminated them all. + httpContext.SetEndpoint(CreateRejectionEndpoint(methods!.OrderBy(m => m, StringComparer.OrdinalIgnoreCase))); + httpContext.Request.RouteValues = null!; } - /// - /// For framework use only. - /// - /// - /// - public IReadOnlyList GetEdges(IReadOnlyList endpoints) + return Task.CompletedTask; + } + + /// + /// For framework use only. + /// + /// + /// + public IReadOnlyList GetEdges(IReadOnlyList endpoints) + { + // The algorithm here is designed to be preserve the order of the endpoints + // while also being relatively simple. Preserving order is important. + + // First, build a dictionary of all possible HTTP method/CORS combinations + // that exist in this list of endpoints. + // + // For now we're just building up the set of keys. We don't add any endpoints + // to lists now because we don't want ordering problems. + var allHttpMethods = new List(); + var edges = new Dictionary>(); + for (var i = 0; i < endpoints.Count; i++) { - // The algorithm here is designed to be preserve the order of the endpoints - // while also being relatively simple. Preserving order is important. - - // First, build a dictionary of all possible HTTP method/CORS combinations - // that exist in this list of endpoints. - // - // For now we're just building up the set of keys. We don't add any endpoints - // to lists now because we don't want ordering problems. - var allHttpMethods = new List(); - var edges = new Dictionary>(); - for (var i = 0; i < endpoints.Count; i++) + var endpoint = endpoints[i]; + var (httpMethods, acceptCorsPreFlight) = GetHttpMethods(endpoint); + + // If the action doesn't list HTTP methods then it supports all methods. + // In this phase we use a sentinel value to represent the *other* HTTP method + // a state that represents any HTTP method that doesn't have a match. + if (httpMethods.Count == 0) { - var endpoint = endpoints[i]; - var (httpMethods, acceptCorsPreFlight) = GetHttpMethods(endpoint); + httpMethods = new[] { AnyMethod, }; + } - // If the action doesn't list HTTP methods then it supports all methods. - // In this phase we use a sentinel value to represent the *other* HTTP method - // a state that represents any HTTP method that doesn't have a match. - if (httpMethods.Count == 0) + for (var j = 0; j < httpMethods.Count; j++) + { + // An endpoint that allows CORS reqests will match both CORS and non-CORS + // so we model it as both. + var httpMethod = httpMethods[j]; + var key = new EdgeKey(httpMethod, acceptCorsPreFlight); + if (!edges.ContainsKey(key)) { - httpMethods = new[] { AnyMethod, }; + edges.Add(key, new List()); } - for (var j = 0; j < httpMethods.Count; j++) + // An endpoint that allows CORS reqests will match both CORS and non-CORS + // so we model it as both. + if (acceptCorsPreFlight) { - // An endpoint that allows CORS reqests will match both CORS and non-CORS - // so we model it as both. - var httpMethod = httpMethods[j]; - var key = new EdgeKey(httpMethod, acceptCorsPreFlight); + key = new EdgeKey(httpMethod, false); if (!edges.ContainsKey(key)) { edges.Add(key, new List()); } + } - // An endpoint that allows CORS reqests will match both CORS and non-CORS - // so we model it as both. - if (acceptCorsPreFlight) - { - key = new EdgeKey(httpMethod, false); - if (!edges.ContainsKey(key)) - { - edges.Add(key, new List()); - } - } - - // Also if it's not the *any* method key, then track it. - if (!string.Equals(AnyMethod, httpMethod, StringComparison.OrdinalIgnoreCase)) + // Also if it's not the *any* method key, then track it. + if (!string.Equals(AnyMethod, httpMethod, StringComparison.OrdinalIgnoreCase)) + { + if (!ContainsHttpMethod(allHttpMethods, httpMethod)) { - if (!ContainsHttpMethod(allHttpMethods, httpMethod)) - { - allHttpMethods.Add(httpMethod); - } + allHttpMethods.Add(httpMethod); } } } + } - allHttpMethods.Sort(StringComparer.OrdinalIgnoreCase); + allHttpMethods.Sort(StringComparer.OrdinalIgnoreCase); - // Now in a second loop, add endpoints to these lists. We've enumerated all of - // the states, so we want to see which states this endpoint matches. - for (var i = 0; i < endpoints.Count; i++) - { - var endpoint = endpoints[i]; - var (httpMethods, acceptCorsPreFlight) = GetHttpMethods(endpoint); + // Now in a second loop, add endpoints to these lists. We've enumerated all of + // the states, so we want to see which states this endpoint matches. + for (var i = 0; i < endpoints.Count; i++) + { + var endpoint = endpoints[i]; + var (httpMethods, acceptCorsPreFlight) = GetHttpMethods(endpoint); - if (httpMethods.Count == 0) + if (httpMethods.Count == 0) + { + // OK this means that this endpoint matches *all* HTTP methods. + // So, loop and add it to all states. + foreach (var kvp in edges) { - // OK this means that this endpoint matches *all* HTTP methods. - // So, loop and add it to all states. - foreach (var kvp in edges) + if (acceptCorsPreFlight || !kvp.Key.IsCorsPreflightRequest) { - if (acceptCorsPreFlight || !kvp.Key.IsCorsPreflightRequest) - { - kvp.Value.Add(endpoint); - } + kvp.Value.Add(endpoint); } } - else + } + else + { + // OK this endpoint matches specific methods. + for (var j = 0; j < httpMethods.Count; j++) { - // OK this endpoint matches specific methods. - for (var j = 0; j < httpMethods.Count; j++) - { - var httpMethod = httpMethods[j]; - var key = new EdgeKey(httpMethod, acceptCorsPreFlight); + var httpMethod = httpMethods[j]; + var key = new EdgeKey(httpMethod, acceptCorsPreFlight); - edges[key].Add(endpoint); + edges[key].Add(endpoint); - // An endpoint that allows CORS reqests will match both CORS and non-CORS - // so we model it as both. - if (acceptCorsPreFlight) - { - key = new EdgeKey(httpMethod, false); - edges[key].Add(endpoint); - } + // An endpoint that allows CORS reqests will match both CORS and non-CORS + // so we model it as both. + if (acceptCorsPreFlight) + { + key = new EdgeKey(httpMethod, false); + edges[key].Add(endpoint); } } } + } - // Adds a very low priority endpoint that will reject the request with - // a 405 if nothing else can handle this verb. This is only done if - // no other actions exist that handle the 'all verbs'. - // - // The rationale for this is that we want to report a 405 if none of - // the supported methods match, but we don't want to report a 405 in a - // case where an application defines an endpoint that handles all verbs, but - // a constraint rejects the request, or a complex segment fails to parse. We - // consider a case like that a 'user input validation' failure rather than - // a semantic violation of HTTP. - // - // This will make 405 much more likely in API-focused applications, and somewhat - // unlikely in a traditional MVC application. That's good. - // - // We don't bother returning a 405 when the CORS preflight method doesn't exist. - // The developer calling the API will see it as a CORS error, which is fine because - // there isn't an endpoint to check for a CORS policy. - if (!edges.TryGetValue(new EdgeKey(AnyMethod, false), out var matches)) - { - // Methods sorted for testability. - var endpoint = CreateRejectionEndpoint(allHttpMethods); - matches = new List() { endpoint, }; - edges[new EdgeKey(AnyMethod, false)] = matches; - } + // Adds a very low priority endpoint that will reject the request with + // a 405 if nothing else can handle this verb. This is only done if + // no other actions exist that handle the 'all verbs'. + // + // The rationale for this is that we want to report a 405 if none of + // the supported methods match, but we don't want to report a 405 in a + // case where an application defines an endpoint that handles all verbs, but + // a constraint rejects the request, or a complex segment fails to parse. We + // consider a case like that a 'user input validation' failure rather than + // a semantic violation of HTTP. + // + // This will make 405 much more likely in API-focused applications, and somewhat + // unlikely in a traditional MVC application. That's good. + // + // We don't bother returning a 405 when the CORS preflight method doesn't exist. + // The developer calling the API will see it as a CORS error, which is fine because + // there isn't an endpoint to check for a CORS policy. + if (!edges.TryGetValue(new EdgeKey(AnyMethod, false), out var matches)) + { + // Methods sorted for testability. + var endpoint = CreateRejectionEndpoint(allHttpMethods); + matches = new List() { endpoint, }; + edges[new EdgeKey(AnyMethod, false)] = matches; + } - var policyNodeEdges = new PolicyNodeEdge[edges.Count]; - var index = 0; - foreach (var kvp in edges) - { - policyNodeEdges[index++] = new PolicyNodeEdge(kvp.Key, kvp.Value); - } + var policyNodeEdges = new PolicyNodeEdge[edges.Count]; + var index = 0; + foreach (var kvp in edges) + { + policyNodeEdges[index++] = new PolicyNodeEdge(kvp.Key, kvp.Value); + } - return policyNodeEdges; + return policyNodeEdges; - (IReadOnlyList httpMethods, bool acceptCorsPreflight) GetHttpMethods(Endpoint e) - { - var metadata = e.Metadata.GetMetadata(); - return metadata == null ? (Array.Empty(), false) : (metadata.HttpMethods, metadata.AcceptCorsPreflight); - } + (IReadOnlyList httpMethods, bool acceptCorsPreflight) GetHttpMethods(Endpoint e) + { + var metadata = e.Metadata.GetMetadata(); + return metadata == null ? (Array.Empty(), false) : (metadata.HttpMethods, metadata.AcceptCorsPreflight); } + } - /// - /// For framework use only. - /// - /// - /// - /// - public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) + /// + /// For framework use only. + /// + /// + /// + /// + public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) + { + Dictionary? destinations = null; + Dictionary? corsPreflightDestinations = null; + for (var i = 0; i < edges.Count; i++) { - Dictionary? destinations = null; - Dictionary? corsPreflightDestinations = null; - for (var i = 0; i < edges.Count; i++) + // We create this data, so it's safe to cast it. + var key = (EdgeKey)edges[i].State; + if (key.IsCorsPreflightRequest) { - // We create this data, so it's safe to cast it. - var key = (EdgeKey)edges[i].State; - if (key.IsCorsPreflightRequest) + if (corsPreflightDestinations == null) { - if (corsPreflightDestinations == null) - { - corsPreflightDestinations = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - corsPreflightDestinations.Add(key.HttpMethod, edges[i].Destination); - } - else - { - if (destinations == null) - { - destinations = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - destinations.Add(key.HttpMethod, edges[i].Destination); + corsPreflightDestinations = new Dictionary(StringComparer.OrdinalIgnoreCase); } - } - int corsPreflightExitDestination = exitDestination; - if (corsPreflightDestinations != null && corsPreflightDestinations.TryGetValue(AnyMethod, out var matchesAnyVerb)) - { - // If we have endpoints that match any HTTP method, use that as the exit. - corsPreflightExitDestination = matchesAnyVerb; - corsPreflightDestinations.Remove(AnyMethod); + corsPreflightDestinations.Add(key.HttpMethod, edges[i].Destination); } - - if (destinations != null && destinations.TryGetValue(AnyMethod, out matchesAnyVerb)) - { - // If we have endpoints that match any HTTP method, use that as the exit. - exitDestination = matchesAnyVerb; - destinations.Remove(AnyMethod); - } - - if (destinations?.Count == 1) + else { - // If there is only a single valid HTTP method then use an optimized jump table. - // It avoids unnecessary dictionary lookups with the method name. - var httpMethodDestination = destinations.Single(); - var method = httpMethodDestination.Key; - var destination = httpMethodDestination.Value; - var supportsCorsPreflight = false; - var corsPreflightDestination = 0; - - if (corsPreflightDestinations?.Count > 0) + if (destinations == null) { - supportsCorsPreflight = true; - corsPreflightDestination = corsPreflightDestinations.Single().Value; + destinations = new Dictionary(StringComparer.OrdinalIgnoreCase); } - return new HttpMethodSingleEntryPolicyJumpTable( - exitDestination, - method, - destination, - supportsCorsPreflight, - corsPreflightExitDestination, - corsPreflightDestination); + destinations.Add(key.HttpMethod, edges[i].Destination); } - else + } + + int corsPreflightExitDestination = exitDestination; + if (corsPreflightDestinations != null && corsPreflightDestinations.TryGetValue(AnyMethod, out var matchesAnyVerb)) + { + // If we have endpoints that match any HTTP method, use that as the exit. + corsPreflightExitDestination = matchesAnyVerb; + corsPreflightDestinations.Remove(AnyMethod); + } + + if (destinations != null && destinations.TryGetValue(AnyMethod, out matchesAnyVerb)) + { + // If we have endpoints that match any HTTP method, use that as the exit. + exitDestination = matchesAnyVerb; + destinations.Remove(AnyMethod); + } + + if (destinations?.Count == 1) + { + // If there is only a single valid HTTP method then use an optimized jump table. + // It avoids unnecessary dictionary lookups with the method name. + var httpMethodDestination = destinations.Single(); + var method = httpMethodDestination.Key; + var destination = httpMethodDestination.Value; + var supportsCorsPreflight = false; + var corsPreflightDestination = 0; + + if (corsPreflightDestinations?.Count > 0) { - return new HttpMethodDictionaryPolicyJumpTable( - exitDestination, - destinations, - corsPreflightExitDestination, - corsPreflightDestinations); + supportsCorsPreflight = true; + corsPreflightDestination = corsPreflightDestinations.Single().Value; } - } - private static Endpoint CreateRejectionEndpoint(IEnumerable httpMethods) + return new HttpMethodSingleEntryPolicyJumpTable( + exitDestination, + method, + destination, + supportsCorsPreflight, + corsPreflightExitDestination, + corsPreflightDestination); + } + else { - var allow = string.Join(", ", httpMethods); - return new Endpoint( - (context) => - { - context.Response.StatusCode = 405; + return new HttpMethodDictionaryPolicyJumpTable( + exitDestination, + destinations, + corsPreflightExitDestination, + corsPreflightDestinations); + } + } + + private static Endpoint CreateRejectionEndpoint(IEnumerable httpMethods) + { + var allow = string.Join(", ", httpMethods); + return new Endpoint( + (context) => + { + context.Response.StatusCode = 405; // Prevent ArgumentException from duplicate key if header already added, such as when the // request is re-executed by an error handler (see https://github.com/dotnet/aspnetcore/issues/6415) context.Response.Headers.Allow = allow; - return Task.CompletedTask; - }, - EndpointMetadataCollection.Empty, - Http405EndpointDisplayName); - } + return Task.CompletedTask; + }, + EndpointMetadataCollection.Empty, + Http405EndpointDisplayName); + } - private static bool ContainsHttpMethod(List httpMethods, string httpMethod) + private static bool ContainsHttpMethod(List httpMethods, string httpMethod) + { + var methods = CollectionsMarshal.AsSpan(httpMethods); + for (var i = 0; i < methods.Length; i++) { - var methods = CollectionsMarshal.AsSpan(httpMethods); - for (var i = 0; i < methods.Length; i++) + // This is a fast path for when everything is using static HttpMethods instances. + if (object.ReferenceEquals(methods[i], httpMethod)) { - // This is a fast path for when everything is using static HttpMethods instances. - if (object.ReferenceEquals(methods[i], httpMethod)) - { - return true; - } + return true; } + } - for (var i = 0; i < methods.Length; i++) + for (var i = 0; i < methods.Length; i++) + { + if (HttpMethods.Equals(methods[i], httpMethod)) { - if (HttpMethods.Equals(methods[i], httpMethod)) - { - return true; - } + return true; } - - return false; } - internal static bool IsCorsPreflightRequest(HttpContext httpContext, string httpMethod, out StringValues accessControlRequestMethod) - { - accessControlRequestMethod = default; - var headers = httpContext.Request.Headers; + return false; + } - return HttpMethods.Equals(httpMethod, PreflightHttpMethod) && - headers.ContainsKey(HeaderNames.Origin) && - headers.TryGetValue(HeaderNames.AccessControlRequestMethod, out accessControlRequestMethod) && - !StringValues.IsNullOrEmpty(accessControlRequestMethod); - } + internal static bool IsCorsPreflightRequest(HttpContext httpContext, string httpMethod, out StringValues accessControlRequestMethod) + { + accessControlRequestMethod = default; + var headers = httpContext.Request.Headers; + + return HttpMethods.Equals(httpMethod, PreflightHttpMethod) && + headers.ContainsKey(HeaderNames.Origin) && + headers.TryGetValue(HeaderNames.AccessControlRequestMethod, out accessControlRequestMethod) && + !StringValues.IsNullOrEmpty(accessControlRequestMethod); + } - private class HttpMethodMetadataEndpointComparer : EndpointMetadataComparer + private class HttpMethodMetadataEndpointComparer : EndpointMetadataComparer + { + protected override int CompareMetadata(IHttpMethodMetadata? x, IHttpMethodMetadata? y) { - protected override int CompareMetadata(IHttpMethodMetadata? x, IHttpMethodMetadata? y) - { - // Ignore the metadata if it has an empty list of HTTP methods. - return base.CompareMetadata( - x?.HttpMethods.Count > 0 ? x : null, - y?.HttpMethods.Count > 0 ? y : null); - } + // Ignore the metadata if it has an empty list of HTTP methods. + return base.CompareMetadata( + x?.HttpMethods.Count > 0 ? x : null, + y?.HttpMethods.Count > 0 ? y : null); } + } + + internal readonly struct EdgeKey : IEquatable, IComparable, IComparable + { + // Note that in contrast with the metadata, the edge represents a possible state change + // rather than a list of what's allowed. We represent CORS and non-CORS requests as separate + // states. + public readonly bool IsCorsPreflightRequest; + public readonly string HttpMethod; - internal readonly struct EdgeKey : IEquatable, IComparable, IComparable + public EdgeKey(string httpMethod, bool isCorsPreflightRequest) { - // Note that in contrast with the metadata, the edge represents a possible state change - // rather than a list of what's allowed. We represent CORS and non-CORS requests as separate - // states. - public readonly bool IsCorsPreflightRequest; - public readonly string HttpMethod; + HttpMethod = httpMethod; + IsCorsPreflightRequest = isCorsPreflightRequest; + } - public EdgeKey(string httpMethod, bool isCorsPreflightRequest) + // These are comparable so they can be sorted in tests. + public int CompareTo(EdgeKey other) + { + var compare = string.Compare(HttpMethod, other.HttpMethod, StringComparison.Ordinal); + if (compare != 0) { - HttpMethod = httpMethod; - IsCorsPreflightRequest = isCorsPreflightRequest; + return compare; } - // These are comparable so they can be sorted in tests. - public int CompareTo(EdgeKey other) - { - var compare = string.Compare(HttpMethod, other.HttpMethod, StringComparison.Ordinal); - if (compare != 0) - { - return compare; - } - - return IsCorsPreflightRequest.CompareTo(other.IsCorsPreflightRequest); - } + return IsCorsPreflightRequest.CompareTo(other.IsCorsPreflightRequest); + } - public int CompareTo(object? obj) - { - return CompareTo((EdgeKey)obj!); - } + public int CompareTo(object? obj) + { + return CompareTo((EdgeKey)obj!); + } - public bool Equals(EdgeKey other) - { - return - IsCorsPreflightRequest == other.IsCorsPreflightRequest && - HttpMethods.Equals(HttpMethod, other.HttpMethod); - } + public bool Equals(EdgeKey other) + { + return + IsCorsPreflightRequest == other.IsCorsPreflightRequest && + HttpMethods.Equals(HttpMethod, other.HttpMethod); + } - public override bool Equals(object? obj) - { - var other = obj as EdgeKey?; - return other == null ? false : Equals(other.Value); - } + public override bool Equals(object? obj) + { + var other = obj as EdgeKey?; + return other == null ? false : Equals(other.Value); + } - public override int GetHashCode() - { - var hash = new HashCode(); - hash.Add(IsCorsPreflightRequest ? 1 : 0); - hash.Add(HttpMethod, StringComparer.Ordinal); - return hash.ToHashCode(); - } + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(IsCorsPreflightRequest ? 1 : 0); + hash.Add(HttpMethod, StringComparer.Ordinal); + return hash.ToHashCode(); + } - // Used in GraphViz output. - public override string ToString() - { - return IsCorsPreflightRequest ? $"CORS: {HttpMethod}" : $"HTTP: {HttpMethod}"; - } + // Used in GraphViz output. + public override string ToString() + { + return IsCorsPreflightRequest ? $"CORS: {HttpMethod}" : $"HTTP: {HttpMethod}"; } } } diff --git a/src/Http/Routing/src/Matching/HttpMethodSingleEntryPolicyJumpTable.cs b/src/Http/Routing/src/Matching/HttpMethodSingleEntryPolicyJumpTable.cs index 52b1607d0d..75e03b76bd 100644 --- a/src/Http/Routing/src/Matching/HttpMethodSingleEntryPolicyJumpTable.cs +++ b/src/Http/Routing/src/Matching/HttpMethodSingleEntryPolicyJumpTable.cs @@ -3,43 +3,42 @@ using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal sealed class HttpMethodSingleEntryPolicyJumpTable : PolicyJumpTable { - internal sealed class HttpMethodSingleEntryPolicyJumpTable : PolicyJumpTable - { - private readonly int _exitDestination; - private readonly string _method; - private readonly int _destination; - private readonly int _corsPreflightExitDestination; - private readonly int _corsPreflightDestination; + private readonly int _exitDestination; + private readonly string _method; + private readonly int _destination; + private readonly int _corsPreflightExitDestination; + private readonly int _corsPreflightDestination; - private readonly bool _supportsCorsPreflight; + private readonly bool _supportsCorsPreflight; - public HttpMethodSingleEntryPolicyJumpTable( - int exitDestination, - string method, - int destination, - bool supportsCorsPreflight, - int corsPreflightExitDestination, - int corsPreflightDestination) - { - _exitDestination = exitDestination; - _method = method; - _destination = destination; - _supportsCorsPreflight = supportsCorsPreflight; - _corsPreflightExitDestination = corsPreflightExitDestination; - _corsPreflightDestination = corsPreflightDestination; - } + public HttpMethodSingleEntryPolicyJumpTable( + int exitDestination, + string method, + int destination, + bool supportsCorsPreflight, + int corsPreflightExitDestination, + int corsPreflightDestination) + { + _exitDestination = exitDestination; + _method = method; + _destination = destination; + _supportsCorsPreflight = supportsCorsPreflight; + _corsPreflightExitDestination = corsPreflightExitDestination; + _corsPreflightDestination = corsPreflightDestination; + } - public override int GetDestination(HttpContext httpContext) + public override int GetDestination(HttpContext httpContext) + { + var httpMethod = httpContext.Request.Method; + if (_supportsCorsPreflight && HttpMethodMatcherPolicy.IsCorsPreflightRequest(httpContext, httpMethod, out var accessControlRequestMethod)) { - var httpMethod = httpContext.Request.Method; - if (_supportsCorsPreflight && HttpMethodMatcherPolicy.IsCorsPreflightRequest(httpContext, httpMethod, out var accessControlRequestMethod)) - { - return HttpMethods.Equals(accessControlRequestMethod.ToString(), _method) ? _corsPreflightDestination : _corsPreflightExitDestination; - } - - return HttpMethods.Equals(httpMethod, _method) ? _destination : _exitDestination; + return HttpMethods.Equals(accessControlRequestMethod.ToString(), _method) ? _corsPreflightDestination : _corsPreflightExitDestination; } + + return HttpMethods.Equals(httpMethod, _method) ? _destination : _exitDestination; } } diff --git a/src/Http/Routing/src/Matching/IEndpointComparerPolicy.cs b/src/Http/Routing/src/Matching/IEndpointComparerPolicy.cs index 9aec53af23..a6bb7c0848 100644 --- a/src/Http/Routing/src/Matching/IEndpointComparerPolicy.cs +++ b/src/Http/Routing/src/Matching/IEndpointComparerPolicy.cs @@ -4,32 +4,31 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +/// +/// A interface that can be implemented to sort +/// endpoints. Implementations of must +/// inherit from and should be registered in +/// the dependency injection container as singleton services of type . +/// +/// +/// +/// Candidates in a are sorted based on their priority. Defining +/// a adds an additional criterion to the sorting +/// operation used to order candidates. +/// +/// +/// As an example, the implementation of implements +/// to ensure that endpoints matching specific HTTP +/// methods are sorted with a higher priority than endpoints without a specific HTTP method +/// requirement. +/// +/// +public interface IEndpointComparerPolicy { /// - /// A interface that can be implemented to sort - /// endpoints. Implementations of must - /// inherit from and should be registered in - /// the dependency injection container as singleton services of type . + /// Gets an that will be used to sort the endpoints. /// - /// - /// - /// Candidates in a are sorted based on their priority. Defining - /// a adds an additional criterion to the sorting - /// operation used to order candidates. - /// - /// - /// As an example, the implementation of implements - /// to ensure that endpoints matching specific HTTP - /// methods are sorted with a higher priority than endpoints without a specific HTTP method - /// requirement. - /// - /// - public interface IEndpointComparerPolicy - { - /// - /// Gets an that will be used to sort the endpoints. - /// - IComparer Comparer { get; } - } + IComparer Comparer { get; } } diff --git a/src/Http/Routing/src/Matching/IEndpointSelectorPolicy.cs b/src/Http/Routing/src/Matching/IEndpointSelectorPolicy.cs index 2698280e2d..006e5d5108 100644 --- a/src/Http/Routing/src/Matching/IEndpointSelectorPolicy.cs +++ b/src/Http/Routing/src/Matching/IEndpointSelectorPolicy.cs @@ -1,50 +1,49 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Http; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Matching +/// +/// A interface that can implemented to filter endpoints +/// in a . Implementations of must +/// inherit from and should be registered in +/// the dependency injection container as singleton services of type . +/// +public interface IEndpointSelectorPolicy { /// - /// A interface that can implemented to filter endpoints - /// in a . Implementations of must - /// inherit from and should be registered in - /// the dependency injection container as singleton services of type . + /// Returns a value that indicates whether the applies + /// to any endpoint in . /// - public interface IEndpointSelectorPolicy - { - /// - /// Returns a value that indicates whether the applies - /// to any endpoint in . - /// - /// The set of candidate values. - /// - /// true if the policy applies to any endpoint in , otherwise false. - /// - bool AppliesToEndpoints(IReadOnlyList endpoints); + /// The set of candidate values. + /// + /// true if the policy applies to any endpoint in , otherwise false. + /// + bool AppliesToEndpoints(IReadOnlyList endpoints); - /// - /// Applies the policy to the . - /// - /// - /// The associated with the current request. - /// - /// The . - /// - /// - /// Implementations of should implement this method - /// and filter the set of candidates in the by setting - /// to false where desired. - /// - /// - /// To signal an error condition, the should assign the endpoint by - /// calling - /// and setting to an - /// value that will produce the desired error when executed. - /// - /// - Task ApplyAsync(HttpContext httpContext, CandidateSet candidates); - } + /// + /// Applies the policy to the . + /// + /// + /// The associated with the current request. + /// + /// The . + /// + /// + /// Implementations of should implement this method + /// and filter the set of candidates in the by setting + /// to false where desired. + /// + /// + /// To signal an error condition, the should assign the endpoint by + /// calling + /// and setting to an + /// value that will produce the desired error when executed. + /// + /// + Task ApplyAsync(HttpContext httpContext, CandidateSet candidates); } diff --git a/src/Http/Routing/src/Matching/ILEmitTrieFactory.cs b/src/Http/Routing/src/Matching/ILEmitTrieFactory.cs index 5f17adbe71..2334256dfe 100644 --- a/src/Http/Routing/src/Matching/ILEmitTrieFactory.cs +++ b/src/Http/Routing/src/Matching/ILEmitTrieFactory.cs @@ -11,388 +11,388 @@ using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal static class ILEmitTrieFactory { - internal static class ILEmitTrieFactory + // The algorthm we use only works for ASCII text. If we find non-ASCII text in the input + // we need to reject it and let is be processed with a fallback technique. + public const int NotAscii = int.MinValue; + + // Creates a Func of (string path, int start, int length) => destination + // Not using PathSegment here because we don't want to mess with visibility checks and + // generating IL without it is easier. + public static Func Create( + int defaultDestination, + int exitDestination, + (string text, int destination)[] entries, + bool? vectorize) { - // The algorthm we use only works for ASCII text. If we find non-ASCII text in the input - // we need to reject it and let is be processed with a fallback technique. - public const int NotAscii = int.MinValue; - - // Creates a Func of (string path, int start, int length) => destination - // Not using PathSegment here because we don't want to mess with visibility checks and - // generating IL without it is easier. - public static Func Create( - int defaultDestination, - int exitDestination, - (string text, int destination)[] entries, - bool? vectorize) - { - var method = new DynamicMethod( - "GetDestination", - typeof(int), - new[] { typeof(string), typeof(int), typeof(int), }); + var method = new DynamicMethod( + "GetDestination", + typeof(int), + new[] { typeof(string), typeof(int), typeof(int), }); - GenerateMethodBody(method.GetILGenerator(), defaultDestination, exitDestination, entries, vectorize); + GenerateMethodBody(method.GetILGenerator(), defaultDestination, exitDestination, entries, vectorize); #if IL_EMIT_SAVE_ASSEMBLY SaveAssembly(method.GetILGenerator(), defaultDestination, exitDestination, entries, vectorize); #endif - return (Func)method.CreateDelegate(typeof(Func)); - } - - // Internal for testing - internal static bool ShouldVectorize((string text, int destination)[] entries) - { - // There's no value in vectorizing the computation if we're on 32bit or - // if no string is long enough. We do the vectorized comparison with uint64 ulongs - // which isn't beneficial if they don't map to the native size of the CPU. The - // vectorized algorithm introduces additional overhead for casing. - - // Vectorize by default on 64bit (allow override for testing) - return (IntPtr.Size == 8) && - - // Don't vectorize if all of the strings are small (prevents allocating unused locals) - entries.Any(e => e.text.Length >= 4); - } + return (Func)method.CreateDelegate(typeof(Func)); + } - private static void GenerateMethodBody( - ILGenerator il, - int defaultDestination, - int exitDestination, - (string text, int destination)[] entries, - bool? vectorize) - { + // Internal for testing + internal static bool ShouldVectorize((string text, int destination)[] entries) + { + // There's no value in vectorizing the computation if we're on 32bit or + // if no string is long enough. We do the vectorized comparison with uint64 ulongs + // which isn't beneficial if they don't map to the native size of the CPU. The + // vectorized algorithm introduces additional overhead for casing. - vectorize = vectorize ?? ShouldVectorize(entries); + // Vectorize by default on 64bit (allow override for testing) + return (IntPtr.Size == 8) && - // See comments on Locals for details - var locals = new Locals(il, vectorize.Value); + // Don't vectorize if all of the strings are small (prevents allocating unused locals) + entries.Any(e => e.text.Length >= 4); + } - // See comments on Labels for details - var labels = new Labels() - { - ReturnDefault = il.DefineLabel(), - ReturnNotAscii = il.DefineLabel(), - }; + private static void GenerateMethodBody( + ILGenerator il, + int defaultDestination, + int exitDestination, + (string text, int destination)[] entries, + bool? vectorize) + { - // See comments on Methods for details - var methods = Methods.Instance; + vectorize = vectorize ?? ShouldVectorize(entries); - // Initializing top-level locals - this is similar to... - // ReadOnlySpan span = arg0.AsSpan(arg1, arg2); - // ref byte p = ref Unsafe.As(MemoryMarshal.GetReference(span)) + // See comments on Locals for details + var locals = new Locals(il, vectorize.Value); - // arg0.AsSpan(arg1, arg2) - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Ldarg_1); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Call, methods.AsSpan); + // See comments on Labels for details + var labels = new Labels() + { + ReturnDefault = il.DefineLabel(), + ReturnNotAscii = il.DefineLabel(), + }; - // ReadOnlySpan = ... - il.Emit(OpCodes.Stloc, locals.Span); + // See comments on Methods for details + var methods = Methods.Instance; - // MemoryMarshal.GetReference(span) - il.Emit(OpCodes.Ldloc, locals.Span); - il.Emit(OpCodes.Call, methods.GetReference); + // Initializing top-level locals - this is similar to... + // ReadOnlySpan span = arg0.AsSpan(arg1, arg2); + // ref byte p = ref Unsafe.As(MemoryMarshal.GetReference(span)) - // Unsafe.As(...) - il.Emit(OpCodes.Call, methods.As); + // arg0.AsSpan(arg1, arg2) + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Call, methods.AsSpan); - // ref byte p = ... - il.Emit(OpCodes.Stloc_0, locals.P); + // ReadOnlySpan = ... + il.Emit(OpCodes.Stloc, locals.Span); - var groups = entries.GroupBy(e => e.text.Length).ToArray(); - for (var i = 0; i < groups.Length; i++) - { - var group = groups[i]; + // MemoryMarshal.GetReference(span) + il.Emit(OpCodes.Ldloc, locals.Span); + il.Emit(OpCodes.Call, methods.GetReference); - // Similar to 'if (length != X) { ... } - var inside = il.DefineLabel(); - var next = il.DefineLabel(); - il.Emit(OpCodes.Ldarg_2); - il.Emit(OpCodes.Ldc_I4, group.Key); - il.Emit(OpCodes.Beq, inside); - il.Emit(OpCodes.Br, next); + // Unsafe.As(...) + il.Emit(OpCodes.Call, methods.As); - // Process the group - il.MarkLabel(inside); - EmitTable(il, group.ToArray(), 0, group.Key, locals, labels, methods); - il.MarkLabel(next); - } + // ref byte p = ... + il.Emit(OpCodes.Stloc_0, locals.P); - // Exit point - we end up here when the text doesn't match - il.MarkLabel(labels.ReturnDefault); - il.Emit(OpCodes.Ldc_I4, defaultDestination); - il.Emit(OpCodes.Ret); + var groups = entries.GroupBy(e => e.text.Length).ToArray(); + for (var i = 0; i < groups.Length; i++) + { + var group = groups[i]; - // Exit point - we end up here with the text contains non-ASCII text - il.MarkLabel(labels.ReturnNotAscii); - il.Emit(OpCodes.Ldc_I4, NotAscii); - il.Emit(OpCodes.Ret); + // Similar to 'if (length != X) { ... } + var inside = il.DefineLabel(); + var next = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Ldc_I4, group.Key); + il.Emit(OpCodes.Beq, inside); + il.Emit(OpCodes.Br, next); + + // Process the group + il.MarkLabel(inside); + EmitTable(il, group.ToArray(), 0, group.Key, locals, labels, methods); + il.MarkLabel(next); } - private static void EmitTable( - ILGenerator il, - (string text, int destination)[] entries, - int index, - int length, - Locals locals, - Labels labels, - Methods methods) - { - // We've reached the end of the string. - if (index == length) - { - EmitReturnDestination(il, entries); - return; - } + // Exit point - we end up here when the text doesn't match + il.MarkLabel(labels.ReturnDefault); + il.Emit(OpCodes.Ldc_I4, defaultDestination); + il.Emit(OpCodes.Ret); - // If 4 or more characters remain, and we're vectorizing, we should process 4 characters at a time. - if (length - index >= 4 && locals.UInt64Value != null) - { - EmitVectorizedTable(il, entries, index, length, locals, labels, methods); - return; - } + // Exit point - we end up here with the text contains non-ASCII text + il.MarkLabel(labels.ReturnNotAscii); + il.Emit(OpCodes.Ldc_I4, NotAscii); + il.Emit(OpCodes.Ret); + } - // Fall back to processing a character at a time. - EmitSingleCharacterTable(il, entries, index, length, locals, labels, methods); + private static void EmitTable( + ILGenerator il, + (string text, int destination)[] entries, + int index, + int length, + Locals locals, + Labels labels, + Methods methods) + { + // We've reached the end of the string. + if (index == length) + { + EmitReturnDestination(il, entries); + return; } - private static void EmitVectorizedTable( - ILGenerator il, - (string text, int destination)[] entries, - int index, - int length, - Locals locals, - Labels labels, - Methods methods) + // If 4 or more characters remain, and we're vectorizing, we should process 4 characters at a time. + if (length - index >= 4 && locals.UInt64Value != null) { - // Emits code similar to: - // - // uint64Value = Unsafe.ReadUnaligned(ref p); - // p = ref Unsafe.Add(ref p, 8); - // - // if ((uint64Value & ~0x007F007F007F007FUL) == 0) - // { - // return NotAscii; - // } - // uint64LowerIndicator = value + (0x0080008000800080UL - 0x0041004100410041UL); - // uint64UpperIndicator = value + (0x0080008000800080UL - 0x005B005B005B005BUL); - // ulong temp1 = uint64LowerIndicator ^ uint64UpperIndicator - // ulong temp2 = temp1 & 0x0080008000800080UL; - // ulong temp3 = (temp2) >> 2; - // uint64Value = uint64Value ^ temp3; - // - // This is a vectorized non-branching technique for processing 4 utf16 characters - // at a time inside a single uint64. - // - // Similar to: - // https://github.com/GrabYourPitchforks/coreclr/commit/a3c1df25c4225995ffd6b18fd0fc39d6b81fd6a5#diff-d89b6ca07ea349899e45eed5f688a7ebR81 - // - // Basically we need to check if the text is non-ASCII first and bail if it is. - // The rest of the steps will convert the text to lowercase by checking all characters - // at a time to see if they are in the A-Z range, that's where 0x0041 and 0x005B come in. - - // IMPORTANT - // - // If you are modifying this code, be aware that the easiest way to make a mistake is by - // getting the set of casts wrong doing something like: - // - // il.Emit(OpCodes.Ldc_I8, ~0x007F007F007F007FUL); - // - // The IL Emit apis don't have overloads that accept ulong or ushort, and will resolve - // an overload that does an undesirable conversion (for instance converting ulong to float). - // - // IMPORTANT - - // Unsafe.ReadUnaligned(ref p) - il.Emit(OpCodes.Ldloc, locals.P); - il.Emit(OpCodes.Call, methods.ReadUnalignedUInt64); - - // uint64Value = ... - il.Emit(OpCodes.Stloc, locals.UInt64Value); - - // Unsafe.Add(ref p, 8) - il.Emit(OpCodes.Ldloc, locals.P); - il.Emit(OpCodes.Ldc_I4, 8); // 8 bytes were read - il.Emit(OpCodes.Call, methods.Add); - - // p = ref ... - il.Emit(OpCodes.Stloc, locals.P); - - // if ((uint64Value & ~0x007F007F007F007FUL) == 0) - // { - // goto: NotAscii; - // } - il.Emit(OpCodes.Ldloc, locals.UInt64Value); - il.Emit(OpCodes.Ldc_I8, unchecked((long)~0x007F007F007F007FUL)); - il.Emit(OpCodes.And); - il.Emit(OpCodes.Brtrue, labels.ReturnNotAscii); - - // uint64Value + (0x0080008000800080UL - 0x0041004100410041UL) - il.Emit(OpCodes.Ldloc, locals.UInt64Value); - il.Emit(OpCodes.Ldc_I8, unchecked((long)(0x0080008000800080UL - 0x0041004100410041UL))); - il.Emit(OpCodes.Add); - - // uint64LowerIndicator = ... - il.Emit(OpCodes.Stloc, locals.UInt64LowerIndicator); - - // value + (0x0080008000800080UL - 0x005B005B005B005BUL) - il.Emit(OpCodes.Ldloc, locals.UInt64Value); - il.Emit(OpCodes.Ldc_I8, unchecked((long)(0x0080008000800080UL - 0x005B005B005B005BUL))); - il.Emit(OpCodes.Add); - - // uint64UpperIndicator = ... - il.Emit(OpCodes.Stloc, locals.UInt64UpperIndicator); - - // ulongLowerIndicator ^ ulongUpperIndicator - il.Emit(OpCodes.Ldloc, locals.UInt64LowerIndicator); - il.Emit(OpCodes.Ldloc, locals.UInt64UpperIndicator); - il.Emit(OpCodes.Xor); - - // ... & 0x0080008000800080UL - il.Emit(OpCodes.Ldc_I8, unchecked((long)0x0080008000800080UL)); - il.Emit(OpCodes.And); + EmitVectorizedTable(il, entries, index, length, locals, labels, methods); + return; + } - // ... >> 2; - il.Emit(OpCodes.Ldc_I4, 2); - il.Emit(OpCodes.Shr_Un); + // Fall back to processing a character at a time. + EmitSingleCharacterTable(il, entries, index, length, locals, labels, methods); + } - // ... ^ uint64Value + private static void EmitVectorizedTable( + ILGenerator il, + (string text, int destination)[] entries, + int index, + int length, + Locals locals, + Labels labels, + Methods methods) + { + // Emits code similar to: + // + // uint64Value = Unsafe.ReadUnaligned(ref p); + // p = ref Unsafe.Add(ref p, 8); + // + // if ((uint64Value & ~0x007F007F007F007FUL) == 0) + // { + // return NotAscii; + // } + // uint64LowerIndicator = value + (0x0080008000800080UL - 0x0041004100410041UL); + // uint64UpperIndicator = value + (0x0080008000800080UL - 0x005B005B005B005BUL); + // ulong temp1 = uint64LowerIndicator ^ uint64UpperIndicator + // ulong temp2 = temp1 & 0x0080008000800080UL; + // ulong temp3 = (temp2) >> 2; + // uint64Value = uint64Value ^ temp3; + // + // This is a vectorized non-branching technique for processing 4 utf16 characters + // at a time inside a single uint64. + // + // Similar to: + // https://github.com/GrabYourPitchforks/coreclr/commit/a3c1df25c4225995ffd6b18fd0fc39d6b81fd6a5#diff-d89b6ca07ea349899e45eed5f688a7ebR81 + // + // Basically we need to check if the text is non-ASCII first and bail if it is. + // The rest of the steps will convert the text to lowercase by checking all characters + // at a time to see if they are in the A-Z range, that's where 0x0041 and 0x005B come in. + + // IMPORTANT + // + // If you are modifying this code, be aware that the easiest way to make a mistake is by + // getting the set of casts wrong doing something like: + // + // il.Emit(OpCodes.Ldc_I8, ~0x007F007F007F007FUL); + // + // The IL Emit apis don't have overloads that accept ulong or ushort, and will resolve + // an overload that does an undesirable conversion (for instance converting ulong to float). + // + // IMPORTANT + + // Unsafe.ReadUnaligned(ref p) + il.Emit(OpCodes.Ldloc, locals.P); + il.Emit(OpCodes.Call, methods.ReadUnalignedUInt64); + + // uint64Value = ... + il.Emit(OpCodes.Stloc, locals.UInt64Value); + + // Unsafe.Add(ref p, 8) + il.Emit(OpCodes.Ldloc, locals.P); + il.Emit(OpCodes.Ldc_I4, 8); // 8 bytes were read + il.Emit(OpCodes.Call, methods.Add); + + // p = ref ... + il.Emit(OpCodes.Stloc, locals.P); + + // if ((uint64Value & ~0x007F007F007F007FUL) == 0) + // { + // goto: NotAscii; + // } + il.Emit(OpCodes.Ldloc, locals.UInt64Value); + il.Emit(OpCodes.Ldc_I8, unchecked((long)~0x007F007F007F007FUL)); + il.Emit(OpCodes.And); + il.Emit(OpCodes.Brtrue, labels.ReturnNotAscii); + + // uint64Value + (0x0080008000800080UL - 0x0041004100410041UL) + il.Emit(OpCodes.Ldloc, locals.UInt64Value); + il.Emit(OpCodes.Ldc_I8, unchecked((long)(0x0080008000800080UL - 0x0041004100410041UL))); + il.Emit(OpCodes.Add); + + // uint64LowerIndicator = ... + il.Emit(OpCodes.Stloc, locals.UInt64LowerIndicator); + + // value + (0x0080008000800080UL - 0x005B005B005B005BUL) + il.Emit(OpCodes.Ldloc, locals.UInt64Value); + il.Emit(OpCodes.Ldc_I8, unchecked((long)(0x0080008000800080UL - 0x005B005B005B005BUL))); + il.Emit(OpCodes.Add); + + // uint64UpperIndicator = ... + il.Emit(OpCodes.Stloc, locals.UInt64UpperIndicator); + + // ulongLowerIndicator ^ ulongUpperIndicator + il.Emit(OpCodes.Ldloc, locals.UInt64LowerIndicator); + il.Emit(OpCodes.Ldloc, locals.UInt64UpperIndicator); + il.Emit(OpCodes.Xor); + + // ... & 0x0080008000800080UL + il.Emit(OpCodes.Ldc_I8, unchecked((long)0x0080008000800080UL)); + il.Emit(OpCodes.And); + + // ... >> 2; + il.Emit(OpCodes.Ldc_I4, 2); + il.Emit(OpCodes.Shr_Un); + + // ... ^ uint64Value + il.Emit(OpCodes.Ldloc, locals.UInt64Value); + il.Emit(OpCodes.Xor); + + // uint64Value = ... + il.Emit(OpCodes.Stloc, locals.UInt64Value); + + // Now we generate an 'if' ladder with an entry for each of the unique 64 bit sections + // of the text. + var groups = entries.GroupBy(e => GetUInt64Key(e.text, index)); + foreach (var group in groups) + { + // if (uint64Value == 0x.....) { ... } + var next = il.DefineLabel(); il.Emit(OpCodes.Ldloc, locals.UInt64Value); - il.Emit(OpCodes.Xor); - - // uint64Value = ... - il.Emit(OpCodes.Stloc, locals.UInt64Value); + il.Emit(OpCodes.Ldc_I8, unchecked((long)group.Key)); + il.Emit(OpCodes.Bne_Un, next); - // Now we generate an 'if' ladder with an entry for each of the unique 64 bit sections - // of the text. - var groups = entries.GroupBy(e => GetUInt64Key(e.text, index)); - foreach (var group in groups) - { - // if (uint64Value == 0x.....) { ... } - var next = il.DefineLabel(); - il.Emit(OpCodes.Ldloc, locals.UInt64Value); - il.Emit(OpCodes.Ldc_I8, unchecked((long)group.Key)); - il.Emit(OpCodes.Bne_Un, next); - - // Process the group - EmitTable(il, group.ToArray(), index + 4, length, locals, labels, methods); - il.MarkLabel(next); - } - - // goto: defaultDestination - il.Emit(OpCodes.Br, labels.ReturnDefault); + // Process the group + EmitTable(il, group.ToArray(), index + 4, length, locals, labels, methods); + il.MarkLabel(next); } - private static void EmitSingleCharacterTable( - ILGenerator il, - (string text, int destination)[] entries, - int index, - int length, - Locals locals, - Labels labels, - Methods methods) + // goto: defaultDestination + il.Emit(OpCodes.Br, labels.ReturnDefault); + } + + private static void EmitSingleCharacterTable( + ILGenerator il, + (string text, int destination)[] entries, + int index, + int length, + Locals locals, + Labels labels, + Methods methods) + { + // See the vectorized code path for a much more thorough explanation. + + // IMPORTANT + // + // If you are modifying this code, be aware that the easiest way to make a mistake is by + // getting the set of casts wrong doing something like: + // + // il.Emit(OpCodes.Ldc_I4, ~0x007F); + // + // The IL Emit apis don't have overloads that accept ulong or ushort, and will resolve + // an overload that does an undesirable conversion (for instance convering ulong to float). + // + // IMPORTANT + + // Unsafe.ReadUnaligned(ref p) + il.Emit(OpCodes.Ldloc, locals.P); + il.Emit(OpCodes.Call, methods.ReadUnalignedUInt16); + + // uint16Value = ... + il.Emit(OpCodes.Stloc, locals.UInt16Value); + + // Unsafe.Add(ref p, 2) + il.Emit(OpCodes.Ldloc, locals.P); + il.Emit(OpCodes.Ldc_I4, 2); // 2 bytes were read + il.Emit(OpCodes.Call, methods.Add); + + // p = ref ... + il.Emit(OpCodes.Stloc, locals.P); + + // if ((uInt16Value & ~0x007FUL) == 0) + // { + // goto: NotAscii; + // } + il.Emit(OpCodes.Ldloc, locals.UInt16Value); + il.Emit(OpCodes.Ldc_I4, unchecked((int)((uint)~0x007F))); + il.Emit(OpCodes.And); + il.Emit(OpCodes.Brtrue, labels.ReturnNotAscii); + + // Since we're handling a single character at a time, it's easier to just + // generate an 'if' with two comparisons instead of doing complicated conversion + // logic. + + // Now we generate an 'if' ladder with an entry for each of the unique + // characters in the group. + var groups = entries.GroupBy(e => GetUInt16Key(e.text, index)); + foreach (var group in groups) { - // See the vectorized code path for a much more thorough explanation. - - // IMPORTANT - // - // If you are modifying this code, be aware that the easiest way to make a mistake is by - // getting the set of casts wrong doing something like: - // - // il.Emit(OpCodes.Ldc_I4, ~0x007F); - // - // The IL Emit apis don't have overloads that accept ulong or ushort, and will resolve - // an overload that does an undesirable conversion (for instance convering ulong to float). - // - // IMPORTANT - - // Unsafe.ReadUnaligned(ref p) - il.Emit(OpCodes.Ldloc, locals.P); - il.Emit(OpCodes.Call, methods.ReadUnalignedUInt16); - - // uint16Value = ... - il.Emit(OpCodes.Stloc, locals.UInt16Value); - - // Unsafe.Add(ref p, 2) - il.Emit(OpCodes.Ldloc, locals.P); - il.Emit(OpCodes.Ldc_I4, 2); // 2 bytes were read - il.Emit(OpCodes.Call, methods.Add); - - // p = ref ... - il.Emit(OpCodes.Stloc, locals.P); - - // if ((uInt16Value & ~0x007FUL) == 0) - // { - // goto: NotAscii; - // } + // if (uInt16Value == 'A' || uint16Value == 'a') { ... } + var next = il.DefineLabel(); + var inside = il.DefineLabel(); il.Emit(OpCodes.Ldloc, locals.UInt16Value); - il.Emit(OpCodes.Ldc_I4, unchecked((int)((uint)~0x007F))); - il.Emit(OpCodes.And); - il.Emit(OpCodes.Brtrue, labels.ReturnNotAscii); - - // Since we're handling a single character at a time, it's easier to just - // generate an 'if' with two comparisons instead of doing complicated conversion - // logic. - - // Now we generate an 'if' ladder with an entry for each of the unique - // characters in the group. - var groups = entries.GroupBy(e => GetUInt16Key(e.text, index)); - foreach (var group in groups) + il.Emit(OpCodes.Ldc_I4, unchecked((int)((uint)group.Key))); + il.Emit(OpCodes.Beq, inside); + + var upper = (ushort)char.ToUpperInvariant((char)group.Key); + if (upper != group.Key) { - // if (uInt16Value == 'A' || uint16Value == 'a') { ... } - var next = il.DefineLabel(); - var inside = il.DefineLabel(); il.Emit(OpCodes.Ldloc, locals.UInt16Value); - il.Emit(OpCodes.Ldc_I4, unchecked((int)((uint)group.Key))); + il.Emit(OpCodes.Ldc_I4, unchecked((int)((uint)upper))); il.Emit(OpCodes.Beq, inside); - - var upper = (ushort)char.ToUpperInvariant((char)group.Key); - if (upper != group.Key) - { - il.Emit(OpCodes.Ldloc, locals.UInt16Value); - il.Emit(OpCodes.Ldc_I4, unchecked((int)((uint)upper))); - il.Emit(OpCodes.Beq, inside); - } - - il.Emit(OpCodes.Br, next); - - // Process the group - il.MarkLabel(inside); - EmitTable(il, group.ToArray(), index + 1, length, locals, labels, methods); - il.MarkLabel(next); } - // goto: defaultDestination - il.Emit(OpCodes.Br, labels.ReturnDefault); - } + il.Emit(OpCodes.Br, next); - public static void EmitReturnDestination(ILGenerator il, (string text, int destination)[] entries) - { - Debug.Assert(entries.Length == 1, "We should have a single entry"); - il.Emit(OpCodes.Ldc_I4, entries[0].destination); - il.Emit(OpCodes.Ret); + // Process the group + il.MarkLabel(inside); + EmitTable(il, group.ToArray(), index + 1, length, locals, labels, methods); + il.MarkLabel(next); } - private static ulong GetUInt64Key(string text, int index) - { - Debug.Assert(index + 4 <= text.Length); - var span = text.ToLowerInvariant().AsSpan(index); - ref var p = ref Unsafe.As(ref MemoryMarshal.GetReference(span)); - return Unsafe.ReadUnaligned(ref p); - } + // goto: defaultDestination + il.Emit(OpCodes.Br, labels.ReturnDefault); + } - private static ushort GetUInt16Key(string text, int index) - { - Debug.Assert(index + 1 <= text.Length); - return (ushort)char.ToLowerInvariant(text[index]); - } + public static void EmitReturnDestination(ILGenerator il, (string text, int destination)[] entries) + { + Debug.Assert(entries.Length == 1, "We should have a single entry"); + il.Emit(OpCodes.Ldc_I4, entries[0].destination); + il.Emit(OpCodes.Ret); + } + + private static ulong GetUInt64Key(string text, int index) + { + Debug.Assert(index + 4 <= text.Length); + var span = text.ToLowerInvariant().AsSpan(index); + ref var p = ref Unsafe.As(ref MemoryMarshal.GetReference(span)); + return Unsafe.ReadUnaligned(ref p); + } + + private static ushort GetUInt16Key(string text, int index) + { + Debug.Assert(index + 1 <= text.Length); + return (ushort)char.ToLowerInvariant(text[index]); + } - // We require a special build-time define since this is a testing/debugging - // feature that will litter the app directory with assemblies. + // We require a special build-time define since this is a testing/debugging + // feature that will litter the app directory with assemblies. #if IL_EMIT_SAVE_ASSEMBLY private static void SaveAssembly( int defaultDestination, @@ -419,181 +419,180 @@ namespace Microsoft.AspNetCore.Routing.Matching } #endif - private class Locals + private class Locals + { + public Locals(ILGenerator il, bool vectorize) { - public Locals(ILGenerator il, bool vectorize) - { - P = il.DeclareLocal(typeof(byte).MakeByRefType()); - Span = il.DeclareLocal(typeof(ReadOnlySpan)); + P = il.DeclareLocal(typeof(byte).MakeByRefType()); + Span = il.DeclareLocal(typeof(ReadOnlySpan)); - UInt16Value = il.DeclareLocal(typeof(ushort)); + UInt16Value = il.DeclareLocal(typeof(ushort)); - if (vectorize) - { - UInt64Value = il.DeclareLocal(typeof(ulong)); - UInt64LowerIndicator = il.DeclareLocal(typeof(ulong)); - UInt64UpperIndicator = il.DeclareLocal(typeof(ulong)); - } + if (vectorize) + { + UInt64Value = il.DeclareLocal(typeof(ulong)); + UInt64LowerIndicator = il.DeclareLocal(typeof(ulong)); + UInt64UpperIndicator = il.DeclareLocal(typeof(ulong)); } - - /// - /// Holds current character when processing a character at a time. - /// - public LocalBuilder UInt16Value { get; } - - /// - /// Holds current character when processing 4 characters at a time. - /// - public LocalBuilder UInt64Value { get; } - - /// - /// Used to covert casing. See comments where it's used. - /// - public LocalBuilder UInt64LowerIndicator { get; } - - /// - /// Used to covert casing. See comments where it's used. - /// - public LocalBuilder UInt64UpperIndicator { get; } - - /// - /// Holds a 'ref byte' reference to the current character (in bytes). - /// - public LocalBuilder P { get; } - - /// - /// Holds the relevant portion of the path as a Span[byte]. - /// - public LocalBuilder Span { get; } } - private class Labels - { - /// - /// Label to goto that will return the default destination (not a match). - /// - public Label ReturnDefault { get; set; } - - /// - /// Label to goto that will return a sentinel value for non-ascii text. - /// - public Label ReturnNotAscii { get; set; } - } + /// + /// Holds current character when processing a character at a time. + /// + public LocalBuilder UInt16Value { get; } + + /// + /// Holds current character when processing 4 characters at a time. + /// + public LocalBuilder UInt64Value { get; } + + /// + /// Used to covert casing. See comments where it's used. + /// + public LocalBuilder UInt64LowerIndicator { get; } + + /// + /// Used to covert casing. See comments where it's used. + /// + public LocalBuilder UInt64UpperIndicator { get; } + + /// + /// Holds a 'ref byte' reference to the current character (in bytes). + /// + public LocalBuilder P { get; } + + /// + /// Holds the relevant portion of the path as a Span[byte]. + /// + public LocalBuilder Span { get; } + } + + private class Labels + { + /// + /// Label to goto that will return the default destination (not a match). + /// + public Label ReturnDefault { get; set; } + + /// + /// Label to goto that will return a sentinel value for non-ascii text. + /// + public Label ReturnNotAscii { get; set; } + } - private class Methods + private class Methods + { + // Caching because the methods won't change, if we're being called once we're likely to + // be called again. + public static readonly Methods Instance = new Methods(); + + private Methods() { - // Caching because the methods won't change, if we're being called once we're likely to - // be called again. - public static readonly Methods Instance = new Methods(); + // Can't use GetMethod because the parameter is a generic method parameters. + Add = typeof(Unsafe) + .GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(m => m.Name == nameof(Unsafe.Add)) + .Where(m => m.GetGenericArguments().Length == 1) + .Where(m => m.GetParameters().Length == 2) + .FirstOrDefault() + ?.MakeGenericMethod(typeof(byte)); + if (Add == null) + { + throw new InvalidOperationException("Failed to find Unsafe.Add{T}(ref T, int)"); + } + + // Can't use GetMethod because the parameter is a generic method parameters. + As = typeof(Unsafe) + .GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(m => m.Name == nameof(Unsafe.As)) + .Where(m => m.GetGenericArguments().Length == 2) + .Where(m => m.GetParameters().Length == 1) + .FirstOrDefault() + ?.MakeGenericMethod(typeof(char), typeof(byte)); + if (Add == null) + { + throw new InvalidOperationException("Failed to find Unsafe.As{TFrom, TTo}(ref TFrom)"); + } + + AsSpan = typeof(MemoryExtensions).GetMethod( + nameof(MemoryExtensions.AsSpan), + BindingFlags.Public | BindingFlags.Static, + binder: null, + new[] { typeof(string), typeof(int), typeof(int), }, + modifiers: null); + if (AsSpan == null) + { + throw new InvalidOperationException("Failed to find MemoryExtensions.AsSpan(string, int, int)"); + } - private Methods() + // Can't use GetMethod because the parameter is a generic method parameters. + GetReference = typeof(MemoryMarshal) + .GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(m => m.Name == nameof(MemoryMarshal.GetReference)) + .Where(m => m.GetGenericArguments().Length == 1) + .Where(m => m.GetParameters().Length == 1) + // Disambiguate between ReadOnlySpan<> and Span<> - this method is overloaded. + .Where(m => m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(ReadOnlySpan<>)) + .FirstOrDefault() + ?.MakeGenericMethod(typeof(char)); + if (GetReference == null) { - // Can't use GetMethod because the parameter is a generic method parameters. - Add = typeof(Unsafe) - .GetMethods(BindingFlags.Public | BindingFlags.Static) - .Where(m => m.Name == nameof(Unsafe.Add)) - .Where(m => m.GetGenericArguments().Length == 1) - .Where(m => m.GetParameters().Length == 2) - .FirstOrDefault() - ?.MakeGenericMethod(typeof(byte)); - if (Add == null) - { - throw new InvalidOperationException("Failed to find Unsafe.Add{T}(ref T, int)"); - } - - // Can't use GetMethod because the parameter is a generic method parameters. - As = typeof(Unsafe) - .GetMethods(BindingFlags.Public | BindingFlags.Static) - .Where(m => m.Name == nameof(Unsafe.As)) - .Where(m => m.GetGenericArguments().Length == 2) - .Where(m => m.GetParameters().Length == 1) - .FirstOrDefault() - ?.MakeGenericMethod(typeof(char), typeof(byte)); - if (Add == null) - { - throw new InvalidOperationException("Failed to find Unsafe.As{TFrom, TTo}(ref TFrom)"); - } - - AsSpan = typeof(MemoryExtensions).GetMethod( - nameof(MemoryExtensions.AsSpan), - BindingFlags.Public | BindingFlags.Static, - binder: null, - new[] { typeof(string), typeof(int), typeof(int), }, - modifiers: null); - if (AsSpan == null) - { - throw new InvalidOperationException("Failed to find MemoryExtensions.AsSpan(string, int, int)"); - } - - // Can't use GetMethod because the parameter is a generic method parameters. - GetReference = typeof(MemoryMarshal) - .GetMethods(BindingFlags.Public | BindingFlags.Static) - .Where(m => m.Name == nameof(MemoryMarshal.GetReference)) - .Where(m => m.GetGenericArguments().Length == 1) - .Where(m => m.GetParameters().Length == 1) - // Disambiguate between ReadOnlySpan<> and Span<> - this method is overloaded. - .Where(m => m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(ReadOnlySpan<>)) - .FirstOrDefault() - ?.MakeGenericMethod(typeof(char)); - if (GetReference == null) - { - throw new InvalidOperationException("Failed to find MemoryMarshal.GetReference{T}(ReadOnlySpan{T})"); - } - - ReadUnalignedUInt64 = typeof(Unsafe).GetMethod( - nameof(Unsafe.ReadUnaligned), - BindingFlags.Public | BindingFlags.Static, - binder: null, - new[] { typeof(byte).MakeByRefType(), }, - modifiers: null) - .MakeGenericMethod(typeof(ulong)); - if (ReadUnalignedUInt64 == null) - { - throw new InvalidOperationException("Failed to find Unsafe.ReadUnaligned{T}(ref byte)"); - } - - ReadUnalignedUInt16 = typeof(Unsafe).GetMethod( - nameof(Unsafe.ReadUnaligned), - BindingFlags.Public | BindingFlags.Static, - binder: null, - new[] { typeof(byte).MakeByRefType(), }, - modifiers: null) - .MakeGenericMethod(typeof(ushort)); - if (ReadUnalignedUInt16 == null) - { - throw new InvalidOperationException("Failed to find Unsafe.ReadUnaligned{T}(ref byte)"); - } + throw new InvalidOperationException("Failed to find MemoryMarshal.GetReference{T}(ReadOnlySpan{T})"); } - /// - /// - Add[ref byte] - /// - public MethodInfo Add { get; } - - /// - /// - As[char, byte] - /// - public MethodInfo As { get; } - - /// - /// - /// - public MethodInfo AsSpan { get; } - - /// - /// - GetReference[char] - /// - public MethodInfo GetReference { get; } - - /// - /// - ReadUnaligned[ulong] - /// - public MethodInfo ReadUnalignedUInt64 { get; } - - /// - /// - ReadUnaligned[ushort] - /// - public MethodInfo ReadUnalignedUInt16 { get; } + ReadUnalignedUInt64 = typeof(Unsafe).GetMethod( + nameof(Unsafe.ReadUnaligned), + BindingFlags.Public | BindingFlags.Static, + binder: null, + new[] { typeof(byte).MakeByRefType(), }, + modifiers: null) + .MakeGenericMethod(typeof(ulong)); + if (ReadUnalignedUInt64 == null) + { + throw new InvalidOperationException("Failed to find Unsafe.ReadUnaligned{T}(ref byte)"); + } + + ReadUnalignedUInt16 = typeof(Unsafe).GetMethod( + nameof(Unsafe.ReadUnaligned), + BindingFlags.Public | BindingFlags.Static, + binder: null, + new[] { typeof(byte).MakeByRefType(), }, + modifiers: null) + .MakeGenericMethod(typeof(ushort)); + if (ReadUnalignedUInt16 == null) + { + throw new InvalidOperationException("Failed to find Unsafe.ReadUnaligned{T}(ref byte)"); + } } + + /// + /// - Add[ref byte] + /// + public MethodInfo Add { get; } + + /// + /// - As[char, byte] + /// + public MethodInfo As { get; } + + /// + /// + /// + public MethodInfo AsSpan { get; } + + /// + /// - GetReference[char] + /// + public MethodInfo GetReference { get; } + + /// + /// - ReadUnaligned[ulong] + /// + public MethodInfo ReadUnalignedUInt64 { get; } + + /// + /// - ReadUnaligned[ushort] + /// + public MethodInfo ReadUnalignedUInt16 { get; } } } diff --git a/src/Http/Routing/src/Matching/ILEmitTrieJumpTable.cs b/src/Http/Routing/src/Matching/ILEmitTrieJumpTable.cs index e742795995..8f0128bb18 100644 --- a/src/Http/Routing/src/Matching/ILEmitTrieJumpTable.cs +++ b/src/Http/Routing/src/Matching/ILEmitTrieJumpTable.cs @@ -7,96 +7,95 @@ using System; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// Uses generated IL to implement the JumpTable contract. This approach requires +// a fallback jump table for two reasons: +// 1. We compute the IL lazily to avoid taking up significant time when processing a request +// 2. The generated IL only supports ASCII in the URL path +internal class ILEmitTrieJumpTable : JumpTable { - // Uses generated IL to implement the JumpTable contract. This approach requires - // a fallback jump table for two reasons: - // 1. We compute the IL lazily to avoid taking up significant time when processing a request - // 2. The generated IL only supports ASCII in the URL path - internal class ILEmitTrieJumpTable : JumpTable + private readonly int _defaultDestination; + private readonly int _exitDestination; + private readonly (string text, int destination)[] _entries; + + private readonly bool? _vectorize; + private readonly JumpTable _fallback; + + // Used to protect the initialization of the compiled delegate + private object _lock; + private bool _initializing; + private Task _task; + + // Will be replaced at runtime by the generated code. + // + // Internal for testing + internal Func _getDestination; + + public ILEmitTrieJumpTable( + int defaultDestination, + int exitDestination, + (string text, int destination)[] entries, + bool? vectorize, + JumpTable fallback) { - private readonly int _defaultDestination; - private readonly int _exitDestination; - private readonly (string text, int destination)[] _entries; - - private readonly bool? _vectorize; - private readonly JumpTable _fallback; - - // Used to protect the initialization of the compiled delegate - private object _lock; - private bool _initializing; - private Task _task; - - // Will be replaced at runtime by the generated code. - // - // Internal for testing - internal Func _getDestination; - - public ILEmitTrieJumpTable( - int defaultDestination, - int exitDestination, - (string text, int destination)[] entries, - bool? vectorize, - JumpTable fallback) - { - _defaultDestination = defaultDestination; - _exitDestination = exitDestination; - _entries = entries; - _vectorize = vectorize; - _fallback = fallback; + _defaultDestination = defaultDestination; + _exitDestination = exitDestination; + _entries = entries; + _vectorize = vectorize; + _fallback = fallback; - _getDestination = FallbackGetDestination; - } + _getDestination = FallbackGetDestination; + } + + public override int GetDestination(string path, PathSegment segment) + { + return _getDestination(path, segment); + } - public override int GetDestination(string path, PathSegment segment) + // Used when we haven't yet initialized the IL trie. We defer compilation of the IL for startup + // performance. + private int FallbackGetDestination(string path, PathSegment segment) + { + if (path.Length == 0) { - return _getDestination(path, segment); + return _exitDestination; } - // Used when we haven't yet initialized the IL trie. We defer compilation of the IL for startup - // performance. - private int FallbackGetDestination(string path, PathSegment segment) - { - if (path.Length == 0) - { - return _exitDestination; - } + // We only hit this code path if the IL delegate is still initializing. + LazyInitializer.EnsureInitialized(ref _task, ref _initializing, ref _lock, InitializeILDelegateAsync); - // We only hit this code path if the IL delegate is still initializing. - LazyInitializer.EnsureInitialized(ref _task, ref _initializing, ref _lock, InitializeILDelegateAsync); + return _fallback.GetDestination(path, segment); + } - return _fallback.GetDestination(path, segment); - } + // Internal for testing + internal async Task InitializeILDelegateAsync() + { + // Offload the creation of the IL delegate to the thread pool. + await Task.Run(() => + { + InitializeILDelegate(); + }); + } - // Internal for testing - internal async Task InitializeILDelegateAsync() + // Internal for testing + internal void InitializeILDelegate() + { + var generated = ILEmitTrieFactory.Create(_defaultDestination, _exitDestination, _entries, _vectorize); + _getDestination = (string path, PathSegment segment) => { - // Offload the creation of the IL delegate to the thread pool. - await Task.Run(() => + if (segment.Length == 0) { - InitializeILDelegate(); - }); - } + return _exitDestination; + } - // Internal for testing - internal void InitializeILDelegate() - { - var generated = ILEmitTrieFactory.Create(_defaultDestination, _exitDestination, _entries, _vectorize); - _getDestination = (string path, PathSegment segment) => + var result = generated(path, segment.Start, segment.Length); + if (result == ILEmitTrieFactory.NotAscii) { - if (segment.Length == 0) - { - return _exitDestination; - } - - var result = generated(path, segment.Start, segment.Length); - if (result == ILEmitTrieFactory.NotAscii) - { - result = _fallback.GetDestination(path, segment); - } - - return result; - }; - } + result = _fallback.GetDestination(path, segment); + } + + return result; + }; } } diff --git a/src/Http/Routing/src/Matching/INodeBuilderPolicy.cs b/src/Http/Routing/src/Matching/INodeBuilderPolicy.cs index f25c64fa57..c8fd4abf31 100644 --- a/src/Http/Routing/src/Matching/INodeBuilderPolicy.cs +++ b/src/Http/Routing/src/Matching/INodeBuilderPolicy.cs @@ -4,33 +4,32 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +/// +/// Implements an interface for a matcher policy with support for generating graph representations of the endpoints. +/// +public interface INodeBuilderPolicy { /// - /// Implements an interface for a matcher policy with support for generating graph representations of the endpoints. + /// Evaluates if the policy matches any of the endpoints provided in . /// - public interface INodeBuilderPolicy - { - /// - /// Evaluates if the policy matches any of the endpoints provided in . - /// - /// A list of . - /// if the policy applies to any of the provided . - bool AppliesToEndpoints(IReadOnlyList endpoints); + /// A list of . + /// if the policy applies to any of the provided . + bool AppliesToEndpoints(IReadOnlyList endpoints); - /// - /// Generates a graph that representations the relationship between endpoints and hosts. - /// - /// A list of . - /// A graph representing the relationship between endpoints and hosts. - IReadOnlyList GetEdges(IReadOnlyList endpoints); + /// + /// Generates a graph that representations the relationship between endpoints and hosts. + /// + /// A list of . + /// A graph representing the relationship between endpoints and hosts. + IReadOnlyList GetEdges(IReadOnlyList endpoints); - /// - /// Constructs a jump table given the a set of . - /// - /// The default destination for lookups. - /// A list of . - /// A instance. - PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges); - } + /// + /// Constructs a jump table given the a set of . + /// + /// The default destination for lookups. + /// A list of . + /// A instance. + PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges); } diff --git a/src/Http/Routing/src/Matching/IParameterLiteralNodeMatchingPolicy.cs b/src/Http/Routing/src/Matching/IParameterLiteralNodeMatchingPolicy.cs index 805527ed21..c7b5f833d5 100644 --- a/src/Http/Routing/src/Matching/IParameterLiteralNodeMatchingPolicy.cs +++ b/src/Http/Routing/src/Matching/IParameterLiteralNodeMatchingPolicy.cs @@ -1,22 +1,21 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +/// +/// Defines the contract that a class must implement in order to check if a literal value is valid for a given constraint. +/// +/// When a parameter implements this interface, the router is able to optimize away some paths from the route table that don't match this constraint. +/// +/// +public interface IParameterLiteralNodeMatchingPolicy : IParameterPolicy { /// - /// Defines the contract that a class must implement in order to check if a literal value is valid for a given constraint. - /// - /// When a parameter implements this interface, the router is able to optimize away some paths from the route table that don't match this constraint. - /// + /// Determines whether the given can match the constraint. /// - public interface IParameterLiteralNodeMatchingPolicy : IParameterPolicy - { - /// - /// Determines whether the given can match the constraint. - /// - /// The parameter name we are currently evaluating. - /// The literal to test the constraint against. - /// true if the literal contains a valid value; otherwise, false. - bool MatchesLiteral(string parameterName, string literal); - } + /// The parameter name we are currently evaluating. + /// The literal to test the constraint against. + /// true if the literal contains a valid value; otherwise, false. + bool MatchesLiteral(string parameterName, string literal); } diff --git a/src/Http/Routing/src/Matching/JumpTable.cs b/src/Http/Routing/src/Matching/JumpTable.cs index 1cad22dc56..db3a4fa4c9 100644 --- a/src/Http/Routing/src/Matching/JumpTable.cs +++ b/src/Http/Routing/src/Matching/JumpTable.cs @@ -3,16 +3,15 @@ using System.Diagnostics; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +[DebuggerDisplay("{DebuggerToString(),nq}")] +internal abstract class JumpTable { - [DebuggerDisplay("{DebuggerToString(),nq}")] - internal abstract class JumpTable - { - public abstract int GetDestination(string path, PathSegment segment); + public abstract int GetDestination(string path, PathSegment segment); - public virtual string DebuggerToString() - { - return GetType().Name; - } + public virtual string DebuggerToString() + { + return GetType().Name; } } diff --git a/src/Http/Routing/src/Matching/JumpTableBuilder.cs b/src/Http/Routing/src/Matching/JumpTableBuilder.cs index 3f7bb52e60..62e4d49c7a 100644 --- a/src/Http/Routing/src/Matching/JumpTableBuilder.cs +++ b/src/Http/Routing/src/Matching/JumpTableBuilder.cs @@ -4,94 +4,93 @@ using System; using System.Runtime.CompilerServices; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal static class JumpTableBuilder { - internal static class JumpTableBuilder - { - public const int InvalidDestination = -1; + public const int InvalidDestination = -1; - public static JumpTable Build(int defaultDestination, int exitDestination, (string text, int destination)[] pathEntries) + public static JumpTable Build(int defaultDestination, int exitDestination, (string text, int destination)[] pathEntries) + { + if (defaultDestination == InvalidDestination) { - if (defaultDestination == InvalidDestination) - { - var message = $"{nameof(defaultDestination)} is not set. Please report this as a bug."; - throw new InvalidOperationException(message); - } - - if (exitDestination == InvalidDestination) - { - var message = $"{nameof(exitDestination)} is not set. Please report this as a bug."; - throw new InvalidOperationException(message); - } + var message = $"{nameof(defaultDestination)} is not set. Please report this as a bug."; + throw new InvalidOperationException(message); + } - // The JumpTable implementation is chosen based on the number of entries. - // - // Basically the concerns that we're juggling here are that different implementations - // make sense depending on the characteristics of the entries. - // - // On netcoreapp we support IL generation of optimized tries that is much faster - // than anything we can do with string.Compare or dictionaries. However the IL emit - // strategy requires us to produce a fallback jump table - see comments on the class. + if (exitDestination == InvalidDestination) + { + var message = $"{nameof(exitDestination)} is not set. Please report this as a bug."; + throw new InvalidOperationException(message); + } - // We have an optimized fast path for zero entries since we don't have to - // do any string comparisons. - if (pathEntries == null || pathEntries.Length == 0) - { - return new ZeroEntryJumpTable(defaultDestination, exitDestination); - } + // The JumpTable implementation is chosen based on the number of entries. + // + // Basically the concerns that we're juggling here are that different implementations + // make sense depending on the characteristics of the entries. + // + // On netcoreapp we support IL generation of optimized tries that is much faster + // than anything we can do with string.Compare or dictionaries. However the IL emit + // strategy requires us to produce a fallback jump table - see comments on the class. - // The IL Emit jump table is not faster for a single entry - but we have an optimized version when all text - // is ASCII - if (pathEntries.Length == 1 && Ascii.IsAscii(pathEntries[0].text)) - { - var entry = pathEntries[0]; - return new SingleEntryAsciiJumpTable(defaultDestination, exitDestination, entry.text, entry.destination); - } + // We have an optimized fast path for zero entries since we don't have to + // do any string comparisons. + if (pathEntries == null || pathEntries.Length == 0) + { + return new ZeroEntryJumpTable(defaultDestination, exitDestination); + } - // We have a fallback that works for non-ASCII - if (pathEntries.Length == 1) - { - var entry = pathEntries[0]; - return new SingleEntryJumpTable(defaultDestination, exitDestination, entry.text, entry.destination); - } + // The IL Emit jump table is not faster for a single entry - but we have an optimized version when all text + // is ASCII + if (pathEntries.Length == 1 && Ascii.IsAscii(pathEntries[0].text)) + { + var entry = pathEntries[0]; + return new SingleEntryAsciiJumpTable(defaultDestination, exitDestination, entry.text, entry.destination); + } - // We choose a hard upper bound of 100 as the limit for when we switch to a dictionary - // over a trie. The reason is that while the dictionary has a bigger constant factor, - // it is O(1) vs a trie which is O(M * log(N)). Our perf testing shows that the trie - // is better for ~90 entries based on all of Azure's route table. Anything above 100 edges - // we'd consider to be a very very large node, and so while we don't think anyone will - // have a node this large in practice, we want to make sure the performance is reasonable - // for any size. - // - // Additionally if we're on 32bit, the scalability is worse, so switch to the dictionary at 50 - // entries. - var threshold = IntPtr.Size == 8 ? 100 : 50; - if (pathEntries.Length >= threshold) - { - return new DictionaryJumpTable(defaultDestination, exitDestination, pathEntries); - } + // We have a fallback that works for non-ASCII + if (pathEntries.Length == 1) + { + var entry = pathEntries[0]; + return new SingleEntryJumpTable(defaultDestination, exitDestination, entry.text, entry.destination); + } - // If we have more than a single string, the IL emit strategy is the fastest - but we need to decide - // what do for the fallback case. - JumpTable fallback; + // We choose a hard upper bound of 100 as the limit for when we switch to a dictionary + // over a trie. The reason is that while the dictionary has a bigger constant factor, + // it is O(1) vs a trie which is O(M * log(N)). Our perf testing shows that the trie + // is better for ~90 entries based on all of Azure's route table. Anything above 100 edges + // we'd consider to be a very very large node, and so while we don't think anyone will + // have a node this large in practice, we want to make sure the performance is reasonable + // for any size. + // + // Additionally if we're on 32bit, the scalability is worse, so switch to the dictionary at 50 + // entries. + var threshold = IntPtr.Size == 8 ? 100 : 50; + if (pathEntries.Length >= threshold) + { + return new DictionaryJumpTable(defaultDestination, exitDestination, pathEntries); + } - // Based on our testing a linear search is still faster than a dictionary at ten entries. - if (pathEntries.Length <= 10) - { - fallback = new LinearSearchJumpTable(defaultDestination, exitDestination, pathEntries); - } - else - { - fallback = new DictionaryJumpTable(defaultDestination, exitDestination, pathEntries); - } + // If we have more than a single string, the IL emit strategy is the fastest - but we need to decide + // what do for the fallback case. + JumpTable fallback; - // Use the ILEmitTrieJumpTable if the IL is going to be compiled (not interpreted) - if (RuntimeFeature.IsDynamicCodeCompiled) - { - return new ILEmitTrieJumpTable(defaultDestination, exitDestination, pathEntries, vectorize: null, fallback); - } + // Based on our testing a linear search is still faster than a dictionary at ten entries. + if (pathEntries.Length <= 10) + { + fallback = new LinearSearchJumpTable(defaultDestination, exitDestination, pathEntries); + } + else + { + fallback = new DictionaryJumpTable(defaultDestination, exitDestination, pathEntries); + } - return fallback; + // Use the ILEmitTrieJumpTable if the IL is going to be compiled (not interpreted) + if (RuntimeFeature.IsDynamicCodeCompiled) + { + return new ILEmitTrieJumpTable(defaultDestination, exitDestination, pathEntries, vectorize: null, fallback); } + + return fallback; } } diff --git a/src/Http/Routing/src/Matching/LinearSearchJumpTable.cs b/src/Http/Routing/src/Matching/LinearSearchJumpTable.cs index af4d2c12b3..0673a1bcb6 100644 --- a/src/Http/Routing/src/Matching/LinearSearchJumpTable.cs +++ b/src/Http/Routing/src/Matching/LinearSearchJumpTable.cs @@ -5,68 +5,67 @@ using System; using System.Linq; using System.Text; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal class LinearSearchJumpTable : JumpTable { - internal class LinearSearchJumpTable : JumpTable + private readonly int _defaultDestination; + private readonly int _exitDestination; + private readonly (string text, int destination)[] _entries; + + public LinearSearchJumpTable( + int defaultDestination, + int exitDestination, + (string text, int destination)[] entries) { - private readonly int _defaultDestination; - private readonly int _exitDestination; - private readonly (string text, int destination)[] _entries; + _defaultDestination = defaultDestination; + _exitDestination = exitDestination; + _entries = entries; + } - public LinearSearchJumpTable( - int defaultDestination, - int exitDestination, - (string text, int destination)[] entries) + public override int GetDestination(string path, PathSegment segment) + { + if (segment.Length == 0) { - _defaultDestination = defaultDestination; - _exitDestination = exitDestination; - _entries = entries; + return _exitDestination; } - public override int GetDestination(string path, PathSegment segment) + var entries = _entries; + for (var i = 0; i < entries.Length; i++) { - if (segment.Length == 0) - { - return _exitDestination; - } - - var entries = _entries; - for (var i = 0; i < entries.Length; i++) + var text = entries[i].text; + if (segment.Length == text.Length && + string.Compare( + path, + segment.Start, + text, + 0, + segment.Length, + StringComparison.OrdinalIgnoreCase) == 0) { - var text = entries[i].text; - if (segment.Length == text.Length && - string.Compare( - path, - segment.Start, - text, - 0, - segment.Length, - StringComparison.OrdinalIgnoreCase) == 0) - { - return entries[i].destination; - } + return entries[i].destination; } - - return _defaultDestination; } - public override string DebuggerToString() - { - var builder = new StringBuilder(); - builder.Append("{ "); + return _defaultDestination; + } - builder.AppendJoin(", ", _entries.Select(e => $"{e.text}: {e.destination}")); + public override string DebuggerToString() + { + var builder = new StringBuilder(); + builder.Append("{ "); - builder.Append("$+: "); - builder.Append(_defaultDestination); - builder.Append(", "); + builder.AppendJoin(", ", _entries.Select(e => $"{e.text}: {e.destination}")); - builder.Append("$0: "); - builder.Append(_defaultDestination); + builder.Append("$+: "); + builder.Append(_defaultDestination); + builder.Append(", "); - builder.Append(" }"); + builder.Append("$0: "); + builder.Append(_defaultDestination); - return builder.ToString(); - } + builder.Append(" }"); + + return builder.ToString(); } } diff --git a/src/Http/Routing/src/Matching/Matcher.cs b/src/Http/Routing/src/Matching/Matcher.cs index 040ea74df9..34d06fe9e7 100644 --- a/src/Http/Routing/src/Matching/Matcher.cs +++ b/src/Http/Routing/src/Matching/Matcher.cs @@ -5,19 +5,18 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +/// +/// An interface for components that can select an given the current request, as part +/// of the execution of . +/// +internal abstract class Matcher { /// - /// An interface for components that can select an given the current request, as part - /// of the execution of . + /// Attempts to asynchronously select an for the current request. /// - internal abstract class Matcher - { - /// - /// Attempts to asynchronously select an for the current request. - /// - /// The associated with the current request. - /// A which represents the asynchronous completion of the operation. - public abstract Task MatchAsync(HttpContext httpContext); - } + /// The associated with the current request. + /// A which represents the asynchronous completion of the operation. + public abstract Task MatchAsync(HttpContext httpContext); } diff --git a/src/Http/Routing/src/Matching/MatcherBuilder.cs b/src/Http/Routing/src/Matching/MatcherBuilder.cs index 65514ea9df..77771e2a4e 100644 --- a/src/Http/Routing/src/Matching/MatcherBuilder.cs +++ b/src/Http/Routing/src/Matching/MatcherBuilder.cs @@ -1,12 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal abstract class MatcherBuilder { - internal abstract class MatcherBuilder - { - public abstract void AddEndpoint(RouteEndpoint endpoint); + public abstract void AddEndpoint(RouteEndpoint endpoint); - public abstract Matcher Build(); - } + public abstract Matcher Build(); } diff --git a/src/Http/Routing/src/Matching/MatcherFactory.cs b/src/Http/Routing/src/Matching/MatcherFactory.cs index 3289d3a382..1ac712fcc2 100644 --- a/src/Http/Routing/src/Matching/MatcherFactory.cs +++ b/src/Http/Routing/src/Matching/MatcherFactory.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal abstract class MatcherFactory { - internal abstract class MatcherFactory - { - public abstract Matcher CreateMatcher(EndpointDataSource dataSource); - } + public abstract Matcher CreateMatcher(EndpointDataSource dataSource); } diff --git a/src/Http/Routing/src/Matching/MatcherPolicy.cs b/src/Http/Routing/src/Matching/MatcherPolicy.cs index 109f087e0c..3beef63b71 100644 --- a/src/Http/Routing/src/Matching/MatcherPolicy.cs +++ b/src/Http/Routing/src/Matching/MatcherPolicy.cs @@ -6,63 +6,62 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Defines a policy that applies behaviors to the URL matcher. Implementations +/// of and related interfaces must be registered +/// in the dependency injection container as singleton services of type +/// . +/// +/// +/// implementations can implement the following +/// interfaces , , +/// and . +/// +public abstract class MatcherPolicy { /// - /// Defines a policy that applies behaviors to the URL matcher. Implementations - /// of and related interfaces must be registered - /// in the dependency injection container as singleton services of type - /// . + /// Gets a value that determines the order the should + /// be applied. Policies are applied in ascending numeric value of the + /// property. + /// + public abstract int Order { get; } + + /// + /// Returns a value that indicates whether the provided contains + /// one or more dynamic endpoints. /// + /// The set of endpoints. + /// true if a dynamic endpoint is found; otherwise returns false. /// - /// implementations can implement the following - /// interfaces , , - /// and . + /// + /// The presence of signifies that an endpoint that may be replaced + /// during processing by an . + /// + /// + /// An implementation of should also implement + /// and use its implementation when a node contains a dynamic endpoint. + /// implementations rely on caching of data based on a static set of endpoints. This + /// is not possible when endpoints are replaced dynamically. + /// /// - public abstract class MatcherPolicy + protected static bool ContainsDynamicEndpoints(IReadOnlyList endpoints) { - /// - /// Gets a value that determines the order the should - /// be applied. Policies are applied in ascending numeric value of the - /// property. - /// - public abstract int Order { get; } - - /// - /// Returns a value that indicates whether the provided contains - /// one or more dynamic endpoints. - /// - /// The set of endpoints. - /// true if a dynamic endpoint is found; otherwise returns false. - /// - /// - /// The presence of signifies that an endpoint that may be replaced - /// during processing by an . - /// - /// - /// An implementation of should also implement - /// and use its implementation when a node contains a dynamic endpoint. - /// implementations rely on caching of data based on a static set of endpoints. This - /// is not possible when endpoints are replaced dynamically. - /// - /// - protected static bool ContainsDynamicEndpoints(IReadOnlyList endpoints) + if (endpoints == null) { - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } + throw new ArgumentNullException(nameof(endpoints)); + } - for (var i = 0; i < endpoints.Count; i++) + for (var i = 0; i < endpoints.Count; i++) + { + var metadata = endpoints[i].Metadata.GetMetadata(); + if (metadata?.IsDynamic == true) { - var metadata = endpoints[i].Metadata.GetMetadata(); - if (metadata?.IsDynamic == true) - { - return true; - } + return true; } - - return false; } + + return false; } } diff --git a/src/Http/Routing/src/Matching/PathSegment.cs b/src/Http/Routing/src/Matching/PathSegment.cs index a9c598ccfd..485db311a7 100644 --- a/src/Http/Routing/src/Matching/PathSegment.cs +++ b/src/Http/Routing/src/Matching/PathSegment.cs @@ -3,37 +3,36 @@ using System; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal readonly struct PathSegment : IEquatable { - internal readonly struct PathSegment : IEquatable + public readonly int Start; + public readonly int Length; + + public PathSegment(int start, int length) + { + Start = start; + Length = length; + } + + public override bool Equals(object? obj) + { + return obj is PathSegment segment ? Equals(segment) : false; + } + + public bool Equals(PathSegment other) + { + return Start == other.Start && Length == other.Length; + } + + public override int GetHashCode() + { + return Start; + } + + public override string ToString() { - public readonly int Start; - public readonly int Length; - - public PathSegment(int start, int length) - { - Start = start; - Length = length; - } - - public override bool Equals(object? obj) - { - return obj is PathSegment segment ? Equals(segment) : false; - } - - public bool Equals(PathSegment other) - { - return Start == other.Start && Length == other.Length; - } - - public override int GetHashCode() - { - return Start; - } - - public override string ToString() - { - return $"Segment({Start}:{Length})"; - } + return $"Segment({Start}:{Length})"; } } diff --git a/src/Http/Routing/src/Matching/PolicyJumpTable.cs b/src/Http/Routing/src/Matching/PolicyJumpTable.cs index 7420a6fe75..ba98dfb039 100644 --- a/src/Http/Routing/src/Matching/PolicyJumpTable.cs +++ b/src/Http/Routing/src/Matching/PolicyJumpTable.cs @@ -3,22 +3,21 @@ using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +/// +/// Supports retrieving endpoints that fulfill a certain matcher policy. +/// +public abstract class PolicyJumpTable { /// - /// Supports retrieving endpoints that fulfill a certain matcher policy. + /// Returns the destination for a given in the current jump table. /// - public abstract class PolicyJumpTable - { - /// - /// Returns the destination for a given in the current jump table. - /// - /// The associated with the current request. - public abstract int GetDestination(HttpContext httpContext); + /// The associated with the current request. + public abstract int GetDestination(HttpContext httpContext); - internal virtual string DebuggerToString() - { - return GetType().Name; - } + internal virtual string DebuggerToString() + { + return GetType().Name; } } diff --git a/src/Http/Routing/src/Matching/PolicyJumpTableEdge.cs b/src/Http/Routing/src/Matching/PolicyJumpTableEdge.cs index 30d85a1232..19ba2cb4d4 100644 --- a/src/Http/Routing/src/Matching/PolicyJumpTableEdge.cs +++ b/src/Http/Routing/src/Matching/PolicyJumpTableEdge.cs @@ -1,33 +1,32 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +/// +/// Represents an entry in a . +/// +public readonly struct PolicyJumpTableEdge { /// - /// Represents an entry in a . + /// Constructs a new instance. /// - public readonly struct PolicyJumpTableEdge + /// Represents the match heuristic of the policy. + /// + public PolicyJumpTableEdge(object state, int destination) { - /// - /// Constructs a new instance. - /// - /// Represents the match heuristic of the policy. - /// - public PolicyJumpTableEdge(object state, int destination) - { - State = state ?? throw new System.ArgumentNullException(nameof(state)); - Destination = destination; - } + State = state ?? throw new System.ArgumentNullException(nameof(state)); + Destination = destination; + } - /// - /// Gets the object used to represent the match heuristic. Can be a host, HTTP method, etc. - /// depending on the matcher policy. - /// - public object State { get; } + /// + /// Gets the object used to represent the match heuristic. Can be a host, HTTP method, etc. + /// depending on the matcher policy. + /// + public object State { get; } - /// - /// Gets the destination of the current entry. - /// - public int Destination { get; } - } + /// + /// Gets the destination of the current entry. + /// + public int Destination { get; } } diff --git a/src/Http/Routing/src/Matching/PolicyNodeEdge.cs b/src/Http/Routing/src/Matching/PolicyNodeEdge.cs index a845cdd92f..c13376064b 100644 --- a/src/Http/Routing/src/Matching/PolicyNodeEdge.cs +++ b/src/Http/Routing/src/Matching/PolicyNodeEdge.cs @@ -4,33 +4,32 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +/// +/// Represents an edge in a matcher policy graph. +/// +public readonly struct PolicyNodeEdge { /// - /// Represents an edge in a matcher policy graph. + /// Constructs a new instance. /// - public readonly struct PolicyNodeEdge + /// Represents the match heuristic of the policy. + /// Represents the endpoints that match the policy + public PolicyNodeEdge(object state, IReadOnlyList endpoints) { - /// - /// Constructs a new instance. - /// - /// Represents the match heuristic of the policy. - /// Represents the endpoints that match the policy - public PolicyNodeEdge(object state, IReadOnlyList endpoints) - { - State = state ?? throw new System.ArgumentNullException(nameof(state)); - Endpoints = endpoints ?? throw new System.ArgumentNullException(nameof(endpoints)); - } + State = state ?? throw new System.ArgumentNullException(nameof(state)); + Endpoints = endpoints ?? throw new System.ArgumentNullException(nameof(endpoints)); + } - /// - /// Gets the endpoints that match the policy defined by . - /// - public IReadOnlyList Endpoints { get; } + /// + /// Gets the endpoints that match the policy defined by . + /// + public IReadOnlyList Endpoints { get; } - /// - /// Gets the object used to represent the match heuristic. Can be a host, HTTP method, etc. - /// depending on the matcher policy. - /// - public object State { get; } - } + /// + /// Gets the object used to represent the match heuristic. Can be a host, HTTP method, etc. + /// depending on the matcher policy. + /// + public object State { get; } } diff --git a/src/Http/Routing/src/Matching/SingleEntryAsciiJumpTable.cs b/src/Http/Routing/src/Matching/SingleEntryAsciiJumpTable.cs index ccbcffed74..274f29a730 100644 --- a/src/Http/Routing/src/Matching/SingleEntryAsciiJumpTable.cs +++ b/src/Http/Routing/src/Matching/SingleEntryAsciiJumpTable.cs @@ -4,52 +4,51 @@ using System; using System.Runtime.CompilerServices; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// Optimized implementation for cases where we know that we're +// comparing to ASCII. +internal class SingleEntryAsciiJumpTable : JumpTable { - // Optimized implementation for cases where we know that we're - // comparing to ASCII. - internal class SingleEntryAsciiJumpTable : JumpTable + private readonly int _defaultDestination; + private readonly int _exitDestination; + private readonly string _text; + private readonly int _destination; + + public SingleEntryAsciiJumpTable( + int defaultDestination, + int exitDestination, + string text, + int destination) { - private readonly int _defaultDestination; - private readonly int _exitDestination; - private readonly string _text; - private readonly int _destination; - - public SingleEntryAsciiJumpTable( - int defaultDestination, - int exitDestination, - string text, - int destination) - { - _defaultDestination = defaultDestination; - _exitDestination = exitDestination; - _text = text; - _destination = destination; - } + _defaultDestination = defaultDestination; + _exitDestination = exitDestination; + _text = text; + _destination = destination; + } - public override int GetDestination(string path, PathSegment segment) + public override int GetDestination(string path, PathSegment segment) + { + var length = segment.Length; + if (length == 0) { - var length = segment.Length; - if (length == 0) - { - return _exitDestination; - } - - var text = _text; - if (length != text.Length) - { - return _defaultDestination; - } - - var a = path.AsSpan(segment.Start, length); - var b = text.AsSpan(); - - return Ascii.AsciiIgnoreCaseEquals(a, b, length) ? _destination : _defaultDestination; + return _exitDestination; } - public override string DebuggerToString() + var text = _text; + if (length != text.Length) { - return $"{{ {_text}: {_destination}, $+: {_defaultDestination}, $0: {_exitDestination} }}"; + return _defaultDestination; } + + var a = path.AsSpan(segment.Start, length); + var b = text.AsSpan(); + + return Ascii.AsciiIgnoreCaseEquals(a, b, length) ? _destination : _defaultDestination; + } + + public override string DebuggerToString() + { + return $"{{ {_text}: {_destination}, $+: {_defaultDestination}, $0: {_exitDestination} }}"; } } diff --git a/src/Http/Routing/src/Matching/SingleEntryJumpTable.cs b/src/Http/Routing/src/Matching/SingleEntryJumpTable.cs index 1c6a255858..cf1a25ebfb 100644 --- a/src/Http/Routing/src/Matching/SingleEntryJumpTable.cs +++ b/src/Http/Routing/src/Matching/SingleEntryJumpTable.cs @@ -3,52 +3,51 @@ using System; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal class SingleEntryJumpTable : JumpTable { - internal class SingleEntryJumpTable : JumpTable + private readonly int _defaultDestination; + private readonly int _exitDestination; + private readonly string _text; + private readonly int _destination; + + public SingleEntryJumpTable( + int defaultDestination, + int exitDestination, + string text, + int destination) { - private readonly int _defaultDestination; - private readonly int _exitDestination; - private readonly string _text; - private readonly int _destination; - - public SingleEntryJumpTable( - int defaultDestination, - int exitDestination, - string text, - int destination) - { - _defaultDestination = defaultDestination; - _exitDestination = exitDestination; - _text = text; - _destination = destination; - } + _defaultDestination = defaultDestination; + _exitDestination = exitDestination; + _text = text; + _destination = destination; + } - public override int GetDestination(string path, PathSegment segment) + public override int GetDestination(string path, PathSegment segment) + { + if (segment.Length == 0) { - if (segment.Length == 0) - { - return _exitDestination; - } - - if (segment.Length == _text.Length && - string.Compare( - path, - segment.Start, - _text, - 0, - segment.Length, - StringComparison.OrdinalIgnoreCase) == 0) - { - return _destination; - } - - return _defaultDestination; + return _exitDestination; } - public override string DebuggerToString() + if (segment.Length == _text.Length && + string.Compare( + path, + segment.Start, + _text, + 0, + segment.Length, + StringComparison.OrdinalIgnoreCase) == 0) { - return $"{{ {_text}: {_destination}, $+: {_defaultDestination}, $0: {_exitDestination} }}"; + return _destination; } + + return _defaultDestination; + } + + public override string DebuggerToString() + { + return $"{{ {_text}: {_destination}, $+: {_defaultDestination}, $0: {_exitDestination} }}"; } } diff --git a/src/Http/Routing/src/Matching/ZeroEntryJumpTable.cs b/src/Http/Routing/src/Matching/ZeroEntryJumpTable.cs index 70c4d90d7e..ba4243008c 100644 --- a/src/Http/Routing/src/Matching/ZeroEntryJumpTable.cs +++ b/src/Http/Routing/src/Matching/ZeroEntryJumpTable.cs @@ -1,27 +1,26 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal class ZeroEntryJumpTable : JumpTable { - internal class ZeroEntryJumpTable : JumpTable - { - private readonly int _defaultDestination; - private readonly int _exitDestination; + private readonly int _defaultDestination; + private readonly int _exitDestination; - public ZeroEntryJumpTable(int defaultDestination, int exitDestination) - { - _defaultDestination = defaultDestination; - _exitDestination = exitDestination; - } + public ZeroEntryJumpTable(int defaultDestination, int exitDestination) + { + _defaultDestination = defaultDestination; + _exitDestination = exitDestination; + } - public override int GetDestination(string path, PathSegment segment) - { - return segment.Length == 0 ? _exitDestination : _defaultDestination; - } + public override int GetDestination(string path, PathSegment segment) + { + return segment.Length == 0 ? _exitDestination : _defaultDestination; + } - public override string DebuggerToString() - { - return $"{{ $+: {_defaultDestination}, $0: {_exitDestination} }}"; - } + public override string DebuggerToString() + { + return $"{{ $+: {_defaultDestination}, $0: {_exitDestination} }}"; } } diff --git a/src/Http/Routing/src/ModelEndpointDataSource.cs b/src/Http/Routing/src/ModelEndpointDataSource.cs index e97f41ce90..0f9e7703b9 100644 --- a/src/Http/Routing/src/ModelEndpointDataSource.cs +++ b/src/Http/Routing/src/ModelEndpointDataSource.cs @@ -8,33 +8,32 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +internal class ModelEndpointDataSource : EndpointDataSource { - internal class ModelEndpointDataSource : EndpointDataSource - { - private readonly List _endpointConventionBuilders; + private readonly List _endpointConventionBuilders; - public ModelEndpointDataSource() - { - _endpointConventionBuilders = new List(); - } + public ModelEndpointDataSource() + { + _endpointConventionBuilders = new List(); + } - public IEndpointConventionBuilder AddEndpointBuilder(EndpointBuilder endpointBuilder) - { - var builder = new DefaultEndpointConventionBuilder(endpointBuilder); - _endpointConventionBuilders.Add(builder); + public IEndpointConventionBuilder AddEndpointBuilder(EndpointBuilder endpointBuilder) + { + var builder = new DefaultEndpointConventionBuilder(endpointBuilder); + _endpointConventionBuilders.Add(builder); - return builder; - } + return builder; + } - public override IChangeToken GetChangeToken() - { - return NullChangeToken.Singleton; - } + public override IChangeToken GetChangeToken() + { + return NullChangeToken.Singleton; + } - public override IReadOnlyList Endpoints => _endpointConventionBuilders.Select(e => e.Build()).ToArray(); + public override IReadOnlyList Endpoints => _endpointConventionBuilders.Select(e => e.Build()).ToArray(); - // for testing - internal IEnumerable EndpointBuilders => _endpointConventionBuilders.Select(b => b.EndpointBuilder); - } + // for testing + internal IEnumerable EndpointBuilders => _endpointConventionBuilders.Select(b => b.EndpointBuilder); } diff --git a/src/Http/Routing/src/NullRouter.cs b/src/Http/Routing/src/NullRouter.cs index 5a653a4803..e55e9a3687 100644 --- a/src/Http/Routing/src/NullRouter.cs +++ b/src/Http/Routing/src/NullRouter.cs @@ -3,24 +3,23 @@ using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +internal class NullRouter : IRouter { - internal class NullRouter : IRouter - { - public static readonly NullRouter Instance = new NullRouter(); + public static readonly NullRouter Instance = new NullRouter(); - private NullRouter() - { - } + private NullRouter() + { + } - public VirtualPathData? GetVirtualPath(VirtualPathContext context) - { - return null; - } + public VirtualPathData? GetVirtualPath(VirtualPathContext context) + { + return null; + } - public Task RouteAsync(RouteContext context) - { - return Task.CompletedTask; - } + public Task RouteAsync(RouteContext context) + { + return Task.CompletedTask; } } diff --git a/src/Http/Routing/src/ParameterPolicyActivator.cs b/src/Http/Routing/src/ParameterPolicyActivator.cs index 7db87a8d10..b3eec68457 100644 --- a/src/Http/Routing/src/ParameterPolicyActivator.cs +++ b/src/Http/Routing/src/ParameterPolicyActivator.cs @@ -11,178 +11,177 @@ using System.Linq; using System.Reflection; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +internal static class ParameterPolicyActivator { - internal static class ParameterPolicyActivator + public static T ResolveParameterPolicy( + IDictionary inlineParameterPolicyMap, + IServiceProvider serviceProvider, + string inlineParameterPolicy, + out string parameterPolicyKey) + where T : IParameterPolicy { - public static T ResolveParameterPolicy( - IDictionary inlineParameterPolicyMap, - IServiceProvider serviceProvider, - string inlineParameterPolicy, - out string parameterPolicyKey) - where T : IParameterPolicy - { - // IServiceProvider could be null - // DefaultInlineConstraintResolver can be created without an IServiceProvider and then call this method - - if (inlineParameterPolicyMap == null) - { - throw new ArgumentNullException(nameof(inlineParameterPolicyMap)); - } + // IServiceProvider could be null + // DefaultInlineConstraintResolver can be created without an IServiceProvider and then call this method - if (inlineParameterPolicy == null) - { - throw new ArgumentNullException(nameof(inlineParameterPolicy)); - } - - string argumentString; - var indexOfFirstOpenParens = inlineParameterPolicy.IndexOf('('); - if (indexOfFirstOpenParens >= 0 && inlineParameterPolicy.EndsWith(")", StringComparison.Ordinal)) - { - parameterPolicyKey = inlineParameterPolicy.Substring(0, indexOfFirstOpenParens); - argumentString = inlineParameterPolicy.Substring( - indexOfFirstOpenParens + 1, - inlineParameterPolicy.Length - indexOfFirstOpenParens - 2); - } - else - { - parameterPolicyKey = inlineParameterPolicy; - argumentString = null; - } + if (inlineParameterPolicyMap == null) + { + throw new ArgumentNullException(nameof(inlineParameterPolicyMap)); + } - if (!inlineParameterPolicyMap.TryGetValue(parameterPolicyKey, out var parameterPolicyType)) - { - return default; - } + if (inlineParameterPolicy == null) + { + throw new ArgumentNullException(nameof(inlineParameterPolicy)); + } - if (!typeof(T).IsAssignableFrom(parameterPolicyType)) - { - if (!typeof(IParameterPolicy).IsAssignableFrom(parameterPolicyType)) - { - // Error if type is not a parameter policy - throw new RouteCreationException( - Resources.FormatDefaultInlineConstraintResolver_TypeNotConstraint( - parameterPolicyType, parameterPolicyKey, typeof(T).Name)); - } + string argumentString; + var indexOfFirstOpenParens = inlineParameterPolicy.IndexOf('('); + if (indexOfFirstOpenParens >= 0 && inlineParameterPolicy.EndsWith(")", StringComparison.Ordinal)) + { + parameterPolicyKey = inlineParameterPolicy.Substring(0, indexOfFirstOpenParens); + argumentString = inlineParameterPolicy.Substring( + indexOfFirstOpenParens + 1, + inlineParameterPolicy.Length - indexOfFirstOpenParens - 2); + } + else + { + parameterPolicyKey = inlineParameterPolicy; + argumentString = null; + } - // Return null if type is parameter policy but is not the exact type - // This is used by IInlineConstraintResolver for backwards compatibility - // e.g. looking for an IRouteConstraint but get a different IParameterPolicy type - return default; - } + if (!inlineParameterPolicyMap.TryGetValue(parameterPolicyKey, out var parameterPolicyType)) + { + return default; + } - try - { - return (T)CreateParameterPolicy(serviceProvider, parameterPolicyType, argumentString); - } - catch (RouteCreationException) - { - throw; - } - catch (Exception exception) + if (!typeof(T).IsAssignableFrom(parameterPolicyType)) + { + if (!typeof(IParameterPolicy).IsAssignableFrom(parameterPolicyType)) { + // Error if type is not a parameter policy throw new RouteCreationException( - $"An error occurred while trying to create an instance of '{parameterPolicyType.FullName}'.", - exception); + Resources.FormatDefaultInlineConstraintResolver_TypeNotConstraint( + parameterPolicyType, parameterPolicyKey, typeof(T).Name)); } + + // Return null if type is parameter policy but is not the exact type + // This is used by IInlineConstraintResolver for backwards compatibility + // e.g. looking for an IRouteConstraint but get a different IParameterPolicy type + return default; } - [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2006:UnrecognizedReflectionPattern", Justification = "This type comes from the ConstraintMap.")] - private static IParameterPolicy CreateParameterPolicy(IServiceProvider serviceProvider, Type parameterPolicyType, string argumentString) + try + { + return (T)CreateParameterPolicy(serviceProvider, parameterPolicyType, argumentString); + } + catch (RouteCreationException) { - ConstructorInfo activationConstructor = null; - object[] parameters = null; - var constructors = parameterPolicyType.GetConstructors(); + throw; + } + catch (Exception exception) + { + throw new RouteCreationException( + $"An error occurred while trying to create an instance of '{parameterPolicyType.FullName}'.", + exception); + } + } + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2006:UnrecognizedReflectionPattern", Justification = "This type comes from the ConstraintMap.")] + private static IParameterPolicy CreateParameterPolicy(IServiceProvider serviceProvider, Type parameterPolicyType, string argumentString) + { + ConstructorInfo activationConstructor = null; + object[] parameters = null; + var constructors = parameterPolicyType.GetConstructors(); - // If there is only one constructor and it has a single parameter, pass the argument string directly - // This is necessary for the Regex RouteConstraint to ensure that patterns are not split on commas. - if (constructors.Length == 1 && GetNonConvertableParameterTypeCount(serviceProvider, constructors[0].GetParameters()) == 1) + // If there is only one constructor and it has a single parameter, pass the argument string directly + // This is necessary for the Regex RouteConstraint to ensure that patterns are not split on commas. + if (constructors.Length == 1 && GetNonConvertableParameterTypeCount(serviceProvider, constructors[0].GetParameters()) == 1) + { + activationConstructor = constructors[0]; + parameters = ConvertArguments(serviceProvider, activationConstructor.GetParameters(), new string[] { argumentString }); + } + else + { + var arguments = argumentString?.Split(',', StringSplitOptions.TrimEntries) ?? Array.Empty(); + + // We want to find the constructors that match the number of passed in arguments + // We either want a single match, or a single best match. The best match is the one with the most + // arguments that can be resolved from DI + // + // For example, ctor(string, IService) will beat ctor(string) + var matchingConstructors = constructors + .Where(ci => GetNonConvertableParameterTypeCount(serviceProvider, ci.GetParameters()) == arguments.Length) + .OrderByDescending(ci => ci.GetParameters().Length) + .ToArray(); + + if (matchingConstructors.Length == 0) { - activationConstructor = constructors[0]; - parameters = ConvertArguments(serviceProvider, activationConstructor.GetParameters(), new string[] { argumentString }); + throw new RouteCreationException( + Resources.FormatDefaultInlineConstraintResolver_CouldNotFindCtor( + parameterPolicyType.Name, arguments.Length)); } else { - var arguments = argumentString?.Split(',', StringSplitOptions.TrimEntries) ?? Array.Empty(); - - // We want to find the constructors that match the number of passed in arguments - // We either want a single match, or a single best match. The best match is the one with the most - // arguments that can be resolved from DI - // - // For example, ctor(string, IService) will beat ctor(string) - var matchingConstructors = constructors - .Where(ci => GetNonConvertableParameterTypeCount(serviceProvider, ci.GetParameters()) == arguments.Length) - .OrderByDescending(ci => ci.GetParameters().Length) - .ToArray(); - - if (matchingConstructors.Length == 0) + // When there are multiple matching constructors, choose the one with the most service arguments + if (matchingConstructors.Length == 1 + || matchingConstructors[0].GetParameters().Length > matchingConstructors[1].GetParameters().Length) { - throw new RouteCreationException( - Resources.FormatDefaultInlineConstraintResolver_CouldNotFindCtor( - parameterPolicyType.Name, arguments.Length)); + activationConstructor = matchingConstructors[0]; } else { - // When there are multiple matching constructors, choose the one with the most service arguments - if (matchingConstructors.Length == 1 - || matchingConstructors[0].GetParameters().Length > matchingConstructors[1].GetParameters().Length) - { - activationConstructor = matchingConstructors[0]; - } - else - { - throw new RouteCreationException( - Resources.FormatDefaultInlineConstraintResolver_AmbiguousCtors( - parameterPolicyType.Name, matchingConstructors[0].GetParameters().Length)); - } - - parameters = ConvertArguments(serviceProvider, activationConstructor.GetParameters(), arguments); + throw new RouteCreationException( + Resources.FormatDefaultInlineConstraintResolver_AmbiguousCtors( + parameterPolicyType.Name, matchingConstructors[0].GetParameters().Length)); } - } - return (IParameterPolicy)activationConstructor.Invoke(parameters); + parameters = ConvertArguments(serviceProvider, activationConstructor.GetParameters(), arguments); + } } - private static int GetNonConvertableParameterTypeCount(IServiceProvider serviceProvider, ParameterInfo[] parameters) + return (IParameterPolicy)activationConstructor.Invoke(parameters); + } + + private static int GetNonConvertableParameterTypeCount(IServiceProvider serviceProvider, ParameterInfo[] parameters) + { + if (serviceProvider == null) { - if (serviceProvider == null) - { - return parameters.Length; - } + return parameters.Length; + } - var count = 0; - for (var i = 0; i < parameters.Length; i++) + var count = 0; + for (var i = 0; i < parameters.Length; i++) + { + if (typeof(IConvertible).IsAssignableFrom(parameters[i].ParameterType)) { - if (typeof(IConvertible).IsAssignableFrom(parameters[i].ParameterType)) - { - count++; - } + count++; } - - return count; } - private static object[] ConvertArguments(IServiceProvider serviceProvider, ParameterInfo[] parameterInfos, string[] arguments) + return count; + } + + private static object[] ConvertArguments(IServiceProvider serviceProvider, ParameterInfo[] parameterInfos, string[] arguments) + { + var parameters = new object[parameterInfos.Length]; + var argumentPosition = 0; + for (var i = 0; i < parameterInfos.Length; i++) { - var parameters = new object[parameterInfos.Length]; - var argumentPosition = 0; - for (var i = 0; i < parameterInfos.Length; i++) - { - var parameter = parameterInfos[i]; - var parameterType = parameter.ParameterType; + var parameter = parameterInfos[i]; + var parameterType = parameter.ParameterType; - if (serviceProvider != null && !typeof(IConvertible).IsAssignableFrom(parameterType)) - { - parameters[i] = serviceProvider.GetRequiredService(parameterType); - } - else - { - parameters[i] = Convert.ChangeType(arguments[argumentPosition], parameterType, CultureInfo.InvariantCulture); - argumentPosition++; - } + if (serviceProvider != null && !typeof(IConvertible).IsAssignableFrom(parameterType)) + { + parameters[i] = serviceProvider.GetRequiredService(parameterType); + } + else + { + parameters[i] = Convert.ChangeType(arguments[argumentPosition], parameterType, CultureInfo.InvariantCulture); + argumentPosition++; } - - return parameters; } + + return parameters; } } diff --git a/src/Http/Routing/src/ParameterPolicyFactory.cs b/src/Http/Routing/src/ParameterPolicyFactory.cs index 79d044b269..330e8da156 100644 --- a/src/Http/Routing/src/ParameterPolicyFactory.cs +++ b/src/Http/Routing/src/ParameterPolicyFactory.cs @@ -5,56 +5,55 @@ using System; using System.Diagnostics; using Microsoft.AspNetCore.Routing.Patterns; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Defines an abstraction for resolving inline parameter policies as instances of . +/// +public abstract class ParameterPolicyFactory { /// - /// Defines an abstraction for resolving inline parameter policies as instances of . + /// Creates a parameter policy. + /// + /// The parameter the parameter policy is being created for. + /// The inline text to resolve. + /// The for the parameter. + public abstract IParameterPolicy Create(RoutePatternParameterPart? parameter, string inlineText); + + /// + /// Creates a parameter policy. + /// + /// The parameter the parameter policy is being created for. + /// An existing parameter policy. + /// The for the parameter. + public abstract IParameterPolicy Create(RoutePatternParameterPart? parameter, IParameterPolicy parameterPolicy); + + /// + /// Creates a parameter policy. /// - public abstract class ParameterPolicyFactory + /// The parameter the parameter policy is being created for. + /// The reference to resolve. + /// The for the parameter. + public IParameterPolicy Create(RoutePatternParameterPart? parameter, RoutePatternParameterPolicyReference reference) { - /// - /// Creates a parameter policy. - /// - /// The parameter the parameter policy is being created for. - /// The inline text to resolve. - /// The for the parameter. - public abstract IParameterPolicy Create(RoutePatternParameterPart? parameter, string inlineText); - - /// - /// Creates a parameter policy. - /// - /// The parameter the parameter policy is being created for. - /// An existing parameter policy. - /// The for the parameter. - public abstract IParameterPolicy Create(RoutePatternParameterPart? parameter, IParameterPolicy parameterPolicy); - - /// - /// Creates a parameter policy. - /// - /// The parameter the parameter policy is being created for. - /// The reference to resolve. - /// The for the parameter. - public IParameterPolicy Create(RoutePatternParameterPart? parameter, RoutePatternParameterPolicyReference reference) + if (reference == null) { - if (reference == null) - { - throw new ArgumentNullException(nameof(reference)); - } - - Debug.Assert(reference.ParameterPolicy != null || reference.Content != null); + throw new ArgumentNullException(nameof(reference)); + } - if (reference.ParameterPolicy != null) - { - return Create(parameter, reference.ParameterPolicy); - } + Debug.Assert(reference.ParameterPolicy != null || reference.Content != null); - if (reference.Content != null) - { - return Create(parameter, reference.Content); - } + if (reference.ParameterPolicy != null) + { + return Create(parameter, reference.ParameterPolicy); + } - // Unreachable - throw new NotSupportedException(); + if (reference.Content != null) + { + return Create(parameter, reference.Content); } + + // Unreachable + throw new NotSupportedException(); } } diff --git a/src/Http/Routing/src/PathTokenizer.cs b/src/Http/Routing/src/PathTokenizer.cs index 1bb76dbf72..c6f04b463d 100644 --- a/src/Http/Routing/src/PathTokenizer.cs +++ b/src/Http/Routing/src/PathTokenizer.cs @@ -10,198 +10,197 @@ using System.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +internal struct PathTokenizer : IReadOnlyList { - internal struct PathTokenizer : IReadOnlyList - { - private readonly string _path; - private int _count; + private readonly string _path; + private int _count; - public PathTokenizer(PathString path) - { - _path = path.Value; - _count = -1; - } + public PathTokenizer(PathString path) + { + _path = path.Value; + _count = -1; + } - public int Count + public int Count + { + get { - get + if (_count == -1) { - if (_count == -1) + // We haven't computed the real count of segments yet. + if (_path.Length == 0) { - // We haven't computed the real count of segments yet. - if (_path.Length == 0) - { - // The empty string has length of 0. - _count = 0; - return _count; - } + // The empty string has length of 0. + _count = 0; + return _count; + } - // A string of length 1 must be "/" - all PathStrings start with '/' - if (_path.Length == 1) - { - // We treat this as empty - there's nothing to parse here for routing, because routing ignores - // a trailing slash. - Debug.Assert(_path[0] == '/'); - _count = 0; - return _count; - } + // A string of length 1 must be "/" - all PathStrings start with '/' + if (_path.Length == 1) + { + // We treat this as empty - there's nothing to parse here for routing, because routing ignores + // a trailing slash. + Debug.Assert(_path[0] == '/'); + _count = 0; + return _count; + } - // This is a non-trivial PathString - _count = 1; + // This is a non-trivial PathString + _count = 1; - // Since a non-empty PathString must begin with a `/`, we can just count the number of occurrences - // of `/` to find the number of segments. However, we don't look at the last character, because - // routing ignores a trailing slash. - for (var i = 1; i < _path.Length - 1; i++) + // Since a non-empty PathString must begin with a `/`, we can just count the number of occurrences + // of `/` to find the number of segments. However, we don't look at the last character, because + // routing ignores a trailing slash. + for (var i = 1; i < _path.Length - 1; i++) + { + if (_path[i] == '/') { - if (_path[i] == '/') - { - _count++; - } + _count++; } } - - return _count; } + + return _count; } + } - public StringSegment this[int index] + public StringSegment this[int index] + { + get { - get + if (index >= Count) { - if (index >= Count) - { - throw new IndexOutOfRangeException(); - } + throw new IndexOutOfRangeException(); + } - var currentSegmentIndex = 0; - var currentSegmentStart = 1; + var currentSegmentIndex = 0; + var currentSegmentStart = 1; - // Skip the first `/`. - var delimiterIndex = 1; - while ((delimiterIndex = _path.IndexOf('/', delimiterIndex)) != -1) + // Skip the first `/`. + var delimiterIndex = 1; + while ((delimiterIndex = _path.IndexOf('/', delimiterIndex)) != -1) + { + if (currentSegmentIndex++ == index) { - if (currentSegmentIndex++ == index) - { - return new StringSegment(_path, currentSegmentStart, delimiterIndex - currentSegmentStart); - } - else - { - currentSegmentStart = delimiterIndex + 1; - delimiterIndex++; - } + return new StringSegment(_path, currentSegmentStart, delimiterIndex - currentSegmentStart); + } + else + { + currentSegmentStart = delimiterIndex + 1; + delimiterIndex++; } + } - // If we get here we're at the end of the string. The implementation of .Count should protect us - // from these cases. - Debug.Assert(_path[_path.Length - 1] != '/'); - Debug.Assert(currentSegmentIndex == index); + // If we get here we're at the end of the string. The implementation of .Count should protect us + // from these cases. + Debug.Assert(_path[_path.Length - 1] != '/'); + Debug.Assert(currentSegmentIndex == index); - return new StringSegment(_path, currentSegmentStart, _path.Length - currentSegmentStart); - } + return new StringSegment(_path, currentSegmentStart, _path.Length - currentSegmentStart); } + } + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } - public Enumerator GetEnumerator() + public struct Enumerator : IEnumerator + { + private readonly string _path; + + private int _index; + private int _length; + + public Enumerator(PathTokenizer tokenizer) { - return new Enumerator(this); + _path = tokenizer._path; + + _index = -1; + _length = -1; } - IEnumerator IEnumerable.GetEnumerator() + public StringSegment Current { - return GetEnumerator(); + get + { + return new StringSegment(_path, _index, _length); + } } - IEnumerator IEnumerable.GetEnumerator() + object IEnumerator.Current { - return GetEnumerator(); + get + { + return Current; + } } - public struct Enumerator : IEnumerator + public void Dispose() { - private readonly string _path; - - private int _index; - private int _length; + } - public Enumerator(PathTokenizer tokenizer) + public bool MoveNext() + { + if (_path == null || _path.Length <= 1) { - _path = tokenizer._path; - - _index = -1; - _length = -1; + return false; } - public StringSegment Current + if (_index == -1) { - get - { - return new StringSegment(_path, _index, _length); - } + // Skip the first `/`. + _index = 1; } - - object IEnumerator.Current + else { - get - { - return Current; - } + // Skip to the end of the previous segment + the separator. + _index += _length + 1; } - public void Dispose() + if (_index >= _path.Length) { + // We're at the end + return false; } - public bool MoveNext() + var delimiterIndex = _path.IndexOf('/', _index); + if (delimiterIndex != -1) { - if (_path == null || _path.Length <= 1) - { - return false; - } - - if (_index == -1) - { - // Skip the first `/`. - _index = 1; - } - else - { - // Skip to the end of the previous segment + the separator. - _index += _length + 1; - } - - if (_index >= _path.Length) - { - // We're at the end - return false; - } - - var delimiterIndex = _path.IndexOf('/', _index); - if (delimiterIndex != -1) - { - _length = delimiterIndex - _index; - return true; - } - - // We might have some trailing text after the last separator. - if (_path[_path.Length - 1] == '/') - { - // If the last char is a '/' then it's just a trailing slash, we don't have another segment. - return false; - } - else - { - _length = _path.Length - _index; - return true; - } + _length = delimiterIndex - _index; + return true; } - public void Reset() + // We might have some trailing text after the last separator. + if (_path[_path.Length - 1] == '/') { - _index = -1; - _length = -1; + // If the last char is a '/' then it's just a trailing slash, we don't have another segment. + return false; } + else + { + _length = _path.Length - _index; + return true; + } + } + + public void Reset() + { + _index = -1; + _length = -1; } } } diff --git a/src/Http/Routing/src/Patterns/DefaultRoutePatternTransformer.cs b/src/Http/Routing/src/Patterns/DefaultRoutePatternTransformer.cs index 7c7b07e825..14a960c856 100644 --- a/src/Http/Routing/src/Patterns/DefaultRoutePatternTransformer.cs +++ b/src/Http/Routing/src/Patterns/DefaultRoutePatternTransformer.cs @@ -6,245 +6,244 @@ using System; using System.Collections.Generic; -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +internal class DefaultRoutePatternTransformer : RoutePatternTransformer { - internal class DefaultRoutePatternTransformer : RoutePatternTransformer - { - private readonly ParameterPolicyFactory _policyFactory; + private readonly ParameterPolicyFactory _policyFactory; - public DefaultRoutePatternTransformer(ParameterPolicyFactory policyFactory) + public DefaultRoutePatternTransformer(ParameterPolicyFactory policyFactory) + { + if (policyFactory == null) { - if (policyFactory == null) - { - throw new ArgumentNullException(nameof(policyFactory)); - } - - _policyFactory = policyFactory; + throw new ArgumentNullException(nameof(policyFactory)); } - public override RoutePattern SubstituteRequiredValues(RoutePattern original, object requiredValues) - { - if (original == null) - { - throw new ArgumentNullException(nameof(original)); - } + _policyFactory = policyFactory; + } - return SubstituteRequiredValuesCore(original, new RouteValueDictionary(requiredValues)); + public override RoutePattern SubstituteRequiredValues(RoutePattern original, object requiredValues) + { + if (original == null) + { + throw new ArgumentNullException(nameof(original)); } - private RoutePattern SubstituteRequiredValuesCore(RoutePattern original, RouteValueDictionary requiredValues) + return SubstituteRequiredValuesCore(original, new RouteValueDictionary(requiredValues)); + } + + private RoutePattern SubstituteRequiredValuesCore(RoutePattern original, RouteValueDictionary requiredValues) + { + // Process each required value in sequence. Bail if we find any rejection criteria. The goal + // of rejection is to avoid creating RoutePattern instances that can't *ever* match. + // + // If we succeed, then we need to create a new RoutePattern with the provided required values. + // + // Substitution can merge with existing RequiredValues already on the RoutePattern as long + // as all of the success criteria are still met at the end. + foreach (var kvp in requiredValues) { - // Process each required value in sequence. Bail if we find any rejection criteria. The goal - // of rejection is to avoid creating RoutePattern instances that can't *ever* match. - // - // If we succeed, then we need to create a new RoutePattern with the provided required values. + // There are three possible cases here: + // 1. Required value is null-ish + // 2. Required value is *any* + // 3. Required value corresponds to a parameter + // 4. Required value corresponds to a matching default value // - // Substitution can merge with existing RequiredValues already on the RoutePattern as long - // as all of the success criteria are still met at the end. - foreach (var kvp in requiredValues) + // If none of these are true then we can reject this substitution. + RoutePatternParameterPart parameter; + if (RouteValueEqualityComparer.Default.Equals(kvp.Value, string.Empty)) { - // There are three possible cases here: - // 1. Required value is null-ish - // 2. Required value is *any* - // 3. Required value corresponds to a parameter - // 4. Required value corresponds to a matching default value - // - // If none of these are true then we can reject this substitution. - RoutePatternParameterPart parameter; - if (RouteValueEqualityComparer.Default.Equals(kvp.Value, string.Empty)) - { - // 1. Required value is null-ish - check to make sure that this route doesn't have a - // parameter or filter-like default. + // 1. Required value is null-ish - check to make sure that this route doesn't have a + // parameter or filter-like default. - if (original.GetParameter(kvp.Key) != null) - { - // Fail: we can't 'require' that a parameter be null. In theory this would be possible - // for an optional parameter, but that's not really in line with the usage of this feature - // so we don't handle it. - // - // Ex: {controller=Home}/{action=Index}/{id?} - with required values: { controller = "" } - return null; - } - else if (original.Defaults.TryGetValue(kvp.Key, out var defaultValue) && - !RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue)) - { - // Fail: this route has a non-parameter default that doesn't match. - // - // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" } - with required values: { area = "" } - return null; - } - - // Success: (for this parameter at least) + if (original.GetParameter(kvp.Key) != null) + { + // Fail: we can't 'require' that a parameter be null. In theory this would be possible + // for an optional parameter, but that's not really in line with the usage of this feature + // so we don't handle it. // - // Ex: {controller=Home}/{action=Index}/{id?} - with required values: { area = "", ... } - continue; + // Ex: {controller=Home}/{action=Index}/{id?} - with required values: { controller = "" } + return null; } - else if (RoutePattern.IsRequiredValueAny(kvp.Value)) + else if (original.Defaults.TryGetValue(kvp.Key, out var defaultValue) && + !RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue)) { - // 2. Required value is *any* - this is allowed for a parameter with a default, but not - // a non-parameter default. - if (original.GetParameter(kvp.Key) == null && - original.Defaults.TryGetValue(kvp.Key, out var defaultValue) && - !RouteValueEqualityComparer.Default.Equals(string.Empty, defaultValue)) - { - // Fail: this route as a non-parameter default that is stricter than *any*. - // - // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" } - with required values: { area = *any* } - return null; - } - - // Success: (for this parameter at least) + // Fail: this route has a non-parameter default that doesn't match. // - // Ex: {controller=Home}/{action=Index}/{id?} - with required values: { controller = *any*, ... } - continue; + // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" } - with required values: { area = "" } + return null; } - else if ((parameter = original.GetParameter(kvp.Key)) != null) - { - // 3. Required value corresponds to a parameter - check to make sure that this value matches - // any IRouteConstraint implementations. - if (!MatchesConstraints(original, parameter, kvp.Key, requiredValues)) - { - // Fail: this route has a constraint that failed. - // - // Ex: Admin/{controller:regex(Home|Login)}/{action=Index}/{id?} - with required values: { controller = "Store" } - return null; - } - // Success: (for this parameter at least) + // Success: (for this parameter at least) + // + // Ex: {controller=Home}/{action=Index}/{id?} - with required values: { area = "", ... } + continue; + } + else if (RoutePattern.IsRequiredValueAny(kvp.Value)) + { + // 2. Required value is *any* - this is allowed for a parameter with a default, but not + // a non-parameter default. + if (original.GetParameter(kvp.Key) == null && + original.Defaults.TryGetValue(kvp.Key, out var defaultValue) && + !RouteValueEqualityComparer.Default.Equals(string.Empty, defaultValue)) + { + // Fail: this route as a non-parameter default that is stricter than *any*. // - // Ex: {area}/{controller=Home}/{action=Index}/{id?} - with required values: { area = "", ... } - continue; + // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" } - with required values: { area = *any* } + return null; } - else if (original.Defaults.TryGetValue(kvp.Key, out var defaultValue) && - RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue)) - { - // 4. Required value corresponds to a matching default value - check to make sure that this value matches - // any IRouteConstraint implementations. It's unlikely that this would happen in practice but it doesn't - // hurt for us to check. - if (!MatchesConstraints(original, parameter: null, kvp.Key, requiredValues)) - { - // Fail: this route has a constraint that failed. - // - // Ex: - // Admin/Home/{action=Index}/{id?} - // defaults: { area = "Admin" } - // constraints: { area = "Blog" } - // with required values: { area = "Admin" } - return null; - } - // Success: (for this parameter at least) + // Success: (for this parameter at least) + // + // Ex: {controller=Home}/{action=Index}/{id?} - with required values: { controller = *any*, ... } + continue; + } + else if ((parameter = original.GetParameter(kvp.Key)) != null) + { + // 3. Required value corresponds to a parameter - check to make sure that this value matches + // any IRouteConstraint implementations. + if (!MatchesConstraints(original, parameter, kvp.Key, requiredValues)) + { + // Fail: this route has a constraint that failed. // - // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" }- with required values: { area = "Admin", ... } - continue; + // Ex: Admin/{controller:regex(Home|Login)}/{action=Index}/{id?} - with required values: { controller = "Store" } + return null; } - else + + // Success: (for this parameter at least) + // + // Ex: {area}/{controller=Home}/{action=Index}/{id?} - with required values: { area = "", ... } + continue; + } + else if (original.Defaults.TryGetValue(kvp.Key, out var defaultValue) && + RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue)) + { + // 4. Required value corresponds to a matching default value - check to make sure that this value matches + // any IRouteConstraint implementations. It's unlikely that this would happen in practice but it doesn't + // hurt for us to check. + if (!MatchesConstraints(original, parameter: null, kvp.Key, requiredValues)) { - // Fail: this is a required value for a key that doesn't appear in the templates, or the route - // pattern has a different default value for a non-parameter. + // Fail: this route has a constraint that failed. // - // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" }- with required values: { area = "Blog", ... } - // OR (less likely) - // Ex: Admin/{controller=Home}/{action=Index}/{id?} with required values: { page = "/Index", ... } + // Ex: + // Admin/Home/{action=Index}/{id?} + // defaults: { area = "Admin" } + // constraints: { area = "Blog" } + // with required values: { area = "Admin" } return null; } + + // Success: (for this parameter at least) + // + // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" }- with required values: { area = "Admin", ... } + continue; + } + else + { + // Fail: this is a required value for a key that doesn't appear in the templates, or the route + // pattern has a different default value for a non-parameter. + // + // Ex: Admin/{controller=Home}/{action=Index}/{id?} defaults: { area = "Admin" }- with required values: { area = "Blog", ... } + // OR (less likely) + // Ex: Admin/{controller=Home}/{action=Index}/{id?} with required values: { page = "/Index", ... } + return null; } + } - List updatedParameters = null; - List updatedSegments = null; - RouteValueDictionary updatedDefaults = null; + List updatedParameters = null; + List updatedSegments = null; + RouteValueDictionary updatedDefaults = null; - // So if we get here, we're ready to update the route pattern. We need to update two things: - // 1. Remove any default values that conflict with the required values. - // 2. Merge any existing required values - foreach (var kvp in requiredValues) - { - var parameter = original.GetParameter(kvp.Key); + // So if we get here, we're ready to update the route pattern. We need to update two things: + // 1. Remove any default values that conflict with the required values. + // 2. Merge any existing required values + foreach (var kvp in requiredValues) + { + var parameter = original.GetParameter(kvp.Key); - // We only need to handle the case where the required value maps to a parameter. That's the only - // case where we allow a default and a required value to disagree, and we already validated the - // other cases. - // - // If the required value is *any* then don't remove the default. - if (parameter != null && - !RoutePattern.IsRequiredValueAny(kvp.Value) && - original.Defaults.TryGetValue(kvp.Key, out var defaultValue) && - !RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue)) + // We only need to handle the case where the required value maps to a parameter. That's the only + // case where we allow a default and a required value to disagree, and we already validated the + // other cases. + // + // If the required value is *any* then don't remove the default. + if (parameter != null && + !RoutePattern.IsRequiredValueAny(kvp.Value) && + original.Defaults.TryGetValue(kvp.Key, out var defaultValue) && + !RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue)) + { + if (updatedDefaults == null && updatedSegments == null && updatedParameters == null) { - if (updatedDefaults == null && updatedSegments == null && updatedParameters == null) - { - updatedDefaults = new RouteValueDictionary(original.Defaults); - updatedSegments = new List(original.PathSegments); - updatedParameters = new List(original.Parameters); - } - - updatedDefaults.Remove(kvp.Key); - RemoveParameterDefault(updatedSegments, updatedParameters, parameter); + updatedDefaults = new RouteValueDictionary(original.Defaults); + updatedSegments = new List(original.PathSegments); + updatedParameters = new List(original.Parameters); } - } - foreach (var kvp in original.RequiredValues) - { - requiredValues.TryAdd(kvp.Key, kvp.Value); + updatedDefaults.Remove(kvp.Key); + RemoveParameterDefault(updatedSegments, updatedParameters, parameter); } + } - return new RoutePattern( - original.RawText, - updatedDefaults ?? original.Defaults, - original.ParameterPolicies, - requiredValues, - updatedParameters ?? original.Parameters, - updatedSegments ?? original.PathSegments); + foreach (var kvp in original.RequiredValues) + { + requiredValues.TryAdd(kvp.Key, kvp.Value); } - private bool MatchesConstraints(RoutePattern pattern, RoutePatternParameterPart parameter, string key, RouteValueDictionary requiredValues) + return new RoutePattern( + original.RawText, + updatedDefaults ?? original.Defaults, + original.ParameterPolicies, + requiredValues, + updatedParameters ?? original.Parameters, + updatedSegments ?? original.PathSegments); + } + + private bool MatchesConstraints(RoutePattern pattern, RoutePatternParameterPart parameter, string key, RouteValueDictionary requiredValues) + { + if (pattern.ParameterPolicies.TryGetValue(key, out var policies)) { - if (pattern.ParameterPolicies.TryGetValue(key, out var policies)) + for (var i = 0; i < policies.Count; i++) { - for (var i = 0; i < policies.Count; i++) + var policy = _policyFactory.Create(parameter, policies[i]); + if (policy is IRouteConstraint constraint) { - var policy = _policyFactory.Create(parameter, policies[i]); - if (policy is IRouteConstraint constraint) + if (!constraint.Match(httpContext: null, NullRouter.Instance, key, requiredValues, RouteDirection.IncomingRequest)) { - if (!constraint.Match(httpContext: null, NullRouter.Instance, key, requiredValues, RouteDirection.IncomingRequest)) - { - return false; - } + return false; } } } - - return true; } - private static void RemoveParameterDefault(List segments, List parameters, RoutePatternParameterPart parameter) + return true; + } + + private static void RemoveParameterDefault(List segments, List parameters, RoutePatternParameterPart parameter) + { + // We know that a parameter can only appear once, so we only need to rewrite one segment and one parameter. + for (var i = 0; i < segments.Count; i++) { - // We know that a parameter can only appear once, so we only need to rewrite one segment and one parameter. - for (var i = 0; i < segments.Count; i++) + var segment = segments[i]; + for (var j = 0; j < segment.Parts.Count; j++) { - var segment = segments[i]; - for (var j = 0; j < segment.Parts.Count; j++) + if (object.ReferenceEquals(parameter, segment.Parts[j])) { - if (object.ReferenceEquals(parameter, segment.Parts[j])) - { - // Found it! - var updatedParameter = RoutePatternFactory.ParameterPart(parameter.Name, @default: null, parameter.ParameterKind, parameter.ParameterPolicies); + // Found it! + var updatedParameter = RoutePatternFactory.ParameterPart(parameter.Name, @default: null, parameter.ParameterKind, parameter.ParameterPolicies); - var updatedParts = new List(segment.Parts); - updatedParts[j] = updatedParameter; - segments[i] = RoutePatternFactory.Segment(updatedParts); + var updatedParts = new List(segment.Parts); + updatedParts[j] = updatedParameter; + segments[i] = RoutePatternFactory.Segment(updatedParts); - for (var k = 0; k < parameters.Count; k++) + for (var k = 0; k < parameters.Count; k++) + { + if (ReferenceEquals(parameter, parameters[k])) { - if (ReferenceEquals(parameter, parameters[k])) - { - parameters[k] = updatedParameter; - break; - } + parameters[k] = updatedParameter; + break; } - - return; } + + return; } } } diff --git a/src/Http/Routing/src/Patterns/RouteParameterParser.cs b/src/Http/Routing/src/Patterns/RouteParameterParser.cs index 220eccc4d6..ec6d4d490f 100644 --- a/src/Http/Routing/src/Patterns/RouteParameterParser.cs +++ b/src/Http/Routing/src/Patterns/RouteParameterParser.cs @@ -3,256 +3,255 @@ using System; -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +internal static class RouteParameterParser { - internal static class RouteParameterParser + // This code parses the inside of the route parameter + // + // Ex: {hello} - this method is responsible for parsing 'hello' + // The factoring between this class and RoutePatternParser is due to legacy. + public static RoutePatternParameterPart ParseRouteParameter(string parameter) { - // This code parses the inside of the route parameter - // - // Ex: {hello} - this method is responsible for parsing 'hello' - // The factoring between this class and RoutePatternParser is due to legacy. - public static RoutePatternParameterPart ParseRouteParameter(string parameter) + if (parameter == null) { - if (parameter == null) - { - throw new ArgumentNullException(nameof(parameter)); - } - - if (parameter.Length == 0) - { - return new RoutePatternParameterPart(string.Empty, null, RoutePatternParameterKind.Standard, Array.Empty()); - } + throw new ArgumentNullException(nameof(parameter)); + } - var startIndex = 0; - var endIndex = parameter.Length - 1; - var encodeSlashes = true; + if (parameter.Length == 0) + { + return new RoutePatternParameterPart(string.Empty, null, RoutePatternParameterKind.Standard, Array.Empty()); + } - var parameterKind = RoutePatternParameterKind.Standard; + var startIndex = 0; + var endIndex = parameter.Length - 1; + var encodeSlashes = true; - if (parameter.StartsWith("**", StringComparison.Ordinal)) - { - encodeSlashes = false; - parameterKind = RoutePatternParameterKind.CatchAll; - startIndex += 2; - } - else if (parameter[0] == '*') - { - parameterKind = RoutePatternParameterKind.CatchAll; - startIndex++; - } + var parameterKind = RoutePatternParameterKind.Standard; - if (parameter[endIndex] == '?') - { - parameterKind = RoutePatternParameterKind.Optional; - endIndex--; - } + if (parameter.StartsWith("**", StringComparison.Ordinal)) + { + encodeSlashes = false; + parameterKind = RoutePatternParameterKind.CatchAll; + startIndex += 2; + } + else if (parameter[0] == '*') + { + parameterKind = RoutePatternParameterKind.CatchAll; + startIndex++; + } - var currentIndex = startIndex; + if (parameter[endIndex] == '?') + { + parameterKind = RoutePatternParameterKind.Optional; + endIndex--; + } - // Parse parameter name - var parameterName = string.Empty; + var currentIndex = startIndex; - while (currentIndex <= endIndex) - { - var currentChar = parameter[currentIndex]; + // Parse parameter name + var parameterName = string.Empty; - if ((currentChar == ':' || currentChar == '=') && startIndex != currentIndex) - { - // Parameter names are allowed to start with delimiters used to denote constraints or default values. - // i.e. "=foo" or ":bar" would be treated as parameter names rather than default value or constraint - // specifications. - parameterName = parameter.Substring(startIndex, currentIndex - startIndex); + while (currentIndex <= endIndex) + { + var currentChar = parameter[currentIndex]; - // Roll the index back and move to the constraint parsing stage. - currentIndex--; - break; - } - else if (currentIndex == endIndex) - { - parameterName = parameter.Substring(startIndex, currentIndex - startIndex + 1); - } + if ((currentChar == ':' || currentChar == '=') && startIndex != currentIndex) + { + // Parameter names are allowed to start with delimiters used to denote constraints or default values. + // i.e. "=foo" or ":bar" would be treated as parameter names rather than default value or constraint + // specifications. + parameterName = parameter.Substring(startIndex, currentIndex - startIndex); - currentIndex++; + // Roll the index back and move to the constraint parsing stage. + currentIndex--; + break; } - - var parseResults = ParseConstraints(parameter, currentIndex, endIndex); - currentIndex = parseResults.CurrentIndex; - - string? defaultValue = null; - if (currentIndex <= endIndex && - parameter[currentIndex] == '=') + else if (currentIndex == endIndex) { - defaultValue = parameter.Substring(currentIndex + 1, endIndex - currentIndex); + parameterName = parameter.Substring(startIndex, currentIndex - startIndex + 1); } - return new RoutePatternParameterPart( - parameterName, - defaultValue, - parameterKind, - parseResults.ParameterPolicies, - encodeSlashes); + currentIndex++; } - private static ParameterPolicyParseResults ParseConstraints( - string text, - int currentIndex, - int endIndex) + var parseResults = ParseConstraints(parameter, currentIndex, endIndex); + currentIndex = parseResults.CurrentIndex; + + string? defaultValue = null; + if (currentIndex <= endIndex && + parameter[currentIndex] == '=') { - var constraints = new ArrayBuilder(0); - var state = ParseState.Start; - var startIndex = currentIndex; - do - { - var currentChar = currentIndex > endIndex ? null : (char?)text[currentIndex]; - switch (state) - { - case ParseState.Start: - switch (currentChar) - { - case null: - state = ParseState.End; - break; - case ':': - state = ParseState.ParsingName; - startIndex = currentIndex + 1; - break; - case '(': - state = ParseState.InsideParenthesis; - break; - case '=': - state = ParseState.End; - currentIndex--; - break; - } - break; - case ParseState.InsideParenthesis: - switch (currentChar) - { - case null: - state = ParseState.End; - var constraintText = text.Substring(startIndex, currentIndex - startIndex); - constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); - break; - case ')': - // Only consume a ')' token if - // (a) it is the last token - // (b) the next character is the start of the new constraint ':' - // (c) the next character is the start of the default value. + defaultValue = parameter.Substring(currentIndex + 1, endIndex - currentIndex); + } - var nextChar = currentIndex + 1 > endIndex ? null : (char?)text[currentIndex + 1]; - switch (nextChar) - { - case null: - state = ParseState.End; - constraintText = text.Substring(startIndex, currentIndex - startIndex + 1); - constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); - break; - case ':': - state = ParseState.Start; - constraintText = text.Substring(startIndex, currentIndex - startIndex + 1); - constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); - startIndex = currentIndex + 1; - break; - case '=': - state = ParseState.End; - constraintText = text.Substring(startIndex, currentIndex - startIndex + 1); - constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); - break; - } - break; - case ':': - case '=': - // In the original implementation, the Regex would've backtracked if it encountered an - // unbalanced opening bracket followed by (not necessarily immediately) a delimiter. - // Simply verifying that the parentheses will eventually be closed should suffice to - // determine if the terminator needs to be consumed as part of the current constraint - // specification. - var indexOfClosingParantheses = text.IndexOf(')', currentIndex + 1); - if (indexOfClosingParantheses == -1) - { - constraintText = text.Substring(startIndex, currentIndex - startIndex); - constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); + return new RoutePatternParameterPart( + parameterName, + defaultValue, + parameterKind, + parseResults.ParameterPolicies, + encodeSlashes); + } - if (currentChar == ':') - { - state = ParseState.ParsingName; - startIndex = currentIndex + 1; - } - else - { - state = ParseState.End; - currentIndex--; - } - } - else - { - currentIndex = indexOfClosingParantheses; - } + private static ParameterPolicyParseResults ParseConstraints( + string text, + int currentIndex, + int endIndex) + { + var constraints = new ArrayBuilder(0); + var state = ParseState.Start; + var startIndex = currentIndex; + do + { + var currentChar = currentIndex > endIndex ? null : (char?)text[currentIndex]; + switch (state) + { + case ParseState.Start: + switch (currentChar) + { + case null: + state = ParseState.End; + break; + case ':': + state = ParseState.ParsingName; + startIndex = currentIndex + 1; + break; + case '(': + state = ParseState.InsideParenthesis; + break; + case '=': + state = ParseState.End; + currentIndex--; + break; + } + break; + case ParseState.InsideParenthesis: + switch (currentChar) + { + case null: + state = ParseState.End; + var constraintText = text.Substring(startIndex, currentIndex - startIndex); + constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); + break; + case ')': + // Only consume a ')' token if + // (a) it is the last token + // (b) the next character is the start of the new constraint ':' + // (c) the next character is the start of the default value. - break; - } - break; - case ParseState.ParsingName: - switch (currentChar) - { - case null: - state = ParseState.End; - var constraintText = text.Substring(startIndex, currentIndex - startIndex); - if (constraintText.Length > 0) - { + var nextChar = currentIndex + 1 > endIndex ? null : (char?)text[currentIndex + 1]; + switch (nextChar) + { + case null: + state = ParseState.End; + constraintText = text.Substring(startIndex, currentIndex - startIndex + 1); constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); - } - break; - case ':': + break; + case ':': + state = ParseState.Start; + constraintText = text.Substring(startIndex, currentIndex - startIndex + 1); + constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); + startIndex = currentIndex + 1; + break; + case '=': + state = ParseState.End; + constraintText = text.Substring(startIndex, currentIndex - startIndex + 1); + constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); + break; + } + break; + case ':': + case '=': + // In the original implementation, the Regex would've backtracked if it encountered an + // unbalanced opening bracket followed by (not necessarily immediately) a delimiter. + // Simply verifying that the parentheses will eventually be closed should suffice to + // determine if the terminator needs to be consumed as part of the current constraint + // specification. + var indexOfClosingParantheses = text.IndexOf(')', currentIndex + 1); + if (indexOfClosingParantheses == -1) + { constraintText = text.Substring(startIndex, currentIndex - startIndex); - if (constraintText.Length > 0) + constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); + + if (currentChar == ':') { - constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); + state = ParseState.ParsingName; + startIndex = currentIndex + 1; } - startIndex = currentIndex + 1; - break; - case '(': - state = ParseState.InsideParenthesis; - break; - case '=': - state = ParseState.End; - constraintText = text.Substring(startIndex, currentIndex - startIndex); - if (constraintText.Length > 0) + else { - constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); + state = ParseState.End; + currentIndex--; } - currentIndex--; - break; - } - break; - } + } + else + { + currentIndex = indexOfClosingParantheses; + } - currentIndex++; + break; + } + break; + case ParseState.ParsingName: + switch (currentChar) + { + case null: + state = ParseState.End; + var constraintText = text.Substring(startIndex, currentIndex - startIndex); + if (constraintText.Length > 0) + { + constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); + } + break; + case ':': + constraintText = text.Substring(startIndex, currentIndex - startIndex); + if (constraintText.Length > 0) + { + constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); + } + startIndex = currentIndex + 1; + break; + case '(': + state = ParseState.InsideParenthesis; + break; + case '=': + state = ParseState.End; + constraintText = text.Substring(startIndex, currentIndex - startIndex); + if (constraintText.Length > 0) + { + constraints.Add(RoutePatternFactory.ParameterPolicy(constraintText)); + } + currentIndex--; + break; + } + break; + } - } while (state != ParseState.End); + currentIndex++; - return new ParameterPolicyParseResults(currentIndex, constraints.ToArray()); - } + } while (state != ParseState.End); - private enum ParseState - { - Start, - ParsingName, - InsideParenthesis, - End - } + return new ParameterPolicyParseResults(currentIndex, constraints.ToArray()); + } - private readonly struct ParameterPolicyParseResults - { - public readonly int CurrentIndex; + private enum ParseState + { + Start, + ParsingName, + InsideParenthesis, + End + } - public readonly RoutePatternParameterPolicyReference[] ParameterPolicies; + private readonly struct ParameterPolicyParseResults + { + public readonly int CurrentIndex; - public ParameterPolicyParseResults(int currentIndex, RoutePatternParameterPolicyReference[] parameterPolicies) - { - CurrentIndex = currentIndex; - ParameterPolicies = parameterPolicies; - } + public readonly RoutePatternParameterPolicyReference[] ParameterPolicies; + + public ParameterPolicyParseResults(int currentIndex, RoutePatternParameterPolicyReference[] parameterPolicies) + { + CurrentIndex = currentIndex; + ParameterPolicies = parameterPolicies; } } } diff --git a/src/Http/Routing/src/Patterns/RoutePattern.cs b/src/Http/Routing/src/Patterns/RoutePattern.cs index 64836e1543..19125fa443 100644 --- a/src/Http/Routing/src/Patterns/RoutePattern.cs +++ b/src/Http/Routing/src/Patterns/RoutePattern.cs @@ -7,162 +7,161 @@ using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Routing.Template; -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +/// +/// Represents a parsed route template with default values and constraints. +/// Use to create +/// instances. Instances of are immutable. +/// +[DebuggerDisplay("{DebuggerToString()}")] +public sealed class RoutePattern { /// - /// Represents a parsed route template with default values and constraints. - /// Use to create - /// instances. Instances of are immutable. + /// A marker object that can be used in to designate that + /// any non-null or non-empty value is required. /// - [DebuggerDisplay("{DebuggerToString()}")] - public sealed class RoutePattern + /// + /// is only use in routing is in . + /// is not valid as a route value, and will convert to the null/empty string. + /// + public static readonly object RequiredValueAny = new RequiredValueAnySentinal(); + + internal static bool IsRequiredValueAny(object? value) { - /// - /// A marker object that can be used in to designate that - /// any non-null or non-empty value is required. - /// - /// - /// is only use in routing is in . - /// is not valid as a route value, and will convert to the null/empty string. - /// - public static readonly object RequiredValueAny = new RequiredValueAnySentinal(); - - internal static bool IsRequiredValueAny(object? value) - { - return object.ReferenceEquals(RequiredValueAny, value); - } + return object.ReferenceEquals(RequiredValueAny, value); + } - private const string SeparatorString = "/"; + private const string SeparatorString = "/"; - internal RoutePattern( - string? rawText, - IReadOnlyDictionary defaults, - IReadOnlyDictionary> parameterPolicies, - IReadOnlyDictionary requiredValues, - IReadOnlyList parameters, - IReadOnlyList pathSegments) - { - Debug.Assert(defaults != null); - Debug.Assert(parameterPolicies != null); - Debug.Assert(parameters != null); - Debug.Assert(requiredValues != null); - Debug.Assert(pathSegments != null); - - RawText = rawText; - Defaults = defaults; - ParameterPolicies = parameterPolicies; - RequiredValues = requiredValues; - Parameters = parameters; - PathSegments = pathSegments; - - InboundPrecedence = RoutePrecedence.ComputeInbound(this); - OutboundPrecedence = RoutePrecedence.ComputeOutbound(this); - } + internal RoutePattern( + string? rawText, + IReadOnlyDictionary defaults, + IReadOnlyDictionary> parameterPolicies, + IReadOnlyDictionary requiredValues, + IReadOnlyList parameters, + IReadOnlyList pathSegments) + { + Debug.Assert(defaults != null); + Debug.Assert(parameterPolicies != null); + Debug.Assert(parameters != null); + Debug.Assert(requiredValues != null); + Debug.Assert(pathSegments != null); + + RawText = rawText; + Defaults = defaults; + ParameterPolicies = parameterPolicies; + RequiredValues = requiredValues; + Parameters = parameters; + PathSegments = pathSegments; + + InboundPrecedence = RoutePrecedence.ComputeInbound(this); + OutboundPrecedence = RoutePrecedence.ComputeOutbound(this); + } - /// - /// Gets the set of default values for the route pattern. - /// The keys of are the route parameter names. - /// - public IReadOnlyDictionary Defaults { get; } - - /// - /// Gets the set of parameter policy references for the route pattern. - /// The keys of are the route parameter names. - /// - public IReadOnlyDictionary> ParameterPolicies { get; } - - /// - /// Gets a collection of route values that must be provided for this route pattern to be considered - /// applicable. - /// - /// - /// - /// allows a framework to substitute route values into a parameterized template - /// so that the same route template specification can be used to create multiple route patterns. - /// - /// This example shows how a route template can be used with required values to substitute known - /// route values for parameters. - /// - /// Route Template: "{controller=Home}/{action=Index}/{id?}" - /// Route Values: { controller = "Store", action = "Index" } - /// - /// - /// A route pattern produced in this way will match and generate URL paths like: /Store, - /// /Store/Index, and /Store/Index/17. - /// - /// - /// - public IReadOnlyDictionary RequiredValues { get; } - - /// - /// Gets the precedence value of the route pattern for URL matching. - /// - /// - /// Precedence is a computed value based on the structure of the route pattern - /// used for building URL matching data structures. - /// - public decimal InboundPrecedence { get; } - - /// - /// Gets the precedence value of the route pattern for URL generation. - /// - /// - /// Precedence is a computed value based on the structure of the route pattern - /// used for building URL generation data structures. - /// - public decimal OutboundPrecedence { get; } - - /// - /// Gets the raw text supplied when parsing the route pattern. May be null. - /// - public string? RawText { get; } - - /// - /// Gets the list of route parameters. - /// - public IReadOnlyList Parameters { get; } - - /// - /// Gets the list of path segments. - /// - public IReadOnlyList PathSegments { get; } - - /// - /// Gets the parameter matching the given name. - /// - /// The name of the parameter to match. - /// The matching parameter or null if no parameter matches the given name. - public RoutePatternParameterPart? GetParameter(string name) - { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } + /// + /// Gets the set of default values for the route pattern. + /// The keys of are the route parameter names. + /// + public IReadOnlyDictionary Defaults { get; } - var parameters = Parameters; - // Read interface .Count once rather than per iteration - var parametersCount = parameters.Count; - for (var i = 0; i < parametersCount; i++) - { - var parameter = parameters[i]; - if (string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase)) - { - return parameter; - } - } + /// + /// Gets the set of parameter policy references for the route pattern. + /// The keys of are the route parameter names. + /// + public IReadOnlyDictionary> ParameterPolicies { get; } - return null; - } + /// + /// Gets a collection of route values that must be provided for this route pattern to be considered + /// applicable. + /// + /// + /// + /// allows a framework to substitute route values into a parameterized template + /// so that the same route template specification can be used to create multiple route patterns. + /// + /// This example shows how a route template can be used with required values to substitute known + /// route values for parameters. + /// + /// Route Template: "{controller=Home}/{action=Index}/{id?}" + /// Route Values: { controller = "Store", action = "Index" } + /// + /// + /// A route pattern produced in this way will match and generate URL paths like: /Store, + /// /Store/Index, and /Store/Index/17. + /// + /// + /// + public IReadOnlyDictionary RequiredValues { get; } + + /// + /// Gets the precedence value of the route pattern for URL matching. + /// + /// + /// Precedence is a computed value based on the structure of the route pattern + /// used for building URL matching data structures. + /// + public decimal InboundPrecedence { get; } - internal string DebuggerToString() + /// + /// Gets the precedence value of the route pattern for URL generation. + /// + /// + /// Precedence is a computed value based on the structure of the route pattern + /// used for building URL generation data structures. + /// + public decimal OutboundPrecedence { get; } + + /// + /// Gets the raw text supplied when parsing the route pattern. May be null. + /// + public string? RawText { get; } + + /// + /// Gets the list of route parameters. + /// + public IReadOnlyList Parameters { get; } + + /// + /// Gets the list of path segments. + /// + public IReadOnlyList PathSegments { get; } + + /// + /// Gets the parameter matching the given name. + /// + /// The name of the parameter to match. + /// The matching parameter or null if no parameter matches the given name. + public RoutePatternParameterPart? GetParameter(string name) + { + if (name == null) { - return RawText ?? string.Join(SeparatorString, PathSegments.Select(s => s.DebuggerToString())); + throw new ArgumentNullException(nameof(name)); } - [DebuggerDisplay("{DebuggerToString(),nq}")] - private class RequiredValueAnySentinal + var parameters = Parameters; + // Read interface .Count once rather than per iteration + var parametersCount = parameters.Count; + for (var i = 0; i < parametersCount; i++) { - private string DebuggerToString() => "*any*"; + var parameter = parameters[i]; + if (string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase)) + { + return parameter; + } } + + return null; + } + + internal string DebuggerToString() + { + return RawText ?? string.Join(SeparatorString, PathSegments.Select(s => s.DebuggerToString())); + } + + [DebuggerDisplay("{DebuggerToString(),nq}")] + private class RequiredValueAnySentinal + { + private string DebuggerToString() => "*any*"; } } diff --git a/src/Http/Routing/src/Patterns/RoutePatternException.cs b/src/Http/Routing/src/Patterns/RoutePatternException.cs index f99ff8d04e..d488b2724d 100644 --- a/src/Http/Routing/src/Patterns/RoutePatternException.cs +++ b/src/Http/Routing/src/Patterns/RoutePatternException.cs @@ -4,55 +4,54 @@ using System; using System.Runtime.Serialization; -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +/// +/// An exception that is thrown for error constructing a . +/// +[Serializable] +public sealed class RoutePatternException : Exception { + private RoutePatternException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + Pattern = (string)info.GetValue(nameof(Pattern), typeof(string))!; + } + /// - /// An exception that is thrown for error constructing a . + /// Creates a new instance of . /// - [Serializable] - public sealed class RoutePatternException : Exception + /// The route pattern as raw text. + /// The exception message. + public RoutePatternException(string pattern, string message) + : base(message) { - private RoutePatternException(SerializationInfo info, StreamingContext context) - : base(info, context) + if (pattern == null) { - Pattern = (string)info.GetValue(nameof(Pattern), typeof(string))!; + throw new ArgumentNullException(nameof(pattern)); } - /// - /// Creates a new instance of . - /// - /// The route pattern as raw text. - /// The exception message. - public RoutePatternException(string pattern, string message) - : base(message) + if (message == null) { - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } + throw new ArgumentNullException(nameof(message)); + } - if (message == null) - { - throw new ArgumentNullException(nameof(message)); - } + Pattern = pattern; + } - Pattern = pattern; - } + /// + /// Gets the route pattern associated with this exception. + /// + public string Pattern { get; } - /// - /// Gets the route pattern associated with this exception. - /// - public string Pattern { get; } - - /// - /// Populates a with the data needed to serialize the target object. - /// - /// The to populate with data. - /// The destination () for this serialization. - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(Pattern), Pattern); - base.GetObjectData(info, context); - } + /// + /// Populates a with the data needed to serialize the target object. + /// + /// The to populate with data. + /// The destination () for this serialization. + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue(nameof(Pattern), Pattern); + base.GetObjectData(info, context); } } diff --git a/src/Http/Routing/src/Patterns/RoutePatternFactory.cs b/src/Http/Routing/src/Patterns/RoutePatternFactory.cs index 91e6d91349..0bd5705104 100644 --- a/src/Http/Routing/src/Patterns/RoutePatternFactory.cs +++ b/src/Http/Routing/src/Patterns/RoutePatternFactory.cs @@ -8,907 +8,906 @@ using System.Collections.ObjectModel; using System.Linq; using Microsoft.AspNetCore.Routing.Constraints; -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +/// +/// Contains factory methods for creating and related types. +/// Use to parse a route pattern in +/// string format. +/// +public static class RoutePatternFactory { + private static readonly IReadOnlyDictionary EmptyDictionary = + new ReadOnlyDictionary(new Dictionary()); + + private static readonly IReadOnlyDictionary> EmptyPoliciesDictionary = + new ReadOnlyDictionary>(new Dictionary>()); + /// - /// Contains factory methods for creating and related types. - /// Use to parse a route pattern in - /// string format. + /// Creates a from its string representation. /// - public static class RoutePatternFactory + /// The route pattern string to parse. + /// The . + public static RoutePattern Parse(string pattern) { - private static readonly IReadOnlyDictionary EmptyDictionary = - new ReadOnlyDictionary(new Dictionary()); + if (pattern == null) + { + throw new ArgumentNullException(nameof(pattern)); + } - private static readonly IReadOnlyDictionary> EmptyPoliciesDictionary = - new ReadOnlyDictionary>(new Dictionary>()); + return RoutePatternParser.Parse(pattern); + } - /// - /// Creates a from its string representation. - /// - /// The route pattern string to parse. - /// The . - public static RoutePattern Parse(string pattern) + /// + /// Creates a from its string representation along + /// with provided default values and parameter policies. + /// + /// The route pattern string to parse. + /// + /// Additional default values to associated with the route pattern. May be null. + /// The provided object will be converted to key-value pairs using + /// and then merged into the parsed route pattern. + /// + /// + /// Additional parameter policies to associated with the route pattern. May be null. + /// The provided object will be converted to key-value pairs using + /// and then merged into the parsed route pattern. + /// Multiple policies can be specified for a key by providing a collection as the value. + /// + /// The . + public static RoutePattern Parse(string pattern, object? defaults, object? parameterPolicies) + { + if (pattern == null) { - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } + throw new ArgumentNullException(nameof(pattern)); + } - return RoutePatternParser.Parse(pattern); - } - - /// - /// Creates a from its string representation along - /// with provided default values and parameter policies. - /// - /// The route pattern string to parse. - /// - /// Additional default values to associated with the route pattern. May be null. - /// The provided object will be converted to key-value pairs using - /// and then merged into the parsed route pattern. - /// - /// - /// Additional parameter policies to associated with the route pattern. May be null. - /// The provided object will be converted to key-value pairs using - /// and then merged into the parsed route pattern. - /// Multiple policies can be specified for a key by providing a collection as the value. - /// - /// The . - public static RoutePattern Parse(string pattern, object? defaults, object? parameterPolicies) - { - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } + var original = RoutePatternParser.Parse(pattern); + return PatternCore(original.RawText, Wrap(defaults), Wrap(parameterPolicies), requiredValues: null, original.PathSegments); + } - var original = RoutePatternParser.Parse(pattern); - return PatternCore(original.RawText, Wrap(defaults), Wrap(parameterPolicies), requiredValues: null, original.PathSegments); - } - - /// - /// Creates a from its string representation along - /// with provided default values and parameter policies. - /// - /// The route pattern string to parse. - /// - /// Additional default values to associated with the route pattern. May be null. - /// The provided object will be converted to key-value pairs using - /// and then merged into the parsed route pattern. - /// - /// - /// Additional parameter policies to associated with the route pattern. May be null. - /// The provided object will be converted to key-value pairs using - /// and then merged into the parsed route pattern. - /// Multiple policies can be specified for a key by providing a collection as the value. - /// - /// - /// Route values that can be substituted for parameters in the route pattern. See remarks on . - /// - /// The . - public static RoutePattern Parse(string pattern, object? defaults, object? parameterPolicies, object? requiredValues) - { - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } + /// + /// Creates a from its string representation along + /// with provided default values and parameter policies. + /// + /// The route pattern string to parse. + /// + /// Additional default values to associated with the route pattern. May be null. + /// The provided object will be converted to key-value pairs using + /// and then merged into the parsed route pattern. + /// + /// + /// Additional parameter policies to associated with the route pattern. May be null. + /// The provided object will be converted to key-value pairs using + /// and then merged into the parsed route pattern. + /// Multiple policies can be specified for a key by providing a collection as the value. + /// + /// + /// Route values that can be substituted for parameters in the route pattern. See remarks on . + /// + /// The . + public static RoutePattern Parse(string pattern, object? defaults, object? parameterPolicies, object? requiredValues) + { + if (pattern == null) + { + throw new ArgumentNullException(nameof(pattern)); + } + + var original = RoutePatternParser.Parse(pattern); + return PatternCore(original.RawText, Wrap(defaults), Wrap(parameterPolicies), Wrap(requiredValues), original.PathSegments); + } - var original = RoutePatternParser.Parse(pattern); - return PatternCore(original.RawText, Wrap(defaults), Wrap(parameterPolicies), Wrap(requiredValues), original.PathSegments); + /// + /// Creates a new instance of from a collection of segments. + /// + /// The collection of segments. + /// The . + public static RoutePattern Pattern(IEnumerable segments) + { + if (segments == null) + { + throw new ArgumentNullException(nameof(segments)); } - /// - /// Creates a new instance of from a collection of segments. - /// - /// The collection of segments. - /// The . - public static RoutePattern Pattern(IEnumerable segments) + return PatternCore(null, null, null, null, segments); + } + + /// + /// Creates a new instance of from a collection of segments. + /// + /// The raw text to associate with the route pattern. May be null. + /// The collection of segments. + /// The . + public static RoutePattern Pattern(string? rawText, IEnumerable segments) + { + if (segments == null) { - if (segments == null) - { - throw new ArgumentNullException(nameof(segments)); - } + throw new ArgumentNullException(nameof(segments)); + } + + return PatternCore(rawText, null, null, null, segments); + } - return PatternCore(null, null, null, null, segments); + /// + /// Creates a from a collection of segments along + /// with provided default values and parameter policies. + /// + /// + /// Additional default values to associated with the route pattern. May be null. + /// The provided object will be converted to key-value pairs using + /// and then merged into the route pattern. + /// + /// + /// Additional parameter policies to associated with the route pattern. May be null. + /// The provided object will be converted to key-value pairs using + /// and then merged into the route pattern. + /// Multiple policies can be specified for a key by providing a collection as the value. + /// + /// The collection of segments. + /// The . + public static RoutePattern Pattern( + object? defaults, + object? parameterPolicies, + IEnumerable segments) + { + if (segments == null) + { + throw new ArgumentNullException(nameof(segments)); } - /// - /// Creates a new instance of from a collection of segments. - /// - /// The raw text to associate with the route pattern. May be null. - /// The collection of segments. - /// The . - public static RoutePattern Pattern(string? rawText, IEnumerable segments) + return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments); + } + + /// + /// Creates a from a collection of segments along + /// with provided default values and parameter policies. + /// + /// The raw text to associate with the route pattern. May be null. + /// + /// Additional default values to associated with the route pattern. May be null. + /// The provided object will be converted to key-value pairs using + /// and then merged into the route pattern. + /// + /// + /// Additional parameter policies to associated with the route pattern. May be null. + /// The provided object will be converted to key-value pairs using + /// and then merged into the route pattern. + /// Multiple policies can be specified for a key by providing a collection as the value. + /// + /// The collection of segments. + /// The . + public static RoutePattern Pattern( + string? rawText, + object? defaults, + object? parameterPolicies, + IEnumerable segments) + { + if (segments == null) { - if (segments == null) - { - throw new ArgumentNullException(nameof(segments)); - } + throw new ArgumentNullException(nameof(segments)); + } - return PatternCore(rawText, null, null, null, segments); - } - - /// - /// Creates a from a collection of segments along - /// with provided default values and parameter policies. - /// - /// - /// Additional default values to associated with the route pattern. May be null. - /// The provided object will be converted to key-value pairs using - /// and then merged into the route pattern. - /// - /// - /// Additional parameter policies to associated with the route pattern. May be null. - /// The provided object will be converted to key-value pairs using - /// and then merged into the route pattern. - /// Multiple policies can be specified for a key by providing a collection as the value. - /// - /// The collection of segments. - /// The . - public static RoutePattern Pattern( - object? defaults, - object? parameterPolicies, - IEnumerable segments) - { - if (segments == null) - { - throw new ArgumentNullException(nameof(segments)); - } + return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments); + } - return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments); - } - - /// - /// Creates a from a collection of segments along - /// with provided default values and parameter policies. - /// - /// The raw text to associate with the route pattern. May be null. - /// - /// Additional default values to associated with the route pattern. May be null. - /// The provided object will be converted to key-value pairs using - /// and then merged into the route pattern. - /// - /// - /// Additional parameter policies to associated with the route pattern. May be null. - /// The provided object will be converted to key-value pairs using - /// and then merged into the route pattern. - /// Multiple policies can be specified for a key by providing a collection as the value. - /// - /// The collection of segments. - /// The . - public static RoutePattern Pattern( - string? rawText, - object? defaults, - object? parameterPolicies, - IEnumerable segments) - { - if (segments == null) - { - throw new ArgumentNullException(nameof(segments)); - } + /// + /// Creates a new instance of from a collection of segments. + /// + /// The collection of segments. + /// The . + public static RoutePattern Pattern(params RoutePatternPathSegment[] segments) + { + if (segments == null) + { + throw new ArgumentNullException(nameof(segments)); + } + + return PatternCore(null, null, null, requiredValues: null, segments); + } - return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments); + /// + /// Creates a new instance of from a collection of segments. + /// + /// The raw text to associate with the route pattern. May be null. + /// The collection of segments. + /// The . + public static RoutePattern Pattern(string rawText, params RoutePatternPathSegment[] segments) + { + if (segments == null) + { + throw new ArgumentNullException(nameof(segments)); } - /// - /// Creates a new instance of from a collection of segments. - /// - /// The collection of segments. - /// The . - public static RoutePattern Pattern(params RoutePatternPathSegment[] segments) + return PatternCore(rawText, null, null, requiredValues: null, segments); + } + + /// + /// Creates a from a collection of segments along + /// with provided default values and parameter policies. + /// + /// + /// Additional default values to associated with the route pattern. May be null. + /// The provided object will be converted to key-value pairs using + /// and then merged into the route pattern. + /// + /// + /// Additional parameter policies to associated with the route pattern. May be null. + /// The provided object will be converted to key-value pairs using + /// and then merged into the route pattern. + /// Multiple policies can be specified for a key by providing a collection as the value. + /// + /// The collection of segments. + /// The . + public static RoutePattern Pattern( + object? defaults, + object? parameterPolicies, + params RoutePatternPathSegment[] segments) + { + if (segments == null) { - if (segments == null) - { - throw new ArgumentNullException(nameof(segments)); - } + throw new ArgumentNullException(nameof(segments)); + } + + return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments); + } - return PatternCore(null, null, null, requiredValues: null, segments); + /// + /// Creates a from a collection of segments along + /// with provided default values and parameter policies. + /// + /// The raw text to associate with the route pattern. + /// + /// Additional default values to associated with the route pattern. May be null. + /// The provided object will be converted to key-value pairs using + /// and then merged into the route pattern. + /// + /// + /// Additional parameter policies to associated with the route pattern. May be null. + /// The provided object will be converted to key-value pairs using + /// and then merged into the route pattern. + /// Multiple policies can be specified for a key by providing a collection as the value. + /// + /// The collection of segments. + /// The . + public static RoutePattern Pattern( + string? rawText, + object? defaults, + object? parameterPolicies, + params RoutePatternPathSegment[] segments) + { + if (segments == null) + { + throw new ArgumentNullException(nameof(segments)); } - /// - /// Creates a new instance of from a collection of segments. - /// - /// The raw text to associate with the route pattern. May be null. - /// The collection of segments. - /// The . - public static RoutePattern Pattern(string rawText, params RoutePatternPathSegment[] segments) + return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments); + } + + private static RoutePattern PatternCore( + string? rawText, + RouteValueDictionary? defaults, + RouteValueDictionary? parameterPolicies, + RouteValueDictionary? requiredValues, + IEnumerable segments) + { + // We want to merge the segment data with the 'out of line' defaults and parameter policies. + // + // This means that for parameters that have 'out of line' defaults we will modify + // the parameter to contain the default (same story for parameter policies). + // + // We also maintain a collection of defaults and parameter policies that will also + // contain the values that don't match a parameter. + // + // It's important that these two views of the data are consistent. We don't want + // values specified out of line to have a different behavior. + + Dictionary? updatedDefaults = null; + if (defaults != null && defaults.Count > 0) { - if (segments == null) - { - throw new ArgumentNullException(nameof(segments)); - } + updatedDefaults = new Dictionary(defaults.Count, StringComparer.OrdinalIgnoreCase); - return PatternCore(rawText, null, null, requiredValues: null, segments); - } - - /// - /// Creates a from a collection of segments along - /// with provided default values and parameter policies. - /// - /// - /// Additional default values to associated with the route pattern. May be null. - /// The provided object will be converted to key-value pairs using - /// and then merged into the route pattern. - /// - /// - /// Additional parameter policies to associated with the route pattern. May be null. - /// The provided object will be converted to key-value pairs using - /// and then merged into the route pattern. - /// Multiple policies can be specified for a key by providing a collection as the value. - /// - /// The collection of segments. - /// The . - public static RoutePattern Pattern( - object? defaults, - object? parameterPolicies, - params RoutePatternPathSegment[] segments) - { - if (segments == null) + foreach (var kvp in defaults) { - throw new ArgumentNullException(nameof(segments)); + updatedDefaults.Add(kvp.Key, kvp.Value); } + } - return PatternCore(null, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments); - } - - /// - /// Creates a from a collection of segments along - /// with provided default values and parameter policies. - /// - /// The raw text to associate with the route pattern. - /// - /// Additional default values to associated with the route pattern. May be null. - /// The provided object will be converted to key-value pairs using - /// and then merged into the route pattern. - /// - /// - /// Additional parameter policies to associated with the route pattern. May be null. - /// The provided object will be converted to key-value pairs using - /// and then merged into the route pattern. - /// Multiple policies can be specified for a key by providing a collection as the value. - /// - /// The collection of segments. - /// The . - public static RoutePattern Pattern( - string? rawText, - object? defaults, - object? parameterPolicies, - params RoutePatternPathSegment[] segments) - { - if (segments == null) - { - throw new ArgumentNullException(nameof(segments)); - } + Dictionary>? updatedParameterPolicies = null; + if (parameterPolicies != null && parameterPolicies.Count > 0) + { + updatedParameterPolicies = new Dictionary>(parameterPolicies.Count, StringComparer.OrdinalIgnoreCase); - return PatternCore(rawText, new RouteValueDictionary(defaults), new RouteValueDictionary(parameterPolicies), requiredValues: null, segments); - } - - private static RoutePattern PatternCore( - string? rawText, - RouteValueDictionary? defaults, - RouteValueDictionary? parameterPolicies, - RouteValueDictionary? requiredValues, - IEnumerable segments) - { - // We want to merge the segment data with the 'out of line' defaults and parameter policies. - // - // This means that for parameters that have 'out of line' defaults we will modify - // the parameter to contain the default (same story for parameter policies). - // - // We also maintain a collection of defaults and parameter policies that will also - // contain the values that don't match a parameter. - // - // It's important that these two views of the data are consistent. We don't want - // values specified out of line to have a different behavior. - - Dictionary? updatedDefaults = null; - if (defaults != null && defaults.Count > 0) + foreach (var kvp in parameterPolicies) { - updatedDefaults = new Dictionary(defaults.Count, StringComparer.OrdinalIgnoreCase); + var policyReferences = new List(); - foreach (var kvp in defaults) + if (kvp.Value is IParameterPolicy parameterPolicy) { - updatedDefaults.Add(kvp.Key, kvp.Value); + policyReferences.Add(ParameterPolicy(parameterPolicy)); } - } - - Dictionary>? updatedParameterPolicies = null; - if (parameterPolicies != null && parameterPolicies.Count > 0) - { - updatedParameterPolicies = new Dictionary>(parameterPolicies.Count, StringComparer.OrdinalIgnoreCase); - - foreach (var kvp in parameterPolicies) + else if (kvp.Value is string) { - var policyReferences = new List(); - - if (kvp.Value is IParameterPolicy parameterPolicy) - { - policyReferences.Add(ParameterPolicy(parameterPolicy)); - } - else if (kvp.Value is string) + // Constraint will convert string values into regex constraints + policyReferences.Add(Constraint(kvp.Value)); + } + else if (kvp.Value is IEnumerable multiplePolicies) + { + foreach (var item in multiplePolicies) { // Constraint will convert string values into regex constraints - policyReferences.Add(Constraint(kvp.Value)); + policyReferences.Add(item is IParameterPolicy p ? ParameterPolicy(p) : Constraint(item)); } - else if (kvp.Value is IEnumerable multiplePolicies) - { - foreach (var item in multiplePolicies) - { - // Constraint will convert string values into regex constraints - policyReferences.Add(item is IParameterPolicy p ? ParameterPolicy(p) : Constraint(item)); - } - } - else - { - throw new InvalidOperationException(Resources.FormatRoutePattern_InvalidConstraintReference( - kvp.Value ?? "null", - typeof(IRouteConstraint))); - } - - updatedParameterPolicies.Add(kvp.Key, policyReferences); } - } - - List? parameters = null; - var updatedSegments = segments.ToArray(); - for (var i = 0; i < updatedSegments.Length; i++) - { - var segment = VisitSegment(updatedSegments[i]); - updatedSegments[i] = segment; - - for (var j = 0; j < segment.Parts.Count; j++) + else { - if (segment.Parts[j] is RoutePatternParameterPart parameter) - { - if (parameters == null) - { - parameters = new List(); - } - - parameters.Add(parameter); - } + throw new InvalidOperationException(Resources.FormatRoutePattern_InvalidConstraintReference( + kvp.Value ?? "null", + typeof(IRouteConstraint))); } + + updatedParameterPolicies.Add(kvp.Key, policyReferences); } + } + + List? parameters = null; + var updatedSegments = segments.ToArray(); + for (var i = 0; i < updatedSegments.Length; i++) + { + var segment = VisitSegment(updatedSegments[i]); + updatedSegments[i] = segment; - // Each Required Value either needs to either: - // 1. be null-ish - // 2. have a corresponding parameter - // 3. have a corresponding default that matches both key and value - if (requiredValues != null) + for (var j = 0; j < segment.Parts.Count; j++) { - foreach (var kvp in requiredValues) + if (segment.Parts[j] is RoutePatternParameterPart parameter) { - // 1.be null-ish - var found = RouteValueEqualityComparer.Default.Equals(string.Empty, kvp.Value); - - // 2. have a corresponding parameter - if (!found && parameters != null) + if (parameters == null) { - for (var i = 0; i < parameters.Count; i++) - { - if (string.Equals(kvp.Key, parameters[i].Name, StringComparison.OrdinalIgnoreCase)) - { - found = true; - break; - } - } + parameters = new List(); } - // 3. have a corresponding default that matches both key and value - if (!found && - updatedDefaults != null && - updatedDefaults.TryGetValue(kvp.Key, out var defaultValue) && - RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue)) - { - found = true; - } - - if (!found) - { - throw new InvalidOperationException( - $"No corresponding parameter or default value could be found for the required value " + - $"'{kvp.Key}={kvp.Value}'. A non-null required value must correspond to a route parameter or the " + - $"route pattern must have a matching default value."); - } + parameters.Add(parameter); } } + } - return new RoutePattern( - rawText, - updatedDefaults ?? EmptyDictionary, - updatedParameterPolicies != null - ? updatedParameterPolicies.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value.ToArray()) - : EmptyPoliciesDictionary, - requiredValues ?? EmptyDictionary, - (IReadOnlyList?)parameters ?? Array.Empty(), - updatedSegments); - - RoutePatternPathSegment VisitSegment(RoutePatternPathSegment segment) + // Each Required Value either needs to either: + // 1. be null-ish + // 2. have a corresponding parameter + // 3. have a corresponding default that matches both key and value + if (requiredValues != null) + { + foreach (var kvp in requiredValues) { - RoutePatternPart[]? updatedParts = null; - for (var i = 0; i < segment.Parts.Count; i++) - { - var part = segment.Parts[i]; - var updatedPart = VisitPart(part); + // 1.be null-ish + var found = RouteValueEqualityComparer.Default.Equals(string.Empty, kvp.Value); - if (part != updatedPart) + // 2. have a corresponding parameter + if (!found && parameters != null) + { + for (var i = 0; i < parameters.Count; i++) { - if (updatedParts == null) + if (string.Equals(kvp.Key, parameters[i].Name, StringComparison.OrdinalIgnoreCase)) { - updatedParts = segment.Parts.ToArray(); + found = true; + break; } - - updatedParts[i] = updatedPart; } } - if (updatedParts == null) + // 3. have a corresponding default that matches both key and value + if (!found && + updatedDefaults != null && + updatedDefaults.TryGetValue(kvp.Key, out var defaultValue) && + RouteValueEqualityComparer.Default.Equals(kvp.Value, defaultValue)) { - // Segment has not changed - return segment; + found = true; } - return new RoutePatternPathSegment(updatedParts); - } - - RoutePatternPart VisitPart(RoutePatternPart part) - { - if (!part.IsParameter) + if (!found) { - return part; + throw new InvalidOperationException( + $"No corresponding parameter or default value could be found for the required value " + + $"'{kvp.Key}={kvp.Value}'. A non-null required value must correspond to a route parameter or the " + + $"route pattern must have a matching default value."); } + } + } - var parameter = (RoutePatternParameterPart)part; - var @default = parameter.Default; + return new RoutePattern( + rawText, + updatedDefaults ?? EmptyDictionary, + updatedParameterPolicies != null + ? updatedParameterPolicies.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value.ToArray()) + : EmptyPoliciesDictionary, + requiredValues ?? EmptyDictionary, + (IReadOnlyList?)parameters ?? Array.Empty(), + updatedSegments); - if (updatedDefaults != null && updatedDefaults.TryGetValue(parameter.Name, out var newDefault)) - { - if (parameter.Default != null && !Equals(newDefault, parameter.Default)) - { - var message = Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(parameter.Name); - throw new InvalidOperationException(message); - } + RoutePatternPathSegment VisitSegment(RoutePatternPathSegment segment) + { + RoutePatternPart[]? updatedParts = null; + for (var i = 0; i < segment.Parts.Count; i++) + { + var part = segment.Parts[i]; + var updatedPart = VisitPart(part); - if (parameter.IsOptional) + if (part != updatedPart) + { + if (updatedParts == null) { - var message = Resources.TemplateRoute_OptionalCannotHaveDefaultValue; - throw new InvalidOperationException(message); + updatedParts = segment.Parts.ToArray(); } - @default = newDefault; + updatedParts[i] = updatedPart; } + } - if (parameter.Default != null) - { - if (updatedDefaults == null) - { - updatedDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase); - } + if (updatedParts == null) + { + // Segment has not changed + return segment; + } - updatedDefaults[parameter.Name] = parameter.Default; - } + return new RoutePatternPathSegment(updatedParts); + } - List? parameterConstraints = null; - if ((updatedParameterPolicies == null || !updatedParameterPolicies.TryGetValue(parameter.Name, out parameterConstraints)) && - parameter.ParameterPolicies.Count > 0) - { - if (updatedParameterPolicies == null) - { - updatedParameterPolicies = new Dictionary>(StringComparer.OrdinalIgnoreCase); - } + RoutePatternPart VisitPart(RoutePatternPart part) + { + if (!part.IsParameter) + { + return part; + } - parameterConstraints = new List(parameter.ParameterPolicies.Count); - updatedParameterPolicies.Add(parameter.Name, parameterConstraints); - } + var parameter = (RoutePatternParameterPart)part; + var @default = parameter.Default; - if (parameter.ParameterPolicies.Count > 0) + if (updatedDefaults != null && updatedDefaults.TryGetValue(parameter.Name, out var newDefault)) + { + if (parameter.Default != null && !Equals(newDefault, parameter.Default)) { - parameterConstraints!.AddRange(parameter.ParameterPolicies); + var message = Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly(parameter.Name); + throw new InvalidOperationException(message); } - if (Equals(parameter.Default, @default) - && parameter.ParameterPolicies.Count == 0 - && (parameterConstraints?.Count ?? 0) == 0) + if (parameter.IsOptional) { - // Part has not changed - return part; + var message = Resources.TemplateRoute_OptionalCannotHaveDefaultValue; + throw new InvalidOperationException(message); } - return ParameterPartCore( - parameter.Name, - @default, - parameter.ParameterKind, - parameterConstraints?.ToArray() ?? Array.Empty(), - parameter.EncodeSlashes); + @default = newDefault; } - } - /// - /// Creates a from the provided collection - /// of parts. - /// - /// The collection of parts. - /// The . - public static RoutePatternPathSegment Segment(IEnumerable parts) - { - if (parts == null) + if (parameter.Default != null) { - throw new ArgumentNullException(nameof(parts)); - } - - return SegmentCore(parts.ToArray()); - } + if (updatedDefaults == null) + { + updatedDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase); + } - /// - /// Creates a from the provided collection - /// of parts. - /// - /// The collection of parts. - /// The . - public static RoutePatternPathSegment Segment(params RoutePatternPart[] parts) - { - if (parts == null) - { - throw new ArgumentNullException(nameof(parts)); + updatedDefaults[parameter.Name] = parameter.Default; } - return SegmentCore((RoutePatternPart[])parts.Clone()); - } + List? parameterConstraints = null; + if ((updatedParameterPolicies == null || !updatedParameterPolicies.TryGetValue(parameter.Name, out parameterConstraints)) && + parameter.ParameterPolicies.Count > 0) + { + if (updatedParameterPolicies == null) + { + updatedParameterPolicies = new Dictionary>(StringComparer.OrdinalIgnoreCase); + } - private static RoutePatternPathSegment SegmentCore(RoutePatternPart[] parts) - { - return new RoutePatternPathSegment(parts); - } + parameterConstraints = new List(parameter.ParameterPolicies.Count); + updatedParameterPolicies.Add(parameter.Name, parameterConstraints); + } - /// - /// Creates a from the provided text - /// content. - /// - /// The text content. - /// The . - public static RoutePatternLiteralPart LiteralPart(string content) - { - if (string.IsNullOrEmpty(content)) + if (parameter.ParameterPolicies.Count > 0) { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content)); + parameterConstraints!.AddRange(parameter.ParameterPolicies); } - if (content.IndexOf('?') >= 0) + if (Equals(parameter.Default, @default) + && parameter.ParameterPolicies.Count == 0 + && (parameterConstraints?.Count ?? 0) == 0) { - throw new ArgumentException(Resources.FormatTemplateRoute_InvalidLiteral(content)); + // Part has not changed + return part; } - return LiteralPartCore(content); + return ParameterPartCore( + parameter.Name, + @default, + parameter.ParameterKind, + parameterConstraints?.ToArray() ?? Array.Empty(), + parameter.EncodeSlashes); } + } - private static RoutePatternLiteralPart LiteralPartCore(string content) + /// + /// Creates a from the provided collection + /// of parts. + /// + /// The collection of parts. + /// The . + public static RoutePatternPathSegment Segment(IEnumerable parts) + { + if (parts == null) { - return new RoutePatternLiteralPart(content); + throw new ArgumentNullException(nameof(parts)); } - /// - /// Creates a from the provided text - /// content. - /// - /// The text content. - /// The . - public static RoutePatternSeparatorPart SeparatorPart(string content) - { - if (string.IsNullOrEmpty(content)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content)); - } + return SegmentCore(parts.ToArray()); + } - return SeparatorPartCore(content); + /// + /// Creates a from the provided collection + /// of parts. + /// + /// The collection of parts. + /// The . + public static RoutePatternPathSegment Segment(params RoutePatternPart[] parts) + { + if (parts == null) + { + throw new ArgumentNullException(nameof(parts)); } - private static RoutePatternSeparatorPart SeparatorPartCore(string content) + return SegmentCore((RoutePatternPart[])parts.Clone()); + } + + private static RoutePatternPathSegment SegmentCore(RoutePatternPart[] parts) + { + return new RoutePatternPathSegment(parts); + } + + /// + /// Creates a from the provided text + /// content. + /// + /// The text content. + /// The . + public static RoutePatternLiteralPart LiteralPart(string content) + { + if (string.IsNullOrEmpty(content)) { - return new RoutePatternSeparatorPart(content); + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content)); } - /// - /// Creates a from the provided parameter name. - /// - /// The parameter name. - /// The . - public static RoutePatternParameterPart ParameterPart(string parameterName) + if (content.IndexOf('?') >= 0) { - if (string.IsNullOrEmpty(parameterName)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); - } - - if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) - { - throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); - } + throw new ArgumentException(Resources.FormatTemplateRoute_InvalidLiteral(content)); + } - return ParameterPartCore( - parameterName: parameterName, - @default: null, - parameterKind: RoutePatternParameterKind.Standard, - parameterPolicies: Array.Empty()); - } - - /// - /// Creates a from the provided parameter name - /// and default value. - /// - /// The parameter name. - /// The parameter default value. May be null. - /// The . - public static RoutePatternParameterPart ParameterPart(string parameterName, object @default) - { - if (string.IsNullOrEmpty(parameterName)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); - } + return LiteralPartCore(content); + } - if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) - { - throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); - } + private static RoutePatternLiteralPart LiteralPartCore(string content) + { + return new RoutePatternLiteralPart(content); + } - return ParameterPartCore( - parameterName: parameterName, - @default: @default, - parameterKind: RoutePatternParameterKind.Standard, - parameterPolicies: Array.Empty()); - } - - /// - /// Creates a from the provided parameter name - /// and default value, and parameter kind. - /// - /// The parameter name. - /// The parameter default value. May be null. - /// The parameter kind. - /// The . - public static RoutePatternParameterPart ParameterPart( - string parameterName, - object? @default, - RoutePatternParameterKind parameterKind) - { - if (string.IsNullOrEmpty(parameterName)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); - } + /// + /// Creates a from the provided text + /// content. + /// + /// The text content. + /// The . + public static RoutePatternSeparatorPart SeparatorPart(string content) + { + if (string.IsNullOrEmpty(content)) + { + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(content)); + } - if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) - { - throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); - } + return SeparatorPartCore(content); + } - if (@default != null && parameterKind == RoutePatternParameterKind.Optional) - { - throw new ArgumentNullException(nameof(parameterKind), Resources.TemplateRoute_OptionalCannotHaveDefaultValue); - } + private static RoutePatternSeparatorPart SeparatorPartCore(string content) + { + return new RoutePatternSeparatorPart(content); + } - return ParameterPartCore( - parameterName: parameterName, - @default: @default, - parameterKind: parameterKind, - parameterPolicies: Array.Empty()); - } - - /// - /// Creates a from the provided parameter name - /// and default value, parameter kind, and parameter policies. - /// - /// The parameter name. - /// The parameter default value. May be null. - /// The parameter kind. - /// The parameter policies to associated with the parameter. - /// The . - public static RoutePatternParameterPart ParameterPart( - string parameterName, - object? @default, - RoutePatternParameterKind parameterKind, - IEnumerable parameterPolicies) - { - if (string.IsNullOrEmpty(parameterName)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); - } + /// + /// Creates a from the provided parameter name. + /// + /// The parameter name. + /// The . + public static RoutePatternParameterPart ParameterPart(string parameterName) + { + if (string.IsNullOrEmpty(parameterName)) + { + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); + } - if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) - { - throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); - } + if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) + { + throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); + } - if (@default != null && parameterKind == RoutePatternParameterKind.Optional) - { - throw new ArgumentNullException(nameof(parameterKind), Resources.TemplateRoute_OptionalCannotHaveDefaultValue); - } + return ParameterPartCore( + parameterName: parameterName, + @default: null, + parameterKind: RoutePatternParameterKind.Standard, + parameterPolicies: Array.Empty()); + } - if (parameterPolicies == null) - { - throw new ArgumentNullException(nameof(parameterPolicies)); - } + /// + /// Creates a from the provided parameter name + /// and default value. + /// + /// The parameter name. + /// The parameter default value. May be null. + /// The . + public static RoutePatternParameterPart ParameterPart(string parameterName, object @default) + { + if (string.IsNullOrEmpty(parameterName)) + { + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); + } - return ParameterPartCore( - parameterName: parameterName, - @default: @default, - parameterKind: parameterKind, - parameterPolicies: parameterPolicies.ToArray()); - } - - /// - /// Creates a from the provided parameter name - /// and default value, parameter kind, and parameter policies. - /// - /// The parameter name. - /// The parameter default value. May be null. - /// The parameter kind. - /// The parameter policies to associated with the parameter. - /// The . - public static RoutePatternParameterPart ParameterPart( - string parameterName, - object? @default, - RoutePatternParameterKind parameterKind, - params RoutePatternParameterPolicyReference[] parameterPolicies) - { - if (string.IsNullOrEmpty(parameterName)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); - } + if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) + { + throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); + } - if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) - { - throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); - } + return ParameterPartCore( + parameterName: parameterName, + @default: @default, + parameterKind: RoutePatternParameterKind.Standard, + parameterPolicies: Array.Empty()); + } - if (@default != null && parameterKind == RoutePatternParameterKind.Optional) - { - throw new ArgumentNullException(nameof(parameterKind), Resources.TemplateRoute_OptionalCannotHaveDefaultValue); - } + /// + /// Creates a from the provided parameter name + /// and default value, and parameter kind. + /// + /// The parameter name. + /// The parameter default value. May be null. + /// The parameter kind. + /// The . + public static RoutePatternParameterPart ParameterPart( + string parameterName, + object? @default, + RoutePatternParameterKind parameterKind) + { + if (string.IsNullOrEmpty(parameterName)) + { + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); + } - if (parameterPolicies == null) - { - throw new ArgumentNullException(nameof(parameterPolicies)); - } + if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) + { + throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); + } - return ParameterPartCore( - parameterName: parameterName, - @default: @default, - parameterKind: parameterKind, - parameterPolicies: (RoutePatternParameterPolicyReference[])parameterPolicies.Clone()); + if (@default != null && parameterKind == RoutePatternParameterKind.Optional) + { + throw new ArgumentNullException(nameof(parameterKind), Resources.TemplateRoute_OptionalCannotHaveDefaultValue); } - private static RoutePatternParameterPart ParameterPartCore( - string parameterName, - object? @default, - RoutePatternParameterKind parameterKind, - RoutePatternParameterPolicyReference[] parameterPolicies) + return ParameterPartCore( + parameterName: parameterName, + @default: @default, + parameterKind: parameterKind, + parameterPolicies: Array.Empty()); + } + + /// + /// Creates a from the provided parameter name + /// and default value, parameter kind, and parameter policies. + /// + /// The parameter name. + /// The parameter default value. May be null. + /// The parameter kind. + /// The parameter policies to associated with the parameter. + /// The . + public static RoutePatternParameterPart ParameterPart( + string parameterName, + object? @default, + RoutePatternParameterKind parameterKind, + IEnumerable parameterPolicies) + { + if (string.IsNullOrEmpty(parameterName)) { - return ParameterPartCore(parameterName, @default, parameterKind, parameterPolicies, encodeSlashes: true); + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); } - private static RoutePatternParameterPart ParameterPartCore( - string parameterName, - object? @default, - RoutePatternParameterKind parameterKind, - RoutePatternParameterPolicyReference[] parameterPolicies, - bool encodeSlashes) + if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) { - return new RoutePatternParameterPart( - parameterName, - @default, - parameterKind, - parameterPolicies, - encodeSlashes); - } - - /// - /// Creates a from the provided contraint. - /// - /// - /// The constraint object, which must be of type - /// or . If the constraint object is a - /// then it will be transformed into an instance of . - /// - /// The . - public static RoutePatternParameterPolicyReference Constraint(object constraint) - { - // Similar to RouteConstraintBuilder - if (constraint is IRouteConstraint policy) - { - return ParameterPolicyCore(policy); - } - else if (constraint is string content) - { - return ParameterPolicyCore(new RegexRouteConstraint("^(" + content + ")$")); - } - else - { - throw new InvalidOperationException(Resources.FormatRoutePattern_InvalidConstraintReference( - constraint ?? "null", - typeof(IRouteConstraint))); - } + throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); } - /// - /// Creates a from the provided constraint. - /// - /// - /// The constraint object. - /// - /// The . - public static RoutePatternParameterPolicyReference Constraint(IRouteConstraint constraint) + if (@default != null && parameterKind == RoutePatternParameterKind.Optional) { - if (constraint == null) - { - throw new ArgumentNullException(nameof(constraint)); - } + throw new ArgumentNullException(nameof(parameterKind), Resources.TemplateRoute_OptionalCannotHaveDefaultValue); + } - return ParameterPolicyCore(constraint); + if (parameterPolicies == null) + { + throw new ArgumentNullException(nameof(parameterPolicies)); } - /// - /// Creates a from the provided constraint. - /// - /// - /// The constraint text, which will be resolved by . - /// - /// The . - public static RoutePatternParameterPolicyReference Constraint(string constraint) + return ParameterPartCore( + parameterName: parameterName, + @default: @default, + parameterKind: parameterKind, + parameterPolicies: parameterPolicies.ToArray()); + } + + /// + /// Creates a from the provided parameter name + /// and default value, parameter kind, and parameter policies. + /// + /// The parameter name. + /// The parameter default value. May be null. + /// The parameter kind. + /// The parameter policies to associated with the parameter. + /// The . + public static RoutePatternParameterPart ParameterPart( + string parameterName, + object? @default, + RoutePatternParameterKind parameterKind, + params RoutePatternParameterPolicyReference[] parameterPolicies) + { + if (string.IsNullOrEmpty(parameterName)) { - if (string.IsNullOrEmpty(constraint)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(constraint)); - } + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterName)); + } - return ParameterPolicyCore(constraint); + if (parameterName.IndexOfAny(RoutePatternParser.InvalidParameterNameChars) >= 0) + { + throw new ArgumentException(Resources.FormatTemplateRoute_InvalidParameterName(parameterName)); } - /// - /// Creates a from the provided object. - /// - /// - /// The parameter policy object. - /// - /// The . - public static RoutePatternParameterPolicyReference ParameterPolicy(IParameterPolicy parameterPolicy) + if (@default != null && parameterKind == RoutePatternParameterKind.Optional) { - if (parameterPolicy == null) - { - throw new ArgumentNullException(nameof(parameterPolicy)); - } + throw new ArgumentNullException(nameof(parameterKind), Resources.TemplateRoute_OptionalCannotHaveDefaultValue); + } - return ParameterPolicyCore(parameterPolicy); + if (parameterPolicies == null) + { + throw new ArgumentNullException(nameof(parameterPolicies)); } - /// - /// Creates a from the provided object. - /// - /// - /// The parameter policy text, which will be resolved by . - /// - /// The . - public static RoutePatternParameterPolicyReference ParameterPolicy(string parameterPolicy) + return ParameterPartCore( + parameterName: parameterName, + @default: @default, + parameterKind: parameterKind, + parameterPolicies: (RoutePatternParameterPolicyReference[])parameterPolicies.Clone()); + } + + private static RoutePatternParameterPart ParameterPartCore( + string parameterName, + object? @default, + RoutePatternParameterKind parameterKind, + RoutePatternParameterPolicyReference[] parameterPolicies) + { + return ParameterPartCore(parameterName, @default, parameterKind, parameterPolicies, encodeSlashes: true); + } + + private static RoutePatternParameterPart ParameterPartCore( + string parameterName, + object? @default, + RoutePatternParameterKind parameterKind, + RoutePatternParameterPolicyReference[] parameterPolicies, + bool encodeSlashes) + { + return new RoutePatternParameterPart( + parameterName, + @default, + parameterKind, + parameterPolicies, + encodeSlashes); + } + + /// + /// Creates a from the provided contraint. + /// + /// + /// The constraint object, which must be of type + /// or . If the constraint object is a + /// then it will be transformed into an instance of . + /// + /// The . + public static RoutePatternParameterPolicyReference Constraint(object constraint) + { + // Similar to RouteConstraintBuilder + if (constraint is IRouteConstraint policy) { - if (string.IsNullOrEmpty(parameterPolicy)) - { - throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterPolicy)); - } + return ParameterPolicyCore(policy); + } + else if (constraint is string content) + { + return ParameterPolicyCore(new RegexRouteConstraint("^(" + content + ")$")); + } + else + { + throw new InvalidOperationException(Resources.FormatRoutePattern_InvalidConstraintReference( + constraint ?? "null", + typeof(IRouteConstraint))); + } + } - return ParameterPolicyCore(parameterPolicy); + /// + /// Creates a from the provided constraint. + /// + /// + /// The constraint object. + /// + /// The . + public static RoutePatternParameterPolicyReference Constraint(IRouteConstraint constraint) + { + if (constraint == null) + { + throw new ArgumentNullException(nameof(constraint)); } - private static RoutePatternParameterPolicyReference ParameterPolicyCore(string parameterPolicy) + return ParameterPolicyCore(constraint); + } + + /// + /// Creates a from the provided constraint. + /// + /// + /// The constraint text, which will be resolved by . + /// + /// The . + public static RoutePatternParameterPolicyReference Constraint(string constraint) + { + if (string.IsNullOrEmpty(constraint)) { - return new RoutePatternParameterPolicyReference(parameterPolicy); + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(constraint)); } - private static RoutePatternParameterPolicyReference ParameterPolicyCore(IParameterPolicy parameterPolicy) + return ParameterPolicyCore(constraint); + } + + /// + /// Creates a from the provided object. + /// + /// + /// The parameter policy object. + /// + /// The . + public static RoutePatternParameterPolicyReference ParameterPolicy(IParameterPolicy parameterPolicy) + { + if (parameterPolicy == null) { - return new RoutePatternParameterPolicyReference(parameterPolicy); + throw new ArgumentNullException(nameof(parameterPolicy)); } - private static RouteValueDictionary? Wrap(object? values) + return ParameterPolicyCore(parameterPolicy); + } + + /// + /// Creates a from the provided object. + /// + /// + /// The parameter policy text, which will be resolved by . + /// + /// The . + public static RoutePatternParameterPolicyReference ParameterPolicy(string parameterPolicy) + { + if (string.IsNullOrEmpty(parameterPolicy)) { - return values == null ? null : new RouteValueDictionary(values); + throw new ArgumentException(Resources.Argument_NullOrEmpty, nameof(parameterPolicy)); } + + return ParameterPolicyCore(parameterPolicy); + } + + private static RoutePatternParameterPolicyReference ParameterPolicyCore(string parameterPolicy) + { + return new RoutePatternParameterPolicyReference(parameterPolicy); + } + + private static RoutePatternParameterPolicyReference ParameterPolicyCore(IParameterPolicy parameterPolicy) + { + return new RoutePatternParameterPolicyReference(parameterPolicy); + } + + private static RouteValueDictionary? Wrap(object? values) + { + return values == null ? null : new RouteValueDictionary(values); } } diff --git a/src/Http/Routing/src/Patterns/RoutePatternLiteralPart.cs b/src/Http/Routing/src/Patterns/RoutePatternLiteralPart.cs index b979f046ec..ae9b930687 100644 --- a/src/Http/Routing/src/Patterns/RoutePatternLiteralPart.cs +++ b/src/Http/Routing/src/Patterns/RoutePatternLiteralPart.cs @@ -3,30 +3,29 @@ using System.Diagnostics; -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +/// +/// Resprents a literal text part of a route pattern. Instances of +/// are immutable. +/// +[DebuggerDisplay("{DebuggerToString()}")] +public sealed class RoutePatternLiteralPart : RoutePatternPart { - /// - /// Resprents a literal text part of a route pattern. Instances of - /// are immutable. - /// - [DebuggerDisplay("{DebuggerToString()}")] - public sealed class RoutePatternLiteralPart : RoutePatternPart + internal RoutePatternLiteralPart(string content) + : base(RoutePatternPartKind.Literal) { - internal RoutePatternLiteralPart(string content) - : base(RoutePatternPartKind.Literal) - { - Debug.Assert(!string.IsNullOrEmpty(content)); - Content = content; - } + Debug.Assert(!string.IsNullOrEmpty(content)); + Content = content; + } - /// - /// Gets the text content. - /// - public string Content { get; } + /// + /// Gets the text content. + /// + public string Content { get; } - internal override string DebuggerToString() - { - return Content; - } + internal override string DebuggerToString() + { + return Content; } } diff --git a/src/Http/Routing/src/Patterns/RoutePatternMatcher.cs b/src/Http/Routing/src/Patterns/RoutePatternMatcher.cs index cf8bd663aa..5a8c9c4b03 100644 --- a/src/Http/Routing/src/Patterns/RoutePatternMatcher.cs +++ b/src/Http/Routing/src/Patterns/RoutePatternMatcher.cs @@ -9,497 +9,496 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +internal class RoutePatternMatcher { - internal class RoutePatternMatcher - { - // Perf: This is a cache to avoid looking things up in 'Defaults' each request. - private readonly bool[] _hasDefaultValue; - private readonly object[] _defaultValues; + // Perf: This is a cache to avoid looking things up in 'Defaults' each request. + private readonly bool[] _hasDefaultValue; + private readonly object[] _defaultValues; - public RoutePatternMatcher( - RoutePattern pattern, - RouteValueDictionary defaults) + public RoutePatternMatcher( + RoutePattern pattern, + RouteValueDictionary defaults) + { + if (pattern == null) { - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } + throw new ArgumentNullException(nameof(pattern)); + } - RoutePattern = pattern; - Defaults = defaults ?? new RouteValueDictionary(); + RoutePattern = pattern; + Defaults = defaults ?? new RouteValueDictionary(); - // Perf: cache the default value for each parameter (other than complex segments). - _hasDefaultValue = new bool[RoutePattern.PathSegments.Count]; - _defaultValues = new object[RoutePattern.PathSegments.Count]; + // Perf: cache the default value for each parameter (other than complex segments). + _hasDefaultValue = new bool[RoutePattern.PathSegments.Count]; + _defaultValues = new object[RoutePattern.PathSegments.Count]; - for (var i = 0; i < RoutePattern.PathSegments.Count; i++) + for (var i = 0; i < RoutePattern.PathSegments.Count; i++) + { + var segment = RoutePattern.PathSegments[i]; + if (!segment.IsSimple) { - var segment = RoutePattern.PathSegments[i]; - if (!segment.IsSimple) - { - continue; - } + continue; + } - var part = segment.Parts[0]; - if (!part.IsParameter) - { - continue; - } + var part = segment.Parts[0]; + if (!part.IsParameter) + { + continue; + } - var parameter = (RoutePatternParameterPart)part; - if (Defaults.TryGetValue(parameter.Name, out var value)) - { - _hasDefaultValue[i] = true; - _defaultValues[i] = value; - } + var parameter = (RoutePatternParameterPart)part; + if (Defaults.TryGetValue(parameter.Name, out var value)) + { + _hasDefaultValue[i] = true; + _defaultValues[i] = value; } } + } - public RouteValueDictionary Defaults { get; } + public RouteValueDictionary Defaults { get; } - public RoutePattern RoutePattern { get; } + public RoutePattern RoutePattern { get; } + + public bool TryMatch(PathString path, RouteValueDictionary values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } - public bool TryMatch(PathString path, RouteValueDictionary values) + var i = 0; + var pathTokenizer = new PathTokenizer(path); + + // Perf: We do a traversal of the request-segments + route-segments twice. + // + // For most segment-types, we only really need to any work on one of the two passes. + // + // On the first pass, we're just looking to see if there's anything that would disqualify us from matching. + // The most common case would be a literal segment that doesn't match. + // + // On the second pass, we're almost certainly going to match the URL, so go ahead and allocate the 'values' + // and start capturing strings. + foreach (var stringSegment in pathTokenizer) { - if (values == null) + if (stringSegment.Length == 0) { - throw new ArgumentNullException(nameof(values)); + return false; } - var i = 0; - var pathTokenizer = new PathTokenizer(path); - - // Perf: We do a traversal of the request-segments + route-segments twice. - // - // For most segment-types, we only really need to any work on one of the two passes. - // - // On the first pass, we're just looking to see if there's anything that would disqualify us from matching. - // The most common case would be a literal segment that doesn't match. - // - // On the second pass, we're almost certainly going to match the URL, so go ahead and allocate the 'values' - // and start capturing strings. - foreach (var stringSegment in pathTokenizer) + var pathSegment = i >= RoutePattern.PathSegments.Count ? null : RoutePattern.PathSegments[i]; + if (pathSegment == null && stringSegment.Length > 0) { - if (stringSegment.Length == 0) - { - return false; - } - - var pathSegment = i >= RoutePattern.PathSegments.Count ? null : RoutePattern.PathSegments[i]; - if (pathSegment == null && stringSegment.Length > 0) - { - // If pathSegment is null, then we're out of route segments. All we can match is the empty - // string. - return false; - } - else if (pathSegment.IsSimple && pathSegment.Parts[0] is RoutePatternParameterPart parameter && parameter.IsCatchAll) - { - // Nothing to validate for a catch-all - it can match any string, including the empty string. - // - // Also, a catch-all has to be the last part, so we're done. - break; - } - if (!TryMatchLiterals(i++, stringSegment, pathSegment)) - { - return false; - } + // If pathSegment is null, then we're out of route segments. All we can match is the empty + // string. + return false; } - - for (; i < RoutePattern.PathSegments.Count; i++) + else if (pathSegment.IsSimple && pathSegment.Parts[0] is RoutePatternParameterPart parameter && parameter.IsCatchAll) { - // We've matched the request path so far, but still have remaining route segments. These need - // to be all single-part parameter segments with default values or else they won't match. - var pathSegment = RoutePattern.PathSegments[i]; - Debug.Assert(pathSegment != null); + // Nothing to validate for a catch-all - it can match any string, including the empty string. + // + // Also, a catch-all has to be the last part, so we're done. + break; + } + if (!TryMatchLiterals(i++, stringSegment, pathSegment)) + { + return false; + } + } - if (!pathSegment.IsSimple) - { - // If the segment is a complex segment, it MUST contain literals, and we've parsed the full - // path so far, so it can't match. - return false; - } + for (; i < RoutePattern.PathSegments.Count; i++) + { + // We've matched the request path so far, but still have remaining route segments. These need + // to be all single-part parameter segments with default values or else they won't match. + var pathSegment = RoutePattern.PathSegments[i]; + Debug.Assert(pathSegment != null); - var part = pathSegment.Parts[0]; - if (part.IsLiteral || part.IsSeparator) - { - // If the segment is a simple literal - which need the URL to provide a value, so we don't match. - return false; - } + if (!pathSegment.IsSimple) + { + // If the segment is a complex segment, it MUST contain literals, and we've parsed the full + // path so far, so it can't match. + return false; + } - var parameter = (RoutePatternParameterPart)part; - if (parameter.IsCatchAll) - { - // Nothing to validate for a catch-all - it can match any string, including the empty string. - // - // Also, a catch-all has to be the last part, so we're done. - break; - } + var part = pathSegment.Parts[0]; + if (part.IsLiteral || part.IsSeparator) + { + // If the segment is a simple literal - which need the URL to provide a value, so we don't match. + return false; + } - // If we get here, this is a simple segment with a parameter. We need it to be optional, or for the - // defaults to have a value. - if (!_hasDefaultValue[i] && !parameter.IsOptional) - { - // There's no default for this (non-optional) parameter so it can't match. - return false; - } + var parameter = (RoutePatternParameterPart)part; + if (parameter.IsCatchAll) + { + // Nothing to validate for a catch-all - it can match any string, including the empty string. + // + // Also, a catch-all has to be the last part, so we're done. + break; } - // At this point we've very likely got a match, so start capturing values for real. - i = 0; - foreach (var requestSegment in pathTokenizer) + // If we get here, this is a simple segment with a parameter. We need it to be optional, or for the + // defaults to have a value. + if (!_hasDefaultValue[i] && !parameter.IsOptional) { - var pathSegment = RoutePattern.PathSegments[i++]; - if (SavePathSegmentsAsValues(i, values, requestSegment, pathSegment)) - { - break; - } - if (!pathSegment.IsSimple) + // There's no default for this (non-optional) parameter so it can't match. + return false; + } + } + + // At this point we've very likely got a match, so start capturing values for real. + i = 0; + foreach (var requestSegment in pathTokenizer) + { + var pathSegment = RoutePattern.PathSegments[i++]; + if (SavePathSegmentsAsValues(i, values, requestSegment, pathSegment)) + { + break; + } + if (!pathSegment.IsSimple) + { + if (!MatchComplexSegment(pathSegment, requestSegment.AsSpan(), values)) { - if (!MatchComplexSegment(pathSegment, requestSegment.AsSpan(), values)) - { - return false; - } + return false; } } + } - for (; i < RoutePattern.PathSegments.Count; i++) - { - // We've matched the request path so far, but still have remaining route segments. We already know these - // are simple parameters that either have a default, or don't need to produce a value. - var pathSegment = RoutePattern.PathSegments[i]; - Debug.Assert(pathSegment != null); - Debug.Assert(pathSegment.IsSimple); + for (; i < RoutePattern.PathSegments.Count; i++) + { + // We've matched the request path so far, but still have remaining route segments. We already know these + // are simple parameters that either have a default, or don't need to produce a value. + var pathSegment = RoutePattern.PathSegments[i]; + Debug.Assert(pathSegment != null); + Debug.Assert(pathSegment.IsSimple); - var part = pathSegment.Parts[0]; - Debug.Assert(part.IsParameter); + var part = pathSegment.Parts[0]; + Debug.Assert(part.IsParameter); - // It's ok for a catch-all to produce a null value - if (part is RoutePatternParameterPart parameter && (parameter.IsCatchAll || _hasDefaultValue[i])) + // It's ok for a catch-all to produce a null value + if (part is RoutePatternParameterPart parameter && (parameter.IsCatchAll || _hasDefaultValue[i])) + { + // Don't replace an existing value with a null. + var defaultValue = _defaultValues[i]; + if (defaultValue != null || !values.ContainsKey(parameter.Name)) { - // Don't replace an existing value with a null. - var defaultValue = _defaultValues[i]; - if (defaultValue != null || !values.ContainsKey(parameter.Name)) - { - values[parameter.Name] = defaultValue; - } + values[parameter.Name] = defaultValue; } } + } - // Copy all remaining default values to the route data - foreach (var kvp in Defaults) - { + // Copy all remaining default values to the route data + foreach (var kvp in Defaults) + { #if RVD_TryAdd values.TryAdd(kvp.Key, kvp.Value); #else - if (!values.ContainsKey(kvp.Key)) - { - values.Add(kvp.Key, kvp.Value); - } -#endif + if (!values.ContainsKey(kvp.Key)) + { + values.Add(kvp.Key, kvp.Value); } - - return true; +#endif } - private bool TryMatchLiterals(int index, StringSegment stringSegment, RoutePatternPathSegment pathSegment) + return true; + } + + private bool TryMatchLiterals(int index, StringSegment stringSegment, RoutePatternPathSegment pathSegment) + { + if (pathSegment.IsSimple && !pathSegment.Parts[0].IsParameter) { - if (pathSegment.IsSimple && !pathSegment.Parts[0].IsParameter) + // This is a literal segment, so we need to match the text, or the route isn't a match. + if (pathSegment.Parts[0].IsLiteral) { - // This is a literal segment, so we need to match the text, or the route isn't a match. - if (pathSegment.Parts[0].IsLiteral) - { - var part = (RoutePatternLiteralPart)pathSegment.Parts[0]; + var part = (RoutePatternLiteralPart)pathSegment.Parts[0]; - if (!stringSegment.Equals(part.Content, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - else + if (!stringSegment.Equals(part.Content, StringComparison.OrdinalIgnoreCase)) { - var part = (RoutePatternSeparatorPart)pathSegment.Parts[0]; - - if (!stringSegment.Equals(part.Content, StringComparison.OrdinalIgnoreCase)) - { - return false; - } + return false; } } - else if (pathSegment.IsSimple && pathSegment.Parts[0].IsParameter) + else { - // For a parameter, validate that it's a has some length, or we have a default, or it's optional. - var part = (RoutePatternParameterPart)pathSegment.Parts[0]; - if (stringSegment.Length == 0 && - !_hasDefaultValue[index] && - !part.IsOptional) + var part = (RoutePatternSeparatorPart)pathSegment.Parts[0]; + + if (!stringSegment.Equals(part.Content, StringComparison.OrdinalIgnoreCase)) { - // There's no value for this parameter, the route can't match. return false; } } - else + } + else if (pathSegment.IsSimple && pathSegment.Parts[0].IsParameter) + { + // For a parameter, validate that it's a has some length, or we have a default, or it's optional. + var part = (RoutePatternParameterPart)pathSegment.Parts[0]; + if (stringSegment.Length == 0 && + !_hasDefaultValue[index] && + !part.IsOptional) { - Debug.Assert(!pathSegment.IsSimple); - // Don't attempt to validate a complex segment at this point other than being non-empty, - // do it in the second pass. + // There's no value for this parameter, the route can't match. + return false; } - return true; } + else + { + Debug.Assert(!pathSegment.IsSimple); + // Don't attempt to validate a complex segment at this point other than being non-empty, + // do it in the second pass. + } + return true; + } - private bool SavePathSegmentsAsValues(int index, RouteValueDictionary values, StringSegment requestSegment, RoutePatternPathSegment pathSegment) + private bool SavePathSegmentsAsValues(int index, RouteValueDictionary values, StringSegment requestSegment, RoutePatternPathSegment pathSegment) + { + if (pathSegment.IsSimple && pathSegment.Parts[0] is RoutePatternParameterPart parameter && parameter.IsCatchAll) { - if (pathSegment.IsSimple && pathSegment.Parts[0] is RoutePatternParameterPart parameter && parameter.IsCatchAll) + // A catch-all captures til the end of the string. + var captured = requestSegment.Buffer.Substring(requestSegment.Offset); + if (captured.Length > 0) { - // A catch-all captures til the end of the string. - var captured = requestSegment.Buffer.Substring(requestSegment.Offset); - if (captured.Length > 0) - { - values[parameter.Name] = captured; - } - else - { - // It's ok for a catch-all to produce a null value, so we don't check _hasDefaultValue. - values[parameter.Name] = _defaultValues[index]; - } + values[parameter.Name] = captured; + } + else + { + // It's ok for a catch-all to produce a null value, so we don't check _hasDefaultValue. + values[parameter.Name] = _defaultValues[index]; + } - // A catch-all has to be the last part, so we're done. - return true; + // A catch-all has to be the last part, so we're done. + return true; + } + else if (pathSegment.IsSimple && pathSegment.Parts[0].IsParameter) + { + // A simple parameter captures the whole segment, or a default value if nothing was + // provided. + parameter = (RoutePatternParameterPart)pathSegment.Parts[0]; + if (requestSegment.Length > 0) + { + values[parameter.Name] = requestSegment.ToString(); } - else if (pathSegment.IsSimple && pathSegment.Parts[0].IsParameter) + else { - // A simple parameter captures the whole segment, or a default value if nothing was - // provided. - parameter = (RoutePatternParameterPart)pathSegment.Parts[0]; - if (requestSegment.Length > 0) + if (_hasDefaultValue[index]) { - values[parameter.Name] = requestSegment.ToString(); - } - else - { - if (_hasDefaultValue[index]) - { - values[parameter.Name] = _defaultValues[index]; - } + values[parameter.Name] = _defaultValues[index]; } } - return false; } + return false; + } - internal static bool MatchComplexSegment( - RoutePatternPathSegment routeSegment, - ReadOnlySpan requestSegment, - RouteValueDictionary values) + internal static bool MatchComplexSegment( + RoutePatternPathSegment routeSegment, + ReadOnlySpan requestSegment, + RouteValueDictionary values) + { + var indexOfLastSegment = routeSegment.Parts.Count - 1; + + // We match the request to the template starting at the rightmost parameter + // If the last segment of template is optional, then request can match the + // template with or without the last parameter. So we start with regular matching, + // but if it doesn't match, we start with next to last parameter. Example: + // Template: {p1}/{p2}.{p3?}. If the request is one/two.three it will match right away + // giving p3 value of three. But if the request is one/two, we start matching from the + // rightmost giving p3 the value of two, then we end up not matching the segment. + // In this case we start again from p2 to match the request and we succeed giving + // the value two to p2 + if (routeSegment.Parts[indexOfLastSegment] is RoutePatternParameterPart parameter && parameter.IsOptional && + routeSegment.Parts[indexOfLastSegment - 1].IsSeparator) { - var indexOfLastSegment = routeSegment.Parts.Count - 1; - - // We match the request to the template starting at the rightmost parameter - // If the last segment of template is optional, then request can match the - // template with or without the last parameter. So we start with regular matching, - // but if it doesn't match, we start with next to last parameter. Example: - // Template: {p1}/{p2}.{p3?}. If the request is one/two.three it will match right away - // giving p3 value of three. But if the request is one/two, we start matching from the - // rightmost giving p3 the value of two, then we end up not matching the segment. - // In this case we start again from p2 to match the request and we succeed giving - // the value two to p2 - if (routeSegment.Parts[indexOfLastSegment] is RoutePatternParameterPart parameter && parameter.IsOptional && - routeSegment.Parts[indexOfLastSegment - 1].IsSeparator) + if (MatchComplexSegmentCore(routeSegment, requestSegment, values, indexOfLastSegment)) { - if (MatchComplexSegmentCore(routeSegment, requestSegment, values, indexOfLastSegment)) - { - return true; - } - else - { - var separator = (RoutePatternSeparatorPart)routeSegment.Parts[indexOfLastSegment - 1]; - if (requestSegment.EndsWith( - separator.Content, - StringComparison.OrdinalIgnoreCase)) - return false; - - return MatchComplexSegmentCore( - routeSegment, - requestSegment, - values, - indexOfLastSegment - 2); - } + return true; } else { - return MatchComplexSegmentCore(routeSegment, requestSegment, values, indexOfLastSegment); + var separator = (RoutePatternSeparatorPart)routeSegment.Parts[indexOfLastSegment - 1]; + if (requestSegment.EndsWith( + separator.Content, + StringComparison.OrdinalIgnoreCase)) + return false; + + return MatchComplexSegmentCore( + routeSegment, + requestSegment, + values, + indexOfLastSegment - 2); } } - - private static bool MatchComplexSegmentCore( - RoutePatternPathSegment routeSegment, - ReadOnlySpan requestSegment, - RouteValueDictionary values, - int indexOfLastSegmentUsed) + else { - Debug.Assert(routeSegment != null); - Debug.Assert(routeSegment.Parts.Count > 1); + return MatchComplexSegmentCore(routeSegment, requestSegment, values, indexOfLastSegment); + } + } + + private static bool MatchComplexSegmentCore( + RoutePatternPathSegment routeSegment, + ReadOnlySpan requestSegment, + RouteValueDictionary values, + int indexOfLastSegmentUsed) + { + Debug.Assert(routeSegment != null); + Debug.Assert(routeSegment.Parts.Count > 1); - // Find last literal segment and get its last index in the string - var lastIndex = requestSegment.Length; + // Find last literal segment and get its last index in the string + var lastIndex = requestSegment.Length; - RoutePatternParameterPart parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value - RoutePatternPart lastLiteral = null; // Keeps track of the left-most literal we've encountered + RoutePatternParameterPart parameterNeedsValue = null; // Keeps track of a parameter segment that is pending a value + RoutePatternPart lastLiteral = null; // Keeps track of the left-most literal we've encountered - var outValues = new RouteValueDictionary(); + var outValues = new RouteValueDictionary(); - while (indexOfLastSegmentUsed >= 0) + while (indexOfLastSegmentUsed >= 0) + { + var newLastIndex = lastIndex; + + var part = routeSegment.Parts[indexOfLastSegmentUsed]; + if (part.IsParameter) { - var newLastIndex = lastIndex; + // Hold on to the parameter so that we can fill it in when we locate the next literal + parameterNeedsValue = (RoutePatternParameterPart)part; + } + else + { + Debug.Assert(part.IsLiteral || part.IsSeparator); + lastLiteral = part; - var part = routeSegment.Parts[indexOfLastSegmentUsed]; - if (part.IsParameter) + var startIndex = lastIndex; + // If we have a pending parameter subsegment, we must leave at least one character for that + if (parameterNeedsValue != null) { - // Hold on to the parameter so that we can fill it in when we locate the next literal - parameterNeedsValue = (RoutePatternParameterPart)part; + startIndex--; + } + + if (startIndex == 0) + { + return false; + } + + int indexOfLiteral; + if (part.IsLiteral) + { + var literal = (RoutePatternLiteralPart)part; + indexOfLiteral = requestSegment.Slice(0, startIndex).LastIndexOf( + literal.Content, + StringComparison.OrdinalIgnoreCase); } else { - Debug.Assert(part.IsLiteral || part.IsSeparator); - lastLiteral = part; + var literal = (RoutePatternSeparatorPart)part; + indexOfLiteral = requestSegment.Slice(0, startIndex).LastIndexOf( + literal.Content, + StringComparison.OrdinalIgnoreCase); + } - var startIndex = lastIndex; - // If we have a pending parameter subsegment, we must leave at least one character for that - if (parameterNeedsValue != null) - { - startIndex--; - } + if (indexOfLiteral == -1) + { + // If we couldn't find this literal index, this segment cannot match + return false; + } - if (startIndex == 0) + // If the first subsegment is a literal, it must match at the right-most extent of the request URI. + // Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/". + // This check is related to the check we do at the very end of this function. + if (indexOfLastSegmentUsed == (routeSegment.Parts.Count - 1)) + { + if (part is RoutePatternLiteralPart literal && ((indexOfLiteral + literal.Content.Length) != requestSegment.Length)) { return false; } - - int indexOfLiteral; - if (part.IsLiteral) - { - var literal = (RoutePatternLiteralPart)part; - indexOfLiteral = requestSegment.Slice(0, startIndex).LastIndexOf( - literal.Content, - StringComparison.OrdinalIgnoreCase); - } - else + else if (part is RoutePatternSeparatorPart separator && ((indexOfLiteral + separator.Content.Length) != requestSegment.Length)) { - var literal = (RoutePatternSeparatorPart)part; - indexOfLiteral = requestSegment.Slice(0, startIndex).LastIndexOf( - literal.Content, - StringComparison.OrdinalIgnoreCase); + return false; } + } + + newLastIndex = indexOfLiteral; + } + + if ((parameterNeedsValue != null) && + (((lastLiteral != null) && !part.IsParameter) || (indexOfLastSegmentUsed == 0))) + { + // If we have a pending parameter that needs a value, grab that value + + int parameterStartIndex; + int parameterTextLength; - if (indexOfLiteral == -1) + if (lastLiteral == null) + { + if (indexOfLastSegmentUsed == 0) { - // If we couldn't find this literal index, this segment cannot match - return false; + parameterStartIndex = 0; } - - // If the first subsegment is a literal, it must match at the right-most extent of the request URI. - // Without this check if your route had "/Foo/" we'd match the request URI "/somethingFoo/". - // This check is related to the check we do at the very end of this function. - if (indexOfLastSegmentUsed == (routeSegment.Parts.Count - 1)) + else { - if (part is RoutePatternLiteralPart literal && ((indexOfLiteral + literal.Content.Length) != requestSegment.Length)) - { - return false; - } - else if (part is RoutePatternSeparatorPart separator && ((indexOfLiteral + separator.Content.Length) != requestSegment.Length)) - { - return false; - } + parameterStartIndex = newLastIndex; + Debug.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above"); } - - newLastIndex = indexOfLiteral; + parameterTextLength = lastIndex; } - - if ((parameterNeedsValue != null) && - (((lastLiteral != null) && !part.IsParameter) || (indexOfLastSegmentUsed == 0))) + else { - // If we have a pending parameter that needs a value, grab that value - - int parameterStartIndex; - int parameterTextLength; - - if (lastLiteral == null) + // If we're getting a value for a parameter that is somewhere in the middle of the segment + if ((indexOfLastSegmentUsed == 0) && (part.IsParameter)) { - if (indexOfLastSegmentUsed == 0) - { - parameterStartIndex = 0; - } - else - { - parameterStartIndex = newLastIndex; - Debug.Assert(false, "indexOfLastSegementUsed should always be 0 from the check above"); - } + parameterStartIndex = 0; parameterTextLength = lastIndex; } else { - // If we're getting a value for a parameter that is somewhere in the middle of the segment - if ((indexOfLastSegmentUsed == 0) && (part.IsParameter)) + if (lastLiteral.IsLiteral) { - parameterStartIndex = 0; - parameterTextLength = lastIndex; + var literal = (RoutePatternLiteralPart)lastLiteral; + parameterStartIndex = newLastIndex + literal.Content.Length; } else { - if (lastLiteral.IsLiteral) - { - var literal = (RoutePatternLiteralPart)lastLiteral; - parameterStartIndex = newLastIndex + literal.Content.Length; - } - else - { - var separator = (RoutePatternSeparatorPart)lastLiteral; - parameterStartIndex = newLastIndex + separator.Content.Length; - } - parameterTextLength = lastIndex - parameterStartIndex; + var separator = (RoutePatternSeparatorPart)lastLiteral; + parameterStartIndex = newLastIndex + separator.Content.Length; } + parameterTextLength = lastIndex - parameterStartIndex; } + } - var parameterValueSpan = requestSegment.Slice(parameterStartIndex, parameterTextLength); - - if (parameterValueSpan.Length == 0) - { - // If we're here that means we have a segment that contains multiple sub-segments. - // For these segments all parameters must have non-empty values. If the parameter - // has an empty value it's not a match. - return false; + var parameterValueSpan = requestSegment.Slice(parameterStartIndex, parameterTextLength); - } - else - { - // If there's a value in the segment for this parameter, use the subsegment value - outValues.Add(parameterNeedsValue.Name, new string(parameterValueSpan)); - } + if (parameterValueSpan.Length == 0) + { + // If we're here that means we have a segment that contains multiple sub-segments. + // For these segments all parameters must have non-empty values. If the parameter + // has an empty value it's not a match. + return false; - parameterNeedsValue = null; - lastLiteral = null; + } + else + { + // If there's a value in the segment for this parameter, use the subsegment value + outValues.Add(parameterNeedsValue.Name, new string(parameterValueSpan)); } - lastIndex = newLastIndex; - indexOfLastSegmentUsed--; + parameterNeedsValue = null; + lastLiteral = null; } - // If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of - // the string since the parameter will have consumed all the remaining text anyway. If the last subsegment - // is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching - // the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire* - // request URI in order for it to be a match. - // This check is related to the check we do earlier in this function for LiteralSubsegments. - if (lastIndex == 0 || routeSegment.Parts[0].IsParameter) - { - foreach (var item in outValues) - { - values[item.Key] = item.Value; - } + lastIndex = newLastIndex; + indexOfLastSegmentUsed--; + } - return true; + // If the last subsegment is a parameter, it's OK that we didn't parse all the way to the left extent of + // the string since the parameter will have consumed all the remaining text anyway. If the last subsegment + // is a literal then we *must* have consumed the entire text in that literal. Otherwise we end up matching + // the route "Foo" to the request URI "somethingFoo". Thus we have to check that we parsed the *entire* + // request URI in order for it to be a match. + // This check is related to the check we do earlier in this function for LiteralSubsegments. + if (lastIndex == 0 || routeSegment.Parts[0].IsParameter) + { + foreach (var item in outValues) + { + values[item.Key] = item.Value; } - return false; + return true; } + + return false; } } diff --git a/src/Http/Routing/src/Patterns/RoutePatternParameterKind.cs b/src/Http/Routing/src/Patterns/RoutePatternParameterKind.cs index 6fbb0fa981..8b62f35abb 100644 --- a/src/Http/Routing/src/Patterns/RoutePatternParameterKind.cs +++ b/src/Http/Routing/src/Patterns/RoutePatternParameterKind.cs @@ -1,27 +1,26 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +/// +/// Defines the kinds of instances. +/// +public enum RoutePatternParameterKind { /// - /// Defines the kinds of instances. + /// The of a standard parameter + /// without optional or catch all behavior. /// - public enum RoutePatternParameterKind - { - /// - /// The of a standard parameter - /// without optional or catch all behavior. - /// - Standard, + Standard, - /// - /// The of an optional parameter. - /// - Optional, + /// + /// The of an optional parameter. + /// + Optional, - /// - /// The of a catch-all parameter. - /// - CatchAll, - } + /// + /// The of a catch-all parameter. + /// + CatchAll, } diff --git a/src/Http/Routing/src/Patterns/RoutePatternParameterPart.cs b/src/Http/Routing/src/Patterns/RoutePatternParameterPart.cs index 607b7bcf01..b8d80e3ac1 100644 --- a/src/Http/Routing/src/Patterns/RoutePatternParameterPart.cs +++ b/src/Http/Routing/src/Patterns/RoutePatternParameterPart.cs @@ -5,113 +5,112 @@ using System.Collections.Generic; using System.Diagnostics; using System.Text; -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +/// +/// Represents a parameter part in a route pattern. Instances of +/// are immutable. +/// +[DebuggerDisplay("{DebuggerToString()}")] +public sealed class RoutePatternParameterPart : RoutePatternPart { + internal RoutePatternParameterPart( + string parameterName, + object? @default, + RoutePatternParameterKind parameterKind, + RoutePatternParameterPolicyReference[] parameterPolicies) + : this(parameterName, @default, parameterKind, parameterPolicies, encodeSlashes: true) + { + } + + internal RoutePatternParameterPart( + string parameterName, + object? @default, + RoutePatternParameterKind parameterKind, + RoutePatternParameterPolicyReference[] parameterPolicies, + bool encodeSlashes) + : base(RoutePatternPartKind.Parameter) + { + // See #475 - this code should have some asserts, but it can't because of the design of RouteParameterParser. + + Name = parameterName; + Default = @default; + ParameterKind = parameterKind; + ParameterPolicies = parameterPolicies; + EncodeSlashes = encodeSlashes; + } + /// - /// Represents a parameter part in a route pattern. Instances of - /// are immutable. + /// Gets the list of parameter policies associated with this parameter. /// - [DebuggerDisplay("{DebuggerToString()}")] - public sealed class RoutePatternParameterPart : RoutePatternPart - { - internal RoutePatternParameterPart( - string parameterName, - object? @default, - RoutePatternParameterKind parameterKind, - RoutePatternParameterPolicyReference[] parameterPolicies) - : this(parameterName, @default, parameterKind, parameterPolicies, encodeSlashes: true) - { - } + public IReadOnlyList ParameterPolicies { get; } - internal RoutePatternParameterPart( - string parameterName, - object? @default, - RoutePatternParameterKind parameterKind, - RoutePatternParameterPolicyReference[] parameterPolicies, - bool encodeSlashes) - : base(RoutePatternPartKind.Parameter) - { - // See #475 - this code should have some asserts, but it can't because of the design of RouteParameterParser. + /// + /// Gets the value indicating if slashes in current parameter's value should be encoded. + /// + public bool EncodeSlashes { get; } - Name = parameterName; - Default = @default; - ParameterKind = parameterKind; - ParameterPolicies = parameterPolicies; - EncodeSlashes = encodeSlashes; - } + /// + /// Gets the default value of this route parameter. May be null. + /// + public object? Default { get; } - /// - /// Gets the list of parameter policies associated with this parameter. - /// - public IReadOnlyList ParameterPolicies { get; } - - /// - /// Gets the value indicating if slashes in current parameter's value should be encoded. - /// - public bool EncodeSlashes { get; } - - /// - /// Gets the default value of this route parameter. May be null. - /// - public object? Default { get; } - - /// - /// Returns true if this part is a catch-all parameter. - /// Otherwise returns false. - /// - public bool IsCatchAll => ParameterKind == RoutePatternParameterKind.CatchAll; - - /// - /// Returns true if this part is an optional parameter. - /// Otherwise returns false. - /// - public bool IsOptional => ParameterKind == RoutePatternParameterKind.Optional; - - /// - /// Gets the of this parameter. - /// - public RoutePatternParameterKind ParameterKind { get; } - - /// - /// Gets the parameter name. - /// - public string Name { get; } - - internal override string DebuggerToString() - { - var builder = new StringBuilder(); - builder.Append('{'); + /// + /// Returns true if this part is a catch-all parameter. + /// Otherwise returns false. + /// + public bool IsCatchAll => ParameterKind == RoutePatternParameterKind.CatchAll; - if (IsCatchAll) - { - builder.Append('*'); - if (!EncodeSlashes) - { - builder.Append('*'); - } - } + /// + /// Returns true if this part is an optional parameter. + /// Otherwise returns false. + /// + public bool IsOptional => ParameterKind == RoutePatternParameterKind.Optional; - builder.Append(Name); + /// + /// Gets the of this parameter. + /// + public RoutePatternParameterKind ParameterKind { get; } - foreach (var constraint in ParameterPolicies) - { - builder.Append(':'); - builder.Append(constraint.ParameterPolicy); - } + /// + /// Gets the parameter name. + /// + public string Name { get; } - if (Default != null) - { - builder.Append('='); - builder.Append(Default); - } + internal override string DebuggerToString() + { + var builder = new StringBuilder(); + builder.Append('{'); - if (IsOptional) + if (IsCatchAll) + { + builder.Append('*'); + if (!EncodeSlashes) { - builder.Append('?'); + builder.Append('*'); } + } + + builder.Append(Name); - builder.Append('}'); - return builder.ToString(); + foreach (var constraint in ParameterPolicies) + { + builder.Append(':'); + builder.Append(constraint.ParameterPolicy); } + + if (Default != null) + { + builder.Append('='); + builder.Append(Default); + } + + if (IsOptional) + { + builder.Append('?'); + } + + builder.Append('}'); + return builder.ToString(); } } diff --git a/src/Http/Routing/src/Patterns/RoutePatternParameterPolicyReference.cs b/src/Http/Routing/src/Patterns/RoutePatternParameterPolicyReference.cs index 637907146f..f9ca6b1f5a 100644 --- a/src/Http/Routing/src/Patterns/RoutePatternParameterPolicyReference.cs +++ b/src/Http/Routing/src/Patterns/RoutePatternParameterPolicyReference.cs @@ -4,38 +4,37 @@ using System.Diagnostics; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +/// +/// The parsed representation of a policy in a parameter. Instances +/// of are immutable. +/// +[DebuggerDisplay("{DebuggerToString()}")] +public sealed class RoutePatternParameterPolicyReference { - /// - /// The parsed representation of a policy in a parameter. Instances - /// of are immutable. - /// - [DebuggerDisplay("{DebuggerToString()}")] - public sealed class RoutePatternParameterPolicyReference + internal RoutePatternParameterPolicyReference(string content) { - internal RoutePatternParameterPolicyReference(string content) - { - Content = content; - } + Content = content; + } - internal RoutePatternParameterPolicyReference(IParameterPolicy parameterPolicy) - { - ParameterPolicy = parameterPolicy; - } + internal RoutePatternParameterPolicyReference(IParameterPolicy parameterPolicy) + { + ParameterPolicy = parameterPolicy; + } - /// - /// Gets the constraint text. - /// - public string? Content { get; } + /// + /// Gets the constraint text. + /// + public string? Content { get; } - /// - /// Gets a pre-existing that was used to construct this reference. - /// - public IParameterPolicy? ParameterPolicy { get; } + /// + /// Gets a pre-existing that was used to construct this reference. + /// + public IParameterPolicy? ParameterPolicy { get; } - private string? DebuggerToString() - { - return Content; - } + private string? DebuggerToString() + { + return Content; } } diff --git a/src/Http/Routing/src/Patterns/RoutePatternParser.cs b/src/Http/Routing/src/Patterns/RoutePatternParser.cs index 26e9f69b4a..8b3e4760ec 100644 --- a/src/Http/Routing/src/Patterns/RoutePatternParser.cs +++ b/src/Http/Routing/src/Patterns/RoutePatternParser.cs @@ -8,568 +8,567 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +internal static class RoutePatternParser { - internal static class RoutePatternParser + private const char Separator = '/'; + private const char OpenBrace = '{'; + private const char CloseBrace = '}'; + private const char QuestionMark = '?'; + private const char Asterisk = '*'; + private const string PeriodString = "."; + + internal static readonly char[] InvalidParameterNameChars = new char[] { - private const char Separator = '/'; - private const char OpenBrace = '{'; - private const char CloseBrace = '}'; - private const char QuestionMark = '?'; - private const char Asterisk = '*'; - private const string PeriodString = "."; - - internal static readonly char[] InvalidParameterNameChars = new char[] - { Separator, OpenBrace, CloseBrace, QuestionMark, Asterisk - }; + }; - public static RoutePattern Parse(string pattern) + public static RoutePattern Parse(string pattern) + { + if (pattern == null) { - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } - - var trimmedPattern = TrimPrefix(pattern); - - var context = new Context(trimmedPattern); - var segments = new List(); - - while (context.MoveNext()) - { - var i = context.Index; + throw new ArgumentNullException(nameof(pattern)); + } - if (context.Current == Separator) - { - // If we get here is means that there's a consecutive '/' character. - // Templates don't start with a '/' and parsing a segment consumes the separator. - throw new RoutePatternException(pattern, Resources.TemplateRoute_CannotHaveConsecutiveSeparators); - } + var trimmedPattern = TrimPrefix(pattern); - if (!ParseSegment(context, segments)) - { - throw new RoutePatternException(pattern, context.Error); - } + var context = new Context(trimmedPattern); + var segments = new List(); - // A successful parse should always result in us being at the end or at a separator. - Debug.Assert(context.AtEnd() || context.Current == Separator); + while (context.MoveNext()) + { + var i = context.Index; - if (context.Index <= i) - { - // This shouldn't happen, but we want to crash if it does. - var message = "Infinite loop detected in the parser. Please open an issue."; - throw new InvalidProgramException(message); - } + if (context.Current == Separator) + { + // If we get here is means that there's a consecutive '/' character. + // Templates don't start with a '/' and parsing a segment consumes the separator. + throw new RoutePatternException(pattern, Resources.TemplateRoute_CannotHaveConsecutiveSeparators); } - if (IsAllValid(context, segments)) + if (!ParseSegment(context, segments)) { - return RoutePatternFactory.Pattern(pattern, segments); + throw new RoutePatternException(pattern, context.Error); } - else + + // A successful parse should always result in us being at the end or at a separator. + Debug.Assert(context.AtEnd() || context.Current == Separator); + + if (context.Index <= i) { - throw new RoutePatternException(pattern, context.Error); + // This shouldn't happen, but we want to crash if it does. + var message = "Infinite loop detected in the parser. Please open an issue."; + throw new InvalidProgramException(message); } } - private static bool ParseSegment(Context context, List segments) + if (IsAllValid(context, segments)) + { + return RoutePatternFactory.Pattern(pattern, segments); + } + else { - Debug.Assert(context != null); - Debug.Assert(segments != null); + throw new RoutePatternException(pattern, context.Error); + } + } + + private static bool ParseSegment(Context context, List segments) + { + Debug.Assert(context != null); + Debug.Assert(segments != null); - var parts = new List(); + var parts = new List(); + + while (true) + { + var i = context.Index; - while (true) + if (context.Current == OpenBrace) { - var i = context.Index; + if (!context.MoveNext()) + { + // This is a dangling open-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } if (context.Current == OpenBrace) { - if (!context.MoveNext()) + // This is an 'escaped' brace in a literal, like "{{foo" + context.Back(); + if (!ParseLiteral(context, parts)) { - // This is a dangling open-brace, which is not allowed - context.Error = Resources.TemplateRoute_MismatchedParameter; return false; } - - if (context.Current == OpenBrace) - { - // This is an 'escaped' brace in a literal, like "{{foo" - context.Back(); - if (!ParseLiteral(context, parts)) - { - return false; - } - } - else - { - // This is a parameter - context.Back(); - if (!ParseParameter(context, parts)) - { - return false; - } - } } else { - if (!ParseLiteral(context, parts)) + // This is a parameter + context.Back(); + if (!ParseParameter(context, parts)) { return false; } } - - if (context.Current == Separator || context.AtEnd()) - { - // We've reached the end of the segment - break; - } - - if (context.Index <= i) + } + else + { + if (!ParseLiteral(context, parts)) { - // This shouldn't happen, but we want to crash if it does. - var message = "Infinite loop detected in the parser. Please open an issue."; - throw new InvalidProgramException(message); + return false; } } - if (IsSegmentValid(context, parts)) + if (context.Current == Separator || context.AtEnd()) { - segments.Add(new RoutePatternPathSegment(parts)); - return true; + // We've reached the end of the segment + break; } - else + + if (context.Index <= i) { - return false; + // This shouldn't happen, but we want to crash if it does. + var message = "Infinite loop detected in the parser. Please open an issue."; + throw new InvalidProgramException(message); } } - private static bool ParseParameter(Context context, List parts) + if (IsSegmentValid(context, parts)) { - Debug.Assert(context.Current == OpenBrace); - context.Mark(); + segments.Add(new RoutePatternPathSegment(parts)); + return true; + } + else + { + return false; + } + } + + private static bool ParseParameter(Context context, List parts) + { + Debug.Assert(context.Current == OpenBrace); + context.Mark(); - context.MoveNext(); + context.MoveNext(); - while (true) + while (true) + { + if (context.Current == OpenBrace) { - if (context.Current == OpenBrace) + // This is an open brace inside of a parameter, it has to be escaped + if (context.MoveNext()) { - // This is an open brace inside of a parameter, it has to be escaped - if (context.MoveNext()) - { - if (context.Current != OpenBrace) - { - // If we see something like "{p1:regex(^\d{3", we will come here. - context.Error = Resources.TemplateRoute_UnescapedBrace; - return false; - } - } - else + if (context.Current != OpenBrace) { - // This is a dangling open-brace, which is not allowed - // Example: "{p1:regex(^\d{" - context.Error = Resources.TemplateRoute_MismatchedParameter; + // If we see something like "{p1:regex(^\d{3", we will come here. + context.Error = Resources.TemplateRoute_UnescapedBrace; return false; } } - else if (context.Current == CloseBrace) - { - // When we encounter Closed brace here, it either means end of the parameter or it is a closed - // brace in the parameter, in that case it needs to be escaped. - // Example: {p1:regex(([}}])\w+}. First pair is escaped one and last marks end of the parameter - if (!context.MoveNext()) - { - // This is the end of the string -and we have a valid parameter - break; - } - - if (context.Current == CloseBrace) - { - // This is an 'escaped' brace in a parameter name - } - else - { - // This is the end of the parameter - break; - } - } - - if (!context.MoveNext()) + else { // This is a dangling open-brace, which is not allowed + // Example: "{p1:regex(^\d{" context.Error = Resources.TemplateRoute_MismatchedParameter; return false; } } + else if (context.Current == CloseBrace) + { + // When we encounter Closed brace here, it either means end of the parameter or it is a closed + // brace in the parameter, in that case it needs to be escaped. + // Example: {p1:regex(([}}])\w+}. First pair is escaped one and last marks end of the parameter + if (!context.MoveNext()) + { + // This is the end of the string -and we have a valid parameter + break; + } + + if (context.Current == CloseBrace) + { + // This is an 'escaped' brace in a parameter name + } + else + { + // This is the end of the parameter + break; + } + } - var text = context.Capture(); - if (text == "{}") + if (!context.MoveNext()) { - context.Error = Resources.FormatTemplateRoute_InvalidParameterName(string.Empty); + // This is a dangling open-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; return false; } + } - var inside = text.Substring(1, text.Length - 2); - var decoded = inside.Replace("}}", "}").Replace("{{", "{"); + var text = context.Capture(); + if (text == "{}") + { + context.Error = Resources.FormatTemplateRoute_InvalidParameterName(string.Empty); + return false; + } - // At this point, we need to parse the raw name for inline constraint, - // default values and optional parameters. - var templatePart = RouteParameterParser.ParseRouteParameter(decoded); + var inside = text.Substring(1, text.Length - 2); + var decoded = inside.Replace("}}", "}").Replace("{{", "{"); - // See #475 - this is here because InlineRouteParameterParser can't return errors - if (decoded.StartsWith("*", StringComparison.Ordinal) && decoded.EndsWith("?", StringComparison.Ordinal)) - { - context.Error = Resources.TemplateRoute_CatchAllCannotBeOptional; - return false; - } + // At this point, we need to parse the raw name for inline constraint, + // default values and optional parameters. + var templatePart = RouteParameterParser.ParseRouteParameter(decoded); - if (templatePart.IsOptional && templatePart.Default != null) - { - // Cannot be optional and have a default value. - // The only way to declare an optional parameter is to have a ? at the end, - // hence we cannot have both default value and optional parameter within the template. - // A workaround is to add it as a separate entry in the defaults argument. - context.Error = Resources.TemplateRoute_OptionalCannotHaveDefaultValue; - return false; - } + // See #475 - this is here because InlineRouteParameterParser can't return errors + if (decoded.StartsWith("*", StringComparison.Ordinal) && decoded.EndsWith("?", StringComparison.Ordinal)) + { + context.Error = Resources.TemplateRoute_CatchAllCannotBeOptional; + return false; + } - var parameterName = templatePart.Name; - if (IsValidParameterName(context, parameterName)) - { - parts.Add(templatePart); - return true; - } - else - { - return false; - } + if (templatePart.IsOptional && templatePart.Default != null) + { + // Cannot be optional and have a default value. + // The only way to declare an optional parameter is to have a ? at the end, + // hence we cannot have both default value and optional parameter within the template. + // A workaround is to add it as a separate entry in the defaults argument. + context.Error = Resources.TemplateRoute_OptionalCannotHaveDefaultValue; + return false; } - private static bool ParseLiteral(Context context, List parts) + var parameterName = templatePart.Name; + if (IsValidParameterName(context, parameterName)) { - context.Mark(); + parts.Add(templatePart); + return true; + } + else + { + return false; + } + } + + private static bool ParseLiteral(Context context, List parts) + { + context.Mark(); - while (true) + while (true) + { + if (context.Current == Separator) { - if (context.Current == Separator) + // End of the segment + break; + } + else if (context.Current == OpenBrace) + { + if (!context.MoveNext()) { - // End of the segment - break; + // This is a dangling open-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; } - else if (context.Current == OpenBrace) - { - if (!context.MoveNext()) - { - // This is a dangling open-brace, which is not allowed - context.Error = Resources.TemplateRoute_MismatchedParameter; - return false; - } - if (context.Current == OpenBrace) - { - // This is an 'escaped' brace in a literal, like "{{foo" - keep going. - } - else - { - // We've just seen the start of a parameter, so back up. - context.Back(); - break; - } - } - else if (context.Current == CloseBrace) + if (context.Current == OpenBrace) { - if (!context.MoveNext()) - { - // This is a dangling close-brace, which is not allowed - context.Error = Resources.TemplateRoute_MismatchedParameter; - return false; - } - - if (context.Current == CloseBrace) - { - // This is an 'escaped' brace in a literal, like "{{foo" - keep going. - } - else - { - // This is an unbalanced close-brace, which is not allowed - context.Error = Resources.TemplateRoute_MismatchedParameter; - return false; - } + // This is an 'escaped' brace in a literal, like "{{foo" - keep going. } - - if (!context.MoveNext()) + else { + // We've just seen the start of a parameter, so back up. + context.Back(); break; } } - - var encoded = context.Capture(); - var decoded = encoded.Replace("}}", "}").Replace("{{", "{"); - if (IsValidLiteral(context, decoded)) + else if (context.Current == CloseBrace) { - parts.Add(RoutePatternFactory.LiteralPart(decoded)); - return true; + if (!context.MoveNext()) + { + // This is a dangling close-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } + + if (context.Current == CloseBrace) + { + // This is an 'escaped' brace in a literal, like "{{foo" - keep going. + } + else + { + // This is an unbalanced close-brace, which is not allowed + context.Error = Resources.TemplateRoute_MismatchedParameter; + return false; + } } - else + + if (!context.MoveNext()) { - return false; + break; } } - private static bool IsAllValid(Context context, List segments) + var encoded = context.Capture(); + var decoded = encoded.Replace("}}", "}").Replace("{{", "{"); + if (IsValidLiteral(context, decoded)) { - // A catch-all parameter must be the last part of the last segment - for (var i = 0; i < segments.Count; i++) - { - var segment = segments[i]; - for (var j = 0; j < segment.Parts.Count; j++) - { - var part = segment.Parts[j]; - if (part is RoutePatternParameterPart parameter - && parameter.IsCatchAll && - (i != segments.Count - 1 || j != segment.Parts.Count - 1)) - { - context.Error = Resources.TemplateRoute_CatchAllMustBeLast; - return false; - } - } - } - + parts.Add(RoutePatternFactory.LiteralPart(decoded)); return true; } + else + { + return false; + } + } - private static bool IsSegmentValid(Context context, List parts) + private static bool IsAllValid(Context context, List segments) + { + // A catch-all parameter must be the last part of the last segment + for (var i = 0; i < segments.Count; i++) { - // If a segment has multiple parts, then it can't contain a catch all. - for (var i = 0; i < parts.Count; i++) + var segment = segments[i]; + for (var j = 0; j < segment.Parts.Count; j++) { - var part = parts[i]; - if (part is RoutePatternParameterPart parameter && parameter.IsCatchAll && parts.Count > 1) + var part = segment.Parts[j]; + if (part is RoutePatternParameterPart parameter + && parameter.IsCatchAll && + (i != segments.Count - 1 || j != segment.Parts.Count - 1)) { - context.Error = Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment; + context.Error = Resources.TemplateRoute_CatchAllMustBeLast; return false; } } + } - // if a segment has multiple parts, then only the last one parameter can be optional - // if it is following a optional separator. - for (var i = 0; i < parts.Count; i++) + return true; + } + + private static bool IsSegmentValid(Context context, List parts) + { + // If a segment has multiple parts, then it can't contain a catch all. + for (var i = 0; i < parts.Count; i++) + { + var part = parts[i]; + if (part is RoutePatternParameterPart parameter && parameter.IsCatchAll && parts.Count > 1) { - var part = parts[i]; + context.Error = Resources.TemplateRoute_CannotHaveCatchAllInMultiSegment; + return false; + } + } + + // if a segment has multiple parts, then only the last one parameter can be optional + // if it is following a optional separator. + for (var i = 0; i < parts.Count; i++) + { + var part = parts[i]; - if (part is RoutePatternParameterPart parameter && parameter.IsOptional && parts.Count > 1) + if (part is RoutePatternParameterPart parameter && parameter.IsOptional && parts.Count > 1) + { + // This optional parameter is the last part in the segment + if (i == parts.Count - 1) { - // This optional parameter is the last part in the segment - if (i == parts.Count - 1) + var previousPart = parts[i - 1]; + + if (!previousPart.IsLiteral && !previousPart.IsSeparator) { - var previousPart = parts[i - 1]; - - if (!previousPart.IsLiteral && !previousPart.IsSeparator) - { - // The optional parameter is preceded by something that is not a literal or separator - // Example of error message: - // "In the segment '{RouteValue}{param?}', the optional parameter 'param' is preceded - // by an invalid segment '{RouteValue}'. Only a period (.) can precede an optional parameter. - context.Error = Resources.FormatTemplateRoute_OptionalParameterCanbBePrecededByPeriod( - RoutePatternPathSegment.DebuggerToString(parts), - parameter.Name, - parts[i - 1].DebuggerToString()); - - return false; - } - else if (previousPart is RoutePatternLiteralPart literal && literal.Content != PeriodString) - { - // The optional parameter is preceded by a literal other than period. - // Example of error message: - // "In the segment '{RouteValue}-{param?}', the optional parameter 'param' is preceded - // by an invalid segment '-'. Only a period (.) can precede an optional parameter. - context.Error = Resources.FormatTemplateRoute_OptionalParameterCanbBePrecededByPeriod( - RoutePatternPathSegment.DebuggerToString(parts), - parameter.Name, - parts[i - 1].DebuggerToString()); - - return false; - } - - parts[i - 1] = RoutePatternFactory.SeparatorPart(((RoutePatternLiteralPart)previousPart).Content); + // The optional parameter is preceded by something that is not a literal or separator + // Example of error message: + // "In the segment '{RouteValue}{param?}', the optional parameter 'param' is preceded + // by an invalid segment '{RouteValue}'. Only a period (.) can precede an optional parameter. + context.Error = Resources.FormatTemplateRoute_OptionalParameterCanbBePrecededByPeriod( + RoutePatternPathSegment.DebuggerToString(parts), + parameter.Name, + parts[i - 1].DebuggerToString()); + + return false; } - else + else if (previousPart is RoutePatternLiteralPart literal && literal.Content != PeriodString) { - // This optional parameter is not the last one in the segment - // Example: - // An optional parameter must be at the end of the segment. In the segment '{RouteValue?})', - // optional parameter 'RouteValue' is followed by ')' - context.Error = Resources.FormatTemplateRoute_OptionalParameterHasTobeTheLast( + // The optional parameter is preceded by a literal other than period. + // Example of error message: + // "In the segment '{RouteValue}-{param?}', the optional parameter 'param' is preceded + // by an invalid segment '-'. Only a period (.) can precede an optional parameter. + context.Error = Resources.FormatTemplateRoute_OptionalParameterCanbBePrecededByPeriod( RoutePatternPathSegment.DebuggerToString(parts), parameter.Name, - parts[i + 1].DebuggerToString()); + parts[i - 1].DebuggerToString()); return false; } - } - } - // A segment cannot contain two consecutive parameters - var isLastSegmentParameter = false; - for (var i = 0; i < parts.Count; i++) - { - var part = parts[i]; - if (part.IsParameter && isLastSegmentParameter) + parts[i - 1] = RoutePatternFactory.SeparatorPart(((RoutePatternLiteralPart)previousPart).Content); + } + else { - context.Error = Resources.TemplateRoute_CannotHaveConsecutiveParameters; + // This optional parameter is not the last one in the segment + // Example: + // An optional parameter must be at the end of the segment. In the segment '{RouteValue?})', + // optional parameter 'RouteValue' is followed by ')' + context.Error = Resources.FormatTemplateRoute_OptionalParameterHasTobeTheLast( + RoutePatternPathSegment.DebuggerToString(parts), + parameter.Name, + parts[i + 1].DebuggerToString()); + return false; } - - isLastSegmentParameter = part.IsParameter; } - - return true; } - private static bool IsValidParameterName(Context context, string parameterName) + // A segment cannot contain two consecutive parameters + var isLastSegmentParameter = false; + for (var i = 0; i < parts.Count; i++) { - if (parameterName.Length == 0 || parameterName.IndexOfAny(InvalidParameterNameChars) >= 0) + var part = parts[i]; + if (part.IsParameter && isLastSegmentParameter) { - context.Error = Resources.FormatTemplateRoute_InvalidParameterName(parameterName); + context.Error = Resources.TemplateRoute_CannotHaveConsecutiveParameters; return false; } - if (!context.ParameterNames.Add(parameterName)) - { - context.Error = Resources.FormatTemplateRoute_RepeatedParameter(parameterName); - return false; - } + isLastSegmentParameter = part.IsParameter; + } - return true; + return true; + } + + private static bool IsValidParameterName(Context context, string parameterName) + { + if (parameterName.Length == 0 || parameterName.IndexOfAny(InvalidParameterNameChars) >= 0) + { + context.Error = Resources.FormatTemplateRoute_InvalidParameterName(parameterName); + return false; } - private static bool IsValidLiteral(Context context, string literal) + if (!context.ParameterNames.Add(parameterName)) { - Debug.Assert(context != null); - Debug.Assert(literal != null); + context.Error = Resources.FormatTemplateRoute_RepeatedParameter(parameterName); + return false; + } - if (literal.IndexOf(QuestionMark) != -1) - { - context.Error = Resources.FormatTemplateRoute_InvalidLiteral(literal); - return false; - } + return true; + } - return true; + private static bool IsValidLiteral(Context context, string literal) + { + Debug.Assert(context != null); + Debug.Assert(literal != null); + + if (literal.IndexOf(QuestionMark) != -1) + { + context.Error = Resources.FormatTemplateRoute_InvalidLiteral(literal); + return false; } - private static string TrimPrefix(string routePattern) + return true; + } + + private static string TrimPrefix(string routePattern) + { + if (routePattern.StartsWith("~/", StringComparison.Ordinal)) { - if (routePattern.StartsWith("~/", StringComparison.Ordinal)) - { - return routePattern.Substring(2); - } - else if (routePattern.StartsWith("/", StringComparison.Ordinal)) - { - return routePattern.Substring(1); - } - else if (routePattern.StartsWith("~", StringComparison.Ordinal)) - { - throw new RoutePatternException(routePattern, Resources.TemplateRoute_InvalidRouteTemplate); - } - return routePattern; + return routePattern.Substring(2); + } + else if (routePattern.StartsWith("/", StringComparison.Ordinal)) + { + return routePattern.Substring(1); + } + else if (routePattern.StartsWith("~", StringComparison.Ordinal)) + { + throw new RoutePatternException(routePattern, Resources.TemplateRoute_InvalidRouteTemplate); } + return routePattern; + } + + [DebuggerDisplay("{DebuggerToString()}")] + private class Context + { + private readonly string _template; + private int _index; + private int? _mark; + + private readonly HashSet _parameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); - [DebuggerDisplay("{DebuggerToString()}")] - private class Context + public Context(string template) { - private readonly string _template; - private int _index; - private int? _mark; + Debug.Assert(template != null); + _template = template; - private readonly HashSet _parameterNames = new HashSet(StringComparer.OrdinalIgnoreCase); + _index = -1; + } - public Context(string template) - { - Debug.Assert(template != null); - _template = template; + public char Current + { + get { return (_index < _template.Length && _index >= 0) ? _template[_index] : (char)0; } + } - _index = -1; - } + public int Index => _index; - public char Current - { - get { return (_index < _template.Length && _index >= 0) ? _template[_index] : (char)0; } - } + public string Error + { + get; + set; + } - public int Index => _index; + public HashSet ParameterNames + { + get { return _parameterNames; } + } - public string Error - { - get; - set; - } + public bool Back() + { + return --_index >= 0; + } - public HashSet ParameterNames - { - get { return _parameterNames; } - } + public bool AtEnd() + { + return _index >= _template.Length; + } - public bool Back() - { - return --_index >= 0; - } + public bool MoveNext() + { + return ++_index < _template.Length; + } + + public void Mark() + { + Debug.Assert(_index >= 0); + + // Index is always the index of the character *past* Current - we want to 'mark' Current. + _mark = _index; + } - public bool AtEnd() + public string Capture() + { + if (_mark.HasValue) { - return _index >= _template.Length; + var value = _template.Substring(_mark.Value, _index - _mark.Value); + _mark = null; + return value; } - - public bool MoveNext() + else { - return ++_index < _template.Length; + return null; } + } - public void Mark() + private string DebuggerToString() + { + if (_index == -1) { - Debug.Assert(_index >= 0); - - // Index is always the index of the character *past* Current - we want to 'mark' Current. - _mark = _index; + return _template; } - - public string Capture() + else if (_mark.HasValue) { - if (_mark.HasValue) - { - var value = _template.Substring(_mark.Value, _index - _mark.Value); - _mark = null; - return value; - } - else - { - return null; - } + return _template.Substring(0, _mark.Value) + + "|" + + _template.Substring(_mark.Value, _index - _mark.Value) + + "|" + + _template.Substring(_index); } - - private string DebuggerToString() + else { - if (_index == -1) - { - return _template; - } - else if (_mark.HasValue) - { - return _template.Substring(0, _mark.Value) + - "|" + - _template.Substring(_mark.Value, _index - _mark.Value) + - "|" + - _template.Substring(_index); - } - else - { - return string.Concat(_template.Substring(0, _index), "|", _template.Substring(_index)); - } + return string.Concat(_template.Substring(0, _index), "|", _template.Substring(_index)); } } } diff --git a/src/Http/Routing/src/Patterns/RoutePatternPart.cs b/src/Http/Routing/src/Patterns/RoutePatternPart.cs index 016d145545..09308938f2 100644 --- a/src/Http/Routing/src/Patterns/RoutePatternPart.cs +++ b/src/Http/Routing/src/Patterns/RoutePatternPart.cs @@ -1,42 +1,41 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +/// +/// Represents a part of a route pattern. +/// +public abstract class RoutePatternPart { - /// - /// Represents a part of a route pattern. - /// - public abstract class RoutePatternPart + // This class is **not** an extensibility point - every part of the routing system + // needs to be aware of what kind of parts we support. + // + // It is abstract so we can add semantics later inside the library. + private protected RoutePatternPart(RoutePatternPartKind partKind) { - // This class is **not** an extensibility point - every part of the routing system - // needs to be aware of what kind of parts we support. - // - // It is abstract so we can add semantics later inside the library. - private protected RoutePatternPart(RoutePatternPartKind partKind) - { - PartKind = partKind; - } + PartKind = partKind; + } - /// - /// Gets the of this part. - /// - public RoutePatternPartKind PartKind { get; } + /// + /// Gets the of this part. + /// + public RoutePatternPartKind PartKind { get; } - /// - /// Returns true if this part is literal text. Otherwise returns false. - /// - public bool IsLiteral => PartKind == RoutePatternPartKind.Literal; + /// + /// Returns true if this part is literal text. Otherwise returns false. + /// + public bool IsLiteral => PartKind == RoutePatternPartKind.Literal; - /// - /// Returns true if this part is a route parameter. Otherwise returns false. - /// - public bool IsParameter => PartKind == RoutePatternPartKind.Parameter; + /// + /// Returns true if this part is a route parameter. Otherwise returns false. + /// + public bool IsParameter => PartKind == RoutePatternPartKind.Parameter; - /// - /// Returns true if this part is an optional separator. Otherwise returns false. - /// - public bool IsSeparator => PartKind == RoutePatternPartKind.Separator; + /// + /// Returns true if this part is an optional separator. Otherwise returns false. + /// + public bool IsSeparator => PartKind == RoutePatternPartKind.Separator; - internal abstract string DebuggerToString(); - } + internal abstract string DebuggerToString(); } diff --git a/src/Http/Routing/src/Patterns/RoutePatternPartKind.cs b/src/Http/Routing/src/Patterns/RoutePatternPartKind.cs index 98904cf84b..03d58d60d5 100644 --- a/src/Http/Routing/src/Patterns/RoutePatternPartKind.cs +++ b/src/Http/Routing/src/Patterns/RoutePatternPartKind.cs @@ -1,26 +1,25 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +/// +/// Defines the kinds of instances. +/// +public enum RoutePatternPartKind { /// - /// Defines the kinds of instances. + /// The of a . /// - public enum RoutePatternPartKind - { - /// - /// The of a . - /// - Literal, + Literal, - /// - /// The of a . - /// - Parameter, + /// + /// The of a . + /// + Parameter, - /// - /// The of a . - /// - Separator, - } + /// + /// The of a . + /// + Separator, } diff --git a/src/Http/Routing/src/Patterns/RoutePatternPathSegment.cs b/src/Http/Routing/src/Patterns/RoutePatternPathSegment.cs index d7d1223f0a..5e04dd8f6f 100644 --- a/src/Http/Routing/src/Patterns/RoutePatternPathSegment.cs +++ b/src/Http/Routing/src/Patterns/RoutePatternPathSegment.cs @@ -5,45 +5,44 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +/// +/// Represents a path segment in a route pattern. Instances of are +/// immutable. +/// +/// +/// Route patterns are made up of URL path segments, delimited by /. A +/// contains a group of +/// that represent the structure of a segment +/// in a route pattern. +/// +[DebuggerDisplay("{DebuggerToString()}")] +public sealed class RoutePatternPathSegment { - /// - /// Represents a path segment in a route pattern. Instances of are - /// immutable. - /// - /// - /// Route patterns are made up of URL path segments, delimited by /. A - /// contains a group of - /// that represent the structure of a segment - /// in a route pattern. - /// - [DebuggerDisplay("{DebuggerToString()}")] - public sealed class RoutePatternPathSegment + internal RoutePatternPathSegment(IReadOnlyList parts) { - internal RoutePatternPathSegment(IReadOnlyList parts) - { - Parts = parts; - } + Parts = parts; + } - /// - /// Returns true if the segment contains a single part; - /// otherwise returns false. - /// - public bool IsSimple => Parts.Count == 1; + /// + /// Returns true if the segment contains a single part; + /// otherwise returns false. + /// + public bool IsSimple => Parts.Count == 1; - /// - /// Gets the list of parts in this segment. - /// - public IReadOnlyList Parts { get; } + /// + /// Gets the list of parts in this segment. + /// + public IReadOnlyList Parts { get; } - internal string DebuggerToString() - { - return DebuggerToString(Parts); - } + internal string DebuggerToString() + { + return DebuggerToString(Parts); + } - internal static string DebuggerToString(IReadOnlyList parts) - { - return string.Join(string.Empty, parts.Select(p => p.DebuggerToString())); - } + internal static string DebuggerToString(IReadOnlyList parts) + { + return string.Join(string.Empty, parts.Select(p => p.DebuggerToString())); } } diff --git a/src/Http/Routing/src/Patterns/RoutePatternSeparatorPart.cs b/src/Http/Routing/src/Patterns/RoutePatternSeparatorPart.cs index 9dd59537c7..47f124f799 100644 --- a/src/Http/Routing/src/Patterns/RoutePatternSeparatorPart.cs +++ b/src/Http/Routing/src/Patterns/RoutePatternSeparatorPart.cs @@ -3,48 +3,47 @@ using System.Diagnostics; -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +/// +/// Represents an optional separator part of a route pattern. Instances of +/// are immutable. +/// +/// +/// +/// An optional separator is a literal text delimiter that appears between +/// two parameter parts in the last segment of a route pattern. The only separator +/// that is recognized is .. +/// +/// +/// +/// In the route pattern /{controller}/{action}/{id?}.{extension?} +/// the . character is an optional separator. +/// +/// +/// +/// An optional separator character does not need to present in the URL path +/// of a request for the route pattern to match. +/// +/// +[DebuggerDisplay("{DebuggerToString()}")] +public sealed class RoutePatternSeparatorPart : RoutePatternPart { - /// - /// Represents an optional separator part of a route pattern. Instances of - /// are immutable. - /// - /// - /// - /// An optional separator is a literal text delimiter that appears between - /// two parameter parts in the last segment of a route pattern. The only separator - /// that is recognized is .. - /// - /// - /// - /// In the route pattern /{controller}/{action}/{id?}.{extension?} - /// the . character is an optional separator. - /// - /// - /// - /// An optional separator character does not need to present in the URL path - /// of a request for the route pattern to match. - /// - /// - [DebuggerDisplay("{DebuggerToString()}")] - public sealed class RoutePatternSeparatorPart : RoutePatternPart + internal RoutePatternSeparatorPart(string content) + : base(RoutePatternPartKind.Separator) { - internal RoutePatternSeparatorPart(string content) - : base(RoutePatternPartKind.Separator) - { - Debug.Assert(!string.IsNullOrEmpty(content)); + Debug.Assert(!string.IsNullOrEmpty(content)); - Content = content; - } + Content = content; + } - /// - /// Gets the text content of the part. - /// - public string Content { get; } + /// + /// Gets the text content of the part. + /// + public string Content { get; } - internal override string DebuggerToString() - { - return Content; - } + internal override string DebuggerToString() + { + return Content; } } diff --git a/src/Http/Routing/src/Patterns/RoutePatternTransformer.cs b/src/Http/Routing/src/Patterns/RoutePatternTransformer.cs index a7eaf7b908..bd642fa6c0 100644 --- a/src/Http/Routing/src/Patterns/RoutePatternTransformer.cs +++ b/src/Http/Routing/src/Patterns/RoutePatternTransformer.cs @@ -1,35 +1,34 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +/// +/// A singleton service that provides transformations on . +/// +public abstract class RoutePatternTransformer { /// - /// A singleton service that provides transformations on . + /// Attempts to substitute the provided into the provided + /// . /// - public abstract class RoutePatternTransformer - { - /// - /// Attempts to substitute the provided into the provided - /// . - /// - /// The original . - /// The required values to substitute. - /// - /// A new if substitution succeeds, otherwise null. - /// - /// - /// - /// Substituting required values into a route pattern is intended for us with a general-purpose - /// parameterize route specification that can match many logical endpoints. Calling - /// can produce a derived route pattern - /// for each set of route values that corresponds to an endpoint. - /// - /// - /// The substitution process considers default values and implementations - /// when examining a required value. will - /// return null if any required value cannot be substituted. - /// - /// - public abstract RoutePattern? SubstituteRequiredValues(RoutePattern original, object requiredValues); - } + /// The original . + /// The required values to substitute. + /// + /// A new if substitution succeeds, otherwise null. + /// + /// + /// + /// Substituting required values into a route pattern is intended for us with a general-purpose + /// parameterize route specification that can match many logical endpoints. Calling + /// can produce a derived route pattern + /// for each set of route values that corresponds to an endpoint. + /// + /// + /// The substitution process considers default values and implementations + /// when examining a required value. will + /// return null if any required value cannot be substituted. + /// + /// + public abstract RoutePattern? SubstituteRequiredValues(RoutePattern original, object requiredValues); } diff --git a/src/Http/Routing/src/RequestDelegateRouteBuilderExtensions.cs b/src/Http/Routing/src/RequestDelegateRouteBuilderExtensions.cs index 2ae7e9515b..9a17f64d9e 100644 --- a/src/Http/Routing/src/RequestDelegateRouteBuilderExtensions.cs +++ b/src/Http/Routing/src/RequestDelegateRouteBuilderExtensions.cs @@ -8,291 +8,290 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Provides extension methods for adding new handlers to a . +/// +public static class RequestDelegateRouteBuilderExtensions { /// - /// Provides extension methods for adding new handlers to a . + /// Adds a route to the for the given , and + /// . /// - public static class RequestDelegateRouteBuilderExtensions + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapRoute(this IRouteBuilder builder, string template, RequestDelegate handler) { - /// - /// Adds a route to the for the given , and - /// . - /// - /// The . - /// The route template. - /// The route handler. - /// A reference to the after this operation has completed. - public static IRouteBuilder MapRoute(this IRouteBuilder builder, string template, RequestDelegate handler) - { - var route = new Route( - new RouteHandler(handler), - template, - defaults: null, - constraints: null, - dataTokens: null, - inlineConstraintResolver: GetConstraintResolver(builder)); + var route = new Route( + new RouteHandler(handler), + template, + defaults: null, + constraints: null, + dataTokens: null, + inlineConstraintResolver: GetConstraintResolver(builder)); - builder.Routes.Add(route); - return builder; - } + builder.Routes.Add(route); + return builder; + } - /// - /// Adds a route to the for the given , and - /// . - /// - /// The . - /// The route template. - /// The action to apply to the . - /// A reference to the after this operation has completed. - public static IRouteBuilder MapMiddlewareRoute(this IRouteBuilder builder, string template, Action action) - { - var nested = builder.ApplicationBuilder.New(); - action(nested); - return builder.MapRoute(template, nested.Build()); - } + /// + /// Adds a route to the for the given , and + /// . + /// + /// The . + /// The route template. + /// The action to apply to the . + /// A reference to the after this operation has completed. + public static IRouteBuilder MapMiddlewareRoute(this IRouteBuilder builder, string template, Action action) + { + var nested = builder.ApplicationBuilder.New(); + action(nested); + return builder.MapRoute(template, nested.Build()); + } - /// - /// Adds a route to the that only matches HTTP DELETE requests for the given - /// , and . - /// - /// The . - /// The route template. - /// The route handler. - /// A reference to the after this operation has completed. - public static IRouteBuilder MapDelete(this IRouteBuilder builder, string template, RequestDelegate handler) - { - return builder.MapVerb("DELETE", template, handler); - } + /// + /// Adds a route to the that only matches HTTP DELETE requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapDelete(this IRouteBuilder builder, string template, RequestDelegate handler) + { + return builder.MapVerb("DELETE", template, handler); + } - /// - /// Adds a route to the that only matches HTTP DELETE requests for the given - /// , and . - /// - /// The . - /// The route template. - /// The action to apply to the . - /// A reference to the after this operation has completed. - public static IRouteBuilder MapMiddlewareDelete(this IRouteBuilder builder, string template, Action action) - { - return builder.MapMiddlewareVerb("DELETE", template, action); - } + /// + /// Adds a route to the that only matches HTTP DELETE requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The action to apply to the . + /// A reference to the after this operation has completed. + public static IRouteBuilder MapMiddlewareDelete(this IRouteBuilder builder, string template, Action action) + { + return builder.MapMiddlewareVerb("DELETE", template, action); + } - /// - /// Adds a route to the that only matches HTTP DELETE requests for the given - /// , and . - /// - /// The . - /// The route template. - /// The route handler. - /// A reference to the after this operation has completed. - public static IRouteBuilder MapDelete( - this IRouteBuilder builder, - string template, - Func handler) - { - return builder.MapVerb("DELETE", template, handler); - } + /// + /// Adds a route to the that only matches HTTP DELETE requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapDelete( + this IRouteBuilder builder, + string template, + Func handler) + { + return builder.MapVerb("DELETE", template, handler); + } - /// - /// Adds a route to the that only matches HTTP GET requests for the given - /// , and . - /// - /// The . - /// The route template. - /// The route handler. - /// A reference to the after this operation has completed. - public static IRouteBuilder MapGet(this IRouteBuilder builder, string template, RequestDelegate handler) - { - return builder.MapVerb("GET", template, handler); - } + /// + /// Adds a route to the that only matches HTTP GET requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapGet(this IRouteBuilder builder, string template, RequestDelegate handler) + { + return builder.MapVerb("GET", template, handler); + } - /// - /// Adds a route to the that only matches HTTP GET requests for the given - /// , and . - /// - /// The . - /// The route template. - /// The action to apply to the . - /// A reference to the after this operation has completed. - public static IRouteBuilder MapMiddlewareGet(this IRouteBuilder builder, string template, Action action) - { - return builder.MapMiddlewareVerb("GET", template, action); - } + /// + /// Adds a route to the that only matches HTTP GET requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The action to apply to the . + /// A reference to the after this operation has completed. + public static IRouteBuilder MapMiddlewareGet(this IRouteBuilder builder, string template, Action action) + { + return builder.MapMiddlewareVerb("GET", template, action); + } - /// - /// Adds a route to the that only matches HTTP GET requests for the given - /// , and . - /// - /// The . - /// The route template. - /// The route handler. - /// A reference to the after this operation has completed. - public static IRouteBuilder MapGet( - this IRouteBuilder builder, - string template, - Func handler) - { - return builder.MapVerb("GET", template, handler); - } + /// + /// Adds a route to the that only matches HTTP GET requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapGet( + this IRouteBuilder builder, + string template, + Func handler) + { + return builder.MapVerb("GET", template, handler); + } - /// - /// Adds a route to the that only matches HTTP POST requests for the given - /// , and . - /// - /// The . - /// The route template. - /// The route handler. - /// A reference to the after this operation has completed. - public static IRouteBuilder MapPost(this IRouteBuilder builder, string template, RequestDelegate handler) - { - return builder.MapVerb("POST", template, handler); - } + /// + /// Adds a route to the that only matches HTTP POST requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapPost(this IRouteBuilder builder, string template, RequestDelegate handler) + { + return builder.MapVerb("POST", template, handler); + } - /// - /// Adds a route to the that only matches HTTP POST requests for the given - /// , and . - /// - /// The . - /// The route template. - /// The action to apply to the . - /// A reference to the after this operation has completed. - public static IRouteBuilder MapMiddlewarePost(this IRouteBuilder builder, string template, Action action) - { - return builder.MapMiddlewareVerb("POST", template, action); - } + /// + /// Adds a route to the that only matches HTTP POST requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The action to apply to the . + /// A reference to the after this operation has completed. + public static IRouteBuilder MapMiddlewarePost(this IRouteBuilder builder, string template, Action action) + { + return builder.MapMiddlewareVerb("POST", template, action); + } - /// - /// Adds a route to the that only matches HTTP POST requests for the given - /// , and . - /// - /// The . - /// The route template. - /// The route handler. - /// A reference to the after this operation has completed. - public static IRouteBuilder MapPost( - this IRouteBuilder builder, - string template, - Func handler) - { - return builder.MapVerb("POST", template, handler); - } + /// + /// Adds a route to the that only matches HTTP POST requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapPost( + this IRouteBuilder builder, + string template, + Func handler) + { + return builder.MapVerb("POST", template, handler); + } - /// - /// Adds a route to the that only matches HTTP PUT requests for the given - /// , and . - /// - /// The . - /// The route template. - /// The route handler. - /// A reference to the after this operation has completed. - public static IRouteBuilder MapPut(this IRouteBuilder builder, string template, RequestDelegate handler) - { - return builder.MapVerb("PUT", template, handler); - } + /// + /// Adds a route to the that only matches HTTP PUT requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapPut(this IRouteBuilder builder, string template, RequestDelegate handler) + { + return builder.MapVerb("PUT", template, handler); + } - /// - /// Adds a route to the that only matches HTTP PUT requests for the given - /// , and . - /// - /// The . - /// The route template. - /// The action to apply to the . - /// A reference to the after this operation has completed. - public static IRouteBuilder MapMiddlewarePut(this IRouteBuilder builder, string template, Action action) - { - return builder.MapMiddlewareVerb("PUT", template, action); - } + /// + /// Adds a route to the that only matches HTTP PUT requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The action to apply to the . + /// A reference to the after this operation has completed. + public static IRouteBuilder MapMiddlewarePut(this IRouteBuilder builder, string template, Action action) + { + return builder.MapMiddlewareVerb("PUT", template, action); + } - /// - /// Adds a route to the that only matches HTTP PUT requests for the given - /// , and . - /// - /// The . - /// The route template. - /// The route handler. - /// A reference to the after this operation has completed. - public static IRouteBuilder MapPut( - this IRouteBuilder builder, - string template, - Func handler) - { - return builder.MapVerb("PUT", template, handler); - } + /// + /// Adds a route to the that only matches HTTP PUT requests for the given + /// , and . + /// + /// The . + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapPut( + this IRouteBuilder builder, + string template, + Func handler) + { + return builder.MapVerb("PUT", template, handler); + } - /// - /// Adds a route to the that only matches HTTP requests for the given - /// , , and . - /// - /// The . - /// The HTTP verb allowed by the route. - /// The route template. - /// The route handler. - /// A reference to the after this operation has completed. - public static IRouteBuilder MapVerb( - this IRouteBuilder builder, - string verb, - string template, - Func handler) + /// + /// Adds a route to the that only matches HTTP requests for the given + /// , , and . + /// + /// The . + /// The HTTP verb allowed by the route. + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapVerb( + this IRouteBuilder builder, + string verb, + string template, + Func handler) + { + RequestDelegate requestDelegate = (httpContext) => { - RequestDelegate requestDelegate = (httpContext) => - { - return handler(httpContext.Request, httpContext.Response, httpContext.GetRouteData()); - }; + return handler(httpContext.Request, httpContext.Response, httpContext.GetRouteData()); + }; - return builder.MapVerb(verb, template, requestDelegate); - } + return builder.MapVerb(verb, template, requestDelegate); + } - /// - /// Adds a route to the that only matches HTTP requests for the given - /// , , and . - /// - /// The . - /// The HTTP verb allowed by the route. - /// The route template. - /// The route handler. - /// A reference to the after this operation has completed. - public static IRouteBuilder MapVerb( - this IRouteBuilder builder, - string verb, - string template, - RequestDelegate handler) - { - var route = new Route( - new RouteHandler(handler), - template, - defaults: null, - constraints: new RouteValueDictionary(new { httpMethod = new HttpMethodRouteConstraint(verb) })!, - dataTokens: null, - inlineConstraintResolver: GetConstraintResolver(builder)); + /// + /// Adds a route to the that only matches HTTP requests for the given + /// , , and . + /// + /// The . + /// The HTTP verb allowed by the route. + /// The route template. + /// The route handler. + /// A reference to the after this operation has completed. + public static IRouteBuilder MapVerb( + this IRouteBuilder builder, + string verb, + string template, + RequestDelegate handler) + { + var route = new Route( + new RouteHandler(handler), + template, + defaults: null, + constraints: new RouteValueDictionary(new { httpMethod = new HttpMethodRouteConstraint(verb) })!, + dataTokens: null, + inlineConstraintResolver: GetConstraintResolver(builder)); - builder.Routes.Add(route); - return builder; - } + builder.Routes.Add(route); + return builder; + } - /// - /// Adds a route to the that only matches HTTP requests for the given - /// , , and . - /// - /// The . - /// The HTTP verb allowed by the route. - /// The route template. - /// The action to apply to the . - /// A reference to the after this operation has completed. - public static IRouteBuilder MapMiddlewareVerb( - this IRouteBuilder builder, - string verb, - string template, - Action action) - { - var nested = builder.ApplicationBuilder.New(); - action(nested); - return builder.MapVerb(verb, template, nested.Build()); - } + /// + /// Adds a route to the that only matches HTTP requests for the given + /// , , and . + /// + /// The . + /// The HTTP verb allowed by the route. + /// The route template. + /// The action to apply to the . + /// A reference to the after this operation has completed. + public static IRouteBuilder MapMiddlewareVerb( + this IRouteBuilder builder, + string verb, + string template, + Action action) + { + var nested = builder.ApplicationBuilder.New(); + action(nested); + return builder.MapVerb(verb, template, nested.Build()); + } - private static IInlineConstraintResolver GetConstraintResolver(IRouteBuilder builder) - { - return builder.ServiceProvider.GetRequiredService(); - } + private static IInlineConstraintResolver GetConstraintResolver(IRouteBuilder builder) + { + return builder.ServiceProvider.GetRequiredService(); } } diff --git a/src/Http/Routing/src/Route.cs b/src/Http/Routing/src/Route.cs index 80150899f1..ed9ba05b76 100644 --- a/src/Http/Routing/src/Route.cs +++ b/src/Http/Routing/src/Route.cs @@ -5,105 +5,104 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Represents an instance of a route. +/// +public class Route : RouteBase { + private readonly IRouter _target; + /// - /// Represents an instance of a route. + /// Constructs a new instance. /// - public class Route : RouteBase + /// An instance associated with the component. + /// A string representation of the route template. + /// An used for resolving inline constraints. + public Route( + IRouter target, + string routeTemplate, + IInlineConstraintResolver inlineConstraintResolver) + : this( + target, + routeTemplate, + defaults: null, + constraints: null, + dataTokens: null, + inlineConstraintResolver: inlineConstraintResolver) { - private readonly IRouter _target; + } - /// - /// Constructs a new instance. - /// - /// An instance associated with the component. - /// A string representation of the route template. - /// An used for resolving inline constraints. - public Route( - IRouter target, - string routeTemplate, - IInlineConstraintResolver inlineConstraintResolver) - : this( - target, - routeTemplate, - defaults: null, - constraints: null, - dataTokens: null, - inlineConstraintResolver: inlineConstraintResolver) - { - } + /// + /// Constructs a new instance. + /// + /// An instance associated with the component. + /// A string representation of the route template. + /// The default values for parameters in the route. + /// The constraints for the route. + /// The data tokens for the route. + /// An used for resolving inline constraints. + public Route( + IRouter target, + string routeTemplate, + RouteValueDictionary? defaults, + IDictionary? constraints, + RouteValueDictionary? dataTokens, + IInlineConstraintResolver inlineConstraintResolver) + : this(target, null, routeTemplate, defaults, constraints, dataTokens, inlineConstraintResolver) + { + } - /// - /// Constructs a new instance. - /// - /// An instance associated with the component. - /// A string representation of the route template. - /// The default values for parameters in the route. - /// The constraints for the route. - /// The data tokens for the route. - /// An used for resolving inline constraints. - public Route( - IRouter target, - string routeTemplate, - RouteValueDictionary? defaults, - IDictionary? constraints, - RouteValueDictionary? dataTokens, - IInlineConstraintResolver inlineConstraintResolver) - : this(target, null, routeTemplate, defaults, constraints, dataTokens, inlineConstraintResolver) + /// + /// Constructs a new instance. + /// + /// An instance associated with the component. + /// The name of the route. + /// A string representation of the route template. + /// The default values for parameters in the route. + /// The constraints for the route. + /// The data tokens for the route. + /// An used for resolving inline constraints. + public Route( + IRouter target, + string? routeName, + string? routeTemplate, + RouteValueDictionary? defaults, + IDictionary? constraints, + RouteValueDictionary? dataTokens, + IInlineConstraintResolver inlineConstraintResolver) + : base( + routeTemplate, + routeName, + inlineConstraintResolver, + defaults, + constraints, + dataTokens) + { + if (target == null) { + throw new ArgumentNullException(nameof(target)); } - /// - /// Constructs a new instance. - /// - /// An instance associated with the component. - /// The name of the route. - /// A string representation of the route template. - /// The default values for parameters in the route. - /// The constraints for the route. - /// The data tokens for the route. - /// An used for resolving inline constraints. - public Route( - IRouter target, - string? routeName, - string? routeTemplate, - RouteValueDictionary? defaults, - IDictionary? constraints, - RouteValueDictionary? dataTokens, - IInlineConstraintResolver inlineConstraintResolver) - : base( - routeTemplate, - routeName, - inlineConstraintResolver, - defaults, - constraints, - dataTokens) - { - if (target == null) - { - throw new ArgumentNullException(nameof(target)); - } - - _target = target; - } + _target = target; + } - /// - /// Gets a string representation of the route template. - /// - public string? RouteTemplate => ParsedTemplate.TemplateText; + /// + /// Gets a string representation of the route template. + /// + public string? RouteTemplate => ParsedTemplate.TemplateText; - /// - protected override Task OnRouteMatched(RouteContext context) - { - context.RouteData.Routers.Add(_target); - return _target.RouteAsync(context); - } + /// + protected override Task OnRouteMatched(RouteContext context) + { + context.RouteData.Routers.Add(_target); + return _target.RouteAsync(context); + } - /// - protected override VirtualPathData? OnVirtualPathGenerated(VirtualPathContext context) - { - return _target.GetVirtualPath(context); - } + /// + protected override VirtualPathData? OnVirtualPathGenerated(VirtualPathContext context) + { + return _target.GetVirtualPath(context); } } diff --git a/src/Http/Routing/src/RouteBase.cs b/src/Http/Routing/src/RouteBase.cs index f6a05da567..7b5a07f45e 100644 --- a/src/Http/Routing/src/RouteBase.cs +++ b/src/Http/Routing/src/RouteBase.cs @@ -11,251 +11,251 @@ using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Base class implementation of an . +/// +public abstract partial class RouteBase : IRouter, INamedRouter { + private readonly object _loggersLock = new object(); + + private TemplateMatcher? _matcher; + private TemplateBinder? _binder; + private ILogger? _logger; + private ILogger? _constraintLogger; + /// - /// Base class implementation of an . + /// Creates a new instance. /// - public abstract partial class RouteBase : IRouter, INamedRouter + /// The route template. + /// The name of the route. + /// An used for resolving inline constraints. + /// The default values for parameters in the route. + /// The constraints for the route. + /// The data tokens for the route. + public RouteBase( + string? template, + string? name, + IInlineConstraintResolver constraintResolver, + RouteValueDictionary? defaults, + IDictionary? constraints, + RouteValueDictionary? dataTokens) { - private readonly object _loggersLock = new object(); - - private TemplateMatcher? _matcher; - private TemplateBinder? _binder; - private ILogger? _logger; - private ILogger? _constraintLogger; - - /// - /// Creates a new instance. - /// - /// The route template. - /// The name of the route. - /// An used for resolving inline constraints. - /// The default values for parameters in the route. - /// The constraints for the route. - /// The data tokens for the route. - public RouteBase( - string? template, - string? name, - IInlineConstraintResolver constraintResolver, - RouteValueDictionary? defaults, - IDictionary? constraints, - RouteValueDictionary? dataTokens) + if (constraintResolver == null) { - if (constraintResolver == null) - { - throw new ArgumentNullException(nameof(constraintResolver)); - } + throw new ArgumentNullException(nameof(constraintResolver)); + } - template = template ?? string.Empty; - Name = name; - ConstraintResolver = constraintResolver; - DataTokens = dataTokens ?? new RouteValueDictionary(); + template = template ?? string.Empty; + Name = name; + ConstraintResolver = constraintResolver; + DataTokens = dataTokens ?? new RouteValueDictionary(); - try - { - // Data we parse from the template will be used to fill in the rest of the constraints or - // defaults. The parser will throw for invalid routes. - ParsedTemplate = TemplateParser.Parse(template); + try + { + // Data we parse from the template will be used to fill in the rest of the constraints or + // defaults. The parser will throw for invalid routes. + ParsedTemplate = TemplateParser.Parse(template); - Constraints = GetConstraints(constraintResolver, ParsedTemplate, constraints); - Defaults = GetDefaults(ParsedTemplate, defaults); - } - catch (Exception exception) - { - throw new RouteCreationException(Resources.FormatTemplateRoute_Exception(name, template), exception); - } + Constraints = GetConstraints(constraintResolver, ParsedTemplate, constraints); + Defaults = GetDefaults(ParsedTemplate, defaults); } - - /// - /// Gets the set of constraints associated with each route. - /// - public virtual IDictionary Constraints { get; protected set; } - - /// - /// Gets the resolver used for resolving inline constraints. - /// - protected virtual IInlineConstraintResolver ConstraintResolver { get; set; } - - /// - /// Gets the data tokens associated with the route. - /// - public virtual RouteValueDictionary DataTokens { get; protected set; } - - /// - /// Gets the default values for each route parameter. - /// - public virtual RouteValueDictionary Defaults { get; protected set; } - - /// - public virtual string? Name { get; protected set; } - - /// - /// Gets the associated with the route. - /// - public virtual RouteTemplate ParsedTemplate { get; protected set; } - - /// - /// Executes asynchronously whenever routing occurs. - /// - /// A instance. - protected abstract Task OnRouteMatched(RouteContext context); - - /// - /// Executes whenever a virtual path is derived from a . - /// - /// A instance. - /// A instance. - protected abstract VirtualPathData? OnVirtualPathGenerated(VirtualPathContext context); - - /// - public virtual Task RouteAsync(RouteContext context) + catch (Exception exception) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } + throw new RouteCreationException(Resources.FormatTemplateRoute_Exception(name, template), exception); + } + } - EnsureMatcher(); - EnsureLoggers(context.HttpContext); + /// + /// Gets the set of constraints associated with each route. + /// + public virtual IDictionary Constraints { get; protected set; } - var requestPath = context.HttpContext.Request.Path; + /// + /// Gets the resolver used for resolving inline constraints. + /// + protected virtual IInlineConstraintResolver ConstraintResolver { get; set; } - if (!_matcher.TryMatch(requestPath, context.RouteData.Values)) - { - // If we got back a null value set, that means the URI did not match - return Task.CompletedTask; - } + /// + /// Gets the data tokens associated with the route. + /// + public virtual RouteValueDictionary DataTokens { get; protected set; } - // Perf: Avoid accessing dictionaries if you don't need to write to them, these dictionaries are all - // created lazily. - if (DataTokens.Count > 0) - { - MergeValues(context.RouteData.DataTokens, DataTokens); - } + /// + /// Gets the default values for each route parameter. + /// + public virtual RouteValueDictionary Defaults { get; protected set; } - if (!RouteConstraintMatcher.Match( - Constraints, - context.RouteData.Values, - context.HttpContext, - this, - RouteDirection.IncomingRequest, - _constraintLogger)) - { - return Task.CompletedTask; - } - Log.RequestMatchedRoute(_logger, Name, ParsedTemplate.TemplateText); + /// + public virtual string? Name { get; protected set; } + + /// + /// Gets the associated with the route. + /// + public virtual RouteTemplate ParsedTemplate { get; protected set; } - return OnRouteMatched(context); + /// + /// Executes asynchronously whenever routing occurs. + /// + /// A instance. + protected abstract Task OnRouteMatched(RouteContext context); + + /// + /// Executes whenever a virtual path is derived from a . + /// + /// A instance. + /// A instance. + protected abstract VirtualPathData? OnVirtualPathGenerated(VirtualPathContext context); + + /// + public virtual Task RouteAsync(RouteContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); } - /// - public virtual VirtualPathData? GetVirtualPath(VirtualPathContext context) + EnsureMatcher(); + EnsureLoggers(context.HttpContext); + + var requestPath = context.HttpContext.Request.Path; + + if (!_matcher.TryMatch(requestPath, context.RouteData.Values)) { - EnsureBinder(context.HttpContext); - EnsureLoggers(context.HttpContext); + // If we got back a null value set, that means the URI did not match + return Task.CompletedTask; + } - var values = _binder.GetValues(context.AmbientValues, context.Values); - if (values == null) - { - // We're missing one of the required values for this route. - return null; - } + // Perf: Avoid accessing dictionaries if you don't need to write to them, these dictionaries are all + // created lazily. + if (DataTokens.Count > 0) + { + MergeValues(context.RouteData.DataTokens, DataTokens); + } - if (!RouteConstraintMatcher.Match( - Constraints, - values.CombinedValues, - context.HttpContext, - this, - RouteDirection.UrlGeneration, - _constraintLogger)) - { - return null; - } + if (!RouteConstraintMatcher.Match( + Constraints, + context.RouteData.Values, + context.HttpContext, + this, + RouteDirection.IncomingRequest, + _constraintLogger)) + { + return Task.CompletedTask; + } + Log.RequestMatchedRoute(_logger, Name, ParsedTemplate.TemplateText); - context.Values = values.CombinedValues; + return OnRouteMatched(context); + } - var pathData = OnVirtualPathGenerated(context); - if (pathData != null) - { - // If the target generates a value then that can short circuit. - return pathData; - } + /// + public virtual VirtualPathData? GetVirtualPath(VirtualPathContext context) + { + EnsureBinder(context.HttpContext); + EnsureLoggers(context.HttpContext); - // If we can produce a value go ahead and do it, the caller can check context.IsBound - // to see if the values were validated. + var values = _binder.GetValues(context.AmbientValues, context.Values); + if (values == null) + { + // We're missing one of the required values for this route. + return null; + } - // When we still cannot produce a value, this should return null. - var virtualPath = _binder.BindValues(values.AcceptedValues); - if (virtualPath == null) - { - return null; - } + if (!RouteConstraintMatcher.Match( + Constraints, + values.CombinedValues, + context.HttpContext, + this, + RouteDirection.UrlGeneration, + _constraintLogger)) + { + return null; + } - pathData = new VirtualPathData(this, virtualPath); - if (DataTokens != null) - { - foreach (var dataToken in DataTokens) - { - pathData.DataTokens.Add(dataToken.Key, dataToken.Value); - } - } + context.Values = values.CombinedValues; + var pathData = OnVirtualPathGenerated(context); + if (pathData != null) + { + // If the target generates a value then that can short circuit. return pathData; } - /// - /// Extracts constatins from a given . - /// - /// An used for resolving inline constraints. - /// A instance. - /// A collection of constraints on the route template. - protected static IDictionary GetConstraints( - IInlineConstraintResolver inlineConstraintResolver, - RouteTemplate parsedTemplate, - IDictionary? constraints) + // If we can produce a value go ahead and do it, the caller can check context.IsBound + // to see if the values were validated. + + // When we still cannot produce a value, this should return null. + var virtualPath = _binder.BindValues(values.AcceptedValues); + if (virtualPath == null) { - var constraintBuilder = new RouteConstraintBuilder(inlineConstraintResolver, parsedTemplate.TemplateText!); + return null; + } - if (constraints != null) + pathData = new VirtualPathData(this, virtualPath); + if (DataTokens != null) + { + foreach (var dataToken in DataTokens) { - foreach (var kvp in constraints) - { - constraintBuilder.AddConstraint(kvp.Key, kvp.Value); - } + pathData.DataTokens.Add(dataToken.Key, dataToken.Value); } + } + + return pathData; + } + + /// + /// Extracts constatins from a given . + /// + /// An used for resolving inline constraints. + /// A instance. + /// A collection of constraints on the route template. + protected static IDictionary GetConstraints( + IInlineConstraintResolver inlineConstraintResolver, + RouteTemplate parsedTemplate, + IDictionary? constraints) + { + var constraintBuilder = new RouteConstraintBuilder(inlineConstraintResolver, parsedTemplate.TemplateText!); - foreach (var parameter in parsedTemplate.Parameters) + if (constraints != null) + { + foreach (var kvp in constraints) { - if (parameter.IsOptional) - { - constraintBuilder.SetOptional(parameter.Name!); - } + constraintBuilder.AddConstraint(kvp.Key, kvp.Value); + } + } - foreach (var inlineConstraint in parameter.InlineConstraints) - { - constraintBuilder.AddResolvedConstraint(parameter.Name!, inlineConstraint.Constraint); - } + foreach (var parameter in parsedTemplate.Parameters) + { + if (parameter.IsOptional) + { + constraintBuilder.SetOptional(parameter.Name!); } - return constraintBuilder.Build(); + foreach (var inlineConstraint in parameter.InlineConstraints) + { + constraintBuilder.AddResolvedConstraint(parameter.Name!, inlineConstraint.Constraint); + } } - /// - /// Gets the default values for parameters in a templates. - /// - /// A instance. - /// A collection of defaults for each parameter. - protected static RouteValueDictionary GetDefaults( - RouteTemplate parsedTemplate, - RouteValueDictionary? defaults) - { - var result = defaults == null ? new RouteValueDictionary() : new RouteValueDictionary(defaults); + return constraintBuilder.Build(); + } + + /// + /// Gets the default values for parameters in a templates. + /// + /// A instance. + /// A collection of defaults for each parameter. + protected static RouteValueDictionary GetDefaults( + RouteTemplate parsedTemplate, + RouteValueDictionary? defaults) + { + var result = defaults == null ? new RouteValueDictionary() : new RouteValueDictionary(defaults); - foreach (var parameter in parsedTemplate.Parameters) + foreach (var parameter in parsedTemplate.Parameters) + { + if (parameter.DefaultValue != null) { - if (parameter.DefaultValue != null) - { #if RVD_TryAdd if (!result.TryAdd(parameter.Name, parameter.DefaultValue)) { @@ -264,97 +264,96 @@ namespace Microsoft.AspNetCore.Routing parameter.Name)); } #else - if (result.ContainsKey(parameter.Name!)) - { - throw new InvalidOperationException( - Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly( - parameter.Name)); - } - else - { - result.Add(parameter.Name!, parameter.DefaultValue); - } -#endif + if (result.ContainsKey(parameter.Name!)) + { + throw new InvalidOperationException( + Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly( + parameter.Name)); + } + else + { + result.Add(parameter.Name!, parameter.DefaultValue); } +#endif } - - return result; } - private static void MergeValues( - RouteValueDictionary destination, - RouteValueDictionary values) + return result; + } + + private static void MergeValues( + RouteValueDictionary destination, + RouteValueDictionary values) + { + foreach (var kvp in values) { - foreach (var kvp in values) - { - // This will replace the original value for the specified key. - // Values from the matched route will take preference over previous - // data in the route context. - destination[kvp.Key] = kvp.Value; - } + // This will replace the original value for the specified key. + // Values from the matched route will take preference over previous + // data in the route context. + destination[kvp.Key] = kvp.Value; } + } - [MemberNotNull(nameof(_binder))] - private void EnsureBinder(HttpContext context) + [MemberNotNull(nameof(_binder))] + private void EnsureBinder(HttpContext context) + { + if (_binder == null) { - if (_binder == null) - { - var binderFactory = context.RequestServices.GetRequiredService(); - _binder = binderFactory.Create(ParsedTemplate, Defaults); - } + var binderFactory = context.RequestServices.GetRequiredService(); + _binder = binderFactory.Create(ParsedTemplate, Defaults); } + } - [MemberNotNull(nameof(_logger), nameof(_constraintLogger))] - private void EnsureLoggers(HttpContext context) + [MemberNotNull(nameof(_logger), nameof(_constraintLogger))] + private void EnsureLoggers(HttpContext context) + { + // We check first using the _logger to see if the loggers have been initialized to avoid taking + // the lock on the most common case. + if (_logger == null) { - // We check first using the _logger to see if the loggers have been initialized to avoid taking - // the lock on the most common case. - if (_logger == null) + // We need to lock here to ensure that _constraintLogger and _logger get initialized atomically. + lock (_loggersLock) { - // We need to lock here to ensure that _constraintLogger and _logger get initialized atomically. - lock (_loggersLock) + if (_logger != null) { - if (_logger != null) - { - // Multiple threads might have tried to acquire the lock at the same time. Technically - // there is nothing wrong if things get reinitialized by a second thread, but its easy - // to prevent by just rechecking and returning here. - Debug.Assert(_constraintLogger != null); + // Multiple threads might have tried to acquire the lock at the same time. Technically + // there is nothing wrong if things get reinitialized by a second thread, but its easy + // to prevent by just rechecking and returning here. + Debug.Assert(_constraintLogger != null); - return; - } - - var factory = context.RequestServices.GetRequiredService(); - _constraintLogger = factory.CreateLogger(typeof(RouteConstraintMatcher).FullName!); - _logger = factory.CreateLogger(typeof(RouteBase).FullName!); + return; } + var factory = context.RequestServices.GetRequiredService(); + _constraintLogger = factory.CreateLogger(typeof(RouteConstraintMatcher).FullName!); + _logger = factory.CreateLogger(typeof(RouteBase).FullName!); } - Debug.Assert(_constraintLogger != null); } - [MemberNotNull(nameof(_matcher))] - private void EnsureMatcher() - { - if (_matcher == null) - { - _matcher = new TemplateMatcher(ParsedTemplate, Defaults); - } - } + Debug.Assert(_constraintLogger != null); + } - /// - public override string ToString() + [MemberNotNull(nameof(_matcher))] + private void EnsureMatcher() + { + if (_matcher == null) { - return ParsedTemplate.TemplateText!; + _matcher = new TemplateMatcher(ParsedTemplate, Defaults); } + } - private static partial class Log - { - [LoggerMessage(1, LogLevel.Debug, - "Request successfully matched the route with name '{RouteName}' and template '{RouteTemplate}'", - EventName = "RequestMatchedRoute")] - public static partial void RequestMatchedRoute(ILogger logger, string? routeName, string? routeTemplate); - } + /// + public override string ToString() + { + return ParsedTemplate.TemplateText!; + } + + private static partial class Log + { + [LoggerMessage(1, LogLevel.Debug, + "Request successfully matched the route with name '{RouteName}' and template '{RouteTemplate}'", + EventName = "RequestMatchedRoute")] + public static partial void RequestMatchedRoute(ILogger logger, string? routeName, string? routeTemplate); } } diff --git a/src/Http/Routing/src/RouteBuilder.cs b/src/Http/Routing/src/RouteBuilder.cs index 8836122f62..b40120bccd 100644 --- a/src/Http/Routing/src/RouteBuilder.cs +++ b/src/Http/Routing/src/RouteBuilder.cs @@ -8,73 +8,72 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Provides support for specifying routes in an application. +/// +public class RouteBuilder : IRouteBuilder { /// - /// Provides support for specifying routes in an application. + /// Constructs a new instance given an . /// - public class RouteBuilder : IRouteBuilder + /// An instance. + public RouteBuilder(IApplicationBuilder applicationBuilder) + : this(applicationBuilder, defaultHandler: null) { - /// - /// Constructs a new instance given an . - /// - /// An instance. - public RouteBuilder(IApplicationBuilder applicationBuilder) - : this(applicationBuilder, defaultHandler: null) + } + + /// + /// Constructs a new instance given an + /// and . + /// + /// An instance. + /// The default used if a new route is added without a handler. + public RouteBuilder(IApplicationBuilder applicationBuilder, IRouter? defaultHandler) + { + if (applicationBuilder == null) { + throw new ArgumentNullException(nameof(applicationBuilder)); } - /// - /// Constructs a new instance given an - /// and . - /// - /// An instance. - /// The default used if a new route is added without a handler. - public RouteBuilder(IApplicationBuilder applicationBuilder, IRouter? defaultHandler) + if (applicationBuilder.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null) { - if (applicationBuilder == null) - { - throw new ArgumentNullException(nameof(applicationBuilder)); - } + throw new InvalidOperationException(Resources.FormatUnableToFindServices( + nameof(IServiceCollection), + nameof(RoutingServiceCollectionExtensions.AddRouting), + "ConfigureServices(...)")); + } - if (applicationBuilder.ApplicationServices.GetService(typeof(RoutingMarkerService)) == null) - { - throw new InvalidOperationException(Resources.FormatUnableToFindServices( - nameof(IServiceCollection), - nameof(RoutingServiceCollectionExtensions.AddRouting), - "ConfigureServices(...)")); - } + ApplicationBuilder = applicationBuilder; + DefaultHandler = defaultHandler; + ServiceProvider = applicationBuilder.ApplicationServices; - ApplicationBuilder = applicationBuilder; - DefaultHandler = defaultHandler; - ServiceProvider = applicationBuilder.ApplicationServices; + Routes = new List(); + } - Routes = new List(); - } + /// + public IApplicationBuilder ApplicationBuilder { get; } - /// - public IApplicationBuilder ApplicationBuilder { get; } + /// + public IRouter? DefaultHandler { get; set; } - /// - public IRouter? DefaultHandler { get; set; } + /// + public IServiceProvider ServiceProvider { get; } - /// - public IServiceProvider ServiceProvider { get; } + /// + public IList Routes { get; } - /// - public IList Routes { get; } + /// + public IRouter Build() + { + var routeCollection = new RouteCollection(); - /// - public IRouter Build() + foreach (var route in Routes) { - var routeCollection = new RouteCollection(); - - foreach (var route in Routes) - { - routeCollection.Add(route); - } - - return routeCollection; + routeCollection.Add(route); } + + return routeCollection; } } diff --git a/src/Http/Routing/src/RouteCollection.cs b/src/Http/Routing/src/RouteCollection.cs index 55e21d4225..4d904c8078 100644 --- a/src/Http/Routing/src/RouteCollection.cs +++ b/src/Http/Routing/src/RouteCollection.cs @@ -12,195 +12,194 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Supports managing a collection for multiple routes. +/// +public class RouteCollection : IRouteCollection { + private static readonly char[] UrlQueryDelimiters = new char[] { '?', '#' }; + private readonly List _routes = new List(); + private readonly List _unnamedRoutes = new List(); + private readonly Dictionary _namedRoutes = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + private RouteOptions? _options; + /// - /// Supports managing a collection for multiple routes. + /// Gets the route at a given index. /// - public class RouteCollection : IRouteCollection + /// The route at the given index. + public IRouter this[int index] { - private static readonly char[] UrlQueryDelimiters = new char[] { '?', '#' }; - private readonly List _routes = new List(); - private readonly List _unnamedRoutes = new List(); - private readonly Dictionary _namedRoutes = - new Dictionary(StringComparer.OrdinalIgnoreCase); - - private RouteOptions? _options; - - /// - /// Gets the route at a given index. - /// - /// The route at the given index. - public IRouter this[int index] - { - get { return _routes[index]; } - } + get { return _routes[index]; } + } + + /// + /// Gets the total number of routes registered in the collection. + /// + public int Count + { + get { return _routes.Count; } + } - /// - /// Gets the total number of routes registered in the collection. - /// - public int Count + /// + public void Add(IRouter router) + { + if (router == null) { - get { return _routes.Count; } + throw new ArgumentNullException(nameof(router)); } - /// - public void Add(IRouter router) + var namedRouter = router as INamedRouter; + if (namedRouter != null) { - if (router == null) + if (!string.IsNullOrEmpty(namedRouter.Name)) { - throw new ArgumentNullException(nameof(router)); + _namedRoutes.Add(namedRouter.Name, namedRouter); } + } + else + { + _unnamedRoutes.Add(router); + } - var namedRouter = router as INamedRouter; - if (namedRouter != null) - { - if (!string.IsNullOrEmpty(namedRouter.Name)) - { - _namedRoutes.Add(namedRouter.Name, namedRouter); - } - } - else - { - _unnamedRoutes.Add(router); - } + _routes.Add(router); + } - _routes.Add(router); - } + /// + public virtual async Task RouteAsync(RouteContext context) + { + // Perf: We want to avoid allocating a new RouteData for each route we need to process. + // We can do this by snapshotting the state at the beginning and then restoring it + // for each router we execute. + var snapshot = context.RouteData.PushState(null, values: null, dataTokens: null); - /// - public virtual async Task RouteAsync(RouteContext context) + for (var i = 0; i < Count; i++) { - // Perf: We want to avoid allocating a new RouteData for each route we need to process. - // We can do this by snapshotting the state at the beginning and then restoring it - // for each router we execute. - var snapshot = context.RouteData.PushState(null, values: null, dataTokens: null); + var route = this[i]; + context.RouteData.Routers.Add(route); - for (var i = 0; i < Count; i++) + try { - var route = this[i]; - context.RouteData.Routers.Add(route); + await route.RouteAsync(context); - try + if (context.Handler != null) { - await route.RouteAsync(context); - - if (context.Handler != null) - { - break; - } + break; } - finally + } + finally + { + if (context.Handler == null) { - if (context.Handler == null) - { - snapshot.Restore(); - } + snapshot.Restore(); } } } + } + + /// + public virtual VirtualPathData? GetVirtualPath(VirtualPathContext context) + { + EnsureOptions(context.HttpContext); - /// - public virtual VirtualPathData? GetVirtualPath(VirtualPathContext context) + if (!string.IsNullOrEmpty(context.RouteName)) { - EnsureOptions(context.HttpContext); + VirtualPathData? namedRoutePathData = null; - if (!string.IsNullOrEmpty(context.RouteName)) + if (_namedRoutes.TryGetValue(context.RouteName, out var matchedNamedRoute)) { - VirtualPathData? namedRoutePathData = null; - - if (_namedRoutes.TryGetValue(context.RouteName, out var matchedNamedRoute)) - { - namedRoutePathData = matchedNamedRoute.GetVirtualPath(context); - } - - var pathData = GetVirtualPath(context, _unnamedRoutes); + namedRoutePathData = matchedNamedRoute.GetVirtualPath(context); + } - // If the named route and one of the unnamed routes also matches, then we have an ambiguity. - if (namedRoutePathData != null && pathData != null) - { - var message = Resources.FormatNamedRoutes_AmbiguousRoutesFound(context.RouteName); - throw new InvalidOperationException(message); - } + var pathData = GetVirtualPath(context, _unnamedRoutes); - return NormalizeVirtualPath(namedRoutePathData ?? pathData); - } - else + // If the named route and one of the unnamed routes also matches, then we have an ambiguity. + if (namedRoutePathData != null && pathData != null) { - return NormalizeVirtualPath(GetVirtualPath(context, _routes)); + var message = Resources.FormatNamedRoutes_AmbiguousRoutesFound(context.RouteName); + throw new InvalidOperationException(message); } - } - private static VirtualPathData? GetVirtualPath(VirtualPathContext context, List routes) + return NormalizeVirtualPath(namedRoutePathData ?? pathData); + } + else { - for (var i = 0; i < routes.Count; i++) - { - var route = routes[i]; - - var pathData = route.GetVirtualPath(context); - if (pathData != null) - { - return pathData; - } - } - - return null; + return NormalizeVirtualPath(GetVirtualPath(context, _routes)); } + } - private VirtualPathData? NormalizeVirtualPath(VirtualPathData? pathData) + private static VirtualPathData? GetVirtualPath(VirtualPathContext context, List routes) + { + for (var i = 0; i < routes.Count; i++) { - if (pathData == null) + var route = routes[i]; + + var pathData = route.GetVirtualPath(context); + if (pathData != null) { return pathData; } + } - Debug.Assert(_options != null); + return null; + } - var url = pathData.VirtualPath; + private VirtualPathData? NormalizeVirtualPath(VirtualPathData? pathData) + { + if (pathData == null) + { + return pathData; + } - if (!string.IsNullOrEmpty(url) && (_options.LowercaseUrls || _options.AppendTrailingSlash)) - { - var indexOfSeparator = url.IndexOfAny(UrlQueryDelimiters); - var urlWithoutQueryString = url; - var queryString = string.Empty; + Debug.Assert(_options != null); - if (indexOfSeparator != -1) - { - urlWithoutQueryString = url.Substring(0, indexOfSeparator); - queryString = url.Substring(indexOfSeparator); - } + var url = pathData.VirtualPath; - if (_options.LowercaseUrls) - { - urlWithoutQueryString = urlWithoutQueryString.ToLowerInvariant(); - } + if (!string.IsNullOrEmpty(url) && (_options.LowercaseUrls || _options.AppendTrailingSlash)) + { + var indexOfSeparator = url.IndexOfAny(UrlQueryDelimiters); + var urlWithoutQueryString = url; + var queryString = string.Empty; - if (_options.LowercaseUrls && _options.LowercaseQueryStrings) - { - queryString = queryString.ToLowerInvariant(); - } + if (indexOfSeparator != -1) + { + urlWithoutQueryString = url.Substring(0, indexOfSeparator); + queryString = url.Substring(indexOfSeparator); + } - if (_options.AppendTrailingSlash && !urlWithoutQueryString.EndsWith("/", StringComparison.Ordinal)) - { - urlWithoutQueryString += "/"; - } + if (_options.LowercaseUrls) + { + urlWithoutQueryString = urlWithoutQueryString.ToLowerInvariant(); + } - // queryString will contain the delimiter ? or # as the first character, so it's safe to append. - url = urlWithoutQueryString + queryString; + if (_options.LowercaseUrls && _options.LowercaseQueryStrings) + { + queryString = queryString.ToLowerInvariant(); + } - return new VirtualPathData(pathData.Router, url, pathData.DataTokens); + if (_options.AppendTrailingSlash && !urlWithoutQueryString.EndsWith("/", StringComparison.Ordinal)) + { + urlWithoutQueryString += "/"; } - return pathData; + // queryString will contain the delimiter ? or # as the first character, so it's safe to append. + url = urlWithoutQueryString + queryString; + + return new VirtualPathData(pathData.Router, url, pathData.DataTokens); } - [MemberNotNull(nameof(_options))] - private void EnsureOptions(HttpContext context) + return pathData; + } + + [MemberNotNull(nameof(_options))] + private void EnsureOptions(HttpContext context) + { + if (_options == null) { - if (_options == null) - { - _options = context.RequestServices.GetRequiredService>().Value; - } + _options = context.RequestServices.GetRequiredService>().Value; } } } diff --git a/src/Http/Routing/src/RouteConstraintBuilder.cs b/src/Http/Routing/src/RouteConstraintBuilder.cs index 287db69d6b..729ca07838 100644 --- a/src/Http/Routing/src/RouteConstraintBuilder.cs +++ b/src/Http/Routing/src/RouteConstraintBuilder.cs @@ -5,191 +5,190 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Routing.Constraints; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// A builder for produding a mapping of keys to see . +/// +/// +/// allows iterative building a set of route constraints, and will +/// merge multiple entries for the same key. +/// +public class RouteConstraintBuilder { + private readonly IInlineConstraintResolver _inlineConstraintResolver; + private readonly string _displayName; + + private readonly Dictionary> _constraints; + private readonly HashSet _optionalParameters; /// - /// A builder for produding a mapping of keys to see . + /// Creates a new instance of instance. /// - /// - /// allows iterative building a set of route constraints, and will - /// merge multiple entries for the same key. - /// - public class RouteConstraintBuilder + /// The . + /// The display name (for use in error messages). + public RouteConstraintBuilder( + IInlineConstraintResolver inlineConstraintResolver, + string displayName) { - private readonly IInlineConstraintResolver _inlineConstraintResolver; - private readonly string _displayName; - - private readonly Dictionary> _constraints; - private readonly HashSet _optionalParameters; - /// - /// Creates a new instance of instance. - /// - /// The . - /// The display name (for use in error messages). - public RouteConstraintBuilder( - IInlineConstraintResolver inlineConstraintResolver, - string displayName) + if (inlineConstraintResolver == null) { - if (inlineConstraintResolver == null) - { - throw new ArgumentNullException(nameof(inlineConstraintResolver)); - } + throw new ArgumentNullException(nameof(inlineConstraintResolver)); + } - if (displayName == null) - { - throw new ArgumentNullException(nameof(displayName)); - } + if (displayName == null) + { + throw new ArgumentNullException(nameof(displayName)); + } - _inlineConstraintResolver = inlineConstraintResolver; - _displayName = displayName; + _inlineConstraintResolver = inlineConstraintResolver; + _displayName = displayName; - _constraints = new Dictionary>(StringComparer.OrdinalIgnoreCase); - _optionalParameters = new HashSet(StringComparer.OrdinalIgnoreCase); - } + _constraints = new Dictionary>(StringComparer.OrdinalIgnoreCase); + _optionalParameters = new HashSet(StringComparer.OrdinalIgnoreCase); + } - /// - /// Builds a mapping of constraints. - /// - /// An of the constraints. - public IDictionary Build() + /// + /// Builds a mapping of constraints. + /// + /// An of the constraints. + public IDictionary Build() + { + var constraints = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in _constraints) { - var constraints = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var kvp in _constraints) + IRouteConstraint constraint; + if (kvp.Value.Count == 1) { - IRouteConstraint constraint; - if (kvp.Value.Count == 1) - { - constraint = kvp.Value[0]; - } - else - { - constraint = new CompositeRouteConstraint(kvp.Value.ToArray()); - } - - if (_optionalParameters.Contains(kvp.Key)) - { - var optionalConstraint = new OptionalRouteConstraint(constraint); - constraints.Add(kvp.Key, optionalConstraint); - } - else - { - constraints.Add(kvp.Key, constraint); - } + constraint = kvp.Value[0]; } - - return constraints; - } - - /// - /// Adds a constraint instance for the given key. - /// - /// The key. - /// - /// The constraint instance. Must either be a string or an instance of . - /// - /// - /// If the is a string, it will be converted to a . - /// - /// For example, the string Product[0-9]+ will be converted to the regular expression - /// ^(Product[0-9]+). See for more details. - /// - public void AddConstraint(string key, object value) - { - if (key == null) + else { - throw new ArgumentNullException(nameof(key)); + constraint = new CompositeRouteConstraint(kvp.Value.ToArray()); } - if (value == null) + if (_optionalParameters.Contains(kvp.Key)) { - throw new ArgumentNullException(nameof(value)); + var optionalConstraint = new OptionalRouteConstraint(constraint); + constraints.Add(kvp.Key, optionalConstraint); } - - var constraint = value as IRouteConstraint; - if (constraint == null) + else { - var regexPattern = value as string; - if (regexPattern == null) - { - throw new RouteCreationException( - Resources.FormatRouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint( - key, - value, - _displayName, - typeof(IRouteConstraint))); - } - - var constraintsRegEx = "^(" + regexPattern + ")$"; - constraint = new RegexRouteConstraint(constraintsRegEx); + constraints.Add(kvp.Key, constraint); } - - Add(key, constraint); } - /// - /// Adds a constraint for the given key, resolved by the . - /// - /// The key. - /// The text to be resolved by . - /// - /// The can create instances - /// based on . See to register - /// custom constraint types. - /// - public void AddResolvedConstraint(string key, string constraintText) + return constraints; + } + + /// + /// Adds a constraint instance for the given key. + /// + /// The key. + /// + /// The constraint instance. Must either be a string or an instance of . + /// + /// + /// If the is a string, it will be converted to a . + /// + /// For example, the string Product[0-9]+ will be converted to the regular expression + /// ^(Product[0-9]+). See for more details. + /// + public void AddConstraint(string key, object value) + { + if (key == null) { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } + throw new ArgumentNullException(nameof(key)); + } - if (constraintText == null) - { - throw new ArgumentNullException(nameof(constraintText)); - } + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } - var constraint = _inlineConstraintResolver.ResolveConstraint(constraintText); - if (constraint == null) + var constraint = value as IRouteConstraint; + if (constraint == null) + { + var regexPattern = value as string; + if (regexPattern == null) { - throw new InvalidOperationException( - Resources.FormatRouteConstraintBuilder_CouldNotResolveConstraint( + throw new RouteCreationException( + Resources.FormatRouteConstraintBuilder_ValidationMustBeStringOrCustomConstraint( key, - constraintText, + value, _displayName, - _inlineConstraintResolver.GetType().Name)); - } - else if (constraint == NullRouteConstraint.Instance) - { - // A null route constraint can be returned for other parameter policy types - return; + typeof(IRouteConstraint))); } - Add(key, constraint); + var constraintsRegEx = "^(" + regexPattern + ")$"; + constraint = new RegexRouteConstraint(constraintsRegEx); } - /// - /// Sets the given key as optional. - /// - /// The key. - public void SetOptional(string key) + Add(key, constraint); + } + + /// + /// Adds a constraint for the given key, resolved by the . + /// + /// The key. + /// The text to be resolved by . + /// + /// The can create instances + /// based on . See to register + /// custom constraint types. + /// + public void AddResolvedConstraint(string key, string constraintText) + { + if (key == null) { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } + throw new ArgumentNullException(nameof(key)); + } - _optionalParameters.Add(key); + if (constraintText == null) + { + throw new ArgumentNullException(nameof(constraintText)); } - private void Add(string key, IRouteConstraint constraint) + var constraint = _inlineConstraintResolver.ResolveConstraint(constraintText); + if (constraint == null) { - if (!_constraints.TryGetValue(key, out var list)) - { - list = new List(); - _constraints.Add(key, list); - } + throw new InvalidOperationException( + Resources.FormatRouteConstraintBuilder_CouldNotResolveConstraint( + key, + constraintText, + _displayName, + _inlineConstraintResolver.GetType().Name)); + } + else if (constraint == NullRouteConstraint.Instance) + { + // A null route constraint can be returned for other parameter policy types + return; + } + + Add(key, constraint); + } + + /// + /// Sets the given key as optional. + /// + /// The key. + public void SetOptional(string key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + _optionalParameters.Add(key); + } - list.Add(constraint); + private void Add(string key, IRouteConstraint constraint) + { + if (!_constraints.TryGetValue(key, out var list)) + { + list = new List(); + _constraints.Add(key, list); } + + list.Add(constraint); } } diff --git a/src/Http/Routing/src/RouteConstraintMatcher.cs b/src/Http/Routing/src/RouteConstraintMatcher.cs index 41a2eaf18b..40d6dd2d71 100644 --- a/src/Http/Routing/src/RouteConstraintMatcher.cs +++ b/src/Http/Routing/src/RouteConstraintMatcher.cs @@ -6,88 +6,87 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Use to evaluate if all route parameter values match their constraints. +/// +public static partial class RouteConstraintMatcher { /// - /// Use to evaluate if all route parameter values match their constraints. + /// Determines if match the provided . /// - public static partial class RouteConstraintMatcher + /// The constraints for the route. + /// The route parameter values extracted from the matched route. + /// The associated with the current request. + /// The router that this constraint belongs to. + /// + /// Indicates whether the constraint check is performed + /// when the incoming request is handled or when a URL is generated. + /// + /// The . + /// if the all route values match their constraints. + public static bool Match( + IDictionary constraints, + RouteValueDictionary routeValues, + HttpContext httpContext, + IRouter route, + RouteDirection routeDirection, + ILogger logger) { - /// - /// Determines if match the provided . - /// - /// The constraints for the route. - /// The route parameter values extracted from the matched route. - /// The associated with the current request. - /// The router that this constraint belongs to. - /// - /// Indicates whether the constraint check is performed - /// when the incoming request is handled or when a URL is generated. - /// - /// The . - /// if the all route values match their constraints. - public static bool Match( - IDictionary constraints, - RouteValueDictionary routeValues, - HttpContext httpContext, - IRouter route, - RouteDirection routeDirection, - ILogger logger) + if (routeValues == null) { - if (routeValues == null) - { - throw new ArgumentNullException(nameof(routeValues)); - } + throw new ArgumentNullException(nameof(routeValues)); + } - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } - if (route == null) - { - throw new ArgumentNullException(nameof(route)); - } + if (route == null) + { + throw new ArgumentNullException(nameof(route)); + } - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } - if (constraints == null || constraints.Count == 0) - { - return true; - } + if (constraints == null || constraints.Count == 0) + { + return true; + } - foreach (var kvp in constraints) + foreach (var kvp in constraints) + { + var constraint = kvp.Value; + if (!constraint.Match(httpContext, route, kvp.Key, routeValues, routeDirection)) { - var constraint = kvp.Value; - if (!constraint.Match(httpContext, route, kvp.Key, routeValues, routeDirection)) + if (routeDirection.Equals(RouteDirection.IncomingRequest)) { - if (routeDirection.Equals(RouteDirection.IncomingRequest)) - { - routeValues.TryGetValue(kvp.Key, out var routeValue); + routeValues.TryGetValue(kvp.Key, out var routeValue); - Log.ConstraintNotMatched(logger, routeValue!, kvp.Key, kvp.Value); - } - - return false; + Log.ConstraintNotMatched(logger, routeValue!, kvp.Key, kvp.Value); } - } - return true; + return false; + } } - private static partial class Log - { - [LoggerMessage(1, LogLevel.Debug, - "Route value '{RouteValue}' with key '{RouteKey}' did not match the constraint '{RouteConstraint}'", - EventName = "ConstraintNotMatched")] - public static partial void ConstraintNotMatched( - ILogger logger, - object routeValue, - string routeKey, - IRouteConstraint routeConstraint); - } + return true; + } + + private static partial class Log + { + [LoggerMessage(1, LogLevel.Debug, + "Route value '{RouteValue}' with key '{RouteKey}' did not match the constraint '{RouteConstraint}'", + EventName = "ConstraintNotMatched")] + public static partial void ConstraintNotMatched( + ILogger logger, + object routeValue, + string routeKey, + IRouteConstraint routeConstraint); } } diff --git a/src/Http/Routing/src/RouteCreationException.cs b/src/Http/Routing/src/RouteCreationException.cs index 14c79faad9..f17bdac29f 100644 --- a/src/Http/Routing/src/RouteCreationException.cs +++ b/src/Http/Routing/src/RouteCreationException.cs @@ -3,31 +3,30 @@ using System; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// The exception that is thrown for invalid routes or constraints. +/// +public class RouteCreationException : Exception { /// - /// The exception that is thrown for invalid routes or constraints. + /// Initializes a new instance of the class with a specified error message. /// - public class RouteCreationException : Exception + /// The message that describes the error. + public RouteCreationException(string message) + : base(message) { - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// The message that describes the error. - public RouteCreationException(string message) - : base(message) - { - } + } - /// - /// Initializes a new instance of the class with a specified error message - /// and a reference to the inner exception that is the cause of this exception. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception. - public RouteCreationException(string message, Exception innerException) - : base(message, innerException) - { - } + /// + /// Initializes a new instance of the class with a specified error message + /// and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + public RouteCreationException(string message, Exception innerException) + : base(message, innerException) + { } } diff --git a/src/Http/Routing/src/RouteEndpoint.cs b/src/Http/Routing/src/RouteEndpoint.cs index 693f79ec15..1cde8a0843 100644 --- a/src/Http/Routing/src/RouteEndpoint.cs +++ b/src/Http/Routing/src/RouteEndpoint.cs @@ -7,57 +7,56 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Patterns; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Represents an that can be used in URL matching or URL generation. +/// +public sealed class RouteEndpoint : Endpoint { /// - /// Represents an that can be used in URL matching or URL generation. + /// Initializes a new instance of the class. /// - public sealed class RouteEndpoint : Endpoint + /// The delegate used to process requests for the endpoint. + /// The to use in URL matching. + /// The order assigned to the endpoint. + /// + /// The or metadata associated with the endpoint. + /// + /// The informational display name of the endpoint. + public RouteEndpoint( + RequestDelegate requestDelegate, + RoutePattern routePattern, + int order, + EndpointMetadataCollection? metadata, + string? displayName) + : base(requestDelegate, metadata, displayName) { - /// - /// Initializes a new instance of the class. - /// - /// The delegate used to process requests for the endpoint. - /// The to use in URL matching. - /// The order assigned to the endpoint. - /// - /// The or metadata associated with the endpoint. - /// - /// The informational display name of the endpoint. - public RouteEndpoint( - RequestDelegate requestDelegate, - RoutePattern routePattern, - int order, - EndpointMetadataCollection? metadata, - string? displayName) - : base(requestDelegate, metadata, displayName) + if (requestDelegate == null) { - if (requestDelegate == null) - { - throw new ArgumentNullException(nameof(requestDelegate)); - } - - if (routePattern == null) - { - throw new ArgumentNullException(nameof(routePattern)); - } - - RoutePattern = routePattern; - Order = order; + throw new ArgumentNullException(nameof(requestDelegate)); } - /// - /// Gets the order value of endpoint. - /// - /// - /// The order value provides absolute control over the priority - /// of an endpoint. Endpoints with a lower numeric value of order have higher priority. - /// - public int Order { get; } + if (routePattern == null) + { + throw new ArgumentNullException(nameof(routePattern)); + } - /// - /// Gets the associated with the endpoint. - /// - public RoutePattern RoutePattern { get; } + RoutePattern = routePattern; + Order = order; } + + /// + /// Gets the order value of endpoint. + /// + /// + /// The order value provides absolute control over the priority + /// of an endpoint. Endpoints with a lower numeric value of order have higher priority. + /// + public int Order { get; } + + /// + /// Gets the associated with the endpoint. + /// + public RoutePattern RoutePattern { get; } } diff --git a/src/Http/Routing/src/RouteEndpointBuilder.cs b/src/Http/Routing/src/RouteEndpointBuilder.cs index 73dcc9cf79..be8cc2cb97 100644 --- a/src/Http/Routing/src/RouteEndpointBuilder.cs +++ b/src/Http/Routing/src/RouteEndpointBuilder.cs @@ -6,55 +6,54 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Patterns; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Supports building a new . +/// +public sealed class RouteEndpointBuilder : EndpointBuilder { /// - /// Supports building a new . + /// Gets or sets the associated with this endpoint. + /// + public RoutePattern RoutePattern { get; set; } + + /// + /// Gets or sets the order assigned to the endpoint. + /// + public int Order { get; set; } + + /// + /// Constructs a new instance. /// - public sealed class RouteEndpointBuilder : EndpointBuilder + /// The delegate used to process requests for the endpoint. + /// The to use in URL matching. + /// The order assigned to the endpoint. + public RouteEndpointBuilder( + RequestDelegate requestDelegate, + RoutePattern routePattern, + int order) { - /// - /// Gets or sets the associated with this endpoint. - /// - public RoutePattern RoutePattern { get; set; } - - /// - /// Gets or sets the order assigned to the endpoint. - /// - public int Order { get; set; } - - /// - /// Constructs a new instance. - /// - /// The delegate used to process requests for the endpoint. - /// The to use in URL matching. - /// The order assigned to the endpoint. - public RouteEndpointBuilder( - RequestDelegate requestDelegate, - RoutePattern routePattern, - int order) - { - RequestDelegate = requestDelegate; - RoutePattern = routePattern; - Order = order; - } + RequestDelegate = requestDelegate; + RoutePattern = routePattern; + Order = order; + } - /// - public override Endpoint Build() + /// + public override Endpoint Build() + { + if (RequestDelegate is null) { - if (RequestDelegate is null) - { - throw new InvalidOperationException($"{nameof(RequestDelegate)} must be specified to construct a {nameof(RouteEndpoint)}."); - } - - var routeEndpoint = new RouteEndpoint( - RequestDelegate, - RoutePattern, - Order, - new EndpointMetadataCollection(Metadata), - DisplayName); - - return routeEndpoint; + throw new InvalidOperationException($"{nameof(RequestDelegate)} must be specified to construct a {nameof(RouteEndpoint)}."); } + + var routeEndpoint = new RouteEndpoint( + RequestDelegate, + RoutePattern, + Order, + new EndpointMetadataCollection(Metadata), + DisplayName); + + return routeEndpoint; } } diff --git a/src/Http/Routing/src/RouteHandler.cs b/src/Http/Routing/src/RouteHandler.cs index a255ff99a0..3849ca0ae2 100644 --- a/src/Http/Routing/src/RouteHandler.cs +++ b/src/Http/Routing/src/RouteHandler.cs @@ -6,42 +6,41 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Supports implementing a handler that executes for a given route. +/// +public class RouteHandler : IRouteHandler, IRouter { + private readonly RequestDelegate _requestDelegate; + /// - /// Supports implementing a handler that executes for a given route. + /// Constructs a new instance. /// - public class RouteHandler : IRouteHandler, IRouter + /// The delegate used to process requests. + public RouteHandler(RequestDelegate requestDelegate) + { + _requestDelegate = requestDelegate; + } + + /// + public RequestDelegate GetRequestHandler(HttpContext httpContext, RouteData routeData) + { + return _requestDelegate; + } + + /// + public VirtualPathData? GetVirtualPath(VirtualPathContext context) + { + // Nothing to do. + return null; + } + + /// + public Task RouteAsync(RouteContext context) { - private readonly RequestDelegate _requestDelegate; - - /// - /// Constructs a new instance. - /// - /// The delegate used to process requests. - public RouteHandler(RequestDelegate requestDelegate) - { - _requestDelegate = requestDelegate; - } - - /// - public RequestDelegate GetRequestHandler(HttpContext httpContext, RouteData routeData) - { - return _requestDelegate; - } - - /// - public VirtualPathData? GetVirtualPath(VirtualPathContext context) - { - // Nothing to do. - return null; - } - - /// - public Task RouteAsync(RouteContext context) - { - context.Handler = _requestDelegate; - return Task.CompletedTask; - } + context.Handler = _requestDelegate; + return Task.CompletedTask; } } diff --git a/src/Http/Routing/src/RouteHandlerOptions.cs b/src/Http/Routing/src/RouteHandlerOptions.cs index b16f6db6f2..4fb2106e17 100644 --- a/src/Http/Routing/src/RouteHandlerOptions.cs +++ b/src/Http/Routing/src/RouteHandlerOptions.cs @@ -6,21 +6,20 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Options for controlling the behavior of +/// and similar methods. +/// +public sealed class RouteHandlerOptions { /// - /// Options for controlling the behavior of - /// and similar methods. + /// Controls whether endpoints should throw a in addition to + /// writing a log when handling invalid requests. /// - public sealed class RouteHandlerOptions - { - /// - /// Controls whether endpoints should throw a in addition to - /// writing a log when handling invalid requests. - /// - /// - /// Defaults to . - /// - public bool ThrowOnBadRequest { get; set; } - } + /// + /// Defaults to . + /// + public bool ThrowOnBadRequest { get; set; } } diff --git a/src/Http/Routing/src/RouteNameMetadata.cs b/src/Http/Routing/src/RouteNameMetadata.cs index e60b98406a..9f7a1f9470 100644 --- a/src/Http/Routing/src/RouteNameMetadata.cs +++ b/src/Http/Routing/src/RouteNameMetadata.cs @@ -4,31 +4,30 @@ using System; using System.Diagnostics; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Metadata used during link generation to find the associated endpoint using route name. +/// +[DebuggerDisplay("{DebuggerToString(),nq}")] +public sealed class RouteNameMetadata : IRouteNameMetadata { /// - /// Metadata used during link generation to find the associated endpoint using route name. + /// Creates a new instance of with the provided route name. /// - [DebuggerDisplay("{DebuggerToString(),nq}")] - public sealed class RouteNameMetadata : IRouteNameMetadata + /// The route name. Can be . + public RouteNameMetadata(string? routeName) { - /// - /// Creates a new instance of with the provided route name. - /// - /// The route name. Can be . - public RouteNameMetadata(string? routeName) - { - RouteName = routeName; - } + RouteName = routeName; + } - /// - /// Gets the route name. Can be . - /// - public string? RouteName { get; } + /// + /// Gets the route name. Can be . + /// + public string? RouteName { get; } - internal string DebuggerToString() - { - return $"Name: {RouteName}"; - } + internal string DebuggerToString() + { + return $"Name: {RouteName}"; } } diff --git a/src/Http/Routing/src/RouteOptions.cs b/src/Http/Routing/src/RouteOptions.cs index 1a2e81f66b..48f691d527 100644 --- a/src/Http/Routing/src/RouteOptions.cs +++ b/src/Http/Routing/src/RouteOptions.cs @@ -7,153 +7,152 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Routing.Constraints; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Represents the configurable options on a route. +/// +public class RouteOptions { + private IDictionary _constraintTypeMap = GetDefaultConstraintMap(); + private ICollection _endpointDataSources = default!; + /// - /// Represents the configurable options on a route. + /// Gets a collection of instances configured with routing. /// - public class RouteOptions + internal ICollection EndpointDataSources { - private IDictionary _constraintTypeMap = GetDefaultConstraintMap(); - private ICollection _endpointDataSources = default!; - - /// - /// Gets a collection of instances configured with routing. - /// - internal ICollection EndpointDataSources + get { - get - { - Debug.Assert(_endpointDataSources != null, "Endpoint data sources should have been set in DI."); - return _endpointDataSources; - } - set => _endpointDataSources = value; + Debug.Assert(_endpointDataSources != null, "Endpoint data sources should have been set in DI."); + return _endpointDataSources; } + set => _endpointDataSources = value; + } - /// - /// Gets or sets a value indicating whether all generated paths URLs are lowercase. - /// Use to configure the behavior for query strings. - /// - public bool LowercaseUrls { get; set; } - - /// - /// Gets or sets a value indicating whether a generated query strings are lowercase. - /// This property will not be used unless is also true. - /// - public bool LowercaseQueryStrings { get; set; } - - /// - /// Gets or sets a value indicating whether a trailing slash should be appended to the generated URLs. - /// - public bool AppendTrailingSlash { get; set; } - - /// - /// Gets or sets a value that indicates if the check for unhandled security endpoint metadata is suppressed. - /// - /// Endpoints can be associated with metadata such as authorization, or CORS, that needs to be - /// handled by a specific middleware to be actionable. If the middleware is not configured, such - /// metadata will go unhandled. - /// - /// - /// When , prior to the execution of the endpoint, routing will verify that - /// all known security-specific metadata has been handled. - /// Setting this property to suppresses this check. - /// - /// - /// Defaults to . - /// - /// This check exists as a safeguard against accidental insecure configuration. You may suppress - /// this check if it does not match your application's requirements. - /// - public bool SuppressCheckForUnhandledSecurityMetadata { get; set; } - - /// - /// Gets or sets a collection of constraints on the current route. - /// - public IDictionary ConstraintMap - { - [RequiresUnreferencedCode($"The linker cannot determine what constraints are being added via the ConstraintMap property. Prefer {nameof(RouteOptions)}.{nameof(SetParameterPolicy)} instead for setting constraints. This warning can be suppressed if this property is being used to read of delete constraints.")] - get - { - return _constraintTypeMap; - } - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(ConstraintMap)); - } + /// + /// Gets or sets a value indicating whether all generated paths URLs are lowercase. + /// Use to configure the behavior for query strings. + /// + public bool LowercaseUrls { get; set; } - _constraintTypeMap = value; - } - } + /// + /// Gets or sets a value indicating whether a generated query strings are lowercase. + /// This property will not be used unless is also true. + /// + public bool LowercaseQueryStrings { get; set; } - private static IDictionary GetDefaultConstraintMap() - { - var defaults = new Dictionary(StringComparer.OrdinalIgnoreCase); - - // Type-specific constraints - AddConstraint(defaults, "int"); - AddConstraint(defaults, "bool"); - AddConstraint(defaults, "datetime"); - AddConstraint(defaults, "decimal"); - AddConstraint(defaults, "double"); - AddConstraint(defaults, "float"); - AddConstraint(defaults, "guid"); - AddConstraint(defaults, "long"); - - // Length constraints - AddConstraint(defaults, "minlength"); - AddConstraint(defaults, "maxlength"); - AddConstraint(defaults, "length"); - - // Min/Max value constraints - AddConstraint(defaults, "min"); - AddConstraint(defaults, "max"); - AddConstraint(defaults, "range"); - - // Regex-based constraints - AddConstraint(defaults, "alpha"); - AddConstraint(defaults, "regex"); - - AddConstraint(defaults, "required"); - - // Files - AddConstraint(defaults, "file"); - AddConstraint(defaults, "nonfile"); - - return defaults; - } + /// + /// Gets or sets a value indicating whether a trailing slash should be appended to the generated URLs. + /// + public bool AppendTrailingSlash { get; set; } - /// - /// Adds or overwrites the parameter policy with the associated route pattern token. - /// - /// The parameter policy type. - /// The route token used to apply the parameter policy. - public void SetParameterPolicy<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]T>(string token) where T : IParameterPolicy + /// + /// Gets or sets a value that indicates if the check for unhandled security endpoint metadata is suppressed. + /// + /// Endpoints can be associated with metadata such as authorization, or CORS, that needs to be + /// handled by a specific middleware to be actionable. If the middleware is not configured, such + /// metadata will go unhandled. + /// + /// + /// When , prior to the execution of the endpoint, routing will verify that + /// all known security-specific metadata has been handled. + /// Setting this property to suppresses this check. + /// + /// + /// Defaults to . + /// + /// This check exists as a safeguard against accidental insecure configuration. You may suppress + /// this check if it does not match your application's requirements. + /// + public bool SuppressCheckForUnhandledSecurityMetadata { get; set; } + + /// + /// Gets or sets a collection of constraints on the current route. + /// + public IDictionary ConstraintMap + { + [RequiresUnreferencedCode($"The linker cannot determine what constraints are being added via the ConstraintMap property. Prefer {nameof(RouteOptions)}.{nameof(SetParameterPolicy)} instead for setting constraints. This warning can be suppressed if this property is being used to read of delete constraints.")] + get { - ConstraintMap[token] = typeof(T); + return _constraintTypeMap; } - - /// - /// Adds or overwrites the parameter policy with the associated route pattern token. - /// - /// The route token used to apply the parameter policy. - /// The parameter policy type. - /// Throws an exception if the type is not an . - public void SetParameterPolicy(string token, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type) + set { - if (!type.IsAssignableTo(typeof(IParameterPolicy))) + if (value == null) { - throw new InvalidOperationException($"{type} must implement {typeof(IParameterPolicy)}"); + throw new ArgumentNullException(nameof(ConstraintMap)); } - ConstraintMap[token] = type; + _constraintTypeMap = value; } + } + + private static IDictionary GetDefaultConstraintMap() + { + var defaults = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // Type-specific constraints + AddConstraint(defaults, "int"); + AddConstraint(defaults, "bool"); + AddConstraint(defaults, "datetime"); + AddConstraint(defaults, "decimal"); + AddConstraint(defaults, "double"); + AddConstraint(defaults, "float"); + AddConstraint(defaults, "guid"); + AddConstraint(defaults, "long"); + + // Length constraints + AddConstraint(defaults, "minlength"); + AddConstraint(defaults, "maxlength"); + AddConstraint(defaults, "length"); + + // Min/Max value constraints + AddConstraint(defaults, "min"); + AddConstraint(defaults, "max"); + AddConstraint(defaults, "range"); + + // Regex-based constraints + AddConstraint(defaults, "alpha"); + AddConstraint(defaults, "regex"); + + AddConstraint(defaults, "required"); + + // Files + AddConstraint(defaults, "file"); + AddConstraint(defaults, "nonfile"); + + return defaults; + } - private static void AddConstraint<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]TConstraint>(Dictionary constraintMap, string text) where TConstraint : IRouteConstraint + /// + /// Adds or overwrites the parameter policy with the associated route pattern token. + /// + /// The parameter policy type. + /// The route token used to apply the parameter policy. + public void SetParameterPolicy<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]T>(string token) where T : IParameterPolicy + { + ConstraintMap[token] = typeof(T); + } + + /// + /// Adds or overwrites the parameter policy with the associated route pattern token. + /// + /// The route token used to apply the parameter policy. + /// The parameter policy type. + /// Throws an exception if the type is not an . + public void SetParameterPolicy(string token, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type) + { + if (!type.IsAssignableTo(typeof(IParameterPolicy))) { - constraintMap[text] = typeof(TConstraint); + throw new InvalidOperationException($"{type} must implement {typeof(IParameterPolicy)}"); } + + ConstraintMap[token] = type; + } + + private static void AddConstraint<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]TConstraint>(Dictionary constraintMap, string text) where TConstraint : IRouteConstraint + { + constraintMap[text] = typeof(TConstraint); } } diff --git a/src/Http/Routing/src/RouteValueEqualityComparer.cs b/src/Http/Routing/src/RouteValueEqualityComparer.cs index f38980ddbe..3960d2329d 100644 --- a/src/Http/Routing/src/RouteValueEqualityComparer.cs +++ b/src/Http/Routing/src/RouteValueEqualityComparer.cs @@ -5,54 +5,53 @@ using System; using System.Collections.Generic; using System.Globalization; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// An implementation that compares objects as-if +/// they were route value strings. +/// +/// +/// Values that are are not strings are converted to strings using +/// Convert.ToString(x, CultureInfo.InvariantCulture). null values are converted +/// to the empty string. +/// +/// strings are compared using . +/// +public class RouteValueEqualityComparer : IEqualityComparer { /// - /// An implementation that compares objects as-if - /// they were route value strings. + /// A default instance of the . /// - /// - /// Values that are are not strings are converted to strings using - /// Convert.ToString(x, CultureInfo.InvariantCulture). null values are converted - /// to the empty string. - /// - /// strings are compared using . - /// - public class RouteValueEqualityComparer : IEqualityComparer + public static readonly RouteValueEqualityComparer Default = new RouteValueEqualityComparer(); + + /// + public new bool Equals(object? x, object? y) { - /// - /// A default instance of the . - /// - public static readonly RouteValueEqualityComparer Default = new RouteValueEqualityComparer(); + var stringX = x as string ?? Convert.ToString(x, CultureInfo.InvariantCulture); + var stringY = y as string ?? Convert.ToString(y, CultureInfo.InvariantCulture); - /// - public new bool Equals(object? x, object? y) + if (string.IsNullOrEmpty(stringX) && string.IsNullOrEmpty(stringY)) { - var stringX = x as string ?? Convert.ToString(x, CultureInfo.InvariantCulture); - var stringY = y as string ?? Convert.ToString(y, CultureInfo.InvariantCulture); - - if (string.IsNullOrEmpty(stringX) && string.IsNullOrEmpty(stringY)) - { - return true; - } - else - { - return string.Equals(stringX, stringY, StringComparison.OrdinalIgnoreCase); - } + return true; + } + else + { + return string.Equals(stringX, stringY, StringComparison.OrdinalIgnoreCase); } + } - /// - public int GetHashCode(object obj) + /// + public int GetHashCode(object obj) + { + var stringObj = obj as string ?? Convert.ToString(obj, CultureInfo.InvariantCulture); + if (string.IsNullOrEmpty(stringObj)) + { + return StringComparer.OrdinalIgnoreCase.GetHashCode(string.Empty); + } + else { - var stringObj = obj as string ?? Convert.ToString(obj, CultureInfo.InvariantCulture); - if (string.IsNullOrEmpty(stringObj)) - { - return StringComparer.OrdinalIgnoreCase.GetHashCode(string.Empty); - } - else - { - return StringComparer.OrdinalIgnoreCase.GetHashCode(stringObj); - } + return StringComparer.OrdinalIgnoreCase.GetHashCode(stringObj); } } } diff --git a/src/Http/Routing/src/RouteValuesAddress.cs b/src/Http/Routing/src/RouteValuesAddress.cs index 54ce175c47..23ed96b10b 100644 --- a/src/Http/Routing/src/RouteValuesAddress.cs +++ b/src/Http/Routing/src/RouteValuesAddress.cs @@ -3,26 +3,25 @@ #nullable enable -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// An address of route name and values. +/// +public class RouteValuesAddress { /// - /// An address of route name and values. + /// Gets or sets the route name. /// - public class RouteValuesAddress - { - /// - /// Gets or sets the route name. - /// - public string? RouteName { get; set; } + public string? RouteName { get; set; } - /// - /// Gets or sets the route values that are explicitly specified. - /// - public RouteValueDictionary ExplicitValues { get; set; } = default!; + /// + /// Gets or sets the route values that are explicitly specified. + /// + public RouteValueDictionary ExplicitValues { get; set; } = default!; - /// - /// Gets or sets ambient route values from the current HTTP request. - /// - public RouteValueDictionary? AmbientValues { get; set; } - } + /// + /// Gets or sets ambient route values from the current HTTP request. + /// + public RouteValueDictionary? AmbientValues { get; set; } } diff --git a/src/Http/Routing/src/RouteValuesAddressScheme.cs b/src/Http/Routing/src/RouteValuesAddressScheme.cs index e9e7db70fe..12043f677b 100644 --- a/src/Http/Routing/src/RouteValuesAddressScheme.cs +++ b/src/Http/Routing/src/RouteValuesAddressScheme.cs @@ -7,186 +7,185 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Template; using Microsoft.AspNetCore.Routing.Tree; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +internal sealed class RouteValuesAddressScheme : IEndpointAddressScheme, IDisposable { - internal sealed class RouteValuesAddressScheme : IEndpointAddressScheme, IDisposable + private readonly DataSourceDependentCache _cache; + + public RouteValuesAddressScheme(EndpointDataSource dataSource) { - private readonly DataSourceDependentCache _cache; + _cache = new DataSourceDependentCache(dataSource, Initialize); + } + + // Internal for tests + internal StateEntry State => _cache.EnsureInitialized(); - public RouteValuesAddressScheme(EndpointDataSource dataSource) + public IEnumerable FindEndpoints(RouteValuesAddress address) + { + if (address == null) { - _cache = new DataSourceDependentCache(dataSource, Initialize); + throw new ArgumentNullException(nameof(address)); } - // Internal for tests - internal StateEntry State => _cache.EnsureInitialized(); + var state = State; - public IEnumerable FindEndpoints(RouteValuesAddress address) + IList? matchResults = null; + if (string.IsNullOrEmpty(address.RouteName)) { - if (address == null) - { - throw new ArgumentNullException(nameof(address)); - } - - var state = State; - - IList? matchResults = null; - if (string.IsNullOrEmpty(address.RouteName)) - { - matchResults = state.AllMatchesLinkGenerationTree.GetMatches( - address.ExplicitValues, - address.AmbientValues); - } - else if (state.NamedMatches.TryGetValue(address.RouteName, out var namedMatchResults)) - { - matchResults = namedMatchResults; - } + matchResults = state.AllMatchesLinkGenerationTree.GetMatches( + address.ExplicitValues, + address.AmbientValues); + } + else if (state.NamedMatches.TryGetValue(address.RouteName, out var namedMatchResults)) + { + matchResults = namedMatchResults; + } - if (matchResults != null) + if (matchResults != null) + { + var matchCount = matchResults.Count; + if (matchCount > 0) { - var matchCount = matchResults.Count; - if (matchCount > 0) + if (matchResults.Count == 1) + { + // Special case having a single result to avoid creating iterator state machine + return new[] { (RouteEndpoint)matchResults[0].Match.Entry.Data }; + } + else { - if (matchResults.Count == 1) - { - // Special case having a single result to avoid creating iterator state machine - return new[] { (RouteEndpoint)matchResults[0].Match.Entry.Data }; - } - else - { - // Use separate method since one cannot have regular returns in an iterator method - return GetEndpoints(matchResults, matchCount); - } + // Use separate method since one cannot have regular returns in an iterator method + return GetEndpoints(matchResults, matchCount); } } - - return Array.Empty(); } - private static IEnumerable GetEndpoints(IList matchResults, int matchCount) + return Array.Empty(); + } + + private static IEnumerable GetEndpoints(IList matchResults, int matchCount) + { + for (var i = 0; i < matchCount; i++) { - for (var i = 0; i < matchCount; i++) - { - yield return (RouteEndpoint)matchResults[i].Match.Entry.Data; - } + yield return (RouteEndpoint)matchResults[i].Match.Entry.Data; } + } - private StateEntry Initialize(IReadOnlyList endpoints) + private StateEntry Initialize(IReadOnlyList endpoints) + { + var matchesWithRequiredValues = new List(); + var namedOutboundMatchResults = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + // Decision tree is built using the 'required values' of actions. + // - When generating a url using route values, decision tree checks the explicitly supplied route values + + // ambient values to see if they have a match for the required-values-based-tree. + // - When generating a url using route name, route values for controller, action etc.might not be provided + // (this is expected because as a user I want to avoid writing all those and instead chose to use a + // routename which is quick). So since these values are not provided and might not be even in ambient + // values, decision tree would fail to find a match. So for this reason decision tree is not used for named + // matches. Instead all named matches are returned as is and the LinkGenerator uses a TemplateBinder to + // decide which of the matches can generate a url. + // For example, for a route defined like below with current ambient values like new { controller = "Home", + // action = "Index" } + // "api/orders/{id}", + // routeName: "OrdersApi", + // defaults: new { controller = "Orders", action = "GetById" }, + // requiredValues: new { controller = "Orders", action = "GetById" }, + // A call to GetLink("OrdersApi", new { id = "10" }) cannot generate url as neither the supplied values or + // current ambient values do not satisfy the decision tree that is built based on the required values. + for (var i = 0; i < endpoints.Count; i++) { - var matchesWithRequiredValues = new List(); - var namedOutboundMatchResults = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - // Decision tree is built using the 'required values' of actions. - // - When generating a url using route values, decision tree checks the explicitly supplied route values + - // ambient values to see if they have a match for the required-values-based-tree. - // - When generating a url using route name, route values for controller, action etc.might not be provided - // (this is expected because as a user I want to avoid writing all those and instead chose to use a - // routename which is quick). So since these values are not provided and might not be even in ambient - // values, decision tree would fail to find a match. So for this reason decision tree is not used for named - // matches. Instead all named matches are returned as is and the LinkGenerator uses a TemplateBinder to - // decide which of the matches can generate a url. - // For example, for a route defined like below with current ambient values like new { controller = "Home", - // action = "Index" } - // "api/orders/{id}", - // routeName: "OrdersApi", - // defaults: new { controller = "Orders", action = "GetById" }, - // requiredValues: new { controller = "Orders", action = "GetById" }, - // A call to GetLink("OrdersApi", new { id = "10" }) cannot generate url as neither the supplied values or - // current ambient values do not satisfy the decision tree that is built based on the required values. - for (var i = 0; i < endpoints.Count; i++) + var endpoint = endpoints[i]; + if (!(endpoint is RouteEndpoint routeEndpoint)) { - var endpoint = endpoints[i]; - if (!(endpoint is RouteEndpoint routeEndpoint)) - { - continue; - } - - var metadata = endpoint.Metadata.GetMetadata(); - if (metadata == null && routeEndpoint.RoutePattern.RequiredValues.Count == 0) - { - continue; - } - - if (endpoint.Metadata.GetMetadata()?.SuppressLinkGeneration == true) - { - continue; - } + continue; + } - var entry = CreateOutboundRouteEntry( - routeEndpoint, - routeEndpoint.RoutePattern.RequiredValues, - metadata?.RouteName); + var metadata = endpoint.Metadata.GetMetadata(); + if (metadata == null && routeEndpoint.RoutePattern.RequiredValues.Count == 0) + { + continue; + } - var outboundMatch = new OutboundMatch() { Entry = entry }; + if (endpoint.Metadata.GetMetadata()?.SuppressLinkGeneration == true) + { + continue; + } - if (routeEndpoint.RoutePattern.RequiredValues.Count > 0) - { - // Entries with a route name but no required values can only be matched by name. - // Otherwise, these endpoints will match any attempt at action link generation. - // Entries with neither a route name nor required values have already been skipped above. - // See https://github.com/dotnet/aspnetcore/issues/35592 - matchesWithRequiredValues.Add(outboundMatch); - } + var entry = CreateOutboundRouteEntry( + routeEndpoint, + routeEndpoint.RoutePattern.RequiredValues, + metadata?.RouteName); - if (string.IsNullOrEmpty(entry.RouteName)) - { - continue; - } + var outboundMatch = new OutboundMatch() { Entry = entry }; - if (!namedOutboundMatchResults.TryGetValue(entry.RouteName, out var matchResults)) - { - matchResults = new List(); - namedOutboundMatchResults.Add(entry.RouteName, matchResults); - } - matchResults.Add(new OutboundMatchResult(outboundMatch, isFallbackMatch: false)); + if (routeEndpoint.RoutePattern.RequiredValues.Count > 0) + { + // Entries with a route name but no required values can only be matched by name. + // Otherwise, these endpoints will match any attempt at action link generation. + // Entries with neither a route name nor required values have already been skipped above. + // See https://github.com/dotnet/aspnetcore/issues/35592 + matchesWithRequiredValues.Add(outboundMatch); } - return new StateEntry( - matchesWithRequiredValues, - new LinkGenerationDecisionTree(matchesWithRequiredValues), - namedOutboundMatchResults); - } + if (string.IsNullOrEmpty(entry.RouteName)) + { + continue; + } - private static OutboundRouteEntry CreateOutboundRouteEntry( - RouteEndpoint endpoint, - IReadOnlyDictionary requiredValues, - string? routeName) - { - var entry = new OutboundRouteEntry() + if (!namedOutboundMatchResults.TryGetValue(entry.RouteName, out var matchResults)) { - Handler = NullRouter.Instance, - Order = endpoint.Order, - Precedence = RoutePrecedence.ComputeOutbound(endpoint.RoutePattern), - RequiredLinkValues = new RouteValueDictionary(requiredValues), - RouteTemplate = new RouteTemplate(endpoint.RoutePattern), - Data = endpoint, - RouteName = routeName, - }; - entry.Defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults); - return entry; + matchResults = new List(); + namedOutboundMatchResults.Add(entry.RouteName, matchResults); + } + matchResults.Add(new OutboundMatchResult(outboundMatch, isFallbackMatch: false)); } - public void Dispose() + return new StateEntry( + matchesWithRequiredValues, + new LinkGenerationDecisionTree(matchesWithRequiredValues), + namedOutboundMatchResults); + } + + private static OutboundRouteEntry CreateOutboundRouteEntry( + RouteEndpoint endpoint, + IReadOnlyDictionary requiredValues, + string? routeName) + { + var entry = new OutboundRouteEntry() { - _cache.Dispose(); - } + Handler = NullRouter.Instance, + Order = endpoint.Order, + Precedence = RoutePrecedence.ComputeOutbound(endpoint.RoutePattern), + RequiredLinkValues = new RouteValueDictionary(requiredValues), + RouteTemplate = new RouteTemplate(endpoint.RoutePattern), + Data = endpoint, + RouteName = routeName, + }; + entry.Defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults); + return entry; + } + + public void Dispose() + { + _cache.Dispose(); + } - internal class StateEntry + internal class StateEntry + { + // For testing + public readonly List MatchesWithRequiredValues; + public readonly LinkGenerationDecisionTree AllMatchesLinkGenerationTree; + public readonly Dictionary> NamedMatches; + + public StateEntry( + List matchesWithRequiredValues, + LinkGenerationDecisionTree allMatchesLinkGenerationTree, + Dictionary> namedMatches) { - // For testing - public readonly List MatchesWithRequiredValues; - public readonly LinkGenerationDecisionTree AllMatchesLinkGenerationTree; - public readonly Dictionary> NamedMatches; - - public StateEntry( - List matchesWithRequiredValues, - LinkGenerationDecisionTree allMatchesLinkGenerationTree, - Dictionary> namedMatches) - { - MatchesWithRequiredValues = matchesWithRequiredValues; - AllMatchesLinkGenerationTree = allMatchesLinkGenerationTree; - NamedMatches = namedMatches; - } + MatchesWithRequiredValues = matchesWithRequiredValues; + AllMatchesLinkGenerationTree = allMatchesLinkGenerationTree; + NamedMatches = namedMatches; } } } diff --git a/src/Http/Routing/src/RouterMiddleware.cs b/src/Http/Routing/src/RouterMiddleware.cs index cb1650b14a..fa884cf621 100644 --- a/src/Http/Routing/src/RouterMiddleware.cs +++ b/src/Http/Routing/src/RouterMiddleware.cs @@ -6,70 +6,69 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +/// +/// Middleware responsible for routing. +/// +public partial class RouterMiddleware { + private readonly ILogger _logger; + private readonly RequestDelegate _next; + private readonly IRouter _router; + /// - /// Middleware responsible for routing. + /// Constructs a new instance with a given . /// - public partial class RouterMiddleware + /// The delegate representing the remaining middleware in the request pipeline. + /// The . + /// The to use for routing requests. + public RouterMiddleware( + RequestDelegate next, + ILoggerFactory loggerFactory, + IRouter router) { - private readonly ILogger _logger; - private readonly RequestDelegate _next; - private readonly IRouter _router; + _next = next; + _router = router; - /// - /// Constructs a new instance with a given . - /// - /// The delegate representing the remaining middleware in the request pipeline. - /// The . - /// The to use for routing requests. - public RouterMiddleware( - RequestDelegate next, - ILoggerFactory loggerFactory, - IRouter router) - { - _next = next; - _router = router; - - _logger = loggerFactory.CreateLogger(); - } + _logger = loggerFactory.CreateLogger(); + } - /// - /// Evaluates the handler associated with the - /// derived from . - /// - /// A instance. - public async Task Invoke(HttpContext httpContext) - { - var context = new RouteContext(httpContext); - context.RouteData.Routers.Add(_router); + /// + /// Evaluates the handler associated with the + /// derived from . + /// + /// A instance. + public async Task Invoke(HttpContext httpContext) + { + var context = new RouteContext(httpContext); + context.RouteData.Routers.Add(_router); - await _router.RouteAsync(context); + await _router.RouteAsync(context); - if (context.Handler == null) - { - Log.RequestNotMatched(_logger); - await _next.Invoke(httpContext); - } - else + if (context.Handler == null) + { + Log.RequestNotMatched(_logger); + await _next.Invoke(httpContext); + } + else + { + var routingFeature = new RoutingFeature() { - var routingFeature = new RoutingFeature() - { - RouteData = context.RouteData - }; + RouteData = context.RouteData + }; - // Set the RouteValues on the current request, this is to keep the IRouteValuesFeature inline with the IRoutingFeature - httpContext.Request.RouteValues = context.RouteData.Values; - httpContext.Features.Set(routingFeature); + // Set the RouteValues on the current request, this is to keep the IRouteValuesFeature inline with the IRoutingFeature + httpContext.Request.RouteValues = context.RouteData.Values; + httpContext.Features.Set(routingFeature); - await context.Handler(context.HttpContext); - } + await context.Handler(context.HttpContext); } + } - private static partial class Log - { - [LoggerMessage(1, LogLevel.Debug, "Request did not match any routes", EventName = "RequestNotMatched")] - public static partial void RequestNotMatched(ILogger logger); - } + private static partial class Log + { + [LoggerMessage(1, LogLevel.Debug, "Request did not match any routes", EventName = "RequestNotMatched")] + public static partial void RequestNotMatched(ILogger logger); } } diff --git a/src/Http/Routing/src/RoutingFeature.cs b/src/Http/Routing/src/RoutingFeature.cs index 8eb288220a..587dcaf205 100644 --- a/src/Http/Routing/src/RoutingFeature.cs +++ b/src/Http/Routing/src/RoutingFeature.cs @@ -1,14 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// A feature for routing functionality. +/// +public class RoutingFeature : IRoutingFeature { - /// - /// A feature for routing functionality. - /// - public class RoutingFeature : IRoutingFeature - { - /// - public RouteData? RouteData { get; set; } - } + /// + public RouteData? RouteData { get; set; } } diff --git a/src/Http/Routing/src/RoutingMarkerService.cs b/src/Http/Routing/src/RoutingMarkerService.cs index 63f5266a35..b529fac658 100644 --- a/src/Http/Routing/src/RoutingMarkerService.cs +++ b/src/Http/Routing/src/RoutingMarkerService.cs @@ -3,13 +3,12 @@ using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// A marker class used to determine if all the routing services were added +/// to the before routing is configured. +/// +internal class RoutingMarkerService { - /// - /// A marker class used to determine if all the routing services were added - /// to the before routing is configured. - /// - internal class RoutingMarkerService - { - } } diff --git a/src/Http/Routing/src/SegmentState.cs b/src/Http/Routing/src/SegmentState.cs index 995a2d6d40..66dfb773a3 100644 --- a/src/Http/Routing/src/SegmentState.cs +++ b/src/Http/Routing/src/SegmentState.cs @@ -1,17 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +// Segments are treated as all-or-none. We should never output a partial segment. +// If we add any subsegment of this segment to the generated URI, we have to add +// the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we +// used a value for {p1}, we have to output the entire segment up to the next "/". +// Otherwise we could end up with the partial segment "v1" instead of the entire +// segment "v1-v2.xml". +internal enum SegmentState { - // Segments are treated as all-or-none. We should never output a partial segment. - // If we add any subsegment of this segment to the generated URI, we have to add - // the complete match. For example, if the subsegment is "{p1}-{p2}.xml" and we - // used a value for {p1}, we have to output the entire segment up to the next "/". - // Otherwise we could end up with the partial segment "v1" instead of the entire - // segment "v1-v2.xml". - internal enum SegmentState - { - Beginning, - Inside, - } + Beginning, + Inside, } diff --git a/src/Http/Routing/src/SuppressLinkGenerationMetadata.cs b/src/Http/Routing/src/SuppressLinkGenerationMetadata.cs index 31c32603c7..d42614ae1a 100644 --- a/src/Http/Routing/src/SuppressLinkGenerationMetadata.cs +++ b/src/Http/Routing/src/SuppressLinkGenerationMetadata.cs @@ -1,17 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Represents metadata used during link generation. If is true +/// the associated endpoint will not be used for link generation. +/// +public sealed class SuppressLinkGenerationMetadata : ISuppressLinkGenerationMetadata { /// - /// Represents metadata used during link generation. If is true - /// the associated endpoint will not be used for link generation. + /// Gets a value indicating whether the assocated endpoint should be used for link generation. /// - public sealed class SuppressLinkGenerationMetadata : ISuppressLinkGenerationMetadata - { - /// - /// Gets a value indicating whether the assocated endpoint should be used for link generation. - /// - public bool SuppressLinkGeneration => true; - } -} \ No newline at end of file + public bool SuppressLinkGeneration => true; +} diff --git a/src/Http/Routing/src/SuppressMatchingMetadata.cs b/src/Http/Routing/src/SuppressMatchingMetadata.cs index 540f748b28..8d59dcefb3 100644 --- a/src/Http/Routing/src/SuppressMatchingMetadata.cs +++ b/src/Http/Routing/src/SuppressMatchingMetadata.cs @@ -1,17 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +/// +/// Metadata used to prevent URL matching. If is true the +/// associated endpoint will not be considered for URL matching. +/// +public sealed class SuppressMatchingMetadata : ISuppressMatchingMetadata { /// - /// Metadata used to prevent URL matching. If is true the - /// associated endpoint will not be considered for URL matching. + /// Gets a value indicating whether the associated endpoint should be used for URL matching. /// - public sealed class SuppressMatchingMetadata : ISuppressMatchingMetadata - { - /// - /// Gets a value indicating whether the associated endpoint should be used for URL matching. - /// - public bool SuppressMatching => true; - } + public bool SuppressMatching => true; } diff --git a/src/Http/Routing/src/Template/DefaultTemplateBinderFactory.cs b/src/Http/Routing/src/Template/DefaultTemplateBinderFactory.cs index 9522355d14..652ae0860f 100644 --- a/src/Http/Routing/src/Template/DefaultTemplateBinderFactory.cs +++ b/src/Http/Routing/src/Template/DefaultTemplateBinderFactory.cs @@ -7,82 +7,81 @@ using System.Text.Encodings.Web; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.ObjectPool; -namespace Microsoft.AspNetCore.Routing.Template +namespace Microsoft.AspNetCore.Routing.Template; + +internal sealed class DefaultTemplateBinderFactory : TemplateBinderFactory { - internal sealed class DefaultTemplateBinderFactory : TemplateBinderFactory + private readonly ParameterPolicyFactory _policyFactory; + private readonly ObjectPool _pool; + + public DefaultTemplateBinderFactory( + ParameterPolicyFactory policyFactory, + ObjectPool pool) { - private readonly ParameterPolicyFactory _policyFactory; - private readonly ObjectPool _pool; + if (policyFactory == null) + { + throw new ArgumentNullException(nameof(policyFactory)); + } - public DefaultTemplateBinderFactory( - ParameterPolicyFactory policyFactory, - ObjectPool pool) + if (pool == null) { - if (policyFactory == null) - { - throw new ArgumentNullException(nameof(policyFactory)); - } + throw new ArgumentNullException(nameof(pool)); + } - if (pool == null) - { - throw new ArgumentNullException(nameof(pool)); - } + _policyFactory = policyFactory; + _pool = pool; - _policyFactory = policyFactory; - _pool = pool; + } + public override TemplateBinder Create(RouteTemplate template, RouteValueDictionary defaults) + { + if (template == null) + { + throw new ArgumentNullException(nameof(template)); } - public override TemplateBinder Create(RouteTemplate template, RouteValueDictionary defaults) + if (defaults == null) { - if (template == null) - { - throw new ArgumentNullException(nameof(template)); - } + throw new ArgumentNullException(nameof(defaults)); + } - if (defaults == null) - { - throw new ArgumentNullException(nameof(defaults)); - } + return new TemplateBinder(UrlEncoder.Default, _pool, template, defaults); + } - return new TemplateBinder(UrlEncoder.Default, _pool, template, defaults); + public override TemplateBinder Create(RoutePattern pattern) + { + if (pattern == null) + { + throw new ArgumentNullException(nameof(pattern)); } - public override TemplateBinder Create(RoutePattern pattern) + // Now create the constraints and parameter transformers from the pattern + var policies = new List<(string parameterName, IParameterPolicy policy)>(); + foreach (var kvp in pattern.ParameterPolicies) { - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } + var parameterName = kvp.Key; - // Now create the constraints and parameter transformers from the pattern - var policies = new List<(string parameterName, IParameterPolicy policy)>(); - foreach (var kvp in pattern.ParameterPolicies) - { - var parameterName = kvp.Key; + // It's possible that we don't have an actual route parameter, we need to support that case. + var parameter = pattern.GetParameter(parameterName); - // It's possible that we don't have an actual route parameter, we need to support that case. - var parameter = pattern.GetParameter(parameterName); + // Use the first parameter transformer per parameter + var foundTransformer = false; + for (var i = 0; i < kvp.Value.Count; i++) + { + var parameterPolicy = _policyFactory.Create(parameter, kvp.Value[i]); + if (!foundTransformer && parameterPolicy is IOutboundParameterTransformer parameterTransformer) + { + policies.Add((parameterName, parameterTransformer)); + foundTransformer = true; + } - // Use the first parameter transformer per parameter - var foundTransformer = false; - for (var i = 0; i < kvp.Value.Count; i++) + if (parameterPolicy is IRouteConstraint constraint) { - var parameterPolicy = _policyFactory.Create(parameter, kvp.Value[i]); - if (!foundTransformer && parameterPolicy is IOutboundParameterTransformer parameterTransformer) - { - policies.Add((parameterName, parameterTransformer)); - foundTransformer = true; - } - - if (parameterPolicy is IRouteConstraint constraint) - { - policies.Add((parameterName, constraint)); - } + policies.Add((parameterName, constraint)); } } - - return new TemplateBinder(UrlEncoder.Default, _pool, pattern, policies); } + + return new TemplateBinder(UrlEncoder.Default, _pool, pattern, policies); } } diff --git a/src/Http/Routing/src/Template/InlineConstraint.cs b/src/Http/Routing/src/Template/InlineConstraint.cs index 92f921c7df..d716bcac8c 100644 --- a/src/Http/Routing/src/Template/InlineConstraint.cs +++ b/src/Http/Routing/src/Template/InlineConstraint.cs @@ -4,44 +4,43 @@ using System; using Microsoft.AspNetCore.Routing.Patterns; -namespace Microsoft.AspNetCore.Routing.Template +namespace Microsoft.AspNetCore.Routing.Template; + +/// +/// The parsed representation of an inline constraint in a route parameter. +/// +public class InlineConstraint { /// - /// The parsed representation of an inline constraint in a route parameter. + /// Creates a new instance of . /// - public class InlineConstraint + /// The constraint text. + public InlineConstraint(string constraint) { - /// - /// Creates a new instance of . - /// - /// The constraint text. - public InlineConstraint(string constraint) + if (constraint == null) { - if (constraint == null) - { - throw new ArgumentNullException(nameof(constraint)); - } - - Constraint = constraint; + throw new ArgumentNullException(nameof(constraint)); } - /// - /// Creates a new instance given a . - /// - /// A instance. - public InlineConstraint(RoutePatternParameterPolicyReference other) - { - if (other == null) - { - throw new ArgumentNullException(nameof(other)); - } + Constraint = constraint; + } - Constraint = other.Content!; + /// + /// Creates a new instance given a . + /// + /// A instance. + public InlineConstraint(RoutePatternParameterPolicyReference other) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other)); } - /// - /// Gets the constraint text. - /// - public string Constraint { get; } + Constraint = other.Content!; } + + /// + /// Gets the constraint text. + /// + public string Constraint { get; } } diff --git a/src/Http/Routing/src/Template/RoutePrecedence.cs b/src/Http/Routing/src/Template/RoutePrecedence.cs index 03957d7acd..d8de9bce01 100644 --- a/src/Http/Routing/src/Template/RoutePrecedence.cs +++ b/src/Http/Routing/src/Template/RoutePrecedence.cs @@ -8,270 +8,269 @@ using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Routing.Patterns; -namespace Microsoft.AspNetCore.Routing.Template +namespace Microsoft.AspNetCore.Routing.Template; + +/// +/// Computes precedence for a route template. +/// +public static class RoutePrecedence { /// - /// Computes precedence for a route template. + /// Compute the precedence for matching a provided url /// - public static class RoutePrecedence + /// + /// e.g.: /api/template == 1.1 + /// /api/template/{id} == 1.13 + /// /api/{id:int} == 1.2 + /// /api/template/{id:int} == 1.12 + /// + /// The to compute precedence for. + /// A representing the route's precedence. + public static decimal ComputeInbound(RouteTemplate template) { - /// - /// Compute the precedence for matching a provided url - /// - /// - /// e.g.: /api/template == 1.1 - /// /api/template/{id} == 1.13 - /// /api/{id:int} == 1.2 - /// /api/template/{id:int} == 1.12 - /// - /// The to compute precedence for. - /// A representing the route's precedence. - public static decimal ComputeInbound(RouteTemplate template) + ValidateSegementLength(template.Segments.Count); + + // Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1, + // and 4 results in a combined precedence of 2.14 (decimal). + var precedence = 0m; + + for (var i = 0; i < template.Segments.Count; i++) { - ValidateSegementLength(template.Segments.Count); + var segment = template.Segments[i]; - // Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1, - // and 4 results in a combined precedence of 2.14 (decimal). - var precedence = 0m; + var digit = ComputeInboundPrecedenceDigit(segment); + Debug.Assert(digit >= 0 && digit < 10); - for (var i = 0; i < template.Segments.Count; i++) - { - var segment = template.Segments[i]; + precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); + } - var digit = ComputeInboundPrecedenceDigit(segment); - Debug.Assert(digit >= 0 && digit < 10); + return precedence; + } - precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); - } + // See description on ComputeInbound(RouteTemplate) + internal static decimal ComputeInbound(RoutePattern routePattern) + { + ValidateSegementLength(routePattern.PathSegments.Count); - return precedence; - } + var precedence = 0m; - // See description on ComputeInbound(RouteTemplate) - internal static decimal ComputeInbound(RoutePattern routePattern) + for (var i = 0; i < routePattern.PathSegments.Count; i++) { - ValidateSegementLength(routePattern.PathSegments.Count); + var segment = routePattern.PathSegments[i]; - var precedence = 0m; + var digit = ComputeInboundPrecedenceDigit(routePattern, segment); + Debug.Assert(digit >= 0 && digit < 10); - for (var i = 0; i < routePattern.PathSegments.Count; i++) - { - var segment = routePattern.PathSegments[i]; + precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); + } - var digit = ComputeInboundPrecedenceDigit(routePattern, segment); - Debug.Assert(digit >= 0 && digit < 10); + return precedence; + } - precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); - } + /// + /// Compute the precedence for generating a url. + /// + /// + /// e.g.: /api/template == 5.5 + /// /api/template/{id} == 5.53 + /// /api/{id:int} == 5.4 + /// /api/template/{id:int} == 5.54 + /// + /// The to compute precedence for. + /// A representing the route's precedence. + public static decimal ComputeOutbound(RouteTemplate template) + { + ValidateSegementLength(template.Segments.Count); - return precedence; - } + // Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1, + // and 4 results in a combined precedence of 2.14 (decimal). + var precedence = 0m; - /// - /// Compute the precedence for generating a url. - /// - /// - /// e.g.: /api/template == 5.5 - /// /api/template/{id} == 5.53 - /// /api/{id:int} == 5.4 - /// /api/template/{id:int} == 5.54 - /// - /// The to compute precedence for. - /// A representing the route's precedence. - public static decimal ComputeOutbound(RouteTemplate template) + for (var i = 0; i < template.Segments.Count; i++) { - ValidateSegementLength(template.Segments.Count); + var segment = template.Segments[i]; - // Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1, - // and 4 results in a combined precedence of 2.14 (decimal). - var precedence = 0m; + var digit = ComputeOutboundPrecedenceDigit(segment); + Debug.Assert(digit >= 0 && digit < 10); - for (var i = 0; i < template.Segments.Count; i++) - { - var segment = template.Segments[i]; + precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); + } - var digit = ComputeOutboundPrecedenceDigit(segment); - Debug.Assert(digit >= 0 && digit < 10); + return precedence; + } - precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); - } + // see description on ComputeOutbound(RouteTemplate) + internal static decimal ComputeOutbound(RoutePattern routePattern) + { + ValidateSegementLength(routePattern.PathSegments.Count); - return precedence; - } + // Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1, + // and 4 results in a combined precedence of 2.14 (decimal). + var precedence = 0m; - // see description on ComputeOutbound(RouteTemplate) - internal static decimal ComputeOutbound(RoutePattern routePattern) + for (var i = 0; i < routePattern.PathSegments.Count; i++) { - ValidateSegementLength(routePattern.PathSegments.Count); + var segment = routePattern.PathSegments[i]; - // Each precedence digit corresponds to one decimal place. For example, 3 segments with precedences 2, 1, - // and 4 results in a combined precedence of 2.14 (decimal). - var precedence = 0m; + var digit = ComputeOutboundPrecedenceDigit(segment); + Debug.Assert(digit >= 0 && digit < 10); - for (var i = 0; i < routePattern.PathSegments.Count; i++) - { - var segment = routePattern.PathSegments[i]; - - var digit = ComputeOutboundPrecedenceDigit(segment); - Debug.Assert(digit >= 0 && digit < 10); + precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); + } - precedence += decimal.Divide(digit, (decimal)Math.Pow(10, i)); - } + return precedence; + } - return precedence; + private static void ValidateSegementLength(int length) + { + if (length > 28) + { + // An OverflowException will be thrown by Math.Pow when greater than 28 + throw new InvalidOperationException("Route exceeds the maximum number of allowed segments of 28 and is unable to be processed."); } + } - private static void ValidateSegementLength(int length) + // Segments have the following order: + // 5 - Literal segments + // 4 - Multi-part segments && Constrained parameter segments + // 3 - Unconstrained parameter segements + // 2 - Constrained wildcard parameter segments + // 1 - Unconstrained wildcard parameter segments + private static int ComputeOutboundPrecedenceDigit(TemplateSegment segment) + { + if (segment.Parts.Count > 1) { - if (length > 28) - { - // An OverflowException will be thrown by Math.Pow when greater than 28 - throw new InvalidOperationException("Route exceeds the maximum number of allowed segments of 28 and is unable to be processed."); - } + return 4; } - // Segments have the following order: - // 5 - Literal segments - // 4 - Multi-part segments && Constrained parameter segments - // 3 - Unconstrained parameter segements - // 2 - Constrained wildcard parameter segments - // 1 - Unconstrained wildcard parameter segments - private static int ComputeOutboundPrecedenceDigit(TemplateSegment segment) + var part = segment.Parts[0]; + if (part.IsLiteral) { - if(segment.Parts.Count > 1) - { - return 4; - } + return 5; + } + else + { + Debug.Assert(part.IsParameter); + var digit = part.IsCatchAll ? 1 : 3; - var part = segment.Parts[0]; - if(part.IsLiteral) + if (part.InlineConstraints != null && part.InlineConstraints.Any()) { - return 5; + digit++; } - else - { - Debug.Assert(part.IsParameter); - var digit = part.IsCatchAll ? 1 : 3; - if (part.InlineConstraints != null && part.InlineConstraints.Any()) - { - digit++; - } + return digit; + } + } - return digit; - } + // See description on ComputeOutboundPrecedenceDigit(TemplateSegment segment) + private static int ComputeOutboundPrecedenceDigit(RoutePatternPathSegment pathSegment) + { + if (pathSegment.Parts.Count > 1) + { + return 4; } - // See description on ComputeOutboundPrecedenceDigit(TemplateSegment segment) - private static int ComputeOutboundPrecedenceDigit(RoutePatternPathSegment pathSegment) + var part = pathSegment.Parts[0]; + if (part.IsLiteral) { - if (pathSegment.Parts.Count > 1) - { - return 4; - } + return 5; + } + else if (part is RoutePatternParameterPart parameterPart) + { + Debug.Assert(parameterPart != null); + var digit = parameterPart.IsCatchAll ? 1 : 3; - var part = pathSegment.Parts[0]; - if (part.IsLiteral) + if (parameterPart.ParameterPolicies.Count > 0) { - return 5; + digit++; } - else if (part is RoutePatternParameterPart parameterPart) - { - Debug.Assert(parameterPart != null); - var digit = parameterPart.IsCatchAll ? 1 : 3; - if (parameterPart.ParameterPolicies.Count > 0) - { - digit++; - } + return digit; + } + else + { + // Unreachable + throw new NotSupportedException(); + } + } - return digit; - } - else - { - // Unreachable - throw new NotSupportedException(); - } + // Segments have the following order: + // 1 - Literal segments + // 2 - Constrained parameter segments / Multi-part segments + // 3 - Unconstrained parameter segments + // 4 - Constrained wildcard parameter segments + // 5 - Unconstrained wildcard parameter segments + private static int ComputeInboundPrecedenceDigit(TemplateSegment segment) + { + if (segment.Parts.Count > 1) + { + // Multi-part segments should appear after literal segments and along with parameter segments + return 2; } - // Segments have the following order: - // 1 - Literal segments - // 2 - Constrained parameter segments / Multi-part segments - // 3 - Unconstrained parameter segments - // 4 - Constrained wildcard parameter segments - // 5 - Unconstrained wildcard parameter segments - private static int ComputeInboundPrecedenceDigit(TemplateSegment segment) + var part = segment.Parts[0]; + // Literal segments always go first + if (part.IsLiteral) { - if (segment.Parts.Count > 1) - { - // Multi-part segments should appear after literal segments and along with parameter segments - return 2; - } + return 1; + } + else + { + Debug.Assert(part.IsParameter); + var digit = part.IsCatchAll ? 5 : 3; - var part = segment.Parts[0]; - // Literal segments always go first - if (part.IsLiteral) + // If there is a route constraint for the parameter, reduce order by 1 + // Constrained parameters end up with order 2, Constrained catch alls end up with order 4 + if (part.InlineConstraints != null && part.InlineConstraints.Any()) { - return 1; + digit--; } - else - { - Debug.Assert(part.IsParameter); - var digit = part.IsCatchAll ? 5 : 3; - // If there is a route constraint for the parameter, reduce order by 1 - // Constrained parameters end up with order 2, Constrained catch alls end up with order 4 - if (part.InlineConstraints != null && part.InlineConstraints.Any()) - { - digit--; - } - - return digit; - } + return digit; } + } - // see description on ComputeInboundPrecedenceDigit(TemplateSegment segment) - // - // With a RoutePattern, parameters with a required value are treated as a literal segment - internal static int ComputeInboundPrecedenceDigit(RoutePattern routePattern, RoutePatternPathSegment pathSegment) + // see description on ComputeInboundPrecedenceDigit(TemplateSegment segment) + // + // With a RoutePattern, parameters with a required value are treated as a literal segment + internal static int ComputeInboundPrecedenceDigit(RoutePattern routePattern, RoutePatternPathSegment pathSegment) + { + if (pathSegment.Parts.Count > 1) { - if (pathSegment.Parts.Count > 1) - { - // Multi-part segments should appear after literal segments and along with parameter segments - return 2; - } + // Multi-part segments should appear after literal segments and along with parameter segments + return 2; + } - var part = pathSegment.Parts[0]; - // Literal segments always go first - if (part.IsLiteral) + var part = pathSegment.Parts[0]; + // Literal segments always go first + if (part.IsLiteral) + { + return 1; + } + else if (part is RoutePatternParameterPart parameterPart) + { + // Parameter with a required value is matched as a literal + if (routePattern.RequiredValues.TryGetValue(parameterPart.Name, out var requiredValue) && + !RouteValueEqualityComparer.Default.Equals(requiredValue, string.Empty)) { return 1; } - else if (part is RoutePatternParameterPart parameterPart) - { - // Parameter with a required value is matched as a literal - if (routePattern.RequiredValues.TryGetValue(parameterPart.Name, out var requiredValue) && - !RouteValueEqualityComparer.Default.Equals(requiredValue, string.Empty)) - { - return 1; - } - - var digit = parameterPart.IsCatchAll ? 5 : 3; - - // If there is a route constraint for the parameter, reduce order by 1 - // Constrained parameters end up with order 2, Constrained catch alls end up with order 4 - if (parameterPart.ParameterPolicies.Count > 0) - { - digit--; - } - - return digit; - } - else + + var digit = parameterPart.IsCatchAll ? 5 : 3; + + // If there is a route constraint for the parameter, reduce order by 1 + // Constrained parameters end up with order 2, Constrained catch alls end up with order 4 + if (parameterPart.ParameterPolicies.Count > 0) { - // Unreachable - throw new NotSupportedException(); + digit--; } + + return digit; + } + else + { + // Unreachable + throw new NotSupportedException(); } } } diff --git a/src/Http/Routing/src/Template/RouteTemplate.cs b/src/Http/Routing/src/Template/RouteTemplate.cs index 0476bf4996..846060e3d1 100644 --- a/src/Http/Routing/src/Template/RouteTemplate.cs +++ b/src/Http/Routing/src/Template/RouteTemplate.cs @@ -9,141 +9,140 @@ using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Routing.Patterns; -namespace Microsoft.AspNetCore.Routing.Template +namespace Microsoft.AspNetCore.Routing.Template; + +/// +/// Represents the template for a route. +/// +[DebuggerDisplay("{DebuggerToString()}")] +public class RouteTemplate { + private const string SeparatorString = "/"; + /// - /// Represents the template for a route. + /// Constructs a new instance given . /// - [DebuggerDisplay("{DebuggerToString()}")] - public class RouteTemplate + /// A instance. + public RouteTemplate(RoutePattern other) { - private const string SeparatorString = "/"; - - /// - /// Constructs a new instance given . - /// - /// A instance. - public RouteTemplate(RoutePattern other) + if (other == null) { - if (other == null) - { - throw new ArgumentNullException(nameof(other)); - } + throw new ArgumentNullException(nameof(other)); + } - // RequiredValues will be ignored. RouteTemplate doesn't support them. + // RequiredValues will be ignored. RouteTemplate doesn't support them. - TemplateText = other.RawText; - Segments = new List(other.PathSegments.Select(p => new TemplateSegment(p))); - Parameters = new List(); - for (var i = 0; i < Segments.Count; i++) + TemplateText = other.RawText; + Segments = new List(other.PathSegments.Select(p => new TemplateSegment(p))); + Parameters = new List(); + for (var i = 0; i < Segments.Count; i++) + { + var segment = Segments[i]; + for (var j = 0; j < segment.Parts.Count; j++) { - var segment = Segments[i]; - for (var j = 0; j < segment.Parts.Count; j++) + var part = segment.Parts[j]; + if (part.IsParameter) { - var part = segment.Parts[j]; - if (part.IsParameter) - { - Parameters.Add(part); - } + Parameters.Add(part); } } } + } - /// - /// Constructs a a new instance given the string - /// and a list of . Computes the parameters in the route template. - /// - /// A string representation of the route template. - /// A list of . - public RouteTemplate(string template, List segments) + /// + /// Constructs a a new instance given the string + /// and a list of . Computes the parameters in the route template. + /// + /// A string representation of the route template. + /// A list of . + public RouteTemplate(string template, List segments) + { + if (segments == null) { - if (segments == null) - { - throw new ArgumentNullException(nameof(segments)); - } + throw new ArgumentNullException(nameof(segments)); + } - TemplateText = template; + TemplateText = template; - Segments = segments; + Segments = segments; - Parameters = new List(); - for (var i = 0; i < segments.Count; i++) + Parameters = new List(); + for (var i = 0; i < segments.Count; i++) + { + var segment = Segments[i]; + for (var j = 0; j < segment.Parts.Count; j++) { - var segment = Segments[i]; - for (var j = 0; j < segment.Parts.Count; j++) + var part = segment.Parts[j]; + if (part.IsParameter) { - var part = segment.Parts[j]; - if (part.IsParameter) - { - Parameters.Add(part); - } + Parameters.Add(part); } } } + } - /// - /// Gets the string representation of the route template. - /// - public string? TemplateText { get; } - - /// - /// Gets the list of that represent that parameters defined in the route template. - /// - public IList Parameters { get; } - - /// - /// Gets the list of that compromise the route template. - /// - public IList Segments { get; } - - /// - /// Gets the at a given index. - /// - /// The index of the element to retrieve. - /// A instance. - public TemplateSegment? GetSegment(int index) - { - if (index < 0) - { - throw new IndexOutOfRangeException(); - } + /// + /// Gets the string representation of the route template. + /// + public string? TemplateText { get; } - return index >= Segments.Count ? null : Segments[index]; - } + /// + /// Gets the list of that represent that parameters defined in the route template. + /// + public IList Parameters { get; } - private string DebuggerToString() + /// + /// Gets the list of that compromise the route template. + /// + public IList Segments { get; } + + /// + /// Gets the at a given index. + /// + /// The index of the element to retrieve. + /// A instance. + public TemplateSegment? GetSegment(int index) + { + if (index < 0) { - return string.Join(SeparatorString, Segments.Select(s => s.DebuggerToString())); + throw new IndexOutOfRangeException(); } - /// - /// Gets the parameter matching the given name. - /// - /// The name of the parameter to match. - /// The matching parameter or null if no parameter matches the given name. - public TemplatePart? GetParameter(string name) + return index >= Segments.Count ? null : Segments[index]; + } + + private string DebuggerToString() + { + return string.Join(SeparatorString, Segments.Select(s => s.DebuggerToString())); + } + + /// + /// Gets the parameter matching the given name. + /// + /// The name of the parameter to match. + /// The matching parameter or null if no parameter matches the given name. + public TemplatePart? GetParameter(string name) + { + for (var i = 0; i < Parameters.Count; i++) { - for (var i = 0; i < Parameters.Count; i++) + var parameter = Parameters[i]; + if (string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase)) { - var parameter = Parameters[i]; - if (string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase)) - { - return parameter; - } + return parameter; } - - return null; } - /// - /// Converts the to the equivalent - /// - /// - /// A . - public RoutePattern ToRoutePattern() - { - var segments = Segments.Select(s => s.ToRoutePatternPathSegment()); - return RoutePatternFactory.Pattern(TemplateText, segments); - } + return null; + } + + /// + /// Converts the to the equivalent + /// + /// + /// A . + public RoutePattern ToRoutePattern() + { + var segments = Segments.Select(s => s.ToRoutePatternPathSegment()); + return RoutePatternFactory.Pattern(TemplateText, segments); } } diff --git a/src/Http/Routing/src/Template/TemplateBinder.cs b/src/Http/Routing/src/Template/TemplateBinder.cs index ccedd90527..03bc4c6710 100644 --- a/src/Http/Routing/src/Template/TemplateBinder.cs +++ b/src/Http/Routing/src/Template/TemplateBinder.cs @@ -13,752 +13,751 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.ObjectPool; -namespace Microsoft.AspNetCore.Routing.Template +namespace Microsoft.AspNetCore.Routing.Template; + +/// +/// Supports processing and binding parameter values in a route template. +/// +public class TemplateBinder { + private readonly UrlEncoder _urlEncoder; + private readonly ObjectPool _pool; + + private readonly (string parameterName, IRouteConstraint constraint)[] _constraints; + private readonly RouteValueDictionary? _defaults; + private readonly KeyValuePair[] _filters; + private readonly (string parameterName, IOutboundParameterTransformer transformer)[] _parameterTransformers; + private readonly RoutePattern _pattern; + private readonly string[] _requiredKeys; + + // A pre-allocated template for the 'known' route values that this template binder uses. + // + // We always make a copy of this and operate on the copy, so that we don't mutate shared state. + private readonly KeyValuePair[] _slots; + /// - /// Supports processing and binding parameter values in a route template. + /// Creates a new instance of . /// - public class TemplateBinder + /// The . + /// The . + /// The to bind values to. + /// The default values for . + internal TemplateBinder( + UrlEncoder urlEncoder, + ObjectPool pool, + RouteTemplate template, + RouteValueDictionary defaults) + : this(urlEncoder, pool, template?.ToRoutePattern()!, defaults, requiredKeys: null, parameterPolicies: null) { - private readonly UrlEncoder _urlEncoder; - private readonly ObjectPool _pool; - - private readonly (string parameterName, IRouteConstraint constraint)[] _constraints; - private readonly RouteValueDictionary? _defaults; - private readonly KeyValuePair[] _filters; - private readonly (string parameterName, IOutboundParameterTransformer transformer)[] _parameterTransformers; - private readonly RoutePattern _pattern; - private readonly string[] _requiredKeys; - - // A pre-allocated template for the 'known' route values that this template binder uses. - // - // We always make a copy of this and operate on the copy, so that we don't mutate shared state. - private readonly KeyValuePair[] _slots; - - /// - /// Creates a new instance of . - /// - /// The . - /// The . - /// The to bind values to. - /// The default values for . - internal TemplateBinder( - UrlEncoder urlEncoder, - ObjectPool pool, - RouteTemplate template, - RouteValueDictionary defaults) - : this(urlEncoder, pool, template?.ToRoutePattern()!, defaults, requiredKeys: null, parameterPolicies: null) - { - } - - /// - /// Creates a new instance of . - /// - /// The . - /// The . - /// The to bind values to. - /// The default values for . Optional. - /// Keys used to determine if the ambient values apply. Optional. - /// - /// A list of (, ) pairs to evaluate when producing a URI. - /// - internal TemplateBinder( - UrlEncoder urlEncoder, - ObjectPool pool, - RoutePattern pattern, - RouteValueDictionary? defaults, - IEnumerable? requiredKeys, - IEnumerable<(string parameterName, IParameterPolicy policy)>? parameterPolicies) - { - if (urlEncoder == null) - { - throw new ArgumentNullException(nameof(urlEncoder)); - } + } - if (pool == null) - { - throw new ArgumentNullException(nameof(pool)); - } + /// + /// Creates a new instance of . + /// + /// The . + /// The . + /// The to bind values to. + /// The default values for . Optional. + /// Keys used to determine if the ambient values apply. Optional. + /// + /// A list of (, ) pairs to evaluate when producing a URI. + /// + internal TemplateBinder( + UrlEncoder urlEncoder, + ObjectPool pool, + RoutePattern pattern, + RouteValueDictionary? defaults, + IEnumerable? requiredKeys, + IEnumerable<(string parameterName, IParameterPolicy policy)>? parameterPolicies) + { + if (urlEncoder == null) + { + throw new ArgumentNullException(nameof(urlEncoder)); + } - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } + if (pool == null) + { + throw new ArgumentNullException(nameof(pool)); + } - _urlEncoder = urlEncoder; - _pool = pool; - _pattern = pattern; - _defaults = defaults; - _requiredKeys = requiredKeys?.ToArray() ?? Array.Empty(); + if (pattern == null) + { + throw new ArgumentNullException(nameof(pattern)); + } - // Any default that doesn't have a corresponding parameter is a 'filter' and if a value - // is provided for that 'filter' it must match the value in defaults. - var filters = new RouteValueDictionary(_defaults); - for (var i = 0; i < pattern.Parameters.Count; i++) - { - filters.Remove(pattern.Parameters[i].Name); - } - _filters = filters.ToArray(); + _urlEncoder = urlEncoder; + _pool = pool; + _pattern = pattern; + _defaults = defaults; + _requiredKeys = requiredKeys?.ToArray() ?? Array.Empty(); - _constraints = parameterPolicies - ?.Where(p => p.policy is IRouteConstraint) - .Select(p => (p.parameterName, (IRouteConstraint)p.policy)) - .ToArray() ?? Array.Empty<(string, IRouteConstraint)>(); - _parameterTransformers = parameterPolicies - ?.Where(p => p.policy is IOutboundParameterTransformer) - .Select(p => (p.parameterName, (IOutboundParameterTransformer)p.policy)) - .ToArray() ?? Array.Empty<(string, IOutboundParameterTransformer)>(); + // Any default that doesn't have a corresponding parameter is a 'filter' and if a value + // is provided for that 'filter' it must match the value in defaults. + var filters = new RouteValueDictionary(_defaults); + for (var i = 0; i < pattern.Parameters.Count; i++) + { + filters.Remove(pattern.Parameters[i].Name); + } + _filters = filters.ToArray(); + + _constraints = parameterPolicies + ?.Where(p => p.policy is IRouteConstraint) + .Select(p => (p.parameterName, (IRouteConstraint)p.policy)) + .ToArray() ?? Array.Empty<(string, IRouteConstraint)>(); + _parameterTransformers = parameterPolicies + ?.Where(p => p.policy is IOutboundParameterTransformer) + .Select(p => (p.parameterName, (IOutboundParameterTransformer)p.policy)) + .ToArray() ?? Array.Empty<(string, IOutboundParameterTransformer)>(); + + _slots = AssignSlots(_pattern, _filters); + } - _slots = AssignSlots(_pattern, _filters); + internal TemplateBinder( + UrlEncoder urlEncoder, + ObjectPool pool, + RoutePattern pattern, + IEnumerable<(string parameterName, IParameterPolicy policy)> parameterPolicies) + { + if (urlEncoder == null) + { + throw new ArgumentNullException(nameof(urlEncoder)); } - internal TemplateBinder( - UrlEncoder urlEncoder, - ObjectPool pool, - RoutePattern pattern, - IEnumerable<(string parameterName, IParameterPolicy policy)> parameterPolicies) + if (pool == null) { - if (urlEncoder == null) - { - throw new ArgumentNullException(nameof(urlEncoder)); - } + throw new ArgumentNullException(nameof(pool)); + } - if (pool == null) - { - throw new ArgumentNullException(nameof(pool)); - } + if (pattern == null) + { + throw new ArgumentNullException(nameof(pattern)); + } - if (pattern == null) - { - throw new ArgumentNullException(nameof(pattern)); - } + // Parameter policies can be null. - // Parameter policies can be null. + _urlEncoder = urlEncoder; + _pool = pool; + _pattern = pattern; + _defaults = new RouteValueDictionary(pattern.Defaults); + _requiredKeys = pattern.RequiredValues.Keys.ToArray(); - _urlEncoder = urlEncoder; - _pool = pool; - _pattern = pattern; - _defaults = new RouteValueDictionary(pattern.Defaults); - _requiredKeys = pattern.RequiredValues.Keys.ToArray(); + // Any default that doesn't have a corresponding parameter is a 'filter' and if a value + // is provided for that 'filter' it must match the value in defaults. + var filters = new RouteValueDictionary(_defaults); + for (var i = 0; i < pattern.Parameters.Count; i++) + { + filters.Remove(pattern.Parameters[i].Name); + } + _filters = filters.ToArray(); + + _constraints = parameterPolicies + ?.Where(p => p.policy is IRouteConstraint) + .Select(p => (p.parameterName, (IRouteConstraint)p.policy)) + .ToArray() ?? Array.Empty<(string, IRouteConstraint)>(); + _parameterTransformers = parameterPolicies + ?.Where(p => p.policy is IOutboundParameterTransformer) + .Select(p => (p.parameterName, (IOutboundParameterTransformer)p.policy)) + .ToArray() ?? Array.Empty<(string, IOutboundParameterTransformer)>(); + + _slots = AssignSlots(_pattern, _filters); + } - // Any default that doesn't have a corresponding parameter is a 'filter' and if a value - // is provided for that 'filter' it must match the value in defaults. - var filters = new RouteValueDictionary(_defaults); - for (var i = 0; i < pattern.Parameters.Count; i++) + /// + /// Generates the parameter values in the route. + /// + /// The values associated with the current request. + /// The route values to process. + /// A instance. Can be null. + public TemplateValuesResult? GetValues(RouteValueDictionary? ambientValues, RouteValueDictionary values) + { + // Make a new copy of the slots array, we'll use this as 'scratch' space + // and then the RVD will take ownership of it. + var slots = new KeyValuePair[_slots.Length]; + Array.Copy(_slots, 0, slots, 0, slots.Length); + + // Keeping track of the number of 'values' we've processed can be used to avoid doing + // some expensive 'merge' operations later. + var valueProcessedCount = 0; + + // Start by copying all of the values out of the 'values' and into the slots. There's no success + // case where we *don't* use all of the 'values' so there's no reason not to do this up front + // to avoid visiting the values dictionary again and again. + for (var i = 0; i < slots.Length; i++) + { + var key = slots[i].Key; + if (values.TryGetValue(key, out var value)) { - filters.Remove(pattern.Parameters[i].Name); + // We will need to know later if the value in the 'values' was an null value. + // This affects how we process ambient values. Since the 'slots' are initialized + // with null values, we use the null-object-pattern to track 'explicit null', which means that + // null means omitted. + value = IsRoutePartNonEmpty(value) ? value : SentinullValue.Instance; + slots[i] = new KeyValuePair(key, value); + + // Track the count of processed values - this allows a fast path later. + valueProcessedCount++; } - _filters = filters.ToArray(); - - _constraints = parameterPolicies - ?.Where(p => p.policy is IRouteConstraint) - .Select(p => (p.parameterName, (IRouteConstraint)p.policy)) - .ToArray() ?? Array.Empty<(string, IRouteConstraint)>(); - _parameterTransformers = parameterPolicies - ?.Where(p => p.policy is IOutboundParameterTransformer) - .Select(p => (p.parameterName, (IOutboundParameterTransformer)p.policy)) - .ToArray() ?? Array.Empty<(string, IOutboundParameterTransformer)>(); - - _slots = AssignSlots(_pattern, _filters); - } - - /// - /// Generates the parameter values in the route. - /// - /// The values associated with the current request. - /// The route values to process. - /// A instance. Can be null. - public TemplateValuesResult? GetValues(RouteValueDictionary? ambientValues, RouteValueDictionary values) - { - // Make a new copy of the slots array, we'll use this as 'scratch' space - // and then the RVD will take ownership of it. - var slots = new KeyValuePair[_slots.Length]; - Array.Copy(_slots, 0, slots, 0, slots.Length); - - // Keeping track of the number of 'values' we've processed can be used to avoid doing - // some expensive 'merge' operations later. - var valueProcessedCount = 0; - - // Start by copying all of the values out of the 'values' and into the slots. There's no success - // case where we *don't* use all of the 'values' so there's no reason not to do this up front - // to avoid visiting the values dictionary again and again. - for (var i = 0; i < slots.Length; i++) + } + + // In Endpoint Routing, patterns can have logical parameters that appear 'to the left' of + // the route template. This governs whether or not the template can be selected (they act like + // filters), and whether the remaining ambient values should be used. + // should be used. + // For example, in case of MVC it flattens out a route template like below + // {controller}/{action}/{id?} + // to + // Products/Index/{id?}, + // defaults: new { controller = "Products", action = "Index" }, + // requiredValues: new { controller = "Products", action = "Index" } + // In the above example, "controller" and "action" are no longer parameters. + var copyAmbientValues = ambientValues != null; + if (copyAmbientValues) + { + var requiredKeys = _requiredKeys; + for (var i = 0; i < requiredKeys.Length; i++) { - var key = slots[i].Key; - if (values.TryGetValue(key, out var value)) + // For each required key, the values and ambient values need to have the same value. + var key = requiredKeys[i]; + var hasExplicitValue = values.TryGetValue(key, out var value); + + if (ambientValues == null || !ambientValues.TryGetValue(key, out var ambientValue)) { - // We will need to know later if the value in the 'values' was an null value. - // This affects how we process ambient values. Since the 'slots' are initialized - // with null values, we use the null-object-pattern to track 'explicit null', which means that - // null means omitted. - value = IsRoutePartNonEmpty(value) ? value : SentinullValue.Instance; - slots[i] = new KeyValuePair(key, value); - - // Track the count of processed values - this allows a fast path later. - valueProcessedCount++; + ambientValue = null; } - } - // In Endpoint Routing, patterns can have logical parameters that appear 'to the left' of - // the route template. This governs whether or not the template can be selected (they act like - // filters), and whether the remaining ambient values should be used. - // should be used. - // For example, in case of MVC it flattens out a route template like below - // {controller}/{action}/{id?} - // to - // Products/Index/{id?}, - // defaults: new { controller = "Products", action = "Index" }, - // requiredValues: new { controller = "Products", action = "Index" } - // In the above example, "controller" and "action" are no longer parameters. - var copyAmbientValues = ambientValues != null; - if (copyAmbientValues) - { - var requiredKeys = _requiredKeys; - for (var i = 0; i < requiredKeys.Length; i++) + // For now, only check ambient values with required values that don't have a parameter + // Ambient values for parameters are processed below + var hasParameter = _pattern.GetParameter(key) != null; + if (!hasParameter) { - // For each required key, the values and ambient values need to have the same value. - var key = requiredKeys[i]; - var hasExplicitValue = values.TryGetValue(key, out var value); - - if (ambientValues == null || !ambientValues.TryGetValue(key, out var ambientValue)) + if (!_pattern.RequiredValues.TryGetValue(key, out var requiredValue)) { - ambientValue = null; + throw new InvalidOperationException($"Unable to find required value '{key}' on route pattern."); } - // For now, only check ambient values with required values that don't have a parameter - // Ambient values for parameters are processed below - var hasParameter = _pattern.GetParameter(key) != null; - if (!hasParameter) + if (!RoutePartsEqual(ambientValue, _pattern.RequiredValues[key]) && + !RoutePattern.IsRequiredValueAny(_pattern.RequiredValues[key])) { - if (!_pattern.RequiredValues.TryGetValue(key, out var requiredValue)) - { - throw new InvalidOperationException($"Unable to find required value '{key}' on route pattern."); - } - - if (!RoutePartsEqual(ambientValue, _pattern.RequiredValues[key]) && - !RoutePattern.IsRequiredValueAny(_pattern.RequiredValues[key])) - { - copyAmbientValues = false; - break; - } + copyAmbientValues = false; + break; + } - if (hasExplicitValue && !RoutePartsEqual(value, ambientValue)) - { - copyAmbientValues = false; - break; - } + if (hasExplicitValue && !RoutePartsEqual(value, ambientValue)) + { + copyAmbientValues = false; + break; } } } + } - // We can now process the rest of the parameters (from left to right) and copy the ambient - // values as long as the conditions are met. - // - // Find out which entries in the URI are valid for the URI we want to generate. - // If the URI had ordered parameters a="1", b="2", c="3" and the new values - // specified that b="9", then we need to invalidate everything after it. The new - // values should then be a="1", b="9", c=. - // - // We also handle the case where a parameter is optional but has no value - we shouldn't - // accept additional parameters that appear *after* that parameter. - var parameters = _pattern.Parameters; - var parameterCount = _pattern.Parameters.Count; - for (var i = 0; i < parameterCount; i++) - { - var key = slots[i].Key; - var value = slots[i].Value; + // We can now process the rest of the parameters (from left to right) and copy the ambient + // values as long as the conditions are met. + // + // Find out which entries in the URI are valid for the URI we want to generate. + // If the URI had ordered parameters a="1", b="2", c="3" and the new values + // specified that b="9", then we need to invalidate everything after it. The new + // values should then be a="1", b="9", c=. + // + // We also handle the case where a parameter is optional but has no value - we shouldn't + // accept additional parameters that appear *after* that parameter. + var parameters = _pattern.Parameters; + var parameterCount = _pattern.Parameters.Count; + for (var i = 0; i < parameterCount; i++) + { + var key = slots[i].Key; + var value = slots[i].Value; - // Whether or not the value was explicitly provided is signficant when comparing - // ambient values. Remember that we're using a special sentinel value so that we - // can tell the difference between an omitted value and an explicitly specified null. - var hasExplicitValue = value != null; + // Whether or not the value was explicitly provided is signficant when comparing + // ambient values. Remember that we're using a special sentinel value so that we + // can tell the difference between an omitted value and an explicitly specified null. + var hasExplicitValue = value != null; - var hasAmbientValue = false; - var ambientValue = (object?)null; + var hasAmbientValue = false; + var ambientValue = (object?)null; - var parameter = parameters[i]; + var parameter = parameters[i]; - // We are copying **all** ambient values - if (copyAmbientValues) + // We are copying **all** ambient values + if (copyAmbientValues) + { + hasAmbientValue = ambientValues != null && ambientValues.TryGetValue(key, out ambientValue); + if (hasExplicitValue && hasAmbientValue && !RoutePartsEqual(ambientValue, value)) { - hasAmbientValue = ambientValues != null && ambientValues.TryGetValue(key, out ambientValue); - if (hasExplicitValue && hasAmbientValue && !RoutePartsEqual(ambientValue, value)) - { - // Stop copying current values when we find one that doesn't match - copyAmbientValues = false; - } - - if (!hasExplicitValue && - !hasAmbientValue && - _defaults?.ContainsKey(parameter.Name) != true) - { - // This is an unsatisfied parameter value and there are no defaults. We might still - // be able to generate a URL but we should stop 'accepting' ambient values. - // - // This might be a case like: - // template: a/{b?}/{c?} - // ambient: { c = 17 } - // values: { } - // - // We can still generate a URL from this ("/a") but we shouldn't accept 'c' because - // we can't use it. - // - // In the example above we should fall into this block for 'b'. - copyAmbientValues = false; - } + // Stop copying current values when we find one that doesn't match + copyAmbientValues = false; } - // This might be an ambient value that matches a required value. We want to use these even if we're - // not bulk-copying ambient values. - // - // This comes up in a case like the following: - // ambient-values: { page = "/DeleteUser", area = "Admin", } - // values: { controller = "Home", action = "Index", } - // pattern: {area}/{controller}/{action}/{id?} - // required-values: { area = "Admin", controller = "Home", action = "Index", page = (string)null, } - // - // OR in plain English... when linking from a page in an area to an action in the same area, it should - // be possible to use the area as an ambient value. - if (!copyAmbientValues && !hasExplicitValue && _pattern.RequiredValues.TryGetValue(key, out var requiredValue)) + if (!hasExplicitValue && + !hasAmbientValue && + _defaults?.ContainsKey(parameter.Name) != true) { - hasAmbientValue = ambientValues != null && ambientValues.TryGetValue(key, out ambientValue); - if (hasAmbientValue && - (RoutePartsEqual(requiredValue, ambientValue) || RoutePattern.IsRequiredValueAny(requiredValue))) - { - // Treat this an an explicit value to *force it*. - slots[i] = new KeyValuePair(key, ambientValue); - hasExplicitValue = true; - value = ambientValue; - } + // This is an unsatisfied parameter value and there are no defaults. We might still + // be able to generate a URL but we should stop 'accepting' ambient values. + // + // This might be a case like: + // template: a/{b?}/{c?} + // ambient: { c = 17 } + // values: { } + // + // We can still generate a URL from this ("/a") but we shouldn't accept 'c' because + // we can't use it. + // + // In the example above we should fall into this block for 'b'. + copyAmbientValues = false; } + } - // If the parameter is a match, add it to the list of values we will use for URI generation - if (hasExplicitValue && !ReferenceEquals(value, SentinullValue.Instance)) - { - // Already has a value in the list, do nothing - } - else if (copyAmbientValues && hasAmbientValue) + // This might be an ambient value that matches a required value. We want to use these even if we're + // not bulk-copying ambient values. + // + // This comes up in a case like the following: + // ambient-values: { page = "/DeleteUser", area = "Admin", } + // values: { controller = "Home", action = "Index", } + // pattern: {area}/{controller}/{action}/{id?} + // required-values: { area = "Admin", controller = "Home", action = "Index", page = (string)null, } + // + // OR in plain English... when linking from a page in an area to an action in the same area, it should + // be possible to use the area as an ambient value. + if (!copyAmbientValues && !hasExplicitValue && _pattern.RequiredValues.TryGetValue(key, out var requiredValue)) + { + hasAmbientValue = ambientValues != null && ambientValues.TryGetValue(key, out ambientValue); + if (hasAmbientValue && + (RoutePartsEqual(requiredValue, ambientValue) || RoutePattern.IsRequiredValueAny(requiredValue))) { + // Treat this an an explicit value to *force it*. slots[i] = new KeyValuePair(key, ambientValue); + hasExplicitValue = true; + value = ambientValue; } - else if (parameter.IsOptional || parameter.IsCatchAll) - { - // Value isn't needed for optional or catchall parameters - wipe out the key, so it - // will be omitted from the RVD. - slots[i] = default; - } - else if (_defaults != null && _defaults.TryGetValue(parameter.Name, out var defaultValue)) - { + } - // Add the default value only if there isn't already a new value for it and - // only if it actually has a default value. - slots[i] = new KeyValuePair(key, defaultValue); - } - else - { - // If we get here, this parameter needs a value, but doesn't have one. This is a - // failure case. - return null; - } + // If the parameter is a match, add it to the list of values we will use for URI generation + if (hasExplicitValue && !ReferenceEquals(value, SentinullValue.Instance)) + { + // Already has a value in the list, do nothing } + else if (copyAmbientValues && hasAmbientValue) + { + slots[i] = new KeyValuePair(key, ambientValue); + } + else if (parameter.IsOptional || parameter.IsCatchAll) + { + // Value isn't needed for optional or catchall parameters - wipe out the key, so it + // will be omitted from the RVD. + slots[i] = default; + } + else if (_defaults != null && _defaults.TryGetValue(parameter.Name, out var defaultValue)) + { - // Any default values that don't appear as parameters are treated like filters. Any new values - // provided must match these defaults. - var filters = _filters; - for (var i = 0; i < filters.Length; i++) + // Add the default value only if there isn't already a new value for it and + // only if it actually has a default value. + slots[i] = new KeyValuePair(key, defaultValue); + } + else { - var key = filters[i].Key; - var value = slots[i + parameterCount].Value; + // If we get here, this parameter needs a value, but doesn't have one. This is a + // failure case. + return null; + } + } - // We use a sentinel value here so we can track the different between omission and explicit null. - // 'real null' means that the value was omitted. - var hasExplictValue = value != null; - if (hasExplictValue) - { - // If there is a non-parameterized value in the route and there is a - // new value for it and it doesn't match, this route won't match. - if (!RoutePartsEqual(value, filters[i].Value)) - { - return null; - } - } - else + // Any default values that don't appear as parameters are treated like filters. Any new values + // provided must match these defaults. + var filters = _filters; + for (var i = 0; i < filters.Length; i++) + { + var key = filters[i].Key; + var value = slots[i + parameterCount].Value; + + // We use a sentinel value here so we can track the different between omission and explicit null. + // 'real null' means that the value was omitted. + var hasExplictValue = value != null; + if (hasExplictValue) + { + // If there is a non-parameterized value in the route and there is a + // new value for it and it doesn't match, this route won't match. + if (!RoutePartsEqual(value, filters[i].Value)) { - // If no value was provided, then blank out this slot so that it doesn't show up in accepted values. - slots[i + parameterCount] = default; + return null; } } + else + { + // If no value was provided, then blank out this slot so that it doesn't show up in accepted values. + slots[i + parameterCount] = default; + } + } - // At this point we've captured all of the 'known' route values, but we have't - // handled an extra route values that were provided in 'values'. These all - // need to be included in the accepted values. - var acceptedValues = RouteValueDictionary.FromArray(slots); + // At this point we've captured all of the 'known' route values, but we have't + // handled an extra route values that were provided in 'values'. These all + // need to be included in the accepted values. + var acceptedValues = RouteValueDictionary.FromArray(slots); - if (valueProcessedCount < values.Count) + if (valueProcessedCount < values.Count) + { + // There are some values in 'value' that are unaccounted for, merge them into + // the dictionary. + foreach (var kvp in values) { - // There are some values in 'value' that are unaccounted for, merge them into - // the dictionary. - foreach (var kvp in values) + if (!_defaults!.ContainsKey(kvp.Key)) { - if (!_defaults!.ContainsKey(kvp.Key)) - { #if RVD_TryAdd acceptedValues.TryAdd(kvp.Key, kvp.Value); #else - if (!acceptedValues.ContainsKey(kvp.Key)) - { - acceptedValues.Add(kvp.Key, kvp.Value); - } -#endif + if (!acceptedValues.ContainsKey(kvp.Key)) + { + acceptedValues.Add(kvp.Key, kvp.Value); } +#endif } } + } - // Currently this copy is required because BindValues will mutate the accepted values :( - var combinedValues = new RouteValueDictionary(acceptedValues); - - // Add any ambient values that don't match parameters - they need to be visible to constraints - // but they will ignored by link generation. - CopyNonParameterAmbientValues( - ambientValues: ambientValues, - acceptedValues: acceptedValues, - combinedValues: combinedValues); + // Currently this copy is required because BindValues will mutate the accepted values :( + var combinedValues = new RouteValueDictionary(acceptedValues); - return new TemplateValuesResult() - { - AcceptedValues = acceptedValues, - CombinedValues = combinedValues, - }; - } - - // Step 1.5: Process constraints - /// - /// Processes the constraints **if** they were passed in to the TemplateBinder constructor. - /// - /// The associated with the current request. - /// A dictionary that contains the parameters for the route. - /// The name of the parameter. - /// The constraint object. - /// if constraints were processed succesfully and false otherwise. - public bool TryProcessConstraints(HttpContext? httpContext, RouteValueDictionary combinedValues, out string? parameterName, out IRouteConstraint? constraint) - { - var constraints = _constraints; - for (var i = 0; i < constraints.Length; i++) - { - (parameterName, constraint) = constraints[i]; - - if (!constraint.Match(httpContext, NullRouter.Instance, parameterName, combinedValues, RouteDirection.UrlGeneration)) - { - return false; - } - } + // Add any ambient values that don't match parameters - they need to be visible to constraints + // but they will ignored by link generation. + CopyNonParameterAmbientValues( + ambientValues: ambientValues, + acceptedValues: acceptedValues, + combinedValues: combinedValues); - parameterName = null; - constraint = null; - return true; - } + return new TemplateValuesResult() + { + AcceptedValues = acceptedValues, + CombinedValues = combinedValues, + }; + } - // Step 2: If the route is a match generate the appropriate URI - /// - /// Returns a string representation of the URI associated with the route. - /// - /// A dictionary that contains the parameters for the route. - /// The string representation of the route. - public string? BindValues(RouteValueDictionary acceptedValues) + // Step 1.5: Process constraints + /// + /// Processes the constraints **if** they were passed in to the TemplateBinder constructor. + /// + /// The associated with the current request. + /// A dictionary that contains the parameters for the route. + /// The name of the parameter. + /// The constraint object. + /// if constraints were processed succesfully and false otherwise. + public bool TryProcessConstraints(HttpContext? httpContext, RouteValueDictionary combinedValues, out string? parameterName, out IRouteConstraint? constraint) + { + var constraints = _constraints; + for (var i = 0; i < constraints.Length; i++) { - var context = _pool.Get(); + (parameterName, constraint) = constraints[i]; - try - { - return TryBindValuesCore(context, acceptedValues) ? context.ToString() : null; - } - finally + if (!constraint.Match(httpContext, NullRouter.Instance, parameterName, combinedValues, RouteDirection.UrlGeneration)) { - _pool.Return(context); + return false; } } - // Step 2: If the route is a match generate the appropriate URI - internal bool TryBindValues( - RouteValueDictionary acceptedValues, - LinkOptions? options, - LinkOptions globalOptions, - out (PathString path, QueryString query) result) + parameterName = null; + constraint = null; + return true; + } + + // Step 2: If the route is a match generate the appropriate URI + /// + /// Returns a string representation of the URI associated with the route. + /// + /// A dictionary that contains the parameters for the route. + /// The string representation of the route. + public string? BindValues(RouteValueDictionary acceptedValues) + { + var context = _pool.Get(); + + try + { + return TryBindValuesCore(context, acceptedValues) ? context.ToString() : null; + } + finally { - var context = _pool.Get(); + _pool.Return(context); + } + } - context.AppendTrailingSlash = options?.AppendTrailingSlash ?? globalOptions.AppendTrailingSlash ?? false; - context.LowercaseQueryStrings = options?.LowercaseQueryStrings ?? globalOptions.LowercaseQueryStrings ?? false; - context.LowercaseUrls = options?.LowercaseUrls ?? globalOptions.LowercaseUrls ?? false; + // Step 2: If the route is a match generate the appropriate URI + internal bool TryBindValues( + RouteValueDictionary acceptedValues, + LinkOptions? options, + LinkOptions globalOptions, + out (PathString path, QueryString query) result) + { + var context = _pool.Get(); - try - { - if (TryBindValuesCore(context, acceptedValues)) - { - result = (context.ToPathString(), context.ToQueryString()); - return true; - } + context.AppendTrailingSlash = options?.AppendTrailingSlash ?? globalOptions.AppendTrailingSlash ?? false; + context.LowercaseQueryStrings = options?.LowercaseQueryStrings ?? globalOptions.LowercaseQueryStrings ?? false; + context.LowercaseUrls = options?.LowercaseUrls ?? globalOptions.LowercaseUrls ?? false; - result = default; - return false; - } - finally + try + { + if (TryBindValuesCore(context, acceptedValues)) { - _pool.Return(context); + result = (context.ToPathString(), context.ToQueryString()); + return true; } + + result = default; + return false; + } + finally + { + _pool.Return(context); } + } - private bool TryBindValuesCore(UriBuildingContext context, RouteValueDictionary acceptedValues) + private bool TryBindValuesCore(UriBuildingContext context, RouteValueDictionary acceptedValues) + { + // If we have any output parameter transformers, allow them a chance to influence the parameter values + // before we build the URI. + var parameterTransformers = _parameterTransformers; + for (var i = 0; i < parameterTransformers.Length; i++) { - // If we have any output parameter transformers, allow them a chance to influence the parameter values - // before we build the URI. - var parameterTransformers = _parameterTransformers; - for (var i = 0; i < parameterTransformers.Length; i++) + (var parameterName, var transformer) = parameterTransformers[i]; + if (acceptedValues.TryGetValue(parameterName, out var value)) { - (var parameterName, var transformer) = parameterTransformers[i]; - if (acceptedValues.TryGetValue(parameterName, out var value)) - { - acceptedValues[parameterName] = transformer.TransformOutbound(value); - } + acceptedValues[parameterName] = transformer.TransformOutbound(value); } + } + + var segments = _pattern.PathSegments; + // Read interface .Count once rather than per iteration + var segmentsCount = segments.Count; + for (var i = 0; i < segmentsCount; i++) + { + Debug.Assert(context.BufferState == SegmentState.Beginning); + Debug.Assert(context.UriState == SegmentState.Beginning); - var segments = _pattern.PathSegments; + var parts = segments[i].Parts; // Read interface .Count once rather than per iteration - var segmentsCount = segments.Count; - for (var i = 0; i < segmentsCount; i++) + var partsCount = parts.Count; + for (var j = 0; j < partsCount; j++) { - Debug.Assert(context.BufferState == SegmentState.Beginning); - Debug.Assert(context.UriState == SegmentState.Beginning); - - var parts = segments[i].Parts; - // Read interface .Count once rather than per iteration - var partsCount = parts.Count; - for (var j = 0; j < partsCount; j++) + var part = parts[j]; + if (part is RoutePatternLiteralPart literalPart) { - var part = parts[j]; - if (part is RoutePatternLiteralPart literalPart) + if (!context.Accept(literalPart.Content)) { - if (!context.Accept(literalPart.Content)) - { - return false; - } + return false; + } + } + else if (part is RoutePatternSeparatorPart separatorPart) + { + if (!context.Accept(separatorPart.Content)) + { + return false; } - else if (part is RoutePatternSeparatorPart separatorPart) + } + else if (part is RoutePatternParameterPart parameterPart) + { + // If it's a parameter, get its value + acceptedValues.Remove(parameterPart.Name, out var value); + + var isSameAsDefault = false; + if (_defaults != null && + _defaults.TryGetValue(parameterPart.Name, out var defaultValue) && + RoutePartsEqual(value, defaultValue)) { - if (!context.Accept(separatorPart.Content)) + isSameAsDefault = true; + } + + var converted = Convert.ToString(value, CultureInfo.InvariantCulture); + if (isSameAsDefault) + { + // If the accepted value is the same as the default value buffer it since + // we won't necessarily add it to the URI we generate. + if (!context.Buffer(converted)) { return false; } } - else if (part is RoutePatternParameterPart parameterPart) + else { - // If it's a parameter, get its value - acceptedValues.Remove(parameterPart.Name, out var value); - - var isSameAsDefault = false; - if (_defaults != null && - _defaults.TryGetValue(parameterPart.Name, out var defaultValue) && - RoutePartsEqual(value, defaultValue)) + // If the value is not accepted, it is null or empty value in the + // middle of the segment. We accept this if the parameter is an + // optional parameter and it is preceded by an optional seperator. + // In this case, we need to remove the optional seperator that we + // have added to the URI + // Example: template = {id}.{format?}. parameters: id=5 + // In this case after we have generated "5.", we wont find any value + // for format, so we remove '.' and generate 5. + if (!context.Accept(converted, parameterPart.EncodeSlashes)) { - isSameAsDefault = true; - } - - var converted = Convert.ToString(value, CultureInfo.InvariantCulture); - if (isSameAsDefault) - { - // If the accepted value is the same as the default value buffer it since - // we won't necessarily add it to the URI we generate. - if (!context.Buffer(converted)) + RoutePatternSeparatorPart? nullablePart; + if (j != 0 && parameterPart.IsOptional && (nullablePart = parts[j - 1] as RoutePatternSeparatorPart) != null) { - return false; + separatorPart = nullablePart; + context.Remove(separatorPart.Content); } - } - else - { - // If the value is not accepted, it is null or empty value in the - // middle of the segment. We accept this if the parameter is an - // optional parameter and it is preceded by an optional seperator. - // In this case, we need to remove the optional seperator that we - // have added to the URI - // Example: template = {id}.{format?}. parameters: id=5 - // In this case after we have generated "5.", we wont find any value - // for format, so we remove '.' and generate 5. - if (!context.Accept(converted, parameterPart.EncodeSlashes)) + else { - RoutePatternSeparatorPart? nullablePart; - if (j != 0 && parameterPart.IsOptional && (nullablePart = parts[j - 1] as RoutePatternSeparatorPart) != null) - { - separatorPart = nullablePart; - context.Remove(separatorPart.Content); - } - else - { - return false; - } + return false; } } } } - - context.EndSegment(); - } - - // Generate the query string from the remaining values - var wroteFirst = false; - foreach (var kvp in acceptedValues) - { - if (_defaults != null && _defaults.ContainsKey(kvp.Key)) - { - // This value is a 'filter' we don't need to put it in the query string. - continue; - } - - var values = kvp.Value as IEnumerable; - if (values != null && !(values is string)) - { - foreach (var value in values) - { - wroteFirst |= AddQueryKeyValueToContext(context, kvp.Key, value, wroteFirst); - } - } - else - { - wroteFirst |= AddQueryKeyValueToContext(context, kvp.Key, kvp.Value, wroteFirst); - } } - return true; + context.EndSegment(); } - private bool AddQueryKeyValueToContext(UriBuildingContext context, string key, object? value, bool wroteFirst) + // Generate the query string from the remaining values + var wroteFirst = false; + foreach (var kvp in acceptedValues) { - var converted = Convert.ToString(value, CultureInfo.InvariantCulture); - if (!string.IsNullOrEmpty(converted)) + if (_defaults != null && _defaults.ContainsKey(kvp.Key)) { - if (context.LowercaseQueryStrings) - { - key = key.ToLowerInvariant(); - converted = converted.ToLowerInvariant(); - } - - context.QueryWriter.Write(wroteFirst ? '&' : '?'); - _urlEncoder.Encode(context.QueryWriter, key); - context.QueryWriter.Write('='); - _urlEncoder.Encode(context.QueryWriter, converted); - return true; + // This value is a 'filter' we don't need to put it in the query string. + continue; } - return false; - } - /// - /// Compares two objects for equality as parts of a case-insensitive path. - /// - /// An object to compare. - /// An object to compare. - /// True if the object are equal, otherwise false. - public static bool RoutePartsEqual(object? a, object? b) - { - var sa = a as string ?? (ReferenceEquals(SentinullValue.Instance, a) ? string.Empty : null); - var sb = b as string ?? (ReferenceEquals(SentinullValue.Instance, b) ? string.Empty : null); - - // In case of strings, consider empty and null the same. - // Since null cannot tell us the type, consider it to be a string if the other value is a string. - if ((sa == string.Empty && sb == null) || (sb == string.Empty && sa == null)) - { - return true; - } - else if (sa != null && sb != null) + var values = kvp.Value as IEnumerable; + if (values != null && !(values is string)) { - // For strings do a case-insensitive comparison - return string.Equals(sa, sb, StringComparison.OrdinalIgnoreCase); + foreach (var value in values) + { + wroteFirst |= AddQueryKeyValueToContext(context, kvp.Key, value, wroteFirst); + } } else { - if (a != null && b != null) - { - // Explicitly call .Equals() in case it is overridden in the type - return a.Equals(b); - } - else - { - // At least one of them is null. Return true if they both are - return a == b; - } + wroteFirst |= AddQueryKeyValueToContext(context, kvp.Key, kvp.Value, wroteFirst); } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsRoutePartNonEmpty(object? part) + return true; + } + + private bool AddQueryKeyValueToContext(UriBuildingContext context, string key, object? value, bool wroteFirst) + { + var converted = Convert.ToString(value, CultureInfo.InvariantCulture); + if (!string.IsNullOrEmpty(converted)) { - if (part == null) + if (context.LowercaseQueryStrings) { - return false; + key = key.ToLowerInvariant(); + converted = converted.ToLowerInvariant(); } - if (ReferenceEquals(SentinullValue.Instance, part)) - { - return false; - } + context.QueryWriter.Write(wroteFirst ? '&' : '?'); + _urlEncoder.Encode(context.QueryWriter, key); + context.QueryWriter.Write('='); + _urlEncoder.Encode(context.QueryWriter, converted); + return true; + } + return false; + } - if (part is string stringPart && stringPart.Length == 0) - { - return false; - } + /// + /// Compares two objects for equality as parts of a case-insensitive path. + /// + /// An object to compare. + /// An object to compare. + /// True if the object are equal, otherwise false. + public static bool RoutePartsEqual(object? a, object? b) + { + var sa = a as string ?? (ReferenceEquals(SentinullValue.Instance, a) ? string.Empty : null); + var sb = b as string ?? (ReferenceEquals(SentinullValue.Instance, b) ? string.Empty : null); + // In case of strings, consider empty and null the same. + // Since null cannot tell us the type, consider it to be a string if the other value is a string. + if ((sa == string.Empty && sb == null) || (sb == string.Empty && sa == null)) + { return true; } - - private void CopyNonParameterAmbientValues( - RouteValueDictionary? ambientValues, - RouteValueDictionary acceptedValues, - RouteValueDictionary combinedValues) + else if (sa != null && sb != null) { - if (ambientValues == null) + // For strings do a case-insensitive comparison + return string.Equals(sa, sb, StringComparison.OrdinalIgnoreCase); + } + else + { + if (a != null && b != null) { - return; + // Explicitly call .Equals() in case it is overridden in the type + return a.Equals(b); } - - foreach (var kvp in ambientValues) + else { - if (IsRoutePartNonEmpty(kvp.Value)) - { - var parameter = _pattern.GetParameter(kvp.Key); - if (parameter == null && !acceptedValues.ContainsKey(kvp.Key)) - { - combinedValues.Add(kvp.Key, kvp.Value); - } - } + // At least one of them is null. Return true if they both are + return a == b; } } + } - private static KeyValuePair[] AssignSlots(RoutePattern pattern, KeyValuePair[] filters) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsRoutePartNonEmpty(object? part) + { + if (part == null) { - var slots = new KeyValuePair[pattern.Parameters.Count + filters.Length]; + return false; + } - for (var i = 0; i < pattern.Parameters.Count; i++) - { - slots[i] = new KeyValuePair(pattern.Parameters[i].Name, null); - } + if (ReferenceEquals(SentinullValue.Instance, part)) + { + return false; + } + + if (part is string stringPart && stringPart.Length == 0) + { + return false; + } + + return true; + } + + private void CopyNonParameterAmbientValues( + RouteValueDictionary? ambientValues, + RouteValueDictionary acceptedValues, + RouteValueDictionary combinedValues) + { + if (ambientValues == null) + { + return; + } - for (var i = 0; i < filters.Length; i++) + foreach (var kvp in ambientValues) + { + if (IsRoutePartNonEmpty(kvp.Value)) { - slots[i + pattern.Parameters.Count] = new KeyValuePair(filters[i].Key, null); + var parameter = _pattern.GetParameter(kvp.Key); + if (parameter == null && !acceptedValues.ContainsKey(kvp.Key)) + { + combinedValues.Add(kvp.Key, kvp.Value); + } } + } + } + + private static KeyValuePair[] AssignSlots(RoutePattern pattern, KeyValuePair[] filters) + { + var slots = new KeyValuePair[pattern.Parameters.Count + filters.Length]; - return slots; + for (var i = 0; i < pattern.Parameters.Count; i++) + { + slots[i] = new KeyValuePair(pattern.Parameters[i].Name, null); } - // This represents an 'explicit null' in the slots array. - [DebuggerDisplay("explicit null")] - private class SentinullValue + for (var i = 0; i < filters.Length; i++) { - public static object Instance = new SentinullValue(); + slots[i + pattern.Parameters.Count] = new KeyValuePair(filters[i].Key, null); + } - private SentinullValue() - { - } + return slots; + } - public override string ToString() => string.Empty; + // This represents an 'explicit null' in the slots array. + [DebuggerDisplay("explicit null")] + private class SentinullValue + { + public static object Instance = new SentinullValue(); + + private SentinullValue() + { } + + public override string ToString() => string.Empty; } } diff --git a/src/Http/Routing/src/Template/TemplateBinderFactory.cs b/src/Http/Routing/src/Template/TemplateBinderFactory.cs index fef822a7da..ad14113ddd 100644 --- a/src/Http/Routing/src/Template/TemplateBinderFactory.cs +++ b/src/Http/Routing/src/Template/TemplateBinderFactory.cs @@ -3,27 +3,26 @@ using Microsoft.AspNetCore.Routing.Patterns; -namespace Microsoft.AspNetCore.Routing.Template +namespace Microsoft.AspNetCore.Routing.Template; + +/// +/// A factory used to create instances. +/// +public abstract class TemplateBinderFactory { /// - /// A factory used to create instances. + /// Creates a new from the provided and + /// . /// - public abstract class TemplateBinderFactory - { - /// - /// Creates a new from the provided and - /// . - /// - /// The route template. - /// A collection of extra default values that do not appear in the route template. - /// A . - public abstract TemplateBinder Create(RouteTemplate template, RouteValueDictionary defaults); + /// The route template. + /// A collection of extra default values that do not appear in the route template. + /// A . + public abstract TemplateBinder Create(RouteTemplate template, RouteValueDictionary defaults); - /// - /// Creates a new from the provided . - /// - /// The . - /// A . - public abstract TemplateBinder Create(RoutePattern pattern); - } + /// + /// Creates a new from the provided . + /// + /// The . + /// A . + public abstract TemplateBinder Create(RoutePattern pattern); } diff --git a/src/Http/Routing/src/Template/TemplateMatcher.cs b/src/Http/Routing/src/Template/TemplateMatcher.cs index 657a7f0e97..5cf49d7a96 100644 --- a/src/Http/Routing/src/Template/TemplateMatcher.cs +++ b/src/Http/Routing/src/Template/TemplateMatcher.cs @@ -4,93 +4,92 @@ #nullable enable using System; -using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; -namespace Microsoft.AspNetCore.Routing.Template +namespace Microsoft.AspNetCore.Routing.Template; + +/// +/// Supports matching paths to route templates and extracting parameter values. +/// +public class TemplateMatcher { + // Perf: This is a cache to avoid looking things up in 'Defaults' each request. + private readonly bool[] _hasDefaultValue; + private readonly object?[] _defaultValues; + + private readonly RoutePatternMatcher _routePatternMatcher; + /// - /// Supports matching paths to route templates and extracting parameter values. + /// Creates a new instance given a and . /// - public class TemplateMatcher + /// The to compare against. + /// The default values for parameters in the . + public TemplateMatcher( + RouteTemplate template, + RouteValueDictionary defaults) { - // Perf: This is a cache to avoid looking things up in 'Defaults' each request. - private readonly bool[] _hasDefaultValue; - private readonly object?[] _defaultValues; - - private readonly RoutePatternMatcher _routePatternMatcher; - - /// - /// Creates a new instance given a and . - /// - /// The to compare against. - /// The default values for parameters in the . - public TemplateMatcher( - RouteTemplate template, - RouteValueDictionary defaults) + if (template == null) { - if (template == null) - { - throw new ArgumentNullException(nameof(template)); - } + throw new ArgumentNullException(nameof(template)); + } - Template = template; - Defaults = defaults ?? new RouteValueDictionary(); + Template = template; + Defaults = defaults ?? new RouteValueDictionary(); - // Perf: cache the default value for each parameter (other than complex segments). - _hasDefaultValue = new bool[Template.Segments.Count]; - _defaultValues = new object[Template.Segments.Count]; + // Perf: cache the default value for each parameter (other than complex segments). + _hasDefaultValue = new bool[Template.Segments.Count]; + _defaultValues = new object[Template.Segments.Count]; - for (var i = 0; i < Template.Segments.Count; i++) + for (var i = 0; i < Template.Segments.Count; i++) + { + var segment = Template.Segments[i]; + if (!segment.IsSimple) { - var segment = Template.Segments[i]; - if (!segment.IsSimple) - { - continue; - } - - var part = segment.Parts[0]; - if (!part.IsParameter) - { - continue; - } - - if (Defaults.TryGetValue(part.Name!, out var value)) - { - _hasDefaultValue[i] = true; - _defaultValues[i] = value; - } + continue; } - var routePattern = Template.ToRoutePattern(); - _routePatternMatcher = new RoutePatternMatcher(routePattern, Defaults); - } + var part = segment.Parts[0]; + if (!part.IsParameter) + { + continue; + } - /// - /// Gets the default values for parameters in the . - /// - public RouteValueDictionary Defaults { get; } - - /// - /// Gets the to match against. - /// - public RouteTemplate Template { get; } - - /// - /// Evaluates if the provided matches the . Populates - /// with parameter values. - /// - /// A representing the route to match. - /// A to populate with parameter values. - /// if matches . - public bool TryMatch(PathString path, RouteValueDictionary values) - { - if (values == null) + if (Defaults.TryGetValue(part.Name!, out var value)) { - throw new ArgumentNullException(nameof(values)); + _hasDefaultValue[i] = true; + _defaultValues[i] = value; } + } - return _routePatternMatcher.TryMatch(path, values); + var routePattern = Template.ToRoutePattern(); + _routePatternMatcher = new RoutePatternMatcher(routePattern, Defaults); + } + + /// + /// Gets the default values for parameters in the . + /// + public RouteValueDictionary Defaults { get; } + + /// + /// Gets the to match against. + /// + public RouteTemplate Template { get; } + + /// + /// Evaluates if the provided matches the . Populates + /// with parameter values. + /// + /// A representing the route to match. + /// A to populate with parameter values. + /// if matches . + public bool TryMatch(PathString path, RouteValueDictionary values) + { + if (values == null) + { + throw new ArgumentNullException(nameof(values)); } + + return _routePatternMatcher.TryMatch(path, values); } } diff --git a/src/Http/Routing/src/Template/TemplateParser.cs b/src/Http/Routing/src/Template/TemplateParser.cs index 6a059e5356..0f8544db3e 100644 --- a/src/Http/Routing/src/Template/TemplateParser.cs +++ b/src/Http/Routing/src/Template/TemplateParser.cs @@ -4,35 +4,34 @@ using System; using Microsoft.AspNetCore.Routing.Patterns; -namespace Microsoft.AspNetCore.Routing.Template +namespace Microsoft.AspNetCore.Routing.Template; + +/// +/// Provides methods for parsing route template strings. +/// +public static class TemplateParser { /// - /// Provides methods for parsing route template strings. + /// Creates a for a given string. /// - public static class TemplateParser + /// A string representation of the route template. + /// A instance. + public static RouteTemplate Parse(string routeTemplate) { - /// - /// Creates a for a given string. - /// - /// A string representation of the route template. - /// A instance. - public static RouteTemplate Parse(string routeTemplate) + if (routeTemplate == null) { - if (routeTemplate == null) - { - throw new ArgumentNullException(routeTemplate); - } + throw new ArgumentNullException(routeTemplate); + } - try - { - var inner = RoutePatternFactory.Parse(routeTemplate); - return new RouteTemplate(inner); - } - catch (RoutePatternException ex) - { - // Preserving the existing behavior of this API even though the logic moved. - throw new ArgumentException(ex.Message, nameof(routeTemplate), ex); - } + try + { + var inner = RoutePatternFactory.Parse(routeTemplate); + return new RouteTemplate(inner); + } + catch (RoutePatternException ex) + { + // Preserving the existing behavior of this API even though the logic moved. + throw new ArgumentException(ex.Message, nameof(routeTemplate), ex); } } } diff --git a/src/Http/Routing/src/Template/TemplatePart.cs b/src/Http/Routing/src/Template/TemplatePart.cs index 9d791d7831..334b3399ba 100644 --- a/src/Http/Routing/src/Template/TemplatePart.cs +++ b/src/Http/Routing/src/Template/TemplatePart.cs @@ -8,175 +8,174 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.AspNetCore.Routing.Patterns; -namespace Microsoft.AspNetCore.Routing.Template +namespace Microsoft.AspNetCore.Routing.Template; + +/// +/// Represents a part of a route template segment. +/// +[DebuggerDisplay("{DebuggerToString()}")] +public class TemplatePart { /// - /// Represents a part of a route template segment. + /// Constructs a new instance. /// - [DebuggerDisplay("{DebuggerToString()}")] - public class TemplatePart + public TemplatePart() { - /// - /// Constructs a new instance. - /// - public TemplatePart() + } + + /// + /// Constructs a new instance given a . + /// + /// A instance representing the route part. + public TemplatePart(RoutePatternPart other) + { + IsLiteral = other.IsLiteral || other.IsSeparator; + IsParameter = other.IsParameter; + + if (other.IsLiteral && other is RoutePatternLiteralPart literal) { + Text = literal.Content; } - - /// - /// Constructs a new instance given a . - /// - /// A instance representing the route part. - public TemplatePart(RoutePatternPart other) + else if (other.IsParameter && other is RoutePatternParameterPart parameter) { - IsLiteral = other.IsLiteral || other.IsSeparator; - IsParameter = other.IsParameter; - - if (other.IsLiteral && other is RoutePatternLiteralPart literal) - { - Text = literal.Content; - } - else if (other.IsParameter && other is RoutePatternParameterPart parameter) - { - // Text is unused by TemplatePart and assumed to be null when the part is a parameter. - Name = parameter.Name; - IsCatchAll = parameter.IsCatchAll; - IsOptional = parameter.IsOptional; - DefaultValue = parameter.Default; - InlineConstraints = parameter.ParameterPolicies?.Select(p => new InlineConstraint(p)) ?? Enumerable.Empty(); - } - else if (other.IsSeparator && other is RoutePatternSeparatorPart separator) - { - Text = separator.Content; - IsOptionalSeperator = true; - } - else - { - // Unreachable - throw new NotSupportedException(); - } + // Text is unused by TemplatePart and assumed to be null when the part is a parameter. + Name = parameter.Name; + IsCatchAll = parameter.IsCatchAll; + IsOptional = parameter.IsOptional; + DefaultValue = parameter.Default; + InlineConstraints = parameter.ParameterPolicies?.Select(p => new InlineConstraint(p)) ?? Enumerable.Empty(); } - - /// - /// Create a representing a literal route part. - /// - /// The text of the literate route part. - /// A instance. - public static TemplatePart CreateLiteral(string text) + else if (other.IsSeparator && other is RoutePatternSeparatorPart separator) { - return new TemplatePart() - { - IsLiteral = true, - Text = text, - }; + Text = separator.Content; + IsOptionalSeperator = true; } + else + { + // Unreachable + throw new NotSupportedException(); + } + } - /// - /// Creates a representing a parameter part. - /// - /// The name of the parameter. - /// if the parameter is a catch-all parameter. - /// if the parameter is an optional parameter. - /// The default value of the parameter. - /// A collection of constraints associated with the parameter. - /// A instance. - public static TemplatePart CreateParameter( - string name, - bool isCatchAll, - bool isOptional, - object? defaultValue, - IEnumerable? inlineConstraints) + /// + /// Create a representing a literal route part. + /// + /// The text of the literate route part. + /// A instance. + public static TemplatePart CreateLiteral(string text) + { + return new TemplatePart() { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } + IsLiteral = true, + Text = text, + }; + } - return new TemplatePart() - { - IsParameter = true, - Name = name, - IsCatchAll = isCatchAll, - IsOptional = isOptional, - DefaultValue = defaultValue, - InlineConstraints = inlineConstraints ?? Enumerable.Empty(), - }; + /// + /// Creates a representing a parameter part. + /// + /// The name of the parameter. + /// if the parameter is a catch-all parameter. + /// if the parameter is an optional parameter. + /// The default value of the parameter. + /// A collection of constraints associated with the parameter. + /// A instance. + public static TemplatePart CreateParameter( + string name, + bool isCatchAll, + bool isOptional, + object? defaultValue, + IEnumerable? inlineConstraints) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); } - /// - /// if the route part is is a catch-all part (e.g. /*). - /// - public bool IsCatchAll { get; private set; } - /// - /// if the route part is represents a literal value. - /// - public bool IsLiteral { get; private set; } - /// - /// if the route part represents a parameterized value. - /// - public bool IsParameter { get; private set; } - /// - /// if the route part represents an optional part. - /// - public bool IsOptional { get; private set; } - /// - /// if the route part represents an optional seperator. - /// - public bool IsOptionalSeperator { get; set; } - /// - /// The name of the route parameter. Can be null. - /// - public string? Name { get; private set; } - /// - /// The textual representation of the route parameter. Can be null. Used to represent route seperators and literal parts. - /// - public string? Text { get; private set; } - /// - /// The default value for route parameters. Can be null. - /// - public object? DefaultValue { get; private set; } - /// - /// The constraints associates with a route parameter. - /// - public IEnumerable InlineConstraints { get; private set; } = Enumerable.Empty(); + return new TemplatePart() + { + IsParameter = true, + Name = name, + IsCatchAll = isCatchAll, + IsOptional = isOptional, + DefaultValue = defaultValue, + InlineConstraints = inlineConstraints ?? Enumerable.Empty(), + }; + } - internal string? DebuggerToString() + /// + /// if the route part is is a catch-all part (e.g. /*). + /// + public bool IsCatchAll { get; private set; } + /// + /// if the route part is represents a literal value. + /// + public bool IsLiteral { get; private set; } + /// + /// if the route part represents a parameterized value. + /// + public bool IsParameter { get; private set; } + /// + /// if the route part represents an optional part. + /// + public bool IsOptional { get; private set; } + /// + /// if the route part represents an optional seperator. + /// + public bool IsOptionalSeperator { get; set; } + /// + /// The name of the route parameter. Can be null. + /// + public string? Name { get; private set; } + /// + /// The textual representation of the route parameter. Can be null. Used to represent route seperators and literal parts. + /// + public string? Text { get; private set; } + /// + /// The default value for route parameters. Can be null. + /// + public object? DefaultValue { get; private set; } + /// + /// The constraints associates with a route parameter. + /// + public IEnumerable InlineConstraints { get; private set; } = Enumerable.Empty(); + + internal string? DebuggerToString() + { + if (IsParameter) { - if (IsParameter) - { - return "{" + (IsCatchAll ? "*" : string.Empty) + Name + (IsOptional ? "?" : string.Empty) + "}"; - } - else - { - return Text; - } + return "{" + (IsCatchAll ? "*" : string.Empty) + Name + (IsOptional ? "?" : string.Empty) + "}"; } + else + { + return Text; + } + } - /// - /// Creates a for the route part designated by the . - /// - /// A instance. - public RoutePatternPart ToRoutePatternPart() + /// + /// Creates a for the route part designated by the . + /// + /// A instance. + public RoutePatternPart ToRoutePatternPart() + { + if (IsLiteral && IsOptionalSeperator) + { + return RoutePatternFactory.SeparatorPart(Text!); + } + else if (IsLiteral) + { + return RoutePatternFactory.LiteralPart(Text!); + } + else { - if (IsLiteral && IsOptionalSeperator) - { - return RoutePatternFactory.SeparatorPart(Text!); - } - else if (IsLiteral) - { - return RoutePatternFactory.LiteralPart(Text!); - } - else - { - var kind = IsCatchAll ? - RoutePatternParameterKind.CatchAll : - IsOptional ? - RoutePatternParameterKind.Optional : - RoutePatternParameterKind.Standard; + var kind = IsCatchAll ? + RoutePatternParameterKind.CatchAll : + IsOptional ? + RoutePatternParameterKind.Optional : + RoutePatternParameterKind.Standard; - var constraints = InlineConstraints.Select(c => new RoutePatternParameterPolicyReference(c.Constraint)); - return RoutePatternFactory.ParameterPart(Name!, DefaultValue, kind, constraints); - } + var constraints = InlineConstraints.Select(c => new RoutePatternParameterPolicyReference(c.Constraint)); + return RoutePatternFactory.ParameterPart(Name!, DefaultValue, kind, constraints); } } } diff --git a/src/Http/Routing/src/Template/TemplateSegment.cs b/src/Http/Routing/src/Template/TemplateSegment.cs index 9f92a24787..a02a27d788 100644 --- a/src/Http/Routing/src/Template/TemplateSegment.cs +++ b/src/Http/Routing/src/Template/TemplateSegment.cs @@ -7,64 +7,63 @@ using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Routing.Patterns; -namespace Microsoft.AspNetCore.Routing.Template +namespace Microsoft.AspNetCore.Routing.Template; + +/// +/// Represents a segment of a route template. +/// +[DebuggerDisplay("{DebuggerToString()}")] +public class TemplateSegment { /// - /// Represents a segment of a route template. + /// Constructs a new instance. + /// + public TemplateSegment() + { + Parts = new List(); + } + + /// + /// Constructs a new instance given another . /// - [DebuggerDisplay("{DebuggerToString()}")] - public class TemplateSegment + /// A instance. + public TemplateSegment(RoutePatternPathSegment other) { - /// - /// Constructs a new instance. - /// - public TemplateSegment() + if (other == null) { - Parts = new List(); + throw new ArgumentNullException(nameof(other)); } - /// - /// Constructs a new instance given another . - /// - /// A instance. - public TemplateSegment(RoutePatternPathSegment other) + var partCount = other.Parts.Count; + Parts = new List(partCount); + for (var i = 0; i < partCount; i++) { - if (other == null) - { - throw new ArgumentNullException(nameof(other)); - } - - var partCount = other.Parts.Count; - Parts = new List(partCount); - for (var i = 0; i < partCount; i++) - { - Parts.Add(new TemplatePart(other.Parts[i])); - } + Parts.Add(new TemplatePart(other.Parts[i])); } + } - /// - /// if the segment contains a single entry. - /// - public bool IsSimple => Parts.Count == 1; + /// + /// if the segment contains a single entry. + /// + public bool IsSimple => Parts.Count == 1; - /// - /// Gets the list of individual parts in the template segment. - /// - public List Parts { get; } + /// + /// Gets the list of individual parts in the template segment. + /// + public List Parts { get; } - internal string DebuggerToString() - { - return string.Join(string.Empty, Parts.Select(p => p.DebuggerToString())); - } + internal string DebuggerToString() + { + return string.Join(string.Empty, Parts.Select(p => p.DebuggerToString())); + } - /// - /// Returns a for the template segment. - /// - /// A instance. - public RoutePatternPathSegment ToRoutePatternPathSegment() - { - var parts = Parts.Select(p => p.ToRoutePatternPart()); - return RoutePatternFactory.Segment(parts); - } + /// + /// Returns a for the template segment. + /// + /// A instance. + public RoutePatternPathSegment ToRoutePatternPathSegment() + { + var parts = Parts.Select(p => p.ToRoutePatternPart()); + return RoutePatternFactory.Segment(parts); } } diff --git a/src/Http/Routing/src/Template/TemplateValuesResult.cs b/src/Http/Routing/src/Template/TemplateValuesResult.cs index 18a1762a6a..bfe086fdcd 100644 --- a/src/Http/Routing/src/Template/TemplateValuesResult.cs +++ b/src/Http/Routing/src/Template/TemplateValuesResult.cs @@ -1,29 +1,28 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Template +namespace Microsoft.AspNetCore.Routing.Template; + +/// +/// The values used as inputs for constraints and link generation. +/// +public class TemplateValuesResult { /// - /// The values used as inputs for constraints and link generation. + /// The set of values that will appear in the URL. /// - public class TemplateValuesResult - { - /// - /// The set of values that will appear in the URL. - /// - public RouteValueDictionary AcceptedValues { get; set; } = default!; + public RouteValueDictionary AcceptedValues { get; set; } = default!; - /// - /// The set of values that that were supplied for URL generation. - /// - /// - /// This combines implicit (ambient) values from the of the current request - /// (if applicable), explictly provided values, and default values for parameters that appear in - /// the route template. - /// - /// Implicit (ambient) values which are invalidated due to changes in values lexically earlier in the - /// route template are excluded from this set. - /// - public RouteValueDictionary CombinedValues { get; set; } = default!; - } + /// + /// The set of values that that were supplied for URL generation. + /// + /// + /// This combines implicit (ambient) values from the of the current request + /// (if applicable), explictly provided values, and default values for parameters that appear in + /// the route template. + /// + /// Implicit (ambient) values which are invalidated due to changes in values lexically earlier in the + /// route template are excluded from this set. + /// + public RouteValueDictionary CombinedValues { get; set; } = default!; } diff --git a/src/Http/Routing/src/Tree/InboundMatch.cs b/src/Http/Routing/src/Tree/InboundMatch.cs index 604b1be6c0..e709c247fe 100644 --- a/src/Http/Routing/src/Tree/InboundMatch.cs +++ b/src/Http/Routing/src/Tree/InboundMatch.cs @@ -6,27 +6,26 @@ using System.Diagnostics; using Microsoft.AspNetCore.Routing.Template; -namespace Microsoft.AspNetCore.Routing.Tree +namespace Microsoft.AspNetCore.Routing.Tree; + +/// +/// A candidate route to match incoming URLs in a . +/// +[DebuggerDisplay("{DebuggerToString(),nq}")] +public class InboundMatch { /// - /// A candidate route to match incoming URLs in a . + /// Gets or sets the . /// - [DebuggerDisplay("{DebuggerToString(),nq}")] - public class InboundMatch - { - /// - /// Gets or sets the . - /// - public InboundRouteEntry Entry { get; set; } + public InboundRouteEntry Entry { get; set; } - /// - /// Gets or sets the . - /// - public TemplateMatcher TemplateMatcher { get; set; } + /// + /// Gets or sets the . + /// + public TemplateMatcher TemplateMatcher { get; set; } - private string DebuggerToString() - { - return TemplateMatcher?.Template?.TemplateText; - } + private string DebuggerToString() + { + return TemplateMatcher?.Template?.TemplateText; } } diff --git a/src/Http/Routing/src/Tree/InboundRouteEntry.cs b/src/Http/Routing/src/Tree/InboundRouteEntry.cs index efb52600b7..b6688b7647 100644 --- a/src/Http/Routing/src/Tree/InboundRouteEntry.cs +++ b/src/Http/Routing/src/Tree/InboundRouteEntry.cs @@ -6,53 +6,52 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Routing.Template; -namespace Microsoft.AspNetCore.Routing.Tree +namespace Microsoft.AspNetCore.Routing.Tree; + +/// +/// Used to build an . Represents a URL template tha will be used to match incoming +/// request URLs. +/// +public class InboundRouteEntry { /// - /// Used to build an . Represents a URL template tha will be used to match incoming - /// request URLs. + /// Gets or sets the route constraints. + /// + public IDictionary Constraints { get; set; } + + /// + /// Gets or sets the route defaults. + /// + public RouteValueDictionary Defaults { get; set; } + + /// + /// Gets or sets the to invoke when this entry matches. + /// + public IRouter Handler { get; set; } + + /// + /// Gets or sets the order of the entry. + /// + /// + /// Entries are ordered first by (ascending) then by (descending). + /// + public int Order { get; set; } + + /// + /// Gets or sets the precedence of the entry. + /// + /// + /// Entries are ordered first by (ascending) then by (descending). + /// + public decimal Precedence { get; set; } + + /// + /// Gets or sets the name of the route. + /// + public string RouteName { get; set; } + + /// + /// Gets or sets the . /// - public class InboundRouteEntry - { - /// - /// Gets or sets the route constraints. - /// - public IDictionary Constraints { get; set; } - - /// - /// Gets or sets the route defaults. - /// - public RouteValueDictionary Defaults { get; set; } - - /// - /// Gets or sets the to invoke when this entry matches. - /// - public IRouter Handler { get; set; } - - /// - /// Gets or sets the order of the entry. - /// - /// - /// Entries are ordered first by (ascending) then by (descending). - /// - public int Order { get; set; } - - /// - /// Gets or sets the precedence of the entry. - /// - /// - /// Entries are ordered first by (ascending) then by (descending). - /// - public decimal Precedence { get; set; } - - /// - /// Gets or sets the name of the route. - /// - public string RouteName { get; set; } - - /// - /// Gets or sets the . - /// - public RouteTemplate RouteTemplate { get; set; } - } + public RouteTemplate RouteTemplate { get; set; } } diff --git a/src/Http/Routing/src/Tree/LinkGenerationDecisionTree.cs b/src/Http/Routing/src/Tree/LinkGenerationDecisionTree.cs index e7eb90bb07..1998da0b1c 100644 --- a/src/Http/Routing/src/Tree/LinkGenerationDecisionTree.cs +++ b/src/Http/Routing/src/Tree/LinkGenerationDecisionTree.cs @@ -11,257 +11,256 @@ using System.Text; using Microsoft.AspNetCore.Routing.DecisionTree; using Microsoft.AspNetCore.Routing.Patterns; -namespace Microsoft.AspNetCore.Routing.Tree +namespace Microsoft.AspNetCore.Routing.Tree; + +// A decision tree that matches link generation entries based on route data. +[DebuggerDisplay("{DebuggerDisplayString,nq}")] +internal class LinkGenerationDecisionTree { - // A decision tree that matches link generation entries based on route data. - [DebuggerDisplay("{DebuggerDisplayString,nq}")] - internal class LinkGenerationDecisionTree - { - // Fallback value for cases where the ambient values weren't provided. - // - // This is safe because we don't mutate the route values in here. - private static readonly RouteValueDictionary EmptyAmbientValues = new RouteValueDictionary(); + // Fallback value for cases where the ambient values weren't provided. + // + // This is safe because we don't mutate the route values in here. + private static readonly RouteValueDictionary EmptyAmbientValues = new RouteValueDictionary(); - private readonly DecisionTreeNode _root; - private readonly List _conventionalEntries; + private readonly DecisionTreeNode _root; + private readonly List _conventionalEntries; - public LinkGenerationDecisionTree(IReadOnlyList entries) - { - // We split up the entries into: - // 1. attribute routes - these go into the tree - // 2. conventional routes - these are a list - var attributedEntries = new List(); - _conventionalEntries = new List(); + public LinkGenerationDecisionTree(IReadOnlyList entries) + { + // We split up the entries into: + // 1. attribute routes - these go into the tree + // 2. conventional routes - these are a list + var attributedEntries = new List(); + _conventionalEntries = new List(); - // Anything with a RoutePattern.RequiredValueAny as a RequiredValue is a conventional route. - // This is because RequiredValueAny acts as a wildcard, whereas an attribute route entry - // is denormalized to contain an exact set of required values. - // - // We will only see conventional routes show up here for endpoint routing. - for (var i = 0; i < entries.Count; i++) + // Anything with a RoutePattern.RequiredValueAny as a RequiredValue is a conventional route. + // This is because RequiredValueAny acts as a wildcard, whereas an attribute route entry + // is denormalized to contain an exact set of required values. + // + // We will only see conventional routes show up here for endpoint routing. + for (var i = 0; i < entries.Count; i++) + { + var isAttributeRoute = true; + var entry = entries[i]; + foreach (var kvp in entry.Entry.RequiredLinkValues) { - var isAttributeRoute = true; - var entry = entries[i]; - foreach (var kvp in entry.Entry.RequiredLinkValues) - { - if (RoutePattern.IsRequiredValueAny(kvp.Value)) - { - isAttributeRoute = false; - break; - } - } - - if (isAttributeRoute) - { - attributedEntries.Add(entry); - } - else + if (RoutePattern.IsRequiredValueAny(kvp.Value)) { - _conventionalEntries.Add(entry); + isAttributeRoute = false; + break; } } - _root = DecisionTreeBuilder.GenerateTree( - attributedEntries, - new OutboundMatchClassifier()); + if (isAttributeRoute) + { + attributedEntries.Add(entry); + } + else + { + _conventionalEntries.Add(entry); + } } - public IList GetMatches(RouteValueDictionary values, RouteValueDictionary ambientValues) + _root = DecisionTreeBuilder.GenerateTree( + attributedEntries, + new OutboundMatchClassifier()); + } + + public IList GetMatches(RouteValueDictionary values, RouteValueDictionary ambientValues) + { + // Perf: Avoid allocation for List if there aren't any Matches or Criteria + if (_root.Matches.Count > 0 || _root.Criteria.Count > 0 || _conventionalEntries.Count > 0) { - // Perf: Avoid allocation for List if there aren't any Matches or Criteria - if (_root.Matches.Count > 0 || _root.Criteria.Count > 0 || _conventionalEntries.Count > 0) - { - var results = new List(); - Walk(results, values, ambientValues ?? EmptyAmbientValues, _root, isFallbackPath: false); - ProcessConventionalEntries(results, values, ambientValues ?? EmptyAmbientValues); - results.Sort(OutboundMatchResultComparer.Instance); - return results; - } + var results = new List(); + Walk(results, values, ambientValues ?? EmptyAmbientValues, _root, isFallbackPath: false); + ProcessConventionalEntries(results, values, ambientValues ?? EmptyAmbientValues); + results.Sort(OutboundMatchResultComparer.Instance); + return results; + } + + return null; + } - return null; + // We need to recursively walk the decision tree based on the provided route data + // (context.Values + context.AmbientValues) to find all entries that match. This process is + // virtually identical to action selection. + // + // Each entry has a collection of 'required link values' that must be satisfied. These are + // key-value pairs that make up the decision tree. + // + // A 'require link value' is considered satisfied IF: + // 1. The value in context.Values matches the required value OR + // 2. There is no value in context.Values and the value in context.AmbientValues matches OR + // 3. The required value is 'null' and there is no value in context.Values. + // + // Ex: + // entry requires { area = null, controller = Store, action = Buy } + // context.Values = { controller = Store, action = Buy } + // context.AmbientValues = { area = Help, controller = AboutStore, action = HowToBuyThings } + // + // In this case the entry is a match. The 'controller' and 'action' are both supplied by context.Values, + // and the 'area' is satisfied because there's NOT a value in context.Values. It's OK to ignore ambient + // values in link generation. + // + // If another entry existed like { area = Help, controller = Store, action = Buy }, this would also + // match. + // + // The decision tree uses a tree data structure to execute these rules across all candidates at once. + private void Walk( + List results, + RouteValueDictionary values, + RouteValueDictionary ambientValues, + DecisionTreeNode node, + bool isFallbackPath) + { + // Any entries in node.Matches have had all their required values satisfied, so add them + // to the results. + var matches = node.Matches; + // Read interface .Count once rather than per iteration + var matchesCount = matches.Count; + for (var i = 0; i < matchesCount; i++) + { + results.Add(new OutboundMatchResult(matches[i], isFallbackPath)); } - // We need to recursively walk the decision tree based on the provided route data - // (context.Values + context.AmbientValues) to find all entries that match. This process is - // virtually identical to action selection. - // - // Each entry has a collection of 'required link values' that must be satisfied. These are - // key-value pairs that make up the decision tree. - // - // A 'require link value' is considered satisfied IF: - // 1. The value in context.Values matches the required value OR - // 2. There is no value in context.Values and the value in context.AmbientValues matches OR - // 3. The required value is 'null' and there is no value in context.Values. - // - // Ex: - // entry requires { area = null, controller = Store, action = Buy } - // context.Values = { controller = Store, action = Buy } - // context.AmbientValues = { area = Help, controller = AboutStore, action = HowToBuyThings } - // - // In this case the entry is a match. The 'controller' and 'action' are both supplied by context.Values, - // and the 'area' is satisfied because there's NOT a value in context.Values. It's OK to ignore ambient - // values in link generation. - // - // If another entry existed like { area = Help, controller = Store, action = Buy }, this would also - // match. - // - // The decision tree uses a tree data structure to execute these rules across all candidates at once. - private void Walk( - List results, - RouteValueDictionary values, - RouteValueDictionary ambientValues, - DecisionTreeNode node, - bool isFallbackPath) + var criteria = node.Criteria; + // Read interface .Count once rather than per iteration + var criteriaCount = criteria.Count; + for (var i = 0; i < criteriaCount; i++) { - // Any entries in node.Matches have had all their required values satisfied, so add them - // to the results. - var matches = node.Matches; - // Read interface .Count once rather than per iteration - var matchesCount = matches.Count; - for (var i = 0; i < matchesCount; i++) + var criterion = criteria[i]; + var key = criterion.Key; + + if (values.TryGetValue(key, out var value)) { - results.Add(new OutboundMatchResult(matches[i], isFallbackPath)); + if (criterion.Branches.TryGetValue(value ?? string.Empty, out var branch)) + { + Walk(results, values, ambientValues, branch, isFallbackPath); + } } - - var criteria = node.Criteria; - // Read interface .Count once rather than per iteration - var criteriaCount = criteria.Count; - for (var i = 0; i < criteriaCount; i++) + else { - var criterion = criteria[i]; - var key = criterion.Key; - - if (values.TryGetValue(key, out var value)) + // If a value wasn't explicitly supplied, match BOTH the ambient value and the empty value + // if an ambient value was supplied. The path explored with the empty value is considered + // the fallback path. + DecisionTreeNode branch; + if (ambientValues.TryGetValue(key, out value) && + !criterion.Branches.Comparer.Equals(value, string.Empty)) { - if (criterion.Branches.TryGetValue(value ?? string.Empty, out var branch)) + if (criterion.Branches.TryGetValue(value, out branch)) { Walk(results, values, ambientValues, branch, isFallbackPath); } } - else - { - // If a value wasn't explicitly supplied, match BOTH the ambient value and the empty value - // if an ambient value was supplied. The path explored with the empty value is considered - // the fallback path. - DecisionTreeNode branch; - if (ambientValues.TryGetValue(key, out value) && - !criterion.Branches.Comparer.Equals(value, string.Empty)) - { - if (criterion.Branches.TryGetValue(value, out branch)) - { - Walk(results, values, ambientValues, branch, isFallbackPath); - } - } - if (criterion.Branches.TryGetValue(string.Empty, out branch)) - { - Walk(results, values, ambientValues, branch, isFallbackPath: true); - } + if (criterion.Branches.TryGetValue(string.Empty, out branch)) + { + Walk(results, values, ambientValues, branch, isFallbackPath: true); } } } + } - private void ProcessConventionalEntries( - List results, - RouteValueDictionary values, - RouteValueDictionary ambientvalues) + private void ProcessConventionalEntries( + List results, + RouteValueDictionary values, + RouteValueDictionary ambientvalues) + { + for (var i = 0; i < _conventionalEntries.Count; i++) { - for (var i = 0; i < _conventionalEntries.Count; i++) - { - results.Add(new OutboundMatchResult(_conventionalEntries[i], isFallbackMatch: false)); - } + results.Add(new OutboundMatchResult(_conventionalEntries[i], isFallbackMatch: false)); } + } - private class OutboundMatchClassifier : IClassifier - { - public IEqualityComparer ValueComparer => RouteValueEqualityComparer.Default; + private class OutboundMatchClassifier : IClassifier + { + public IEqualityComparer ValueComparer => RouteValueEqualityComparer.Default; - public IDictionary GetCriteria(OutboundMatch item) + public IDictionary GetCriteria(OutboundMatch item) + { + var results = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var kvp in item.Entry.RequiredLinkValues) { - var results = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var kvp in item.Entry.RequiredLinkValues) - { - results.Add(kvp.Key, new DecisionCriterionValue(kvp.Value ?? string.Empty)); - } - - return results; + results.Add(kvp.Key, new DecisionCriterionValue(kvp.Value ?? string.Empty)); } + + return results; } + } - private class OutboundMatchResultComparer : IComparer - { - public static readonly OutboundMatchResultComparer Instance = new OutboundMatchResultComparer(); + private class OutboundMatchResultComparer : IComparer + { + public static readonly OutboundMatchResultComparer Instance = new OutboundMatchResultComparer(); - public int Compare(OutboundMatchResult x, OutboundMatchResult y) + public int Compare(OutboundMatchResult x, OutboundMatchResult y) + { + // For this comparison lower is better. + if (x.Match.Entry.Order != y.Match.Entry.Order) { - // For this comparison lower is better. - if (x.Match.Entry.Order != y.Match.Entry.Order) - { - return x.Match.Entry.Order.CompareTo(y.Match.Entry.Order); - } - - if (x.Match.Entry.Precedence != y.Match.Entry.Precedence) - { - // Reversed because higher is better - return y.Match.Entry.Precedence.CompareTo(x.Match.Entry.Precedence); - } + return x.Match.Entry.Order.CompareTo(y.Match.Entry.Order); + } - if (x.IsFallbackMatch != y.IsFallbackMatch) - { - // A fallback match is worse than a non-fallback - return x.IsFallbackMatch.CompareTo(y.IsFallbackMatch); - } + if (x.Match.Entry.Precedence != y.Match.Entry.Precedence) + { + // Reversed because higher is better + return y.Match.Entry.Precedence.CompareTo(x.Match.Entry.Precedence); + } - return string.Compare( - x.Match.Entry.RouteTemplate.TemplateText, - y.Match.Entry.RouteTemplate.TemplateText, - StringComparison.Ordinal); + if (x.IsFallbackMatch != y.IsFallbackMatch) + { + // A fallback match is worse than a non-fallback + return x.IsFallbackMatch.CompareTo(y.IsFallbackMatch); } + + return string.Compare( + x.Match.Entry.RouteTemplate.TemplateText, + y.Match.Entry.RouteTemplate.TemplateText, + StringComparison.Ordinal); } + } - // Example output: - // - // => action: Buy => controller: Store => version: V1(Matches: Store/Buy/V1) - // => action: Buy => controller: Store => version: V2(Matches: Store/Buy/V2) - // => action: Buy => controller: Store => area: Admin(Matches: Admin/Store/Buy) - // => action: Buy => controller: Products(Matches: Products/Buy) - // => action: Cart => controller: Store(Matches: Store/Cart) - internal string DebuggerDisplayString + // Example output: + // + // => action: Buy => controller: Store => version: V1(Matches: Store/Buy/V1) + // => action: Buy => controller: Store => version: V2(Matches: Store/Buy/V2) + // => action: Buy => controller: Store => area: Admin(Matches: Admin/Store/Buy) + // => action: Buy => controller: Products(Matches: Products/Buy) + // => action: Cart => controller: Store(Matches: Store/Cart) + internal string DebuggerDisplayString + { + get { - get - { - var sb = new StringBuilder(); - var branchStack = new Stack(); - branchStack.Push(string.Empty); - FlattenTree(branchStack, sb, _root); - return sb.ToString(); - } + var sb = new StringBuilder(); + var branchStack = new Stack(); + branchStack.Push(string.Empty); + FlattenTree(branchStack, sb, _root); + return sb.ToString(); } + } - private void FlattenTree(Stack branchStack, StringBuilder sb, DecisionTreeNode node) + private void FlattenTree(Stack branchStack, StringBuilder sb, DecisionTreeNode node) + { + // leaf node + if (node.Criteria.Count == 0) { - // leaf node - if (node.Criteria.Count == 0) + var matchesSb = new StringBuilder(); + foreach (var branch in branchStack) { - var matchesSb = new StringBuilder(); - foreach (var branch in branchStack) - { - matchesSb.Insert(0, branch); - } - sb.Append(matchesSb); - sb.Append(" (Matches: "); - sb.AppendJoin(", ", node.Matches.Select(m => m.Entry.RouteTemplate.TemplateText)); - sb.AppendLine(")"); + matchesSb.Insert(0, branch); } + sb.Append(matchesSb); + sb.Append(" (Matches: "); + sb.AppendJoin(", ", node.Matches.Select(m => m.Entry.RouteTemplate.TemplateText)); + sb.AppendLine(")"); + } - foreach (var criterion in node.Criteria) + foreach (var criterion in node.Criteria) + { + foreach (var branch in criterion.Branches) { - foreach (var branch in criterion.Branches) - { - branchStack.Push($" => {criterion.Key}: {branch.Key}"); - FlattenTree(branchStack, sb, branch.Value); - branchStack.Pop(); - } + branchStack.Push($" => {criterion.Key}: {branch.Key}"); + FlattenTree(branchStack, sb, branch.Value); + branchStack.Pop(); } } } diff --git a/src/Http/Routing/src/Tree/OutboundMatch.cs b/src/Http/Routing/src/Tree/OutboundMatch.cs index e63ca507a1..eef5a421db 100644 --- a/src/Http/Routing/src/Tree/OutboundMatch.cs +++ b/src/Http/Routing/src/Tree/OutboundMatch.cs @@ -5,21 +5,20 @@ using Microsoft.AspNetCore.Routing.Template; -namespace Microsoft.AspNetCore.Routing.Tree +namespace Microsoft.AspNetCore.Routing.Tree; + +/// +/// A candidate match for link generation in a . +/// +public class OutboundMatch { /// - /// A candidate match for link generation in a . + /// Gets or sets the . /// - public class OutboundMatch - { - /// - /// Gets or sets the . - /// - public OutboundRouteEntry Entry { get; set; } + public OutboundRouteEntry Entry { get; set; } - /// - /// Gets or sets the . - /// - public TemplateBinder TemplateBinder { get; set; } - } + /// + /// Gets or sets the . + /// + public TemplateBinder TemplateBinder { get; set; } } diff --git a/src/Http/Routing/src/Tree/OutboundMatchResult.cs b/src/Http/Routing/src/Tree/OutboundMatchResult.cs index 36c85ec701..db1d31a9eb 100644 --- a/src/Http/Routing/src/Tree/OutboundMatchResult.cs +++ b/src/Http/Routing/src/Tree/OutboundMatchResult.cs @@ -1,18 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Tree +namespace Microsoft.AspNetCore.Routing.Tree; + +internal readonly struct OutboundMatchResult { - internal readonly struct OutboundMatchResult + public OutboundMatchResult(OutboundMatch match, bool isFallbackMatch) { - public OutboundMatchResult(OutboundMatch match, bool isFallbackMatch) - { - Match = match; - IsFallbackMatch = isFallbackMatch; - } + Match = match; + IsFallbackMatch = isFallbackMatch; + } - public OutboundMatch Match { get; } + public OutboundMatch Match { get; } - public bool IsFallbackMatch { get; } - } + public bool IsFallbackMatch { get; } } diff --git a/src/Http/Routing/src/Tree/OutboundRouteEntry.cs b/src/Http/Routing/src/Tree/OutboundRouteEntry.cs index 17979afdd1..3ef79dcf82 100644 --- a/src/Http/Routing/src/Tree/OutboundRouteEntry.cs +++ b/src/Http/Routing/src/Tree/OutboundRouteEntry.cs @@ -6,64 +6,63 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Routing.Template; -namespace Microsoft.AspNetCore.Routing.Tree +namespace Microsoft.AspNetCore.Routing.Tree; + +/// +/// Used to build a . Represents a URL template that will be used to generate +/// outgoing URLs. +/// +public class OutboundRouteEntry { /// - /// Used to build a . Represents a URL template that will be used to generate - /// outgoing URLs. + /// Gets or sets the route constraints. /// - public class OutboundRouteEntry - { - /// - /// Gets or sets the route constraints. - /// - public IDictionary Constraints { get; set; } + public IDictionary Constraints { get; set; } - /// - /// Gets or sets the route defaults. - /// - public RouteValueDictionary Defaults { get; set; } + /// + /// Gets or sets the route defaults. + /// + public RouteValueDictionary Defaults { get; set; } - /// - /// The to invoke when this entry matches. - /// - public IRouter Handler { get; set; } + /// + /// The to invoke when this entry matches. + /// + public IRouter Handler { get; set; } - /// - /// Gets or sets the order of the entry. - /// - /// - /// Entries are ordered first by (ascending) then by (descending). - /// - public int Order { get; set; } + /// + /// Gets or sets the order of the entry. + /// + /// + /// Entries are ordered first by (ascending) then by (descending). + /// + public int Order { get; set; } - /// - /// Gets or sets the precedence of the template for link generation. A greater value of - /// means that an entry is considered first. - /// - /// - /// Entries are ordered first by (ascending) then by (descending). - /// - public decimal Precedence { get; set; } + /// + /// Gets or sets the precedence of the template for link generation. A greater value of + /// means that an entry is considered first. + /// + /// + /// Entries are ordered first by (ascending) then by (descending). + /// + public decimal Precedence { get; set; } - /// - /// Gets or sets the name of the route. - /// - public string RouteName { get; set; } + /// + /// Gets or sets the name of the route. + /// + public string RouteName { get; set; } - /// - /// Gets or sets the set of values that must be present for link genration. - /// - public RouteValueDictionary RequiredLinkValues { get; set; } + /// + /// Gets or sets the set of values that must be present for link genration. + /// + public RouteValueDictionary RequiredLinkValues { get; set; } - /// - /// Gets or sets the . - /// - public RouteTemplate RouteTemplate { get; set; } + /// + /// Gets or sets the . + /// + public RouteTemplate RouteTemplate { get; set; } - /// - /// Gets or sets the data that is associated with this entry. - /// - public object Data { get; set; } - } + /// + /// Gets or sets the data that is associated with this entry. + /// + public object Data { get; set; } } diff --git a/src/Http/Routing/src/Tree/TreeEnumerator.cs b/src/Http/Routing/src/Tree/TreeEnumerator.cs index de825aa810..f326cb3d86 100644 --- a/src/Http/Routing/src/Tree/TreeEnumerator.cs +++ b/src/Http/Routing/src/Tree/TreeEnumerator.cs @@ -7,106 +7,105 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; -namespace Microsoft.AspNetCore.Routing.Tree +namespace Microsoft.AspNetCore.Routing.Tree; + +internal struct TreeEnumerator : IEnumerator { - internal struct TreeEnumerator : IEnumerator + private readonly Stack _stack; + private readonly PathTokenizer _tokenizer; + + public TreeEnumerator(UrlMatchingNode root, PathTokenizer tokenizer) { - private readonly Stack _stack; - private readonly PathTokenizer _tokenizer; + _stack = new Stack(); + _tokenizer = tokenizer; + Current = null; - public TreeEnumerator(UrlMatchingNode root, PathTokenizer tokenizer) - { - _stack = new Stack(); - _tokenizer = tokenizer; - Current = null; + _stack.Push(root); + } - _stack.Push(root); - } + public UrlMatchingNode Current { get; private set; } - public UrlMatchingNode Current { get; private set; } + object IEnumerator.Current => Current; - object IEnumerator.Current => Current; + public void Dispose() + { + } - public void Dispose() + public bool MoveNext() + { + if (_stack == null) { + return false; } - public bool MoveNext() + while (_stack.Count > 0) { - if (_stack == null) + var next = _stack.Pop(); + + // In case of wild card segment, the request path segment length can be greater + // Example: + // Template: a/{*path} + // Request Url: a/b/c/d + if (next.IsCatchAll && next.Matches.Count > 0) { - return false; + Current = next; + return true; } - - while (_stack.Count > 0) + // Next template has the same length as the url we are trying to match + // The only possible matching segments are either our current matches or + // any catch-all segment after this segment in which the catch all is empty. + else if (next.Depth == _tokenizer.Count) { - var next = _stack.Pop(); - - // In case of wild card segment, the request path segment length can be greater - // Example: - // Template: a/{*path} - // Request Url: a/b/c/d - if (next.IsCatchAll && next.Matches.Count > 0) + if (next.Matches.Count > 0) { Current = next; return true; } - // Next template has the same length as the url we are trying to match - // The only possible matching segments are either our current matches or - // any catch-all segment after this segment in which the catch all is empty. - else if (next.Depth == _tokenizer.Count) + else { - if (next.Matches.Count > 0) - { - Current = next; - return true; - } - else - { - // We can stop looking as any other child node from this node will be - // either a literal, a constrained parameter or a parameter. - // (Catch alls and constrained catch alls will show up as candidate matches). - continue; - } + // We can stop looking as any other child node from this node will be + // either a literal, a constrained parameter or a parameter. + // (Catch alls and constrained catch alls will show up as candidate matches). + continue; } + } - if (next.CatchAlls != null) - { - _stack.Push(next.CatchAlls); - } + if (next.CatchAlls != null) + { + _stack.Push(next.CatchAlls); + } - if (next.ConstrainedCatchAlls != null) - { - _stack.Push(next.ConstrainedCatchAlls); - } + if (next.ConstrainedCatchAlls != null) + { + _stack.Push(next.ConstrainedCatchAlls); + } - if (next.Parameters != null) - { - _stack.Push(next.Parameters); - } + if (next.Parameters != null) + { + _stack.Push(next.Parameters); + } - if (next.ConstrainedParameters != null) - { - _stack.Push(next.ConstrainedParameters); - } + if (next.ConstrainedParameters != null) + { + _stack.Push(next.ConstrainedParameters); + } - if (next.Literals.Count > 0) + if (next.Literals.Count > 0) + { + Debug.Assert(next.Depth < _tokenizer.Count); + if (next.Literals.TryGetValue(_tokenizer[next.Depth].Value, out var node)) { - Debug.Assert(next.Depth < _tokenizer.Count); - if (next.Literals.TryGetValue(_tokenizer[next.Depth].Value, out var node)) - { - _stack.Push(node); - } + _stack.Push(node); } } - - return false; } - public void Reset() - { - _stack.Clear(); - Current = null; - } + return false; + } + + public void Reset() + { + _stack.Clear(); + Current = null; } } diff --git a/src/Http/Routing/src/Tree/TreeRouteBuilder.cs b/src/Http/Routing/src/Tree/TreeRouteBuilder.cs index 41612718d1..a588816a21 100644 --- a/src/Http/Routing/src/Tree/TreeRouteBuilder.cs +++ b/src/Http/Routing/src/Tree/TreeRouteBuilder.cs @@ -11,253 +11,252 @@ using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ObjectPool; -namespace Microsoft.AspNetCore.Routing.Tree +namespace Microsoft.AspNetCore.Routing.Tree; + +/// +/// Builder for instances. +/// +public class TreeRouteBuilder { + private readonly ILogger _logger; + private readonly ILogger _constraintLogger; + private readonly UrlEncoder _urlEncoder; + private readonly ObjectPool _objectPool; + private readonly IInlineConstraintResolver _constraintResolver; + /// - /// Builder for instances. + /// Initializes a new instance of . /// - public class TreeRouteBuilder + /// The . + /// The . + /// The . + internal TreeRouteBuilder( + ILoggerFactory loggerFactory, + ObjectPool objectPool, + IInlineConstraintResolver constraintResolver) { - private readonly ILogger _logger; - private readonly ILogger _constraintLogger; - private readonly UrlEncoder _urlEncoder; - private readonly ObjectPool _objectPool; - private readonly IInlineConstraintResolver _constraintResolver; - - /// - /// Initializes a new instance of . - /// - /// The . - /// The . - /// The . - internal TreeRouteBuilder( - ILoggerFactory loggerFactory, - ObjectPool objectPool, - IInlineConstraintResolver constraintResolver) + if (loggerFactory == null) { - if (loggerFactory == null) - { - throw new ArgumentNullException(nameof(loggerFactory)); - } + throw new ArgumentNullException(nameof(loggerFactory)); + } - if (objectPool == null) - { - throw new ArgumentNullException(nameof(objectPool)); - } + if (objectPool == null) + { + throw new ArgumentNullException(nameof(objectPool)); + } - if (constraintResolver == null) - { - throw new ArgumentNullException(nameof(constraintResolver)); - } + if (constraintResolver == null) + { + throw new ArgumentNullException(nameof(constraintResolver)); + } - _urlEncoder = UrlEncoder.Default; - _objectPool = objectPool; - _constraintResolver = constraintResolver; + _urlEncoder = UrlEncoder.Default; + _objectPool = objectPool; + _constraintResolver = constraintResolver; - _logger = loggerFactory.CreateLogger(); - _constraintLogger = loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName); - } + _logger = loggerFactory.CreateLogger(); + _constraintLogger = loggerFactory.CreateLogger(typeof(RouteConstraintMatcher).FullName); + } - /// - /// Adds a new inbound route to the . - /// - /// The for handling the route. - /// The of the route. - /// The route name. - /// The route order. - /// The . - public InboundRouteEntry MapInbound( - IRouter handler, - RouteTemplate routeTemplate, - string routeName, - int order) + /// + /// Adds a new inbound route to the . + /// + /// The for handling the route. + /// The of the route. + /// The route name. + /// The route order. + /// The . + public InboundRouteEntry MapInbound( + IRouter handler, + RouteTemplate routeTemplate, + string routeName, + int order) + { + if (handler == null) { - if (handler == null) - { - throw new ArgumentNullException(nameof(handler)); - } + throw new ArgumentNullException(nameof(handler)); + } - if (routeTemplate == null) - { - throw new ArgumentNullException(nameof(routeTemplate)); - } + if (routeTemplate == null) + { + throw new ArgumentNullException(nameof(routeTemplate)); + } - var entry = new InboundRouteEntry() - { - Handler = handler, - Order = order, - Precedence = RoutePrecedence.ComputeInbound(routeTemplate), - RouteName = routeName, - RouteTemplate = routeTemplate, - }; - - var constraintBuilder = new RouteConstraintBuilder(_constraintResolver, routeTemplate.TemplateText); - foreach (var parameter in routeTemplate.Parameters) + var entry = new InboundRouteEntry() + { + Handler = handler, + Order = order, + Precedence = RoutePrecedence.ComputeInbound(routeTemplate), + RouteName = routeName, + RouteTemplate = routeTemplate, + }; + + var constraintBuilder = new RouteConstraintBuilder(_constraintResolver, routeTemplate.TemplateText); + foreach (var parameter in routeTemplate.Parameters) + { + if (parameter.InlineConstraints != null) { - if (parameter.InlineConstraints != null) + if (parameter.IsOptional) { - if (parameter.IsOptional) - { - constraintBuilder.SetOptional(parameter.Name); - } - - foreach (var constraint in parameter.InlineConstraints) - { - constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint); - } + constraintBuilder.SetOptional(parameter.Name); } - } - - entry.Constraints = constraintBuilder.Build(); - entry.Defaults = new RouteValueDictionary(); - foreach (var parameter in entry.RouteTemplate.Parameters) - { - if (parameter.DefaultValue != null) + foreach (var constraint in parameter.InlineConstraints) { - entry.Defaults.Add(parameter.Name, parameter.DefaultValue); + constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint); } } - - InboundEntries.Add(entry); - return entry; } - /// - /// Adds a new outbound route to the . - /// - /// The for handling the link generation. - /// The of the route. - /// The containing the route values. - /// The route name. - /// The route order. - /// The . - public OutboundRouteEntry MapOutbound( - IRouter handler, - RouteTemplate routeTemplate, - RouteValueDictionary requiredLinkValues, - string routeName, - int order) + entry.Constraints = constraintBuilder.Build(); + + entry.Defaults = new RouteValueDictionary(); + foreach (var parameter in entry.RouteTemplate.Parameters) { - if (handler == null) + if (parameter.DefaultValue != null) { - throw new ArgumentNullException(nameof(handler)); + entry.Defaults.Add(parameter.Name, parameter.DefaultValue); } + } - if (routeTemplate == null) - { - throw new ArgumentNullException(nameof(routeTemplate)); - } + InboundEntries.Add(entry); + return entry; + } - if (requiredLinkValues == null) - { - throw new ArgumentNullException(nameof(requiredLinkValues)); - } + /// + /// Adds a new outbound route to the . + /// + /// The for handling the link generation. + /// The of the route. + /// The containing the route values. + /// The route name. + /// The route order. + /// The . + public OutboundRouteEntry MapOutbound( + IRouter handler, + RouteTemplate routeTemplate, + RouteValueDictionary requiredLinkValues, + string routeName, + int order) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } - var entry = new OutboundRouteEntry() - { - Handler = handler, - Order = order, - Precedence = RoutePrecedence.ComputeOutbound(routeTemplate), - RequiredLinkValues = requiredLinkValues, - RouteName = routeName, - RouteTemplate = routeTemplate, - }; - - var constraintBuilder = new RouteConstraintBuilder(_constraintResolver, routeTemplate.TemplateText); - foreach (var parameter in routeTemplate.Parameters) + if (routeTemplate == null) + { + throw new ArgumentNullException(nameof(routeTemplate)); + } + + if (requiredLinkValues == null) + { + throw new ArgumentNullException(nameof(requiredLinkValues)); + } + + var entry = new OutboundRouteEntry() + { + Handler = handler, + Order = order, + Precedence = RoutePrecedence.ComputeOutbound(routeTemplate), + RequiredLinkValues = requiredLinkValues, + RouteName = routeName, + RouteTemplate = routeTemplate, + }; + + var constraintBuilder = new RouteConstraintBuilder(_constraintResolver, routeTemplate.TemplateText); + foreach (var parameter in routeTemplate.Parameters) + { + if (parameter.InlineConstraints != null) { - if (parameter.InlineConstraints != null) + if (parameter.IsOptional) { - if (parameter.IsOptional) - { - constraintBuilder.SetOptional(parameter.Name); - } - - foreach (var constraint in parameter.InlineConstraints) - { - constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint); - } + constraintBuilder.SetOptional(parameter.Name); } - } - - entry.Constraints = constraintBuilder.Build(); - entry.Defaults = new RouteValueDictionary(); - foreach (var parameter in entry.RouteTemplate.Parameters) - { - if (parameter.DefaultValue != null) + foreach (var constraint in parameter.InlineConstraints) { - entry.Defaults.Add(parameter.Name, parameter.DefaultValue); + constraintBuilder.AddResolvedConstraint(parameter.Name, constraint.Constraint); } } - - OutboundEntries.Add(entry); - return entry; } - /// - /// Gets the list of . - /// - public IList InboundEntries { get; } = new List(); - - /// - /// Gets the list of . - /// - public IList OutboundEntries { get; } = new List(); - - /// - /// Builds a with the - /// and defined in this . - /// - /// The . - public TreeRouter Build() + entry.Constraints = constraintBuilder.Build(); + + entry.Defaults = new RouteValueDictionary(); + foreach (var parameter in entry.RouteTemplate.Parameters) { - return Build(version: 0); + if (parameter.DefaultValue != null) + { + entry.Defaults.Add(parameter.Name, parameter.DefaultValue); + } } - /// - /// Builds a with the - /// and defined in this . - /// - /// The version of the . - /// The . - public TreeRouter Build(int version) - { - // Tree route builder builds a tree for each of the different route orders defined by - // the user. When a route needs to be matched, the matching algorithm in tree router - // just iterates over the trees in ascending order when it tries to match the route. - var trees = new Dictionary(); + OutboundEntries.Add(entry); + return entry; + } - foreach (var entry in InboundEntries) - { - if (!trees.TryGetValue(entry.Order, out var tree)) - { - tree = new UrlMatchingTree(entry.Order); - trees.Add(entry.Order, tree); - } + /// + /// Gets the list of . + /// + public IList InboundEntries { get; } = new List(); - tree.AddEntry(entry); - } + /// + /// Gets the list of . + /// + public IList OutboundEntries { get; } = new List(); - return new TreeRouter( - trees.Values.OrderBy(tree => tree.Order).ToArray(), - OutboundEntries, - _urlEncoder, - _objectPool, - _logger, - _constraintLogger, - version); - } + /// + /// Builds a with the + /// and defined in this . + /// + /// The . + public TreeRouter Build() + { + return Build(version: 0); + } + + /// + /// Builds a with the + /// and defined in this . + /// + /// The version of the . + /// The . + public TreeRouter Build(int version) + { + // Tree route builder builds a tree for each of the different route orders defined by + // the user. When a route needs to be matched, the matching algorithm in tree router + // just iterates over the trees in ascending order when it tries to match the route. + var trees = new Dictionary(); - /// - /// Removes all and from this - /// . - /// - public void Clear() + foreach (var entry in InboundEntries) { - InboundEntries.Clear(); - OutboundEntries.Clear(); + if (!trees.TryGetValue(entry.Order, out var tree)) + { + tree = new UrlMatchingTree(entry.Order); + trees.Add(entry.Order, tree); + } + + tree.AddEntry(entry); } + + return new TreeRouter( + trees.Values.OrderBy(tree => tree.Order).ToArray(), + OutboundEntries, + _urlEncoder, + _objectPool, + _logger, + _constraintLogger, + version); + } + + /// + /// Removes all and from this + /// . + /// + public void Clear() + { + InboundEntries.Clear(); + OutboundEntries.Clear(); } } diff --git a/src/Http/Routing/src/Tree/TreeRouter.cs b/src/Http/Routing/src/Tree/TreeRouter.cs index 2e70a8bffc..5dc2a2c452 100644 --- a/src/Http/Routing/src/Tree/TreeRouter.cs +++ b/src/Http/Routing/src/Tree/TreeRouter.cs @@ -11,317 +11,316 @@ using Microsoft.AspNetCore.Routing.Template; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ObjectPool; -namespace Microsoft.AspNetCore.Routing.Tree +namespace Microsoft.AspNetCore.Routing.Tree; + +/// +/// An implementation for attribute routing. +/// +public partial class TreeRouter : IRouter { /// - /// An implementation for attribute routing. + /// Key used by routing and action selection to match an attribute + /// route entry to a group of action descriptors. + /// + public static readonly string RouteGroupKey = "!__route_group"; + + private readonly LinkGenerationDecisionTree _linkGenerationTree; + private readonly UrlMatchingTree[] _trees; + private readonly IDictionary _namedEntries; + + private readonly ILogger _logger; + private readonly ILogger _constraintLogger; + + /// + /// Creates a new instance of . /// - public partial class TreeRouter : IRouter + /// The list of that contains the route entries. + /// The set of . + /// The . + /// The . + /// The instance. + /// The instance used + /// in . + /// The version of this route. + internal TreeRouter( + UrlMatchingTree[] trees, + IEnumerable linkGenerationEntries, + UrlEncoder urlEncoder, + ObjectPool objectPool, + ILogger routeLogger, + ILogger constraintLogger, + int version) { - /// - /// Key used by routing and action selection to match an attribute - /// route entry to a group of action descriptors. - /// - public static readonly string RouteGroupKey = "!__route_group"; - - private readonly LinkGenerationDecisionTree _linkGenerationTree; - private readonly UrlMatchingTree[] _trees; - private readonly IDictionary _namedEntries; - - private readonly ILogger _logger; - private readonly ILogger _constraintLogger; - - /// - /// Creates a new instance of . - /// - /// The list of that contains the route entries. - /// The set of . - /// The . - /// The . - /// The instance. - /// The instance used - /// in . - /// The version of this route. - internal TreeRouter( - UrlMatchingTree[] trees, - IEnumerable linkGenerationEntries, - UrlEncoder urlEncoder, - ObjectPool objectPool, - ILogger routeLogger, - ILogger constraintLogger, - int version) + if (trees == null) { - if (trees == null) - { - throw new ArgumentNullException(nameof(trees)); - } + throw new ArgumentNullException(nameof(trees)); + } - if (linkGenerationEntries == null) - { - throw new ArgumentNullException(nameof(linkGenerationEntries)); - } + if (linkGenerationEntries == null) + { + throw new ArgumentNullException(nameof(linkGenerationEntries)); + } - if (urlEncoder == null) - { - throw new ArgumentNullException(nameof(urlEncoder)); - } + if (urlEncoder == null) + { + throw new ArgumentNullException(nameof(urlEncoder)); + } - if (objectPool == null) - { - throw new ArgumentNullException(nameof(objectPool)); - } + if (objectPool == null) + { + throw new ArgumentNullException(nameof(objectPool)); + } - if (routeLogger == null) - { - throw new ArgumentNullException(nameof(routeLogger)); - } + if (routeLogger == null) + { + throw new ArgumentNullException(nameof(routeLogger)); + } - if (constraintLogger == null) - { - throw new ArgumentNullException(nameof(constraintLogger)); - } + if (constraintLogger == null) + { + throw new ArgumentNullException(nameof(constraintLogger)); + } - _trees = trees; - _logger = routeLogger; - _constraintLogger = constraintLogger; + _trees = trees; + _logger = routeLogger; + _constraintLogger = constraintLogger; - _namedEntries = new Dictionary(StringComparer.OrdinalIgnoreCase); + _namedEntries = new Dictionary(StringComparer.OrdinalIgnoreCase); - var outboundMatches = new List(); + var outboundMatches = new List(); - foreach (var entry in linkGenerationEntries) - { + foreach (var entry in linkGenerationEntries) + { - var binder = new TemplateBinder(urlEncoder, objectPool, entry.RouteTemplate, entry.Defaults); - var outboundMatch = new OutboundMatch() { Entry = entry, TemplateBinder = binder }; - outboundMatches.Add(outboundMatch); + var binder = new TemplateBinder(urlEncoder, objectPool, entry.RouteTemplate, entry.Defaults); + var outboundMatch = new OutboundMatch() { Entry = entry, TemplateBinder = binder }; + outboundMatches.Add(outboundMatch); - // Skip unnamed entries - if (entry.RouteName == null) - { - continue; - } + // Skip unnamed entries + if (entry.RouteName == null) + { + continue; + } - // We only need to keep one OutboundMatch per route template - // so in case two entries have the same name and the same template we only keep - // the first entry. - if (_namedEntries.TryGetValue(entry.RouteName, out var namedMatch) && - !string.Equals( - namedMatch.Entry.RouteTemplate.TemplateText, - entry.RouteTemplate.TemplateText, - StringComparison.OrdinalIgnoreCase)) - { - throw new ArgumentException( - Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.RouteName), - nameof(linkGenerationEntries)); - } - else if (namedMatch == null) - { - _namedEntries.Add(entry.RouteName, outboundMatch); - } + // We only need to keep one OutboundMatch per route template + // so in case two entries have the same name and the same template we only keep + // the first entry. + if (_namedEntries.TryGetValue(entry.RouteName, out var namedMatch) && + !string.Equals( + namedMatch.Entry.RouteTemplate.TemplateText, + entry.RouteTemplate.TemplateText, + StringComparison.OrdinalIgnoreCase)) + { + throw new ArgumentException( + Resources.FormatAttributeRoute_DifferentLinkGenerationEntries_SameName(entry.RouteName), + nameof(linkGenerationEntries)); } + else if (namedMatch == null) + { + _namedEntries.Add(entry.RouteName, outboundMatch); + } + } - // The decision tree will take care of ordering for these entries. - _linkGenerationTree = new LinkGenerationDecisionTree(outboundMatches.ToArray()); + // The decision tree will take care of ordering for these entries. + _linkGenerationTree = new LinkGenerationDecisionTree(outboundMatches.ToArray()); - Version = version; - } + Version = version; + } - /// - /// Gets the version of this route. - /// - public int Version { get; } + /// + /// Gets the version of this route. + /// + public int Version { get; } - internal IEnumerable MatchingTrees => _trees; + internal IEnumerable MatchingTrees => _trees; - /// - public VirtualPathData GetVirtualPath(VirtualPathContext context) + /// + public VirtualPathData GetVirtualPath(VirtualPathContext context) + { + if (context == null) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } + throw new ArgumentNullException(nameof(context)); + } - // If it's a named route we will try to generate a link directly and - // if we can't, we will not try to generate it using an unnamed route. - if (context.RouteName != null) - { - return GetVirtualPathForNamedRoute(context); - } + // If it's a named route we will try to generate a link directly and + // if we can't, we will not try to generate it using an unnamed route. + if (context.RouteName != null) + { + return GetVirtualPathForNamedRoute(context); + } - // The decision tree will give us back all entries that match the provided route data in the correct - // order. We just need to iterate them and use the first one that can generate a link. - var matches = _linkGenerationTree.GetMatches(context.Values, context.AmbientValues); + // The decision tree will give us back all entries that match the provided route data in the correct + // order. We just need to iterate them and use the first one that can generate a link. + var matches = _linkGenerationTree.GetMatches(context.Values, context.AmbientValues); - if (matches == null) - { - return null; - } + if (matches == null) + { + return null; + } - for (var i = 0; i < matches.Count; i++) + for (var i = 0; i < matches.Count; i++) + { + var path = GenerateVirtualPath(context, matches[i].Match.Entry, matches[i].Match.TemplateBinder); + if (path != null) { - var path = GenerateVirtualPath(context, matches[i].Match.Entry, matches[i].Match.TemplateBinder); - if (path != null) - { - return path; - } + return path; } - - return null; } - /// - public async Task RouteAsync(RouteContext context) + return null; + } + + /// + public async Task RouteAsync(RouteContext context) + { + foreach (var tree in _trees) { - foreach (var tree in _trees) - { - var tokenizer = new PathTokenizer(context.HttpContext.Request.Path); - var root = tree.Root; + var tokenizer = new PathTokenizer(context.HttpContext.Request.Path); + var root = tree.Root; - var treeEnumerator = new TreeEnumerator(root, tokenizer); + var treeEnumerator = new TreeEnumerator(root, tokenizer); - // Create a snapshot before processing the route. We'll restore this snapshot before running each - // to restore the state. This is likely an "empty" snapshot, which doesn't allocate. - var snapshot = context.RouteData.PushState(router: null, values: null, dataTokens: null); + // Create a snapshot before processing the route. We'll restore this snapshot before running each + // to restore the state. This is likely an "empty" snapshot, which doesn't allocate. + var snapshot = context.RouteData.PushState(router: null, values: null, dataTokens: null); - while (treeEnumerator.MoveNext()) + while (treeEnumerator.MoveNext()) + { + var node = treeEnumerator.Current; + foreach (var item in node.Matches) { - var node = treeEnumerator.Current; - foreach (var item in node.Matches) + var entry = item.Entry; + var matcher = item.TemplateMatcher; + + try { - var entry = item.Entry; - var matcher = item.TemplateMatcher; + if (!matcher.TryMatch(context.HttpContext.Request.Path, context.RouteData.Values)) + { + continue; + } - try + if (!RouteConstraintMatcher.Match( + entry.Constraints, + context.RouteData.Values, + context.HttpContext, + this, + RouteDirection.IncomingRequest, + _constraintLogger)) { - if (!matcher.TryMatch(context.HttpContext.Request.Path, context.RouteData.Values)) - { - continue; - } - - if (!RouteConstraintMatcher.Match( - entry.Constraints, - context.RouteData.Values, - context.HttpContext, - this, - RouteDirection.IncomingRequest, - _constraintLogger)) - { - continue; - } - - Log.RequestMatchedRoute(_logger, entry.RouteName, entry.RouteTemplate.TemplateText); - context.RouteData.Routers.Add(entry.Handler); - - await entry.Handler.RouteAsync(context); - if (context.Handler != null) - { - return; - } + continue; } - finally + + Log.RequestMatchedRoute(_logger, entry.RouteName, entry.RouteTemplate.TemplateText); + context.RouteData.Routers.Add(entry.Handler); + + await entry.Handler.RouteAsync(context); + if (context.Handler != null) + { + return; + } + } + finally + { + if (context.Handler == null) { - if (context.Handler == null) - { - // Restore the original values to prevent polluting the route data. - snapshot.Restore(); - } + // Restore the original values to prevent polluting the route data. + snapshot.Restore(); } } } } } + } - private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context) + private VirtualPathData GetVirtualPathForNamedRoute(VirtualPathContext context) + { + if (_namedEntries.TryGetValue(context.RouteName, out var match)) { - if (_namedEntries.TryGetValue(context.RouteName, out var match)) + var path = GenerateVirtualPath(context, match.Entry, match.TemplateBinder); + if (path != null) { - var path = GenerateVirtualPath(context, match.Entry, match.TemplateBinder); - if (path != null) - { - return path; - } + return path; } - return null; } + return null; + } - private VirtualPathData GenerateVirtualPath( - VirtualPathContext context, - OutboundRouteEntry entry, - TemplateBinder binder) + private VirtualPathData GenerateVirtualPath( + VirtualPathContext context, + OutboundRouteEntry entry, + TemplateBinder binder) + { + // In attribute the context includes the values that are used to select this entry - typically + // these will be the standard 'action', 'controller' and maybe 'area' tokens. However, we don't + // want to pass these to the link generation code, or else they will end up as query parameters. + // + // So, we need to exclude from here any values that are 'required link values', but aren't + // parameters in the template. + // + // Ex: + // template: api/Products/{action} + // required values: { id = "5", action = "Buy", Controller = "CoolProducts" } + // + // result: { id = "5", action = "Buy" } + var inputValues = new RouteValueDictionary(); + foreach (var kvp in context.Values) { - // In attribute the context includes the values that are used to select this entry - typically - // these will be the standard 'action', 'controller' and maybe 'area' tokens. However, we don't - // want to pass these to the link generation code, or else they will end up as query parameters. - // - // So, we need to exclude from here any values that are 'required link values', but aren't - // parameters in the template. - // - // Ex: - // template: api/Products/{action} - // required values: { id = "5", action = "Buy", Controller = "CoolProducts" } - // - // result: { id = "5", action = "Buy" } - var inputValues = new RouteValueDictionary(); - foreach (var kvp in context.Values) + if (entry.RequiredLinkValues.ContainsKey(kvp.Key)) { - if (entry.RequiredLinkValues.ContainsKey(kvp.Key)) - { - var parameter = entry.RouteTemplate.GetParameter(kvp.Key); + var parameter = entry.RouteTemplate.GetParameter(kvp.Key); - if (parameter == null) - { - continue; - } + if (parameter == null) + { + continue; } - - inputValues.Add(kvp.Key, kvp.Value); - } - - var bindingResult = binder.GetValues(context.AmbientValues, inputValues); - if (bindingResult == null) - { - // A required parameter in the template didn't get a value. - return null; } - var matched = RouteConstraintMatcher.Match( - entry.Constraints, - bindingResult.CombinedValues, - context.HttpContext, - this, - RouteDirection.UrlGeneration, - _constraintLogger); + inputValues.Add(kvp.Key, kvp.Value); + } - if (!matched) - { - // A constraint rejected this link. - return null; - } + var bindingResult = binder.GetValues(context.AmbientValues, inputValues); + if (bindingResult == null) + { + // A required parameter in the template didn't get a value. + return null; + } - var pathData = entry.Handler.GetVirtualPath(context); - if (pathData != null) - { - // If path is non-null then the target router short-circuited, we don't expect this - // in typical MVC scenarios. - return pathData; - } + var matched = RouteConstraintMatcher.Match( + entry.Constraints, + bindingResult.CombinedValues, + context.HttpContext, + this, + RouteDirection.UrlGeneration, + _constraintLogger); - var path = binder.BindValues(bindingResult.AcceptedValues); - if (path == null) - { - return null; - } + if (!matched) + { + // A constraint rejected this link. + return null; + } - return new VirtualPathData(this, path); + var pathData = entry.Handler.GetVirtualPath(context); + if (pathData != null) + { + // If path is non-null then the target router short-circuited, we don't expect this + // in typical MVC scenarios. + return pathData; } - private static partial class Log + var path = binder.BindValues(bindingResult.AcceptedValues); + if (path == null) { - [LoggerMessage(1, LogLevel.Debug, - "Request successfully matched the route with name '{RouteName}' and template '{RouteTemplate}'", - EventName = "RequestMatchedRoute")] - public static partial void RequestMatchedRoute(ILogger logger, string routeName, string routeTemplate); + return null; } + + return new VirtualPathData(this, path); + } + + private static partial class Log + { + [LoggerMessage(1, LogLevel.Debug, + "Request successfully matched the route with name '{RouteName}' and template '{RouteTemplate}'", + EventName = "RequestMatchedRoute")] + public static partial void RequestMatchedRoute(ILogger logger, string routeName, string routeTemplate); } } diff --git a/src/Http/Routing/src/Tree/UrlMatchingNode.cs b/src/Http/Routing/src/Tree/UrlMatchingNode.cs index df7ef828c0..d9bee82da9 100644 --- a/src/Http/Routing/src/Tree/UrlMatchingNode.cs +++ b/src/Http/Routing/src/Tree/UrlMatchingNode.cs @@ -8,76 +8,75 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -namespace Microsoft.AspNetCore.Routing.Tree +namespace Microsoft.AspNetCore.Routing.Tree; + +/// +/// A node in a . +/// +[DebuggerDisplay("{DebuggerToString(),nq}")] +public class UrlMatchingNode { /// - /// A node in a . + /// Initializes a new instance of . /// - [DebuggerDisplay("{DebuggerToString(),nq}")] - public class UrlMatchingNode + /// The length of the path to this node in the . + public UrlMatchingNode(int length) { - /// - /// Initializes a new instance of . - /// - /// The length of the path to this node in the . - public UrlMatchingNode(int length) - { - Depth = length; + Depth = length; - Matches = new List(); - Literals = new Dictionary(StringComparer.OrdinalIgnoreCase); - } + Matches = new List(); + Literals = new Dictionary(StringComparer.OrdinalIgnoreCase); + } - /// - /// Gets the length of the path to this node in the . - /// - public int Depth { get; } + /// + /// Gets the length of the path to this node in the . + /// + public int Depth { get; } - /// - /// Gets or sets a value indicating whether this node represents a catch all segment. - /// - public bool IsCatchAll { get; set; } + /// + /// Gets or sets a value indicating whether this node represents a catch all segment. + /// + public bool IsCatchAll { get; set; } - /// - /// Gets the list of matching route entries associated with this node. - /// - /// - /// These entries are sorted by precedence then template. - /// - public List Matches { get; } + /// + /// Gets the list of matching route entries associated with this node. + /// + /// + /// These entries are sorted by precedence then template. + /// + public List Matches { get; } - /// - /// Gets the literal segments following this segment. - /// - public Dictionary Literals { get; } + /// + /// Gets the literal segments following this segment. + /// + public Dictionary Literals { get; } - /// - /// Gets or sets the representing - /// parameter segments with constraints following this segment in the . - /// - public UrlMatchingNode ConstrainedParameters { get; set; } + /// + /// Gets or sets the representing + /// parameter segments with constraints following this segment in the . + /// + public UrlMatchingNode ConstrainedParameters { get; set; } - /// - /// Gets or sets the representing - /// parameter segments following this segment in the . - /// - public UrlMatchingNode Parameters { get; set; } + /// + /// Gets or sets the representing + /// parameter segments following this segment in the . + /// + public UrlMatchingNode Parameters { get; set; } - /// - /// Gets or sets the representing - /// catch all parameter segments with constraints following this segment in the . - /// - public UrlMatchingNode ConstrainedCatchAlls { get; set; } + /// + /// Gets or sets the representing + /// catch all parameter segments with constraints following this segment in the . + /// + public UrlMatchingNode ConstrainedCatchAlls { get; set; } - /// - /// Gets or sets the representing - /// catch all parameter segments following this segment in the . - /// - public UrlMatchingNode CatchAlls { get; set; } + /// + /// Gets or sets the representing + /// catch all parameter segments following this segment in the . + /// + public UrlMatchingNode CatchAlls { get; set; } - private string DebuggerToString() - { - return $"Length: {Depth}, Matches: {string.Join(" | ", Matches?.Select(m => $"({m.TemplateMatcher.Template.TemplateText})"))}"; - } + private string DebuggerToString() + { + return $"Length: {Depth}, Matches: {string.Join(" | ", Matches?.Select(m => $"({m.TemplateMatcher.Template.TemplateText})"))}"; } } diff --git a/src/Http/Routing/src/Tree/UrlMatchingTree.cs b/src/Http/Routing/src/Tree/UrlMatchingTree.cs index 3de6e6074e..1c91b637b9 100644 --- a/src/Http/Routing/src/Tree/UrlMatchingTree.cs +++ b/src/Http/Routing/src/Tree/UrlMatchingTree.cs @@ -7,191 +7,190 @@ using System.Diagnostics; using System.Linq; using Microsoft.AspNetCore.Routing.Template; -namespace Microsoft.AspNetCore.Routing.Tree +namespace Microsoft.AspNetCore.Routing.Tree; + +/// +/// A tree part of a . +/// +public class UrlMatchingTree { /// - /// A tree part of a . + /// Initializes a new instance of . /// - public class UrlMatchingTree + /// The order associated with routes in this . + public UrlMatchingTree(int order) { - /// - /// Initializes a new instance of . - /// - /// The order associated with routes in this . - public UrlMatchingTree(int order) - { - Order = order; - } + Order = order; + } - /// - /// Gets the order of the routes associated with this . - /// - public int Order { get; } + /// + /// Gets the order of the routes associated with this . + /// + public int Order { get; } - /// - /// Gets the root of the . - /// - public UrlMatchingNode Root { get; } = new UrlMatchingNode(length: 0); + /// + /// Gets the root of the . + /// + public UrlMatchingNode Root { get; } = new UrlMatchingNode(length: 0); - internal void AddEntry(InboundRouteEntry entry) + internal void AddEntry(InboundRouteEntry entry) + { + // The url matching tree represents all the routes asociated with a given + // order. Each node in the tree represents all the different categories + // a segment can have for which there is a defined inbound route entry. + // Each node contains a set of Matches that indicate all the routes for which + // a URL is a potential match. This list contains the routes with the same + // number of segments and the routes with the same number of segments plus an + // additional catch all parameter (as it can be empty). + // For example, for a set of routes like: + // 'Customer/Index/{id}' + // '{Controller}/{Action}/{*parameters}' + // + // The route tree will look like: + // Root -> + // Literals: Customer -> + // Literals: Index -> + // Parameters: {id} + // Matches: 'Customer/Index/{id}' + // Parameters: {Controller} -> + // Parameters: {Action} -> + // Matches: '{Controller}/{Action}/{*parameters}' + // CatchAlls: {*parameters} + // Matches: '{Controller}/{Action}/{*parameters}' + // + // When the tree router tries to match a route, it iterates the list of url matching trees + // in ascending order. For each tree it traverses each node starting from the root in the + // following order: Literals, constrained parameters, parameters, constrained catch all routes, catch alls. + // When it gets to a node of the same length as the route its trying to match, it simply looks at the list of + // candidates (which is in precence order) and tries to match the url against it. + // + + var current = Root; + var matcher = new TemplateMatcher(entry.RouteTemplate, entry.Defaults); + + for (var i = 0; i < entry.RouteTemplate.Segments.Count; i++) { - // The url matching tree represents all the routes asociated with a given - // order. Each node in the tree represents all the different categories - // a segment can have for which there is a defined inbound route entry. - // Each node contains a set of Matches that indicate all the routes for which - // a URL is a potential match. This list contains the routes with the same - // number of segments and the routes with the same number of segments plus an - // additional catch all parameter (as it can be empty). - // For example, for a set of routes like: - // 'Customer/Index/{id}' - // '{Controller}/{Action}/{*parameters}' - // - // The route tree will look like: - // Root -> - // Literals: Customer -> - // Literals: Index -> - // Parameters: {id} - // Matches: 'Customer/Index/{id}' - // Parameters: {Controller} -> - // Parameters: {Action} -> - // Matches: '{Controller}/{Action}/{*parameters}' - // CatchAlls: {*parameters} - // Matches: '{Controller}/{Action}/{*parameters}' - // - // When the tree router tries to match a route, it iterates the list of url matching trees - // in ascending order. For each tree it traverses each node starting from the root in the - // following order: Literals, constrained parameters, parameters, constrained catch all routes, catch alls. - // When it gets to a node of the same length as the route its trying to match, it simply looks at the list of - // candidates (which is in precence order) and tries to match the url against it. - // - - var current = Root; - var matcher = new TemplateMatcher(entry.RouteTemplate, entry.Defaults); - - for (var i = 0; i < entry.RouteTemplate.Segments.Count; i++) + var segment = entry.RouteTemplate.Segments[i]; + if (!segment.IsSimple) { - var segment = entry.RouteTemplate.Segments[i]; - if (!segment.IsSimple) + // Treat complex segments as a constrained parameter + if (current.ConstrainedParameters == null) { - // Treat complex segments as a constrained parameter - if (current.ConstrainedParameters == null) - { - current.ConstrainedParameters = new UrlMatchingNode(length: i + 1); - } - - current = current.ConstrainedParameters; - continue; + current.ConstrainedParameters = new UrlMatchingNode(length: i + 1); } - Debug.Assert(segment.Parts.Count == 1); - var part = segment.Parts[0]; - if (part.IsLiteral) - { - if (!current.Literals.TryGetValue(part.Text, out var next)) - { - next = new UrlMatchingNode(length: i + 1); - current.Literals.Add(part.Text, next); - } - - current = next; - continue; - } + current = current.ConstrainedParameters; + continue; + } - // We accept templates that have intermediate optional values, but we ignore - // those values for route matching. For that reason, we need to add the entry - // to the list of matches, only if the remaining segments are optional. For example: - // /{controller}/{action=Index}/{id} will be equivalent to /{controller}/{action}/{id} - // for the purposes of route matching. - if (part.IsParameter && - RemainingSegmentsAreOptional(entry.RouteTemplate.Segments, i)) + Debug.Assert(segment.Parts.Count == 1); + var part = segment.Parts[0]; + if (part.IsLiteral) + { + if (!current.Literals.TryGetValue(part.Text, out var next)) { - current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher }); + next = new UrlMatchingNode(length: i + 1); + current.Literals.Add(part.Text, next); } - if (part.IsParameter && part.InlineConstraints.Any() && !part.IsCatchAll) - { - if (current.ConstrainedParameters == null) - { - current.ConstrainedParameters = new UrlMatchingNode(length: i + 1); - } + current = next; + continue; + } - current = current.ConstrainedParameters; - continue; - } + // We accept templates that have intermediate optional values, but we ignore + // those values for route matching. For that reason, we need to add the entry + // to the list of matches, only if the remaining segments are optional. For example: + // /{controller}/{action=Index}/{id} will be equivalent to /{controller}/{action}/{id} + // for the purposes of route matching. + if (part.IsParameter && + RemainingSegmentsAreOptional(entry.RouteTemplate.Segments, i)) + { + current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher }); + } - if (part.IsParameter && !part.IsCatchAll) + if (part.IsParameter && part.InlineConstraints.Any() && !part.IsCatchAll) + { + if (current.ConstrainedParameters == null) { - if (current.Parameters == null) - { - current.Parameters = new UrlMatchingNode(length: i + 1); - } - - current = current.Parameters; - continue; + current.ConstrainedParameters = new UrlMatchingNode(length: i + 1); } - if (part.IsParameter && part.InlineConstraints.Any() && part.IsCatchAll) - { - if (current.ConstrainedCatchAlls == null) - { - current.ConstrainedCatchAlls = new UrlMatchingNode(length: i + 1) { IsCatchAll = true }; - } + current = current.ConstrainedParameters; + continue; + } - current = current.ConstrainedCatchAlls; - continue; + if (part.IsParameter && !part.IsCatchAll) + { + if (current.Parameters == null) + { + current.Parameters = new UrlMatchingNode(length: i + 1); } - if (part.IsParameter && part.IsCatchAll) - { - if (current.CatchAlls == null) - { - current.CatchAlls = new UrlMatchingNode(length: i + 1) { IsCatchAll = true }; - } + current = current.Parameters; + continue; + } - current = current.CatchAlls; - continue; + if (part.IsParameter && part.InlineConstraints.Any() && part.IsCatchAll) + { + if (current.ConstrainedCatchAlls == null) + { + current.ConstrainedCatchAlls = new UrlMatchingNode(length: i + 1) { IsCatchAll = true }; } - Debug.Fail("We shouldn't get here."); + current = current.ConstrainedCatchAlls; + continue; } - current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher }); - current.Matches.Sort((x, y) => + if (part.IsParameter && part.IsCatchAll) { - var result = x.Entry.Precedence.CompareTo(y.Entry.Precedence); - return result == 0 ? string.Compare(x.Entry.RouteTemplate.TemplateText, y.Entry.RouteTemplate.TemplateText, StringComparison.Ordinal) : result; - }); + if (current.CatchAlls == null) + { + current.CatchAlls = new UrlMatchingNode(length: i + 1) { IsCatchAll = true }; + } + + current = current.CatchAlls; + continue; + } + + Debug.Fail("We shouldn't get here."); } - private static bool RemainingSegmentsAreOptional(IList segments, int currentParameterIndex) + current.Matches.Add(new InboundMatch() { Entry = entry, TemplateMatcher = matcher }); + current.Matches.Sort((x, y) => + { + var result = x.Entry.Precedence.CompareTo(y.Entry.Precedence); + return result == 0 ? string.Compare(x.Entry.RouteTemplate.TemplateText, y.Entry.RouteTemplate.TemplateText, StringComparison.Ordinal) : result; + }); + } + + private static bool RemainingSegmentsAreOptional(IList segments, int currentParameterIndex) + { + for (var i = currentParameterIndex; i < segments.Count; i++) { - for (var i = currentParameterIndex; i < segments.Count; i++) + if (!segments[i].IsSimple) { - if (!segments[i].IsSimple) - { - // /{complex}-{segment} - return false; - } + // /{complex}-{segment} + return false; + } - var part = segments[i].Parts[0]; - if (!part.IsParameter) - { - // /literal - return false; - } + var part = segments[i].Parts[0]; + if (!part.IsParameter) + { + // /literal + return false; + } - var isOptionlCatchAllOrHasDefaultValue = part.IsOptional || - part.IsCatchAll || - part.DefaultValue != null; + var isOptionlCatchAllOrHasDefaultValue = part.IsOptional || + part.IsCatchAll || + part.DefaultValue != null; - if (!isOptionlCatchAllOrHasDefaultValue) - { - // /{parameter} - return false; - } + if (!isOptionlCatchAllOrHasDefaultValue) + { + // /{parameter} + return false; } - - return true; } + + return true; } } diff --git a/src/Http/Routing/src/UriBuilderContextPooledObjectPolicy.cs b/src/Http/Routing/src/UriBuilderContextPooledObjectPolicy.cs index 4a8aeabe21..b9df322028 100644 --- a/src/Http/Routing/src/UriBuilderContextPooledObjectPolicy.cs +++ b/src/Http/Routing/src/UriBuilderContextPooledObjectPolicy.cs @@ -4,19 +4,18 @@ using System.Text.Encodings.Web; using Microsoft.Extensions.ObjectPool; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +internal class UriBuilderContextPooledObjectPolicy : IPooledObjectPolicy { - internal class UriBuilderContextPooledObjectPolicy : IPooledObjectPolicy + public UriBuildingContext Create() { - public UriBuildingContext Create() - { - return new UriBuildingContext(UrlEncoder.Default); - } + return new UriBuildingContext(UrlEncoder.Default); + } - public bool Return(UriBuildingContext obj) - { - obj.Clear(); - return true; - } + public bool Return(UriBuildingContext obj) + { + obj.Clear(); + return true; } } diff --git a/src/Http/Routing/src/UriBuildingContext.cs b/src/Http/Routing/src/UriBuildingContext.cs index 4ac0699d21..a87a0c40c3 100644 --- a/src/Http/Routing/src/UriBuildingContext.cs +++ b/src/Http/Routing/src/UriBuildingContext.cs @@ -9,331 +9,330 @@ using System.Text; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +[DebuggerDisplay("{DebuggerToString(),nq}")] +internal class UriBuildingContext { - [DebuggerDisplay("{DebuggerToString(),nq}")] - internal class UriBuildingContext - { - // Holds the 'accepted' parts of the path. - private readonly StringBuilder _path; - private readonly StringBuilder _query; + // Holds the 'accepted' parts of the path. + private readonly StringBuilder _path; + private readonly StringBuilder _query; - // Holds the 'optional' parts of the path. We need a secondary buffer to handle cases where an optional - // segment is in the middle of the uri. We don't know if we need to write it out - if it's - // followed by other optional segments than we will just throw it away. - private readonly List _buffer; - private readonly UrlEncoder _urlEncoder; + // Holds the 'optional' parts of the path. We need a secondary buffer to handle cases where an optional + // segment is in the middle of the uri. We don't know if we need to write it out - if it's + // followed by other optional segments than we will just throw it away. + private readonly List _buffer; + private readonly UrlEncoder _urlEncoder; - private bool _hasEmptySegment; - private int _lastValueOffset; + private bool _hasEmptySegment; + private int _lastValueOffset; - public UriBuildingContext(UrlEncoder urlEncoder) - { - _urlEncoder = urlEncoder; - _path = new StringBuilder(); - _query = new StringBuilder(); - _buffer = new List(); - PathWriter = new StringWriter(_path); - QueryWriter = new StringWriter(_query); - _lastValueOffset = -1; - - BufferState = SegmentState.Beginning; - UriState = SegmentState.Beginning; - } + public UriBuildingContext(UrlEncoder urlEncoder) + { + _urlEncoder = urlEncoder; + _path = new StringBuilder(); + _query = new StringBuilder(); + _buffer = new List(); + PathWriter = new StringWriter(_path); + QueryWriter = new StringWriter(_query); + _lastValueOffset = -1; + + BufferState = SegmentState.Beginning; + UriState = SegmentState.Beginning; + } - public bool LowercaseUrls { get; set; } + public bool LowercaseUrls { get; set; } - public bool LowercaseQueryStrings { get; set; } + public bool LowercaseQueryStrings { get; set; } - public bool AppendTrailingSlash { get; set; } + public bool AppendTrailingSlash { get; set; } - public SegmentState BufferState { get; private set; } + public SegmentState BufferState { get; private set; } - public SegmentState UriState { get; private set; } + public SegmentState UriState { get; private set; } - public TextWriter PathWriter { get; } + public TextWriter PathWriter { get; } - public TextWriter QueryWriter { get; } + public TextWriter QueryWriter { get; } - public bool Accept(string? value) - { - return Accept(value, encodeSlashes: true); - } + public bool Accept(string? value) + { + return Accept(value, encodeSlashes: true); + } - public bool Accept(string? value, bool encodeSlashes) + public bool Accept(string? value, bool encodeSlashes) + { + if (string.IsNullOrEmpty(value)) { - if (string.IsNullOrEmpty(value)) + if (UriState == SegmentState.Inside || BufferState == SegmentState.Inside) { - if (UriState == SegmentState.Inside || BufferState == SegmentState.Inside) - { - // We can't write an 'empty' part inside a segment - return false; - } - else - { - _hasEmptySegment = true; - return true; - } + // We can't write an 'empty' part inside a segment + return false; } - else if (_hasEmptySegment) + else { - // We're trying to write text after an empty segment - this is not allowed. - return false; + _hasEmptySegment = true; + return true; } + } + else if (_hasEmptySegment) + { + // We're trying to write text after an empty segment - this is not allowed. + return false; + } - // NOTE: this needs to be above all 'EncodeValue' and _path.Append calls + // NOTE: this needs to be above all 'EncodeValue' and _path.Append calls + if (LowercaseUrls) + { + value = value.ToLowerInvariant(); + } + + var buffer = _buffer; + for (var i = 0; i < buffer.Count; i++) + { + var bufferValue = buffer[i].Value; if (LowercaseUrls) { - value = value.ToLowerInvariant(); + bufferValue = bufferValue.ToLowerInvariant(); } - var buffer = _buffer; - for (var i = 0; i < buffer.Count; i++) + if (buffer[i].RequiresEncoding) { - var bufferValue = buffer[i].Value; - if (LowercaseUrls) - { - bufferValue = bufferValue.ToLowerInvariant(); - } - - if (buffer[i].RequiresEncoding) - { - EncodeValue(bufferValue); - } - else - { - _path.Append(bufferValue); - } + EncodeValue(bufferValue); } - buffer.Clear(); - - if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning) + else { - if (_path.Length != 0) - { - _path.Append('/'); - } + _path.Append(bufferValue); } + } + buffer.Clear(); - BufferState = SegmentState.Inside; - UriState = SegmentState.Inside; - - _lastValueOffset = _path.Length; - - // Allow the first segment to have a leading slash. - // This prevents the leading slash from PathString segments from being encoded. - if (_path.Length == 0 && value.Length > 0 && value[0] == '/') + if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning) + { + if (_path.Length != 0) { _path.Append('/'); - EncodeValue(value, 1, value.Length - 1, encodeSlashes); - } - else - { - EncodeValue(value, encodeSlashes); } - - return true; } - public void Remove(string literal) + BufferState = SegmentState.Inside; + UriState = SegmentState.Inside; + + _lastValueOffset = _path.Length; + + // Allow the first segment to have a leading slash. + // This prevents the leading slash from PathString segments from being encoded. + if (_path.Length == 0 && value.Length > 0 && value[0] == '/') + { + _path.Append('/'); + EncodeValue(value, 1, value.Length - 1, encodeSlashes); + } + else { - Debug.Assert(_lastValueOffset != -1, "Cannot invoke Remove more than once."); - _path.Length = _lastValueOffset; - _lastValueOffset = -1; + EncodeValue(value, encodeSlashes); } - public bool Buffer(string? value) + return true; + } + + public void Remove(string literal) + { + Debug.Assert(_lastValueOffset != -1, "Cannot invoke Remove more than once."); + _path.Length = _lastValueOffset; + _lastValueOffset = -1; + } + + public bool Buffer(string? value) + { + if (string.IsNullOrEmpty(value)) { - if (string.IsNullOrEmpty(value)) + if (BufferState == SegmentState.Inside) { - if (BufferState == SegmentState.Inside) - { - // We can't write an 'empty' part inside a segment - return false; - } - else - { - _hasEmptySegment = true; - return true; - } + // We can't write an 'empty' part inside a segment + return false; } - else if (_hasEmptySegment) + else { - // We're trying to write text after an empty segment - this is not allowed. - return false; + _hasEmptySegment = true; + return true; } + } + else if (_hasEmptySegment) + { + // We're trying to write text after an empty segment - this is not allowed. + return false; + } - if (UriState == SegmentState.Inside) - { - // We've already written part of this segment so there's no point in buffering, we need to - // write out the rest or give up. - var result = Accept(value); + if (UriState == SegmentState.Inside) + { + // We've already written part of this segment so there's no point in buffering, we need to + // write out the rest or give up. + var result = Accept(value); - // We've already checked the conditions that could result in a rejected part, so this should - // always be true. - Debug.Assert(result); + // We've already checked the conditions that could result in a rejected part, so this should + // always be true. + Debug.Assert(result); - return result; - } + return result; + } - if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning) + if (UriState == SegmentState.Beginning && BufferState == SegmentState.Beginning) + { + if (_path.Length != 0 || _buffer.Count != 0) { - if (_path.Length != 0 || _buffer.Count != 0) - { - _buffer.Add(new BufferValue("/", requiresEncoding: false)); - } - - BufferState = SegmentState.Inside; + _buffer.Add(new BufferValue("/", requiresEncoding: false)); } - _buffer.Add(new BufferValue(value, requiresEncoding: true)); - return true; + BufferState = SegmentState.Inside; } - public void EndSegment() + _buffer.Add(new BufferValue(value, requiresEncoding: true)); + return true; + } + + public void EndSegment() + { + BufferState = SegmentState.Beginning; + UriState = SegmentState.Beginning; + } + + public void Clear() + { + _path.Clear(); + if (_path.Capacity > 128) { - BufferState = SegmentState.Beginning; - UriState = SegmentState.Beginning; + // We don't want to retain too much memory if this is getting pooled. + _path.Capacity = 128; } - public void Clear() + _query.Clear(); + if (_query.Capacity > 128) { - _path.Clear(); - if (_path.Capacity > 128) - { - // We don't want to retain too much memory if this is getting pooled. - _path.Capacity = 128; - } + _query.Capacity = 128; + } - _query.Clear(); - if (_query.Capacity > 128) - { - _query.Capacity = 128; - } + _buffer.Clear(); + if (_buffer.Capacity > 8) + { + _buffer.Capacity = 8; + } - _buffer.Clear(); - if (_buffer.Capacity > 8) - { - _buffer.Capacity = 8; - } + _hasEmptySegment = false; + _lastValueOffset = -1; + BufferState = SegmentState.Beginning; + UriState = SegmentState.Beginning; - _hasEmptySegment = false; - _lastValueOffset = -1; - BufferState = SegmentState.Beginning; - UriState = SegmentState.Beginning; + AppendTrailingSlash = false; + LowercaseQueryStrings = false; + LowercaseUrls = false; + } - AppendTrailingSlash = false; - LowercaseQueryStrings = false; - LowercaseUrls = false; + // Used by TemplateBinder.BindValues - the legacy code path of IRouter + public override string ToString() + { + // We can ignore any currently buffered segments - they are are guaranteed to be 'defaults'. + if (_path.Length > 0 && _path[0] != '/') + { + // Normalize generated paths so that they always contain a leading slash. + _path.Insert(0, '/'); } - // Used by TemplateBinder.BindValues - the legacy code path of IRouter - public override string ToString() + return _path.ToString() + _query.ToString(); + } + + // Used by TemplateBinder.TryBindValues - the new code path of LinkGenerator + public PathString ToPathString() + { + PathString pathString; + + if (_path.Length > 0) { - // We can ignore any currently buffered segments - they are are guaranteed to be 'defaults'. - if (_path.Length > 0 && _path[0] != '/') + if (_path[0] != '/') { // Normalize generated paths so that they always contain a leading slash. _path.Insert(0, '/'); } - return _path.ToString() + _query.ToString(); - } - - // Used by TemplateBinder.TryBindValues - the new code path of LinkGenerator - public PathString ToPathString() - { - PathString pathString; - - if (_path.Length > 0) - { - if (_path[0] != '/') - { - // Normalize generated paths so that they always contain a leading slash. - _path.Insert(0, '/'); - } - - if (AppendTrailingSlash && _path[_path.Length - 1] != '/') - { - _path.Append('/'); - } - - pathString = new PathString(_path.ToString()); - } - else + if (AppendTrailingSlash && _path[_path.Length - 1] != '/') { - pathString = PathString.Empty; + _path.Append('/'); } - return pathString; + pathString = new PathString(_path.ToString()); } - - // Used by TemplateBinder.TryBindValues - the new code path of LinkGenerator - public QueryString ToQueryString() + else { - if (_query.Length > 0 && _query[0] != '?') - { - // Normalize generated query so that they always contain a leading ?. - _query.Insert(0, '?'); - } - - return new QueryString(_query.ToString()); + pathString = PathString.Empty; } - private void EncodeValue(string value) + return pathString; + } + + // Used by TemplateBinder.TryBindValues - the new code path of LinkGenerator + public QueryString ToQueryString() + { + if (_query.Length > 0 && _query[0] != '?') { - EncodeValue(value, encodeSlashes: true); + // Normalize generated query so that they always contain a leading ?. + _query.Insert(0, '?'); } - private void EncodeValue(string value, bool encodeSlashes) + return new QueryString(_query.ToString()); + } + + private void EncodeValue(string value) + { + EncodeValue(value, encodeSlashes: true); + } + + private void EncodeValue(string value, bool encodeSlashes) + { + EncodeValue(value, start: 0, characterCount: value.Length, encodeSlashes); + } + + // For testing + internal void EncodeValue(string value, int start, int characterCount, bool encodeSlashes) + { + // Just encode everything if its ok to encode slashes + if (encodeSlashes) { - EncodeValue(value, start: 0, characterCount: value.Length, encodeSlashes); + _urlEncoder.Encode(PathWriter, value, start, characterCount); } - - // For testing - internal void EncodeValue(string value, int start, int characterCount, bool encodeSlashes) + else { - // Just encode everything if its ok to encode slashes - if (encodeSlashes) + int end; + int length = start + characterCount; + while ((end = value.IndexOf('/', start, characterCount)) >= 0) { - _urlEncoder.Encode(PathWriter, value, start, characterCount); + _urlEncoder.Encode(PathWriter, value, start, end - start); + _path.Append('/'); + + start = end + 1; + characterCount = length - start; } - else + + if (end < 0 && characterCount >= 0) { - int end; - int length = start + characterCount; - while ((end = value.IndexOf('/', start, characterCount)) >= 0) - { - _urlEncoder.Encode(PathWriter, value, start, end - start); - _path.Append('/'); - - start = end + 1; - characterCount = length - start; - } - - if (end < 0 && characterCount >= 0) - { - _urlEncoder.Encode(PathWriter, value, start, length - start); - } + _urlEncoder.Encode(PathWriter, value, start, length - start); } } + } - private string DebuggerToString() - { - return string.Format(CultureInfo.InvariantCulture, "{{Accepted: '{0}' Buffered: '{1}'}}", _path, string.Join("", _buffer)); - } + private string DebuggerToString() + { + return string.Format(CultureInfo.InvariantCulture, "{{Accepted: '{0}' Buffered: '{1}'}}", _path, string.Join("", _buffer)); + } - private readonly struct BufferValue + private readonly struct BufferValue + { + public BufferValue(string value, bool requiresEncoding) { - public BufferValue(string value, bool requiresEncoding) - { - Value = value; - RequiresEncoding = requiresEncoding; - } + Value = value; + RequiresEncoding = requiresEncoding; + } - public bool RequiresEncoding { get; } + public bool RequiresEncoding { get; } - public string Value { get; } - } + public string Value { get; } } } diff --git a/src/Http/Routing/test/FunctionalTests/Benchmarks/EndpointRoutingBenchmarkTest.cs b/src/Http/Routing/test/FunctionalTests/Benchmarks/EndpointRoutingBenchmarkTest.cs index 2467bbe41b..0c976d78c3 100644 --- a/src/Http/Routing/test/FunctionalTests/Benchmarks/EndpointRoutingBenchmarkTest.cs +++ b/src/Http/Routing/test/FunctionalTests/Benchmarks/EndpointRoutingBenchmarkTest.cs @@ -11,57 +11,56 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Xunit; -namespace Microsoft.AspNetCore.Routing.FunctionalTests +namespace Microsoft.AspNetCore.Routing.FunctionalTests; + +public class EndpointRoutingBenchmarkTest : IDisposable { - public class EndpointRoutingBenchmarkTest : IDisposable - { - private readonly HttpClient _client; - private readonly IHost _host; - private readonly TestServer _testServer; + private readonly HttpClient _client; + private readonly IHost _host; + private readonly TestServer _testServer; - public EndpointRoutingBenchmarkTest() - { - // This switch and value are set by benchmark server when running the app for profiling. - var args = new[] { "--scenarios", "PlaintextEndpointRouting" }; - var hostBuilder = Benchmarks.Program.GetHostBuilder(args); + public EndpointRoutingBenchmarkTest() + { + // This switch and value are set by benchmark server when running the app for profiling. + var args = new[] { "--scenarios", "PlaintextEndpointRouting" }; + var hostBuilder = Benchmarks.Program.GetHostBuilder(args); - _host = hostBuilder.Build(); + _host = hostBuilder.Build(); - // Make sure we are using the right startup - var configuration = _host.Services.GetService(); - var startupName = configuration["Startup"]; - Assert.Equal(nameof(Benchmarks.StartupUsingEndpointRouting), startupName); + // Make sure we are using the right startup + var configuration = _host.Services.GetService(); + var startupName = configuration["Startup"]; + Assert.Equal(nameof(Benchmarks.StartupUsingEndpointRouting), startupName); - _testServer = _host.GetTestServer(); - _host.Start(); - _client = _testServer.CreateClient(); - _client.BaseAddress = new Uri("http://localhost"); - } + _testServer = _host.GetTestServer(); + _host.Start(); + _client = _testServer.CreateClient(); + _client.BaseAddress = new Uri("http://localhost"); + } - [Fact] - public async Task RouteEndpoint_ReturnsPlaintextResponse() - { - // Arrange - var expectedContentType = "text/plain"; - var expectedContent = "Hello, World!"; + [Fact] + public async Task RouteEndpoint_ReturnsPlaintextResponse() + { + // Arrange + var expectedContentType = "text/plain"; + var expectedContent = "Hello, World!"; - // Act - var response = await _client.GetAsync("/plaintext"); + // Act + var response = await _client.GetAsync("/plaintext"); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - Assert.NotNull(response.Content.Headers.ContentType); - Assert.Equal(expectedContentType, response.Content.Headers.ContentType.MediaType); - var actualContent = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedContent, actualContent); - } + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal(expectedContentType, response.Content.Headers.ContentType.MediaType); + var actualContent = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedContent, actualContent); + } - public void Dispose() - { - _testServer.Dispose(); - _client.Dispose(); - _host.Dispose(); - } + public void Dispose() + { + _testServer.Dispose(); + _client.Dispose(); + _host.Dispose(); } } diff --git a/src/Http/Routing/test/FunctionalTests/Benchmarks/RouterBenchmarkTest.cs b/src/Http/Routing/test/FunctionalTests/Benchmarks/RouterBenchmarkTest.cs index e05acf80a0..b27f426012 100644 --- a/src/Http/Routing/test/FunctionalTests/Benchmarks/RouterBenchmarkTest.cs +++ b/src/Http/Routing/test/FunctionalTests/Benchmarks/RouterBenchmarkTest.cs @@ -5,64 +5,63 @@ using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; -using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Xunit; -namespace Microsoft.AspNetCore.Routing.FunctionalTests +namespace Microsoft.AspNetCore.Routing.FunctionalTests; + +public class RouterBenchmarkTest : IDisposable { - public class RouterBenchmarkTest : IDisposable - { - private readonly HttpClient _client; - private readonly IHost _host; - private readonly TestServer _testServer; + private readonly HttpClient _client; + private readonly IHost _host; + private readonly TestServer _testServer; - public RouterBenchmarkTest() - { - // This switch and value are set by benchmark server when running the app for profiling. - var args = new[] { "--scenarios", "PlaintextRouting" }; - var hostBuilder = Benchmarks.Program.GetHostBuilder(args); + public RouterBenchmarkTest() + { + // This switch and value are set by benchmark server when running the app for profiling. + var args = new[] { "--scenarios", "PlaintextRouting" }; + var hostBuilder = Benchmarks.Program.GetHostBuilder(args); - _host = hostBuilder.Build(); + _host = hostBuilder.Build(); - // Make sure we are using the right startup - var configuration = _host.Services.GetService(); - var startupName = configuration["Startup"]; - Assert.Equal(nameof(Benchmarks.StartupUsingRouter), startupName); + // Make sure we are using the right startup + var configuration = _host.Services.GetService(); + var startupName = configuration["Startup"]; + Assert.Equal(nameof(Benchmarks.StartupUsingRouter), startupName); - _testServer = _host.GetTestServer(); - _host.Start(); - _client = _testServer.CreateClient(); - _client.BaseAddress = new Uri("http://localhost"); - } + _testServer = _host.GetTestServer(); + _host.Start(); + _client = _testServer.CreateClient(); + _client.BaseAddress = new Uri("http://localhost"); + } - [Fact] - public async Task RouteHandlerWritesResponse() - { - // Arrange - var expectedContentType = "text/plain"; - var expectedContent = "Hello, World!"; + [Fact] + public async Task RouteHandlerWritesResponse() + { + // Arrange + var expectedContentType = "text/plain"; + var expectedContent = "Hello, World!"; - // Act - var response = await _client.GetAsync("/plaintext"); + // Act + var response = await _client.GetAsync("/plaintext"); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - Assert.NotNull(response.Content.Headers.ContentType); - Assert.Equal(expectedContentType, response.Content.Headers.ContentType.MediaType); - var actualContent = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedContent, actualContent); - } + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal(expectedContentType, response.Content.Headers.ContentType.MediaType); + var actualContent = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedContent, actualContent); + } - public void Dispose() - { - _testServer.Dispose(); - _client.Dispose(); - _host.Dispose(); - } + public void Dispose() + { + _testServer.Dispose(); + _client.Dispose(); + _host.Dispose(); } } diff --git a/src/Http/Routing/test/FunctionalTests/EndpointRoutingIntegrationTest.cs b/src/Http/Routing/test/FunctionalTests/EndpointRoutingIntegrationTest.cs index 0d6f2f4fa7..9c87e84e31 100644 --- a/src/Http/Routing/test/FunctionalTests/EndpointRoutingIntegrationTest.cs +++ b/src/Http/Routing/test/FunctionalTests/EndpointRoutingIntegrationTest.cs @@ -13,301 +13,300 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Xunit; -namespace Microsoft.AspNetCore.Routing.FunctionalTests +namespace Microsoft.AspNetCore.Routing.FunctionalTests; + +public class EndpointRoutingIntegrationTest { - public class EndpointRoutingIntegrationTest + private static readonly RequestDelegate TestDelegate = async context => await Task.Yield(); + private static readonly string AuthErrorMessage = "Endpoint / contains authorization metadata, but a middleware was not found that supports authorization." + + Environment.NewLine + + "Configure your application startup by adding app.UseAuthorization() in the application startup code. " + + "If there are calls to app.UseRouting() and app.UseEndpoints(...), the call to app.UseAuthorization() must go between them."; + + private static readonly string CORSErrorMessage = "Endpoint / contains CORS metadata, but a middleware was not found that supports CORS." + + Environment.NewLine + + "Configure your application startup by adding app.UseCors() in the application startup code. " + + "If there are calls to app.UseRouting() and app.UseEndpoints(...), the call to app.UseCors() must go between them."; + + [Fact] + public async Task AuthorizationMiddleware_WhenNoAuthMetadataIsConfigured() + { + // Arrange + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .Configure(app => + { + app.UseRouting(); + app.UseAuthorization(); + app.UseEndpoints(b => b.Map("/", TestDelegate)); + }) + .UseTestServer(); + }) + .ConfigureServices(services => + { + services.AddAuthorization(); + services.AddRouting(); + }) + .Build(); + + using var server = host.GetTestServer(); + + await host.StartAsync(); + + var response = await server.CreateRequest("/").SendAsync("GET"); + + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task AuthorizationMiddleware_WhenEndpointIsNotFound() + { + // Arrange + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .Configure(app => + { + app.UseRouting(); + app.UseAuthorization(); + app.UseEndpoints(b => b.Map("/", TestDelegate)); + }) + .UseTestServer(); + }) + .ConfigureServices(services => + { + services.AddAuthorization(); + services.AddRouting(); + }) + .Build(); + + using var server = host.GetTestServer(); + + await host.StartAsync(); + + var response = await server.CreateRequest("/not-found").SendAsync("GET"); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task AuthorizationMiddleware_WithAuthorizedEndpoint() + { + // Arrange + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .Configure(app => + { + app.UseRouting(); + app.UseAuthorization(); + app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization()); + }) + .UseTestServer(); + }) + .ConfigureServices(services => + { + services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build()); + services.AddRouting(); + }) + .Build(); + + using var server = host.GetTestServer(); + + await host.StartAsync(); + + var response = await server.CreateRequest("/").SendAsync("GET"); + + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task AuthorizationMiddleware_NotConfigured_Throws() + { + // Arrange + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization()); + + }) + .UseTestServer(); + }) + .ConfigureServices(services => + { + services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build()); + services.AddRouting(); + }) + .Build(); + + using var server = host.GetTestServer(); + + await host.StartAsync(); + + var ex = await Assert.ThrowsAsync(() => server.CreateRequest("/").SendAsync("GET")); + Assert.Equal(AuthErrorMessage, ex.Message); + } + + [Fact] + public async Task AuthorizationMiddleware_NotConfigured_WhenEndpointIsNotFound() + { + // Arrange + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization()); + }) + .UseTestServer(); + }) + .ConfigureServices(services => + { + services.AddRouting(); + }) + .Build(); + + using var server = host.GetTestServer(); + + await host.StartAsync(); + + var response = await server.CreateRequest("/not-found").SendAsync("GET"); + + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task AuthorizationMiddleware_ConfiguredBeforeRouting_Throws() + { + // Arrange + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .Configure(app => + { + app.UseAuthorization(); + app.UseRouting(); + app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization()); + }) + .UseTestServer(); + }) + .ConfigureServices(services => + { + services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build()); + services.AddRouting(); + }) + .Build(); + + using var server = host.GetTestServer(); + + await host.StartAsync(); + + var ex = await Assert.ThrowsAsync(() => server.CreateRequest("/").SendAsync("GET")); + Assert.Equal(AuthErrorMessage, ex.Message); + } + + [Fact] + public async Task AuthorizationMiddleware_ConfiguredAfterRouting_Throws() + { + // Arrange + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization()); + app.UseAuthorization(); + }) + .UseTestServer(); + }) + .ConfigureServices(services => + { + services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build()); + services.AddRouting(); + }) + .Build(); + + using var server = host.GetTestServer(); + + await host.StartAsync(); + + var ex = await Assert.ThrowsAsync(() => server.CreateRequest("/").SendAsync("GET")); + Assert.Equal(AuthErrorMessage, ex.Message); + } + + [Fact] + public async Task CorsMiddleware_WithCorsEndpoint() + { + // Arrange + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .Configure(app => + { + app.UseRouting(); + app.UseCors(); + app.UseEndpoints(b => b.Map("/", TestDelegate).RequireCors(policy => policy.AllowAnyOrigin())); + }) + .UseTestServer(); + }) + .ConfigureServices(services => + { + services.AddCors(); + services.AddRouting(); + }) + .Build(); + + using var server = host.GetTestServer(); + + await host.StartAsync(); + + var response = await server.CreateRequest("/").SendAsync("PUT"); + + response.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task CorsMiddleware_ConfiguredBeforeRouting_Throws() { - private static readonly RequestDelegate TestDelegate = async context => await Task.Yield(); - private static readonly string AuthErrorMessage = "Endpoint / contains authorization metadata, but a middleware was not found that supports authorization." + - Environment.NewLine + - "Configure your application startup by adding app.UseAuthorization() in the application startup code. " + - "If there are calls to app.UseRouting() and app.UseEndpoints(...), the call to app.UseAuthorization() must go between them."; - - private static readonly string CORSErrorMessage = "Endpoint / contains CORS metadata, but a middleware was not found that supports CORS." + - Environment.NewLine + - "Configure your application startup by adding app.UseCors() in the application startup code. " + - "If there are calls to app.UseRouting() and app.UseEndpoints(...), the call to app.UseCors() must go between them."; - - [Fact] - public async Task AuthorizationMiddleware_WhenNoAuthMetadataIsConfigured() - { - // Arrange - using var host = new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .Configure(app => - { - app.UseRouting(); - app.UseAuthorization(); - app.UseEndpoints(b => b.Map("/", TestDelegate)); - }) - .UseTestServer(); - }) - .ConfigureServices(services => - { - services.AddAuthorization(); - services.AddRouting(); - }) - .Build(); - - using var server = host.GetTestServer(); - - await host.StartAsync(); - - var response = await server.CreateRequest("/").SendAsync("GET"); - - response.EnsureSuccessStatusCode(); - } - - [Fact] - public async Task AuthorizationMiddleware_WhenEndpointIsNotFound() - { - // Arrange - using var host = new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .Configure(app => - { - app.UseRouting(); - app.UseAuthorization(); - app.UseEndpoints(b => b.Map("/", TestDelegate)); - }) - .UseTestServer(); - }) - .ConfigureServices(services => - { - services.AddAuthorization(); - services.AddRouting(); - }) - .Build(); - - using var server = host.GetTestServer(); - - await host.StartAsync(); - - var response = await server.CreateRequest("/not-found").SendAsync("GET"); - - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Fact] - public async Task AuthorizationMiddleware_WithAuthorizedEndpoint() - { - // Arrange - using var host = new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .Configure(app => - { - app.UseRouting(); - app.UseAuthorization(); - app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization()); - }) - .UseTestServer(); - }) - .ConfigureServices(services => - { - services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build()); - services.AddRouting(); - }) - .Build(); - - using var server = host.GetTestServer(); - - await host.StartAsync(); - - var response = await server.CreateRequest("/").SendAsync("GET"); - - response.EnsureSuccessStatusCode(); - } - - [Fact] - public async Task AuthorizationMiddleware_NotConfigured_Throws() - { - // Arrange - using var host = new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .Configure(app => - { - app.UseRouting(); - app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization()); - - }) - .UseTestServer(); - }) - .ConfigureServices(services => - { - services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build()); - services.AddRouting(); - }) - .Build(); - - using var server = host.GetTestServer(); - - await host.StartAsync(); - - var ex = await Assert.ThrowsAsync(() => server.CreateRequest("/").SendAsync("GET")); - Assert.Equal(AuthErrorMessage, ex.Message); - } - - [Fact] - public async Task AuthorizationMiddleware_NotConfigured_WhenEndpointIsNotFound() - { - // Arrange - using var host = new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .Configure(app => - { - app.UseRouting(); - app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization()); - }) - .UseTestServer(); - }) - .ConfigureServices(services => - { - services.AddRouting(); - }) - .Build(); - - using var server = host.GetTestServer(); - - await host.StartAsync(); - - var response = await server.CreateRequest("/not-found").SendAsync("GET"); - - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Fact] - public async Task AuthorizationMiddleware_ConfiguredBeforeRouting_Throws() - { - // Arrange - using var host = new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .Configure(app => - { - app.UseAuthorization(); - app.UseRouting(); - app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization()); - }) - .UseTestServer(); - }) - .ConfigureServices(services => - { - services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build()); - services.AddRouting(); - }) - .Build(); - - using var server = host.GetTestServer(); - - await host.StartAsync(); - - var ex = await Assert.ThrowsAsync(() => server.CreateRequest("/").SendAsync("GET")); - Assert.Equal(AuthErrorMessage, ex.Message); - } - - [Fact] - public async Task AuthorizationMiddleware_ConfiguredAfterRouting_Throws() - { - // Arrange - using var host = new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .Configure(app => - { - app.UseRouting(); - app.UseEndpoints(b => b.Map("/", TestDelegate).RequireAuthorization()); - app.UseAuthorization(); - }) - .UseTestServer(); - }) - .ConfigureServices(services => - { - services.AddAuthorization(options => options.DefaultPolicy = new AuthorizationPolicyBuilder().RequireAssertion(_ => true).Build()); - services.AddRouting(); - }) - .Build(); - - using var server = host.GetTestServer(); - - await host.StartAsync(); - - var ex = await Assert.ThrowsAsync(() => server.CreateRequest("/").SendAsync("GET")); - Assert.Equal(AuthErrorMessage, ex.Message); - } - - [Fact] - public async Task CorsMiddleware_WithCorsEndpoint() - { - // Arrange - using var host = new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .Configure(app => - { - app.UseRouting(); - app.UseCors(); - app.UseEndpoints(b => b.Map("/", TestDelegate).RequireCors(policy => policy.AllowAnyOrigin())); - }) - .UseTestServer(); - }) - .ConfigureServices(services => - { - services.AddCors(); - services.AddRouting(); - }) - .Build(); - - using var server = host.GetTestServer(); - - await host.StartAsync(); - - var response = await server.CreateRequest("/").SendAsync("PUT"); - - response.EnsureSuccessStatusCode(); - } - - [Fact] - public async Task CorsMiddleware_ConfiguredBeforeRouting_Throws() - { - // Arrange - using var host = new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .Configure(app => - { - app.UseCors(); - app.UseRouting(); - app.UseEndpoints(b => b.Map("/", TestDelegate).RequireCors(policy => policy.AllowAnyOrigin())); - }) - .UseTestServer(); - }) - .ConfigureServices(services => - { - services.AddCors(); - services.AddRouting(); - }) - .Build(); - - using var server = host.GetTestServer(); - - await host.StartAsync(); - - var ex = await Assert.ThrowsAsync(() => server.CreateRequest("/").SendAsync("GET")); - Assert.Equal(CORSErrorMessage, ex.Message); - } + // Arrange + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .Configure(app => + { + app.UseCors(); + app.UseRouting(); + app.UseEndpoints(b => b.Map("/", TestDelegate).RequireCors(policy => policy.AllowAnyOrigin())); + }) + .UseTestServer(); + }) + .ConfigureServices(services => + { + services.AddCors(); + services.AddRouting(); + }) + .Build(); + + using var server = host.GetTestServer(); + + await host.StartAsync(); + + var ex = await Assert.ThrowsAsync(() => server.CreateRequest("/").SendAsync("GET")); + Assert.Equal(CORSErrorMessage, ex.Message); } } diff --git a/src/Http/Routing/test/FunctionalTests/EndpointRoutingSampleTest.cs b/src/Http/Routing/test/FunctionalTests/EndpointRoutingSampleTest.cs index 36594c7120..a3cb971969 100644 --- a/src/Http/Routing/test/FunctionalTests/EndpointRoutingSampleTest.cs +++ b/src/Http/Routing/test/FunctionalTests/EndpointRoutingSampleTest.cs @@ -10,230 +10,229 @@ using Microsoft.Extensions.Hosting; using RoutingWebSite; using Xunit; -namespace Microsoft.AspNetCore.Routing.FunctionalTests +namespace Microsoft.AspNetCore.Routing.FunctionalTests; + +public class EndpointRoutingSampleTest : IDisposable { - public class EndpointRoutingSampleTest : IDisposable + private readonly HttpClient _client; + private readonly IHost _host; + private readonly TestServer _testServer; + + public EndpointRoutingSampleTest() + { + var hostBuilder = Program.GetHostBuilder(new[] { Program.EndpointRoutingScenario, }); + _host = hostBuilder.Build(); + + _testServer = _host.GetTestServer(); + _host.Start(); + + _client = _testServer.CreateClient(); + _client.BaseAddress = new Uri("http://localhost"); + } + + [Theory] + [InlineData("Branch1")] + [InlineData("Branch2")] + public async Task Routing_CanRouteRequest_ToBranchRouter(string branch) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, $"{branch}/api/get/5"); + + // Act + var response = await _client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal($"{branch} - API Get 5", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task MatchesRootPath_AndReturnsPlaintext() + { + // Arrange + var expectedContentType = "text/plain"; + + // Act + var response = await _client.GetAsync("/"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal(expectedContentType, response.Content.Headers.ContentType.MediaType); + } + + [Fact] + public async Task MatchesStaticRouteTemplate_AndReturnsPlaintext() + { + // Arrange + var expectedContentType = "text/plain"; + var expectedContent = "Plain text!"; + + // Act + var response = await _client.GetAsync("/plaintext"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal(expectedContentType, response.Content.Headers.ContentType.MediaType); + var actualContent = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedContent, actualContent); + } + + [Fact] + public async Task MatchesHelloMiddleware_AndReturnsPlaintext() + { + // Arrange + var expectedContentType = "text/plain"; + var expectedContent = "Hello World"; + + // Act + var response = await _client.GetAsync("/helloworld"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + Assert.NotNull(response.Content.Headers.ContentType); + Assert.Equal(expectedContentType, response.Content.Headers.ContentType.MediaType); + var actualContent = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedContent, actualContent); + } + + [Fact] + public async Task MatchesEndpoint_WithSuccessfulConstraintMatch() + { + // Arrange + var expectedContent = "WithConstraints"; + + // Act + var response = await _client.GetAsync("/withconstraints/555_001"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var actualContent = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedContent, actualContent); + } + + [Fact] + public async Task DoesNotMatchEndpoint_IfConstraintMatchFails() + { + // Arrange & Act + var response = await _client.GetAsync("/withconstraints/555"); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task MatchesEndpoint_WithSuccessful_OptionalConstraintMatch() + { + // Arrange + var expectedContent = "withoptionalconstraints"; + + // Act + var response = await _client.GetAsync("/withoptionalconstraints/555_001"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var actualContent = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedContent, actualContent); + } + + [Fact] + public async Task MatchesEndpoint_WithSuccessful_OptionalConstraintMatch_NoValueForParameter() + { + // Arrange + var expectedContent = "withoptionalconstraints"; + + // Act + var response = await _client.GetAsync("/withoptionalconstraints"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var actualContent = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedContent, actualContent); + } + + [Fact] + public async Task DoesNotMatchEndpoint_IfOptionalConstraintMatchFails() + { + // Arrange & Act + var response = await _client.GetAsync("/withoptionalconstraints/555"); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData("/WithSingleAsteriskCatchAll/a/b/c", "Link: /WithSingleAsteriskCatchAll/a%2Fb%2Fc")] + [InlineData("/WithSingleAsteriskCatchAll/a/b b1/c c1", "Link: /WithSingleAsteriskCatchAll/a%2Fb%20b1%2Fc%20c1")] + public async Task GeneratesLink_ToEndpointWithSingleAsteriskCatchAllParameter_EncodesValue( + string url, + string expected) + { + // Arrange & Act + var response = await _client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var actualContent = await response.Content.ReadAsStringAsync(); + Assert.Equal(expected, actualContent); + } + + [Theory] + [InlineData("/WithDoubleAsteriskCatchAll/a/b/c", "Link: /WithDoubleAsteriskCatchAll/a/b/c")] + [InlineData("/WithDoubleAsteriskCatchAll/a/b/c/", "Link: /WithDoubleAsteriskCatchAll/a/b/c/")] + [InlineData("/WithDoubleAsteriskCatchAll/a//b/c", "Link: /WithDoubleAsteriskCatchAll/a//b/c")] + public async Task GeneratesLink_ToEndpointWithDoubleAsteriskCatchAllParameter_DoesNotEncodeSlashes( + string url, + string expected) + { + // Arrange & Act + var response = await _client.GetAsync(url); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var actualContent = await response.Content.ReadAsStringAsync(); + Assert.Equal(expected, actualContent); + } + + [Fact] + public async Task GeneratesLink_ToEndpointWithDoubleAsteriskCatchAllParameter_EncodesContentOtherThanSlashes() + { + // Arrange & Act + var response = await _client.GetAsync("/WithDoubleAsteriskCatchAll/a/b b1/c c1"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotNull(response.Content); + var actualContent = await response.Content.ReadAsStringAsync(); + Assert.Equal("Link: /WithDoubleAsteriskCatchAll/a/b%20b1/c%20c1", actualContent); + } + + [Fact] + public async Task MapGet_HasConventionMetadata() + { + // Arrange & Act + var response = await _client.GetAsync("/convention"); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var actualContent = await response.Content.ReadAsStringAsync(); + Assert.Equal("Has metadata", actualContent); + } + + public void Dispose() { - private readonly HttpClient _client; - private readonly IHost _host; - private readonly TestServer _testServer; - - public EndpointRoutingSampleTest() - { - var hostBuilder = Program.GetHostBuilder(new[] { Program.EndpointRoutingScenario, }); - _host = hostBuilder.Build(); - - _testServer = _host.GetTestServer(); - _host.Start(); - - _client = _testServer.CreateClient(); - _client.BaseAddress = new Uri("http://localhost"); - } - - [Theory] - [InlineData("Branch1")] - [InlineData("Branch2")] - public async Task Routing_CanRouteRequest_ToBranchRouter(string branch) - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Get, $"{branch}/api/get/5"); - - // Act - var response = await _client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal($"{branch} - API Get 5", await response.Content.ReadAsStringAsync()); - } - - [Fact] - public async Task MatchesRootPath_AndReturnsPlaintext() - { - // Arrange - var expectedContentType = "text/plain"; - - // Act - var response = await _client.GetAsync("/"); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - Assert.NotNull(response.Content.Headers.ContentType); - Assert.Equal(expectedContentType, response.Content.Headers.ContentType.MediaType); - } - - [Fact] - public async Task MatchesStaticRouteTemplate_AndReturnsPlaintext() - { - // Arrange - var expectedContentType = "text/plain"; - var expectedContent = "Plain text!"; - - // Act - var response = await _client.GetAsync("/plaintext"); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - Assert.NotNull(response.Content.Headers.ContentType); - Assert.Equal(expectedContentType, response.Content.Headers.ContentType.MediaType); - var actualContent = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedContent, actualContent); - } - - [Fact] - public async Task MatchesHelloMiddleware_AndReturnsPlaintext() - { - // Arrange - var expectedContentType = "text/plain"; - var expectedContent = "Hello World"; - - // Act - var response = await _client.GetAsync("/helloworld"); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - Assert.NotNull(response.Content.Headers.ContentType); - Assert.Equal(expectedContentType, response.Content.Headers.ContentType.MediaType); - var actualContent = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedContent, actualContent); - } - - [Fact] - public async Task MatchesEndpoint_WithSuccessfulConstraintMatch() - { - // Arrange - var expectedContent = "WithConstraints"; - - // Act - var response = await _client.GetAsync("/withconstraints/555_001"); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - var actualContent = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedContent, actualContent); - } - - [Fact] - public async Task DoesNotMatchEndpoint_IfConstraintMatchFails() - { - // Arrange & Act - var response = await _client.GetAsync("/withconstraints/555"); - - // Assert - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Fact] - public async Task MatchesEndpoint_WithSuccessful_OptionalConstraintMatch() - { - // Arrange - var expectedContent = "withoptionalconstraints"; - - // Act - var response = await _client.GetAsync("/withoptionalconstraints/555_001"); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - var actualContent = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedContent, actualContent); - } - - [Fact] - public async Task MatchesEndpoint_WithSuccessful_OptionalConstraintMatch_NoValueForParameter() - { - // Arrange - var expectedContent = "withoptionalconstraints"; - - // Act - var response = await _client.GetAsync("/withoptionalconstraints"); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - var actualContent = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedContent, actualContent); - } - - [Fact] - public async Task DoesNotMatchEndpoint_IfOptionalConstraintMatchFails() - { - // Arrange & Act - var response = await _client.GetAsync("/withoptionalconstraints/555"); - - // Assert - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Theory] - [InlineData("/WithSingleAsteriskCatchAll/a/b/c", "Link: /WithSingleAsteriskCatchAll/a%2Fb%2Fc")] - [InlineData("/WithSingleAsteriskCatchAll/a/b b1/c c1", "Link: /WithSingleAsteriskCatchAll/a%2Fb%20b1%2Fc%20c1")] - public async Task GeneratesLink_ToEndpointWithSingleAsteriskCatchAllParameter_EncodesValue( - string url, - string expected) - { - // Arrange & Act - var response = await _client.GetAsync(url); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - var actualContent = await response.Content.ReadAsStringAsync(); - Assert.Equal(expected, actualContent); - } - - [Theory] - [InlineData("/WithDoubleAsteriskCatchAll/a/b/c", "Link: /WithDoubleAsteriskCatchAll/a/b/c")] - [InlineData("/WithDoubleAsteriskCatchAll/a/b/c/", "Link: /WithDoubleAsteriskCatchAll/a/b/c/")] - [InlineData("/WithDoubleAsteriskCatchAll/a//b/c", "Link: /WithDoubleAsteriskCatchAll/a//b/c")] - public async Task GeneratesLink_ToEndpointWithDoubleAsteriskCatchAllParameter_DoesNotEncodeSlashes( - string url, - string expected) - { - // Arrange & Act - var response = await _client.GetAsync(url); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - var actualContent = await response.Content.ReadAsStringAsync(); - Assert.Equal(expected, actualContent); - } - - [Fact] - public async Task GeneratesLink_ToEndpointWithDoubleAsteriskCatchAllParameter_EncodesContentOtherThanSlashes() - { - // Arrange & Act - var response = await _client.GetAsync("/WithDoubleAsteriskCatchAll/a/b b1/c c1"); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.NotNull(response.Content); - var actualContent = await response.Content.ReadAsStringAsync(); - Assert.Equal("Link: /WithDoubleAsteriskCatchAll/a/b%20b1/c%20c1", actualContent); - } - - [Fact] - public async Task MapGet_HasConventionMetadata() - { - // Arrange & Act - var response = await _client.GetAsync("/convention"); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var actualContent = await response.Content.ReadAsStringAsync(); - Assert.Equal("Has metadata", actualContent); - } - - public void Dispose() - { - _testServer.Dispose(); - _client.Dispose(); - _host.Dispose(); - } + _testServer.Dispose(); + _client.Dispose(); + _host.Dispose(); } } diff --git a/src/Http/Routing/test/FunctionalTests/HostMatchingTests.cs b/src/Http/Routing/test/FunctionalTests/HostMatchingTests.cs index 1f9b3787a5..e9f0ebb4e3 100644 --- a/src/Http/Routing/test/FunctionalTests/HostMatchingTests.cs +++ b/src/Http/Routing/test/FunctionalTests/HostMatchingTests.cs @@ -8,112 +8,111 @@ using System.Threading.Tasks; using RoutingWebSite; using Xunit; -namespace Microsoft.AspNetCore.Routing.FunctionalTests +namespace Microsoft.AspNetCore.Routing.FunctionalTests; + +public class HostMatchingTests : IClassFixture> { - public class HostMatchingTests : IClassFixture> + private readonly RoutingTestFixture _fixture; + + public HostMatchingTests(RoutingTestFixture fixture) + { + _fixture = fixture; + } + + private HttpClient CreateClient(string baseAddress) + { + var client = _fixture.CreateClient(baseAddress); + + return client; + } + + [Theory] + [InlineData("http://localhost")] + [InlineData("http://localhost:5001")] + public async Task Get_CatchAll(string baseAddress) + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "api/DomainWildcard"); + + // Act + var client = CreateClient(baseAddress); + var response = await client.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("*:*", responseContent); + } + + [Theory] + [InlineData("http://9000.0.0.1")] + [InlineData("http://9000.0.0.1:8888")] + public async Task Get_MatchWildcardDomain(string baseAddress) { - private readonly RoutingTestFixture _fixture; - - public HostMatchingTests(RoutingTestFixture fixture) - { - _fixture = fixture; - } - - private HttpClient CreateClient(string baseAddress) - { - var client = _fixture.CreateClient(baseAddress); - - return client; - } - - [Theory] - [InlineData("http://localhost")] - [InlineData("http://localhost:5001")] - public async Task Get_CatchAll(string baseAddress) - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "api/DomainWildcard"); - - // Act - var client = CreateClient(baseAddress); - var response = await client.SendAsync(request); - var responseContent = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("*:*", responseContent); - } - - [Theory] - [InlineData("http://9000.0.0.1")] - [InlineData("http://9000.0.0.1:8888")] - public async Task Get_MatchWildcardDomain(string baseAddress) - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "api/DomainWildcard"); - - // Act - var client = CreateClient(baseAddress); - var response = await client.SendAsync(request); - var responseContent = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("*.0.0.1:*", responseContent); - } - - [Theory] - [InlineData("http://127.0.0.1")] - [InlineData("http://127.0.0.1:8888")] - public async Task Get_MatchDomain(string baseAddress) - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "api/DomainWildcard"); - - // Act - var client = CreateClient(baseAddress); - var response = await client.SendAsync(request); - var responseContent = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("127.0.0.1:*", responseContent); - } - - [Theory] - [InlineData("http://9000.0.0.1:5000")] - [InlineData("http://9000.0.0.1:5001")] - public async Task Get_MatchWildcardDomainAndPort(string baseAddress) - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "api/DomainWildcard"); - - // Act - var client = CreateClient(baseAddress); - var response = await client.SendAsync(request); - var responseContent = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("*.0.0.1:5000,*.0.0.1:5001", responseContent); - } - - [Theory] - [InlineData("http://www.contoso.com")] - [InlineData("http://contoso.com")] - public async Task Get_MatchWildcardDomainAndSubdomain(string baseAddress) - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "api/DomainWildcard"); - - // Act - var client = CreateClient(baseAddress); - var response = await client.SendAsync(request); - var responseContent = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("contoso.com:*,*.contoso.com:*", responseContent); - } + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "api/DomainWildcard"); + + // Act + var client = CreateClient(baseAddress); + var response = await client.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("*.0.0.1:*", responseContent); + } + + [Theory] + [InlineData("http://127.0.0.1")] + [InlineData("http://127.0.0.1:8888")] + public async Task Get_MatchDomain(string baseAddress) + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "api/DomainWildcard"); + + // Act + var client = CreateClient(baseAddress); + var response = await client.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("127.0.0.1:*", responseContent); + } + + [Theory] + [InlineData("http://9000.0.0.1:5000")] + [InlineData("http://9000.0.0.1:5001")] + public async Task Get_MatchWildcardDomainAndPort(string baseAddress) + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "api/DomainWildcard"); + + // Act + var client = CreateClient(baseAddress); + var response = await client.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("*.0.0.1:5000,*.0.0.1:5001", responseContent); + } + + [Theory] + [InlineData("http://www.contoso.com")] + [InlineData("http://contoso.com")] + public async Task Get_MatchWildcardDomainAndSubdomain(string baseAddress) + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "api/DomainWildcard"); + + // Act + var client = CreateClient(baseAddress); + var response = await client.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("contoso.com:*,*.contoso.com:*", responseContent); } } diff --git a/src/Http/Routing/test/FunctionalTests/MapFallbackTest.cs b/src/Http/Routing/test/FunctionalTests/MapFallbackTest.cs index 2bafe08b88..151f6a2308 100644 --- a/src/Http/Routing/test/FunctionalTests/MapFallbackTest.cs +++ b/src/Http/Routing/test/FunctionalTests/MapFallbackTest.cs @@ -7,100 +7,99 @@ using System.Threading.Tasks; using RoutingWebSite; using Xunit; -namespace Microsoft.AspNetCore.Routing.FunctionalTests +namespace Microsoft.AspNetCore.Routing.FunctionalTests; + +public class MapFallbackTest : IClassFixture> { - public class MapFallbackTest : IClassFixture> + private readonly RoutingTestFixture _fixture; + private readonly HttpClient _client; + + public MapFallbackTest(RoutingTestFixture fixture) + { + _fixture = fixture; + _client = _fixture.CreateClient("http://localhost"); + } + + [Fact] + public async Task Get_HelloWorld() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "helloworld"); + + // Act + var response = await _client.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("Hello World", responseContent); + } + + [Theory] + [InlineData("prefix/favicon.ico")] + [InlineData("prefix/content/js/jquery.min.js")] + public async Task Get_FallbackWithPattern_FileName(string path) { - private readonly RoutingTestFixture _fixture; - private readonly HttpClient _client; - - public MapFallbackTest(RoutingTestFixture fixture) - { - _fixture = fixture; - _client = _fixture.CreateClient("http://localhost"); - } - - [Fact] - public async Task Get_HelloWorld() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "helloworld"); - - // Act - var response = await _client.SendAsync(request); - var responseContent = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("Hello World", responseContent); - } - - [Theory] - [InlineData("prefix/favicon.ico")] - [InlineData("prefix/content/js/jquery.min.js")] - public async Task Get_FallbackWithPattern_FileName(string path) - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, path); - - // Act - var response = await _client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Theory] - [InlineData("prefix")] - [InlineData("prefix/")] - [InlineData("prefix/store")] - [InlineData("prefix/blog/read/18")] - public async Task Get_FallbackWithPattern_NonFileName(string path) - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, path); - - // Act - var response = await _client.SendAsync(request); - var responseContent = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("FallbackCustomPattern", responseContent); - } - - [Theory] - [InlineData("favicon.ico")] - [InlineData("content/js/jquery.min.js")] - public async Task Get_Fallback_FileName(string path) - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, path); - - // Act - var response = await _client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - } - - [Theory] - [InlineData("")] - [InlineData("/")] - [InlineData("store")] - [InlineData("blog/read/18")] - public async Task Get_Fallback_NonFileName(string path) - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, path); - - // Act - var response = await _client.SendAsync(request); - var responseContent = await response.Content.ReadAsStringAsync(); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal("FallbackDefaultPattern", responseContent); - } + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, path); + + // Act + var response = await _client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData("prefix")] + [InlineData("prefix/")] + [InlineData("prefix/store")] + [InlineData("prefix/blog/read/18")] + public async Task Get_FallbackWithPattern_NonFileName(string path) + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, path); + + // Act + var response = await _client.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("FallbackCustomPattern", responseContent); + } + + [Theory] + [InlineData("favicon.ico")] + [InlineData("content/js/jquery.min.js")] + public async Task Get_Fallback_FileName(string path) + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, path); + + // Act + var response = await _client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData("")] + [InlineData("/")] + [InlineData("store")] + [InlineData("blog/read/18")] + public async Task Get_Fallback_NonFileName(string path) + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, path); + + // Act + var response = await _client.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal("FallbackDefaultPattern", responseContent); } } diff --git a/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs b/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs index 6dc71ddd22..f3eff1389c 100644 --- a/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs +++ b/src/Http/Routing/test/FunctionalTests/RouteHandlerTest.cs @@ -11,56 +11,55 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace Microsoft.AspNetCore.Routing.FunctionalTests +namespace Microsoft.AspNetCore.Routing.FunctionalTests; + +public class RouteHandlerTest { - public class RouteHandlerTest + [Fact] + public async Task MapPost_FromBodyWorksWithJsonPayload() { - [Fact] - public async Task MapPost_FromBodyWorksWithJsonPayload() - { - using var host = new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .Configure(app => - { - app.UseRouting(); - app.UseEndpoints(b => - b.MapPost("/EchoTodo/{id}", - (int id, Todo todo) => todo with { Id = id })); - }) - .UseTestServer(); - }) - .ConfigureServices(services => - { - services.AddRouting(); - }) - .Build(); + using var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(b => + b.MapPost("/EchoTodo/{id}", + (int id, Todo todo) => todo with { Id = id })); + }) + .UseTestServer(); + }) + .ConfigureServices(services => + { + services.AddRouting(); + }) + .Build(); - using var server = host.GetTestServer(); - await host.StartAsync(); - var client = server.CreateClient(); + using var server = host.GetTestServer(); + await host.StartAsync(); + var client = server.CreateClient(); - var todo = new Todo - { - Name = "Write tests!" - }; + var todo = new Todo + { + Name = "Write tests!" + }; - var response = await client.PostAsJsonAsync("/EchoTodo/42", todo); - response.EnsureSuccessStatusCode(); + var response = await client.PostAsJsonAsync("/EchoTodo/42", todo); + response.EnsureSuccessStatusCode(); - var echoedTodo = await response.Content.ReadFromJsonAsync(); + var echoedTodo = await response.Content.ReadFromJsonAsync(); - Assert.NotNull(echoedTodo); - Assert.Equal(todo.Name, echoedTodo?.Name); - Assert.Equal(42, echoedTodo?.Id); - } + Assert.NotNull(echoedTodo); + Assert.Equal(todo.Name, echoedTodo?.Name); + Assert.Equal(42, echoedTodo?.Id); + } - private record Todo - { - public int Id { get; set; } - public string Name { get; set; } = "Todo"; - public bool IsComplete { get; set; } - } + private record Todo + { + public int Id { get; set; } + public string Name { get; set; } = "Todo"; + public bool IsComplete { get; set; } } } diff --git a/src/Http/Routing/test/FunctionalTests/RouterSampleTest.cs b/src/Http/Routing/test/FunctionalTests/RouterSampleTest.cs index 7bfb56af2d..66029737e9 100644 --- a/src/Http/Routing/test/FunctionalTests/RouterSampleTest.cs +++ b/src/Http/Routing/test/FunctionalTests/RouterSampleTest.cs @@ -10,97 +10,96 @@ using Microsoft.Extensions.Hosting; using RoutingWebSite; using Xunit; -namespace Microsoft.AspNetCore.Routing.FunctionalTests +namespace Microsoft.AspNetCore.Routing.FunctionalTests; + +public class RouterSampleTest : IDisposable { - public class RouterSampleTest : IDisposable + private readonly HttpClient _client; + private readonly IHost _host; + private readonly TestServer _testServer; + + public RouterSampleTest() + { + var hostBuilder = Program.GetHostBuilder(new[] { Program.RouterScenario, }); + _host = hostBuilder.Build(); + _testServer = _host.GetTestServer(); + _host.Start(); + _client = _testServer.CreateClient(); + _client.BaseAddress = new Uri("http://localhost"); + } + + [Theory] + [InlineData("Branch1")] + [InlineData("Branch2")] + public async Task Routing_CanRouteRequest_ToBranchRouter(string branch) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, $"{branch}/api/get/5"); + + // Act + var response = await _client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal($"{branch} - API Get 5", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Routing_CanRouteRequestDelegate_ToSpecificHttpVerb() + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, "api/get/5"); + + // Act + var response = await _client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal($"API Get 5", await response.Content.ReadAsStringAsync()); + } + + [Fact] + public async Task Routing_CanRouteRequest_ToSpecificMiddleware() + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, "api/middleware"); + + // Act + var response = await _client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal($"Middleware!", await response.Content.ReadAsStringAsync()); + } + + [Theory] + [InlineData("GET")] + [InlineData("POST")] + [InlineData("PUT")] + [InlineData("PATCH")] + [InlineData("DELETE")] + [InlineData("HEAD")] + [InlineData("OPTIONS")] + public async Task Routing_CanRouteRequest_ToDefaultHandler(string httpVerb) + { + // Arrange + var message = new HttpRequestMessage(new HttpMethod(httpVerb), "api/all/Joe/Duf"); + var expectedBody = $"Verb = {httpVerb} - Path = /api/all/Joe/Duf - Route values - [name, Joe], [lastName, Duf]"; + + // Act + var response = await _client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedBody, body); + } + + public void Dispose() { - private readonly HttpClient _client; - private readonly IHost _host; - private readonly TestServer _testServer; - - public RouterSampleTest() - { - var hostBuilder = Program.GetHostBuilder(new[] { Program.RouterScenario, }); - _host = hostBuilder.Build(); - _testServer = _host.GetTestServer(); - _host.Start(); - _client = _testServer.CreateClient(); - _client.BaseAddress = new Uri("http://localhost"); - } - - [Theory] - [InlineData("Branch1")] - [InlineData("Branch2")] - public async Task Routing_CanRouteRequest_ToBranchRouter(string branch) - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Get, $"{branch}/api/get/5"); - - // Act - var response = await _client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal($"{branch} - API Get 5", await response.Content.ReadAsStringAsync()); - } - - [Fact] - public async Task Routing_CanRouteRequestDelegate_ToSpecificHttpVerb() - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Get, "api/get/5"); - - // Act - var response = await _client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal($"API Get 5", await response.Content.ReadAsStringAsync()); - } - - [Fact] - public async Task Routing_CanRouteRequest_ToSpecificMiddleware() - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Get, "api/middleware"); - - // Act - var response = await _client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal($"Middleware!", await response.Content.ReadAsStringAsync()); - } - - [Theory] - [InlineData("GET")] - [InlineData("POST")] - [InlineData("PUT")] - [InlineData("PATCH")] - [InlineData("DELETE")] - [InlineData("HEAD")] - [InlineData("OPTIONS")] - public async Task Routing_CanRouteRequest_ToDefaultHandler(string httpVerb) - { - // Arrange - var message = new HttpRequestMessage(new HttpMethod(httpVerb), "api/all/Joe/Duf"); - var expectedBody = $"Verb = {httpVerb} - Path = /api/all/Joe/Duf - Route values - [name, Joe], [lastName, Duf]"; - - // Act - var response = await _client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - Assert.Equal(expectedBody, body); - } - - public void Dispose() - { - _testServer.Dispose(); - _client.Dispose(); - _host.Dispose(); - } + _testServer.Dispose(); + _client.Dispose(); + _host.Dispose(); } } diff --git a/src/Http/Routing/test/FunctionalTests/RoutingTestFixture.cs b/src/Http/Routing/test/FunctionalTests/RoutingTestFixture.cs index f4c0d63741..65f39e90bb 100644 --- a/src/Http/Routing/test/FunctionalTests/RoutingTestFixture.cs +++ b/src/Http/Routing/test/FunctionalTests/RoutingTestFixture.cs @@ -7,45 +7,44 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Hosting; -namespace Microsoft.AspNetCore.Routing.FunctionalTests +namespace Microsoft.AspNetCore.Routing.FunctionalTests; + +public class RoutingTestFixture : IDisposable { - public class RoutingTestFixture : IDisposable + private readonly TestServer _server; + + public RoutingTestFixture() + { + var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseStartup(typeof(TStartup)) + .UseTestServer(); + }) + .Build(); + + _server = host.GetTestServer(); + + host.Start(); + + Client = _server.CreateClient(); + Client.BaseAddress = new Uri("http://localhost"); + } + + public HttpClient Client { get; } + + public HttpClient CreateClient(string baseAddress) + { + var client = _server.CreateClient(); + client.BaseAddress = new Uri(baseAddress); + + return client; + } + + public void Dispose() { - private readonly TestServer _server; - - public RoutingTestFixture() - { - var host = new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .UseStartup(typeof(TStartup)) - .UseTestServer(); - }) - .Build(); - - _server = host.GetTestServer(); - - host.Start(); - - Client = _server.CreateClient(); - Client.BaseAddress = new Uri("http://localhost"); - } - - public HttpClient Client { get; } - - public HttpClient CreateClient(string baseAddress) - { - var client = _server.CreateClient(); - client.BaseAddress = new Uri(baseAddress); - - return client; - } - - public void Dispose() - { - Client.Dispose(); - _server.Dispose(); - } + Client.Dispose(); + _server.Dispose(); } } diff --git a/src/Http/Routing/test/FunctionalTests/WebHostBuilderExtensionsTest.cs b/src/Http/Routing/test/FunctionalTests/WebHostBuilderExtensionsTest.cs index be75c72910..9d83a978c3 100644 --- a/src/Http/Routing/test/FunctionalTests/WebHostBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/FunctionalTests/WebHostBuilderExtensionsTest.cs @@ -6,23 +6,23 @@ using System.IO; using System.Net; using System.Net.Http; using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; -using Xunit; using Microsoft.Extensions.Hosting; +using Xunit; -namespace Microsoft.AspNetCore.Routing.FunctionalTests +namespace Microsoft.AspNetCore.Routing.FunctionalTests; + +public class WebHostBuilderExtensionsTest { - public class WebHostBuilderExtensionsTest + public static TheoryData, HttpRequestMessage, string> MatchesRequest { - public static TheoryData, HttpRequestMessage, string> MatchesRequest + get { - get - { - return new TheoryData, HttpRequestMessage, string>() + return new TheoryData, HttpRequestMessage, string>() { { (rb) => rb.MapGet("greeting/{name}", (req, resp, routeData) => resp.WriteAsync($"Hello! {routeData.Values["name"]}")), @@ -72,38 +72,37 @@ namespace Microsoft.AspNetCore.Routing.FunctionalTests "James Biography" }, }; - } } + } - [Theory] - [MemberData(nameof(MatchesRequest))] - public async Task UseRouter_MapGet_MatchesRequest(Action routeBuilder, HttpRequestMessage request, string expected) - { - // Arrange - using var host = new HostBuilder() - .ConfigureWebHost(webhostbuilder => - { - webhostbuilder - .Configure(app => - { - app.UseRouter(routeBuilder); - }) - .UseTestServer(); - }) - .ConfigureServices(services => services.AddRouting()) - .Build(); + [Theory] + [MemberData(nameof(MatchesRequest))] + public async Task UseRouter_MapGet_MatchesRequest(Action routeBuilder, HttpRequestMessage request, string expected) + { + // Arrange + using var host = new HostBuilder() + .ConfigureWebHost(webhostbuilder => + { + webhostbuilder + .Configure(app => + { + app.UseRouter(routeBuilder); + }) + .UseTestServer(); + }) + .ConfigureServices(services => services.AddRouting()) + .Build(); - var testServer = host.GetTestServer(); - await host.StartAsync(); - var client = testServer.CreateClient(); + var testServer = host.GetTestServer(); + await host.StartAsync(); + var client = testServer.CreateClient(); - // Act - var response = await client.SendAsync(request); + // Act + var response = await client.SendAsync(request); - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var actual = await response.Content.ReadAsStringAsync(); - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var actual = await response.Content.ReadAsStringAsync(); + Assert.Equal(expected, actual); } } diff --git a/src/Http/Routing/test/UnitTests/Builder/EndpointRoutingApplicationBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/EndpointRoutingApplicationBuilderExtensionsTest.cs index ec9c88e9e1..79f597390e 100644 --- a/src/Http/Routing/test/UnitTests/Builder/EndpointRoutingApplicationBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/EndpointRoutingApplicationBuilderExtensionsTest.cs @@ -16,357 +16,356 @@ using Microsoft.Extensions.Options; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +public class EndpointRoutingApplicationBuilderExtensionsTest { - public class EndpointRoutingApplicationBuilderExtensionsTest + [Fact] + public void UseRouting_ServicesNotRegistered_Throws() { - [Fact] - public void UseRouting_ServicesNotRegistered_Throws() - { - // Arrange - var app = new ApplicationBuilder(Mock.Of()); - - // Act - var ex = Assert.Throws(() => app.UseRouting()); - - // Assert - Assert.Equal( - "Unable to find the required services. " + - "Please add all the required services by calling 'IServiceCollection.AddRouting' " + - "inside the call to 'ConfigureServices(...)' in the application startup code.", - ex.Message); - } - - [Fact] - public void UseEndpoint_ServicesNotRegistered_Throws() - { - // Arrange - var app = new ApplicationBuilder(Mock.Of()); - - // Act - var ex = Assert.Throws(() => app.UseEndpoints(endpoints => { })); - - // Assert - Assert.Equal( - "Unable to find the required services. " + - "Please add all the required services by calling 'IServiceCollection.AddRouting' " + - "inside the call to 'ConfigureServices(...)' in the application startup code.", - ex.Message); - } - - [Fact] - public async Task UseRouting_ServicesRegistered_NoMatch_DoesNotSetFeature() - { - // Arrange - var services = CreateServices(); - - var app = new ApplicationBuilder(services); - - app.UseRouting(); + // Arrange + var app = new ApplicationBuilder(Mock.Of()); + + // Act + var ex = Assert.Throws(() => app.UseRouting()); + + // Assert + Assert.Equal( + "Unable to find the required services. " + + "Please add all the required services by calling 'IServiceCollection.AddRouting' " + + "inside the call to 'ConfigureServices(...)' in the application startup code.", + ex.Message); + } - var appFunc = app.Build(); - var httpContext = new DefaultHttpContext(); + [Fact] + public void UseEndpoint_ServicesNotRegistered_Throws() + { + // Arrange + var app = new ApplicationBuilder(Mock.Of()); + + // Act + var ex = Assert.Throws(() => app.UseEndpoints(endpoints => { })); + + // Assert + Assert.Equal( + "Unable to find the required services. " + + "Please add all the required services by calling 'IServiceCollection.AddRouting' " + + "inside the call to 'ConfigureServices(...)' in the application startup code.", + ex.Message); + } - // Act - await appFunc(httpContext); + [Fact] + public async Task UseRouting_ServicesRegistered_NoMatch_DoesNotSetFeature() + { + // Arrange + var services = CreateServices(); - // Assert - Assert.Null(httpContext.Features.Get()); - } + var app = new ApplicationBuilder(services); - [Fact] - public async Task UseRouting_ServicesRegistered_Match_DoesNotSetsFeature() - { - // Arrange - var endpoint = new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse("{*p}"), - 0, - EndpointMetadataCollection.Empty, - "Test"); + app.UseRouting(); - var services = CreateServices(); + var appFunc = app.Build(); + var httpContext = new DefaultHttpContext(); - var app = new ApplicationBuilder(services); + // Act + await appFunc(httpContext); - app.UseRouting(); + // Assert + Assert.Null(httpContext.Features.Get()); + } - app.UseEndpoints(endpoints => - { - endpoints.DataSources.Add(new DefaultEndpointDataSource(endpoint)); - }); + [Fact] + public async Task UseRouting_ServicesRegistered_Match_DoesNotSetsFeature() + { + // Arrange + var endpoint = new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse("{*p}"), + 0, + EndpointMetadataCollection.Empty, + "Test"); - var appFunc = app.Build(); - var httpContext = new DefaultHttpContext(); + var services = CreateServices(); - // Act - await appFunc(httpContext); + var app = new ApplicationBuilder(services); - // Assert - var feature = httpContext.Features.Get(); - Assert.NotNull(feature); - Assert.Same(endpoint, httpContext.GetEndpoint()); - } + app.UseRouting(); - [Fact] - public void UseEndpoint_WithoutEndpointRoutingMiddleware_Throws() + app.UseEndpoints(endpoints => { - // Arrange - var services = CreateServices(); + endpoints.DataSources.Add(new DefaultEndpointDataSource(endpoint)); + }); - var app = new ApplicationBuilder(services); + var appFunc = app.Build(); + var httpContext = new DefaultHttpContext(); - // Act - var ex = Assert.Throws(() => app.UseEndpoints(endpoints => { })); + // Act + await appFunc(httpContext); - // Assert - Assert.Equal( - "EndpointRoutingMiddleware matches endpoints setup by EndpointMiddleware and so must be added to the request " + - "execution pipeline before EndpointMiddleware. " + - "Please add EndpointRoutingMiddleware by calling 'IApplicationBuilder.UseRouting' " + - "inside the call to 'Configure(...)' in the application startup code.", - ex.Message); - } + // Assert + var feature = httpContext.Features.Get(); + Assert.NotNull(feature); + Assert.Same(endpoint, httpContext.GetEndpoint()); + } - [Fact] - public void UseEndpoint_WithApplicationBuilderMismatch_Throws() - { - // Arrange - var services = CreateServices(); + [Fact] + public void UseEndpoint_WithoutEndpointRoutingMiddleware_Throws() + { + // Arrange + var services = CreateServices(); - var app = new ApplicationBuilder(services); + var app = new ApplicationBuilder(services); - app.UseRouting(); + // Act + var ex = Assert.Throws(() => app.UseEndpoints(endpoints => { })); - // Act - var ex = Assert.Throws(() => app.Map("/Test", b => b.UseEndpoints(endpoints => { }))); + // Assert + Assert.Equal( + "EndpointRoutingMiddleware matches endpoints setup by EndpointMiddleware and so must be added to the request " + + "execution pipeline before EndpointMiddleware. " + + "Please add EndpointRoutingMiddleware by calling 'IApplicationBuilder.UseRouting' " + + "inside the call to 'Configure(...)' in the application startup code.", + ex.Message); + } - // Assert - Assert.Equal( - "The EndpointRoutingMiddleware and EndpointMiddleware must be added to the same IApplicationBuilder instance. " + - "To use Endpoint Routing with 'Map(...)', make sure to call 'IApplicationBuilder.UseRouting' before " + - "'IApplicationBuilder.UseEndpoints' for each branch of the middleware pipeline.", - ex.Message); - } + [Fact] + public void UseEndpoint_WithApplicationBuilderMismatch_Throws() + { + // Arrange + var services = CreateServices(); - [Fact] - public async Task UseEndpoint_ServicesRegisteredAndEndpointRoutingRegistered_NoMatch_DoesNotSetFeature() - { - // Arrange - var services = CreateServices(); + var app = new ApplicationBuilder(services); - var app = new ApplicationBuilder(services); + app.UseRouting(); - app.UseRouting(); - app.UseEndpoints(endpoints => { }); + // Act + var ex = Assert.Throws(() => app.Map("/Test", b => b.UseEndpoints(endpoints => { }))); - var appFunc = app.Build(); - var httpContext = new DefaultHttpContext(); + // Assert + Assert.Equal( + "The EndpointRoutingMiddleware and EndpointMiddleware must be added to the same IApplicationBuilder instance. " + + "To use Endpoint Routing with 'Map(...)', make sure to call 'IApplicationBuilder.UseRouting' before " + + "'IApplicationBuilder.UseEndpoints' for each branch of the middleware pipeline.", + ex.Message); + } - // Act - await appFunc(httpContext); + [Fact] + public async Task UseEndpoint_ServicesRegisteredAndEndpointRoutingRegistered_NoMatch_DoesNotSetFeature() + { + // Arrange + var services = CreateServices(); - // Assert - Assert.Null(httpContext.Features.Get()); - } + var app = new ApplicationBuilder(services); - [Fact] - public void UseEndpoints_CallWithBuilder_SetsEndpointDataSource() - { - // Arrange - var matcherEndpointDataSources = new List(); - var matcherFactoryMock = new Mock(); - matcherFactoryMock - .Setup(m => m.CreateMatcher(It.IsAny())) - .Callback((EndpointDataSource arg) => - { - matcherEndpointDataSources.Add(arg); - }) - .Returns(new TestMatcher(false)); - - var services = CreateServices(matcherFactoryMock.Object); - - var app = new ApplicationBuilder(services); - - // Act - app.UseRouting(); - app.UseEndpoints(builder => - { - builder.Map("/1", d => null).WithDisplayName("Test endpoint 1"); - builder.Map("/2", d => null).WithDisplayName("Test endpoint 2"); - }); + app.UseRouting(); + app.UseEndpoints(endpoints => { }); - app.UseRouting(); - app.UseEndpoints(builder => - { - builder.Map("/3", d => null).WithDisplayName("Test endpoint 3"); - builder.Map("/4", d => null).WithDisplayName("Test endpoint 4"); - }); + var appFunc = app.Build(); + var httpContext = new DefaultHttpContext(); - // This triggers the middleware to be created and the matcher factory to be called - // with the datasource we want to test - var requestDelegate = app.Build(); - requestDelegate(new DefaultHttpContext()); + // Act + await appFunc(httpContext); - // Assert - Assert.Equal(2, matcherEndpointDataSources.Count); + // Assert + Assert.Null(httpContext.Features.Get()); + } - // each UseRouter has its own data source collection - Assert.Collection(matcherEndpointDataSources[0].Endpoints, - e => Assert.Equal("Test endpoint 1", e.DisplayName), - e => Assert.Equal("Test endpoint 2", e.DisplayName)); + [Fact] + public void UseEndpoints_CallWithBuilder_SetsEndpointDataSource() + { + // Arrange + var matcherEndpointDataSources = new List(); + var matcherFactoryMock = new Mock(); + matcherFactoryMock + .Setup(m => m.CreateMatcher(It.IsAny())) + .Callback((EndpointDataSource arg) => + { + matcherEndpointDataSources.Add(arg); + }) + .Returns(new TestMatcher(false)); - Assert.Collection(matcherEndpointDataSources[1].Endpoints, - e => Assert.Equal("Test endpoint 3", e.DisplayName), - e => Assert.Equal("Test endpoint 4", e.DisplayName)); + var services = CreateServices(matcherFactoryMock.Object); - var compositeEndpointBuilder = services.GetRequiredService(); + var app = new ApplicationBuilder(services); - // Global collection has all endpoints - Assert.Collection(compositeEndpointBuilder.Endpoints, - e => Assert.Equal("Test endpoint 1", e.DisplayName), - e => Assert.Equal("Test endpoint 2", e.DisplayName), - e => Assert.Equal("Test endpoint 3", e.DisplayName), - e => Assert.Equal("Test endpoint 4", e.DisplayName)); - } + // Act + app.UseRouting(); + app.UseEndpoints(builder => + { + builder.Map("/1", d => null).WithDisplayName("Test endpoint 1"); + builder.Map("/2", d => null).WithDisplayName("Test endpoint 2"); + }); - // Verifies that it's possible to use endpoints and map together. - [Fact] - public void UseEndpoints_CallWithBuilder_SetsEndpointDataSource_WithMap() + app.UseRouting(); + app.UseEndpoints(builder => { - // Arrange - var matcherEndpointDataSources = new List(); - var matcherFactoryMock = new Mock(); - matcherFactoryMock - .Setup(m => m.CreateMatcher(It.IsAny())) - .Callback((EndpointDataSource arg) => - { - matcherEndpointDataSources.Add(arg); - }) - .Returns(new TestMatcher(false)); + builder.Map("/3", d => null).WithDisplayName("Test endpoint 3"); + builder.Map("/4", d => null).WithDisplayName("Test endpoint 4"); + }); + + // This triggers the middleware to be created and the matcher factory to be called + // with the datasource we want to test + var requestDelegate = app.Build(); + requestDelegate(new DefaultHttpContext()); + + // Assert + Assert.Equal(2, matcherEndpointDataSources.Count); + + // each UseRouter has its own data source collection + Assert.Collection(matcherEndpointDataSources[0].Endpoints, + e => Assert.Equal("Test endpoint 1", e.DisplayName), + e => Assert.Equal("Test endpoint 2", e.DisplayName)); + + Assert.Collection(matcherEndpointDataSources[1].Endpoints, + e => Assert.Equal("Test endpoint 3", e.DisplayName), + e => Assert.Equal("Test endpoint 4", e.DisplayName)); + + var compositeEndpointBuilder = services.GetRequiredService(); + + // Global collection has all endpoints + Assert.Collection(compositeEndpointBuilder.Endpoints, + e => Assert.Equal("Test endpoint 1", e.DisplayName), + e => Assert.Equal("Test endpoint 2", e.DisplayName), + e => Assert.Equal("Test endpoint 3", e.DisplayName), + e => Assert.Equal("Test endpoint 4", e.DisplayName)); + } - var services = CreateServices(matcherFactoryMock.Object); + // Verifies that it's possible to use endpoints and map together. + [Fact] + public void UseEndpoints_CallWithBuilder_SetsEndpointDataSource_WithMap() + { + // Arrange + var matcherEndpointDataSources = new List(); + var matcherFactoryMock = new Mock(); + matcherFactoryMock + .Setup(m => m.CreateMatcher(It.IsAny())) + .Callback((EndpointDataSource arg) => + { + matcherEndpointDataSources.Add(arg); + }) + .Returns(new TestMatcher(false)); - var app = new ApplicationBuilder(services); + var services = CreateServices(matcherFactoryMock.Object); - // Act - app.UseRouting(); + var app = new ApplicationBuilder(services); - app.Map("/foo", b => - { - b.UseRouting(); - b.UseEndpoints(builder => - { - builder.Map("/1", d => null).WithDisplayName("Test endpoint 1"); - builder.Map("/2", d => null).WithDisplayName("Test endpoint 2"); - }); - }); + // Act + app.UseRouting(); - app.UseEndpoints(builder => + app.Map("/foo", b => + { + b.UseRouting(); + b.UseEndpoints(builder => { - builder.Map("/3", d => null).WithDisplayName("Test endpoint 3"); - builder.Map("/4", d => null).WithDisplayName("Test endpoint 4"); + builder.Map("/1", d => null).WithDisplayName("Test endpoint 1"); + builder.Map("/2", d => null).WithDisplayName("Test endpoint 2"); }); + }); - // This triggers the middleware to be created and the matcher factory to be called - // with the datasource we want to test - var requestDelegate = app.Build(); - requestDelegate(new DefaultHttpContext()); - requestDelegate(new DefaultHttpContext() { Request = { Path = "/Foo", }, }); - - // Assert - Assert.Equal(2, matcherEndpointDataSources.Count); + app.UseEndpoints(builder => + { + builder.Map("/3", d => null).WithDisplayName("Test endpoint 3"); + builder.Map("/4", d => null).WithDisplayName("Test endpoint 4"); + }); + + // This triggers the middleware to be created and the matcher factory to be called + // with the datasource we want to test + var requestDelegate = app.Build(); + requestDelegate(new DefaultHttpContext()); + requestDelegate(new DefaultHttpContext() { Request = { Path = "/Foo", }, }); + + // Assert + Assert.Equal(2, matcherEndpointDataSources.Count); + + // Each UseRouter has its own data source + Assert.Collection(matcherEndpointDataSources[1].Endpoints, // app.UseRouter + e => Assert.Equal("Test endpoint 1", e.DisplayName), + e => Assert.Equal("Test endpoint 2", e.DisplayName)); + + Assert.Collection(matcherEndpointDataSources[0].Endpoints, // b.UseRouter + e => Assert.Equal("Test endpoint 3", e.DisplayName), + e => Assert.Equal("Test endpoint 4", e.DisplayName)); + + var compositeEndpointBuilder = services.GetRequiredService(); + + // Global middleware has all endpoints + Assert.Collection(compositeEndpointBuilder.Endpoints, + e => Assert.Equal("Test endpoint 1", e.DisplayName), + e => Assert.Equal("Test endpoint 2", e.DisplayName), + e => Assert.Equal("Test endpoint 3", e.DisplayName), + e => Assert.Equal("Test endpoint 4", e.DisplayName)); + } - // Each UseRouter has its own data source - Assert.Collection(matcherEndpointDataSources[1].Endpoints, // app.UseRouter - e => Assert.Equal("Test endpoint 1", e.DisplayName), - e => Assert.Equal("Test endpoint 2", e.DisplayName)); + [Fact] + public void UseEndpoints_WithGlobalEndpointRouteBuilderHasRoutes() + { + // Arrange + var services = CreateServices(); - Assert.Collection(matcherEndpointDataSources[0].Endpoints, // b.UseRouter - e => Assert.Equal("Test endpoint 3", e.DisplayName), - e => Assert.Equal("Test endpoint 4", e.DisplayName)); + var app = new ApplicationBuilder(services); - var compositeEndpointBuilder = services.GetRequiredService(); + var mockRouteBuilder = new Mock(); + mockRouteBuilder.Setup(m => m.DataSources).Returns(new List()); - // Global middleware has all endpoints - Assert.Collection(compositeEndpointBuilder.Endpoints, - e => Assert.Equal("Test endpoint 1", e.DisplayName), - e => Assert.Equal("Test endpoint 2", e.DisplayName), - e => Assert.Equal("Test endpoint 3", e.DisplayName), - e => Assert.Equal("Test endpoint 4", e.DisplayName)); - } + var routeBuilder = mockRouteBuilder.Object; + app.Properties.Add("__GlobalEndpointRouteBuilder", routeBuilder); + app.UseRouting(); - [Fact] - public void UseEndpoints_WithGlobalEndpointRouteBuilderHasRoutes() + app.UseEndpoints(endpoints => { - // Arrange - var services = CreateServices(); + endpoints.Map("/1", d => Task.CompletedTask).WithDisplayName("Test endpoint 1"); + }); - var app = new ApplicationBuilder(services); + var requestDelegate = app.Build(); - var mockRouteBuilder = new Mock(); - mockRouteBuilder.Setup(m => m.DataSources).Returns(new List()); + var endpointDataSource = Assert.Single(mockRouteBuilder.Object.DataSources); + Assert.Collection(endpointDataSource.Endpoints, + e => Assert.Equal("Test endpoint 1", e.DisplayName)); - var routeBuilder = mockRouteBuilder.Object; - app.Properties.Add("__GlobalEndpointRouteBuilder", routeBuilder); - app.UseRouting(); - - app.UseEndpoints(endpoints => - { - endpoints.Map("/1", d => Task.CompletedTask).WithDisplayName("Test endpoint 1"); - }); - - var requestDelegate = app.Build(); + var routeOptions = app.ApplicationServices.GetRequiredService>(); + Assert.Equal(mockRouteBuilder.Object.DataSources, routeOptions.Value.EndpointDataSources); + } - var endpointDataSource = Assert.Single(mockRouteBuilder.Object.DataSources); - Assert.Collection(endpointDataSource.Endpoints, - e => Assert.Equal("Test endpoint 1", e.DisplayName)); + [Fact] + public void UseRouting_SetsEndpointRouteBuilder_IfGlobalOneExists() + { + // Arrange + var services = CreateServices(); - var routeOptions = app.ApplicationServices.GetRequiredService>(); - Assert.Equal(mockRouteBuilder.Object.DataSources, routeOptions.Value.EndpointDataSources); - } + var app = new ApplicationBuilder(services); - [Fact] - public void UseRouting_SetsEndpointRouteBuilder_IfGlobalOneExists() - { - // Arrange - var services = CreateServices(); + var routeBuilder = new Mock().Object; + app.Properties.Add("__GlobalEndpointRouteBuilder", routeBuilder); + app.UseRouting(); - var app = new ApplicationBuilder(services); + Assert.True(app.Properties.TryGetValue("__EndpointRouteBuilder", out var local)); + Assert.True(app.Properties.TryGetValue("__GlobalEndpointRouteBuilder", out var global)); + Assert.Same(local, global); + } - var routeBuilder = new Mock().Object; - app.Properties.Add("__GlobalEndpointRouteBuilder", routeBuilder); - app.UseRouting(); + private IServiceProvider CreateServices() + { + return CreateServices(matcherFactory: null); + } - Assert.True(app.Properties.TryGetValue("__EndpointRouteBuilder", out var local)); - Assert.True(app.Properties.TryGetValue("__GlobalEndpointRouteBuilder", out var global)); - Assert.Same(local, global); - } + private IServiceProvider CreateServices(MatcherFactory matcherFactory) + { + var services = new ServiceCollection(); - private IServiceProvider CreateServices() + if (matcherFactory != null) { - return CreateServices(matcherFactory: null); + services.AddSingleton(matcherFactory); } - private IServiceProvider CreateServices(MatcherFactory matcherFactory) - { - var services = new ServiceCollection(); + services.AddLogging(); + services.AddOptions(); + services.AddRouting(); + var listener = new DiagnosticListener("Microsoft.AspNetCore"); + services.AddSingleton(listener); + services.AddSingleton(listener); - if (matcherFactory != null) - { - services.AddSingleton(matcherFactory); - } - - services.AddLogging(); - services.AddOptions(); - services.AddRouting(); - var listener = new DiagnosticListener("Microsoft.AspNetCore"); - services.AddSingleton(listener); - services.AddSingleton(listener); + var serviceProvder = services.BuildServiceProvider(); - var serviceProvder = services.BuildServiceProvider(); - - return serviceProvder; - } + return serviceProvder; } } diff --git a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs index f9079850ea..40aae08767 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RequestDelegateEndpointRouteBuilderExtensionsTest.cs @@ -12,215 +12,214 @@ using Microsoft.AspNetCore.Routing.Patterns; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +public class RequestDelegateEndpointRouteBuilderExtensionsTest { - public class RequestDelegateEndpointRouteBuilderExtensionsTest + private ModelEndpointDataSource GetBuilderEndpointDataSource(IEndpointRouteBuilder endpointRouteBuilder) { - private ModelEndpointDataSource GetBuilderEndpointDataSource(IEndpointRouteBuilder endpointRouteBuilder) - { - return Assert.IsType(Assert.Single(endpointRouteBuilder.DataSources)); - } + return Assert.IsType(Assert.Single(endpointRouteBuilder.DataSources)); + } - private RouteEndpointBuilder GetRouteEndpointBuilder(IEndpointRouteBuilder endpointRouteBuilder) - { - return Assert.IsType(Assert.Single(GetBuilderEndpointDataSource(endpointRouteBuilder).EndpointBuilders)); - } + private RouteEndpointBuilder GetRouteEndpointBuilder(IEndpointRouteBuilder endpointRouteBuilder) + { + return Assert.IsType(Assert.Single(GetBuilderEndpointDataSource(endpointRouteBuilder).EndpointBuilders)); + } - public static object[][] MapMethods + public static object[][] MapMethods + { + get { - get - { - IEndpointConventionBuilder MapGet(IEndpointRouteBuilder routes, string template, RequestDelegate action) => - routes.MapGet(template, action); + IEndpointConventionBuilder MapGet(IEndpointRouteBuilder routes, string template, RequestDelegate action) => + routes.MapGet(template, action); - IEndpointConventionBuilder MapPost(IEndpointRouteBuilder routes, string template, RequestDelegate action) => - routes.MapPost(template, action); + IEndpointConventionBuilder MapPost(IEndpointRouteBuilder routes, string template, RequestDelegate action) => + routes.MapPost(template, action); - IEndpointConventionBuilder MapPut(IEndpointRouteBuilder routes, string template, RequestDelegate action) => - routes.MapPut(template, action); + IEndpointConventionBuilder MapPut(IEndpointRouteBuilder routes, string template, RequestDelegate action) => + routes.MapPut(template, action); - IEndpointConventionBuilder MapDelete(IEndpointRouteBuilder routes, string template, RequestDelegate action) => - routes.MapDelete(template, action); + IEndpointConventionBuilder MapDelete(IEndpointRouteBuilder routes, string template, RequestDelegate action) => + routes.MapDelete(template, action); - IEndpointConventionBuilder Map(IEndpointRouteBuilder routes, string template, RequestDelegate action) => - routes.Map(template, action); + IEndpointConventionBuilder Map(IEndpointRouteBuilder routes, string template, RequestDelegate action) => + routes.Map(template, action); - return new object[][] - { + return new object[][] + { new object[] { (Func)MapGet }, new object[] { (Func)MapPost }, new object[] { (Func)MapPut }, new object[] { (Func)MapDelete }, new object[] { (Func)Map }, - }; - } + }; } + } - [Fact] - public void MapEndpoint_StringPattern_BuildsEndpoint() - { - // Arrange - var builder = new DefaultEndpointRouteBuilder(Mock.Of()); - RequestDelegate requestDelegate = (d) => null; + [Fact] + public void MapEndpoint_StringPattern_BuildsEndpoint() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + RequestDelegate requestDelegate = (d) => null; - // Act - var endpointBuilder = builder.Map("/", requestDelegate); + // Act + var endpointBuilder = builder.Map("/", requestDelegate); - // Assert - var endpointBuilder1 = GetRouteEndpointBuilder(builder); + // Assert + var endpointBuilder1 = GetRouteEndpointBuilder(builder); - Assert.Equal(requestDelegate, endpointBuilder1.RequestDelegate); - Assert.Equal("/", endpointBuilder1.DisplayName); - Assert.Equal("/", endpointBuilder1.RoutePattern.RawText); - } + Assert.Equal(requestDelegate, endpointBuilder1.RequestDelegate); + Assert.Equal("/", endpointBuilder1.DisplayName); + Assert.Equal("/", endpointBuilder1.RoutePattern.RawText); + } - [Fact] - public void MapEndpoint_TypedPattern_BuildsEndpoint() - { - // Arrange - var builder = new DefaultEndpointRouteBuilder(Mock.Of()); - RequestDelegate requestDelegate = (d) => null; + [Fact] + public void MapEndpoint_TypedPattern_BuildsEndpoint() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + RequestDelegate requestDelegate = (d) => null; - // Act - var endpointBuilder = builder.Map(RoutePatternFactory.Parse("/"), requestDelegate); + // Act + var endpointBuilder = builder.Map(RoutePatternFactory.Parse("/"), requestDelegate); - // Assert - var endpointBuilder1 = GetRouteEndpointBuilder(builder); + // Assert + var endpointBuilder1 = GetRouteEndpointBuilder(builder); - Assert.Equal(requestDelegate, endpointBuilder1.RequestDelegate); - Assert.Equal("/", endpointBuilder1.DisplayName); - Assert.Equal("/", endpointBuilder1.RoutePattern.RawText); - } + Assert.Equal(requestDelegate, endpointBuilder1.RequestDelegate); + Assert.Equal("/", endpointBuilder1.DisplayName); + Assert.Equal("/", endpointBuilder1.RoutePattern.RawText); + } - [Fact] - public void MapEndpoint_AttributesCollectedAsMetadata() - { - // Arrange - var builder = new DefaultEndpointRouteBuilder(Mock.Of()); - - // Act - var endpointBuilder = builder.Map(RoutePatternFactory.Parse("/"), Handle); - - // Assert - var endpointBuilder1 = GetRouteEndpointBuilder(builder); - Assert.Equal("/", endpointBuilder1.RoutePattern.RawText); - Assert.Equal(2, endpointBuilder1.Metadata.Count); - Assert.IsType(endpointBuilder1.Metadata[0]); - Assert.IsType(endpointBuilder1.Metadata[1]); - } + [Fact] + public void MapEndpoint_AttributesCollectedAsMetadata() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + + // Act + var endpointBuilder = builder.Map(RoutePatternFactory.Parse("/"), Handle); + + // Assert + var endpointBuilder1 = GetRouteEndpointBuilder(builder); + Assert.Equal("/", endpointBuilder1.RoutePattern.RawText); + Assert.Equal(2, endpointBuilder1.Metadata.Count); + Assert.IsType(endpointBuilder1.Metadata[0]); + Assert.IsType(endpointBuilder1.Metadata[1]); + } - [Fact] - public void MapEndpoint_GeneratedDelegateWorks() - { - // Arrange - var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + [Fact] + public void MapEndpoint_GeneratedDelegateWorks() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); - Expression handler = context => Task.CompletedTask; + Expression handler = context => Task.CompletedTask; - // Act - var endpointBuilder = builder.Map(RoutePatternFactory.Parse("/"), handler.Compile()); + // Act + var endpointBuilder = builder.Map(RoutePatternFactory.Parse("/"), handler.Compile()); - // Assert - var endpointBuilder1 = GetRouteEndpointBuilder(builder); - Assert.Equal("/", endpointBuilder1.RoutePattern.RawText); - } + // Assert + var endpointBuilder1 = GetRouteEndpointBuilder(builder); + Assert.Equal("/", endpointBuilder1.RoutePattern.RawText); + } - [Fact] - public void MapEndpoint_PrecedenceOfMetadata_BuilderMetadataReturned() - { - // Arrange - var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + [Fact] + public void MapEndpoint_PrecedenceOfMetadata_BuilderMetadataReturned() + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); - // Act - var endpointBuilder = builder.MapMethods("/", new[] { "METHOD" }, HandleHttpMetdata); - endpointBuilder.WithMetadata(new HttpMethodMetadata(new[] { "BUILDER" })); + // Act + var endpointBuilder = builder.MapMethods("/", new[] { "METHOD" }, HandleHttpMetdata); + endpointBuilder.WithMetadata(new HttpMethodMetadata(new[] { "BUILDER" })); - // Assert - var dataSource = Assert.Single(builder.DataSources); - var endpoint = Assert.Single(dataSource.Endpoints); + // Assert + var dataSource = Assert.Single(builder.DataSources); + var endpoint = Assert.Single(dataSource.Endpoints); - Assert.Equal(3, endpoint.Metadata.Count); - Assert.Equal("ATTRIBUTE", GetMethod(endpoint.Metadata[0])); - Assert.Equal("METHOD", GetMethod(endpoint.Metadata[1])); - Assert.Equal("BUILDER", GetMethod(endpoint.Metadata[2])); + Assert.Equal(3, endpoint.Metadata.Count); + Assert.Equal("ATTRIBUTE", GetMethod(endpoint.Metadata[0])); + Assert.Equal("METHOD", GetMethod(endpoint.Metadata[1])); + Assert.Equal("BUILDER", GetMethod(endpoint.Metadata[2])); - Assert.Equal("BUILDER", endpoint.Metadata.GetMetadata().HttpMethods.Single()); + Assert.Equal("BUILDER", endpoint.Metadata.GetMetadata().HttpMethods.Single()); - string GetMethod(object metadata) - { - var httpMethodMetadata = Assert.IsAssignableFrom(metadata); - return Assert.Single(httpMethodMetadata.HttpMethods); - } - } - - [Theory] - [MemberData(nameof(MapMethods))] - public void Map_EndpointMetadataNotDuplicated(Func map) + string GetMethod(object metadata) { - // Arrange - var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + var httpMethodMetadata = Assert.IsAssignableFrom(metadata); + return Assert.Single(httpMethodMetadata.HttpMethods); + } + } - // Act - var endpointBuilder = map(builder, "/", context => Task.CompletedTask).WithMetadata(new EndpointNameMetadata("MapMe")); + [Theory] + [MemberData(nameof(MapMethods))] + public void Map_EndpointMetadataNotDuplicated(Func map) + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); - // Assert - var ds = GetBuilderEndpointDataSource(builder); + // Act + var endpointBuilder = map(builder, "/", context => Task.CompletedTask).WithMetadata(new EndpointNameMetadata("MapMe")); - _ = ds.Endpoints; - _ = ds.Endpoints; - _ = ds.Endpoints; + // Assert + var ds = GetBuilderEndpointDataSource(builder); - Assert.Single(ds.Endpoints); - var endpoint = ds.Endpoints.Single(); + _ = ds.Endpoints; + _ = ds.Endpoints; + _ = ds.Endpoints; - Assert.Single(endpoint.Metadata.GetOrderedMetadata()); - } + Assert.Single(ds.Endpoints); + var endpoint = ds.Endpoints.Single(); - [Theory] - [MemberData(nameof(MapMethods))] - public void AddingMetadataAfterBuildingEndpointThrows(Func map) - { - // Arrange - var builder = new DefaultEndpointRouteBuilder(Mock.Of()); + Assert.Single(endpoint.Metadata.GetOrderedMetadata()); + } - // Act - var endpointBuilder = map(builder, "/", context => Task.CompletedTask).WithMetadata(new EndpointNameMetadata("MapMe")); + [Theory] + [MemberData(nameof(MapMethods))] + public void AddingMetadataAfterBuildingEndpointThrows(Func map) + { + // Arrange + var builder = new DefaultEndpointRouteBuilder(Mock.Of()); - // Assert - var ds = GetBuilderEndpointDataSource(builder); + // Act + var endpointBuilder = map(builder, "/", context => Task.CompletedTask).WithMetadata(new EndpointNameMetadata("MapMe")); - var endpoint = Assert.Single(ds.Endpoints); + // Assert + var ds = GetBuilderEndpointDataSource(builder); - Assert.Single(endpoint.Metadata.GetOrderedMetadata()); + var endpoint = Assert.Single(ds.Endpoints); - Assert.Throws(() => endpointBuilder.WithMetadata(new RouteNameMetadata("Foo"))); - } + Assert.Single(endpoint.Metadata.GetOrderedMetadata()); - [Attribute1] - [Attribute2] - private static Task Handle(HttpContext context) => Task.CompletedTask; + Assert.Throws(() => endpointBuilder.WithMetadata(new RouteNameMetadata("Foo"))); + } - [HttpMethod("ATTRIBUTE")] - private static Task HandleHttpMetdata(HttpContext context) => Task.CompletedTask; + [Attribute1] + [Attribute2] + private static Task Handle(HttpContext context) => Task.CompletedTask; - private class HttpMethodAttribute : Attribute, IHttpMethodMetadata - { - public bool AcceptCorsPreflight => false; + [HttpMethod("ATTRIBUTE")] + private static Task HandleHttpMetdata(HttpContext context) => Task.CompletedTask; - public IReadOnlyList HttpMethods { get; } + private class HttpMethodAttribute : Attribute, IHttpMethodMetadata + { + public bool AcceptCorsPreflight => false; - public HttpMethodAttribute(params string[] httpMethods) - { - HttpMethods = httpMethods; - } - } + public IReadOnlyList HttpMethods { get; } - private class Attribute1 : Attribute + public HttpMethodAttribute(params string[] httpMethods) { + HttpMethods = httpMethods; } + } - private class Attribute2 : Attribute - { - } + private class Attribute1 : Attribute + { + } + + private class Attribute2 : Attribute + { } } diff --git a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs index e485e0b3fd..e942596cba 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RouteHandlerEndpointRouteBuilderExtensionsTest.cs @@ -10,224 +10,224 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +public class RouteHandlerEndpointRouteBuilderExtensionsTest { - public class RouteHandlerEndpointRouteBuilderExtensionsTest + private ModelEndpointDataSource GetBuilderEndpointDataSource(IEndpointRouteBuilder endpointRouteBuilder) { - private ModelEndpointDataSource GetBuilderEndpointDataSource(IEndpointRouteBuilder endpointRouteBuilder) - { - return Assert.IsType(Assert.Single(endpointRouteBuilder.DataSources)); - } + return Assert.IsType(Assert.Single(endpointRouteBuilder.DataSources)); + } - private RouteEndpointBuilder GetRouteEndpointBuilder(IEndpointRouteBuilder endpointRouteBuilder) - { - return Assert.IsType(Assert.Single(GetBuilderEndpointDataSource(endpointRouteBuilder).EndpointBuilders)); - } + private RouteEndpointBuilder GetRouteEndpointBuilder(IEndpointRouteBuilder endpointRouteBuilder) + { + return Assert.IsType(Assert.Single(GetBuilderEndpointDataSource(endpointRouteBuilder).EndpointBuilders)); + } - public static object?[]?[] MapMethods + public static object?[]?[] MapMethods + { + get { - get - { - IEndpointConventionBuilder MapGet(IEndpointRouteBuilder routes, string template, Delegate action) => - routes.MapGet(template, action); + IEndpointConventionBuilder MapGet(IEndpointRouteBuilder routes, string template, Delegate action) => + routes.MapGet(template, action); - IEndpointConventionBuilder MapPost(IEndpointRouteBuilder routes, string template, Delegate action) => - routes.MapPost(template, action); + IEndpointConventionBuilder MapPost(IEndpointRouteBuilder routes, string template, Delegate action) => + routes.MapPost(template, action); - IEndpointConventionBuilder MapPut(IEndpointRouteBuilder routes, string template, Delegate action) => - routes.MapPut(template, action); + IEndpointConventionBuilder MapPut(IEndpointRouteBuilder routes, string template, Delegate action) => + routes.MapPut(template, action); - IEndpointConventionBuilder MapDelete(IEndpointRouteBuilder routes, string template, Delegate action) => - routes.MapDelete(template, action); + IEndpointConventionBuilder MapDelete(IEndpointRouteBuilder routes, string template, Delegate action) => + routes.MapDelete(template, action); - IEndpointConventionBuilder MapPatch(IEndpointRouteBuilder routes, string template, Delegate action) => - routes.MapPatch(template, action); + IEndpointConventionBuilder MapPatch(IEndpointRouteBuilder routes, string template, Delegate action) => + routes.MapPatch(template, action); - IEndpointConventionBuilder Map(IEndpointRouteBuilder routes, string template, Delegate action) => - routes.Map(template, action); + IEndpointConventionBuilder Map(IEndpointRouteBuilder routes, string template, Delegate action) => + routes.Map(template, action); - return new object?[]?[] - { + return new object?[]?[] + { new object?[] { (Func)MapGet, "GET" }, new object?[] { (Func)MapPost, "POST" }, new object?[] { (Func)MapPut, "PUT" }, new object?[] { (Func)MapDelete, "DELETE" }, new object?[] { (Func)MapPatch, "PATCH" }, new object?[] { (Func)Map, null }, - }; - } + }; } + } - [Fact] - public void MapEndpoint_PrecedenceOfMetadata_BuilderMetadataReturned() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + [Fact] + public void MapEndpoint_PrecedenceOfMetadata_BuilderMetadataReturned() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - [HttpMethod("ATTRIBUTE")] - void TestAction() - { - } + [HttpMethod("ATTRIBUTE")] + void TestAction() + { + } - var endpointBuilder = builder.MapMethods("/", new[] { "METHOD" }, (Action)TestAction); - endpointBuilder.WithMetadata(new HttpMethodMetadata(new[] { "BUILDER" })); + var endpointBuilder = builder.MapMethods("/", new[] { "METHOD" }, (Action)TestAction); + endpointBuilder.WithMetadata(new HttpMethodMetadata(new[] { "BUILDER" })); - var dataSource = Assert.Single(builder.DataSources); - var endpoint = Assert.Single(dataSource.Endpoints); + var dataSource = Assert.Single(builder.DataSources); + var endpoint = Assert.Single(dataSource.Endpoints); - var metadataArray = endpoint.Metadata.OfType().ToArray(); + var metadataArray = endpoint.Metadata.OfType().ToArray(); - static string GetMethod(IHttpMethodMetadata metadata) => Assert.Single(metadata.HttpMethods); + static string GetMethod(IHttpMethodMetadata metadata) => Assert.Single(metadata.HttpMethods); - Assert.Equal(3, metadataArray.Length); - Assert.Equal("ATTRIBUTE", GetMethod(metadataArray[0])); - Assert.Equal("METHOD", GetMethod(metadataArray[1])); - Assert.Equal("BUILDER", GetMethod(metadataArray[2])); + Assert.Equal(3, metadataArray.Length); + Assert.Equal("ATTRIBUTE", GetMethod(metadataArray[0])); + Assert.Equal("METHOD", GetMethod(metadataArray[1])); + Assert.Equal("BUILDER", GetMethod(metadataArray[2])); - Assert.Equal("BUILDER", endpoint.Metadata.GetMetadata()!.HttpMethods.Single()); - } + Assert.Equal("BUILDER", endpoint.Metadata.GetMetadata()!.HttpMethods.Single()); + } - [Fact] - public void MapGet_BuildsEndpointWithCorrectMethod() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - _ = builder.MapGet("/", () => { }); + [Fact] + public void MapGet_BuildsEndpointWithCorrectMethod() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = builder.MapGet("/", () => { }); - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal("GET", method); + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal("GET", method); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("HTTP: GET /", routeEndpointBuilder.DisplayName); - Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); - } + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("HTTP: GET /", routeEndpointBuilder.DisplayName); + Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); + } - [Fact] - public void MapPatch_BuildsEndpointWithCorrectMethod() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - _ = builder.MapPatch("/", () => { }); + [Fact] + public void MapPatch_BuildsEndpointWithCorrectMethod() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = builder.MapPatch("/", () => { }); - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal("PATCH", method); + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal("PATCH", method); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("HTTP: PATCH /", routeEndpointBuilder.DisplayName); - Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); - } + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("HTTP: PATCH /", routeEndpointBuilder.DisplayName); + Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); + } - [Fact] - public async Task MapGetWithRouteParameter_BuildsEndpointWithRouteSpecificBinding() + [Fact] + public async Task MapGetWithRouteParameter_BuildsEndpointWithRouteSpecificBinding() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = builder.MapGet("/{id}", (int? id, HttpContext httpContext) => { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - _ = builder.MapGet("/{id}", (int? id, HttpContext httpContext) => + if (id is not null) { - if (id is not null) - { - httpContext.Items["input"] = id; - } - }); + httpContext.Items["input"] = id; + } + }); - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal("GET", method); + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal("GET", method); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("HTTP: GET /{id}", routeEndpointBuilder.DisplayName); - Assert.Equal("/{id}", routeEndpointBuilder.RoutePattern.RawText); + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("HTTP: GET /{id}", routeEndpointBuilder.DisplayName); + Assert.Equal("/{id}", routeEndpointBuilder.RoutePattern.RawText); - // Assert that we don't fallback to the query string - var httpContext = new DefaultHttpContext(); + // Assert that we don't fallback to the query string + var httpContext = new DefaultHttpContext(); - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["id"] = "42" - }); + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["id"] = "42" + }); - await endpoint.RequestDelegate!(httpContext); + await endpoint.RequestDelegate!(httpContext); - Assert.Null(httpContext.Items["input"]); - } + Assert.Null(httpContext.Items["input"]); + } - [Fact] - public async Task MapGetWithoutRouteParameter_BuildsEndpointWithQuerySpecificBinding() + [Fact] + public async Task MapGetWithoutRouteParameter_BuildsEndpointWithQuerySpecificBinding() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = builder.MapGet("/", (int? id, HttpContext httpContext) => { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - _ = builder.MapGet("/", (int? id, HttpContext httpContext) => + if (id is not null) { - if (id is not null) - { - httpContext.Items["input"] = id; - } - }); + httpContext.Items["input"] = id; + } + }); - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal("GET", method); + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal("GET", method); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("HTTP: GET /", routeEndpointBuilder.DisplayName); - Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("HTTP: GET /", routeEndpointBuilder.DisplayName); + Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); - // Assert that we don't fallback to the route values - var httpContext = new DefaultHttpContext(); + // Assert that we don't fallback to the route values + var httpContext = new DefaultHttpContext(); - httpContext.Request.Query = new QueryCollection(new Dictionary() - { - ["id"] = "41" - }); - httpContext.Request.RouteValues = new(); - httpContext.Request.RouteValues["id"] = "42"; + httpContext.Request.Query = new QueryCollection(new Dictionary() + { + ["id"] = "41" + }); + httpContext.Request.RouteValues = new(); + httpContext.Request.RouteValues["id"] = "42"; - await endpoint.RequestDelegate!(httpContext); + await endpoint.RequestDelegate!(httpContext); - Assert.Equal(41, httpContext.Items["input"]); - } + Assert.Equal(41, httpContext.Items["input"]); + } - [Fact] - public void MapGet_ThrowsWithImplicitFromBody() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - var ex = Assert.Throws(() => builder.MapGet("/", (Todo todo) => { })); - Assert.Contains("Body was inferred but the method does not allow inferred body parameters.", ex.Message); - Assert.Contains("Did you mean to register the \"Body (Inferred)\" parameter(s) as a Service or apply the [FromService] or [FromBody] attribute?", ex.Message); - } + [Fact] + public void MapGet_ThrowsWithImplicitFromBody() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + var ex = Assert.Throws(() => builder.MapGet("/", (Todo todo) => { })); + Assert.Contains("Body was inferred but the method does not allow inferred body parameters.", ex.Message); + Assert.Contains("Did you mean to register the \"Body (Inferred)\" parameter(s) as a Service or apply the [FromService] or [FromBody] attribute?", ex.Message); + } - [Fact] - public void MapDelete_ThrowsWithImplicitFromBody() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - var ex = Assert.Throws(() => builder.MapDelete("/", (Todo todo) => { })); - Assert.Contains("Body was inferred but the method does not allow inferred body parameters.", ex.Message); - Assert.Contains("Did you mean to register the \"Body (Inferred)\" parameter(s) as a Service or apply the [FromService] or [FromBody] attribute?", ex.Message); - } + [Fact] + public void MapDelete_ThrowsWithImplicitFromBody() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + var ex = Assert.Throws(() => builder.MapDelete("/", (Todo todo) => { })); + Assert.Contains("Body was inferred but the method does not allow inferred body parameters.", ex.Message); + Assert.Contains("Did you mean to register the \"Body (Inferred)\" parameter(s) as a Service or apply the [FromService] or [FromBody] attribute?", ex.Message); + } - public static object[][] NonImplicitFromBodyMethods + public static object[][] NonImplicitFromBodyMethods + { + get { - get + return new[] { - return new[] - { new[] { HttpMethods.Delete }, new[] { HttpMethods.Connect }, new[] { HttpMethods.Trace }, @@ -235,699 +235,698 @@ namespace Microsoft.AspNetCore.Builder new[] { HttpMethods.Head }, new[] { HttpMethods.Options }, }; - } - } - - [Theory] - [MemberData(nameof(NonImplicitFromBodyMethods))] - public void MapVerb_ThrowsWithImplicitFromBody(string method) - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - var ex = Assert.Throws(() => builder.MapMethods("/", new[] { method }, (Todo todo) => { })); - Assert.Contains("Body was inferred but the method does not allow inferred body parameters.", ex.Message); - Assert.Contains("Did you mean to register the \"Body (Inferred)\" parameter(s) as a Service or apply the [FromService] or [FromBody] attribute?", ex.Message); } + } - [Fact] - public void MapGet_ImplicitFromService() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton().BuildServiceProvider())); - _ = builder.MapGet("/", (TodoService todo) => { }); + [Theory] + [MemberData(nameof(NonImplicitFromBodyMethods))] + public void MapVerb_ThrowsWithImplicitFromBody(string method) + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + var ex = Assert.Throws(() => builder.MapMethods("/", new[] { method }, (Todo todo) => { })); + Assert.Contains("Body was inferred but the method does not allow inferred body parameters.", ex.Message); + Assert.Contains("Did you mean to register the \"Body (Inferred)\" parameter(s) as a Service or apply the [FromService] or [FromBody] attribute?", ex.Message); + } - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + [Fact] + public void MapGet_ImplicitFromService() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton().BuildServiceProvider())); + _ = builder.MapGet("/", (TodoService todo) => { }); - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal("GET", method); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("HTTP: GET /", routeEndpointBuilder.DisplayName); - Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); - } + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal("GET", method); - [Fact] - public void MapDelete_ImplicitFromService() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton().BuildServiceProvider())); - _ = builder.MapDelete("/", (TodoService todo) => { }); + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("HTTP: GET /", routeEndpointBuilder.DisplayName); + Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); + } - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + [Fact] + public void MapDelete_ImplicitFromService() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton().BuildServiceProvider())); + _ = builder.MapDelete("/", (TodoService todo) => { }); - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal("DELETE", method); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("HTTP: DELETE /", routeEndpointBuilder.DisplayName); - Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); - } + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal("DELETE", method); - [Fact] - public void MapPatch_ImplicitFromService() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton().BuildServiceProvider())); - _ = builder.MapPatch("/", (TodoService todo) => { }); + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("HTTP: DELETE /", routeEndpointBuilder.DisplayName); + Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); + } - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + [Fact] + public void MapPatch_ImplicitFromService() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton().BuildServiceProvider())); + _ = builder.MapPatch("/", (TodoService todo) => { }); - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal("PATCH", method); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("HTTP: PATCH /", routeEndpointBuilder.DisplayName); - Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); - } + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal("PATCH", method); - [AttributeUsage(AttributeTargets.Parameter)] - private class TestFromServiceAttribute : Attribute, IFromServiceMetadata - { } + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("HTTP: PATCH /", routeEndpointBuilder.DisplayName); + Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); + } - [Fact] - public void MapGet_ExplicitFromService() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton().BuildServiceProvider())); - _ = builder.MapGet("/", ([TestFromServiceAttribute] TodoService todo) => { }); + [AttributeUsage(AttributeTargets.Parameter)] + private class TestFromServiceAttribute : Attribute, IFromServiceMetadata + { } - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + [Fact] + public void MapGet_ExplicitFromService() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton().BuildServiceProvider())); + _ = builder.MapGet("/", ([TestFromServiceAttribute] TodoService todo) => { }); - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal("GET", method); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("HTTP: GET /", routeEndpointBuilder.DisplayName); - Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); - } + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal("GET", method); - [Fact] - public void MapDelete_ExplicitFromService() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton().BuildServiceProvider())); - _ = builder.MapDelete("/", ([TestFromServiceAttribute] TodoService todo) => { }); + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("HTTP: GET /", routeEndpointBuilder.DisplayName); + Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); + } - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + [Fact] + public void MapDelete_ExplicitFromService() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton().BuildServiceProvider())); + _ = builder.MapDelete("/", ([TestFromServiceAttribute] TodoService todo) => { }); - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal("DELETE", method); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("HTTP: DELETE /", routeEndpointBuilder.DisplayName); - Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); - } + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal("DELETE", method); - [Fact] - public void MapPatch_ExplicitFromService() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton().BuildServiceProvider())); - _ = builder.MapPatch("/", ([TestFromServiceAttribute] TodoService todo) => { }); + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("HTTP: DELETE /", routeEndpointBuilder.DisplayName); + Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); + } - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + [Fact] + public void MapPatch_ExplicitFromService() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().AddSingleton().BuildServiceProvider())); + _ = builder.MapPatch("/", ([TestFromServiceAttribute] TodoService todo) => { }); - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal("PATCH", method); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("HTTP: PATCH /", routeEndpointBuilder.DisplayName); - Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); - } + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal("PATCH", method); - [AttributeUsage(AttributeTargets.Parameter)] - private class TestFromBodyAttribute : Attribute, IFromBodyMetadata - { } + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("HTTP: PATCH /", routeEndpointBuilder.DisplayName); + Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); + } - [Fact] - public void MapGet_ExplicitFromBody_BuildsEndpointWithCorrectMethod() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - _ = builder.MapGet("/", ([TestFromBody] Todo todo) => { }); + [AttributeUsage(AttributeTargets.Parameter)] + private class TestFromBodyAttribute : Attribute, IFromBodyMetadata + { } - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + [Fact] + public void MapGet_ExplicitFromBody_BuildsEndpointWithCorrectMethod() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = builder.MapGet("/", ([TestFromBody] Todo todo) => { }); - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal("GET", method); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("HTTP: GET /", routeEndpointBuilder.DisplayName); - Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); - } + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal("GET", method); - [Fact] - public void MapDelete_ExplicitFromBody_BuildsEndpointWithCorrectMethod() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - _ = builder.MapDelete("/", ([TestFromBody] Todo todo) => { }); + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("HTTP: GET /", routeEndpointBuilder.DisplayName); + Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); + } - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + [Fact] + public void MapDelete_ExplicitFromBody_BuildsEndpointWithCorrectMethod() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = builder.MapDelete("/", ([TestFromBody] Todo todo) => { }); - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal("DELETE", method); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("HTTP: DELETE /", routeEndpointBuilder.DisplayName); - Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); - } + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal("DELETE", method); - [Fact] - public void MapPatch_ExplicitFromBody_BuildsEndpointWithCorrectMethod() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - _ = builder.MapPatch("/", ([TestFromBody] Todo todo) => { }); + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("HTTP: DELETE /", routeEndpointBuilder.DisplayName); + Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); + } - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + [Fact] + public void MapPatch_ExplicitFromBody_BuildsEndpointWithCorrectMethod() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = builder.MapPatch("/", ([TestFromBody] Todo todo) => { }); - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal("PATCH", method); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("HTTP: PATCH /", routeEndpointBuilder.DisplayName); - Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); - } + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal("PATCH", method); - [Theory] - [MemberData(nameof(MapMethods))] - public void MapVerbDoesNotDuplicateMetadata(Func map, string expectedMethod) - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("HTTP: PATCH /", routeEndpointBuilder.DisplayName); + Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); + } - map(builder, "/{ID}", () => { }).WithName("Foo"); + [Theory] + [MemberData(nameof(MapMethods))] + public void MapVerbDoesNotDuplicateMetadata(Func map, string expectedMethod) + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - var dataSource = GetBuilderEndpointDataSource(builder); + map(builder, "/{ID}", () => { }).WithName("Foo"); - // Access endpoints a couple of times to make sure it gets built - _ = dataSource.Endpoints; - _ = dataSource.Endpoints; - _ = dataSource.Endpoints; + var dataSource = GetBuilderEndpointDataSource(builder); - var endpoint = Assert.Single(dataSource.Endpoints); + // Access endpoints a couple of times to make sure it gets built + _ = dataSource.Endpoints; + _ = dataSource.Endpoints; + _ = dataSource.Endpoints; - var endpointNameMetadata = Assert.Single(endpoint.Metadata.GetOrderedMetadata()); - var routeNameMetadata = Assert.Single(endpoint.Metadata.GetOrderedMetadata()); - Assert.Equal("Foo", endpointNameMetadata.EndpointName); - Assert.Equal("Foo", routeNameMetadata.RouteName); + var endpoint = Assert.Single(dataSource.Endpoints); - if (expectedMethod is not null) - { - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal(expectedMethod, method); - } - } + var endpointNameMetadata = Assert.Single(endpoint.Metadata.GetOrderedMetadata()); + var routeNameMetadata = Assert.Single(endpoint.Metadata.GetOrderedMetadata()); + Assert.Equal("Foo", endpointNameMetadata.EndpointName); + Assert.Equal("Foo", routeNameMetadata.RouteName); - [Theory] - [MemberData(nameof(MapMethods))] - public void AddingMetadataAfterBuildingEndpointThrows(Func map, string expectedMethod) + if (expectedMethod is not null) { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - - var endpointBuilder = map(builder, "/{ID}", () => { }); + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal(expectedMethod, method); + } + } - var dataSource = GetBuilderEndpointDataSource(builder); + [Theory] + [MemberData(nameof(MapMethods))] + public void AddingMetadataAfterBuildingEndpointThrows(Func map, string expectedMethod) + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - var endpoint = Assert.Single(dataSource.Endpoints); + var endpointBuilder = map(builder, "/{ID}", () => { }); - if (expectedMethod is not null) - { - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal(expectedMethod, method); - } + var dataSource = GetBuilderEndpointDataSource(builder); - Assert.Throws(() => endpointBuilder.WithMetadata(new RouteNameMetadata("Foo"))); - } + var endpoint = Assert.Single(dataSource.Endpoints); - [Theory] - [MemberData(nameof(MapMethods))] - public async Task MapVerbWithExplicitRouteParameterIsCaseInsensitive(Func map, string expectedMethod) + if (expectedMethod is not null) { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - - map(builder, "/{ID}", ([FromRoute] int? id, HttpContext httpContext) => - { - if (id is not null) - { - httpContext.Items["input"] = id; - } - }); + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal(expectedMethod, method); + } - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + Assert.Throws(() => endpointBuilder.WithMetadata(new RouteNameMetadata("Foo"))); + } - if (expectedMethod is not null) - { - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal(expectedMethod, method); - } + [Theory] + [MemberData(nameof(MapMethods))] + public async Task MapVerbWithExplicitRouteParameterIsCaseInsensitive(Func map, string expectedMethod) + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - if (expectedMethod is not null) + map(builder, "/{ID}", ([FromRoute] int? id, HttpContext httpContext) => + { + if (id is not null) { - Assert.Equal($"HTTP: {expectedMethod} /{{ID}}", routeEndpointBuilder.DisplayName); + httpContext.Items["input"] = id; } - Assert.Equal($"/{{ID}}", routeEndpointBuilder.RoutePattern.RawText); + }); - var httpContext = new DefaultHttpContext(); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - httpContext.Request.RouteValues["id"] = "13"; - - await endpoint.RequestDelegate!(httpContext); - - Assert.Equal(13, httpContext.Items["input"]); + if (expectedMethod is not null) + { + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal(expectedMethod, method); } - [Theory] - [MemberData(nameof(MapMethods))] - public async Task MapVerbWithRouteParameterDoesNotFallbackToQuery(Func map, string expectedMethod) + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + if (expectedMethod is not null) { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + Assert.Equal($"HTTP: {expectedMethod} /{{ID}}", routeEndpointBuilder.DisplayName); + } + Assert.Equal($"/{{ID}}", routeEndpointBuilder.RoutePattern.RawText); - map(builder, "/{ID}", (int? id, HttpContext httpContext) => - { - if (id is not null) - { - httpContext.Items["input"] = id; - } - }); - - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); - if (expectedMethod is not null) - { - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal(expectedMethod, method); - } + var httpContext = new DefaultHttpContext(); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - if (expectedMethod is not null) - { - Assert.Equal($"HTTP: {expectedMethod} /{{ID}}", routeEndpointBuilder.DisplayName); - } - Assert.Equal($"/{{ID}}", routeEndpointBuilder.RoutePattern.RawText); + httpContext.Request.RouteValues["id"] = "13"; - // Assert that we don't fallback to the query string - var httpContext = new DefaultHttpContext(); + await endpoint.RequestDelegate!(httpContext); - httpContext.Request.Query = new QueryCollection(new Dictionary - { - ["id"] = "42" - }); + Assert.Equal(13, httpContext.Items["input"]); + } - await endpoint.RequestDelegate!(httpContext); + [Theory] + [MemberData(nameof(MapMethods))] + public async Task MapVerbWithRouteParameterDoesNotFallbackToQuery(Func map, string expectedMethod) + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - Assert.Null(httpContext.Items["input"]); - } + map(builder, "/{ID}", (int? id, HttpContext httpContext) => + { + if (id is not null) + { + httpContext.Items["input"] = id; + } + }); - [Fact] - public void MapGetWithRouteParameter_ThrowsIfRouteParameterDoesNotExist() + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + if (expectedMethod is not null) { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - var ex = Assert.Throws(() => builder.MapGet("/", ([FromRoute] int id) => { })); - Assert.Equal("'id' is not a route parameter.", ex.Message); + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal(expectedMethod, method); } - [Fact] - public async Task MapGetWithNamedFromRouteParameter_UsesFromRouteName() + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + if (expectedMethod is not null) { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - _ = builder.MapGet("/{value}", ([FromRoute(Name = "value")] int id, HttpContext httpContext) => - { - httpContext.Items["value"] = id; - }); + Assert.Equal($"HTTP: {expectedMethod} /{{ID}}", routeEndpointBuilder.DisplayName); + } + Assert.Equal($"/{{ID}}", routeEndpointBuilder.RoutePattern.RawText); - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + // Assert that we don't fallback to the query string + var httpContext = new DefaultHttpContext(); - // Assert that we don't fallback to the query string - var httpContext = new DefaultHttpContext(); + httpContext.Request.Query = new QueryCollection(new Dictionary + { + ["id"] = "42" + }); - httpContext.Request.RouteValues["value"] = "42"; + await endpoint.RequestDelegate!(httpContext); - await endpoint.RequestDelegate!(httpContext); + Assert.Null(httpContext.Items["input"]); + } - Assert.Equal(42, httpContext.Items["value"]); - } + [Fact] + public void MapGetWithRouteParameter_ThrowsIfRouteParameterDoesNotExist() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + var ex = Assert.Throws(() => builder.MapGet("/", ([FromRoute] int id) => { })); + Assert.Equal("'id' is not a route parameter.", ex.Message); + } - [Fact] - public async Task MapGetWithNamedFromRouteParameter_FailsForParameterName() + [Fact] + public async Task MapGetWithNamedFromRouteParameter_UsesFromRouteName() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = builder.MapGet("/{value}", ([FromRoute(Name = "value")] int id, HttpContext httpContext) => { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - _ = builder.MapGet("/{value}", ([FromRoute(Name = "value")] int id, HttpContext httpContext) => - { - httpContext.Items["value"] = id; - }); + httpContext.Items["value"] = id; + }); - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - // Assert that we don't fallback to the query string - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = new ServiceCollection().AddLogging().BuildServiceProvider(); + // Assert that we don't fallback to the query string + var httpContext = new DefaultHttpContext(); - httpContext.Request.RouteValues["id"] = "42"; + httpContext.Request.RouteValues["value"] = "42"; - await endpoint.RequestDelegate!(httpContext); + await endpoint.RequestDelegate!(httpContext); - Assert.Null(httpContext.Items["value"]); - Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode); - } + Assert.Equal(42, httpContext.Items["value"]); + } - [Fact] - public void MapGetWithNamedFromRouteParameter_ThrowsForMismatchedPattern() + [Fact] + public async Task MapGetWithNamedFromRouteParameter_FailsForParameterName() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = builder.MapGet("/{value}", ([FromRoute(Name = "value")] int id, HttpContext httpContext) => { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - var ex = Assert.Throws(() =>builder.MapGet("/{id}", ([FromRoute(Name = "value")] int id, HttpContext httpContext) => { })); - Assert.Equal("'value' is not a route parameter.", ex.Message); - } + httpContext.Items["value"] = id; + }); - [Fact] - public void MapPost_BuildsEndpointWithCorrectMethod() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - _ = builder.MapPost("/", () => { }); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + // Assert that we don't fallback to the query string + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = new ServiceCollection().AddLogging().BuildServiceProvider(); - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal("POST", method); + httpContext.Request.RouteValues["id"] = "42"; - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("HTTP: POST /", routeEndpointBuilder.DisplayName); - Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); - } + await endpoint.RequestDelegate!(httpContext); - [Fact] - public void MapPost_BuildsEndpointWithCorrectEndpointMetadata() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - _ = builder.MapPost("/", [TestConsumesAttribute(typeof(Todo), "application/xml")] (Todo todo) => { }); + Assert.Null(httpContext.Items["value"]); + Assert.Equal(StatusCodes.Status400BadRequest, httpContext.Response.StatusCode); + } - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + [Fact] + public void MapGetWithNamedFromRouteParameter_ThrowsForMismatchedPattern() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + var ex = Assert.Throws(() => builder.MapGet("/{id}", ([FromRoute(Name = "value")] int id, HttpContext httpContext) => { })); + Assert.Equal("'value' is not a route parameter.", ex.Message); + } - var endpointMetadata = endpoint.Metadata.GetMetadata(); + [Fact] + public void MapPost_BuildsEndpointWithCorrectMethod() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = builder.MapPost("/", () => { }); - Assert.NotNull(endpointMetadata); - Assert.False(endpointMetadata!.IsOptional); - Assert.Equal(typeof(Todo), endpointMetadata.RequestType); - Assert.Equal(new[] { "application/xml" }, endpointMetadata.ContentTypes); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - } + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal("POST", method); - [Fact] - public void MapPut_BuildsEndpointWithCorrectMethod() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - _ = builder.MapPut("/", () => { }); + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("HTTP: POST /", routeEndpointBuilder.DisplayName); + Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); + } - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + [Fact] + public void MapPost_BuildsEndpointWithCorrectEndpointMetadata() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = builder.MapPost("/", [TestConsumesAttribute(typeof(Todo), "application/xml")] (Todo todo) => { }); - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal("PUT", method); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("HTTP: PUT /", routeEndpointBuilder.DisplayName); - Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); - } + var endpointMetadata = endpoint.Metadata.GetMetadata(); - [Fact] - public void MapDelete_BuildsEndpointWithCorrectMethod() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - _ = builder.MapDelete("/", () => { }); + Assert.NotNull(endpointMetadata); + Assert.False(endpointMetadata!.IsOptional); + Assert.Equal(typeof(Todo), endpointMetadata.RequestType); + Assert.Equal(new[] { "application/xml" }, endpointMetadata.ContentTypes); - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + } - var methodMetadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(methodMetadata); - var method = Assert.Single(methodMetadata!.HttpMethods); - Assert.Equal("DELETE", method); + [Fact] + public void MapPut_BuildsEndpointWithCorrectMethod() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = builder.MapPut("/", () => { }); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("HTTP: DELETE /", routeEndpointBuilder.DisplayName); - Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); - } + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - [Fact] - public void MapFallback_BuildsEndpointWithLowestRouteOrder() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - _ = builder.MapFallback("/", () => { }); + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal("PUT", method); - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("HTTP: PUT /", routeEndpointBuilder.DisplayName); + Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); + } - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("Fallback /", routeEndpointBuilder.DisplayName); - Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); - Assert.Equal(int.MaxValue, routeEndpointBuilder.Order); - } + [Fact] + public void MapDelete_BuildsEndpointWithCorrectMethod() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = builder.MapDelete("/", () => { }); - [Fact] - public void MapFallbackWithoutPath_BuildsEndpointWithLowestRouteOrder() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - _ = builder.MapFallback(() => { }); - - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); - - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Equal("Fallback {*path:nonfile}", routeEndpointBuilder.DisplayName); - Assert.Equal("{*path:nonfile}", routeEndpointBuilder.RoutePattern.RawText); - Assert.Single(routeEndpointBuilder.RoutePattern.Parameters); - Assert.True(routeEndpointBuilder.RoutePattern.Parameters[0].IsCatchAll); - Assert.Equal(int.MaxValue, routeEndpointBuilder.Order); - } + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - [Fact] - public void WithTags_CanSetTagsForEndpoint() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - string GetString() => "Foo"; - _ = builder.MapDelete("/", GetString).WithTags("Some", "Test", "Tags"); + var methodMetadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(methodMetadata); + var method = Assert.Single(methodMetadata!.HttpMethods); + Assert.Equal("DELETE", method); - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("HTTP: DELETE /", routeEndpointBuilder.DisplayName); + Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); + } - var tagsMetadata = endpoint.Metadata.GetMetadata(); - Assert.Equal(new[] { "Some", "Test", "Tags" }, tagsMetadata?.Tags); - } + [Fact] + public void MapFallback_BuildsEndpointWithLowestRouteOrder() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = builder.MapFallback("/", () => { }); - [Fact] - public void MapMethod_DoesNotEndpointNameForMethodGroupByDefault() - { - string GetString() => "Foo"; - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); - _ = builder.MapDelete("/", GetString); - - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); - - var endpointName = endpoint.Metadata.GetMetadata(); - var routeName = endpoint.Metadata.GetMetadata(); - var routeEndpointBuilder = GetRouteEndpointBuilder(builder); - Assert.Null(endpointName); - Assert.Null(routeName); - Assert.Equal("HTTP: DELETE / => GetString", routeEndpointBuilder.DisplayName); - } + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task MapMethod_FlowsThrowOnBadHttpRequest(bool throwOnBadRequest) - { - var serviceProvider = new EmptyServiceProvider(); - serviceProvider.RouteHandlerOptions.ThrowOnBadRequest = throwOnBadRequest; + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("Fallback /", routeEndpointBuilder.DisplayName); + Assert.Equal("/", routeEndpointBuilder.RoutePattern.RawText); + Assert.Equal(int.MaxValue, routeEndpointBuilder.Order); + } - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider)); - _ = builder.Map("/{id}", (int id) => { }); + [Fact] + public void MapFallbackWithoutPath_BuildsEndpointWithLowestRouteOrder() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = builder.MapFallback(() => { }); + + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Equal("Fallback {*path:nonfile}", routeEndpointBuilder.DisplayName); + Assert.Equal("{*path:nonfile}", routeEndpointBuilder.RoutePattern.RawText); + Assert.Single(routeEndpointBuilder.RoutePattern.Parameters); + Assert.True(routeEndpointBuilder.RoutePattern.Parameters[0].IsCatchAll); + Assert.Equal(int.MaxValue, routeEndpointBuilder.Order); + } - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + [Fact] + public void WithTags_CanSetTagsForEndpoint() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + string GetString() => "Foo"; + _ = builder.MapDelete("/", GetString).WithTags("Some", "Test", "Tags"); - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = new ServiceCollection().AddLogging().BuildServiceProvider(); - httpContext.Request.RouteValues["id"] = "invalid!"; + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - if (throwOnBadRequest) - { - var ex = await Assert.ThrowsAsync(() => endpoint.RequestDelegate!(httpContext)); - Assert.Equal(400, ex.StatusCode); - } - else - { - await endpoint.RequestDelegate!(httpContext); - Assert.Equal(400, httpContext.Response.StatusCode); - } - } + var tagsMetadata = endpoint.Metadata.GetMetadata(); + Assert.Equal(new[] { "Some", "Test", "Tags" }, tagsMetadata?.Tags); + } - [Fact] - public async Task MapMethod_DefaultsToNotThrowOnBadHttpRequestIfItCannotResolveRouteHandlerOptions() - { - var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().BuildServiceProvider())); + [Fact] + public void MapMethod_DoesNotEndpointNameForMethodGroupByDefault() + { + string GetString() => "Foo"; + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new EmptyServiceProvider())); + _ = builder.MapDelete("/", GetString); + + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var endpointName = endpoint.Metadata.GetMetadata(); + var routeName = endpoint.Metadata.GetMetadata(); + var routeEndpointBuilder = GetRouteEndpointBuilder(builder); + Assert.Null(endpointName); + Assert.Null(routeName); + Assert.Equal("HTTP: DELETE / => GetString", routeEndpointBuilder.DisplayName); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MapMethod_FlowsThrowOnBadHttpRequest(bool throwOnBadRequest) + { + var serviceProvider = new EmptyServiceProvider(); + serviceProvider.RouteHandlerOptions.ThrowOnBadRequest = throwOnBadRequest; - _ = builder.Map("/{id}", (int id) => { }); + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(serviceProvider)); + _ = builder.Map("/{id}", (int id) => { }); - var dataSource = GetBuilderEndpointDataSource(builder); - // Trigger Endpoint build by calling getter. - var endpoint = Assert.Single(dataSource.Endpoints); + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = new ServiceCollection().AddLogging().BuildServiceProvider(); - httpContext.Request.RouteValues["id"] = "invalid!"; + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = new ServiceCollection().AddLogging().BuildServiceProvider(); + httpContext.Request.RouteValues["id"] = "invalid!"; + if (throwOnBadRequest) + { + var ex = await Assert.ThrowsAsync(() => endpoint.RequestDelegate!(httpContext)); + Assert.Equal(400, ex.StatusCode); + } + else + { await endpoint.RequestDelegate!(httpContext); Assert.Equal(400, httpContext.Response.StatusCode); } + } - class FromRoute : Attribute, IFromRouteMetadata - { - public string? Name { get; set; } - } + [Fact] + public async Task MapMethod_DefaultsToNotThrowOnBadHttpRequestIfItCannotResolveRouteHandlerOptions() + { + var builder = new DefaultEndpointRouteBuilder(new ApplicationBuilder(new ServiceCollection().BuildServiceProvider())); + + _ = builder.Map("/{id}", (int id) => { }); + + var dataSource = GetBuilderEndpointDataSource(builder); + // Trigger Endpoint build by calling getter. + var endpoint = Assert.Single(dataSource.Endpoints); + + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = new ServiceCollection().AddLogging().BuildServiceProvider(); + httpContext.Request.RouteValues["id"] = "invalid!"; - class TestConsumesAttribute : Attribute, IAcceptsMetadata + await endpoint.RequestDelegate!(httpContext); + Assert.Equal(400, httpContext.Response.StatusCode); + } + + class FromRoute : Attribute, IFromRouteMetadata + { + public string? Name { get; set; } + } + + class TestConsumesAttribute : Attribute, IAcceptsMetadata + { + public TestConsumesAttribute(Type requestType, string contentType, params string[] otherContentTypes) { - public TestConsumesAttribute(Type requestType, string contentType, params string[] otherContentTypes) + if (contentType == null) { - if (contentType == null) - { - throw new ArgumentNullException(nameof(contentType)); - } + throw new ArgumentNullException(nameof(contentType)); + } - var contentTypes = new List() + var contentTypes = new List() { contentType }; - for (var i = 0; i < otherContentTypes.Length; i++) - { - contentTypes.Add(otherContentTypes[i]); - } - - _requestType = requestType; - _contentTypes = contentTypes; + for (var i = 0; i < otherContentTypes.Length; i++) + { + contentTypes.Add(otherContentTypes[i]); } - IReadOnlyList IAcceptsMetadata.ContentTypes => _contentTypes; - Type? IAcceptsMetadata.RequestType => _requestType; + _requestType = requestType; + _contentTypes = contentTypes; + } - bool IAcceptsMetadata.IsOptional => false; + IReadOnlyList IAcceptsMetadata.ContentTypes => _contentTypes; + Type? IAcceptsMetadata.RequestType => _requestType; - Type? _requestType; + bool IAcceptsMetadata.IsOptional => false; - List _contentTypes = new(); - } + Type? _requestType; - class Todo - { + List _contentTypes = new(); + } - } + class Todo + { - // Here to more easily disambiguate when ToDo is - // intended to be validated as an implicit service in tests - class TodoService - { + } - } + // Here to more easily disambiguate when ToDo is + // intended to be validated as an implicit service in tests + class TodoService + { - private class HttpMethodAttribute : Attribute, IHttpMethodMetadata - { - public bool AcceptCorsPreflight => false; + } - public IReadOnlyList HttpMethods { get; } + private class HttpMethodAttribute : Attribute, IHttpMethodMetadata + { + public bool AcceptCorsPreflight => false; - public HttpMethodAttribute(params string[] httpMethods) - { - HttpMethods = httpMethods; - } + public IReadOnlyList HttpMethods { get; } + + public HttpMethodAttribute(params string[] httpMethods) + { + HttpMethods = httpMethods; } + } + + private class EmptyServiceProvider : IServiceScope, IServiceProvider, IServiceScopeFactory + { + public IServiceProvider ServiceProvider => this; - private class EmptyServiceProvider : IServiceScope, IServiceProvider, IServiceScopeFactory + public RouteHandlerOptions RouteHandlerOptions { get; set; } = new RouteHandlerOptions(); + + public IServiceScope CreateScope() { - public IServiceProvider ServiceProvider => this; + return this; + } - public RouteHandlerOptions RouteHandlerOptions { get; set; } = new RouteHandlerOptions(); + public void Dispose() + { + } - public IServiceScope CreateScope() + public object? GetService(Type serviceType) + { + if (serviceType == typeof(IServiceScopeFactory)) { return this; } - - public void Dispose() + else if (serviceType == typeof(IOptions)) { + return Options.Create(RouteHandlerOptions); } - public object? GetService(Type serviceType) - { - if (serviceType == typeof(IServiceScopeFactory)) - { - return this; - } - else if (serviceType == typeof(IOptions)) - { - return Options.Create(RouteHandlerOptions); - } - - return null; - } + return null; } } } diff --git a/src/Http/Routing/test/UnitTests/Builder/RoutingBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RoutingBuilderExtensionsTest.cs index 5d8299a442..e6f2b9673b 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RoutingBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RoutingBuilderExtensionsTest.cs @@ -9,128 +9,127 @@ using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +public class RoutingBuilderExtensionsTest { - public class RoutingBuilderExtensionsTest + [Fact] + public void UseRouter_ThrowsInvalidOperationException_IfRoutingMarkerServiceIsNotRegistered() { - [Fact] - public void UseRouter_ThrowsInvalidOperationException_IfRoutingMarkerServiceIsNotRegistered() - { - // Arrange - var applicationBuilderMock = new Mock(); - applicationBuilderMock - .Setup(s => s.ApplicationServices) - .Returns(Mock.Of()); - - var router = Mock.Of(); - - // Act & Assert - var exception = Assert.Throws( - () => applicationBuilderMock.Object.UseRouter(router)); - - Assert.Equal( - "Unable to find the required services. Please add all the required services by calling " + - "'IServiceCollection.AddRouting' inside the call to 'ConfigureServices(...)'" + - " in the application startup code.", - exception.Message); - } - - [Fact] - public void UseRouter_IRouter_ThrowsWithoutCallingAddRouting() - { - // Arrange - var app = new ApplicationBuilder(Mock.Of()); - - // Act - var ex = Assert.Throws(() => app.UseRouter(Mock.Of())); - - // Assert - Assert.Equal( - "Unable to find the required services. " + - "Please add all the required services by calling 'IServiceCollection.AddRouting' " + - "inside the call to 'ConfigureServices(...)' in the application startup code.", - ex.Message); - } - - [Fact] - public void UseRouter_Action_ThrowsWithoutCallingAddRouting() - { - // Arrange - var app = new ApplicationBuilder(Mock.Of()); - - // Act - var ex = Assert.Throws(() => app.UseRouter(b => { })); - - // Assert - Assert.Equal( - "Unable to find the required services. " + - "Please add all the required services by calling 'IServiceCollection.AddRouting' " + - "inside the call to 'ConfigureServices(...)' in the application startup code.", - ex.Message); - } - - [Fact] - public async Task UseRouter_IRouter_CallsRoute() - { - // Arrange - var services = CreateServices(); - - var app = new ApplicationBuilder(services); + // Arrange + var applicationBuilderMock = new Mock(); + applicationBuilderMock + .Setup(s => s.ApplicationServices) + .Returns(Mock.Of()); + + var router = Mock.Of(); + + // Act & Assert + var exception = Assert.Throws( + () => applicationBuilderMock.Object.UseRouter(router)); + + Assert.Equal( + "Unable to find the required services. Please add all the required services by calling " + + "'IServiceCollection.AddRouting' inside the call to 'ConfigureServices(...)'" + + " in the application startup code.", + exception.Message); + } - var router = new Mock(MockBehavior.Strict); - router - .Setup(r => r.RouteAsync(It.IsAny())) - .Returns(Task.CompletedTask) - .Verifiable(); + [Fact] + public void UseRouter_IRouter_ThrowsWithoutCallingAddRouting() + { + // Arrange + var app = new ApplicationBuilder(Mock.Of()); + + // Act + var ex = Assert.Throws(() => app.UseRouter(Mock.Of())); + + // Assert + Assert.Equal( + "Unable to find the required services. " + + "Please add all the required services by calling 'IServiceCollection.AddRouting' " + + "inside the call to 'ConfigureServices(...)' in the application startup code.", + ex.Message); + } - app.UseRouter(router.Object); + [Fact] + public void UseRouter_Action_ThrowsWithoutCallingAddRouting() + { + // Arrange + var app = new ApplicationBuilder(Mock.Of()); + + // Act + var ex = Assert.Throws(() => app.UseRouter(b => { })); + + // Assert + Assert.Equal( + "Unable to find the required services. " + + "Please add all the required services by calling 'IServiceCollection.AddRouting' " + + "inside the call to 'ConfigureServices(...)' in the application startup code.", + ex.Message); + } - var appFunc = app.Build(); + [Fact] + public async Task UseRouter_IRouter_CallsRoute() + { + // Arrange + var services = CreateServices(); - // Act - await appFunc(new DefaultHttpContext()); + var app = new ApplicationBuilder(services); - // Assert - router.Verify(); - } + var router = new Mock(MockBehavior.Strict); + router + .Setup(r => r.RouteAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); - [Fact] - public async Task UseRouter_Action_CallsRoute() - { - // Arrange - var services = CreateServices(); + app.UseRouter(router.Object); - var app = new ApplicationBuilder(services); + var appFunc = app.Build(); - var router = new Mock(MockBehavior.Strict); - router - .Setup(r => r.RouteAsync(It.IsAny())) - .Returns(Task.CompletedTask) - .Verifiable(); + // Act + await appFunc(new DefaultHttpContext()); - app.UseRouter(b => - { - b.Routes.Add(router.Object); - }); + // Assert + router.Verify(); + } - var appFunc = app.Build(); + [Fact] + public async Task UseRouter_Action_CallsRoute() + { + // Arrange + var services = CreateServices(); - // Act - await appFunc(new DefaultHttpContext()); + var app = new ApplicationBuilder(services); - // Assert - router.Verify(); - } + var router = new Mock(MockBehavior.Strict); + router + .Setup(r => r.RouteAsync(It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); - private IServiceProvider CreateServices() + app.UseRouter(b => { - var services = new ServiceCollection(); + b.Routes.Add(router.Object); + }); + + var appFunc = app.Build(); + + // Act + await appFunc(new DefaultHttpContext()); + + // Assert + router.Verify(); + } + + private IServiceProvider CreateServices() + { + var services = new ServiceCollection(); - services.AddLogging(); - services.AddOptions(); - services.AddRouting(); + services.AddLogging(); + services.AddOptions(); + services.AddRouting(); - return services.BuildServiceProvider(); - } + return services.BuildServiceProvider(); } } diff --git a/src/Http/Routing/test/UnitTests/Builder/RoutingEndpointConventionBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/Builder/RoutingEndpointConventionBuilderExtensionsTest.cs index 013e2a55ad..2082cc74ba 100644 --- a/src/Http/Routing/test/UnitTests/Builder/RoutingEndpointConventionBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/Builder/RoutingEndpointConventionBuilderExtensionsTest.cs @@ -7,179 +7,178 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Xunit; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +public class RoutingEndpointConventionBuilderExtensionsTest { - public class RoutingEndpointConventionBuilderExtensionsTest + [Fact] + public void RequireHost_AddsHostMetadata() { - [Fact] - public void RequireHost_AddsHostMetadata() - { - // Arrange - var builder = CreateBuilder(); + // Arrange + var builder = CreateBuilder(); - // Act - builder.RequireHost("www.example.com", "example.com"); + // Act + builder.RequireHost("www.example.com", "example.com"); - // Assert - var endpoint = builder.Build(); + // Assert + var endpoint = builder.Build(); - var metadata = endpoint.Metadata.GetMetadata(); - Assert.NotNull(metadata); - Assert.Equal(new[] { "www.example.com", "example.com" }, metadata.Hosts); - } + var metadata = endpoint.Metadata.GetMetadata(); + Assert.NotNull(metadata); + Assert.Equal(new[] { "www.example.com", "example.com" }, metadata.Hosts); + } - [Fact] - public void RequireHost_ChainedCall_ReturnedBuilderIsDerivedType() - { - // Arrange - var builder = CreateBuilder(); + [Fact] + public void RequireHost_ChainedCall_ReturnedBuilderIsDerivedType() + { + // Arrange + var builder = CreateBuilder(); - // Act - var chainedBuilder = builder.RequireHost("test"); + // Act + var chainedBuilder = builder.RequireHost("test"); - // Assert - Assert.True(chainedBuilder.TestProperty); - } + // Assert + Assert.True(chainedBuilder.TestProperty); + } - [Fact] - public void WithDisplayName_String_SetsDisplayName() - { - // Arrange - var builder = CreateBuilder(); + [Fact] + public void WithDisplayName_String_SetsDisplayName() + { + // Arrange + var builder = CreateBuilder(); - // Act - builder.WithDisplayName("test"); + // Act + builder.WithDisplayName("test"); - // Assert - var endpoint = builder.Build(); - Assert.Equal("test", endpoint.DisplayName); - } + // Assert + var endpoint = builder.Build(); + Assert.Equal("test", endpoint.DisplayName); + } - [Fact] - public void WithDisplayName_ChainedCall_ReturnedBuilderIsDerivedType() - { - // Arrange - var builder = CreateBuilder(); + [Fact] + public void WithDisplayName_ChainedCall_ReturnedBuilderIsDerivedType() + { + // Arrange + var builder = CreateBuilder(); - // Act - var chainedBuilder = builder.WithDisplayName("test"); + // Act + var chainedBuilder = builder.WithDisplayName("test"); - // Assert - Assert.True(chainedBuilder.TestProperty); - } + // Assert + Assert.True(chainedBuilder.TestProperty); + } - [Fact] - public void WithDisplayName_Func_SetsDisplayName() - { - // Arrange - var builder = CreateBuilder(); + [Fact] + public void WithDisplayName_Func_SetsDisplayName() + { + // Arrange + var builder = CreateBuilder(); - // Act - builder.WithDisplayName(b => "test"); + // Act + builder.WithDisplayName(b => "test"); - // Assert - var endpoint = builder.Build(); - Assert.Equal("test", endpoint.DisplayName); - } + // Assert + var endpoint = builder.Build(); + Assert.Equal("test", endpoint.DisplayName); + } - [Fact] - public void WithMetadata_AddsMetadata() - { - // Arrange - var builder = CreateBuilder(); + [Fact] + public void WithMetadata_AddsMetadata() + { + // Arrange + var builder = CreateBuilder(); - // Act - builder.WithMetadata("test", new HostAttribute("www.example.com", "example.com")); + // Act + builder.WithMetadata("test", new HostAttribute("www.example.com", "example.com")); - // Assert - var endpoint = builder.Build(); + // Assert + var endpoint = builder.Build(); - var hosts = endpoint.Metadata.GetMetadata(); - Assert.NotNull(hosts); - Assert.Equal(new[] { "www.example.com", "example.com" }, hosts.Hosts); + var hosts = endpoint.Metadata.GetMetadata(); + Assert.NotNull(hosts); + Assert.Equal(new[] { "www.example.com", "example.com" }, hosts.Hosts); - var @string = endpoint.Metadata.GetMetadata(); - Assert.Equal("test", @string); - } + var @string = endpoint.Metadata.GetMetadata(); + Assert.Equal("test", @string); + } - [Fact] - public void WithMetadata_ChainedCall_ReturnedBuilderIsDerivedType() - { - // Arrange - var builder = CreateBuilder(); + [Fact] + public void WithMetadata_ChainedCall_ReturnedBuilderIsDerivedType() + { + // Arrange + var builder = CreateBuilder(); - // Act - var chainedBuilder = builder.WithMetadata("test"); + // Act + var chainedBuilder = builder.WithMetadata("test"); - // Assert - Assert.True(chainedBuilder.TestProperty); - } + // Assert + Assert.True(chainedBuilder.TestProperty); + } - [Fact] - public void WithName_SetsEndpointName() - { - // Arrange - var name = "SomeEndpointName"; - var builder = CreateBuilder(); + [Fact] + public void WithName_SetsEndpointName() + { + // Arrange + var name = "SomeEndpointName"; + var builder = CreateBuilder(); - // Act - builder.WithName(name); + // Act + builder.WithName(name); - // Assert - var endpoint = builder.Build(); + // Assert + var endpoint = builder.Build(); - var endpointName = endpoint.Metadata.GetMetadata(); - Assert.Equal(name, endpointName.EndpointName); + var endpointName = endpoint.Metadata.GetMetadata(); + Assert.Equal(name, endpointName.EndpointName); - var routeName = endpoint.Metadata.GetMetadata(); - Assert.Equal(name, routeName.RouteName); - } + var routeName = endpoint.Metadata.GetMetadata(); + Assert.Equal(name, routeName.RouteName); + } - [Fact] - public void WithGroupName_SetsEndpointGroupName() - { - // Arrange - var builder = CreateBuilder(); + [Fact] + public void WithGroupName_SetsEndpointGroupName() + { + // Arrange + var builder = CreateBuilder(); - // Act - builder.WithGroupName("SomeEndpointGroupName"); + // Act + builder.WithGroupName("SomeEndpointGroupName"); - // Assert - var endpoint = builder.Build(); + // Assert + var endpoint = builder.Build(); - var endpointGroupName = endpoint.Metadata.GetMetadata(); - Assert.Equal("SomeEndpointGroupName", endpointGroupName.EndpointGroupName); - } + var endpointGroupName = endpoint.Metadata.GetMetadata(); + Assert.Equal("SomeEndpointGroupName", endpointGroupName.EndpointGroupName); + } - private TestEndpointConventionBuilder CreateBuilder() + private TestEndpointConventionBuilder CreateBuilder() + { + var conventionBuilder = new DefaultEndpointConventionBuilder(new RouteEndpointBuilder( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse("/test"), + order: 0)); + + return new TestEndpointConventionBuilder(conventionBuilder); + } + + private class TestEndpointConventionBuilder : IEndpointConventionBuilder + { + private readonly DefaultEndpointConventionBuilder _endpointConventionBuilder; + public bool TestProperty { get; } = true; + + public TestEndpointConventionBuilder(DefaultEndpointConventionBuilder endpointConventionBuilder) { - var conventionBuilder = new DefaultEndpointConventionBuilder(new RouteEndpointBuilder( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse("/test"), - order: 0)); + _endpointConventionBuilder = endpointConventionBuilder; + } - return new TestEndpointConventionBuilder(conventionBuilder); + public void Add(Action convention) + { + _endpointConventionBuilder.Add(convention); } - private class TestEndpointConventionBuilder : IEndpointConventionBuilder + public Endpoint Build() { - private readonly DefaultEndpointConventionBuilder _endpointConventionBuilder; - public bool TestProperty { get; } = true; - - public TestEndpointConventionBuilder(DefaultEndpointConventionBuilder endpointConventionBuilder) - { - _endpointConventionBuilder = endpointConventionBuilder; - } - - public void Add(Action convention) - { - _endpointConventionBuilder.Add(convention); - } - - public Endpoint Build() - { - return _endpointConventionBuilder.Build(); - } + return _endpointConventionBuilder.Build(); } } } diff --git a/src/Http/Routing/test/UnitTests/CompositeEndpointDataSourceTest.cs b/src/Http/Routing/test/UnitTests/CompositeEndpointDataSourceTest.cs index f8cf42bb37..de7ce6a4b9 100644 --- a/src/Http/Routing/test/UnitTests/CompositeEndpointDataSourceTest.cs +++ b/src/Http/Routing/test/UnitTests/CompositeEndpointDataSourceTest.cs @@ -12,168 +12,167 @@ using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.Extensions.Primitives; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class CompositeEndpointDataSourceTest { - public class CompositeEndpointDataSourceTest + [Fact] + public void CreatesShallowCopyOf_ListOfEndpoints() { - [Fact] - public void CreatesShallowCopyOf_ListOfEndpoints() - { - // Arrange - var endpoint1 = CreateEndpoint("/a"); - var endpoint2 = CreateEndpoint("/b"); - var dataSource = new DefaultEndpointDataSource(new Endpoint[] { endpoint1, endpoint2 }); - var compositeDataSource = new CompositeEndpointDataSource(new[] { dataSource }); - - // Act - var endpoints = compositeDataSource.Endpoints; - - // Assert - Assert.NotSame(endpoints, dataSource.Endpoints); - Assert.Equal(endpoints, dataSource.Endpoints); - } + // Arrange + var endpoint1 = CreateEndpoint("/a"); + var endpoint2 = CreateEndpoint("/b"); + var dataSource = new DefaultEndpointDataSource(new Endpoint[] { endpoint1, endpoint2 }); + var compositeDataSource = new CompositeEndpointDataSource(new[] { dataSource }); + + // Act + var endpoints = compositeDataSource.Endpoints; + + // Assert + Assert.NotSame(endpoints, dataSource.Endpoints); + Assert.Equal(endpoints, dataSource.Endpoints); + } - [Fact] - public void Endpoints_ReturnsAllEndpoints_FromMultipleDataSources() + [Fact] + public void Endpoints_ReturnsAllEndpoints_FromMultipleDataSources() + { + // Arrange + var endpoint1 = CreateEndpoint("/a"); + var endpoint2 = CreateEndpoint("/b"); + var endpoint3 = CreateEndpoint("/c"); + var endpoint4 = CreateEndpoint("/d"); + var endpoint5 = CreateEndpoint("/e"); + var compositeDataSource = new CompositeEndpointDataSource(new[] { - // Arrange - var endpoint1 = CreateEndpoint("/a"); - var endpoint2 = CreateEndpoint("/b"); - var endpoint3 = CreateEndpoint("/c"); - var endpoint4 = CreateEndpoint("/d"); - var endpoint5 = CreateEndpoint("/e"); - var compositeDataSource = new CompositeEndpointDataSource(new[] - { new DefaultEndpointDataSource(new Endpoint[] { endpoint1, endpoint2 }), new DefaultEndpointDataSource(new Endpoint[] { endpoint3, endpoint4 }), new DefaultEndpointDataSource(new Endpoint[] { endpoint5 }), }); - // Act - var endpoints = compositeDataSource.Endpoints; - - // Assert - Assert.Collection( - endpoints, - (ep) => Assert.Same(endpoint1, ep), - (ep) => Assert.Same(endpoint2, ep), - (ep) => Assert.Same(endpoint3, ep), - (ep) => Assert.Same(endpoint4, ep), - (ep) => Assert.Same(endpoint5, ep)); - } - - [Fact] - public void DataSourceChanges_AreReflected_InEndpoints() - { - // Arrange1 - var endpoint1 = CreateEndpoint("/a"); - var dataSource1 = new DynamicEndpointDataSource(endpoint1); - var compositeDataSource = new CompositeEndpointDataSource(new[] { dataSource1 }); - - // Act1 - var endpoints = compositeDataSource.Endpoints; - - // Assert1 - var endpoint = Assert.Single(endpoints); - Assert.Same(endpoint1, endpoint); - - // Arrange2 - var endpoint2 = CreateEndpoint("/b"); - - // Act2 - dataSource1.AddEndpoint(endpoint2); - - // Assert2 - Assert.Collection( - compositeDataSource.Endpoints, - (ep) => Assert.Same(endpoint1, ep), - (ep) => Assert.Same(endpoint2, ep)); - - // Arrange3 - var endpoint3 = CreateEndpoint("/c"); - - // Act2 - dataSource1.AddEndpoint(endpoint3); - - // Assert2 - Assert.Collection( - compositeDataSource.Endpoints, - (ep) => Assert.Same(endpoint1, ep), - (ep) => Assert.Same(endpoint2, ep), - (ep) => Assert.Same(endpoint3, ep)); - } + // Act + var endpoints = compositeDataSource.Endpoints; + + // Assert + Assert.Collection( + endpoints, + (ep) => Assert.Same(endpoint1, ep), + (ep) => Assert.Same(endpoint2, ep), + (ep) => Assert.Same(endpoint3, ep), + (ep) => Assert.Same(endpoint4, ep), + (ep) => Assert.Same(endpoint5, ep)); + } - [Fact] - public void ConsumerChangeToken_IsRefreshed_WhenDataSourceCallbackFires() - { - // Arrange1 - var endpoint1 = CreateEndpoint("/a"); - var dataSource1 = new DynamicEndpointDataSource(endpoint1); - var compositeDataSource = new CompositeEndpointDataSource(new[] { dataSource1 }); - - // Act1 - var endpoints = compositeDataSource.Endpoints; - - // Assert1 - var changeToken1 = compositeDataSource.GetChangeToken(); - var token = Assert.IsType(changeToken1); - Assert.False(token.HasChanged); // initial state - - // Arrange2 - var endpoint2 = CreateEndpoint("/b"); - - // Act2 - dataSource1.AddEndpoint(endpoint2); - - // Assert2 - Assert.True(changeToken1.HasChanged); // old token is expected to be changed - var changeToken2 = compositeDataSource.GetChangeToken(); // new token is in a unchanged state - Assert.NotSame(changeToken2, changeToken1); - token = Assert.IsType(changeToken2); - Assert.False(token.HasChanged); - - // Arrange3 - var endpoint3 = CreateEndpoint("/c"); - - // Act2 - dataSource1.AddEndpoint(endpoint3); - - // Assert2 - Assert.True(changeToken2.HasChanged); // old token is expected to be changed - var changeToken3 = compositeDataSource.GetChangeToken(); // new token is in a unchanged state - Assert.NotSame(changeToken3, changeToken2); - Assert.NotSame(changeToken3, changeToken1); - token = Assert.IsType(changeToken3); - Assert.False(token.HasChanged); - } + [Fact] + public void DataSourceChanges_AreReflected_InEndpoints() + { + // Arrange1 + var endpoint1 = CreateEndpoint("/a"); + var dataSource1 = new DynamicEndpointDataSource(endpoint1); + var compositeDataSource = new CompositeEndpointDataSource(new[] { dataSource1 }); + + // Act1 + var endpoints = compositeDataSource.Endpoints; + + // Assert1 + var endpoint = Assert.Single(endpoints); + Assert.Same(endpoint1, endpoint); + + // Arrange2 + var endpoint2 = CreateEndpoint("/b"); + + // Act2 + dataSource1.AddEndpoint(endpoint2); + + // Assert2 + Assert.Collection( + compositeDataSource.Endpoints, + (ep) => Assert.Same(endpoint1, ep), + (ep) => Assert.Same(endpoint2, ep)); + + // Arrange3 + var endpoint3 = CreateEndpoint("/c"); + + // Act2 + dataSource1.AddEndpoint(endpoint3); + + // Assert2 + Assert.Collection( + compositeDataSource.Endpoints, + (ep) => Assert.Same(endpoint1, ep), + (ep) => Assert.Same(endpoint2, ep), + (ep) => Assert.Same(endpoint3, ep)); + } - private RouteEndpoint CreateEndpoint( - string template, - object defaults = null, - int order = 0, - string routeName = null) - { - return new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse(template, defaults, parameterPolicies: null), - order, - EndpointMetadataCollection.Empty, - null); - } + [Fact] + public void ConsumerChangeToken_IsRefreshed_WhenDataSourceCallbackFires() + { + // Arrange1 + var endpoint1 = CreateEndpoint("/a"); + var dataSource1 = new DynamicEndpointDataSource(endpoint1); + var compositeDataSource = new CompositeEndpointDataSource(new[] { dataSource1 }); + + // Act1 + var endpoints = compositeDataSource.Endpoints; + + // Assert1 + var changeToken1 = compositeDataSource.GetChangeToken(); + var token = Assert.IsType(changeToken1); + Assert.False(token.HasChanged); // initial state + + // Arrange2 + var endpoint2 = CreateEndpoint("/b"); + + // Act2 + dataSource1.AddEndpoint(endpoint2); + + // Assert2 + Assert.True(changeToken1.HasChanged); // old token is expected to be changed + var changeToken2 = compositeDataSource.GetChangeToken(); // new token is in a unchanged state + Assert.NotSame(changeToken2, changeToken1); + token = Assert.IsType(changeToken2); + Assert.False(token.HasChanged); + + // Arrange3 + var endpoint3 = CreateEndpoint("/c"); + + // Act2 + dataSource1.AddEndpoint(endpoint3); + + // Assert2 + Assert.True(changeToken2.HasChanged); // old token is expected to be changed + var changeToken3 = compositeDataSource.GetChangeToken(); // new token is in a unchanged state + Assert.NotSame(changeToken3, changeToken2); + Assert.NotSame(changeToken3, changeToken1); + token = Assert.IsType(changeToken3); + Assert.False(token.HasChanged); + } - private class CustomEndpointDataSource : EndpointDataSource - { - private readonly CancellationTokenSource _cts; - private readonly CancellationChangeToken _token; + private RouteEndpoint CreateEndpoint( + string template, + object defaults = null, + int order = 0, + string routeName = null) + { + return new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse(template, defaults, parameterPolicies: null), + order, + EndpointMetadataCollection.Empty, + null); + } - public CustomEndpointDataSource() - { - _cts = new CancellationTokenSource(); - _token = new CancellationChangeToken(_cts.Token); - } + private class CustomEndpointDataSource : EndpointDataSource + { + private readonly CancellationTokenSource _cts; + private readonly CancellationChangeToken _token; - public override IChangeToken GetChangeToken() => _token; - public override IReadOnlyList Endpoints => Array.Empty(); + public CustomEndpointDataSource() + { + _cts = new CancellationTokenSource(); + _token = new CancellationChangeToken(_cts.Token); } + + public override IChangeToken GetChangeToken() => _token; + public override IReadOnlyList Endpoints => Array.Empty(); } -} \ No newline at end of file +} diff --git a/src/Http/Routing/test/UnitTests/ConstraintMatcherTest.cs b/src/Http/Routing/test/UnitTests/ConstraintMatcherTest.cs index 949b070350..186c1d60b8 100644 --- a/src/Http/Routing/test/UnitTests/ConstraintMatcherTest.cs +++ b/src/Http/Routing/test/UnitTests/ConstraintMatcherTest.cs @@ -8,245 +8,244 @@ using Microsoft.Extensions.Logging.Testing; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class ConstraintMatcherTest { - public class ConstraintMatcherTest - { - private const string _name = "name"; + private const string _name = "name"; - [Fact] - public void MatchUrlGeneration_DoesNotLogData() - { - // Arrange - var sink = new TestSink(); - var logger = new TestLogger(_name, sink, enabled: true); + [Fact] + public void MatchUrlGeneration_DoesNotLogData() + { + // Arrange + var sink = new TestSink(); + var logger = new TestLogger(_name, sink, enabled: true); - var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); - var constraints = new Dictionary + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + var constraints = new Dictionary { {"a", new PassConstraint()}, {"b", new FailConstraint()} }; - // Act - RouteConstraintMatcher.Match( - constraints: constraints, - routeValues: routeValueDictionary, - httpContext: new Mock().Object, - route: new Mock().Object, - routeDirection: RouteDirection.UrlGeneration, - logger: logger); - - // Assert - // There are no BeginScopes called. - Assert.Empty(sink.Scopes); - - // There are no WriteCores called. - Assert.Empty(sink.Writes); - } + // Act + RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.UrlGeneration, + logger: logger); + + // Assert + // There are no BeginScopes called. + Assert.Empty(sink.Scopes); + + // There are no WriteCores called. + Assert.Empty(sink.Writes); + } - [Fact] - public void MatchFail_LogsCorrectData() - { - // Arrange & Act - var constraints = new Dictionary + [Fact] + public void MatchFail_LogsCorrectData() + { + // Arrange & Act + var constraints = new Dictionary { {"a", new PassConstraint()}, {"b", new FailConstraint()} }; - var sink = SetUpMatch(constraints, loggerEnabled: true); - var expectedMessage = "Route value 'value' with key 'b' did not match the constraint " + - $"'{typeof(FailConstraint).FullName}'"; - - // Assert - Assert.Empty(sink.Scopes); - var write = Assert.Single(sink.Writes); - Assert.Equal(expectedMessage, write.State?.ToString()); - } + var sink = SetUpMatch(constraints, loggerEnabled: true); + var expectedMessage = "Route value 'value' with key 'b' did not match the constraint " + + $"'{typeof(FailConstraint).FullName}'"; + + // Assert + Assert.Empty(sink.Scopes); + var write = Assert.Single(sink.Writes); + Assert.Equal(expectedMessage, write.State?.ToString()); + } - [Fact] - public void MatchSuccess_DoesNotLog() - { - // Arrange & Act - var constraints = new Dictionary + [Fact] + public void MatchSuccess_DoesNotLog() + { + // Arrange & Act + var constraints = new Dictionary { {"a", new PassConstraint()}, {"b", new PassConstraint()} }; - var sink = SetUpMatch(constraints, false); + var sink = SetUpMatch(constraints, false); - // Assert - Assert.Empty(sink.Scopes); - Assert.Empty(sink.Writes); - } + // Assert + Assert.Empty(sink.Scopes); + Assert.Empty(sink.Writes); + } - [Fact] - public void ReturnsTrueOnValidConstraints() - { - var constraints = new Dictionary + [Fact] + public void ReturnsTrueOnValidConstraints() + { + var constraints = new Dictionary { {"a", new PassConstraint()}, {"b", new PassConstraint()} }; - var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); - Assert.True(RouteConstraintMatcher.Match( - constraints: constraints, - routeValues: routeValueDictionary, - httpContext: new Mock().Object, - route: new Mock().Object, - routeDirection: RouteDirection.IncomingRequest, - logger: NullLogger.Instance)); - } + Assert.True(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } - [Fact] - public void ConstraintsGetTheRightKey() - { - var constraints = new Dictionary + [Fact] + public void ConstraintsGetTheRightKey() + { + var constraints = new Dictionary { {"a", new PassConstraint("a")}, {"b", new PassConstraint("b")} }; - var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); - Assert.True(RouteConstraintMatcher.Match( - constraints: constraints, - routeValues: routeValueDictionary, - httpContext: new Mock().Object, - route: new Mock().Object, - routeDirection: RouteDirection.IncomingRequest, - logger: NullLogger.Instance)); - } + Assert.True(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } - [Fact] - public void ReturnsFalseOnInvalidConstraintsThatDontMatch() - { - var constraints = new Dictionary + [Fact] + public void ReturnsFalseOnInvalidConstraintsThatDontMatch() + { + var constraints = new Dictionary { {"a", new FailConstraint()}, {"b", new FailConstraint()} }; - var routeValueDictionary = new RouteValueDictionary(new { c = "value", d = "value" }); + var routeValueDictionary = new RouteValueDictionary(new { c = "value", d = "value" }); - Assert.False(RouteConstraintMatcher.Match( - constraints: constraints, - routeValues: routeValueDictionary, - httpContext: new Mock().Object, - route: new Mock().Object, - routeDirection: RouteDirection.IncomingRequest, - logger: NullLogger.Instance)); - } + Assert.False(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } - [Fact] - public void ReturnsFalseOnInvalidConstraintsThatMatch() - { - var constraints = new Dictionary + [Fact] + public void ReturnsFalseOnInvalidConstraintsThatMatch() + { + var constraints = new Dictionary { {"a", new FailConstraint()}, {"b", new FailConstraint()} }; - var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); - Assert.False(RouteConstraintMatcher.Match( - constraints: constraints, - routeValues: routeValueDictionary, - httpContext: new Mock().Object, - route: new Mock().Object, - routeDirection: RouteDirection.IncomingRequest, - logger: NullLogger.Instance)); - } + Assert.False(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } - [Fact] - public void ReturnsFalseOnValidAndInvalidConstraintsMixThatMatch() - { - var constraints = new Dictionary + [Fact] + public void ReturnsFalseOnValidAndInvalidConstraintsMixThatMatch() + { + var constraints = new Dictionary { {"a", new PassConstraint()}, {"b", new FailConstraint()} }; - var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); - Assert.False(RouteConstraintMatcher.Match( - constraints: constraints, - routeValues: routeValueDictionary, - httpContext: new Mock().Object, - route: new Mock().Object, - routeDirection: RouteDirection.IncomingRequest, - logger: NullLogger.Instance)); - } + Assert.False(RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } - [Fact] - public void ReturnsTrueOnNullInput() - { - Assert.True(RouteConstraintMatcher.Match( - constraints: null, - routeValues: new RouteValueDictionary(), - httpContext: new Mock().Object, - route: new Mock().Object, - routeDirection: RouteDirection.IncomingRequest, - logger: NullLogger.Instance)); - } + [Fact] + public void ReturnsTrueOnNullInput() + { + Assert.True(RouteConstraintMatcher.Match( + constraints: null, + routeValues: new RouteValueDictionary(), + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: NullLogger.Instance)); + } - private TestSink SetUpMatch(Dictionary constraints, bool loggerEnabled) + private TestSink SetUpMatch(Dictionary constraints, bool loggerEnabled) + { + // Arrange + var sink = new TestSink(); + var logger = new TestLogger(_name, sink, loggerEnabled); + + var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); + + // Act + RouteConstraintMatcher.Match( + constraints: constraints, + routeValues: routeValueDictionary, + httpContext: new Mock().Object, + route: new Mock().Object, + routeDirection: RouteDirection.IncomingRequest, + logger: logger); + return sink; + } + + private class PassConstraint : IRouteConstraint + { + private readonly string _expectedKey; + + public PassConstraint(string expectedKey = null) { - // Arrange - var sink = new TestSink(); - var logger = new TestLogger(_name, sink, loggerEnabled); - - var routeValueDictionary = new RouteValueDictionary(new { a = "value", b = "value" }); - - // Act - RouteConstraintMatcher.Match( - constraints: constraints, - routeValues: routeValueDictionary, - httpContext: new Mock().Object, - route: new Mock().Object, - routeDirection: RouteDirection.IncomingRequest, - logger: logger); - return sink; + _expectedKey = expectedKey; } - private class PassConstraint : IRouteConstraint + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) { - private readonly string _expectedKey; - - public PassConstraint(string expectedKey = null) + if (_expectedKey != null) { - _expectedKey = expectedKey; + Assert.Equal(_expectedKey, routeKey); } - public bool Match( - HttpContext httpContext, - IRouter route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) - { - if (_expectedKey != null) - { - Assert.Equal(_expectedKey, routeKey); - } - - return true; - } + return true; } + } - private class FailConstraint : IRouteConstraint + private class FailConstraint : IRouteConstraint + { + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) { - public bool Match( - HttpContext httpContext, - IRouter route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) - { - return false; - } + return false; } } -} \ No newline at end of file +} diff --git a/src/Http/Routing/test/UnitTests/Constraints/AlphaRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/AlphaRouteConstraintTests.cs index 2c3f4acfc3..1ef3882fa3 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/AlphaRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/AlphaRouteConstraintTests.cs @@ -4,29 +4,28 @@ using Microsoft.AspNetCore.Routing.Constraints; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class AlphaRouteConstraintTests { - public class AlphaRouteConstraintTests + [Theory] + [InlineData("alpha", true)] + [InlineData("a1pha", false)] + [InlineData("ALPHA", true)] + [InlineData("A1PHA", false)] + [InlineData("alPHA", true)] + [InlineData("A1pHA", false)] + [InlineData("AlpHA╥", false)] + [InlineData("", true)] + public void AlphaRouteConstraintTest(string parameterValue, bool expected) { - [Theory] - [InlineData("alpha", true)] - [InlineData("a1pha", false)] - [InlineData("ALPHA", true)] - [InlineData("A1PHA", false)] - [InlineData("alPHA", true)] - [InlineData("A1pHA", false)] - [InlineData("AlpHA╥", false)] - [InlineData("", true)] - public void AlphaRouteConstraintTest(string parameterValue, bool expected) - { - // Arrange - var constraint = new AlphaRouteConstraint(); + // Arrange + var constraint = new AlphaRouteConstraint(); - // Act - var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/BoolRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/BoolRouteConstraintTests.cs index f95890fda8..337fca54a8 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/BoolRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/BoolRouteConstraintTests.cs @@ -9,32 +9,31 @@ using Microsoft.AspNetCore.Routing.Constraints; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class BoolRouteConstraintTests { - public class BoolRouteConstraintTests + [Theory] + [InlineData("true", true)] + [InlineData("TruE", true)] + [InlineData("false", true)] + [InlineData("FalSe", true)] + [InlineData(" FalSe", true)] + [InlineData("True ", true)] + [InlineData(" False ", true)] + [InlineData(true, true)] + [InlineData(false, true)] + [InlineData(1, false)] + [InlineData("not-parseable-as-bool", false)] + public void BoolRouteConstraint(object parameterValue, bool expected) { - [Theory] - [InlineData("true", true)] - [InlineData("TruE", true)] - [InlineData("false", true)] - [InlineData("FalSe", true)] - [InlineData(" FalSe", true)] - [InlineData("True ", true)] - [InlineData(" False ", true)] - [InlineData(true, true)] - [InlineData(false, true)] - [InlineData(1, false)] - [InlineData("not-parseable-as-bool", false)] - public void BoolRouteConstraint(object parameterValue, bool expected) - { - // Arrange - var constraint = new BoolRouteConstraint(); + // Arrange + var constraint = new BoolRouteConstraint(); - // Act - var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/CompositeRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/CompositeRouteConstraintTests.cs index 7a25b9b85e..bb33bcef0d 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/CompositeRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/CompositeRouteConstraintTests.cs @@ -8,47 +8,46 @@ using Microsoft.AspNetCore.Routing.Constraints; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class CompositeRouteConstraintTests { - public class CompositeRouteConstraintTests + [Theory] + [InlineData(true, true, true)] + [InlineData(true, false, false)] + [InlineData(false, true, false)] + [InlineData(false, false, false)] + public void CompositeRouteConstraint_Match_CallsMatchOnInnerConstraints( + bool inner1Result, + bool inner2Result, + bool expected) { - [Theory] - [InlineData(true, true, true)] - [InlineData(true, false, false)] - [InlineData(false, true, false)] - [InlineData(false, false, false)] - public void CompositeRouteConstraint_Match_CallsMatchOnInnerConstraints( - bool inner1Result, - bool inner2Result, - bool expected) - { - // Arrange - var inner1 = MockConstraintWithResult(inner1Result); - var inner2 = MockConstraintWithResult(inner2Result); + // Arrange + var inner1 = MockConstraintWithResult(inner1Result); + var inner2 = MockConstraintWithResult(inner2Result); - // Act - var constraint = new CompositeRouteConstraint(new[] { inner1.Object, inner2.Object }); - var actual = ConstraintsTestHelper.TestConstraint(constraint, null); + // Act + var constraint = new CompositeRouteConstraint(new[] { inner1.Object, inner2.Object }); + var actual = ConstraintsTestHelper.TestConstraint(constraint, null); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); + } - static readonly Expression> ConstraintMatchMethodExpression = - c => c.Match( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny()); + static readonly Expression> ConstraintMatchMethodExpression = + c => c.Match( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()); - private static Mock MockConstraintWithResult(bool result) - { - var mock = new Mock(); - mock.Setup(ConstraintMatchMethodExpression) - .Returns(result) - .Verifiable(); - return mock; - } + private static Mock MockConstraintWithResult(bool result) + { + var mock = new Mock(); + mock.Setup(ConstraintMatchMethodExpression) + .Returns(result) + .Verifiable(); + return mock; } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/ConstraintsTestHelper.cs b/src/Http/Routing/test/UnitTests/Constraints/ConstraintsTestHelper.cs index 6a77eb095b..aa612f03b8 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/ConstraintsTestHelper.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/ConstraintsTestHelper.cs @@ -6,16 +6,15 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Moq; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class ConstraintsTestHelper { - public class ConstraintsTestHelper + public static bool TestConstraint(IRouteConstraint constraint, object value) { - public static bool TestConstraint(IRouteConstraint constraint, object value) - { - var parameterName = "fake"; - var values = new RouteValueDictionary() { { parameterName, value } }; - var routeDirection = RouteDirection.IncomingRequest; - return constraint.Match(httpContext: null, route: null, parameterName, values, routeDirection); - } + var parameterName = "fake"; + var values = new RouteValueDictionary() { { parameterName, value } }; + var routeDirection = RouteDirection.IncomingRequest; + return constraint.Match(httpContext: null, route: null, parameterName, values, routeDirection); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/DateTimeRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/DateTimeRouteConstraintTests.cs index 6622e04112..a0ed6690c5 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/DateTimeRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/DateTimeRouteConstraintTests.cs @@ -6,48 +6,47 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Routing.Constraints; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class DateTimeRouteConstraintTests { - public class DateTimeRouteConstraintTests + public static IEnumerable GetDateTimeObject { - public static IEnumerable GetDateTimeObject + get { - get + yield return new object[] { - yield return new object[] - { DateTime.Now, true - }; - } + }; } + } - [Theory] - [InlineData("12/25/2009", true)] - [InlineData("25/12/2009 11:45:00 PM", false)] - [InlineData("25/12/2009", false)] - [InlineData("11:45:00 PM", true)] - [InlineData("11:45:00", true)] - [InlineData("11:45", true)] - [InlineData("11", false)] - [InlineData("", false)] - [InlineData("Apr 5 2009 11:45:00 PM", true)] - [InlineData("April 5 2009 11:45:00 PM", true)] - [InlineData("12/25/2009 11:45:00 PM", true)] - [InlineData("2009-05-12T11:45:00Z", true)] - [InlineData("not-parseable-as-date", false)] - [InlineData(false, false)] - [MemberData(nameof(GetDateTimeObject))] - public void DateTimeRouteConstraint(object parameterValue, bool expected) - { - // Arrange - var constraint = new DateTimeRouteConstraint(); + [Theory] + [InlineData("12/25/2009", true)] + [InlineData("25/12/2009 11:45:00 PM", false)] + [InlineData("25/12/2009", false)] + [InlineData("11:45:00 PM", true)] + [InlineData("11:45:00", true)] + [InlineData("11:45", true)] + [InlineData("11", false)] + [InlineData("", false)] + [InlineData("Apr 5 2009 11:45:00 PM", true)] + [InlineData("April 5 2009 11:45:00 PM", true)] + [InlineData("12/25/2009 11:45:00 PM", true)] + [InlineData("2009-05-12T11:45:00Z", true)] + [InlineData("not-parseable-as-date", false)] + [InlineData(false, false)] + [MemberData(nameof(GetDateTimeObject))] + public void DateTimeRouteConstraint(object parameterValue, bool expected) + { + // Arrange + var constraint = new DateTimeRouteConstraint(); - // Act - var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/DecimalRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/DecimalRouteConstraintTests.cs index 9acba3ff82..d0ed1738bb 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/DecimalRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/DecimalRouteConstraintTests.cs @@ -5,39 +5,38 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Routing.Constraints; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class DecimalRouteConstraintTests { - public class DecimalRouteConstraintTests + public static IEnumerable GetDecimalObject { - public static IEnumerable GetDecimalObject + get { - get + yield return new object[] { - yield return new object[] - { 2m, true - }; - } + }; } + } - [Theory] - [InlineData("3.14", true)] - [InlineData("9223372036854775808.9223372036854775808", true)] - [InlineData("1.79769313486232E+300", false)] - [InlineData("not-parseable-as-decimal", false)] - [InlineData(false, false)] - [MemberData(nameof(GetDecimalObject))] - public void DecimalRouteConstraint_ApplyConstraint(object parameterValue, bool expected) - { - // Arrange - var constraint = new DecimalRouteConstraint(); + [Theory] + [InlineData("3.14", true)] + [InlineData("9223372036854775808.9223372036854775808", true)] + [InlineData("1.79769313486232E+300", false)] + [InlineData("not-parseable-as-decimal", false)] + [InlineData(false, false)] + [MemberData(nameof(GetDecimalObject))] + public void DecimalRouteConstraint_ApplyConstraint(object parameterValue, bool expected) + { + // Arrange + var constraint = new DecimalRouteConstraint(); - // Act - var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/DoubleRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/DoubleRouteConstraintTests.cs index fef54a2dfe..0881ca8cd2 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/DoubleRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/DoubleRouteConstraintTests.cs @@ -4,27 +4,26 @@ using Microsoft.AspNetCore.Routing.Constraints; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class DoubleRouteConstraintTests { - public class DoubleRouteConstraintTests + [Theory] + [InlineData("3.14", true)] + [InlineData(3.14f, true)] + [InlineData(3.14d, true)] + [InlineData("1.79769313486232E+300", true)] + [InlineData("not-parseable-as-double", false)] + [InlineData(false, false)] + public void DoubleRouteConstraint_ApplyConstraint(object parameterValue, bool expected) { - [Theory] - [InlineData("3.14", true)] - [InlineData(3.14f, true)] - [InlineData(3.14d, true)] - [InlineData("1.79769313486232E+300", true)] - [InlineData("not-parseable-as-double", false)] - [InlineData(false, false)] - public void DoubleRouteConstraint_ApplyConstraint(object parameterValue, bool expected) - { - // Arrange - var constraint = new DoubleRouteConstraint(); + // Arrange + var constraint = new DoubleRouteConstraint(); - // Act - var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/FIleNameRouteConstraintTest.cs b/src/Http/Routing/test/UnitTests/Constraints/FIleNameRouteConstraintTest.cs index a76878a01f..d901a6a7f5 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/FIleNameRouteConstraintTest.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/FIleNameRouteConstraintTest.cs @@ -3,15 +3,15 @@ using Xunit; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +public class FileNameRouteConstraintTest { - public class FileNameRouteConstraintTest + public static TheoryData FileNameData { - public static TheoryData FileNameData + get { - get - { - return new TheoryData() + return new TheoryData() { "hello.txt", "hello.txt.jpg", @@ -23,32 +23,32 @@ namespace Microsoft.AspNetCore.Routing.Constraints ".a", "/.......a" }; - } } + } - [Theory] - [MemberData(nameof(FileNameData))] - public void Match_RouteValue_IsFileName(object value) - { - // Arrange - var constraint = new FileNameRouteConstraint(); + [Theory] + [MemberData(nameof(FileNameData))] + public void Match_RouteValue_IsFileName(object value) + { + // Arrange + var constraint = new FileNameRouteConstraint(); - var values = new RouteValueDictionary(); - values.Add("path", value); + var values = new RouteValueDictionary(); + values.Add("path", value); - // Act - var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest); + // Act + var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest); - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - public static TheoryData NonFileNameData + public static TheoryData NonFileNameData + { + get { - get - { - return new TheoryData() + return new TheoryData() { null, string.Empty, @@ -62,39 +62,38 @@ namespace Microsoft.AspNetCore.Routing.Constraints "/////hello.", "a/b./.c/d.", }; - } } + } - [Theory] - [MemberData(nameof(NonFileNameData))] - public void Match_RouteValue_IsNotFileName(object value) - { - // Arrange - var constraint = new FileNameRouteConstraint(); + [Theory] + [MemberData(nameof(NonFileNameData))] + public void Match_RouteValue_IsNotFileName(object value) + { + // Arrange + var constraint = new FileNameRouteConstraint(); - var values = new RouteValueDictionary(); - values.Add("path", value); + var values = new RouteValueDictionary(); + values.Add("path", value); - // Act - var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest); + // Act + var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Fact] - public void Match_MissingValue_IsNotFileName() - { - // Arrange - var constraint = new FileNameRouteConstraint(); + [Fact] + public void Match_MissingValue_IsNotFileName() + { + // Arrange + var constraint = new FileNameRouteConstraint(); - var values = new RouteValueDictionary(); + var values = new RouteValueDictionary(); - // Act - var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest); + // Act + var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/FloatRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/FloatRouteConstraintTests.cs index 4ce6a72c19..6a51450a83 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/FloatRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/FloatRouteConstraintTests.cs @@ -4,26 +4,25 @@ using Microsoft.AspNetCore.Routing.Constraints; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class FloatRouteConstraintTests { - public class FloatRouteConstraintTests + [Theory] + [InlineData("3.14", true)] + [InlineData(3.14, true)] + [InlineData("not-parseable-as-float", false)] + [InlineData(false, false)] + [InlineData("1.79769313486232E+300", true)] // Parses as infinity + public void FloatRouteConstraint_ApplyConstraint(object parameterValue, bool expected) { - [Theory] - [InlineData("3.14", true)] - [InlineData(3.14, true)] - [InlineData("not-parseable-as-float", false)] - [InlineData(false, false)] - [InlineData("1.79769313486232E+300", true)] // Parses as infinity - public void FloatRouteConstraint_ApplyConstraint(object parameterValue, bool expected) - { - // Arrange - var constraint = new FloatRouteConstraint(); + // Arrange + var constraint = new FloatRouteConstraint(); - // Act - var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/GuidRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/GuidRouteConstraintTests.cs index 32f2c871e9..9954746ec8 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/GuidRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/GuidRouteConstraintTests.cs @@ -6,32 +6,31 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Routing.Constraints; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class GuidRouteConstraintTests { - public class GuidRouteConstraintTests - { - [Theory] - [InlineData("12345678-1234-1234-1234-123456789012", false, true)] - [InlineData("12345678-1234-1234-1234-123456789012", true, true)] - [InlineData("12345678901234567890123456789012", false, true)] - [InlineData("not-parseable-as-guid", false, false)] - [InlineData(12, false, false)] + [Theory] + [InlineData("12345678-1234-1234-1234-123456789012", false, true)] + [InlineData("12345678-1234-1234-1234-123456789012", true, true)] + [InlineData("12345678901234567890123456789012", false, true)] + [InlineData("not-parseable-as-guid", false, false)] + [InlineData(12, false, false)] - public void GuidRouteConstraint_ApplyConstraint(object parameterValue, bool parseBeforeTest, bool expected) + public void GuidRouteConstraint_ApplyConstraint(object parameterValue, bool parseBeforeTest, bool expected) + { + // Arrange + if (parseBeforeTest) { - // Arrange - if (parseBeforeTest) - { - parameterValue = Guid.Parse(parameterValue.ToString()); - } + parameterValue = Guid.Parse(parameterValue.ToString()); + } - var constraint = new GuidRouteConstraint(); + var constraint = new GuidRouteConstraint(); - // Act - var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/HttpMethodRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/HttpMethodRouteConstraintTests.cs index ba344419af..6ed74ea9f4 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/HttpMethodRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/HttpMethodRouteConstraintTests.cs @@ -5,90 +5,89 @@ using Microsoft.AspNetCore.Http; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +public class HttpMethodRouteConstraintTests { - public class HttpMethodRouteConstraintTests + [Theory] + [InlineData("GET")] + [InlineData("PosT")] + public void HttpMethodRouteConstraint_IncomingRequest_AcceptsAllowedMethods(string httpMethod) + { + // Arrange + var constraint = new HttpMethodRouteConstraint("GET", "post"); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = httpMethod; + var route = Mock.Of(); + + var values = new RouteValueDictionary(new { }); + + // Act + var result = constraint.Match(httpContext, route, "httpMethod", values, RouteDirection.IncomingRequest); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("OPTIONS")] + [InlineData("SomeRandomThing")] + public void HttpMethodRouteConstraint_IncomingRequest_RejectsOtherMethods(string httpMethod) + { + // Arrange + var constraint = new HttpMethodRouteConstraint("GET", "post"); + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = httpMethod; + var route = Mock.Of(); + + var values = new RouteValueDictionary(new { }); + + // Act + var result = constraint.Match(httpContext, route, "httpMethod", values, RouteDirection.IncomingRequest); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData("GET")] + [InlineData("PosT")] + public void HttpMethodRouteConstraint_UrlGeneration_AcceptsAllowedMethods(string httpMethod) { - [Theory] - [InlineData("GET")] - [InlineData("PosT")] - public void HttpMethodRouteConstraint_IncomingRequest_AcceptsAllowedMethods(string httpMethod) - { - // Arrange - var constraint = new HttpMethodRouteConstraint("GET", "post"); - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = httpMethod; - var route = Mock.Of(); - - var values = new RouteValueDictionary(new { }); - - // Act - var result = constraint.Match(httpContext, route, "httpMethod", values, RouteDirection.IncomingRequest); - - // Assert - Assert.True(result); - } - - [Theory] - [InlineData("OPTIONS")] - [InlineData("SomeRandomThing")] - public void HttpMethodRouteConstraint_IncomingRequest_RejectsOtherMethods(string httpMethod) - { - // Arrange - var constraint = new HttpMethodRouteConstraint("GET", "post"); - - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = httpMethod; - var route = Mock.Of(); - - var values = new RouteValueDictionary(new { }); - - // Act - var result = constraint.Match(httpContext, route, "httpMethod", values, RouteDirection.IncomingRequest); - - // Assert - Assert.False(result); - } - - [Theory] - [InlineData("GET")] - [InlineData("PosT")] - public void HttpMethodRouteConstraint_UrlGeneration_AcceptsAllowedMethods(string httpMethod) - { - // Arrange - var constraint = new HttpMethodRouteConstraint("GET", "post"); - - var httpContext = new DefaultHttpContext(); - var route = Mock.Of(); - - var values = new RouteValueDictionary(new { httpMethod = httpMethod }); - - // Act - var result = constraint.Match(httpContext, route, "httpMethod", values, RouteDirection.UrlGeneration); - - // Assert - Assert.True(result); - } - - [Theory] - [InlineData("OPTIONS")] - [InlineData("SomeRandomThing")] - public void HttpMethodRouteConstraint_UrlGeneration_RejectsOtherMethods(string httpMethod) - { - // Arrange - var constraint = new HttpMethodRouteConstraint("GET", "post"); - - var httpContext = new DefaultHttpContext(); - var route = Mock.Of(); - - var values = new RouteValueDictionary(new { httpMethod = httpMethod }); - - // Act - var result = constraint.Match(httpContext, route, "httpMethod", values, RouteDirection.UrlGeneration); - - // Assert - Assert.False(result); - } + // Arrange + var constraint = new HttpMethodRouteConstraint("GET", "post"); + + var httpContext = new DefaultHttpContext(); + var route = Mock.Of(); + + var values = new RouteValueDictionary(new { httpMethod = httpMethod }); + + // Act + var result = constraint.Match(httpContext, route, "httpMethod", values, RouteDirection.UrlGeneration); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("OPTIONS")] + [InlineData("SomeRandomThing")] + public void HttpMethodRouteConstraint_UrlGeneration_RejectsOtherMethods(string httpMethod) + { + // Arrange + var constraint = new HttpMethodRouteConstraint("GET", "post"); + + var httpContext = new DefaultHttpContext(); + var route = Mock.Of(); + + var values = new RouteValueDictionary(new { httpMethod = httpMethod }); + + // Act + var result = constraint.Match(httpContext, route, "httpMethod", values, RouteDirection.UrlGeneration); + + // Assert + Assert.False(result); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/IntRouteConstraintsTests.cs b/src/Http/Routing/test/UnitTests/Constraints/IntRouteConstraintsTests.cs index 26e196c736..d69bc1d793 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/IntRouteConstraintsTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/IntRouteConstraintsTests.cs @@ -4,26 +4,25 @@ using Microsoft.AspNetCore.Routing.Constraints; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class IntRouteConstraintsTests { - public class IntRouteConstraintsTests + [Theory] + [InlineData(42, true)] + [InlineData("42", true)] + [InlineData(3.14, false)] + [InlineData("43.567", false)] + [InlineData("42a", false)] + public void IntRouteConstraint_Match_AppliesConstraint(object parameterValue, bool expected) { - [Theory] - [InlineData(42, true)] - [InlineData("42", true)] - [InlineData(3.14, false)] - [InlineData("43.567", false)] - [InlineData("42a", false)] - public void IntRouteConstraint_Match_AppliesConstraint(object parameterValue, bool expected) - { - // Arrange - var constraint = new IntRouteConstraint(); + // Arrange + var constraint = new IntRouteConstraint(); - // Act - var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/LengthRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/LengthRouteConstraintTests.cs index c5311a21fe..df8eb6d2dc 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/LengthRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/LengthRouteConstraintTests.cs @@ -5,99 +5,98 @@ using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Testing; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class LengthRouteConstraintTests { - public class LengthRouteConstraintTests + [Theory] + [InlineData(3, "123", true)] + [InlineData(3, "1234", false)] + [InlineData(0, "", true)] + public void LengthRouteConstraint_ExactLength_Tests(int length, string parameterValue, bool expected) + { + // Arrange + var constraint = new LengthRouteConstraint(length); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(3, 5, "12", false)] + [InlineData(3, 5, "123", true)] + [InlineData(3, 5, "1234", true)] + [InlineData(3, 5, "12345", true)] + [InlineData(3, 5, "123456", false)] + public void LengthRouteConstraint_Range_Tests(int min, int max, string parameterValue, bool expected) + { + // Arrange + var constraint = new LengthRouteConstraint(min, max); + + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + + // Assert + Assert.Equal(expected, actual); + } + + [Fact] + public void LengthRouteConstraint_SettingLengthLessThanZero_Throws() + { + // Arrange + var expectedMessage = "Value must be greater than or equal to 0."; + + // Act & Assert + ExceptionAssert.ThrowsArgumentOutOfRange( + () => new LengthRouteConstraint(-1), + "length", + expectedMessage, + -1); + } + + [Fact] + public void LengthRouteConstraint_SettingMinLengthLessThanZero_Throws() + { + // Arrange + var expectedMessage = "Value must be greater than or equal to 0."; + + // Act & Assert + ExceptionAssert.ThrowsArgumentOutOfRange( + () => new LengthRouteConstraint(-1, 3), + "minLength", + expectedMessage, + -1); + } + + [Fact] + public void LengthRouteConstraint_SettingMaxLengthLessThanZero_Throws() + { + // Arrange + var expectedMessage = "Value must be greater than or equal to 0."; + + // Act & Assert + ExceptionAssert.ThrowsArgumentOutOfRange( + () => new LengthRouteConstraint(0, -1), + "maxLength", + expectedMessage, + -1); + } + + [Fact] + public void LengthRouteConstraint_MinGreaterThanMax_Throws() { - [Theory] - [InlineData(3, "123", true)] - [InlineData(3, "1234", false)] - [InlineData(0, "", true)] - public void LengthRouteConstraint_ExactLength_Tests(int length, string parameterValue, bool expected) - { - // Arrange - var constraint = new LengthRouteConstraint(length); - - // Act - var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); - - // Assert - Assert.Equal(expected, actual); - } - - [Theory] - [InlineData(3, 5, "12", false)] - [InlineData(3, 5, "123", true)] - [InlineData(3, 5, "1234", true)] - [InlineData(3, 5, "12345", true)] - [InlineData(3, 5, "123456", false)] - public void LengthRouteConstraint_Range_Tests(int min, int max, string parameterValue, bool expected) - { - // Arrange - var constraint = new LengthRouteConstraint(min, max); - - // Act - var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); - - // Assert - Assert.Equal(expected, actual); - } - - [Fact] - public void LengthRouteConstraint_SettingLengthLessThanZero_Throws() - { - // Arrange - var expectedMessage = "Value must be greater than or equal to 0."; - - // Act & Assert - ExceptionAssert.ThrowsArgumentOutOfRange( - () => new LengthRouteConstraint(-1), - "length", - expectedMessage, - -1); - } - - [Fact] - public void LengthRouteConstraint_SettingMinLengthLessThanZero_Throws() - { - // Arrange - var expectedMessage = "Value must be greater than or equal to 0."; - - // Act & Assert - ExceptionAssert.ThrowsArgumentOutOfRange( - () => new LengthRouteConstraint(-1, 3), - "minLength", - expectedMessage, - -1); - } - - [Fact] - public void LengthRouteConstraint_SettingMaxLengthLessThanZero_Throws() - { - // Arrange - var expectedMessage = "Value must be greater than or equal to 0."; - - // Act & Assert - ExceptionAssert.ThrowsArgumentOutOfRange( - () => new LengthRouteConstraint(0, -1), - "maxLength", - expectedMessage, - -1); - } - - [Fact] - public void LengthRouteConstraint_MinGreaterThanMax_Throws() - { - // Arrange - var expectedMessage = "The value for argument 'minLength' should be less than or equal to the " + - "value for the argument 'maxLength'."; - - // Arrange Act & Assert - ExceptionAssert.ThrowsArgumentOutOfRange( - () => new LengthRouteConstraint(3, 2), - "minLength", - expectedMessage, - 3); - } + // Arrange + var expectedMessage = "The value for argument 'minLength' should be less than or equal to the " + + "value for the argument 'maxLength'."; + + // Arrange Act & Assert + ExceptionAssert.ThrowsArgumentOutOfRange( + () => new LengthRouteConstraint(3, 2), + "minLength", + expectedMessage, + 3); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/LongRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/LongRouteConstraintTests.cs index ecb2a661cd..ba2c29130c 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/LongRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/LongRouteConstraintTests.cs @@ -4,28 +4,27 @@ using Microsoft.AspNetCore.Routing.Constraints; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class LongRouteConstraintTests { - public class LongRouteConstraintTests + [Theory] + [InlineData(42, true)] + [InlineData(42L, true)] + [InlineData("42", true)] + [InlineData("9223372036854775807", true)] + [InlineData(3.14, false)] + [InlineData("43.567", false)] + [InlineData("42a", false)] + public void LongRouteConstraintTest(object parameterValue, bool expected) { - [Theory] - [InlineData(42, true)] - [InlineData(42L, true)] - [InlineData("42", true)] - [InlineData("9223372036854775807", true)] - [InlineData(3.14, false)] - [InlineData("43.567", false)] - [InlineData("42a", false)] - public void LongRouteConstraintTest(object parameterValue, bool expected) - { - // Arrange - var constraint = new LongRouteConstraint(); + // Arrange + var constraint = new LongRouteConstraint(); - // Act - var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/MaxLengthRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/MaxLengthRouteConstraintTests.cs index c6deec8d75..684c353915 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/MaxLengthRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/MaxLengthRouteConstraintTests.cs @@ -5,39 +5,38 @@ using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Testing; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class MaxLengthRouteConstraintTests { - public class MaxLengthRouteConstraintTests + [Theory] + [InlineData(3, "", true)] + [InlineData(3, "12", true)] + [InlineData(3, "123", true)] + [InlineData(3, "1234", false)] + public void MaxLengthRouteConstraint_ApplyConstraint(int min, string parameterValue, bool expected) { - [Theory] - [InlineData(3, "", true)] - [InlineData(3, "12", true)] - [InlineData(3, "123", true)] - [InlineData(3, "1234", false)] - public void MaxLengthRouteConstraint_ApplyConstraint(int min, string parameterValue, bool expected) - { - // Arrange - var constraint = new MaxLengthRouteConstraint(min); + // Arrange + var constraint = new MaxLengthRouteConstraint(min); - // Act - var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); + } - [Fact] - public void MaxLengthRouteConstraint_SettingMaxLengthLessThanZero_Throws() - { - // Arrange - var expectedMessage = "Value must be greater than or equal to 0."; + [Fact] + public void MaxLengthRouteConstraint_SettingMaxLengthLessThanZero_Throws() + { + // Arrange + var expectedMessage = "Value must be greater than or equal to 0."; - // Act & Assert - ExceptionAssert.ThrowsArgumentOutOfRange( - () => new MaxLengthRouteConstraint(-1), - "maxLength", - expectedMessage, - -1); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentOutOfRange( + () => new MaxLengthRouteConstraint(-1), + "maxLength", + expectedMessage, + -1); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/MaxRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/MaxRouteConstraintTests.cs index 475eaa94e0..a9424ebdd7 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/MaxRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/MaxRouteConstraintTests.cs @@ -4,24 +4,23 @@ using Microsoft.AspNetCore.Routing.Constraints; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class MaxRouteConstraintTests { - public class MaxRouteConstraintTests + [Theory] + [InlineData(3, 2, true)] + [InlineData(3, 3, true)] + [InlineData(3, 4, false)] + public void MaxRouteConstraint_ApplyConstraint(long max, int parameterValue, bool expected) { - [Theory] - [InlineData(3, 2, true)] - [InlineData(3, 3, true)] - [InlineData(3, 4, false)] - public void MaxRouteConstraint_ApplyConstraint(long max, int parameterValue, bool expected) - { - // Arrange - var constraint = new MaxRouteConstraint(max); + // Arrange + var constraint = new MaxRouteConstraint(max); - // Act - var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/MinLengthRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/MinLengthRouteConstraintTests.cs index 92a411d5e3..181d8a7eae 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/MinLengthRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/MinLengthRouteConstraintTests.cs @@ -5,39 +5,38 @@ using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Testing; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class MinLengthRouteConstraintTests { - public class MinLengthRouteConstraintTests + [Theory] + [InlineData(3, "1234", true)] + [InlineData(3, "123", true)] + [InlineData(3, "12", false)] + [InlineData(3, "", false)] + public void MinLengthRouteConstraint_ApplyConstraint(int min, string parameterValue, bool expected) { - [Theory] - [InlineData(3, "1234", true)] - [InlineData(3, "123", true)] - [InlineData(3, "12", false)] - [InlineData(3, "", false)] - public void MinLengthRouteConstraint_ApplyConstraint(int min, string parameterValue, bool expected) - { - // Arrange - var constraint = new MinLengthRouteConstraint(min); + // Arrange + var constraint = new MinLengthRouteConstraint(min); - // Act - var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); + } - [Fact] - public void MinLengthRouteConstraint_SettingMinLengthLessThanZero_Throws() - { - // Arrange - var expectedMessage = "Value must be greater than or equal to 0."; + [Fact] + public void MinLengthRouteConstraint_SettingMinLengthLessThanZero_Throws() + { + // Arrange + var expectedMessage = "Value must be greater than or equal to 0."; - // Act & Assert - ExceptionAssert.ThrowsArgumentOutOfRange( - () => new MinLengthRouteConstraint(-1), - "minLength", - expectedMessage, - -1); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentOutOfRange( + () => new MinLengthRouteConstraint(-1), + "minLength", + expectedMessage, + -1); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/MinRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/MinRouteConstraintTests.cs index 772ddc3669..b008ef8814 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/MinRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/MinRouteConstraintTests.cs @@ -4,24 +4,23 @@ using Microsoft.AspNetCore.Routing.Constraints; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class MinRouteConstraintTests { - public class MinRouteConstraintTests + [Theory] + [InlineData(3, 4, true)] + [InlineData(3, 3, true)] + [InlineData(3, 2, false)] + public void MinRouteConstraint_ApplyConstraint(long min, int parameterValue, bool expected) { - [Theory] - [InlineData(3, 4, true)] - [InlineData(3, 3, true)] - [InlineData(3, 2, false)] - public void MinRouteConstraint_ApplyConstraint(long min, int parameterValue, bool expected) - { - // Arrange - var constraint = new MinRouteConstraint(min); + // Arrange + var constraint = new MinRouteConstraint(min); - // Act - var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/NonFIleNameRouteConstraintTest.cs b/src/Http/Routing/test/UnitTests/Constraints/NonFIleNameRouteConstraintTest.cs index cf461fb700..f5bbb5a29b 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/NonFIleNameRouteConstraintTest.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/NonFIleNameRouteConstraintTest.cs @@ -3,57 +3,56 @@ using Xunit; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +public class NonFileNameRouteConstraintTest { - public class NonFileNameRouteConstraintTest + [Theory] + [MemberData(nameof(FileNameRouteConstraintTest.FileNameData), MemberType = typeof(FileNameRouteConstraintTest))] + public void Match_RouteValue_IsNotNonFileName(object value) + { + // Arrange + var constraint = new NonFileNameRouteConstraint(); + + var values = new RouteValueDictionary(); + values.Add("path", value); + + // Act + var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest); + + // Assert + Assert.False(result); + } + + [Theory] + [MemberData(nameof(FileNameRouteConstraintTest.NonFileNameData), MemberType = typeof(FileNameRouteConstraintTest))] + public void Match_RouteValue_IsNonFileName(object value) + { + // Arrange + var constraint = new NonFileNameRouteConstraint(); + + var values = new RouteValueDictionary(); + values.Add("path", value); + + // Act + var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest); + + // Assert + Assert.True(result); + } + + [Fact] + public void Match_MissingValue_IsNotFileName() { - [Theory] - [MemberData(nameof(FileNameRouteConstraintTest.FileNameData), MemberType = typeof(FileNameRouteConstraintTest))] - public void Match_RouteValue_IsNotNonFileName(object value) - { - // Arrange - var constraint = new NonFileNameRouteConstraint(); - - var values = new RouteValueDictionary(); - values.Add("path", value); - - // Act - var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest); - - // Assert - Assert.False(result); - } - - [Theory] - [MemberData(nameof(FileNameRouteConstraintTest.NonFileNameData), MemberType = typeof(FileNameRouteConstraintTest))] - public void Match_RouteValue_IsNonFileName(object value) - { - // Arrange - var constraint = new NonFileNameRouteConstraint(); - - var values = new RouteValueDictionary(); - values.Add("path", value); - - // Act - var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest); - - // Assert - Assert.True(result); - } - - [Fact] - public void Match_MissingValue_IsNotFileName() - { - // Arrange - var constraint = new NonFileNameRouteConstraint(); - - var values = new RouteValueDictionary(); - - // Act - var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest); - - // Assert - Assert.True(result); - } + // Arrange + var constraint = new NonFileNameRouteConstraint(); + + var values = new RouteValueDictionary(); + + // Act + var result = constraint.Match(httpContext: null, route: null, "path", values, RouteDirection.IncomingRequest); + + // Assert + Assert.True(result); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/RangeRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/RangeRouteConstraintTests.cs index 137b70324a..7d0f13f07d 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/RangeRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/RangeRouteConstraintTests.cs @@ -5,44 +5,43 @@ using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.AspNetCore.Testing; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class RangeRouteConstraintTests { - public class RangeRouteConstraintTests + [Theory] + [InlineData(long.MinValue, long.MaxValue, 2, true)] + [InlineData(3, 5, 3, true)] + [InlineData(3, 5, 4, true)] + [InlineData(3, 5, 5, true)] + [InlineData(3, 5, 6, false)] + [InlineData(3, 5, 2, false)] + [InlineData(3, 3, 2, false)] + [InlineData(3, 3, 3, true)] + public void RangeRouteConstraintTest_ValidValue_ApplyConstraint(long min, long max, int parameterValue, bool expected) { - [Theory] - [InlineData(long.MinValue, long.MaxValue, 2, true)] - [InlineData(3, 5, 3, true)] - [InlineData(3, 5, 4, true)] - [InlineData(3, 5, 5, true)] - [InlineData(3, 5, 6, false)] - [InlineData(3, 5, 2, false)] - [InlineData(3, 3, 2, false)] - [InlineData(3, 3, 3, true)] - public void RangeRouteConstraintTest_ValidValue_ApplyConstraint(long min, long max, int parameterValue, bool expected) - { - // Arrange - var constraint = new RangeRouteConstraint(min, max); + // Arrange + var constraint = new RangeRouteConstraint(min, max); - // Act - var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); + // Act + var actual = ConstraintsTestHelper.TestConstraint(constraint, parameterValue); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); + } - [Fact] - public void RangeRouteConstraint_MinGreaterThanMax_Throws() - { - // Arrange - var expectedMessage = "The value for argument 'min' should be less than or equal to the value for the " + - "argument 'max'."; + [Fact] + public void RangeRouteConstraint_MinGreaterThanMax_Throws() + { + // Arrange + var expectedMessage = "The value for argument 'min' should be less than or equal to the value for the " + + "argument 'max'."; - // Act & Assert - ExceptionAssert.ThrowsArgumentOutOfRange( - () => new RangeRouteConstraint(3, 2), - "min", - expectedMessage, - 3L); - } + // Act & Assert + ExceptionAssert.ThrowsArgumentOutOfRange( + () => new RangeRouteConstraint(3, 2), + "min", + expectedMessage, + 3L); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/RegexInlineRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/RegexInlineRouteConstraintTests.cs index 9eca3f91c4..c5c50f55eb 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/RegexInlineRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/RegexInlineRouteConstraintTests.cs @@ -7,45 +7,75 @@ using Microsoft.AspNetCore.Testing; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class RegexInlineRouteConstraintTests { - public class RegexInlineRouteConstraintTests + [Theory] + [InlineData("abc", "abc", true)] // simple match + [InlineData("Abc", "abc", true)] // case insensitive match + [InlineData("Abc ", "abc", true)] // Extra space on input match (because we don't add ^({0})$ + [InlineData("Abcd", "abc", true)] // Extra char + [InlineData("^Abcd", "abc", true)] // Extra special char + [InlineData("Abc", " abc", false)] // Missing char + public void RegexInlineConstraintBuildRegexVerbatimFromInput( + string routeValue, + string constraintValue, + bool shouldMatch) { - [Theory] - [InlineData("abc", "abc", true)] // simple match - [InlineData("Abc", "abc", true)] // case insensitive match - [InlineData("Abc ", "abc", true)] // Extra space on input match (because we don't add ^({0})$ - [InlineData("Abcd", "abc", true)] // Extra char - [InlineData("^Abcd", "abc", true)] // Extra special char - [InlineData("Abc", " abc", false)] // Missing char - public void RegexInlineConstraintBuildRegexVerbatimFromInput( - string routeValue, - string constraintValue, - bool shouldMatch) - { - // Arrange - var constraint = new RegexInlineRouteConstraint(constraintValue); - var values = new RouteValueDictionary(new { controller = routeValue }); + // Arrange + var constraint = new RegexInlineRouteConstraint(constraintValue); + var values = new RouteValueDictionary(new { controller = routeValue }); - // Act - var match = constraint.Match( - new DefaultHttpContext(), - route: new Mock().Object, - routeKey: "controller", - values: values, - routeDirection: RouteDirection.IncomingRequest); + // Act + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); - // Assert - Assert.Equal(shouldMatch, match); - } + // Assert + Assert.Equal(shouldMatch, match); + } + + [Fact] + public void RegexInlineConstraint_FailsIfKeyIsNotFoundInRouteValues() + { + // Arrange + var constraint = new RegexInlineRouteConstraint("^abc$"); + var values = new RouteValueDictionary(new { action = "abc" }); - [Fact] - public void RegexInlineConstraint_FailsIfKeyIsNotFoundInRouteValues() + // Act + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.False(match); + } + + [Theory] + [InlineData("tr-TR")] + [InlineData("en-US")] + public void RegexInlineConstraint_IsCultureInsensitive(string culture) + { + if (TestPlatformHelper.IsMono) { - // Arrange - var constraint = new RegexInlineRouteConstraint("^abc$"); - var values = new RouteValueDictionary(new { action = "abc" }); + // The Regex in Mono returns true when matching the Turkish I for the a-z range which causes the test + // to fail. Tracked via #100. + return; + } + + // Arrange + var constraint = new RegexInlineRouteConstraint("^([a-z]+)$"); + var values = new RouteValueDictionary(new { controller = "\u0130" }); // Turkish upper-case dotted I + using (new CultureReplacer(culture)) + { // Act var match = constraint.Match( new DefaultHttpContext(), @@ -57,36 +87,5 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(match); } - - [Theory] - [InlineData("tr-TR")] - [InlineData("en-US")] - public void RegexInlineConstraint_IsCultureInsensitive(string culture) - { - if (TestPlatformHelper.IsMono) - { - // The Regex in Mono returns true when matching the Turkish I for the a-z range which causes the test - // to fail. Tracked via #100. - return; - } - - // Arrange - var constraint = new RegexInlineRouteConstraint("^([a-z]+)$"); - var values = new RouteValueDictionary(new { controller = "\u0130" }); // Turkish upper-case dotted I - - using (new CultureReplacer(culture)) - { - // Act - var match = constraint.Match( - new DefaultHttpContext(), - route: new Mock().Object, - routeKey: "controller", - values: values, - routeDirection: RouteDirection.IncomingRequest); - - // Assert - Assert.False(match); - } - } } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/RegexRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/RegexRouteConstraintTests.cs index 3010ce7bdb..5de70c745a 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/RegexRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/RegexRouteConstraintTests.cs @@ -8,89 +8,119 @@ using Microsoft.AspNetCore.Testing; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class RegexRouteConstraintTests { - public class RegexRouteConstraintTests + [Theory] + [InlineData("abc", "abc", true)] // simple match + [InlineData("Abc", "abc", true)] // case insensitive match + [InlineData("Abc ", "abc", true)] // Extra space on input match (because we don't add ^({0})$ + [InlineData("Abcd", "abc", true)] // Extra char + [InlineData("^Abcd", "abc", true)] // Extra special char + [InlineData("Abc", " abc", false)] // Missing char + [InlineData("123-456-2334", @"^\d{3}-\d{3}-\d{4}$", true)] // ssn + [InlineData(@"12/4/2013", @"^\d{1,2}\/\d{1,2}\/\d{4}$", true)] // date + [InlineData(@"abc@def.com", @"^\w+[\w\.]*\@\w+((-\w+)|(\w*))\.[a-z]{2,3}$", true)] // email + public void RegexConstraintBuildRegexVerbatimFromInput( + string routeValue, + string constraintValue, + bool shouldMatch) { - [Theory] - [InlineData("abc", "abc", true)] // simple match - [InlineData("Abc", "abc", true)] // case insensitive match - [InlineData("Abc ", "abc", true)] // Extra space on input match (because we don't add ^({0})$ - [InlineData("Abcd", "abc", true)] // Extra char - [InlineData("^Abcd", "abc", true)] // Extra special char - [InlineData("Abc", " abc", false)] // Missing char - [InlineData("123-456-2334", @"^\d{3}-\d{3}-\d{4}$", true)] // ssn - [InlineData(@"12/4/2013", @"^\d{1,2}\/\d{1,2}\/\d{4}$", true)] // date - [InlineData(@"abc@def.com", @"^\w+[\w\.]*\@\w+((-\w+)|(\w*))\.[a-z]{2,3}$", true)] // email - public void RegexConstraintBuildRegexVerbatimFromInput( - string routeValue, - string constraintValue, - bool shouldMatch) - { - // Arrange - var constraint = new RegexRouteConstraint(constraintValue); - var values = new RouteValueDictionary(new { controller = routeValue }); - - // Act - var match = constraint.Match( - new DefaultHttpContext(), - route: new Mock().Object, - routeKey: "controller", - values: values, - routeDirection: RouteDirection.IncomingRequest); - - // Assert - Assert.Equal(shouldMatch, match); - } + // Arrange + var constraint = new RegexRouteConstraint(constraintValue); + var values = new RouteValueDictionary(new { controller = routeValue }); + + // Act + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.Equal(shouldMatch, match); + } - [Fact] - public void RegexConstraint_TakesRegexAsInput_SimpleMatch() - { - // Arrange - var constraint = new RegexRouteConstraint(new Regex("^abc$")); - var values = new RouteValueDictionary(new { controller = "abc" }); + [Fact] + public void RegexConstraint_TakesRegexAsInput_SimpleMatch() + { + // Arrange + var constraint = new RegexRouteConstraint(new Regex("^abc$")); + var values = new RouteValueDictionary(new { controller = "abc" }); + + // Act + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.True(match); + } - // Act - var match = constraint.Match( - new DefaultHttpContext(), - route: new Mock().Object, - routeKey: "controller", - values: values, - routeDirection: RouteDirection.IncomingRequest); + [Fact] + public void RegexConstraintConstructedWithRegex_SimpleFailedMatch() + { + // Arrange + var constraint = new RegexRouteConstraint(new Regex("^abc$")); + var values = new RouteValueDictionary(new { controller = "Abc" }); + + // Act + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.False(match); + } - // Assert - Assert.True(match); - } + [Fact] + public void RegexConstraintFailsIfKeyIsNotFoundInRouteValues() + { + // Arrange + var constraint = new RegexRouteConstraint(new Regex("^abc$")); + var values = new RouteValueDictionary(new { action = "abc" }); + + // Act + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.False(match); + } - [Fact] - public void RegexConstraintConstructedWithRegex_SimpleFailedMatch() + [Theory] + [InlineData("tr-TR")] + [InlineData("en-US")] + public void RegexConstraintIsCultureInsensitiveWhenConstructedWithString(string culture) + { + if (TestPlatformHelper.IsMono) { - // Arrange - var constraint = new RegexRouteConstraint(new Regex("^abc$")); - var values = new RouteValueDictionary(new { controller = "Abc" }); - - // Act - var match = constraint.Match( - new DefaultHttpContext(), - route: new Mock().Object, - routeKey: "controller", - values: values, - routeDirection: RouteDirection.IncomingRequest); - - // Assert - Assert.False(match); + // The Regex in Mono returns true when matching the Turkish I for the a-z range which causes the test + // to fail. Tracked via #100. + return; } - [Fact] - public void RegexConstraintFailsIfKeyIsNotFoundInRouteValues() - { - // Arrange - var constraint = new RegexRouteConstraint(new Regex("^abc$")); - var values = new RouteValueDictionary(new { action = "abc" }); + // Arrange + var constraint = new RegexRouteConstraint("^([a-z]+)$"); + var values = new RouteValueDictionary(new { controller = "\u0130" }); // Turkish upper-case dotted I + using (new CultureReplacer(culture)) + { // Act var match = constraint.Match( - new DefaultHttpContext(), + httpContext: new Mock().Object, route: new Mock().Object, routeKey: "controller", values: values, @@ -99,36 +129,5 @@ namespace Microsoft.AspNetCore.Routing.Tests // Assert Assert.False(match); } - - [Theory] - [InlineData("tr-TR")] - [InlineData("en-US")] - public void RegexConstraintIsCultureInsensitiveWhenConstructedWithString(string culture) - { - if (TestPlatformHelper.IsMono) - { - // The Regex in Mono returns true when matching the Turkish I for the a-z range which causes the test - // to fail. Tracked via #100. - return; - } - - // Arrange - var constraint = new RegexRouteConstraint("^([a-z]+)$"); - var values = new RouteValueDictionary(new { controller = "\u0130" }); // Turkish upper-case dotted I - - using (new CultureReplacer(culture)) - { - // Act - var match = constraint.Match( - httpContext: new Mock().Object, - route: new Mock().Object, - routeKey: "controller", - values: values, - routeDirection: RouteDirection.IncomingRequest); - - // Assert - Assert.False(match); - } - } } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/RequiredRouteConstraintTests.cs b/src/Http/Routing/test/UnitTests/Constraints/RequiredRouteConstraintTests.cs index 5d01d51592..d54649b7d0 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/RequiredRouteConstraintTests.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/RequiredRouteConstraintTests.cs @@ -6,88 +6,87 @@ using Microsoft.AspNetCore.Routing.Constraints; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class RequiredRouteConstraintTests { - public class RequiredRouteConstraintTests + [Theory] + [InlineData(RouteDirection.IncomingRequest)] + [InlineData(RouteDirection.UrlGeneration)] + public void RequiredRouteConstraint_NoValue(RouteDirection direction) { - [Theory] - [InlineData(RouteDirection.IncomingRequest)] - [InlineData(RouteDirection.UrlGeneration)] - public void RequiredRouteConstraint_NoValue(RouteDirection direction) - { - // Arrange - var constraint = new RequiredRouteConstraint(); + // Arrange + var constraint = new RequiredRouteConstraint(); - // Act - var result = constraint.Match( - new DefaultHttpContext(), - Mock.Of(), - "area", - new RouteValueDictionary(new { controller = "Home", action = "Index" }), - direction); + // Act + var result = constraint.Match( + new DefaultHttpContext(), + Mock.Of(), + "area", + new RouteValueDictionary(new { controller = "Home", action = "Index" }), + direction); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Theory] - [InlineData(RouteDirection.IncomingRequest)] - [InlineData(RouteDirection.UrlGeneration)] - public void RequiredRouteConstraint_Null(RouteDirection direction) - { - // Arrange - var constraint = new RequiredRouteConstraint(); + [Theory] + [InlineData(RouteDirection.IncomingRequest)] + [InlineData(RouteDirection.UrlGeneration)] + public void RequiredRouteConstraint_Null(RouteDirection direction) + { + // Arrange + var constraint = new RequiredRouteConstraint(); - // Act - var result = constraint.Match( - new DefaultHttpContext(), - Mock.Of(), - "area", - new RouteValueDictionary(new { controller = "Home", action = "Index", area = (string)null }), - direction); + // Act + var result = constraint.Match( + new DefaultHttpContext(), + Mock.Of(), + "area", + new RouteValueDictionary(new { controller = "Home", action = "Index", area = (string)null }), + direction); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Theory] - [InlineData(RouteDirection.IncomingRequest)] - [InlineData(RouteDirection.UrlGeneration)] - public void RequiredRouteConstraint_EmptyString(RouteDirection direction) - { - // Arrange - var constraint = new RequiredRouteConstraint(); + [Theory] + [InlineData(RouteDirection.IncomingRequest)] + [InlineData(RouteDirection.UrlGeneration)] + public void RequiredRouteConstraint_EmptyString(RouteDirection direction) + { + // Arrange + var constraint = new RequiredRouteConstraint(); - // Act - var result = constraint.Match( - new DefaultHttpContext(), - Mock.Of(), - "area", - new RouteValueDictionary(new { controller = "Home", action = "Index", area = string.Empty}), - direction); + // Act + var result = constraint.Match( + new DefaultHttpContext(), + Mock.Of(), + "area", + new RouteValueDictionary(new { controller = "Home", action = "Index", area = string.Empty }), + direction); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Theory] - [InlineData(RouteDirection.IncomingRequest)] - [InlineData(RouteDirection.UrlGeneration)] - public void RequiredRouteConstraint_WithValue(RouteDirection direction) - { - // Arrange - var constraint = new RequiredRouteConstraint(); + [Theory] + [InlineData(RouteDirection.IncomingRequest)] + [InlineData(RouteDirection.UrlGeneration)] + public void RequiredRouteConstraint_WithValue(RouteDirection direction) + { + // Arrange + var constraint = new RequiredRouteConstraint(); - // Act - var result = constraint.Match( - new DefaultHttpContext(), - Mock.Of(), - "area", - new RouteValueDictionary(new { controller = "Home", action = "Index", area = "Store" }), - direction); + // Act + var result = constraint.Match( + new DefaultHttpContext(), + Mock.Of(), + "area", + new RouteValueDictionary(new { controller = "Home", action = "Index", area = "Store" }), + direction); - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); } } diff --git a/src/Http/Routing/test/UnitTests/Constraints/StringRouteConstraintTest.cs b/src/Http/Routing/test/UnitTests/Constraints/StringRouteConstraintTest.cs index 7ec1294219..560264e4bc 100644 --- a/src/Http/Routing/test/UnitTests/Constraints/StringRouteConstraintTest.cs +++ b/src/Http/Routing/test/UnitTests/Constraints/StringRouteConstraintTest.cs @@ -6,152 +6,151 @@ using Microsoft.AspNetCore.Routing.Constraints; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing.Constraints +namespace Microsoft.AspNetCore.Routing.Constraints; + +public class StringRouteConstraintTest { - public class StringRouteConstraintTest + [Fact] + public void StringRouteConstraintSimpleTrueWithRouteDirectionIncomingRequestTest() + { + // Arrange + var constraint = new StringRouteConstraint("home"); + + // Act + var values = new RouteValueDictionary(new { controller = "home" }); + + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.True(match); + } + + [Fact] + public void StringRouteConstraintSimpleTrueWithRouteDirectionUrlGenerationTest() + { + // Arrange + var constraint = new StringRouteConstraint("home"); + + // Act + var values = new RouteValueDictionary(new { controller = "home" }); + + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.UrlGeneration); + + // Assert + Assert.True(match); + } + + [Fact] + public void StringRouteConstraintSimpleFalseWithRouteDirectionIncomingRequestTest() + { + // Arrange + var constraint = new StringRouteConstraint("admin"); + + // Act + var values = new RouteValueDictionary(new { controller = "home" }); + + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.False(match); + } + + [Fact] + public void StringRouteConstraintSimpleFalseWithRouteDirectionUrlGenerationTest() + { + // Arrange + var constraint = new StringRouteConstraint("admin"); + + // Act + var values = new RouteValueDictionary(new { controller = "home" }); + + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.UrlGeneration); + + // Assert + Assert.False(match); + } + + [Fact] + public void StringRouteConstraintKeyNotFoundWithRouteDirectionIncomingRequestTest() + { + // Arrange + var constraint = new StringRouteConstraint("admin"); + + // Act + var values = new RouteValueDictionary(new { controller = "admin" }); + + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "action", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.False(match); + } + + [Fact] + public void StringRouteConstraintKeyNotFoundWithRouteDirectionUrlGenerationTest() + { + // Arrange + var constraint = new StringRouteConstraint("admin"); + + // Act + var values = new RouteValueDictionary(new { controller = "admin" }); + + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "action", + values: values, + routeDirection: RouteDirection.UrlGeneration); + + // Assert + Assert.False(match); + } + + [Theory] + [InlineData("User", "uSer", true)] + [InlineData("User.Admin", "User.Admin", true)] + [InlineData(@"User\Admin", "User\\Admin", true)] + [InlineData(null, "user", false)] + public void StringRouteConstraintEscapingCaseSensitiveAndRouteNullTest(string routeValue, string constraintValue, bool expected) { - [Fact] - public void StringRouteConstraintSimpleTrueWithRouteDirectionIncomingRequestTest() - { - // Arrange - var constraint = new StringRouteConstraint("home"); - - // Act - var values = new RouteValueDictionary(new { controller = "home" }); - - var match = constraint.Match( - new DefaultHttpContext(), - route: new Mock().Object, - routeKey: "controller", - values: values, - routeDirection: RouteDirection.IncomingRequest); - - // Assert - Assert.True(match); - } - - [Fact] - public void StringRouteConstraintSimpleTrueWithRouteDirectionUrlGenerationTest() - { - // Arrange - var constraint = new StringRouteConstraint("home"); - - // Act - var values = new RouteValueDictionary(new { controller = "home" }); - - var match = constraint.Match( - new DefaultHttpContext(), - route: new Mock().Object, - routeKey: "controller", - values: values, - routeDirection: RouteDirection.UrlGeneration); - - // Assert - Assert.True(match); - } - - [Fact] - public void StringRouteConstraintSimpleFalseWithRouteDirectionIncomingRequestTest() - { - // Arrange - var constraint = new StringRouteConstraint("admin"); - - // Act - var values = new RouteValueDictionary(new { controller = "home" }); - - var match = constraint.Match( - new DefaultHttpContext(), - route: new Mock().Object, - routeKey: "controller", - values: values, - routeDirection: RouteDirection.IncomingRequest); - - // Assert - Assert.False(match); - } - - [Fact] - public void StringRouteConstraintSimpleFalseWithRouteDirectionUrlGenerationTest() - { - // Arrange - var constraint = new StringRouteConstraint("admin"); - - // Act - var values = new RouteValueDictionary(new { controller = "home" }); - - var match = constraint.Match( - new DefaultHttpContext(), - route: new Mock().Object, - routeKey: "controller", - values: values, - routeDirection: RouteDirection.UrlGeneration); - - // Assert - Assert.False(match); - } - - [Fact] - public void StringRouteConstraintKeyNotFoundWithRouteDirectionIncomingRequestTest() - { - // Arrange - var constraint = new StringRouteConstraint("admin"); - - // Act - var values = new RouteValueDictionary(new { controller = "admin" }); - - var match = constraint.Match( - new DefaultHttpContext(), - route: new Mock().Object, - routeKey: "action", - values: values, - routeDirection: RouteDirection.IncomingRequest); - - // Assert - Assert.False(match); - } - - [Fact] - public void StringRouteConstraintKeyNotFoundWithRouteDirectionUrlGenerationTest() - { - // Arrange - var constraint = new StringRouteConstraint("admin"); - - // Act - var values = new RouteValueDictionary(new { controller = "admin" }); - - var match = constraint.Match( - new DefaultHttpContext(), - route: new Mock().Object, - routeKey: "action", - values: values, - routeDirection: RouteDirection.UrlGeneration); - - // Assert - Assert.False(match); - } - - [Theory] - [InlineData("User", "uSer", true)] - [InlineData("User.Admin", "User.Admin", true)] - [InlineData(@"User\Admin", "User\\Admin", true)] - [InlineData(null, "user", false)] - public void StringRouteConstraintEscapingCaseSensitiveAndRouteNullTest(string routeValue, string constraintValue, bool expected) - { - // Arrange - var constraint = new StringRouteConstraint(constraintValue); - - // Act - var values = new RouteValueDictionary(new { controller = routeValue }); - - var match = constraint.Match( - new DefaultHttpContext(), - route: new Mock().Object, - routeKey: "controller", - values: values, - routeDirection: RouteDirection.IncomingRequest); - - // Assert - Assert.Equal(expected, match); - } + // Arrange + var constraint = new StringRouteConstraint(constraintValue); + + // Act + var values = new RouteValueDictionary(new { controller = routeValue }); + + var match = constraint.Match( + new DefaultHttpContext(), + route: new Mock().Object, + routeKey: "controller", + values: values, + routeDirection: RouteDirection.IncomingRequest); + + // Assert + Assert.Equal(expected, match); } -} \ No newline at end of file +} diff --git a/src/Http/Routing/test/UnitTests/DataSourceDependentCacheTest.cs b/src/Http/Routing/test/UnitTests/DataSourceDependentCacheTest.cs index 4e74f9ec06..c58178552d 100644 --- a/src/Http/Routing/test/UnitTests/DataSourceDependentCacheTest.cs +++ b/src/Http/Routing/test/UnitTests/DataSourceDependentCacheTest.cs @@ -7,119 +7,118 @@ using System.Text; using Microsoft.AspNetCore.Routing.TestObjects; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class DataSourceDependentCacheTest { - public class DataSourceDependentCacheTest + [Fact] + public void Cache_Initializes_WhenEnsureInitializedCalled() { - [Fact] - public void Cache_Initializes_WhenEnsureInitializedCalled() + // Arrange + var called = false; + + var dataSource = new DynamicEndpointDataSource(); + var cache = new DataSourceDependentCache(dataSource, (endpoints) => { - // Arrange - var called = false; - - var dataSource = new DynamicEndpointDataSource(); - var cache = new DataSourceDependentCache(dataSource, (endpoints) => - { - called = true; - return "hello, world!"; - }); - - // Act - cache.EnsureInitialized(); - - // Assert - Assert.True(called); - Assert.Equal("hello, world!", cache.Value); - } - - [Fact] - public void Cache_DoesNotInitialize_WhenValueCalled() + called = true; + return "hello, world!"; + }); + + // Act + cache.EnsureInitialized(); + + // Assert + Assert.True(called); + Assert.Equal("hello, world!", cache.Value); + } + + [Fact] + public void Cache_DoesNotInitialize_WhenValueCalled() + { + // Arrange + var called = false; + + var dataSource = new DynamicEndpointDataSource(); + var cache = new DataSourceDependentCache(dataSource, (endpoints) => { - // Arrange - var called = false; - - var dataSource = new DynamicEndpointDataSource(); - var cache = new DataSourceDependentCache(dataSource, (endpoints) => - { - called = true; - return "hello, world!"; - }); - - // Act - GC.KeepAlive(cache.Value); - - // Assert - Assert.False(called); - Assert.Null(cache.Value); - } - - [Fact] - public void Cache_Reinitializes_WhenDataSourceChanges() + called = true; + return "hello, world!"; + }); + + // Act + GC.KeepAlive(cache.Value); + + // Assert + Assert.False(called); + Assert.Null(cache.Value); + } + + [Fact] + public void Cache_Reinitializes_WhenDataSourceChanges() + { + // Arrange + var count = 0; + + var dataSource = new DynamicEndpointDataSource(); + var cache = new DataSourceDependentCache(dataSource, (endpoints) => { - // Arrange - var count = 0; + count++; + return $"hello, {count}!"; + }); - var dataSource = new DynamicEndpointDataSource(); - var cache = new DataSourceDependentCache(dataSource, (endpoints) => - { - count++; - return $"hello, {count}!"; - }); + cache.EnsureInitialized(); + Assert.Equal("hello, 1!", cache.Value); - cache.EnsureInitialized(); - Assert.Equal("hello, 1!", cache.Value); + // Act + dataSource.AddEndpoint(null); - // Act - dataSource.AddEndpoint(null); + // Assert + Assert.Equal(2, count); + Assert.Equal("hello, 2!", cache.Value); + } - // Assert - Assert.Equal(2, count); - Assert.Equal("hello, 2!", cache.Value); - } + [Fact] + public void Cache_CanDispose_WhenUninitialized() + { + // Arrange + var count = 0; - [Fact] - public void Cache_CanDispose_WhenUninitialized() + var dataSource = new DynamicEndpointDataSource(); + var cache = new DataSourceDependentCache(dataSource, (endpoints) => { - // Arrange - var count = 0; - - var dataSource = new DynamicEndpointDataSource(); - var cache = new DataSourceDependentCache(dataSource, (endpoints) => - { - count++; - return $"hello, {count}!"; - }); - - // Act - cache.Dispose(); - - // Assert - dataSource.AddEndpoint(null); - Assert.Null(cache.Value); - } - - [Fact] - public void Cache_CanDispose_WhenInitialized() + count++; + return $"hello, {count}!"; + }); + + // Act + cache.Dispose(); + + // Assert + dataSource.AddEndpoint(null); + Assert.Null(cache.Value); + } + + [Fact] + public void Cache_CanDispose_WhenInitialized() + { + // Arrange + var count = 0; + + var dataSource = new DynamicEndpointDataSource(); + var cache = new DataSourceDependentCache(dataSource, (endpoints) => { - // Arrange - var count = 0; - - var dataSource = new DynamicEndpointDataSource(); - var cache = new DataSourceDependentCache(dataSource, (endpoints) => - { - count++; - return $"hello, {count}!"; - }); - - cache.EnsureInitialized(); - Assert.Equal("hello, 1!", cache.Value); - - // Act - cache.Dispose(); - - // Assert - dataSource.AddEndpoint(null); - Assert.Equal("hello, 1!", cache.Value); // Ignores update - } + count++; + return $"hello, {count}!"; + }); + + cache.EnsureInitialized(); + Assert.Equal("hello, 1!", cache.Value); + + // Act + cache.Dispose(); + + // Assert + dataSource.AddEndpoint(null); + Assert.Equal("hello, 1!", cache.Value); // Ignores update } } diff --git a/src/Http/Routing/test/UnitTests/DecisionTreeBuilderTest.cs b/src/Http/Routing/test/UnitTests/DecisionTreeBuilderTest.cs index 025601f83a..4615ba5f02 100644 --- a/src/Http/Routing/test/UnitTests/DecisionTreeBuilderTest.cs +++ b/src/Http/Routing/test/UnitTests/DecisionTreeBuilderTest.cs @@ -5,214 +5,213 @@ using System; using System.Collections.Generic; using Xunit; -namespace Microsoft.AspNetCore.Routing.DecisionTree +namespace Microsoft.AspNetCore.Routing.DecisionTree; + +public class DecisionTreeBuilderTest { - public class DecisionTreeBuilderTest + [Fact] + public void BuildTree_Empty() { - [Fact] - public void BuildTree_Empty() - { - // Arrange - var items = new List(); + // Arrange + var items = new List(); - // Act - var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); + // Act + var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); - // Assert - Assert.Empty(tree.Criteria); - Assert.Empty(tree.Matches); - } + // Assert + Assert.Empty(tree.Criteria); + Assert.Empty(tree.Matches); + } - [Fact] - public void BuildTree_TrivialMatch() - { - // Arrange - var items = new List(); + [Fact] + public void BuildTree_TrivialMatch() + { + // Arrange + var items = new List(); - var item = new Item(); - items.Add(item); + var item = new Item(); + items.Add(item); - // Act - var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); + // Act + var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); - // Assert - Assert.Empty(tree.Criteria); - Assert.Same(item, Assert.Single(tree.Matches)); - } + // Assert + Assert.Empty(tree.Criteria); + Assert.Same(item, Assert.Single(tree.Matches)); + } - [Fact] - public void BuildTree_WithMultipleCriteria() - { - // Arrange - var items = new List(); + [Fact] + public void BuildTree_WithMultipleCriteria() + { + // Arrange + var items = new List(); - var item = new Item(); - item.Criteria.Add("area", new DecisionCriterionValue(value: "Admin")); - item.Criteria.Add("controller", new DecisionCriterionValue(value: "Users")); - item.Criteria.Add("action", new DecisionCriterionValue(value: "AddUser")); - items.Add(item); + var item = new Item(); + item.Criteria.Add("area", new DecisionCriterionValue(value: "Admin")); + item.Criteria.Add("controller", new DecisionCriterionValue(value: "Users")); + item.Criteria.Add("action", new DecisionCriterionValue(value: "AddUser")); + items.Add(item); - // Act - var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); + // Act + var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); - // Assert - Assert.Empty(tree.Matches); + // Assert + Assert.Empty(tree.Matches); - var area = Assert.Single(tree.Criteria); - Assert.Equal("area", area.Key); + var area = Assert.Single(tree.Criteria); + Assert.Equal("area", area.Key); - var admin = Assert.Single(area.Branches); - Assert.Equal("Admin", admin.Key); - Assert.Empty(admin.Value.Matches); + var admin = Assert.Single(area.Branches); + Assert.Equal("Admin", admin.Key); + Assert.Empty(admin.Value.Matches); - var controller = Assert.Single(admin.Value.Criteria); - Assert.Equal("controller", controller.Key); + var controller = Assert.Single(admin.Value.Criteria); + Assert.Equal("controller", controller.Key); - var users = Assert.Single(controller.Branches); - Assert.Equal("Users", users.Key); - Assert.Empty(users.Value.Matches); + var users = Assert.Single(controller.Branches); + Assert.Equal("Users", users.Key); + Assert.Empty(users.Value.Matches); - var action = Assert.Single(users.Value.Criteria); - Assert.Equal("action", action.Key); + var action = Assert.Single(users.Value.Criteria); + Assert.Equal("action", action.Key); - var addUser = Assert.Single(action.Branches); - Assert.Equal("AddUser", addUser.Key); - Assert.Empty(addUser.Value.Criteria); - Assert.Same(item, Assert.Single(addUser.Value.Matches)); - } + var addUser = Assert.Single(action.Branches); + Assert.Equal("AddUser", addUser.Key); + Assert.Empty(addUser.Value.Criteria); + Assert.Same(item, Assert.Single(addUser.Value.Matches)); + } - [Fact] - public void BuildTree_WithMultipleItems() - { - // Arrange - var items = new List(); + [Fact] + public void BuildTree_WithMultipleItems() + { + // Arrange + var items = new List(); - var item1 = new Item(); - item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); - item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy")); - items.Add(item1); + var item1 = new Item(); + item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); + item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy")); + items.Add(item1); - var item2 = new Item(); - item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); - item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout")); - items.Add(item2); + var item2 = new Item(); + item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); + item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout")); + items.Add(item2); - // Act - var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); + // Act + var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); - // Assert - Assert.Empty(tree.Matches); + // Assert + Assert.Empty(tree.Matches); - var action = Assert.Single(tree.Criteria); - Assert.Equal("action", action.Key); + var action = Assert.Single(tree.Criteria); + Assert.Equal("action", action.Key); - var buy = action.Branches["Buy"]; - Assert.Empty(buy.Matches); + var buy = action.Branches["Buy"]; + Assert.Empty(buy.Matches); - var controller = Assert.Single(buy.Criteria); - Assert.Equal("controller", controller.Key); + var controller = Assert.Single(buy.Criteria); + Assert.Equal("controller", controller.Key); - var store = Assert.Single(controller.Branches); - Assert.Equal("Store", store.Key); - Assert.Empty(store.Value.Criteria); - Assert.Same(item1, Assert.Single(store.Value.Matches)); + var store = Assert.Single(controller.Branches); + Assert.Equal("Store", store.Key); + Assert.Empty(store.Value.Criteria); + Assert.Same(item1, Assert.Single(store.Value.Matches)); - var checkout = action.Branches["Checkout"]; - Assert.Empty(checkout.Matches); + var checkout = action.Branches["Checkout"]; + Assert.Empty(checkout.Matches); - controller = Assert.Single(checkout.Criteria); - Assert.Equal("controller", controller.Key); + controller = Assert.Single(checkout.Criteria); + Assert.Equal("controller", controller.Key); - store = Assert.Single(controller.Branches); - Assert.Equal("Store", store.Key); - Assert.Empty(store.Value.Criteria); - Assert.Same(item2, Assert.Single(store.Value.Matches)); - } + store = Assert.Single(controller.Branches); + Assert.Equal("Store", store.Key); + Assert.Empty(store.Value.Criteria); + Assert.Same(item2, Assert.Single(store.Value.Matches)); + } - [Fact] - public void BuildTree_WithInteriorMatch() - { - // Arrange - var items = new List(); + [Fact] + public void BuildTree_WithInteriorMatch() + { + // Arrange + var items = new List(); - var item1 = new Item(); - item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); - item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy")); - items.Add(item1); + var item1 = new Item(); + item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); + item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy")); + items.Add(item1); - var item2 = new Item(); - item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); - item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout")); - items.Add(item2); + var item2 = new Item(); + item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); + item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout")); + items.Add(item2); - var item3 = new Item(); - item3.Criteria.Add("action", new DecisionCriterionValue(value: "Buy")); - items.Add(item3); + var item3 = new Item(); + item3.Criteria.Add("action", new DecisionCriterionValue(value: "Buy")); + items.Add(item3); - // Act - var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); + // Act + var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); - // Assert - Assert.Empty(tree.Matches); + // Assert + Assert.Empty(tree.Matches); - var action = Assert.Single(tree.Criteria); - Assert.Equal("action", action.Key); + var action = Assert.Single(tree.Criteria); + Assert.Equal("action", action.Key); - var buy = action.Branches["Buy"]; - Assert.Same(item3, Assert.Single(buy.Matches)); - } + var buy = action.Branches["Buy"]; + Assert.Same(item3, Assert.Single(buy.Matches)); + } - [Fact] - public void BuildTree_WithDivergentCriteria() - { - // Arrange - var items = new List(); + [Fact] + public void BuildTree_WithDivergentCriteria() + { + // Arrange + var items = new List(); - var item1 = new Item(); - item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); - item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy")); - items.Add(item1); + var item1 = new Item(); + item1.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); + item1.Criteria.Add("action", new DecisionCriterionValue(value: "Buy")); + items.Add(item1); - var item2 = new Item(); - item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); - item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout")); - items.Add(item2); + var item2 = new Item(); + item2.Criteria.Add("controller", new DecisionCriterionValue(value: "Store")); + item2.Criteria.Add("action", new DecisionCriterionValue(value: "Checkout")); + items.Add(item2); - var item3 = new Item(); - item3.Criteria.Add("stub", new DecisionCriterionValue(value: "Bleh")); - items.Add(item3); + var item3 = new Item(); + item3.Criteria.Add("stub", new DecisionCriterionValue(value: "Bleh")); + items.Add(item3); - // Act - var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); + // Act + var tree = DecisionTreeBuilder.GenerateTree(items, new ItemClassifier()); - // Assert - Assert.Empty(tree.Matches); + // Assert + Assert.Empty(tree.Matches); - var action = tree.Criteria[0]; - Assert.Equal("action", action.Key); + var action = tree.Criteria[0]; + Assert.Equal("action", action.Key); - var stub = tree.Criteria[1]; - Assert.Equal("stub", stub.Key); - } + var stub = tree.Criteria[1]; + Assert.Equal("stub", stub.Key); + } - private class Item + private class Item + { + public Item() { - public Item() - { - Criteria = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - - public Dictionary Criteria { get; private set; } + Criteria = new Dictionary(StringComparer.OrdinalIgnoreCase); } - private class ItemClassifier : IClassifier - { - public IEqualityComparer ValueComparer => RouteValueEqualityComparer.Default; + public Dictionary Criteria { get; private set; } + } - public IDictionary GetCriteria(Item item) - { - return item.Criteria; - } + private class ItemClassifier : IClassifier + { + public IEqualityComparer ValueComparer => RouteValueEqualityComparer.Default; + + public IDictionary GetCriteria(Item item) + { + return item.Criteria; } } -} \ No newline at end of file +} diff --git a/src/Http/Routing/test/UnitTests/DefaultEndpointDataSourceTests.cs b/src/Http/Routing/test/UnitTests/DefaultEndpointDataSourceTests.cs index 82495c7daf..9156bb4229 100644 --- a/src/Http/Routing/test/UnitTests/DefaultEndpointDataSourceTests.cs +++ b/src/Http/Routing/test/UnitTests/DefaultEndpointDataSourceTests.cs @@ -6,95 +6,94 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class DefaultEndpointDataSourceTests { - public class DefaultEndpointDataSourceTests + [Fact] + public void Constructor_Params_EndpointsInitialized() { - [Fact] - public void Constructor_Params_EndpointsInitialized() - { - // Arrange & Act - var dataSource = new DefaultEndpointDataSource( - new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "1"), - new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "2") - ); - - // Assert - Assert.Collection(dataSource.Endpoints, - endpoint => Assert.Equal("1", endpoint.DisplayName), - endpoint => Assert.Equal("2", endpoint.DisplayName)); - } - - [Fact] - public void Constructor_Params_ShouldMakeCopyOfEndpoints() - { - // Arrange - var endpoint1 = new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "1"); - var endpoint2 = new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "2"); - var endpoints = new[] { endpoint1, endpoint2 }; - - // Act - var dataSource = new DefaultEndpointDataSource(endpoints); - Array.Resize(ref endpoints, 1); - endpoints[0] = null; - - // Assert - Assert.Equal(2, dataSource.Endpoints.Count); - Assert.Contains(endpoint1, dataSource.Endpoints); - Assert.Contains(endpoint2, dataSource.Endpoints); - } - - [Fact] - public void Constructor_Params_ShouldThrowArgumentNullExceptionWhenEndpointsIsNull() - { - Endpoint[] endpoints = null; - - var actual = Assert.Throws(() => new DefaultEndpointDataSource(endpoints)); - Assert.Equal("endpoints", actual.ParamName); - } - - [Fact] - public void Constructor_Enumerable_EndpointsInitialized() - { - // Arrange & Act - var dataSource = new DefaultEndpointDataSource(new List + // Arrange & Act + var dataSource = new DefaultEndpointDataSource( + new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "1"), + new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "2") + ); + + // Assert + Assert.Collection(dataSource.Endpoints, + endpoint => Assert.Equal("1", endpoint.DisplayName), + endpoint => Assert.Equal("2", endpoint.DisplayName)); + } + + [Fact] + public void Constructor_Params_ShouldMakeCopyOfEndpoints() + { + // Arrange + var endpoint1 = new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "1"); + var endpoint2 = new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "2"); + var endpoints = new[] { endpoint1, endpoint2 }; + + // Act + var dataSource = new DefaultEndpointDataSource(endpoints); + Array.Resize(ref endpoints, 1); + endpoints[0] = null; + + // Assert + Assert.Equal(2, dataSource.Endpoints.Count); + Assert.Contains(endpoint1, dataSource.Endpoints); + Assert.Contains(endpoint2, dataSource.Endpoints); + } + + [Fact] + public void Constructor_Params_ShouldThrowArgumentNullExceptionWhenEndpointsIsNull() + { + Endpoint[] endpoints = null; + + var actual = Assert.Throws(() => new DefaultEndpointDataSource(endpoints)); + Assert.Equal("endpoints", actual.ParamName); + } + + [Fact] + public void Constructor_Enumerable_EndpointsInitialized() + { + // Arrange & Act + var dataSource = new DefaultEndpointDataSource(new List { new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "1"), new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "2") }); - // Assert - Assert.Collection(dataSource.Endpoints, - endpoint => Assert.Equal("1", endpoint.DisplayName), - endpoint => Assert.Equal("2", endpoint.DisplayName)); - } - - [Fact] - public void Constructor_Enumerable_ShouldMakeCopyOfEndpoints() - { - // Arrange - var endpoint1 = new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "1"); - var endpoint2 = new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "2"); - var endpoints = new List { endpoint1, endpoint2 }; - - // Act - var dataSource = new DefaultEndpointDataSource((IEnumerable)endpoints); - endpoints.RemoveAt(0); - endpoints[0] = null; - - // Assert - Assert.Equal(2, dataSource.Endpoints.Count); - Assert.Contains(endpoint1, dataSource.Endpoints); - Assert.Contains(endpoint2, dataSource.Endpoints); - } - - [Fact] - public void Constructor_Enumerable_ShouldThrowArgumentNullExceptionWhenEndpointsIsNull() - { - IEnumerable endpoints = null; - - var actual = Assert.Throws(() => new DefaultEndpointDataSource(endpoints)); - Assert.Equal("endpoints", actual.ParamName); - } + // Assert + Assert.Collection(dataSource.Endpoints, + endpoint => Assert.Equal("1", endpoint.DisplayName), + endpoint => Assert.Equal("2", endpoint.DisplayName)); + } + + [Fact] + public void Constructor_Enumerable_ShouldMakeCopyOfEndpoints() + { + // Arrange + var endpoint1 = new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "1"); + var endpoint2 = new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "2"); + var endpoints = new List { endpoint1, endpoint2 }; + + // Act + var dataSource = new DefaultEndpointDataSource((IEnumerable)endpoints); + endpoints.RemoveAt(0); + endpoints[0] = null; + + // Assert + Assert.Equal(2, dataSource.Endpoints.Count); + Assert.Contains(endpoint1, dataSource.Endpoints); + Assert.Contains(endpoint2, dataSource.Endpoints); + } + + [Fact] + public void Constructor_Enumerable_ShouldThrowArgumentNullExceptionWhenEndpointsIsNull() + { + IEnumerable endpoints = null; + + var actual = Assert.Throws(() => new DefaultEndpointDataSource(endpoints)); + Assert.Equal("endpoints", actual.ParamName); } -} \ No newline at end of file +} diff --git a/src/Http/Routing/test/UnitTests/DefaultInlineConstraintResolverTest.cs b/src/Http/Routing/test/UnitTests/DefaultInlineConstraintResolverTest.cs index 5d4731aad0..b8daa9a40e 100644 --- a/src/Http/Routing/test/UnitTests/DefaultInlineConstraintResolverTest.cs +++ b/src/Http/Routing/test/UnitTests/DefaultInlineConstraintResolverTest.cs @@ -11,404 +11,403 @@ using Microsoft.Extensions.Options; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class DefaultInlineConstraintResolverTest { - public class DefaultInlineConstraintResolverTest + private readonly IInlineConstraintResolver _constraintResolver; + + public DefaultInlineConstraintResolverTest() { - private readonly IInlineConstraintResolver _constraintResolver; + var routeOptions = new RouteOptions(); + _constraintResolver = GetInlineConstraintResolver(routeOptions); + } - public DefaultInlineConstraintResolverTest() - { - var routeOptions = new RouteOptions(); - _constraintResolver = GetInlineConstraintResolver(routeOptions); - } + [Fact] + public void ResolveConstraint_RequiredConstraint_ResolvesCorrectly() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("required"); - [Fact] - public void ResolveConstraint_RequiredConstraint_ResolvesCorrectly() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("required"); + // Assert + Assert.IsType(constraint); + } - // Assert - Assert.IsType(constraint); - } + [Fact] + public void ResolveConstraint_IntConstraint_ResolvesCorrectly() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("int"); - [Fact] - public void ResolveConstraint_IntConstraint_ResolvesCorrectly() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("int"); + // Assert + Assert.IsType(constraint); + } - // Assert - Assert.IsType(constraint); - } + [Fact] + public void ResolveConstraint_IntConstraintWithArgument_Throws() + { + // Arrange, Act & Assert + var ex = Assert.Throws( + () => _constraintResolver.ResolveConstraint("int(5)")); - [Fact] - public void ResolveConstraint_IntConstraintWithArgument_Throws() - { - // Arrange, Act & Assert - var ex = Assert.Throws( - () => _constraintResolver.ResolveConstraint("int(5)")); + Assert.Equal("Could not find a constructor for constraint type 'IntRouteConstraint'" + + " with the following number of parameters: 1.", + ex.Message); + } - Assert.Equal("Could not find a constructor for constraint type 'IntRouteConstraint'" + - " with the following number of parameters: 1.", - ex.Message); - } + [Fact] + public void ResolveConstraint_AlphaConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("alpha"); - [Fact] - public void ResolveConstraint_AlphaConstraint() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("alpha"); + // Assert + Assert.IsType(constraint); + } - // Assert - Assert.IsType(constraint); - } + [Fact] + public void ResolveConstraint_RegexInlineConstraint_WithAComma_PassesAsASingleArgument() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("regex(ab,1)"); - [Fact] - public void ResolveConstraint_RegexInlineConstraint_WithAComma_PassesAsASingleArgument() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("regex(ab,1)"); + // Assert + Assert.IsType(constraint); + } - // Assert - Assert.IsType(constraint); - } + [Fact] + public void ResolveConstraint_RegexInlineConstraint_WithCurlyBraces_Balanced() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint( + @"regex(\\b(?\\d{1,2})/(?\\d{1,2})/(?\\d{2,4})\\b)"); - [Fact] - public void ResolveConstraint_RegexInlineConstraint_WithCurlyBraces_Balanced() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint( - @"regex(\\b(?\\d{1,2})/(?\\d{1,2})/(?\\d{2,4})\\b)"); + // Assert + Assert.IsType(constraint); + } - // Assert - Assert.IsType(constraint); - } + [Fact] + public void ResolveConstraint_BoolConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("bool"); - [Fact] - public void ResolveConstraint_BoolConstraint() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("bool"); + // Assert + Assert.IsType(constraint); + } - // Assert - Assert.IsType(constraint); - } + [Fact] + public void ResolveConstraint_CompositeConstraintIsNotRegistered() + { + // Arrange, Act & Assert + Assert.Null(_constraintResolver.ResolveConstraint("composite")); + } - [Fact] - public void ResolveConstraint_CompositeConstraintIsNotRegistered() - { - // Arrange, Act & Assert - Assert.Null(_constraintResolver.ResolveConstraint("composite")); - } + [Fact] + public void ResolveConstraint_DateTimeConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("datetime"); - [Fact] - public void ResolveConstraint_DateTimeConstraint() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("datetime"); + // Assert + Assert.IsType(constraint); + } - // Assert - Assert.IsType(constraint); - } + [Fact] + public void ResolveConstraint_DecimalConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("decimal"); - [Fact] - public void ResolveConstraint_DecimalConstraint() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("decimal"); + // Assert + Assert.IsType(constraint); + } - // Assert - Assert.IsType(constraint); - } + [Fact] + public void ResolveConstraint_DoubleConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("double"); - [Fact] - public void ResolveConstraint_DoubleConstraint() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("double"); + // Assert + Assert.IsType(constraint); + } - // Assert - Assert.IsType(constraint); - } + [Fact] + public void ResolveConstraint_FloatConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("float"); - [Fact] - public void ResolveConstraint_FloatConstraint() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("float"); + // Assert + Assert.IsType(constraint); + } - // Assert - Assert.IsType(constraint); - } + [Fact] + public void ResolveConstraint_GuidConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("guid"); - [Fact] - public void ResolveConstraint_GuidConstraint() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("guid"); + // Assert + Assert.IsType(constraint); + } - // Assert - Assert.IsType(constraint); - } + [Fact] + public void ResolveConstraint_IntConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("int"); - [Fact] - public void ResolveConstraint_IntConstraint() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("int"); + // Assert + Assert.IsType(constraint); + } - // Assert - Assert.IsType(constraint); - } + [Fact] + public void ResolveConstraint_LengthConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("length(5)"); - [Fact] - public void ResolveConstraint_LengthConstraint() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("length(5)"); + // Assert + Assert.IsType(constraint); + Assert.Equal(5, ((LengthRouteConstraint)constraint).MinLength); + Assert.Equal(5, ((LengthRouteConstraint)constraint).MaxLength); + } - // Assert - Assert.IsType(constraint); - Assert.Equal(5, ((LengthRouteConstraint)constraint).MinLength); - Assert.Equal(5, ((LengthRouteConstraint)constraint).MaxLength); - } + [Fact] + public void ResolveConstraint_LengthRangeConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("length(5, 10)"); - [Fact] - public void ResolveConstraint_LengthRangeConstraint() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("length(5, 10)"); + // Assert + var lengthConstraint = Assert.IsType(constraint); + Assert.Equal(5, lengthConstraint.MinLength); + Assert.Equal(10, lengthConstraint.MaxLength); + } - // Assert - var lengthConstraint = Assert.IsType(constraint); - Assert.Equal(5, lengthConstraint.MinLength); - Assert.Equal(10, lengthConstraint.MaxLength); - } + [Fact] + public void ResolveConstraint_LongRangeConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("long"); - [Fact] - public void ResolveConstraint_LongRangeConstraint() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("long"); + // Assert + Assert.IsType(constraint); + } - // Assert - Assert.IsType(constraint); - } + [Fact] + public void ResolveConstraint_MaxConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("max(10)"); - [Fact] - public void ResolveConstraint_MaxConstraint() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("max(10)"); + // Assert + Assert.IsType(constraint); + Assert.Equal(10, ((MaxRouteConstraint)constraint).Max); + } - // Assert - Assert.IsType(constraint); - Assert.Equal(10, ((MaxRouteConstraint)constraint).Max); - } + [Fact] + public void ResolveConstraint_MaxLengthConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("maxlength(10)"); - [Fact] - public void ResolveConstraint_MaxLengthConstraint() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("maxlength(10)"); + // Assert + Assert.IsType(constraint); + Assert.Equal(10, ((MaxLengthRouteConstraint)constraint).MaxLength); + } - // Assert - Assert.IsType(constraint); - Assert.Equal(10, ((MaxLengthRouteConstraint)constraint).MaxLength); - } + [Fact] + public void ResolveConstraint_MinConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("min(3)"); - [Fact] - public void ResolveConstraint_MinConstraint() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("min(3)"); + // Assert + Assert.IsType(constraint); + Assert.Equal(3, ((MinRouteConstraint)constraint).Min); + } - // Assert - Assert.IsType(constraint); - Assert.Equal(3, ((MinRouteConstraint)constraint).Min); - } + [Fact] + public void ResolveConstraint_MinLengthConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("minlength(3)"); - [Fact] - public void ResolveConstraint_MinLengthConstraint() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("minlength(3)"); + // Assert + Assert.IsType(constraint); + Assert.Equal(3, ((MinLengthRouteConstraint)constraint).MinLength); + } - // Assert - Assert.IsType(constraint); - Assert.Equal(3, ((MinLengthRouteConstraint)constraint).MinLength); - } + [Fact] + public void ResolveConstraint_RangeConstraint() + { + // Arrange & Act + var constraint = _constraintResolver.ResolveConstraint("range(5, 10)"); + + // Assert + Assert.IsType(constraint); + var rangeConstraint = (RangeRouteConstraint)constraint; + Assert.Equal(5, rangeConstraint.Min); + Assert.Equal(10, rangeConstraint.Max); + } - [Fact] - public void ResolveConstraint_RangeConstraint() - { - // Arrange & Act - var constraint = _constraintResolver.ResolveConstraint("range(5, 10)"); - - // Assert - Assert.IsType(constraint); - var rangeConstraint = (RangeRouteConstraint)constraint; - Assert.Equal(5, rangeConstraint.Min); - Assert.Equal(10, rangeConstraint.Max); - } + [Fact] + public void ResolveConstraint_SupportsCustomConstraints() + { + // Arrange + var routeOptions = new RouteOptions(); + routeOptions.ConstraintMap.Add("custom", typeof(CustomRouteConstraint)); + var resolver = GetInlineConstraintResolver(routeOptions); - [Fact] - public void ResolveConstraint_SupportsCustomConstraints() - { - // Arrange - var routeOptions = new RouteOptions(); - routeOptions.ConstraintMap.Add("custom", typeof(CustomRouteConstraint)); - var resolver = GetInlineConstraintResolver(routeOptions); + // Act + var constraint = resolver.ResolveConstraint("custom(argument)"); - // Act - var constraint = resolver.ResolveConstraint("custom(argument)"); + // Assert + Assert.IsType(constraint); + } - // Assert - Assert.IsType(constraint); - } + [Fact] + public void ResolveConstraint_SupportsCustomConstraintsUsingNonGenericOverload() + { + // Arrange + var routeOptions = new RouteOptions(); + routeOptions.SetParameterPolicy("custom", typeof(CustomRouteConstraint)); + var resolver = GetInlineConstraintResolver(routeOptions); - [Fact] - public void ResolveConstraint_SupportsCustomConstraintsUsingNonGenericOverload() - { - // Arrange - var routeOptions = new RouteOptions(); - routeOptions.SetParameterPolicy("custom", typeof(CustomRouteConstraint)); - var resolver = GetInlineConstraintResolver(routeOptions); + // Act + var constraint = resolver.ResolveConstraint("custom(argument)"); - // Act - var constraint = resolver.ResolveConstraint("custom(argument)"); + // Assert + Assert.IsType(constraint); + } - // Assert - Assert.IsType(constraint); - } + [Fact] + public void SetParameterPolicyThrowsIfTypeIsNotIParameterPolicy() + { + // Arrange + var routeOptions = new RouteOptions(); + var ex = Assert.Throws(() => routeOptions.SetParameterPolicy("custom", typeof(string))); - [Fact] - public void SetParameterPolicyThrowsIfTypeIsNotIParameterPolicy() - { - // Arrange - var routeOptions = new RouteOptions(); - var ex = Assert.Throws(() => routeOptions.SetParameterPolicy("custom", typeof(string))); + Assert.Equal("System.String must implement Microsoft.AspNetCore.Routing.IParameterPolicy", ex.Message); + } - Assert.Equal("System.String must implement Microsoft.AspNetCore.Routing.IParameterPolicy", ex.Message); - } + [Fact] + public void ResolveConstraint_SupportsCustomConstraintsUsingGenericOverloads() + { + // Arrange + var routeOptions = new RouteOptions(); + routeOptions.SetParameterPolicy("custom"); + var resolver = GetInlineConstraintResolver(routeOptions); - [Fact] - public void ResolveConstraint_SupportsCustomConstraintsUsingGenericOverloads() - { - // Arrange - var routeOptions = new RouteOptions(); - routeOptions.SetParameterPolicy("custom"); - var resolver = GetInlineConstraintResolver(routeOptions); + // Act + var constraint = resolver.ResolveConstraint("custom(argument)"); - // Act - var constraint = resolver.ResolveConstraint("custom(argument)"); + // Assert + Assert.IsType(constraint); + } - // Assert - Assert.IsType(constraint); - } + [Fact] + public void ResolveConstraint_CustomConstraintThatDoesNotImplementIRouteConstraint_Throws() + { + // Arrange + var routeOptions = new RouteOptions(); + routeOptions.ConstraintMap.Add("custom", typeof(string)); + var resolver = GetInlineConstraintResolver(routeOptions); + + // Act & Assert + var ex = Assert.Throws(() => resolver.ResolveConstraint("custom")); + Assert.Equal("The constraint type 'System.String' which is mapped to constraint key 'custom'" + + " must implement the 'IRouteConstraint' interface.", + ex.Message); + } - [Fact] - public void ResolveConstraint_CustomConstraintThatDoesNotImplementIRouteConstraint_Throws() - { - // Arrange - var routeOptions = new RouteOptions(); - routeOptions.ConstraintMap.Add("custom", typeof(string)); - var resolver = GetInlineConstraintResolver(routeOptions); - - // Act & Assert - var ex = Assert.Throws(() => resolver.ResolveConstraint("custom")); - Assert.Equal("The constraint type 'System.String' which is mapped to constraint key 'custom'" + - " must implement the 'IRouteConstraint' interface.", - ex.Message); - } + [Fact] + public void ResolveConstraint_AmbiguousConstructors_Throws() + { + // Arrange + var routeOptions = new RouteOptions(); + routeOptions.ConstraintMap.Add("custom", typeof(MultiConstructorRouteConstraint)); + var resolver = GetInlineConstraintResolver(routeOptions); + + // Act & Assert + var ex = Assert.Throws(() => resolver.ResolveConstraint("custom(5,6)")); + Assert.Equal("The constructor to use for activating the constraint type 'MultiConstructorRouteConstraint' is ambiguous." + + " Multiple constructors were found with the following number of parameters: 2.", + ex.Message); + } - [Fact] - public void ResolveConstraint_AmbiguousConstructors_Throws() - { - // Arrange - var routeOptions = new RouteOptions(); - routeOptions.ConstraintMap.Add("custom", typeof(MultiConstructorRouteConstraint)); - var resolver = GetInlineConstraintResolver(routeOptions); - - // Act & Assert - var ex = Assert.Throws(() => resolver.ResolveConstraint("custom(5,6)")); - Assert.Equal("The constructor to use for activating the constraint type 'MultiConstructorRouteConstraint' is ambiguous." + - " Multiple constructors were found with the following number of parameters: 2.", - ex.Message); - } + // These are cases which parsing does not catch and we'll end up here + [Theory] + [InlineData("regex(abc")] + [InlineData("int/")] + [InlineData("in{t")] + public void ResolveConstraint_Invalid_Throws(string constraint) + { + // Arrange + var routeOptions = new RouteOptions(); + var resolver = GetInlineConstraintResolver(routeOptions); - // These are cases which parsing does not catch and we'll end up here - [Theory] - [InlineData("regex(abc")] - [InlineData("int/")] - [InlineData("in{t")] - public void ResolveConstraint_Invalid_Throws(string constraint) - { - // Arrange - var routeOptions = new RouteOptions(); - var resolver = GetInlineConstraintResolver(routeOptions); + // Act & Assert + Assert.Null(resolver.ResolveConstraint(constraint)); + } - // Act & Assert - Assert.Null(resolver.ResolveConstraint(constraint)); - } + [Fact] + public void ResolveConstraint_NoMatchingConstructor_Throws() + { + // Arrange + // Act & Assert + var ex = Assert.Throws(() => _constraintResolver.ResolveConstraint("int(5,6)")); + Assert.Equal("Could not find a constructor for constraint type 'IntRouteConstraint'" + + " with the following number of parameters: 2.", + ex.Message); + } + + private IInlineConstraintResolver GetInlineConstraintResolver(RouteOptions routeOptions) + { + var optionsAccessor = new Mock>(); + optionsAccessor.SetupGet(o => o.Value).Returns(routeOptions); + + return new DefaultInlineConstraintResolver(optionsAccessor.Object, new TestServiceProvider()); + } - [Fact] - public void ResolveConstraint_NoMatchingConstructor_Throws() + private class MultiConstructorRouteConstraint : IRouteConstraint + { + public MultiConstructorRouteConstraint(string pattern, int intArg) { - // Arrange - // Act & Assert - var ex = Assert.Throws(() => _constraintResolver.ResolveConstraint("int(5,6)")); - Assert.Equal("Could not find a constructor for constraint type 'IntRouteConstraint'" + - " with the following number of parameters: 2.", - ex.Message); } - private IInlineConstraintResolver GetInlineConstraintResolver(RouteOptions routeOptions) + public MultiConstructorRouteConstraint(int intArg, string pattern) { - var optionsAccessor = new Mock>(); - optionsAccessor.SetupGet(o => o.Value).Returns(routeOptions); + } - return new DefaultInlineConstraintResolver(optionsAccessor.Object, new TestServiceProvider()); + public bool Match(HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + return true; } + } - private class MultiConstructorRouteConstraint : IRouteConstraint + private class CustomRouteConstraint : IRouteConstraint + { + public CustomRouteConstraint(string pattern) { - public MultiConstructorRouteConstraint(string pattern, int intArg) - { - } - - public MultiConstructorRouteConstraint(int intArg, string pattern) - { - } - - public bool Match(HttpContext httpContext, - IRouter route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) - { - return true; - } + Pattern = pattern; } - private class CustomRouteConstraint : IRouteConstraint + public string Pattern { get; private set; } + public bool Match(HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) { - public CustomRouteConstraint(string pattern) - { - Pattern = pattern; - } - - public string Pattern { get; private set; } - public bool Match(HttpContext httpContext, - IRouter route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) - { - return true; - } + return true; } } } diff --git a/src/Http/Routing/test/UnitTests/DefaultLinkGeneratorProcessTemplateTest.cs b/src/Http/Routing/test/UnitTests/DefaultLinkGeneratorProcessTemplateTest.cs index 61b47dc9c8..ab2dc4fe44 100644 --- a/src/Http/Routing/test/UnitTests/DefaultLinkGeneratorProcessTemplateTest.cs +++ b/src/Http/Routing/test/UnitTests/DefaultLinkGeneratorProcessTemplateTest.cs @@ -12,1392 +12,1392 @@ using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +// Detailed coverage for how DefaultLinkGenerator processes templates +public class DefaultLinkGeneratorProcessTemplateTest : LinkGeneratorTestBase { - // Detailed coverage for how DefaultLinkGenerator processes templates - public class DefaultLinkGeneratorProcessTemplateTest : LinkGeneratorTestBase + [Fact] + public void TryProcessTemplate_EncodesIntermediate_DefaultValues() { - [Fact] - public void TryProcessTemplate_EncodesIntermediate_DefaultValues() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{p1}/{p2=a b}/{p3=foo}"); - var linkGenerator = CreateLinkGenerator(endpoint); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: null, - endpoint: endpoint, - values: new RouteValueDictionary(new { p1 = "Home", p3 = "bar", }), - ambientValues: null, - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/a%20b/bar", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{p1}/{p2=a b}/{p3=foo}"); + var linkGenerator = CreateLinkGenerator(endpoint); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: null, + endpoint: endpoint, + values: new RouteValueDictionary(new { p1 = "Home", p3 = "bar", }), + ambientValues: null, + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/a%20b/bar", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Theory] - [InlineData("a/b/c", "/Home/Index/a%2Fb%2Fc")] - [InlineData("a/b b1/c c1", "/Home/Index/a%2Fb%20b1%2Fc%20c1")] - public void TryProcessTemplate_EncodesValue_OfSingleAsteriskCatchAllParameter(string routeValue, string expected) - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{*path}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { path = routeValue, }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal(expected, result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Theory] + [InlineData("a/b/c", "/Home/Index/a%2Fb%2Fc")] + [InlineData("a/b b1/c c1", "/Home/Index/a%2Fb%20b1%2Fc%20c1")] + public void TryProcessTemplate_EncodesValue_OfSingleAsteriskCatchAllParameter(string routeValue, string expected) + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{*path}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { path = routeValue, }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal(expected, result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Theory] - [InlineData("/", "/Home/Index//")] - [InlineData("a", "/Home/Index/a")] - [InlineData("a/", "/Home/Index/a/")] - [InlineData("a/b", "/Home/Index/a/b")] - [InlineData("a/b/c", "/Home/Index/a/b/c")] - [InlineData("a/b/cc", "/Home/Index/a/b/cc")] - [InlineData("a/b/c/", "/Home/Index/a/b/c/")] - [InlineData("a/b/c//", "/Home/Index/a/b/c//")] - [InlineData("a//b//c", "/Home/Index/a//b//c")] - public void TryProcessTemplate_DoesNotEncodeSlashes_OfDoubleAsteriskCatchAllParameter(string routeValue, string expected) - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{**path}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { path = routeValue, }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal(expected, result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Theory] + [InlineData("/", "/Home/Index//")] + [InlineData("a", "/Home/Index/a")] + [InlineData("a/", "/Home/Index/a/")] + [InlineData("a/b", "/Home/Index/a/b")] + [InlineData("a/b/c", "/Home/Index/a/b/c")] + [InlineData("a/b/cc", "/Home/Index/a/b/cc")] + [InlineData("a/b/c/", "/Home/Index/a/b/c/")] + [InlineData("a/b/c//", "/Home/Index/a/b/c//")] + [InlineData("a//b//c", "/Home/Index/a//b//c")] + public void TryProcessTemplate_DoesNotEncodeSlashes_OfDoubleAsteriskCatchAllParameter(string routeValue, string expected) + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{**path}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { path = routeValue, }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal(expected, result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_EncodesContentOtherThanSlashes_OfDoubleAsteriskCatchAllParameter() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{**path}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { path = "a/b b1/c c1" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index/a/b%20b1/c%20c1", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_EncodesContentOtherThanSlashes_OfDoubleAsteriskCatchAllParameter() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{**path}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { path = "a/b b1/c c1" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index/a/b%20b1/c%20c1", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_EncodesValues() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { name = "name with %special #characters" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index", result.path.ToUriComponent()); - Assert.Equal("?name=name%20with%20%25special%20%23characters", result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_EncodesValues() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { name = "name with %special #characters" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index", result.path.ToUriComponent()); + Assert.Equal("?name=name%20with%20%25special%20%23characters", result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_ForListOfStrings() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { color = new List { "red", "green", "blue" } }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index", result.path.ToUriComponent()); - Assert.Equal("?color=red&color=green&color=blue", result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_ForListOfStrings() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { color = new List { "red", "green", "blue" } }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index", result.path.ToUriComponent()); + Assert.Equal("?color=red&color=green&color=blue", result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_ForListOfInts() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { items = new List { 10, 20, 30 } }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index", result.path.ToUriComponent()); - Assert.Equal("?items=10&items=20&items=30", result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_ForListOfInts() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { items = new List { 10, 20, 30 } }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index", result.path.ToUriComponent()); + Assert.Equal("?items=10&items=20&items=30", result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_ForList_Empty() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { color = new List { } }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_ForList_Empty() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { color = new List { } }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_ForList_StringWorkaround() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { page = 1, color = new List { "red", "green", "blue" }, message = "textfortest" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index", result.path.ToUriComponent()); - Assert.Equal("?page=1&color=red&color=green&color=blue&message=textfortest", result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_ForList_StringWorkaround() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Index" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { page = 1, color = new List { "red", "green", "blue" }, message = "textfortest" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index", result.path.ToUriComponent()); + Assert.Equal("?page=1&color=red&color=green&color=blue&message=textfortest", result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_Success_AmbientValues() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_Success_AmbientValues() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_GeneratesLowercaseUrl_SetOnRouteOptions() + [Fact] + public void TryProcessTemplate_GeneratesLowercaseUrl_SetOnRouteOptions() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); + Action configure = (s) => { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - Action configure = (s) => - { - s.Configure(o => o.LowercaseUrls = true); - }; - - var linkGenerator = CreateLinkGenerator(configure, endpoints: new[] { endpoint, }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/home/index", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + s.Configure(o => o.LowercaseUrls = true); + }; + + var linkGenerator = CreateLinkGenerator(configure, endpoints: new[] { endpoint, }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/home/index", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - // Regression test for https://github.com/aspnet/Routing/issues/802 - [Fact] - public void TryProcessTemplate_GeneratesLowercaseUrl_Includes_BufferedValues_SetOnRouteOptions() + // Regression test for https://github.com/aspnet/Routing/issues/802 + [Fact] + public void TryProcessTemplate_GeneratesLowercaseUrl_Includes_BufferedValues_SetOnRouteOptions() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("Foo/{bar=BAR}/{id?}"); + Action configure = (s) => { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("Foo/{bar=BAR}/{id?}"); - Action configure = (s) => - { - s.Configure(o => o.LowercaseUrls = true); - }; - - var linkGenerator = CreateLinkGenerator(configure, endpoints: new[] { endpoint, }); - var httpContext = CreateHttpContext(); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { id = "18" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/foo/bar/18", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + s.Configure(o => o.LowercaseUrls = true); + }; + + var linkGenerator = CreateLinkGenerator(configure, endpoints: new[] { endpoint, }); + var httpContext = CreateHttpContext(); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { id = "18" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/foo/bar/18", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - // Regression test for https://github.com/aspnet/Routing/issues/802 - [Fact] - public void TryProcessTemplate_ParameterPolicy_Includes_BufferedValues() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("Foo/{bar=MyBar}/{id?}", policies: new { bar = new SlugifyParameterTransformer(), }); - var linkGenerator = CreateLinkGenerator(endpoints: new[] { endpoint, }); - var httpContext = CreateHttpContext(); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { id = "18" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Foo/my-bar/18", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + // Regression test for https://github.com/aspnet/Routing/issues/802 + [Fact] + public void TryProcessTemplate_ParameterPolicy_Includes_BufferedValues() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("Foo/{bar=MyBar}/{id?}", policies: new { bar = new SlugifyParameterTransformer(), }); + var linkGenerator = CreateLinkGenerator(endpoints: new[] { endpoint, }); + var httpContext = CreateHttpContext(); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { id = "18" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Foo/my-bar/18", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - // Regression test for aspnet/Routing#435 - // - // In this issue we used to lowercase URLs after parameters were encoded, meaning that if a character needed - // encoding (such as a cyrillic character, it would not be encoded). - [Fact] - public void TryProcessTemplate_GeneratesLowercaseUrl_SetOnRouteOptions_CanLowercaseCharactersThatNeedEncoding() + // Regression test for aspnet/Routing#435 + // + // In this issue we used to lowercase URLs after parameters were encoded, meaning that if a character needed + // encoding (such as a cyrillic character, it would not be encoded). + [Fact] + public void TryProcessTemplate_GeneratesLowercaseUrl_SetOnRouteOptions_CanLowercaseCharactersThatNeedEncoding() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); + Action configure = (s) => { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - Action configure = (s) => - { - s.Configure(o => o.LowercaseUrls = true); - }; - - var linkGenerator = CreateLinkGenerator(configure, endpoints: new[] { endpoint, }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "П" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), // Cryillic uppercase Pe - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/home/%D0%BF", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - - // Convert back to decoded. - // - // This is Cyrillic lowercase Pe (not an n). - Assert.Equal("/home/п", PathString.FromUriComponent(result.path.ToUriComponent()).Value); - } + s.Configure(o => o.LowercaseUrls = true); + }; + + var linkGenerator = CreateLinkGenerator(configure, endpoints: new[] { endpoint, }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "П" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), // Cryillic uppercase Pe + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/home/%D0%BF", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + + // Convert back to decoded. + // + // This is Cyrillic lowercase Pe (not an n). + Assert.Equal("/home/п", PathString.FromUriComponent(result.path.ToUriComponent()).Value); + } - [Fact] - public void TryProcessTemplate_GeneratesLowercaseQueryString_SetOnRouteOptions() + [Fact] + public void TryProcessTemplate_GeneratesLowercaseQueryString_SetOnRouteOptions() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); + Action configure = (s) => { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - Action configure = (s) => + s.Configure(o => { - s.Configure(o => - { - o.LowercaseUrls = true; - o.LowercaseQueryStrings = true; - }); - }; - - var linkGenerator = CreateLinkGenerator( - configure, - endpoints: new[] { endpoint, }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/home/index", result.path.ToUriComponent()); - Assert.Equal("?showstatus=true&info=detailed", result.query.ToUriComponent()); - } + o.LowercaseUrls = true; + o.LowercaseQueryStrings = true; + }); + }; + + var linkGenerator = CreateLinkGenerator( + configure, + endpoints: new[] { endpoint, }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/home/index", result.path.ToUriComponent()); + Assert.Equal("?showstatus=true&info=detailed", result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_AppendsTrailingSlash_SetOnRouteOptions() + [Fact] + public void TryProcessTemplate_AppendsTrailingSlash_SetOnRouteOptions() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); + Action configure = (s) => { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - Action configure = (s) => - { - s.Configure(o => o.AppendTrailingSlash = true); - }; - - var linkGenerator = CreateLinkGenerator( - configure, - endpoints: new[] { endpoint }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index/", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + s.Configure(o => o.AppendTrailingSlash = true); + }; + + var linkGenerator = CreateLinkGenerator( + configure, + endpoints: new[] { endpoint }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index/", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_GeneratesLowercaseQueryStringAndTrailingSlash_SetOnRouteOptions() + [Fact] + public void TryProcessTemplate_GeneratesLowercaseQueryStringAndTrailingSlash_SetOnRouteOptions() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); + Action configure = (s) => { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - Action configure = (s) => + s.Configure(o => { - s.Configure(o => - { - o.LowercaseUrls = true; - o.LowercaseQueryStrings = true; - o.AppendTrailingSlash = true; - }); - }; - - var linkGenerator = CreateLinkGenerator( - configure, - endpoints: new[] { endpoint }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/home/index/", result.path.ToUriComponent()); - Assert.Equal("?showstatus=true&info=detailed", result.query.ToUriComponent()); - } + o.LowercaseUrls = true; + o.LowercaseQueryStrings = true; + o.AppendTrailingSlash = true; + }); + }; + + var linkGenerator = CreateLinkGenerator( + configure, + endpoints: new[] { endpoint }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/home/index/", result.path.ToUriComponent()); + Assert.Equal("?showstatus=true&info=detailed", result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_LowercaseUrlSetToTrue_OnRouteOptions_OverridenByCallsiteValue() + [Fact] + public void TryProcessTemplate_LowercaseUrlSetToTrue_OnRouteOptions_OverridenByCallsiteValue() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); + Action configure = (s) => { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - Action configure = (s) => + s.Configure(o => o.LowercaseUrls = true); + }; + + var linkGenerator = CreateLinkGenerator( + configure, + endpoints: new[] { endpoint }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "HoMe" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "InDex" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: new LinkOptions { - s.Configure(o => o.LowercaseUrls = true); - }; - - var linkGenerator = CreateLinkGenerator( - configure, - endpoints: new[] { endpoint }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "HoMe" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "InDex" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: new LinkOptions - { - LowercaseUrls = false - }, - result: out var result); + LowercaseUrls = false + }, + result: out var result); - // Assert - Assert.True(success); - Assert.Equal("/HoMe/InDex", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + // Assert + Assert.True(success); + Assert.Equal("/HoMe/InDex", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_LowercaseUrlSetToFalse_OnRouteOptions_OverridenByCallsiteValue() + [Fact] + public void TryProcessTemplate_LowercaseUrlSetToFalse_OnRouteOptions_OverridenByCallsiteValue() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); + Action configure = (s) => { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - Action configure = (s) => + s.Configure(o => o.LowercaseUrls = false); + }; + + var linkGenerator = CreateLinkGenerator( + configure, + endpoints: new[] { endpoint }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "HoMe" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "InDex" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: new LinkOptions() { - s.Configure(o => o.LowercaseUrls = false); - }; - - var linkGenerator = CreateLinkGenerator( - configure, - endpoints: new[] { endpoint }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "HoMe" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "InDex" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: new LinkOptions() - { - LowercaseUrls = true - }, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/home/index", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + LowercaseUrls = true + }, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/home/index", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_LowercaseUrlQueryStringsSetToTrue_OnRouteOptions_OverridenByCallsiteValue() + [Fact] + public void TryProcessTemplate_LowercaseUrlQueryStringsSetToTrue_OnRouteOptions_OverridenByCallsiteValue() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); + Action configure = (s) => { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - Action configure = (s) => + s.Configure(o => { - s.Configure(o => - { - o.LowercaseUrls = true; - o.LowercaseQueryStrings = true; - }); - }; - - var linkGenerator = CreateLinkGenerator( - configure, - endpoints: new[] { endpoint }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: new LinkOptions - { - LowercaseUrls = false, - LowercaseQueryStrings = false - }, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index", result.path.ToUriComponent()); - Assert.Equal("?ShowStatus=True&INFO=DETAILED", result.query.ToUriComponent()); - } - - [Fact] - public void TryProcessTemplate_LowercaseUrlQueryStringsSetToFalse_OnRouteOptions_OverridenByCallsiteValue() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - Action configure = (s) => + o.LowercaseUrls = true; + o.LowercaseQueryStrings = true; + }); + }; + + var linkGenerator = CreateLinkGenerator( + configure, + endpoints: new[] { endpoint }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: new LinkOptions { - s.Configure(o => - { - o.LowercaseUrls = false; - o.LowercaseQueryStrings = false; - }); - }; - - var linkGenerator = CreateLinkGenerator( - configure, - endpoints: new[] { endpoint }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: new LinkOptions() - { - LowercaseUrls = true, - LowercaseQueryStrings = true, - }, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/home/index", result.path.ToUriComponent()); - Assert.Equal("?showstatus=true&info=detailed", result.query.ToUriComponent()); - } + LowercaseUrls = false, + LowercaseQueryStrings = false + }, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index", result.path.ToUriComponent()); + Assert.Equal("?ShowStatus=True&INFO=DETAILED", result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_AppendTrailingSlashSetToFalse_OnRouteOptions_OverridenByCallsiteValue() + [Fact] + public void TryProcessTemplate_LowercaseUrlQueryStringsSetToFalse_OnRouteOptions_OverridenByCallsiteValue() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); + Action configure = (s) => { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); - Action configure = (s) => + s.Configure(o => { - s.Configure(o => o.AppendTrailingSlash = false); - }; - - var linkGenerator = CreateLinkGenerator( - configure, - endpoints: new[] { endpoint }); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: new LinkOptions() { AppendTrailingSlash = true, }, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index/", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + o.LowercaseUrls = false; + o.LowercaseQueryStrings = false; + }); + }; + + var linkGenerator = CreateLinkGenerator( + configure, + endpoints: new[] { endpoint }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", ShowStatus = "True", INFO = "DETAILED" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: new LinkOptions() + { + LowercaseUrls = true, + LowercaseQueryStrings = true, + }, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/home/index", result.path.ToUriComponent()); + Assert.Equal("?showstatus=true&info=detailed", result.query.ToUriComponent()); + } - [Fact] - public void RouteGenerationRejectsConstraints() + [Fact] + public void TryProcessTemplate_AppendTrailingSlashSetToFalse_OnRouteOptions_OverridenByCallsiteValue() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}"); + Action configure = (s) => { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "{p1}/{p2}", - defaults: new { p2 = "catchall" }, - policies: new { p2 = "\\d{4}" }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { p1 = "abcd" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.False(success); - } + s.Configure(o => o.AppendTrailingSlash = false); + }; + + var linkGenerator = CreateLinkGenerator( + configure, + endpoints: new[] { endpoint }); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: new LinkOptions() { AppendTrailingSlash = true, }, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index/", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void RouteGenerationAcceptsConstraints() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "{p1}/{p2}", - defaults: new { p2 = "catchall" }, - policies: new { p2 = new RegexRouteConstraint("\\d{4}"), }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { p1 = "hello", p2 = "1234" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/hello/1234", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void RouteGenerationRejectsConstraints() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "{p1}/{p2}", + defaults: new { p2 = "catchall" }, + policies: new { p2 = "\\d{4}" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { p1 = "abcd" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.False(success); + } - [Fact] - public void RouteWithCatchAllRejectsConstraints() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "{p1}/{*p2}", - defaults: new { p2 = "catchall" }, - policies: new { p2 = new RegexRouteConstraint("\\d{4}") }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { p1 = "abcd" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.False(success); - } + [Fact] + public void RouteGenerationAcceptsConstraints() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "{p1}/{p2}", + defaults: new { p2 = "catchall" }, + policies: new { p2 = new RegexRouteConstraint("\\d{4}"), }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { p1 = "hello", p2 = "1234" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/hello/1234", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void RouteWithCatchAllAcceptsConstraints() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "{p1}/{*p2}", - defaults: new { p2 = "catchall" }, - policies: new { p2 = new RegexRouteConstraint("\\d{4}") }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { p1 = "hello", p2 = "1234" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/hello/1234", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void RouteWithCatchAllRejectsConstraints() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "{p1}/{*p2}", + defaults: new { p2 = "catchall" }, + policies: new { p2 = new RegexRouteConstraint("\\d{4}") }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { p1 = "abcd" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.False(success); + } - [Fact] - public void GetLinkWithNonParameterConstraintReturnsUrlWithoutQueryString() - { - // Arrange - var target = new Mock(); - target - .Setup( - e => e.Match( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(true) - .Verifiable(); - var endpoint = EndpointFactory.CreateRouteEndpoint( - "{p1}/{p2}", - defaults: new { p2 = "catchall" }, - policies: new { p2 = target.Object }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { p1 = "hello", p2 = "1234" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/hello/1234", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - - target.VerifyAll(); - } + [Fact] + public void RouteWithCatchAllAcceptsConstraints() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "{p1}/{*p2}", + defaults: new { p2 = "catchall" }, + policies: new { p2 = new RegexRouteConstraint("\\d{4}") }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { p1 = "hello", p2 = "1234" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/hello/1234", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } + [Fact] + public void GetLinkWithNonParameterConstraintReturnsUrlWithoutQueryString() + { + // Arrange + var target = new Mock(); + target + .Setup( + e => e.Match( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(true) + .Verifiable(); + var endpoint = EndpointFactory.CreateRouteEndpoint( + "{p1}/{p2}", + defaults: new { p2 = "catchall" }, + policies: new { p2 = target.Object }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { p1 = "hello", p2 = "1234" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/hello/1234", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + + target.VerifyAll(); + } - // Any ambient values from the current request should be visible to constraint, even - // if they have nothing to do with the route generating a link - [Fact] - public void TryProcessTemplate_ConstraintsSeeAmbientValues() - { - // Arrange - var constraint = new CapturingConstraint(); - var endpoint = EndpointFactory.CreateRouteEndpoint( - template: "slug/Home/Store", - defaults: new { controller = "Home", action = "Store" }, - policies: new { c = constraint }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext( - ambientValues: new { controller = "Home", action = "Blog", extra = "42" }); - var expectedValues = new RouteValueDictionary( - new { controller = "Home", action = "Store", extra = "42" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Store" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/slug/Home/Store", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - - Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key)); - } - // Non-parameter default values from the routing generating a link are not in the 'values' - // collection when constraints are processed. - [Fact] - public void TryProcessTemplate_ConstraintsDontSeeDefaults_WhenTheyArentParameters() - { - // Arrange - var constraint = new CapturingConstraint(); - var endpoint = EndpointFactory.CreateRouteEndpoint( - template: "slug/Home/Store", - defaults: new { controller = "Home", action = "Store", otherthing = "17" }, - policies: new { c = constraint }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Blog" }); - var expectedValues = new RouteValueDictionary( - new { controller = "Home", action = "Store" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Store" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/slug/Home/Store", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - - Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key)); - } + // Any ambient values from the current request should be visible to constraint, even + // if they have nothing to do with the route generating a link + [Fact] + public void TryProcessTemplate_ConstraintsSeeAmbientValues() + { + // Arrange + var constraint = new CapturingConstraint(); + var endpoint = EndpointFactory.CreateRouteEndpoint( + template: "slug/Home/Store", + defaults: new { controller = "Home", action = "Store" }, + policies: new { c = constraint }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext( + ambientValues: new { controller = "Home", action = "Blog", extra = "42" }); + var expectedValues = new RouteValueDictionary( + new { controller = "Home", action = "Store", extra = "42" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Store" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/slug/Home/Store", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + + Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key)); + } - // Default values are visible to the constraint when they are used to fill a parameter. - [Fact] - public void TryProcessTemplate_ConstraintsSeesDefault_WhenThereItsAParameter() - { - // Arrange - var constraint = new CapturingConstraint(); - var endpoint = EndpointFactory.CreateRouteEndpoint( - template: "slug/{controller}/{action}", - defaults: new { action = "Index" }, - policies: new { c = constraint, }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Blog" }); - var expectedValues = new RouteValueDictionary( - new { controller = "Shopping", action = "Index" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { controller = "Shopping" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/slug/Shopping", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - Assert.Equal(expectedValues, constraint.Values); - } + // Non-parameter default values from the routing generating a link are not in the 'values' + // collection when constraints are processed. + [Fact] + public void TryProcessTemplate_ConstraintsDontSeeDefaults_WhenTheyArentParameters() + { + // Arrange + var constraint = new CapturingConstraint(); + var endpoint = EndpointFactory.CreateRouteEndpoint( + template: "slug/Home/Store", + defaults: new { controller = "Home", action = "Store", otherthing = "17" }, + policies: new { c = constraint }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Blog" }); + var expectedValues = new RouteValueDictionary( + new { controller = "Home", action = "Store" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Store" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/slug/Home/Store", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + + Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key)); + } - // Default values from the routing generating a link are in the 'values' collection when - // constraints are processed - IFF they are specified as values or ambient values. - [Fact] - public void TryProcessTemplate_ConstraintsSeeDefaults_IfTheyAreSpecifiedOrAmbient() - { - // Arrange - var constraint = new CapturingConstraint(); - var endpoint = EndpointFactory.CreateRouteEndpoint( - template: "slug/Home/Store", - defaults: new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" }, - policies: new { c = constraint, }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext( - ambientValues: new { controller = "Home", action = "Blog", otherthing = "17" }); - - var expectedValues = new RouteValueDictionary( - new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Store", thirdthing = "13" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/slug/Home/Store", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - - Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key)); - } + // Default values are visible to the constraint when they are used to fill a parameter. + [Fact] + public void TryProcessTemplate_ConstraintsSeesDefault_WhenThereItsAParameter() + { + // Arrange + var constraint = new CapturingConstraint(); + var endpoint = EndpointFactory.CreateRouteEndpoint( + template: "slug/{controller}/{action}", + defaults: new { action = "Index" }, + policies: new { c = constraint, }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { controller = "Home", action = "Blog" }); + var expectedValues = new RouteValueDictionary( + new { controller = "Shopping", action = "Index" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { controller = "Shopping" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/slug/Shopping", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + Assert.Equal(expectedValues, constraint.Values); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void TryProcessTemplate_InlineConstraints_Success(bool hasHttpContext) - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - template: "Home/Index/{id:int}", - defaults: new { controller = "Home", action = "Index" }, - policies: new { }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = hasHttpContext ? CreateHttpContext(new { }) : null; - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", controller = "Home", id = 4 }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index/4", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + // Default values from the routing generating a link are in the 'values' collection when + // constraints are processed - IFF they are specified as values or ambient values. + [Fact] + public void TryProcessTemplate_ConstraintsSeeDefaults_IfTheyAreSpecifiedOrAmbient() + { + // Arrange + var constraint = new CapturingConstraint(); + var endpoint = EndpointFactory.CreateRouteEndpoint( + template: "slug/Home/Store", + defaults: new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" }, + policies: new { c = constraint, }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext( + ambientValues: new { controller = "Home", action = "Blog", otherthing = "17" }); + + var expectedValues = new RouteValueDictionary( + new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Store", thirdthing = "13" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/slug/Home/Store", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + + Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key)); + } - [Fact] - public void TryProcessTemplate_InlineConstraints_NonMatchingvalue() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - template: "Home/Index/{id}", - defaults: new { controller = "Home", action = "Index" }, - policies: new { id = "int" }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", controller = "Home", id = "not-an-integer" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.False(success); - } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TryProcessTemplate_InlineConstraints_Success(bool hasHttpContext) + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + template: "Home/Index/{id:int}", + defaults: new { controller = "Home", action = "Index" }, + policies: new { }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = hasHttpContext ? CreateHttpContext(new { }) : null; + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", controller = "Home", id = 4 }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index/4", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void TryProcessTemplate_InlineConstraints_OptionalParameter_ValuePresent(bool hasHttpContext) - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - template: "Home/Index/{id:int?}", - defaults: new { controller = "Home", action = "Index" }, - policies: new { }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = hasHttpContext ? CreateHttpContext(new { }) : null; - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", controller = "Home", id = 98 }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index/98", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_InlineConstraints_NonMatchingvalue() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + template: "Home/Index/{id}", + defaults: new { controller = "Home", action = "Index" }, + policies: new { id = "int" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", controller = "Home", id = "not-an-integer" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.False(success); + } - [Fact] - public void TryProcessTemplate_InlineConstraints_OptionalParameter_ValueNotPresent() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - template: "Home/Index/{id?}", - defaults: new { controller = "Home", action = "Index" }, - policies: new { id = "int" }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", controller = "Home" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TryProcessTemplate_InlineConstraints_OptionalParameter_ValuePresent(bool hasHttpContext) + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + template: "Home/Index/{id:int?}", + defaults: new { controller = "Home", action = "Index" }, + policies: new { }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = hasHttpContext ? CreateHttpContext(new { }) : null; + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", controller = "Home", id = 98 }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index/98", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_InlineConstraints_OptionalParameter_ValuePresent_ConstraintFails() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - template: "Home/Index/{id?}", - defaults: new { controller = "Home", action = "Index" }, - policies: new { id = "int" }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", controller = "Home", id = "not-an-integer" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.False(success); - } + [Fact] + public void TryProcessTemplate_InlineConstraints_OptionalParameter_ValueNotPresent() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + template: "Home/Index/{id?}", + defaults: new { controller = "Home", action = "Index" }, + policies: new { id = "int" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", controller = "Home" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void TryProcessTemplate_InlineConstraints_MultipleInlineConstraints(bool hasHttpContext) - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - template: "Home/Index/{id:int:range(1,20)}", - defaults: new { controller = "Home", action = "Index" }, - policies: new { }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = hasHttpContext ? CreateHttpContext(new { }) : null; - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", controller = "Home", id = 14 }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index/14", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_InlineConstraints_OptionalParameter_ValuePresent_ConstraintFails() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + template: "Home/Index/{id?}", + defaults: new { controller = "Home", action = "Index" }, + policies: new { id = "int" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", controller = "Home", id = "not-an-integer" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.False(success); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public void TryProcessTemplate_InlineConstraints_CompositeInlineConstraint_Fails(bool hasHttpContext) - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - template: "Home/Index/{id:int:range(1,20)}", - defaults: new { controller = "Home", action = "Index" }, - policies: new { }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = hasHttpContext ? CreateHttpContext(new { }) : null; - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", controller = "Home", id = 50 }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.False(success); - } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TryProcessTemplate_InlineConstraints_MultipleInlineConstraints(bool hasHttpContext) + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + template: "Home/Index/{id:int:range(1,20)}", + defaults: new { controller = "Home", action = "Index" }, + policies: new { }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = hasHttpContext ? CreateHttpContext(new { }) : null; + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", controller = "Home", id = 14 }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index/14", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_InlineConstraints_CompositeConstraint_FromConstructor() - { - // Arrange - var constraint = new MaxLengthRouteConstraint(20); - var endpoint = EndpointFactory.CreateRouteEndpoint( - template: "Home/Index/{name}", - defaults: new { controller = "Home", action = "Index" }, - policies: new { name = constraint }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", controller = "Home", name = "products" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index/products", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TryProcessTemplate_InlineConstraints_CompositeInlineConstraint_Fails(bool hasHttpContext) + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + template: "Home/Index/{id:int:range(1,20)}", + defaults: new { controller = "Home", action = "Index" }, + policies: new { }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = hasHttpContext ? CreateHttpContext(new { }) : null; + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", controller = "Home", id = 50 }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.False(success); + } - [Fact] - public void TryProcessTemplate_OptionalParameter_ParameterPresentInValues() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{name?}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", controller = "Home", name = "products" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index/products", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_InlineConstraints_CompositeConstraint_FromConstructor() + { + // Arrange + var constraint = new MaxLengthRouteConstraint(20); + var endpoint = EndpointFactory.CreateRouteEndpoint( + template: "Home/Index/{name}", + defaults: new { controller = "Home", action = "Index" }, + policies: new { name = constraint }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", controller = "Home", name = "products" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index/products", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_OptionalParameter_ParameterNotPresentInValues() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{name?}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", controller = "Home" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_OptionalParameter_ParameterPresentInValues() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{name?}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", controller = "Home", name = "products" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index/products", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_OptionalParameter_ParameterPresentInValuesAndDefaults() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - template: "{controller}/{action}/{name}", - defaults: new { name = "default-products" }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", controller = "Home", name = "products" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index/products", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_OptionalParameter_ParameterNotPresentInValues() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{name?}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", controller = "Home" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_OptionalParameter_ParameterNotPresentInValues_PresentInDefaults() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - template: "{controller}/{action}/{name}", - defaults: new { name = "products" }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", controller = "Home" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_OptionalParameter_ParameterPresentInValuesAndDefaults() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + template: "{controller}/{action}/{name}", + defaults: new { name = "default-products" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", controller = "Home", name = "products" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index/products", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_ParameterNotPresentInTemplate_PresentInValues() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{name}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", controller = "Home", name = "products", format = "json" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index/products", result.path.ToUriComponent()); - Assert.Equal("?format=json", result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_OptionalParameter_ParameterNotPresentInValues_PresentInDefaults() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + template: "{controller}/{action}/{name}", + defaults: new { name = "products" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", controller = "Home" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_OptionalParameter_FollowedByDotAfterSlash_ParameterPresent() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - template: "{controller}/{action}/.{name?}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", controller = "Home", name = "products" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index/.products", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_ParameterNotPresentInTemplate_PresentInValues() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{name}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", controller = "Home", name = "products", format = "json" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index/products", result.path.ToUriComponent()); + Assert.Equal("?format=json", result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_OptionalParameter_FollowedByDotAfterSlash_ParameterNotPresent() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/.{name?}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", controller = "Home" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - - Assert.True(success); - Assert.Equal("/Home/Index/", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_OptionalParameter_FollowedByDotAfterSlash_ParameterPresent() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + template: "{controller}/{action}/.{name?}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", controller = "Home", name = "products" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index/.products", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_OptionalParameter_InSimpleSegment() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{name?}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { action = "Index", controller = "Home" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Home/Index", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_OptionalParameter_FollowedByDotAfterSlash_ParameterNotPresent() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/.{name?}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", controller = "Home" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + + Assert.True(success); + Assert.Equal("/Home/Index/", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_TwoOptionalParameters_OneValueFromAmbientValues() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("a/{b=15}/{c?}/{d?}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { c = "17" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/a/15/17", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_OptionalParameter_InSimpleSegment() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{name?}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { action = "Index", controller = "Home" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Home/Index", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_OptionalParameterAfterDefault_OneValueFromAmbientValues() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("a/{b=15}/{c?}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { c = "17" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/a/15/17", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_TwoOptionalParameters_OneValueFromAmbientValues() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("a/{b=15}/{c?}/{d?}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { c = "17" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/a/15/17", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_TwoOptionalParametersAfterDefault_LastValueFromAmbientValues() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("a/{b=15}/{c?}/{d?}"); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { d = "17" }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/a", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_OptionalParameterAfterDefault_OneValueFromAmbientValues() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("a/{b=15}/{c?}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { c = "17" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/a/15/17", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } + + [Fact] + public void TryProcessTemplate_TwoOptionalParametersAfterDefault_LastValueFromAmbientValues() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("a/{b=15}/{c?}/{d?}"); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { d = "17" }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/a", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - public static TheoryData DoesNotDiscardAmbientValuesData + public static TheoryData DoesNotDiscardAmbientValuesData + { + get { - get - { - // - ambient values - // - explicit values - // - required values - // - defaults - return new TheoryData + // - ambient values + // - explicit values + // - required values + // - defaults + return new TheoryData { // link to same action on same controller { @@ -1459,103 +1459,103 @@ namespace Microsoft.AspNetCore.Routing new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" } }, }; - } } + } - [Theory] - [MemberData(nameof(DoesNotDiscardAmbientValuesData))] - public void TryProcessTemplate_DoesNotDiscardAmbientValues_IfAllRequiredKeysMatch( - object ambientValues, - object explicitValues, - object requiredValues, - object defaults) - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "Products/Edit/{id}", - requiredValues: requiredValues, - defaults: defaults); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(explicitValues), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Products/Edit/10", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Theory] + [MemberData(nameof(DoesNotDiscardAmbientValuesData))] + public void TryProcessTemplate_DoesNotDiscardAmbientValues_IfAllRequiredKeysMatch( + object ambientValues, + object explicitValues, + object requiredValues, + object defaults) + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "Products/Edit/{id}", + requiredValues: requiredValues, + defaults: defaults); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(explicitValues), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Products/Edit/10", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_DoesNotDiscardAmbientValues_IfAllRequiredValuesMatch_ForGenericKeys() - { - // Verifying that discarding works in general usage case i.e when keys are not like controller, action etc. - - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "Products/Edit/{id}", - requiredValues: new { c = "Products", a = "Edit" }, - defaults: new { c = "Products", a = "Edit" }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { c = "Products", a = "Edit", id = 10 }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { c = "Products", a = "Edit" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.True(success); - Assert.Equal("/Products/Edit/10", result.path.ToUriComponent()); - Assert.Equal(string.Empty, result.query.ToUriComponent()); - } + [Fact] + public void TryProcessTemplate_DoesNotDiscardAmbientValues_IfAllRequiredValuesMatch_ForGenericKeys() + { + // Verifying that discarding works in general usage case i.e when keys are not like controller, action etc. + + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "Products/Edit/{id}", + requiredValues: new { c = "Products", a = "Edit" }, + defaults: new { c = "Products", a = "Edit" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { c = "Products", a = "Edit", id = 10 }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { c = "Products", a = "Edit" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.True(success); + Assert.Equal("/Products/Edit/10", result.path.ToUriComponent()); + Assert.Equal(string.Empty, result.query.ToUriComponent()); + } - [Fact] - public void TryProcessTemplate_DiscardsAmbientValues_ForGenericKeys() - { - // Verifying that discarding works in general usage case i.e when keys are not like controller, action etc. - - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "Products/Edit/{id}", - requiredValues: new { c = "Products", a = "Edit" }, - defaults: new { c = "Products", a = "Edit" }); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues: new { c = "Products", a = "Edit", id = 10 }); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(new { c = "Products", a = "List" }), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.False(success); - } + [Fact] + public void TryProcessTemplate_DiscardsAmbientValues_ForGenericKeys() + { + // Verifying that discarding works in general usage case i.e when keys are not like controller, action etc. + + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "Products/Edit/{id}", + requiredValues: new { c = "Products", a = "Edit" }, + defaults: new { c = "Products", a = "Edit" }); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues: new { c = "Products", a = "Edit", id = 10 }); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(new { c = "Products", a = "List" }), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.False(success); + } - public static TheoryData DiscardAmbientValuesData + public static TheoryData DiscardAmbientValuesData + { + get { - get - { - // - ambient values - // - explicit values - // - required values - // - defaults - return new TheoryData + // - ambient values + // - explicit values + // - required values + // - defaults + return new TheoryData { // link to different action on same controller { @@ -1621,38 +1621,37 @@ namespace Microsoft.AspNetCore.Routing new { area = (string)null, controller = (string)null, action = (string)null, page = "Products/Edit" } }, }; - } } + } - [Theory] - [MemberData(nameof(DiscardAmbientValuesData))] - public void TryProcessTemplate_DiscardsAmbientValues_IfAnyAmbientValue_IsDifferentThan_EndpointRequiredValues( - object ambientValues, - object explicitValues, - object requiredValues, - object defaults) - { - // Linking to a different action on the same controller - - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "Products/Edit/{id}", - requiredValues: requiredValues, - defaults: defaults); - var linkGenerator = CreateLinkGenerator(endpoint); - var httpContext = CreateHttpContext(ambientValues); - - // Act - var success = linkGenerator.TryProcessTemplate( - httpContext: httpContext, - endpoint: endpoint, - values: new RouteValueDictionary(explicitValues), - ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), - options: null, - result: out var result); - - // Assert - Assert.False(success); - } + [Theory] + [MemberData(nameof(DiscardAmbientValuesData))] + public void TryProcessTemplate_DiscardsAmbientValues_IfAnyAmbientValue_IsDifferentThan_EndpointRequiredValues( + object ambientValues, + object explicitValues, + object requiredValues, + object defaults) + { + // Linking to a different action on the same controller + + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "Products/Edit/{id}", + requiredValues: requiredValues, + defaults: defaults); + var linkGenerator = CreateLinkGenerator(endpoint); + var httpContext = CreateHttpContext(ambientValues); + + // Act + var success = linkGenerator.TryProcessTemplate( + httpContext: httpContext, + endpoint: endpoint, + values: new RouteValueDictionary(explicitValues), + ambientValues: DefaultLinkGenerator.GetAmbientValues(httpContext), + options: null, + result: out var result); + + // Assert + Assert.False(success); } } diff --git a/src/Http/Routing/test/UnitTests/DefaultLinkGeneratorTest.cs b/src/Http/Routing/test/UnitTests/DefaultLinkGeneratorTest.cs index 69cc3a1db6..1be83277ba 100644 --- a/src/Http/Routing/test/UnitTests/DefaultLinkGeneratorTest.cs +++ b/src/Http/Routing/test/UnitTests/DefaultLinkGeneratorTest.cs @@ -10,754 +10,753 @@ using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +// Tests LinkGenerator functionality using GetXyzByAddress - see tests for the extension +// methods for more E2E tests. +// +// Does not cover template processing in detail, those scenarios are validated by TemplateBinderTests +// and DefaultLinkGeneratorProcessTemplateTest +public class DefaultLinkGeneratorTest : LinkGeneratorTestBase { - // Tests LinkGenerator functionality using GetXyzByAddress - see tests for the extension - // methods for more E2E tests. - // - // Does not cover template processing in detail, those scenarios are validated by TemplateBinderTests - // and DefaultLinkGeneratorProcessTemplateTest - public class DefaultLinkGeneratorTest : LinkGeneratorTestBase - { - [Fact] - public void GetPathByAddress_WithoutHttpContext_NoMatches_ReturnsNull() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + [Fact] + public void GetPathByAddress_WithoutHttpContext_NoMatches_ReturnsNull() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - var linkGenerator = CreateLinkGenerator(endpoint); + var linkGenerator = CreateLinkGenerator(endpoint); - // Act - var path = linkGenerator.GetPathByAddress(0, values: null); + // Act + var path = linkGenerator.GetPathByAddress(0, values: null); - // Assert - Assert.Null(path); - } + // Assert + Assert.Null(path); + } - [Fact] - public void GetPathByAddress_WithHttpContext_NoMatches_ReturnsNull() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + [Fact] + public void GetPathByAddress_WithHttpContext_NoMatches_ReturnsNull() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - var linkGenerator = CreateLinkGenerator(endpoint); + var linkGenerator = CreateLinkGenerator(endpoint); - // Act - var path = linkGenerator.GetPathByAddress(CreateHttpContext(), 0, values: null); + // Act + var path = linkGenerator.GetPathByAddress(CreateHttpContext(), 0, values: null); - // Assert - Assert.Null(path); - } + // Assert + Assert.Null(path); + } - [Fact] - public void GetUriByAddress_WithoutHttpContext_NoMatches_ReturnsNull() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + [Fact] + public void GetUriByAddress_WithoutHttpContext_NoMatches_ReturnsNull() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - var linkGenerator = CreateLinkGenerator(endpoint); + var linkGenerator = CreateLinkGenerator(endpoint); - // Act - var uri = linkGenerator.GetUriByAddress(0, values: null, "http", new HostString("example.com")); + // Act + var uri = linkGenerator.GetUriByAddress(0, values: null, "http", new HostString("example.com")); - // Assert - Assert.Null(uri); - } + // Assert + Assert.Null(uri); + } - [Fact] - public void GetUriByAddress_WithHttpContext_NoMatches_ReturnsNull() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + [Fact] + public void GetUriByAddress_WithHttpContext_NoMatches_ReturnsNull() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - var linkGenerator = CreateLinkGenerator(endpoint); + var linkGenerator = CreateLinkGenerator(endpoint); - // Act - var uri = linkGenerator.GetUriByAddress(CreateHttpContext(), 0, values: null); + // Act + var uri = linkGenerator.GetUriByAddress(CreateHttpContext(), 0, values: null); - // Assert - Assert.Null(uri); - } + // Assert + Assert.Null(uri); + } - [Fact] - public void GetPathByAddress_WithoutHttpContext_HasMatches_ReturnsFirstSuccessfulTemplateResult() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + [Fact] + public void GetPathByAddress_WithoutHttpContext_HasMatches_ReturnsFirstSuccessfulTemplateResult() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - // Act - var path = linkGenerator.GetPathByAddress(1, values: new RouteValueDictionary(new { controller = "Home", action = "Index", })); + // Act + var path = linkGenerator.GetPathByAddress(1, values: new RouteValueDictionary(new { controller = "Home", action = "Index", })); - // Assert - Assert.Equal("/Home/Index", path); - } + // Assert + Assert.Equal("/Home/Index", path); + } - [Fact] - public void GetPathByAddress_WithHttpContext_HasMatches_ReturnsFirstSuccessfulTemplateResult() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + [Fact] + public void GetPathByAddress_WithHttpContext_HasMatches_ReturnsFirstSuccessfulTemplateResult() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - // Act - var path = linkGenerator.GetPathByAddress(CreateHttpContext(), 1, values: new RouteValueDictionary(new { controller = "Home", action = "Index", })); + // Act + var path = linkGenerator.GetPathByAddress(CreateHttpContext(), 1, values: new RouteValueDictionary(new { controller = "Home", action = "Index", })); - // Assert - Assert.Equal("/Home/Index", path); - } + // Assert + Assert.Equal("/Home/Index", path); + } - [Fact] - public void GetUriByAddress_WithoutHttpContext_HasMatches_ReturnsFirstSuccessfulTemplateResult() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + [Fact] + public void GetUriByAddress_WithoutHttpContext_HasMatches_ReturnsFirstSuccessfulTemplateResult() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - // Act - var path = linkGenerator.GetUriByAddress( - 1, - values: new RouteValueDictionary(new { controller = "Home", action = "Index", }), - "http", - new HostString("example.com")); + // Act + var path = linkGenerator.GetUriByAddress( + 1, + values: new RouteValueDictionary(new { controller = "Home", action = "Index", }), + "http", + new HostString("example.com")); - // Assert - Assert.Equal("http://example.com/Home/Index", path); - } + // Assert + Assert.Equal("http://example.com/Home/Index", path); + } - [Fact] - public void GetUriByAddress_WithHttpContext_HasMatches_ReturnsFirstSuccessfulTemplateResult() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + [Fact] + public void GetUriByAddress_WithHttpContext_HasMatches_ReturnsFirstSuccessfulTemplateResult() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - var httpContext = CreateHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("example.com"); + var httpContext = CreateHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("example.com"); - // Act - var uri = linkGenerator.GetUriByAddress(httpContext, 1, values: new RouteValueDictionary(new { controller = "Home", action = "Index", })); + // Act + var uri = linkGenerator.GetUriByAddress(httpContext, 1, values: new RouteValueDictionary(new { controller = "Home", action = "Index", })); - // Assert - Assert.Equal("http://example.com/Home/Index", uri); - } + // Assert + Assert.Equal("http://example.com/Home/Index", uri); + } - [Fact] - public void GetPathByAddress_WithoutHttpContext_WithLinkOptions() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + [Fact] + public void GetPathByAddress_WithoutHttpContext_WithLinkOptions() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - // Act - var path = linkGenerator.GetPathByAddress( - 1, - values: new RouteValueDictionary(new { controller = "Home", action = "Index", }), - options: new LinkOptions() { AppendTrailingSlash = true, }); + // Act + var path = linkGenerator.GetPathByAddress( + 1, + values: new RouteValueDictionary(new { controller = "Home", action = "Index", }), + options: new LinkOptions() { AppendTrailingSlash = true, }); - // Assert - Assert.Equal("/Home/Index/", path); - } + // Assert + Assert.Equal("/Home/Index/", path); + } - [Fact] - public void GetPathByAddress_WithParameterTransformer() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + [Fact] + public void GetPathByAddress_WithParameterTransformer() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - Action configureServices = s => + Action configureServices = s => + { + s.Configure(o => { - s.Configure(o => - { - o.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); - }); - }; + o.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); + }); + }; - var linkGenerator = CreateLinkGenerator(configureServices, endpoint1, endpoint2); + var linkGenerator = CreateLinkGenerator(configureServices, endpoint1, endpoint2); - // Act - var path = linkGenerator.GetPathByAddress( - 1, - values: new RouteValueDictionary(new { controller = "TestController", action = "Index", })); + // Act + var path = linkGenerator.GetPathByAddress( + 1, + values: new RouteValueDictionary(new { controller = "TestController", action = "Index", })); - // Assert - Assert.Equal("/test-controller/Index", path); - } + // Assert + Assert.Equal("/test-controller/Index", path); + } - [Fact] - public void GetPathByAddress_WithParameterTransformer_WithLowercaseUrl() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + [Fact] + public void GetPathByAddress_WithParameterTransformer_WithLowercaseUrl() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller:slugify}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - Action configureServices = s => + Action configureServices = s => + { + s.Configure(o => { - s.Configure(o => - { - o.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); - }); - }; - - var linkGenerator = CreateLinkGenerator(configureServices, endpoint1, endpoint2); - - // Act - var path = linkGenerator.GetPathByAddress( - 1, - values: new RouteValueDictionary(new { controller = "TestController", action = "Index", }), - options: new LinkOptions() { LowercaseUrls = true, }); - - // Assert - Assert.Equal("/test-controller/index", path); - } + o.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); + }); + }; - [Fact] - public void GetPathByAddress_WithHttpContext_WithLinkOptions() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + var linkGenerator = CreateLinkGenerator(configureServices, endpoint1, endpoint2); - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + // Act + var path = linkGenerator.GetPathByAddress( + 1, + values: new RouteValueDictionary(new { controller = "TestController", action = "Index", }), + options: new LinkOptions() { LowercaseUrls = true, }); - // Act - var path = linkGenerator.GetPathByAddress( - CreateHttpContext(), - 1, - values: new RouteValueDictionary(new { controller = "Home", action = "Index", }), - options: new LinkOptions() { AppendTrailingSlash = true, }); + // Assert + Assert.Equal("/test-controller/index", path); + } - // Assert - Assert.Equal("/Home/Index/", path); - } + [Fact] + public void GetPathByAddress_WithHttpContext_WithLinkOptions() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - [Fact] - public void GetUriByAddress_WithoutHttpContext_WithLinkOptions() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - - // Act - var path = linkGenerator.GetUriByAddress( - 1, - values: new RouteValueDictionary(new { controller = "Home", action = "Index", }), - "http", - new HostString("example.com"), - options: new LinkOptions() { AppendTrailingSlash = true, }); - - // Assert - Assert.Equal("http://example.com/Home/Index/", path); - } + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - [Fact] - public void GetUriByAddress_WithHttpContext_WithLinkOptions() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + // Act + var path = linkGenerator.GetPathByAddress( + CreateHttpContext(), + 1, + values: new RouteValueDictionary(new { controller = "Home", action = "Index", }), + options: new LinkOptions() { AppendTrailingSlash = true, }); - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + // Assert + Assert.Equal("/Home/Index/", path); + } - var httpContext = CreateHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("example.com"); + [Fact] + public void GetUriByAddress_WithoutHttpContext_WithLinkOptions() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + // Act + var path = linkGenerator.GetUriByAddress( + 1, + values: new RouteValueDictionary(new { controller = "Home", action = "Index", }), + "http", + new HostString("example.com"), + options: new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("http://example.com/Home/Index/", path); + } - // Act - var uri = linkGenerator.GetUriByAddress( - httpContext, - 1, - values: new RouteValueDictionary(new { controller = "Home", action = "Index", }), - options: new LinkOptions() { AppendTrailingSlash = true, }); + [Fact] + public void GetUriByAddress_WithHttpContext_WithLinkOptions() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - // Assert - Assert.Equal("http://example.com/Home/Index/", uri); - } + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - // Includes characters that need to be encoded - [Fact] - public void GetPathByAddress_WithoutHttpContext_WithPathBaseAndFragment() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + var httpContext = CreateHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("example.com"); - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + // Act + var uri = linkGenerator.GetUriByAddress( + httpContext, + 1, + values: new RouteValueDictionary(new { controller = "Home", action = "Index", }), + options: new LinkOptions() { AppendTrailingSlash = true, }); - // Act - var path = linkGenerator.GetPathByAddress( - 1, - values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }), - new PathString("/Foo/Bar?encodeme?"), - new FragmentString("#Fragment?")); + // Assert + Assert.Equal("http://example.com/Home/Index/", uri); + } - // Assert - Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex?query=some%3Fquery#Fragment?", path); - } + // Includes characters that need to be encoded + [Fact] + public void GetPathByAddress_WithoutHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - [Fact] - public void GetLink_ParameterTransformer() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller:upper-case}/{name}", requiredValues: new { controller = "Home", name = "Test" }); + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + // Act + var path = linkGenerator.GetPathByAddress( + 1, + values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }), + new PathString("/Foo/Bar?encodeme?"), + new FragmentString("#Fragment?")); + + // Assert + Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex?query=some%3Fquery#Fragment?", path); + } + + [Fact] + public void GetLink_ParameterTransformer() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller:upper-case}/{name}", requiredValues: new { controller = "Home", name = "Test" }); - Action configure = (s) => + Action configure = (s) => + { + s.Configure(o => { - s.Configure(o => - { - o.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); - }); - }; + o.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); + }); + }; - var linkGenerator = CreateLinkGenerator(configure, endpoint); + var linkGenerator = CreateLinkGenerator(configure, endpoint); - // Act - var link = linkGenerator.GetPathByRouteValues(routeName: null, new { controller = "Home", name = "Test" }); + // Act + var link = linkGenerator.GetPathByRouteValues(routeName: null, new { controller = "Home", name = "Test" }); - // Assert - Assert.Equal("/HOME/Test", link); - } + // Assert + Assert.Equal("/HOME/Test", link); + } - [Fact] - public void GetLink_ParameterTransformer_ForQueryString() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "{controller:upper-case}/{name}", - requiredValues: new { controller = "Home", name = "Test", }, - policies: new { c = new UpperCaseParameterTransform(), }); + [Fact] + public void GetLink_ParameterTransformer_ForQueryString() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "{controller:upper-case}/{name}", + requiredValues: new { controller = "Home", name = "Test", }, + policies: new { c = new UpperCaseParameterTransform(), }); - Action configure = (s) => + Action configure = (s) => + { + s.Configure(o => { - s.Configure(o => - { - o.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); - }); - }; + o.ConstraintMap["upper-case"] = typeof(UpperCaseParameterTransform); + }); + }; - var linkGenerator = CreateLinkGenerator(configure, endpoint); + var linkGenerator = CreateLinkGenerator(configure, endpoint); - // Act - var link = linkGenerator.GetPathByRouteValues(routeName: null, new { controller = "Home", name = "Test", c = "hithere", }); + // Act + var link = linkGenerator.GetPathByRouteValues(routeName: null, new { controller = "Home", name = "Test", c = "hithere", }); - // Assert - Assert.Equal("/HOME/Test?c=HITHERE", link); - } + // Assert + Assert.Equal("/HOME/Test?c=HITHERE", link); + } - // Includes characters that need to be encoded - [Fact] - public void GetPathByAddress_WithHttpContext_WithPathBaseAndFragment() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + // Includes characters that need to be encoded + [Fact] + public void GetPathByAddress_WithHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - var httpContext = CreateHttpContext(); - httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); + var httpContext = CreateHttpContext(); + httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); - // Act - var path = linkGenerator.GetPathByAddress( - httpContext, - 1, - values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }), - fragment: new FragmentString("#Fragment?")); + // Act + var path = linkGenerator.GetPathByAddress( + httpContext, + 1, + values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }), + fragment: new FragmentString("#Fragment?")); - // Assert - Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex?query=some%3Fquery#Fragment?", path); - } + // Assert + Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex?query=some%3Fquery#Fragment?", path); + } - // Includes characters that need to be encoded - [Fact] - public void GetUriByAddress_WithoutHttpContext_WithPathBaseAndFragment() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - - // Act - var path = linkGenerator.GetUriByAddress( - 1, - values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }), - "http", - new HostString("example.com"), - new PathString("/Foo/Bar?encodeme?"), - new FragmentString("#Fragment?")); - - // Assert - Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex?query=some%3Fquery#Fragment?", path); - } + // Includes characters that need to be encoded + [Fact] + public void GetUriByAddress_WithoutHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + // Act + var path = linkGenerator.GetUriByAddress( + 1, + values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }), + "http", + new HostString("example.com"), + new PathString("/Foo/Bar?encodeme?"), + new FragmentString("#Fragment?")); + + // Assert + Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex?query=some%3Fquery#Fragment?", path); + } - // Includes characters that need to be encoded - [Fact] - public void GetUriByAddress_WithHttpContext_WithPathBaseAndFragment() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - - var httpContext = CreateHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("example.com"); - httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); - - // Act - var uri = linkGenerator.GetUriByAddress( - httpContext, - 1, - values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }), - fragment: new FragmentString("#Fragment?")); - - // Assert - Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex?query=some%3Fquery#Fragment?", uri); - } + // Includes characters that need to be encoded + [Fact] + public void GetUriByAddress_WithHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var httpContext = CreateHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("example.com"); + httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); + + // Act + var uri = linkGenerator.GetUriByAddress( + httpContext, + 1, + values: new RouteValueDictionary(new { controller = "Home", action = "In?dex", query = "some?query" }), + fragment: new FragmentString("#Fragment?")); + + // Assert + Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/In%3Fdex?query=some%3Fquery#Fragment?", uri); + } - [Fact] - public void GetPathByAddress_WithHttpContext_IncludesAmbientValues() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + [Fact] + public void GetPathByAddress_WithHttpContext_IncludesAmbientValues() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - var httpContext = CreateHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("example.com"); + var httpContext = CreateHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("example.com"); - // Act - var uri = linkGenerator.GetPathByAddress( - httpContext, - 1, - values: new RouteValueDictionary(new { action = "Index", }), - ambientValues: new RouteValueDictionary(new { controller = "Home", })); + // Act + var uri = linkGenerator.GetPathByAddress( + httpContext, + 1, + values: new RouteValueDictionary(new { action = "Index", }), + ambientValues: new RouteValueDictionary(new { controller = "Home", })); - // Assert - Assert.Equal("/Home/Index", uri); - } + // Assert + Assert.Equal("/Home/Index", uri); + } - [Fact] - public void GetUriByAddress_WithHttpContext_IncludesAmbientValues() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + [Fact] + public void GetUriByAddress_WithHttpContext_IncludesAmbientValues() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - var httpContext = CreateHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("example.com"); + var httpContext = CreateHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("example.com"); - // Act - var uri = linkGenerator.GetUriByAddress( - httpContext, - 1, - values: new RouteValueDictionary(new { action = "Index", }), - ambientValues: new RouteValueDictionary(new { controller = "Home", })); + // Act + var uri = linkGenerator.GetUriByAddress( + httpContext, + 1, + values: new RouteValueDictionary(new { action = "Index", }), + ambientValues: new RouteValueDictionary(new { controller = "Home", })); - // Assert - Assert.Equal("http://example.com/Home/Index", uri); - } + // Assert + Assert.Equal("http://example.com/Home/Index", uri); + } - [Fact] - public void GetPathByAddress_WithHttpContext_CanOverrideUriParts() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + [Fact] + public void GetPathByAddress_WithHttpContext_CanOverrideUriParts() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - var httpContext = CreateHttpContext(); - httpContext.Request.PathBase = "/Foo"; + var httpContext = CreateHttpContext(); + httpContext.Request.PathBase = "/Foo"; - // Act - var uri = linkGenerator.GetPathByAddress( - httpContext, - 1, - values: new RouteValueDictionary(new { action = "Index", controller = "Home", }), - pathBase: "/"); + // Act + var uri = linkGenerator.GetPathByAddress( + httpContext, + 1, + values: new RouteValueDictionary(new { action = "Index", controller = "Home", }), + pathBase: "/"); - // Assert - Assert.Equal("/Home/Index", uri); - } + // Assert + Assert.Equal("/Home/Index", uri); + } - [Fact] - public void GetUriByAddress_WithHttpContext_CanOverrideUriParts() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); - - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - - var httpContext = CreateHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("example.com"); - httpContext.Request.PathBase = "/Foo"; - - // Act - var uri = linkGenerator.GetUriByAddress( - httpContext, - 1, - values: new RouteValueDictionary(new { action = "Index", controller = "Home", }), - scheme: "ftp", - host: new HostString("example.com:5000"), - pathBase: "/"); - - // Assert - Assert.Equal("ftp://example.com:5000/Home/Index", uri); - } + [Fact] + public void GetUriByAddress_WithHttpContext_CanOverrideUriParts() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new IntMetadata(1), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var httpContext = CreateHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("example.com"); + httpContext.Request.PathBase = "/Foo"; + + // Act + var uri = linkGenerator.GetUriByAddress( + httpContext, + 1, + values: new RouteValueDictionary(new { action = "Index", controller = "Home", }), + scheme: "ftp", + host: new HostString("example.com:5000"), + pathBase: "/"); + + // Assert + Assert.Equal("ftp://example.com:5000/Home/Index", uri); + } - [Fact] - public void GetPathByAddress_WithHttpContext_ContextPassedToConstraint() - { - // Arrange - var constraint = new TestRouteConstraint(); + [Fact] + public void GetPathByAddress_WithHttpContext_ContextPassedToConstraint() + { + // Arrange + var constraint = new TestRouteConstraint(); - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", policies: new { controller = constraint }, metadata: new object[] { new IntMetadata(1), }); + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", policies: new { controller = constraint }, metadata: new object[] { new IntMetadata(1), }); - var linkGenerator = CreateLinkGenerator(endpoint1); + var linkGenerator = CreateLinkGenerator(endpoint1); - var httpContext = CreateHttpContext(); - httpContext.Request.PathBase = "/Foo"; + var httpContext = CreateHttpContext(); + httpContext.Request.PathBase = "/Foo"; - // Act - var uri = linkGenerator.GetPathByAddress( - httpContext, - 1, - values: new RouteValueDictionary(new { action = "Index", controller = "Home", }), - pathBase: "/"); + // Act + var uri = linkGenerator.GetPathByAddress( + httpContext, + 1, + values: new RouteValueDictionary(new { action = "Index", controller = "Home", }), + pathBase: "/"); - // Assert - Assert.Equal("/Home/Index", uri); - Assert.True(constraint.HasHttpContext); - } + // Assert + Assert.Equal("/Home/Index", uri); + Assert.True(constraint.HasHttpContext); + } - private class TestRouteConstraint : IRouteConstraint - { - public bool HasHttpContext { get; set; } + private class TestRouteConstraint : IRouteConstraint + { + public bool HasHttpContext { get; set; } - public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) - { - HasHttpContext = (httpContext != null); - return true; - } + public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) + { + HasHttpContext = (httpContext != null); + return true; } + } - [Fact] - public void GetTemplateBinder_CanCache() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var dataSource = new DynamicEndpointDataSource(endpoint1); + [Fact] + public void GetTemplateBinder_CanCache() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var dataSource = new DynamicEndpointDataSource(endpoint1); - var linkGenerator = CreateLinkGenerator(dataSources: new[] { dataSource }); + var linkGenerator = CreateLinkGenerator(dataSources: new[] { dataSource }); - var expected = linkGenerator.GetTemplateBinder(endpoint1); + var expected = linkGenerator.GetTemplateBinder(endpoint1); - // Act - var actual = linkGenerator.GetTemplateBinder(endpoint1); + // Act + var actual = linkGenerator.GetTemplateBinder(endpoint1); - // Assert - Assert.Same(expected, actual); - } + // Assert + Assert.Same(expected, actual); + } - [Fact] - public void GetTemplateBinder_CanClearCache() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var dataSource = new DynamicEndpointDataSource(endpoint1); + [Fact] + public void GetTemplateBinder_CanClearCache() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var dataSource = new DynamicEndpointDataSource(endpoint1); - var linkGenerator = CreateLinkGenerator(dataSources: new[] { dataSource }); - var original = linkGenerator.GetTemplateBinder(endpoint1); + var linkGenerator = CreateLinkGenerator(dataSources: new[] { dataSource }); + var original = linkGenerator.GetTemplateBinder(endpoint1); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - dataSource.AddEndpoint(endpoint2); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + dataSource.AddEndpoint(endpoint2); - // Act - var actual = linkGenerator.GetTemplateBinder(endpoint1); + // Act + var actual = linkGenerator.GetTemplateBinder(endpoint1); - // Assert - Assert.NotSame(original, actual); - } + // Assert + Assert.NotSame(original, actual); + } - [Theory] - [InlineData(new string[] { }, new string[] { }, "/")] - [InlineData(new string[] { "id" }, new string[] { "3" }, "/Home/Index/3")] - [InlineData(new string[] { "custom" }, new string[] { "Custom" }, "/?custom=Custom")] - public void GetPathByRouteValues_UsesFirstTemplateThatSucceeds(string[] routeNames, string[] routeValues, string expectedPath) - { - // Arrange - var endpointControllerAction = EndpointFactory.CreateRouteEndpoint( - "Home/Index", - order: 3, - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - var endpointController = EndpointFactory.CreateRouteEndpoint( - "Home", - order: 2, - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - var endpointEmpty = EndpointFactory.CreateRouteEndpoint( - "", - order: 1, - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - - // This endpoint should be used to generate the link when an id is present - var endpointControllerActionParameter = EndpointFactory.CreateRouteEndpoint( - "Home/Index/{id}", - order: 0, - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - - var linkGenerator = CreateLinkGenerator(endpointControllerAction, endpointController, endpointEmpty, endpointControllerActionParameter); - - var httpContext = CreateHttpContext(); - httpContext.Request.RouteValues = new RouteValueDictionary(new { controller = "Home", action = "Index" }); - - var values = new RouteValueDictionary(); - for (int i = 0; i < routeNames.Length; i++) - { - values[routeNames[i]] = routeValues[i]; - } + [Theory] + [InlineData(new string[] { }, new string[] { }, "/")] + [InlineData(new string[] { "id" }, new string[] { "3" }, "/Home/Index/3")] + [InlineData(new string[] { "custom" }, new string[] { "Custom" }, "/?custom=Custom")] + public void GetPathByRouteValues_UsesFirstTemplateThatSucceeds(string[] routeNames, string[] routeValues, string expectedPath) + { + // Arrange + var endpointControllerAction = EndpointFactory.CreateRouteEndpoint( + "Home/Index", + order: 3, + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + var endpointController = EndpointFactory.CreateRouteEndpoint( + "Home", + order: 2, + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + var endpointEmpty = EndpointFactory.CreateRouteEndpoint( + "", + order: 1, + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + + // This endpoint should be used to generate the link when an id is present + var endpointControllerActionParameter = EndpointFactory.CreateRouteEndpoint( + "Home/Index/{id}", + order: 0, + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + + var linkGenerator = CreateLinkGenerator(endpointControllerAction, endpointController, endpointEmpty, endpointControllerActionParameter); + + var httpContext = CreateHttpContext(); + httpContext.Request.RouteValues = new RouteValueDictionary(new { controller = "Home", action = "Index" }); + + var values = new RouteValueDictionary(); + for (int i = 0; i < routeNames.Length; i++) + { + values[routeNames[i]] = routeValues[i]; + } + + // Act + var generatedPath = linkGenerator.GetPathByRouteValues( + httpContext, + routeName: null, + values: values); + + // Assert + Assert.Equal(expectedPath, generatedPath); + } - // Act - var generatedPath = linkGenerator.GetPathByRouteValues( - httpContext, - routeName: null, - values: values); + [Theory] + [InlineData(new string[] { }, new string[] { }, "/")] + [InlineData(new string[] { "id" }, new string[] { "3" }, "/Home/Index/3")] + [InlineData(new string[] { "custom" }, new string[] { "Custom" }, "/?custom=Custom")] + [InlineData(new string[] { "controller", "action", "id" }, new string[] { "Home", "Login", "3" }, "/Home/Login/3")] + [InlineData(new string[] { "controller", "action", "id" }, new string[] { "Home", "Fake", "3" }, null)] + public void GetPathByRouteValues_ParameterMatchesRequireValues_HasAmbientValues(string[] routeNames, string[] routeValues, string expectedPath) + { + // Arrange + var homeIndex = EndpointFactory.CreateRouteEndpoint( + "{controller}/{action}/{id?}", + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + var homeLogin = EndpointFactory.CreateRouteEndpoint( + "{controller}/{action}/{id?}", + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Login", }); - // Assert - Assert.Equal(expectedPath, generatedPath); - } + var linkGenerator = CreateLinkGenerator(homeIndex, homeLogin); + + var httpContext = CreateHttpContext(); + httpContext.Request.RouteValues = new RouteValueDictionary(new { controller = "Home", action = "Index", }); - [Theory] - [InlineData(new string[] { }, new string[] { }, "/")] - [InlineData(new string[] { "id" }, new string[] { "3" }, "/Home/Index/3")] - [InlineData(new string[] { "custom" }, new string[] { "Custom" }, "/?custom=Custom")] - [InlineData(new string[] { "controller", "action", "id" }, new string[] { "Home", "Login", "3" }, "/Home/Login/3")] - [InlineData(new string[] { "controller", "action", "id" }, new string[] { "Home", "Fake", "3" }, null)] - public void GetPathByRouteValues_ParameterMatchesRequireValues_HasAmbientValues(string[] routeNames, string[] routeValues, string expectedPath) + var values = new RouteValueDictionary(); + for (int i = 0; i < routeNames.Length; i++) { - // Arrange - var homeIndex = EndpointFactory.CreateRouteEndpoint( - "{controller}/{action}/{id?}", - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - var homeLogin = EndpointFactory.CreateRouteEndpoint( - "{controller}/{action}/{id?}", - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Login", }); - - var linkGenerator = CreateLinkGenerator(homeIndex, homeLogin); - - var httpContext = CreateHttpContext(); - httpContext.Request.RouteValues = new RouteValueDictionary(new { controller = "Home", action = "Index", }); - - var values = new RouteValueDictionary(); - for (int i = 0; i < routeNames.Length; i++) - { - values[routeNames[i]] = routeValues[i]; - } + values[routeNames[i]] = routeValues[i]; + } - // Act - var generatedPath = linkGenerator.GetPathByRouteValues( - httpContext, - routeName: null, - values: values); + // Act + var generatedPath = linkGenerator.GetPathByRouteValues( + httpContext, + routeName: null, + values: values); - // Assert - Assert.Equal(expectedPath, generatedPath); - } + // Assert + Assert.Equal(expectedPath, generatedPath); + } - [Theory] - [InlineData(new string[] { }, new string[] { }, null)] - [InlineData(new string[] { "id" }, new string[] { "3" }, null)] - [InlineData(new string[] { "custom" }, new string[] { "Custom" }, null)] - [InlineData(new string[] { "controller", "action", "id" }, new string[] { "Home", "Login", "3" }, "/Home/Login/3")] - [InlineData(new string[] { "controller", "action", "id" }, new string[] { "Home", "Fake", "3" }, null)] - public void GetPathByRouteValues_ParameterMatchesRequireValues_NoAmbientValues(string[] routeNames, string[] routeValues, string expectedPath) - { - // Arrange - var homeIndex = EndpointFactory.CreateRouteEndpoint( - "{controller}/{action}/{id?}", - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - var homeLogin = EndpointFactory.CreateRouteEndpoint( - "{controller}/{action}/{id?}", - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Login", }); - - var linkGenerator = CreateLinkGenerator(homeIndex, homeLogin); - - var httpContext = CreateHttpContext(); - - var values = new RouteValueDictionary(); - for (int i = 0; i < routeNames.Length; i++) - { - values[routeNames[i]] = routeValues[i]; - } + [Theory] + [InlineData(new string[] { }, new string[] { }, null)] + [InlineData(new string[] { "id" }, new string[] { "3" }, null)] + [InlineData(new string[] { "custom" }, new string[] { "Custom" }, null)] + [InlineData(new string[] { "controller", "action", "id" }, new string[] { "Home", "Login", "3" }, "/Home/Login/3")] + [InlineData(new string[] { "controller", "action", "id" }, new string[] { "Home", "Fake", "3" }, null)] + public void GetPathByRouteValues_ParameterMatchesRequireValues_NoAmbientValues(string[] routeNames, string[] routeValues, string expectedPath) + { + // Arrange + var homeIndex = EndpointFactory.CreateRouteEndpoint( + "{controller}/{action}/{id?}", + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + var homeLogin = EndpointFactory.CreateRouteEndpoint( + "{controller}/{action}/{id?}", + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Login", }); - // Act - var generatedPath = linkGenerator.GetPathByRouteValues( - httpContext, - routeName: null, - values: values); + var linkGenerator = CreateLinkGenerator(homeIndex, homeLogin); - // Assert - Assert.Equal(expectedPath, generatedPath); - } + var httpContext = CreateHttpContext(); - protected override void AddAdditionalServices(IServiceCollection services) + var values = new RouteValueDictionary(); + for (int i = 0; i < routeNames.Length; i++) { - services.AddSingleton, IntAddressScheme>(); + values[routeNames[i]] = routeValues[i]; } - private class IntAddressScheme : IEndpointAddressScheme - { - private readonly EndpointDataSource _dataSource; + // Act + var generatedPath = linkGenerator.GetPathByRouteValues( + httpContext, + routeName: null, + values: values); - public IntAddressScheme(EndpointDataSource dataSource) - { - _dataSource = dataSource; - } + // Assert + Assert.Equal(expectedPath, generatedPath); + } - public IEnumerable FindEndpoints(int address) - { - return _dataSource.Endpoints.Where(e => e.Metadata.GetMetadata().Value == address); - } + protected override void AddAdditionalServices(IServiceCollection services) + { + services.AddSingleton, IntAddressScheme>(); + } + + private class IntAddressScheme : IEndpointAddressScheme + { + private readonly EndpointDataSource _dataSource; + + public IntAddressScheme(EndpointDataSource dataSource) + { + _dataSource = dataSource; } - private class IntMetadata + public IEnumerable FindEndpoints(int address) { - public IntMetadata(int value) - { - Value = value; - } - public int Value { get; } + return _dataSource.Endpoints.Where(e => e.Metadata.GetMetadata().Value == address); + } + } + + private class IntMetadata + { + public IntMetadata(int value) + { + Value = value; } + public int Value { get; } } } diff --git a/src/Http/Routing/test/UnitTests/DefaultLinkParserTest.cs b/src/Http/Routing/test/UnitTests/DefaultLinkParserTest.cs index 08ac1eebfb..46a8170876 100644 --- a/src/Http/Routing/test/UnitTests/DefaultLinkParserTest.cs +++ b/src/Http/Routing/test/UnitTests/DefaultLinkParserTest.cs @@ -13,180 +13,179 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +// Tests LinkParser functionality using ParsePathByAddress - see tests for the extension +// methods for more E2E tests. +// +// Does not cover template processing in detail, those scenarios are validated by other tests. +public class DefaultLinkParserTest : LinkParserTestBase { - // Tests LinkParser functionality using ParsePathByAddress - see tests for the extension - // methods for more E2E tests. - // - // Does not cover template processing in detail, those scenarios are validated by other tests. - public class DefaultLinkParserTest : LinkParserTestBase + [Fact] + public void ParsePathByAddresss_NoMatchingEndpoint_ReturnsNull() { - [Fact] - public void ParsePathByAddresss_NoMatchingEndpoint_ReturnsNull() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", displayName: "Test1", metadata: new object[] { new IntMetadata(1), }); + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", displayName: "Test1", metadata: new object[] { new IntMetadata(1), }); - var sink = new TestSink(); - var loggerFactory = new TestLoggerFactory(sink, enabled: true); - var parser = CreateLinkParser(services => { services.AddSingleton(loggerFactory); }, endpoint); + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var parser = CreateLinkParser(services => { services.AddSingleton(loggerFactory); }, endpoint); - // Act - var values = parser.ParsePathByAddress(0, "/Home/Index/17"); + // Act + var values = parser.ParsePathByAddress(0, "/Home/Index/17"); - // Assert - Assert.Null(values); + // Assert + Assert.Null(values); - Assert.Collection( - sink.Writes, - w => Assert.Equal("No endpoints found for address 0", w.Message)); - } + Assert.Collection( + sink.Writes, + w => Assert.Equal("No endpoints found for address 0", w.Message)); + } - [Fact] - public void ParsePathByAddresss_HasMatches_ReturnsNullWhenParsingFails() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", displayName: "Test1", metadata: new object[] { new IntMetadata(1), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id2}", displayName: "Test2", metadata: new object[] { new IntMetadata(0), }); + [Fact] + public void ParsePathByAddresss_HasMatches_ReturnsNullWhenParsingFails() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", displayName: "Test1", metadata: new object[] { new IntMetadata(1), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id2}", displayName: "Test2", metadata: new object[] { new IntMetadata(0), }); - var sink = new TestSink(); - var loggerFactory = new TestLoggerFactory(sink, enabled: true); - var parser = CreateLinkParser(services => { services.AddSingleton(loggerFactory); }, endpoint1, endpoint2); + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var parser = CreateLinkParser(services => { services.AddSingleton(loggerFactory); }, endpoint1, endpoint2); - // Act - var values = parser.ParsePathByAddress(0, "/"); + // Act + var values = parser.ParsePathByAddress(0, "/"); - // Assert - Assert.Null(values); + // Assert + Assert.Null(values); - Assert.Collection( - sink.Writes, - w => Assert.Equal("Found the endpoints Test2 for address 0", w.Message), - w => Assert.Equal("Path parsing failed for endpoints Test2 and URI path /", w.Message)); - } + Assert.Collection( + sink.Writes, + w => Assert.Equal("Found the endpoints Test2 for address 0", w.Message), + w => Assert.Equal("Path parsing failed for endpoints Test2 and URI path /", w.Message)); + } - [Fact] - public void ParsePathByAddresss_HasMatches_ReturnsFirstSuccessfulParse() - { - // Arrange - var endpoint0 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}", displayName: "Test1",metadata: new object[] { new IntMetadata(0), }); - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", displayName: "Test2", metadata: new object[] { new IntMetadata(0), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id2}", displayName: "Test3", metadata: new object[] { new IntMetadata(0), }); + [Fact] + public void ParsePathByAddresss_HasMatches_ReturnsFirstSuccessfulParse() + { + // Arrange + var endpoint0 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}", displayName: "Test1", metadata: new object[] { new IntMetadata(0), }); + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", displayName: "Test2", metadata: new object[] { new IntMetadata(0), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id2}", displayName: "Test3", metadata: new object[] { new IntMetadata(0), }); - var sink = new TestSink(); - var loggerFactory = new TestLoggerFactory(sink, enabled: true); - var parser = CreateLinkParser(services => { services.AddSingleton(loggerFactory); }, endpoint0, endpoint1, endpoint2); + var sink = new TestSink(); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var parser = CreateLinkParser(services => { services.AddSingleton(loggerFactory); }, endpoint0, endpoint1, endpoint2); - // Act - var values = parser.ParsePathByAddress(0, "/Home/Index/17"); + // Act + var values = parser.ParsePathByAddress(0, "/Home/Index/17"); - // Assert - MatcherAssert.AssertRouteValuesEqual(new { controller= "Home", action = "Index", id = "17" }, values); + // Assert + MatcherAssert.AssertRouteValuesEqual(new { controller = "Home", action = "Index", id = "17" }, values); - Assert.Collection( - sink.Writes, - w => Assert.Equal("Found the endpoints Test1, Test2, Test3 for address 0", w.Message), - w => Assert.Equal("Path parsing succeeded for endpoint Test2 and URI path /Home/Index/17", w.Message)); - } + Assert.Collection( + sink.Writes, + w => Assert.Equal("Found the endpoints Test1, Test2, Test3 for address 0", w.Message), + w => Assert.Equal("Path parsing succeeded for endpoint Test2 and URI path /Home/Index/17", w.Message)); + } - [Fact] - public void ParsePathByAddresss_HasMatches_IncludesDefaults() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller=Home}/{action=Index}/{id?}", metadata: new object[] { new IntMetadata(0), }); + [Fact] + public void ParsePathByAddresss_HasMatches_IncludesDefaults() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller=Home}/{action=Index}/{id?}", metadata: new object[] { new IntMetadata(0), }); - var parser = CreateLinkParser(endpoint); + var parser = CreateLinkParser(endpoint); - // Act - var values = parser.ParsePathByAddress(0, "/"); + // Act + var values = parser.ParsePathByAddress(0, "/"); - // Assert - MatcherAssert.AssertRouteValuesEqual(new { controller = "Home", action = "Index", }, values); - } + // Assert + MatcherAssert.AssertRouteValuesEqual(new { controller = "Home", action = "Index", }, values); + } - [Fact] - public void ParsePathByAddresss_HasMatches_RunsConstraints() - { - // Arrange - var endpoint0 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id:int}", metadata: new object[] { new IntMetadata(0), }); - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id2:alpha}", metadata: new object[] { new IntMetadata(0), }); + [Fact] + public void ParsePathByAddresss_HasMatches_RunsConstraints() + { + // Arrange + var endpoint0 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id:int}", metadata: new object[] { new IntMetadata(0), }); + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id2:alpha}", metadata: new object[] { new IntMetadata(0), }); - var parser = CreateLinkParser(endpoint0, endpoint1); + var parser = CreateLinkParser(endpoint0, endpoint1); - // Act - var values = parser.ParsePathByAddress(0, "/Home/Index/abc"); + // Act + var values = parser.ParsePathByAddress(0, "/Home/Index/abc"); - // Assert - MatcherAssert.AssertRouteValuesEqual(new { controller = "Home", action = "Index", id2 = "abc" }, values); - } + // Assert + MatcherAssert.AssertRouteValuesEqual(new { controller = "Home", action = "Index", id2 = "abc" }, values); + } - [Fact] - public void GetRoutePatternMatcher_CanCache() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var dataSource = new DynamicEndpointDataSource(endpoint1); + [Fact] + public void GetRoutePatternMatcher_CanCache() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var dataSource = new DynamicEndpointDataSource(endpoint1); - var parser = CreateLinkParser(dataSources: new[] { dataSource }); + var parser = CreateLinkParser(dataSources: new[] { dataSource }); - var expected = parser.GetMatcherState(endpoint1); + var expected = parser.GetMatcherState(endpoint1); - // Act - var actual = parser.GetMatcherState(endpoint1); + // Act + var actual = parser.GetMatcherState(endpoint1); - // Assert - Assert.Same(expected.Matcher, actual.Matcher); - Assert.Same(expected.Constraints, actual.Constraints); - } + // Assert + Assert.Same(expected.Matcher, actual.Matcher); + Assert.Same(expected.Constraints, actual.Constraints); + } - [Fact] - public void GetRoutePatternMatcherr_CanClearCache() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - var dataSource = new DynamicEndpointDataSource(endpoint1); + [Fact] + public void GetRoutePatternMatcherr_CanClearCache() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + var dataSource = new DynamicEndpointDataSource(endpoint1); - var parser = CreateLinkParser(dataSources: new[] { dataSource }); - var original = parser.GetMatcherState(endpoint1); + var parser = CreateLinkParser(dataSources: new[] { dataSource }); + var original = parser.GetMatcherState(endpoint1); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); - dataSource.AddEndpoint(endpoint2); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new IntMetadata(1), }); + dataSource.AddEndpoint(endpoint2); - // Act - var actual = parser.GetMatcherState(endpoint1); + // Act + var actual = parser.GetMatcherState(endpoint1); - // Assert - Assert.NotSame(original.Matcher, actual.Matcher); - Assert.NotSame(original.Constraints, actual.Constraints); - } + // Assert + Assert.NotSame(original.Matcher, actual.Matcher); + Assert.NotSame(original.Constraints, actual.Constraints); + } - protected override void AddAdditionalServices(IServiceCollection services) + protected override void AddAdditionalServices(IServiceCollection services) + { + services.AddSingleton, IntAddressScheme>(); + } + + private class IntAddressScheme : IEndpointAddressScheme + { + private readonly EndpointDataSource _dataSource; + + public IntAddressScheme(EndpointDataSource dataSource) { - services.AddSingleton, IntAddressScheme>(); + _dataSource = dataSource; } - private class IntAddressScheme : IEndpointAddressScheme + public IEnumerable FindEndpoints(int address) { - private readonly EndpointDataSource _dataSource; - - public IntAddressScheme(EndpointDataSource dataSource) - { - _dataSource = dataSource; - } - - public IEnumerable FindEndpoints(int address) - { - return _dataSource.Endpoints.Where(e => e.Metadata.GetMetadata().Value == address); - } + return _dataSource.Endpoints.Where(e => e.Metadata.GetMetadata().Value == address); } + } - private class IntMetadata + private class IntMetadata + { + public IntMetadata(int value) { - public IntMetadata(int value) - { - Value = value; - } - public int Value { get; } + Value = value; } + public int Value { get; } } } diff --git a/src/Http/Routing/test/UnitTests/DefaultParameterPolicyFactoryTest.cs b/src/Http/Routing/test/UnitTests/DefaultParameterPolicyFactoryTest.cs index 9cf2f94ac0..89e1abf4e4 100644 --- a/src/Http/Routing/test/UnitTests/DefaultParameterPolicyFactoryTest.cs +++ b/src/Http/Routing/test/UnitTests/DefaultParameterPolicyFactoryTest.cs @@ -10,524 +10,523 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class DefaultParameterPolicyFactoryTest { - public class DefaultParameterPolicyFactoryTest + [Fact] + public void Create_ThrowsException_IfNoConstraintOrParameterPolicy_FoundInMap() { - [Fact] - public void Create_ThrowsException_IfNoConstraintOrParameterPolicy_FoundInMap() - { - // Arrange - var factory = GetParameterPolicyFactory(); - - // Act - var exception = Assert.Throws( - () => factory.Create(RoutePatternFactory.ParameterPart("id", @default: null, RoutePatternParameterKind.Optional), @"notpresent(\d+)")); - - // Assert - Assert.Equal( - "The constraint reference 'notpresent' could not be resolved to a type. " + - $"Register the constraint type with '{typeof(RouteOptions)}.{nameof(RouteOptions.ConstraintMap)}'.", - exception.Message); - } + // Arrange + var factory = GetParameterPolicyFactory(); + + // Act + var exception = Assert.Throws( + () => factory.Create(RoutePatternFactory.ParameterPart("id", @default: null, RoutePatternParameterKind.Optional), @"notpresent(\d+)")); + + // Assert + Assert.Equal( + "The constraint reference 'notpresent' could not be resolved to a type. " + + $"Register the constraint type with '{typeof(RouteOptions)}.{nameof(RouteOptions.ConstraintMap)}'.", + exception.Message); + } - [Fact] - public void Create_ThrowsException_OnInvalidType() - { - // Arrange - var options = new RouteOptions(); - options.ConstraintMap.Add("bad", typeof(string)); + [Fact] + public void Create_ThrowsException_OnInvalidType() + { + // Arrange + var options = new RouteOptions(); + options.ConstraintMap.Add("bad", typeof(string)); - var services = new ServiceCollection(); + var services = new ServiceCollection(); - var factory = GetParameterPolicyFactory(options, services); + var factory = GetParameterPolicyFactory(options, services); - // Act - var exception = Assert.Throws( - () => factory.Create(RoutePatternFactory.ParameterPart("id"), @"bad")); + // Act + var exception = Assert.Throws( + () => factory.Create(RoutePatternFactory.ParameterPart("id"), @"bad")); - // Assert - Assert.Equal( - $"The constraint type '{typeof(string)}' which is mapped to constraint key 'bad' must implement the '{nameof(IParameterPolicy)}' interface.", - exception.Message); - } + // Assert + Assert.Equal( + $"The constraint type '{typeof(string)}' which is mapped to constraint key 'bad' must implement the '{nameof(IParameterPolicy)}' interface.", + exception.Message); + } - [Fact] - public void Create_CreatesParameterPolicy_FromRoutePattern_String() - { - // Arrange - var factory = GetParameterPolicyFactory(); + [Fact] + public void Create_CreatesParameterPolicy_FromRoutePattern_String() + { + // Arrange + var factory = GetParameterPolicyFactory(); - var parameter = RoutePatternFactory.ParameterPart( - "id", - @default: null, - parameterKind: RoutePatternParameterKind.Standard, - parameterPolicies: new[] { RoutePatternFactory.Constraint("int"), }); + var parameter = RoutePatternFactory.ParameterPart( + "id", + @default: null, + parameterKind: RoutePatternParameterKind.Standard, + parameterPolicies: new[] { RoutePatternFactory.Constraint("int"), }); - // Act - var parameterPolicy = factory.Create(parameter, parameter.ParameterPolicies[0]); + // Act + var parameterPolicy = factory.Create(parameter, parameter.ParameterPolicies[0]); - // Assert - Assert.IsType(parameterPolicy); - } + // Assert + Assert.IsType(parameterPolicy); + } - [Fact] - public void Create_CreatesParameterPolicy_FromRoutePattern_String_Optional() - { - // Arrange - var factory = GetParameterPolicyFactory(); + [Fact] + public void Create_CreatesParameterPolicy_FromRoutePattern_String_Optional() + { + // Arrange + var factory = GetParameterPolicyFactory(); - var parameter = RoutePatternFactory.ParameterPart( - "id", - @default: null, - parameterKind: RoutePatternParameterKind.Optional, - parameterPolicies: new[] { RoutePatternFactory.Constraint("int"), }); + var parameter = RoutePatternFactory.ParameterPart( + "id", + @default: null, + parameterKind: RoutePatternParameterKind.Optional, + parameterPolicies: new[] { RoutePatternFactory.Constraint("int"), }); - // Act - var parameterPolicy = factory.Create(parameter, parameter.ParameterPolicies[0]); + // Act + var parameterPolicy = factory.Create(parameter, parameter.ParameterPolicies[0]); - // Assert - var optionalConstraint = Assert.IsType(parameterPolicy); - Assert.IsType(optionalConstraint.InnerConstraint); - } + // Assert + var optionalConstraint = Assert.IsType(parameterPolicy); + Assert.IsType(optionalConstraint.InnerConstraint); + } - [Fact] - public void Create_CreatesParameterPolicy_FromRoutePattern_Constraint() - { - // Arrange - var factory = GetParameterPolicyFactory(); + [Fact] + public void Create_CreatesParameterPolicy_FromRoutePattern_Constraint() + { + // Arrange + var factory = GetParameterPolicyFactory(); - var parameter = RoutePatternFactory.ParameterPart( - "id", - @default: null, - parameterKind: RoutePatternParameterKind.Standard, - parameterPolicies: new[] { RoutePatternFactory.ParameterPolicy(new IntRouteConstraint()), }); + var parameter = RoutePatternFactory.ParameterPart( + "id", + @default: null, + parameterKind: RoutePatternParameterKind.Standard, + parameterPolicies: new[] { RoutePatternFactory.ParameterPolicy(new IntRouteConstraint()), }); - // Act - var parameterPolicy = factory.Create(parameter, parameter.ParameterPolicies[0]); + // Act + var parameterPolicy = factory.Create(parameter, parameter.ParameterPolicies[0]); - // Assert - Assert.IsType(parameterPolicy); - } + // Assert + Assert.IsType(parameterPolicy); + } - [Fact] - public void Create_CreatesParameterPolicy_FromRoutePattern_Constraint_Optional() - { - // Arrange - var factory = GetParameterPolicyFactory(); + [Fact] + public void Create_CreatesParameterPolicy_FromRoutePattern_Constraint_Optional() + { + // Arrange + var factory = GetParameterPolicyFactory(); - var parameter = RoutePatternFactory.ParameterPart( - "id", - @default: null, - parameterKind: RoutePatternParameterKind.Optional, - parameterPolicies: new[] { RoutePatternFactory.ParameterPolicy(new IntRouteConstraint()), }); + var parameter = RoutePatternFactory.ParameterPart( + "id", + @default: null, + parameterKind: RoutePatternParameterKind.Optional, + parameterPolicies: new[] { RoutePatternFactory.ParameterPolicy(new IntRouteConstraint()), }); - // Act - var parameterPolicy = factory.Create(parameter, parameter.ParameterPolicies[0]); + // Act + var parameterPolicy = factory.Create(parameter, parameter.ParameterPolicies[0]); - // Assert - var optionalConstraint = Assert.IsType(parameterPolicy); - Assert.IsType(optionalConstraint.InnerConstraint); - } + // Assert + var optionalConstraint = Assert.IsType(parameterPolicy); + Assert.IsType(optionalConstraint.InnerConstraint); + } - [Fact] - public void Create_CreatesParameterPolicy_FromRoutePattern_ParameterPolicy() - { - // Arrange - var factory = GetParameterPolicyFactory(); + [Fact] + public void Create_CreatesParameterPolicy_FromRoutePattern_ParameterPolicy() + { + // Arrange + var factory = GetParameterPolicyFactory(); - var parameter = RoutePatternFactory.ParameterPart( - "id", - @default: null, - parameterKind: RoutePatternParameterKind.Standard, - parameterPolicies: new[] { RoutePatternFactory.ParameterPolicy(new CustomParameterPolicy()), }); + var parameter = RoutePatternFactory.ParameterPart( + "id", + @default: null, + parameterKind: RoutePatternParameterKind.Standard, + parameterPolicies: new[] { RoutePatternFactory.ParameterPolicy(new CustomParameterPolicy()), }); - // Act - var parameterPolicy = factory.Create(parameter, parameter.ParameterPolicies[0]); + // Act + var parameterPolicy = factory.Create(parameter, parameter.ParameterPolicies[0]); - // Assert - Assert.IsType(parameterPolicy); - } + // Assert + Assert.IsType(parameterPolicy); + } - [Fact] - public void Create_CreatesParameterPolicy_FromConstraintText_AndRouteConstraint() - { - // Arrange - var factory = GetParameterPolicyFactory(); + [Fact] + public void Create_CreatesParameterPolicy_FromConstraintText_AndRouteConstraint() + { + // Arrange + var factory = GetParameterPolicyFactory(); - // Act - var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id"), "int"); + // Act + var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id"), "int"); - // Assert - Assert.IsType(parameterPolicy); - } + // Assert + Assert.IsType(parameterPolicy); + } - [Fact] - public void Create_CreatesParameterPolicy_FromConstraintText_AndRouteConstraintWithArgument() - { - // Arrange - var factory = GetParameterPolicyFactory(); + [Fact] + public void Create_CreatesParameterPolicy_FromConstraintText_AndRouteConstraintWithArgument() + { + // Arrange + var factory = GetParameterPolicyFactory(); - // Act - var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id"), "range(1,20)"); + // Act + var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id"), "range(1,20)"); - // Assert - var constraint = Assert.IsType(parameterPolicy); - Assert.Equal(1, constraint.Min); - Assert.Equal(20, constraint.Max); - } + // Assert + var constraint = Assert.IsType(parameterPolicy); + Assert.Equal(1, constraint.Min); + Assert.Equal(20, constraint.Max); + } - [Fact] - public void Create_CreatesParameterPolicy_FromConstraintText_AndRouteConstraint_Optional() - { - // Arrange - var factory = GetParameterPolicyFactory(); + [Fact] + public void Create_CreatesParameterPolicy_FromConstraintText_AndRouteConstraint_Optional() + { + // Arrange + var factory = GetParameterPolicyFactory(); - // Act - var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id", @default: null, RoutePatternParameterKind.Optional), "int"); + // Act + var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id", @default: null, RoutePatternParameterKind.Optional), "int"); - // Assert - var optionalConstraint = Assert.IsType(parameterPolicy); - Assert.IsType(optionalConstraint.InnerConstraint); - } + // Assert + var optionalConstraint = Assert.IsType(parameterPolicy); + Assert.IsType(optionalConstraint.InnerConstraint); + } - [Fact] - public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicy() - { - // Arrange - var options = new RouteOptions(); - options.ConstraintMap.Add("customParameterPolicy", typeof(CustomParameterPolicy)); + [Fact] + public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicy() + { + // Arrange + var options = new RouteOptions(); + options.ConstraintMap.Add("customParameterPolicy", typeof(CustomParameterPolicy)); - var services = new ServiceCollection(); - services.AddTransient(); + var services = new ServiceCollection(); + services.AddTransient(); - var factory = GetParameterPolicyFactory(options, services); + var factory = GetParameterPolicyFactory(options, services); - // Act - var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id", @default: null, RoutePatternParameterKind.Optional), "customParameterPolicy"); + // Act + var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id", @default: null, RoutePatternParameterKind.Optional), "customParameterPolicy"); - // Assert - Assert.IsType(parameterPolicy); - } + // Assert + Assert.IsType(parameterPolicy); + } - [Fact] - public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicyWithArgumentAndServices() - { - // Arrange - var options = new RouteOptions(); - options.ConstraintMap.Add("customConstraintPolicy", typeof(CustomParameterPolicyWithArguments)); + [Fact] + public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicyWithArgumentAndServices() + { + // Arrange + var options = new RouteOptions(); + options.ConstraintMap.Add("customConstraintPolicy", typeof(CustomParameterPolicyWithArguments)); - var services = new ServiceCollection(); - services.AddTransient(); + var services = new ServiceCollection(); + services.AddTransient(); - var factory = GetParameterPolicyFactory(options, services); + var factory = GetParameterPolicyFactory(options, services); - // Act - var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id"), "customConstraintPolicy(20)"); + // Act + var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id"), "customConstraintPolicy(20)"); - // Assert - var constraint = Assert.IsType(parameterPolicy); - Assert.Equal(20, constraint.Count); - Assert.NotNull(constraint.TestService); - } + // Assert + var constraint = Assert.IsType(parameterPolicy); + Assert.Equal(20, constraint.Count); + Assert.NotNull(constraint.TestService); + } - [Fact] - public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicyWithArgumentAndMultipleServices() - { - // Arrange - var options = new RouteOptions(); - options.ConstraintMap.Add("customConstraintPolicy", typeof(CustomParameterPolicyWithMultipleArguments)); + [Fact] + public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicyWithArgumentAndMultipleServices() + { + // Arrange + var options = new RouteOptions(); + options.ConstraintMap.Add("customConstraintPolicy", typeof(CustomParameterPolicyWithMultipleArguments)); - var services = new ServiceCollection(); - services.AddTransient(); + var services = new ServiceCollection(); + services.AddTransient(); - var factory = GetParameterPolicyFactory(options, services); + var factory = GetParameterPolicyFactory(options, services); - // Act - var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id"), "customConstraintPolicy(20,-1)"); + // Act + var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id"), "customConstraintPolicy(20,-1)"); - // Assert - var constraint = Assert.IsType(parameterPolicy); - Assert.Equal(20, constraint.First); - Assert.Equal(-1, constraint.Second); - Assert.NotNull(constraint.TestService1); - Assert.NotNull(constraint.TestService2); - } + // Assert + var constraint = Assert.IsType(parameterPolicy); + Assert.Equal(20, constraint.First); + Assert.Equal(-1, constraint.Second); + Assert.NotNull(constraint.TestService1); + Assert.NotNull(constraint.TestService2); + } - [Fact] - public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicyWithOnlyServiceArguments() - { - // Arrange - var options = new RouteOptions(); - options.ConstraintMap.Add("customConstraintPolicy", typeof(CustomParameterPolicyWithOnlyServiceArguments)); + [Fact] + public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicyWithOnlyServiceArguments() + { + // Arrange + var options = new RouteOptions(); + options.ConstraintMap.Add("customConstraintPolicy", typeof(CustomParameterPolicyWithOnlyServiceArguments)); - var services = new ServiceCollection(); - services.AddTransient(); + var services = new ServiceCollection(); + services.AddTransient(); - var factory = GetParameterPolicyFactory(options, services); + var factory = GetParameterPolicyFactory(options, services); - // Act - var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id"), "customConstraintPolicy"); + // Act + var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id"), "customConstraintPolicy"); - // Assert - var constraint = Assert.IsType(parameterPolicy); - Assert.NotNull(constraint.TestService1); - Assert.NotNull(constraint.TestService2); - } + // Assert + var constraint = Assert.IsType(parameterPolicy); + Assert.NotNull(constraint.TestService1); + Assert.NotNull(constraint.TestService2); + } - [Fact] - public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicyWithMultipleMatchingCtors() - { - // Arrange - var options = new RouteOptions(); - options.ConstraintMap.Add("customConstraintPolicy", typeof(CustomParameterPolicyWithMultpleCtors)); + [Fact] + public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicyWithMultipleMatchingCtors() + { + // Arrange + var options = new RouteOptions(); + options.ConstraintMap.Add("customConstraintPolicy", typeof(CustomParameterPolicyWithMultpleCtors)); - var services = new ServiceCollection(); - services.AddTransient(); + var services = new ServiceCollection(); + services.AddTransient(); - var factory = GetParameterPolicyFactory(options, services); + var factory = GetParameterPolicyFactory(options, services); - // Act - var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id"), "customConstraintPolicy(1)"); + // Act + var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id"), "customConstraintPolicy(1)"); - // Assert - var constraint = Assert.IsType(parameterPolicy); - Assert.NotNull(constraint.TestService); - Assert.Equal(1, constraint.Count); - } + // Assert + var constraint = Assert.IsType(parameterPolicy); + Assert.NotNull(constraint.TestService); + Assert.Equal(1, constraint.Count); + } - [Fact] - public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicyWithAmbigiousMatchingCtors() - { - // Arrange - var options = new RouteOptions(); - options.ConstraintMap.Add("customConstraintPolicy", typeof(CustomParameterPolicyWithAmbigiousMultpleCtors)); + [Fact] + public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicyWithAmbigiousMatchingCtors() + { + // Arrange + var options = new RouteOptions(); + options.ConstraintMap.Add("customConstraintPolicy", typeof(CustomParameterPolicyWithAmbigiousMultpleCtors)); - var services = new ServiceCollection(); - services.AddTransient(); + var services = new ServiceCollection(); + services.AddTransient(); - var factory = GetParameterPolicyFactory(options, services); + var factory = GetParameterPolicyFactory(options, services); - // Act - var exception = Assert.Throws( - () => factory.Create(RoutePatternFactory.ParameterPart("id"), "customConstraintPolicy(1)")); + // Act + var exception = Assert.Throws( + () => factory.Create(RoutePatternFactory.ParameterPart("id"), "customConstraintPolicy(1)")); - // Assert - Assert.Equal($"The constructor to use for activating the constraint type '{nameof(CustomParameterPolicyWithAmbigiousMultpleCtors)}' is ambiguous. " - + "Multiple constructors were found with the following number of parameters: 2.", exception.Message); - } + // Assert + Assert.Equal($"The constructor to use for activating the constraint type '{nameof(CustomParameterPolicyWithAmbigiousMultpleCtors)}' is ambiguous. " + + "Multiple constructors were found with the following number of parameters: 2.", exception.Message); + } - [Fact] - public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicyWithSingleArgumentAndServiceArgument() - { - // Arrange - var options = new RouteOptions(); - options.ConstraintMap.Add("regex-service", typeof(RegexInlineRouteConstraintWithService)); + [Fact] + public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicyWithSingleArgumentAndServiceArgument() + { + // Arrange + var options = new RouteOptions(); + options.ConstraintMap.Add("regex-service", typeof(RegexInlineRouteConstraintWithService)); - var services = new ServiceCollection(); - services.AddTransient(); + var services = new ServiceCollection(); + services.AddTransient(); - var factory = GetParameterPolicyFactory(options, services); + var factory = GetParameterPolicyFactory(options, services); - // Act - var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id"), @"regex-service(\\d{1,2})"); + // Act + var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id"), @"regex-service(\\d{1,2})"); - // Assert - var constraint = Assert.IsType(parameterPolicy); - Assert.NotNull(constraint.TestService); - Assert.Equal("\\\\d{1,2}", constraint.Constraint.ToString()); - } + // Assert + var constraint = Assert.IsType(parameterPolicy); + Assert.NotNull(constraint.TestService); + Assert.Equal("\\\\d{1,2}", constraint.Constraint.ToString()); + } - [Fact] - public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicyWithArgumentAndUnresolvedServices_Throw() - { - // Arrange - var options = new RouteOptions(); - options.ConstraintMap.Add("customConstraintPolicy", typeof(CustomParameterPolicyWithArguments)); + [Fact] + public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicyWithArgumentAndUnresolvedServices_Throw() + { + // Arrange + var options = new RouteOptions(); + options.ConstraintMap.Add("customConstraintPolicy", typeof(CustomParameterPolicyWithArguments)); - var services = new ServiceCollection(); + var services = new ServiceCollection(); - var factory = GetParameterPolicyFactory(options, services); + var factory = GetParameterPolicyFactory(options, services); - // Act - var exception = Assert.Throws( - () => factory.Create(RoutePatternFactory.ParameterPart("id"), "customConstraintPolicy(20)")); + // Act + var exception = Assert.Throws( + () => factory.Create(RoutePatternFactory.ParameterPart("id"), "customConstraintPolicy(20)")); - // Assert - var inner = Assert.IsType(exception.InnerException); - Assert.Equal($"No service for type '{typeof(ITestService).FullName}' has been registered.", inner.Message); - } + // Assert + var inner = Assert.IsType(exception.InnerException); + Assert.Equal($"No service for type '{typeof(ITestService).FullName}' has been registered.", inner.Message); + } - [Fact] - public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicy_Optional() - { - // Arrange - var options = new RouteOptions(); - options.ConstraintMap.Add("customParameterPolicy", typeof(CustomParameterPolicy)); + [Fact] + public void Create_CreatesParameterPolicy_FromConstraintText_AndParameterPolicy_Optional() + { + // Arrange + var options = new RouteOptions(); + options.ConstraintMap.Add("customParameterPolicy", typeof(CustomParameterPolicy)); - var services = new ServiceCollection(); - services.AddTransient(); + var services = new ServiceCollection(); + services.AddTransient(); - var factory = GetParameterPolicyFactory(options, services); + var factory = GetParameterPolicyFactory(options, services); - // Act - var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id", @default: null, RoutePatternParameterKind.Optional), "customParameterPolicy"); + // Act + var parameterPolicy = factory.Create(RoutePatternFactory.ParameterPart("id", @default: null, RoutePatternParameterKind.Optional), "customParameterPolicy"); - // Assert - Assert.IsType(parameterPolicy); - } + // Assert + Assert.IsType(parameterPolicy); + } - private DefaultParameterPolicyFactory GetParameterPolicyFactory( - RouteOptions options = null, - ServiceCollection services = null) + private DefaultParameterPolicyFactory GetParameterPolicyFactory( + RouteOptions options = null, + ServiceCollection services = null) + { + if (options == null) { - if (options == null) - { - options = new RouteOptions(); - } - - if (services == null) - { - services = new ServiceCollection(); - } - - return new DefaultParameterPolicyFactory( - Options.Create(options), - services.BuildServiceProvider()); + options = new RouteOptions(); } - private class TestRouteConstraint : IRouteConstraint + if (services == null) { - private TestRouteConstraint() { } - - public HttpContext HttpContext { get; private set; } - public IRouter Route { get; private set; } - public string RouteKey { get; private set; } - public RouteValueDictionary Values { get; private set; } - public RouteDirection RouteDirection { get; private set; } - - public static TestRouteConstraint Create() - { - return new TestRouteConstraint(); - } - - public bool Match( - HttpContext httpContext, - IRouter route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) - { - HttpContext = httpContext; - Route = route; - RouteKey = routeKey; - Values = values; - RouteDirection = routeDirection; - return false; - } + services = new ServiceCollection(); } - } - public class CustomParameterPolicy : IParameterPolicy - { + return new DefaultParameterPolicyFactory( + Options.Create(options), + services.BuildServiceProvider()); } - public class CustomParameterPolicyWithArguments : IParameterPolicy + private class TestRouteConstraint : IRouteConstraint { - public CustomParameterPolicyWithArguments(ITestService testService, int count) - { - TestService = testService; - Count = count; - } + private TestRouteConstraint() { } - public ITestService TestService { get; } - public int Count { get; } - } + public HttpContext HttpContext { get; private set; } + public IRouter Route { get; private set; } + public string RouteKey { get; private set; } + public RouteValueDictionary Values { get; private set; } + public RouteDirection RouteDirection { get; private set; } - public class CustomParameterPolicyWithMultpleCtors : IParameterPolicy - { - public CustomParameterPolicyWithMultpleCtors(ITestService testService, int count) + public static TestRouteConstraint Create() { - TestService = testService; - Count = count; + return new TestRouteConstraint(); } - public CustomParameterPolicyWithMultpleCtors(int count) - : this(testService: null, count) + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) { + HttpContext = httpContext; + Route = route; + RouteKey = routeKey; + Values = values; + RouteDirection = routeDirection; + return false; } - - public ITestService TestService { get; } - public int Count { get; } } +} - public class CustomParameterPolicyWithAmbigiousMultpleCtors : IParameterPolicy - { - public CustomParameterPolicyWithAmbigiousMultpleCtors(ITestService testService, int count) - { - TestService = testService; - Count = count; - } +public class CustomParameterPolicy : IParameterPolicy +{ +} - public CustomParameterPolicyWithAmbigiousMultpleCtors(object testService, int count) - : this(testService: null, count) - { - } +public class CustomParameterPolicyWithArguments : IParameterPolicy +{ + public CustomParameterPolicyWithArguments(ITestService testService, int count) + { + TestService = testService; + Count = count; + } - public CustomParameterPolicyWithAmbigiousMultpleCtors(int count) - : this(testService: null, count) - { - } + public ITestService TestService { get; } + public int Count { get; } +} - public ITestService TestService { get; } - public int Count { get; } +public class CustomParameterPolicyWithMultpleCtors : IParameterPolicy +{ + public CustomParameterPolicyWithMultpleCtors(ITestService testService, int count) + { + TestService = testService; + Count = count; } - public class CustomParameterPolicyWithMultipleArguments : IParameterPolicy + public CustomParameterPolicyWithMultpleCtors(int count) + : this(testService: null, count) { - public CustomParameterPolicyWithMultipleArguments(int first, ITestService testService1, int second, ITestService testService2) - { - First = first; - TestService1 = testService1; - Second = second; - TestService2 = testService2; - } - - public int First { get; } - public ITestService TestService1 { get; } - public int Second { get; } - public ITestService TestService2 { get; } } - public class CustomParameterPolicyWithOnlyServiceArguments : IParameterPolicy - { - public CustomParameterPolicyWithOnlyServiceArguments(ITestService testService1, ITestService testService2) - { - TestService1 = testService1; - TestService2 = testService2; - } + public ITestService TestService { get; } + public int Count { get; } +} - public ITestService TestService1 { get; } - public ITestService TestService2 { get; } +public class CustomParameterPolicyWithAmbigiousMultpleCtors : IParameterPolicy +{ + public CustomParameterPolicyWithAmbigiousMultpleCtors(ITestService testService, int count) + { + TestService = testService; + Count = count; } - public interface ITestService + public CustomParameterPolicyWithAmbigiousMultpleCtors(object testService, int count) + : this(testService: null, count) { } - public class TestService : ITestService + public CustomParameterPolicyWithAmbigiousMultpleCtors(int count) + : this(testService: null, count) { + } + + public ITestService TestService { get; } + public int Count { get; } +} +public class CustomParameterPolicyWithMultipleArguments : IParameterPolicy +{ + public CustomParameterPolicyWithMultipleArguments(int first, ITestService testService1, int second, ITestService testService2) + { + First = first; + TestService1 = testService1; + Second = second; + TestService2 = testService2; } - public class RegexInlineRouteConstraintWithService : RegexRouteConstraint + public int First { get; } + public ITestService TestService1 { get; } + public int Second { get; } + public ITestService TestService2 { get; } +} + +public class CustomParameterPolicyWithOnlyServiceArguments : IParameterPolicy +{ + public CustomParameterPolicyWithOnlyServiceArguments(ITestService testService1, ITestService testService2) { - public RegexInlineRouteConstraintWithService(string regexPattern, ITestService testService) - : base(regexPattern) - { - TestService = testService; - } + TestService1 = testService1; + TestService2 = testService2; + } + + public ITestService TestService1 { get; } + public ITestService TestService2 { get; } +} + +public interface ITestService +{ +} + +public class TestService : ITestService +{ + +} - public ITestService TestService { get; } +public class RegexInlineRouteConstraintWithService : RegexRouteConstraint +{ + public RegexInlineRouteConstraintWithService(string regexPattern, ITestService testService) + : base(regexPattern) + { + TestService = testService; } + + public ITestService TestService { get; } } diff --git a/src/Http/Routing/test/UnitTests/EndpointFactory.cs b/src/Http/Routing/test/UnitTests/EndpointFactory.cs index 7d20a720cd..12d5e11248 100644 --- a/src/Http/Routing/test/UnitTests/EndpointFactory.cs +++ b/src/Http/Routing/test/UnitTests/EndpointFactory.cs @@ -11,36 +11,35 @@ using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +internal static class EndpointFactory { - internal static class EndpointFactory + public static RouteEndpoint CreateRouteEndpoint( + string template, + object defaults = null, + object policies = null, + object requiredValues = null, + int order = 0, + string displayName = null, + params object[] metadata) { - public static RouteEndpoint CreateRouteEndpoint( - string template, - object defaults = null, - object policies = null, - object requiredValues = null, - int order = 0, - string displayName = null, - params object[] metadata) - { - var routePattern = RoutePatternFactory.Parse(template, defaults, policies, requiredValues); + var routePattern = RoutePatternFactory.Parse(template, defaults, policies, requiredValues); - return CreateRouteEndpoint(routePattern, order, displayName, metadata); - } + return CreateRouteEndpoint(routePattern, order, displayName, metadata); + } - public static RouteEndpoint CreateRouteEndpoint( - RoutePattern routePattern = null, - int order = 0, - string displayName = null, - IList metadata = null) - { - return new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - routePattern, - order, - new EndpointMetadataCollection(metadata ?? Array.Empty()), - displayName); - } + public static RouteEndpoint CreateRouteEndpoint( + RoutePattern routePattern = null, + int order = 0, + string displayName = null, + IList metadata = null) + { + return new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + routePattern, + order, + new EndpointMetadataCollection(metadata ?? Array.Empty()), + displayName); } } diff --git a/src/Http/Routing/test/UnitTests/EndpointMiddlewareTest.cs b/src/Http/Routing/test/UnitTests/EndpointMiddlewareTest.cs index 2f6482ec71..16c412e710 100644 --- a/src/Http/Routing/test/UnitTests/EndpointMiddlewareTest.cs +++ b/src/Http/Routing/test/UnitTests/EndpointMiddlewareTest.cs @@ -8,289 +8,288 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Moq; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class EndpointMiddlewareTest { - public class EndpointMiddlewareTest + private readonly IOptions RouteOptions = Options.Create(new RouteOptions()); + + [Fact] + public async Task Invoke_NoFeature_NoOps() { - private readonly IOptions RouteOptions = Options.Create(new RouteOptions()); + // Arrange + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = new ServiceProvider(); - [Fact] - public async Task Invoke_NoFeature_NoOps() + var calledNext = false; + RequestDelegate next = (c) => { - // Arrange - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = new ServiceProvider(); + calledNext = true; + return Task.CompletedTask; + }; - var calledNext = false; - RequestDelegate next = (c) => - { - calledNext = true; - return Task.CompletedTask; - }; + var middleware = new EndpointMiddleware(NullLogger.Instance, next, RouteOptions); - var middleware = new EndpointMiddleware(NullLogger.Instance, next, RouteOptions); + // Act + await middleware.Invoke(httpContext); - // Act - await middleware.Invoke(httpContext); + // Assert + Assert.True(calledNext); + } - // Assert - Assert.True(calledNext); - } + [Fact] + public async Task Invoke_NoEndpoint_NoOps() + { + // Arrange + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = new ServiceProvider(); + httpContext.SetEndpoint(null); - [Fact] - public async Task Invoke_NoEndpoint_NoOps() + var calledNext = false; + RequestDelegate next = (c) => { - // Arrange - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = new ServiceProvider(); - httpContext.SetEndpoint(null); + calledNext = true; + return Task.CompletedTask; + }; - var calledNext = false; - RequestDelegate next = (c) => - { - calledNext = true; - return Task.CompletedTask; - }; + var middleware = new EndpointMiddleware(NullLogger.Instance, next, RouteOptions); - var middleware = new EndpointMiddleware(NullLogger.Instance, next, RouteOptions); + // Act + await middleware.Invoke(httpContext); - // Act - await middleware.Invoke(httpContext); + // Assert + Assert.True(calledNext); + } - // Assert - Assert.True(calledNext); - } + [Fact] + public async Task Invoke_WithEndpoint_InvokesDelegate() + { + // Arrange + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = new ServiceProvider(); - [Fact] - public async Task Invoke_WithEndpoint_InvokesDelegate() + var calledEndpoint = false; + RequestDelegate endpointFunc = (c) => { - // Arrange - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = new ServiceProvider(); + calledEndpoint = true; + return Task.CompletedTask; + }; - var calledEndpoint = false; - RequestDelegate endpointFunc = (c) => - { - calledEndpoint = true; - return Task.CompletedTask; - }; + httpContext.SetEndpoint(new Endpoint(endpointFunc, EndpointMetadataCollection.Empty, "Test")); - httpContext.SetEndpoint(new Endpoint(endpointFunc, EndpointMetadataCollection.Empty, "Test")); + RequestDelegate next = (c) => + { + throw new InvalidTimeZoneException("Should not be called"); + }; - RequestDelegate next = (c) => - { - throw new InvalidTimeZoneException("Should not be called"); - }; + var middleware = new EndpointMiddleware(NullLogger.Instance, next, RouteOptions); - var middleware = new EndpointMiddleware(NullLogger.Instance, next, RouteOptions); + // Act + await middleware.Invoke(httpContext); - // Act - await middleware.Invoke(httpContext); + // Assert + Assert.True(calledEndpoint); + } - // Assert - Assert.True(calledEndpoint); - } + [Fact] + public async Task Invoke_WithEndpoint_ThrowsIfAuthAttributesWereFound_ButAuthMiddlewareNotInvoked() + { + // Arrange + var expected = "Endpoint Test contains authorization metadata, but a middleware was not found that supports authorization." + + Environment.NewLine + + "Configure your application startup by adding app.UseAuthorization() in the application startup code. " + + "If there are calls to app.UseRouting() and app.UseEndpoints(...), the call to app.UseAuthorization() must go between them."; + var httpContext = new DefaultHttpContext + { + RequestServices = new ServiceProvider() + }; - [Fact] - public async Task Invoke_WithEndpoint_ThrowsIfAuthAttributesWereFound_ButAuthMiddlewareNotInvoked() + RequestDelegate throwIfCalled = (c) => { - // Arrange - var expected = "Endpoint Test contains authorization metadata, but a middleware was not found that supports authorization." + - Environment.NewLine + - "Configure your application startup by adding app.UseAuthorization() in the application startup code. " + - "If there are calls to app.UseRouting() and app.UseEndpoints(...), the call to app.UseAuthorization() must go between them."; - var httpContext = new DefaultHttpContext - { - RequestServices = new ServiceProvider() - }; - - RequestDelegate throwIfCalled = (c) => - { - throw new InvalidTimeZoneException("Should not be called"); - }; - - httpContext.SetEndpoint(new Endpoint(throwIfCalled, new EndpointMetadataCollection(Mock.Of()), "Test")); - - var middleware = new EndpointMiddleware(NullLogger.Instance, throwIfCalled, RouteOptions); - - // Act & Assert - var ex = await Assert.ThrowsAsync(() => middleware.Invoke(httpContext)); - - // Assert - Assert.Equal(expected, ex.Message); - } + throw new InvalidTimeZoneException("Should not be called"); + }; + + httpContext.SetEndpoint(new Endpoint(throwIfCalled, new EndpointMetadataCollection(Mock.Of()), "Test")); + + var middleware = new EndpointMiddleware(NullLogger.Instance, throwIfCalled, RouteOptions); + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => middleware.Invoke(httpContext)); + + // Assert + Assert.Equal(expected, ex.Message); + } - [Fact] - public async Task Invoke_WithEndpoint_WorksIfAuthAttributesWereFound_AndAuthMiddlewareInvoked() + [Fact] + public async Task Invoke_WithEndpoint_WorksIfAuthAttributesWereFound_AndAuthMiddlewareInvoked() + { + // Arrange + var httpContext = new DefaultHttpContext { - // Arrange - var httpContext = new DefaultHttpContext - { - RequestServices = new ServiceProvider() - }; + RequestServices = new ServiceProvider() + }; - var calledEndpoint = false; - RequestDelegate endpointFunc = (c) => - { - calledEndpoint = true; - return Task.CompletedTask; - }; + var calledEndpoint = false; + RequestDelegate endpointFunc = (c) => + { + calledEndpoint = true; + return Task.CompletedTask; + }; - httpContext.SetEndpoint(new Endpoint(endpointFunc, new EndpointMetadataCollection(Mock.Of()), "Test")); + httpContext.SetEndpoint(new Endpoint(endpointFunc, new EndpointMetadataCollection(Mock.Of()), "Test")); - httpContext.Items[EndpointMiddleware.AuthorizationMiddlewareInvokedKey] = true; + httpContext.Items[EndpointMiddleware.AuthorizationMiddlewareInvokedKey] = true; - RequestDelegate next = (c) => - { - throw new InvalidTimeZoneException("Should not be called"); - }; + RequestDelegate next = (c) => + { + throw new InvalidTimeZoneException("Should not be called"); + }; - var middleware = new EndpointMiddleware(NullLogger.Instance, next, RouteOptions); + var middleware = new EndpointMiddleware(NullLogger.Instance, next, RouteOptions); - // Act - await middleware.Invoke(httpContext); + // Act + await middleware.Invoke(httpContext); - // Assert - Assert.True(calledEndpoint); - } + // Assert + Assert.True(calledEndpoint); + } - [Fact] - public async Task Invoke_WithEndpoint_DoesNotThrowIfUnhandledAuthAttributesWereFound_ButSuppressedViaOptions() + [Fact] + public async Task Invoke_WithEndpoint_DoesNotThrowIfUnhandledAuthAttributesWereFound_ButSuppressedViaOptions() + { + // Arrange + var httpContext = new DefaultHttpContext { - // Arrange - var httpContext = new DefaultHttpContext - { - RequestServices = new ServiceProvider() - }; + RequestServices = new ServiceProvider() + }; - var calledEndpoint = false; - RequestDelegate endpointFunc = (c) => - { - calledEndpoint = true; - return Task.CompletedTask; - }; + var calledEndpoint = false; + RequestDelegate endpointFunc = (c) => + { + calledEndpoint = true; + return Task.CompletedTask; + }; - httpContext.SetEndpoint(new Endpoint(endpointFunc, new EndpointMetadataCollection(Mock.Of()), "Test")); + httpContext.SetEndpoint(new Endpoint(endpointFunc, new EndpointMetadataCollection(Mock.Of()), "Test")); - var routeOptions = Options.Create(new RouteOptions { SuppressCheckForUnhandledSecurityMetadata = true }); + var routeOptions = Options.Create(new RouteOptions { SuppressCheckForUnhandledSecurityMetadata = true }); - RequestDelegate next = (c) => - { - throw new InvalidTimeZoneException("Should not be called"); - }; + RequestDelegate next = (c) => + { + throw new InvalidTimeZoneException("Should not be called"); + }; - var middleware = new EndpointMiddleware(NullLogger.Instance, next, routeOptions); + var middleware = new EndpointMiddleware(NullLogger.Instance, next, routeOptions); - // Act - await middleware.Invoke(httpContext); + // Act + await middleware.Invoke(httpContext); - // Assert - Assert.True(calledEndpoint); - } + // Assert + Assert.True(calledEndpoint); + } - [Fact] - public async Task Invoke_WithEndpoint_ThrowsIfCorsMetadataWasFound_ButCorsMiddlewareNotInvoked() + [Fact] + public async Task Invoke_WithEndpoint_ThrowsIfCorsMetadataWasFound_ButCorsMiddlewareNotInvoked() + { + // Arrange + var expected = "Endpoint Test contains CORS metadata, but a middleware was not found that supports CORS." + + Environment.NewLine + + "Configure your application startup by adding app.UseCors() in the application startup code. " + + "If there are calls to app.UseRouting() and app.UseEndpoints(...), the call to app.UseCors() must go between them."; + var httpContext = new DefaultHttpContext { - // Arrange - var expected = "Endpoint Test contains CORS metadata, but a middleware was not found that supports CORS." + - Environment.NewLine + - "Configure your application startup by adding app.UseCors() in the application startup code. " + - "If there are calls to app.UseRouting() and app.UseEndpoints(...), the call to app.UseCors() must go between them."; - var httpContext = new DefaultHttpContext - { - RequestServices = new ServiceProvider() - }; - - RequestDelegate throwIfCalled = (c) => - { - throw new InvalidTimeZoneException("Should not be called"); - }; - - httpContext.SetEndpoint(new Endpoint(throwIfCalled, new EndpointMetadataCollection(Mock.Of()), "Test")); - - var middleware = new EndpointMiddleware(NullLogger.Instance, throwIfCalled, RouteOptions); - - // Act & Assert - var ex = await Assert.ThrowsAsync(() => middleware.Invoke(httpContext)); - - // Assert - Assert.Equal(expected, ex.Message); - } + RequestServices = new ServiceProvider() + }; - [Fact] - public async Task Invoke_WithEndpoint_WorksIfCorsMetadataWasFound_AndCorsMiddlewareInvoked() + RequestDelegate throwIfCalled = (c) => { - // Arrange - var httpContext = new DefaultHttpContext - { - RequestServices = new ServiceProvider() - }; + throw new InvalidTimeZoneException("Should not be called"); + }; - var calledEndpoint = false; - RequestDelegate endpointFunc = (c) => - { - calledEndpoint = true; - return Task.CompletedTask; - }; + httpContext.SetEndpoint(new Endpoint(throwIfCalled, new EndpointMetadataCollection(Mock.Of()), "Test")); - httpContext.SetEndpoint(new Endpoint(endpointFunc, new EndpointMetadataCollection(Mock.Of()), "Test")); + var middleware = new EndpointMiddleware(NullLogger.Instance, throwIfCalled, RouteOptions); - httpContext.Items[EndpointMiddleware.CorsMiddlewareInvokedKey] = true; + // Act & Assert + var ex = await Assert.ThrowsAsync(() => middleware.Invoke(httpContext)); - RequestDelegate next = (c) => - { - throw new InvalidTimeZoneException("Should not be called"); - }; + // Assert + Assert.Equal(expected, ex.Message); + } + + [Fact] + public async Task Invoke_WithEndpoint_WorksIfCorsMetadataWasFound_AndCorsMiddlewareInvoked() + { + // Arrange + var httpContext = new DefaultHttpContext + { + RequestServices = new ServiceProvider() + }; - var middleware = new EndpointMiddleware(NullLogger.Instance, next, RouteOptions); + var calledEndpoint = false; + RequestDelegate endpointFunc = (c) => + { + calledEndpoint = true; + return Task.CompletedTask; + }; - // Act - await middleware.Invoke(httpContext); + httpContext.SetEndpoint(new Endpoint(endpointFunc, new EndpointMetadataCollection(Mock.Of()), "Test")); - // Assert - Assert.True(calledEndpoint); - } + httpContext.Items[EndpointMiddleware.CorsMiddlewareInvokedKey] = true; - [Fact] - public async Task Invoke_WithEndpoint_DoesNotThrowIfUnhandledCorsAttributesWereFound_ButSuppressedViaOptions() + RequestDelegate next = (c) => { - // Arrange - var httpContext = new DefaultHttpContext - { - RequestServices = new ServiceProvider() - }; + throw new InvalidTimeZoneException("Should not be called"); + }; - var calledEndpoint = false; - RequestDelegate endpointFunc = (c) => - { - calledEndpoint = true; - return Task.CompletedTask; - }; + var middleware = new EndpointMiddleware(NullLogger.Instance, next, RouteOptions); - httpContext.SetEndpoint(new Endpoint(endpointFunc, new EndpointMetadataCollection(Mock.Of()), "Test")); + // Act + await middleware.Invoke(httpContext); - var routeOptions = Options.Create(new RouteOptions { SuppressCheckForUnhandledSecurityMetadata = true }); + // Assert + Assert.True(calledEndpoint); + } + + [Fact] + public async Task Invoke_WithEndpoint_DoesNotThrowIfUnhandledCorsAttributesWereFound_ButSuppressedViaOptions() + { + // Arrange + var httpContext = new DefaultHttpContext + { + RequestServices = new ServiceProvider() + }; - RequestDelegate next = (c) => - { - throw new InvalidTimeZoneException("Should not be called"); - }; + var calledEndpoint = false; + RequestDelegate endpointFunc = (c) => + { + calledEndpoint = true; + return Task.CompletedTask; + }; - var middleware = new EndpointMiddleware(NullLogger.Instance, next, routeOptions); + httpContext.SetEndpoint(new Endpoint(endpointFunc, new EndpointMetadataCollection(Mock.Of()), "Test")); - // Act - await middleware.Invoke(httpContext); + var routeOptions = Options.Create(new RouteOptions { SuppressCheckForUnhandledSecurityMetadata = true }); - // Assert - Assert.True(calledEndpoint); - } + RequestDelegate next = (c) => + { + throw new InvalidTimeZoneException("Should not be called"); + }; + + var middleware = new EndpointMiddleware(NullLogger.Instance, next, routeOptions); + + // Act + await middleware.Invoke(httpContext); - private class ServiceProvider : IServiceProvider + // Assert + Assert.True(calledEndpoint); + } + + private class ServiceProvider : IServiceProvider + { + public object GetService(Type serviceType) { - public object GetService(Type serviceType) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } } } diff --git a/src/Http/Routing/test/UnitTests/EndpointNameAddressSchemeTest.cs b/src/Http/Routing/test/UnitTests/EndpointNameAddressSchemeTest.cs index 330a4450ac..82d703b18b 100644 --- a/src/Http/Routing/test/UnitTests/EndpointNameAddressSchemeTest.cs +++ b/src/Http/Routing/test/UnitTests/EndpointNameAddressSchemeTest.cs @@ -7,155 +7,155 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.TestObjects; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class EndpointNameAddressSchemeTest { - public class EndpointNameAddressSchemeTest + [Fact] + public void AddressScheme_Match_ReturnsMatchingEndpoint() { - [Fact] - public void AddressScheme_Match_ReturnsMatchingEndpoint() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint( - "/a", - metadata: new object[] { new EndpointNameMetadata("name1"), }); + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint( + "/a", + metadata: new object[] { new EndpointNameMetadata("name1"), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint( - "/b", - metadata: new object[] { new EndpointNameMetadata("name2"), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint( + "/b", + metadata: new object[] { new EndpointNameMetadata("name2"), }); - var addressScheme = CreateAddressScheme(endpoint1, endpoint2); + var addressScheme = CreateAddressScheme(endpoint1, endpoint2); - // Act - var endpoints = addressScheme.FindEndpoints("name2"); + // Act + var endpoints = addressScheme.FindEndpoints("name2"); - // Assert - Assert.Collection( - endpoints, - e => Assert.Same(endpoint2, e)); - } + // Assert + Assert.Collection( + endpoints, + e => Assert.Same(endpoint2, e)); + } - [Fact] - public void AddressScheme_NoMatch_ReturnsEmptyCollection() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "/a", - metadata: new object[] { new EndpointNameMetadata("name1"), new SuppressLinkGenerationMetadata(), }); + [Fact] + public void AddressScheme_NoMatch_ReturnsEmptyCollection() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "/a", + metadata: new object[] { new EndpointNameMetadata("name1"), new SuppressLinkGenerationMetadata(), }); - var addressScheme = CreateAddressScheme(endpoint); + var addressScheme = CreateAddressScheme(endpoint); - // Act - var endpoints = addressScheme.FindEndpoints("name2"); + // Act + var endpoints = addressScheme.FindEndpoints("name2"); - // Assert - Assert.Empty(endpoints); - } + // Assert + Assert.Empty(endpoints); + } - [Fact] - public void AddressScheme_NoMatch_CaseSensitive() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "/a", - metadata: new object[] { new EndpointNameMetadata("name1"), new SuppressLinkGenerationMetadata(), }); + [Fact] + public void AddressScheme_NoMatch_CaseSensitive() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "/a", + metadata: new object[] { new EndpointNameMetadata("name1"), new SuppressLinkGenerationMetadata(), }); - var addressScheme = CreateAddressScheme(endpoint); + var addressScheme = CreateAddressScheme(endpoint); - // Act - var endpoints = addressScheme.FindEndpoints("NAME1"); + // Act + var endpoints = addressScheme.FindEndpoints("NAME1"); - // Assert - Assert.Empty(endpoints); - } + // Assert + Assert.Empty(endpoints); + } - [Fact] - public void AddressScheme_UpdatesWhenDataSourceChanges() - { - var endpoint1 = EndpointFactory.CreateRouteEndpoint( - "/a", - metadata: new object[] { new EndpointNameMetadata("name1"), }); - var dynamicDataSource = new DynamicEndpointDataSource(new[] { endpoint1 }); - - // Act 1 - var addressScheme = CreateAddressScheme(dynamicDataSource); - - // Assert 1 - var match = Assert.Single(addressScheme.Entries); - Assert.Same(endpoint1, match.Value.Single()); - - // Arrange 2 - var endpoint2 = EndpointFactory.CreateRouteEndpoint( - "/b", - metadata: new object[] { new EndpointNameMetadata("name2"), }); - - // Act 2 - // Trigger change - dynamicDataSource.AddEndpoint(endpoint2); - - // Assert 2 - Assert.Collection( - addressScheme.Entries.OrderBy(kvp => kvp.Key), - (m) => - { - Assert.Same(endpoint1, m.Value.Single()); - }, - (m) => - { - Assert.Same(endpoint2, m.Value.Single()); - }); - } - - [Fact] - public void AddressScheme_IgnoresEndpointsWithSuppressLinkGeneration() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "/a", - metadata: new object[] { new EndpointNameMetadata("name1"), new SuppressLinkGenerationMetadata(), }); + [Fact] + public void AddressScheme_UpdatesWhenDataSourceChanges() + { + var endpoint1 = EndpointFactory.CreateRouteEndpoint( + "/a", + metadata: new object[] { new EndpointNameMetadata("name1"), }); + var dynamicDataSource = new DynamicEndpointDataSource(new[] { endpoint1 }); + + // Act 1 + var addressScheme = CreateAddressScheme(dynamicDataSource); + + // Assert 1 + var match = Assert.Single(addressScheme.Entries); + Assert.Same(endpoint1, match.Value.Single()); + + // Arrange 2 + var endpoint2 = EndpointFactory.CreateRouteEndpoint( + "/b", + metadata: new object[] { new EndpointNameMetadata("name2"), }); + + // Act 2 + // Trigger change + dynamicDataSource.AddEndpoint(endpoint2); + + // Assert 2 + Assert.Collection( + addressScheme.Entries.OrderBy(kvp => kvp.Key), + (m) => + { + Assert.Same(endpoint1, m.Value.Single()); + }, + (m) => + { + Assert.Same(endpoint2, m.Value.Single()); + }); + } - // Act - var addressScheme = CreateAddressScheme(endpoint); + [Fact] + public void AddressScheme_IgnoresEndpointsWithSuppressLinkGeneration() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "/a", + metadata: new object[] { new EndpointNameMetadata("name1"), new SuppressLinkGenerationMetadata(), }); - // Assert - Assert.Empty(addressScheme.Entries); - } + // Act + var addressScheme = CreateAddressScheme(endpoint); - [Fact] - public void AddressScheme_UnsuppressedEndpoint_IsUsed() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "/a", - metadata: new object[] { new EndpointNameMetadata("name1"), new SuppressLinkGenerationMetadata(), new EncourageLinkGenerationMetadata(), }); + // Assert + Assert.Empty(addressScheme.Entries); + } - // Act - var addressScheme = CreateAddressScheme(endpoint); + [Fact] + public void AddressScheme_UnsuppressedEndpoint_IsUsed() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "/a", + metadata: new object[] { new EndpointNameMetadata("name1"), new SuppressLinkGenerationMetadata(), new EncourageLinkGenerationMetadata(), }); - // Assert - Assert.Same(endpoint, Assert.Single(Assert.Single(addressScheme.Entries).Value)); - } + // Act + var addressScheme = CreateAddressScheme(endpoint); - [Fact] - public void AddressScheme_IgnoresEndpointsWithoutEndpointName() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "/a", - metadata: new object[] { }); + // Assert + Assert.Same(endpoint, Assert.Single(Assert.Single(addressScheme.Entries).Value)); + } - // Act - var addressScheme = CreateAddressScheme(endpoint); + [Fact] + public void AddressScheme_IgnoresEndpointsWithoutEndpointName() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "/a", + metadata: new object[] { }); - // Assert - Assert.Empty(addressScheme.Entries); - } + // Act + var addressScheme = CreateAddressScheme(endpoint); - [Fact] - public void AddressScheme_ThrowsExceptionForDuplicateEndpoints() + // Assert + Assert.Empty(addressScheme.Entries); + } + + [Fact] + public void AddressScheme_ThrowsExceptionForDuplicateEndpoints() + { + // Arrange + var endpoints = new Endpoint[] { - // Arrange - var endpoints = new Endpoint[] - { EndpointFactory.CreateRouteEndpoint("/a", displayName: "a", metadata: new object[] { new EndpointNameMetadata("name1"), }), EndpointFactory.CreateRouteEndpoint("/b", displayName: "b", metadata: new object[] { new EndpointNameMetadata("name1"), }), EndpointFactory.CreateRouteEndpoint("/c", displayName: "c", metadata: new object[] { new EndpointNameMetadata("name1"), }), @@ -165,15 +165,15 @@ namespace Microsoft.AspNetCore.Routing EndpointFactory.CreateRouteEndpoint("/e", displayName: "e", metadata: new object[] { new EndpointNameMetadata("name2"), }), EndpointFactory.CreateRouteEndpoint("/f", displayName: "f", metadata: new object[] { new EndpointNameMetadata("name2"), }), - }; + }; - var addressScheme = CreateAddressScheme(endpoints); + var addressScheme = CreateAddressScheme(endpoints); - // Act - var ex = Assert.Throws(() => addressScheme.FindEndpoints("any name")); + // Act + var ex = Assert.Throws(() => addressScheme.FindEndpoints("any name")); - // Assert - Assert.Equal(String.Join(Environment.NewLine, @"The following endpoints with a duplicate endpoint name were found.", + // Assert + Assert.Equal(String.Join(Environment.NewLine, @"The following endpoints with a duplicate endpoint name were found.", "", "Endpoints with endpoint name 'name1':", "a", @@ -184,21 +184,20 @@ namespace Microsoft.AspNetCore.Routing "e", "f", ""), ex.Message); - } + } - private EndpointNameAddressScheme CreateAddressScheme(params Endpoint[] endpoints) - { - return CreateAddressScheme(new DefaultEndpointDataSource(endpoints)); - } + private EndpointNameAddressScheme CreateAddressScheme(params Endpoint[] endpoints) + { + return CreateAddressScheme(new DefaultEndpointDataSource(endpoints)); + } - private EndpointNameAddressScheme CreateAddressScheme(params EndpointDataSource[] dataSources) - { - return new EndpointNameAddressScheme(new CompositeEndpointDataSource(dataSources)); - } + private EndpointNameAddressScheme CreateAddressScheme(params EndpointDataSource[] dataSources) + { + return new EndpointNameAddressScheme(new CompositeEndpointDataSource(dataSources)); + } - private class EncourageLinkGenerationMetadata : ISuppressLinkGenerationMetadata - { - public bool SuppressLinkGeneration => false; - } + private class EncourageLinkGenerationMetadata : ISuppressLinkGenerationMetadata + { + public bool SuppressLinkGeneration => false; } } diff --git a/src/Http/Routing/test/UnitTests/EndpointRoutingMiddlewareTest.cs b/src/Http/Routing/test/UnitTests/EndpointRoutingMiddlewareTest.cs index f09878453d..3a2e6c8c46 100644 --- a/src/Http/Routing/test/UnitTests/EndpointRoutingMiddlewareTest.cs +++ b/src/Http/Routing/test/UnitTests/EndpointRoutingMiddlewareTest.cs @@ -16,202 +16,201 @@ using Microsoft.Extensions.Logging.Testing; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class EndpointRoutingMiddlewareTest { - public class EndpointRoutingMiddlewareTest + [Fact] + public async Task Invoke_OnCall_SetsEndpointFeature() { - [Fact] - public async Task Invoke_OnCall_SetsEndpointFeature() - { - // Arrange - var httpContext = CreateHttpContext(); + // Arrange + var httpContext = CreateHttpContext(); - var middleware = CreateMiddleware(); + var middleware = CreateMiddleware(); - // Act - await middleware.Invoke(httpContext); + // Act + await middleware.Invoke(httpContext); - // Assert - var endpointFeature = httpContext.Features.Get(); - Assert.NotNull(endpointFeature); - } + // Assert + var endpointFeature = httpContext.Features.Get(); + Assert.NotNull(endpointFeature); + } - [Fact] - public async Task Invoke_SkipsRouting_IfEndpointSet() - { - // Arrange - var httpContext = CreateHttpContext(); - httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(), "myapp")); + [Fact] + public async Task Invoke_SkipsRouting_IfEndpointSet() + { + // Arrange + var httpContext = CreateHttpContext(); + httpContext.SetEndpoint(new Endpoint(c => Task.CompletedTask, new EndpointMetadataCollection(), "myapp")); - var middleware = CreateMiddleware(); + var middleware = CreateMiddleware(); - // Act - await middleware.Invoke(httpContext); + // Act + await middleware.Invoke(httpContext); - // Assert - var endpoint = httpContext.GetEndpoint(); - Assert.NotNull(endpoint); - Assert.Equal("myapp", endpoint.DisplayName); - } + // Assert + var endpoint = httpContext.GetEndpoint(); + Assert.NotNull(endpoint); + Assert.Equal("myapp", endpoint.DisplayName); + } - [Fact] - public async Task Invoke_OnCall_WritesToConfiguredLogger() - { - // Arrange - var expectedMessage = "Request matched endpoint 'Test endpoint'"; - bool eventFired = false; + [Fact] + public async Task Invoke_OnCall_WritesToConfiguredLogger() + { + // Arrange + var expectedMessage = "Request matched endpoint 'Test endpoint'"; + bool eventFired = false; - var sink = new TestSink( - TestSink.EnableWithTypeName, - TestSink.EnableWithTypeName); - var loggerFactory = new TestLoggerFactory(sink, enabled: true); - var listener = new DiagnosticListener("TestListener"); + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var listener = new DiagnosticListener("TestListener"); - using var subscription = listener.Subscribe(new DelegateObserver(pair => - { - eventFired = true; + using var subscription = listener.Subscribe(new DelegateObserver(pair => + { + eventFired = true; - Assert.Equal("Microsoft.AspNetCore.Routing.EndpointMatched", pair.Key); - Assert.IsAssignableFrom(pair.Value); - })); + Assert.Equal("Microsoft.AspNetCore.Routing.EndpointMatched", pair.Key); + Assert.IsAssignableFrom(pair.Value); + })); - var httpContext = CreateHttpContext(); + var httpContext = CreateHttpContext(); - var logger = new Logger(loggerFactory); - var middleware = CreateMiddleware(logger, listener: listener); + var logger = new Logger(loggerFactory); + var middleware = CreateMiddleware(logger, listener: listener); - // Act - await middleware.Invoke(httpContext); + // Act + await middleware.Invoke(httpContext); - // Assert - Assert.Empty(sink.Scopes); - var write = Assert.Single(sink.Writes); - Assert.Equal(expectedMessage, write.State?.ToString()); - Assert.True(eventFired); - } + // Assert + Assert.Empty(sink.Scopes); + var write = Assert.Single(sink.Writes); + Assert.Equal(expectedMessage, write.State?.ToString()); + Assert.True(eventFired); + } - [Fact] - public async Task Invoke_BackCompatGetRouteValue_ValueUsedFromEndpointFeature() - { - // Arrange - var httpContext = CreateHttpContext(); + [Fact] + public async Task Invoke_BackCompatGetRouteValue_ValueUsedFromEndpointFeature() + { + // Arrange + var httpContext = CreateHttpContext(); - var middleware = CreateMiddleware(); + var middleware = CreateMiddleware(); - // Act - await middleware.Invoke(httpContext); - var routeData = httpContext.GetRouteData(); - var routeValue = httpContext.GetRouteValue("controller"); - var routeValuesFeature = httpContext.Features.Get(); + // Act + await middleware.Invoke(httpContext); + var routeData = httpContext.GetRouteData(); + var routeValue = httpContext.GetRouteValue("controller"); + var routeValuesFeature = httpContext.Features.Get(); - // Assert - Assert.NotNull(routeData); - Assert.Equal("Home", (string)routeValue); + // Assert + Assert.NotNull(routeData); + Assert.Equal("Home", (string)routeValue); - // changing route data value is reflected in endpoint feature values - routeData.Values["testKey"] = "testValue"; - Assert.Equal("testValue", routeValuesFeature.RouteValues["testKey"]); - } + // changing route data value is reflected in endpoint feature values + routeData.Values["testKey"] = "testValue"; + Assert.Equal("testValue", routeValuesFeature.RouteValues["testKey"]); + } - [Fact] - public async Task Invoke_BackCompatGetDataTokens_ValueUsedFromEndpointMetadata() - { - // Arrange - var httpContext = CreateHttpContext(); + [Fact] + public async Task Invoke_BackCompatGetDataTokens_ValueUsedFromEndpointMetadata() + { + // Arrange + var httpContext = CreateHttpContext(); - var middleware = CreateMiddleware(); + var middleware = CreateMiddleware(); - // Act - await middleware.Invoke(httpContext); - var routeData = httpContext.GetRouteData(); - var routeValue = httpContext.GetRouteValue("controller"); - var routeValuesFeature = httpContext.Features.Get(); + // Act + await middleware.Invoke(httpContext); + var routeData = httpContext.GetRouteData(); + var routeValue = httpContext.GetRouteValue("controller"); + var routeValuesFeature = httpContext.Features.Get(); - // Assert - Assert.NotNull(routeData); - Assert.Equal("Home", (string)routeValue); + // Assert + Assert.NotNull(routeData); + Assert.Equal("Home", (string)routeValue); - // changing route data value is reflected in endpoint feature values - routeData.Values["testKey"] = "testValue"; - Assert.Equal("testValue", routeValuesFeature.RouteValues["testKey"]); - } + // changing route data value is reflected in endpoint feature values + routeData.Values["testKey"] = "testValue"; + Assert.Equal("testValue", routeValuesFeature.RouteValues["testKey"]); + } - [Fact] - public async Task Invoke_InitializationFailure_AllowsReinitialization() - { - // Arrange - var httpContext = CreateHttpContext(); + [Fact] + public async Task Invoke_InitializationFailure_AllowsReinitialization() + { + // Arrange + var httpContext = CreateHttpContext(); - var matcherFactory = new Mock(); - matcherFactory - .Setup(f => f.CreateMatcher(It.IsAny())) - .Throws(new InvalidTimeZoneException()) - .Verifiable(); + var matcherFactory = new Mock(); + matcherFactory + .Setup(f => f.CreateMatcher(It.IsAny())) + .Throws(new InvalidTimeZoneException()) + .Verifiable(); - var middleware = CreateMiddleware(matcherFactory: matcherFactory.Object); + var middleware = CreateMiddleware(matcherFactory: matcherFactory.Object); - // Act - await Assert.ThrowsAsync(async () => await middleware.Invoke(httpContext)); - await Assert.ThrowsAsync(async () => await middleware.Invoke(httpContext)); + // Act + await Assert.ThrowsAsync(async () => await middleware.Invoke(httpContext)); + await Assert.ThrowsAsync(async () => await middleware.Invoke(httpContext)); - // Assert - matcherFactory - .Verify(f => f.CreateMatcher(It.IsAny()), Times.Exactly(2)); - } + // Assert + matcherFactory + .Verify(f => f.CreateMatcher(It.IsAny()), Times.Exactly(2)); + } - private HttpContext CreateHttpContext() + private HttpContext CreateHttpContext() + { + var httpContext = new DefaultHttpContext { - var httpContext = new DefaultHttpContext - { - RequestServices = new TestServiceProvider() - }; + RequestServices = new TestServiceProvider() + }; - return httpContext; - } + return httpContext; + } - private EndpointRoutingMiddleware CreateMiddleware( - Logger logger = null, - MatcherFactory matcherFactory = null, - DiagnosticListener listener = null, - RequestDelegate next = null) + private EndpointRoutingMiddleware CreateMiddleware( + Logger logger = null, + MatcherFactory matcherFactory = null, + DiagnosticListener listener = null, + RequestDelegate next = null) + { + next ??= c => Task.CompletedTask; + logger ??= new Logger(NullLoggerFactory.Instance); + matcherFactory ??= new TestMatcherFactory(true); + listener ??= new DiagnosticListener("Microsoft.AspNetCore"); + + var middleware = new EndpointRoutingMiddleware( + matcherFactory, + logger, + new DefaultEndpointRouteBuilder(Mock.Of()), + listener, + next); + + return middleware; + } + + private class DelegateObserver : IObserver> + { + private readonly Action> _onNext; + + public DelegateObserver(Action> onNext) { - next ??= c => Task.CompletedTask; - logger ??= new Logger(NullLoggerFactory.Instance); - matcherFactory ??= new TestMatcherFactory(true); - listener ??= new DiagnosticListener("Microsoft.AspNetCore"); - - var middleware = new EndpointRoutingMiddleware( - matcherFactory, - logger, - new DefaultEndpointRouteBuilder(Mock.Of()), - listener, - next); - - return middleware; + _onNext = onNext; } - - private class DelegateObserver : IObserver> + public void OnCompleted() { - private readonly Action> _onNext; - - public DelegateObserver(Action> onNext) - { - _onNext = onNext; - } - public void OnCompleted() - { - } + } - public void OnError(Exception error) - { + public void OnError(Exception error) + { - } + } - public void OnNext(KeyValuePair value) - { - _onNext(value); - } + public void OnNext(KeyValuePair value) + { + _onNext(value); } } } diff --git a/src/Http/Routing/test/UnitTests/InlineRouteParameterParserTests.cs b/src/Http/Routing/test/UnitTests/InlineRouteParameterParserTests.cs index 4a3616540d..627120d28d 100644 --- a/src/Http/Routing/test/UnitTests/InlineRouteParameterParserTests.cs +++ b/src/Http/Routing/test/UnitTests/InlineRouteParameterParserTests.cs @@ -10,983 +10,982 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class InlineRouteParameterParserTests { - public class InlineRouteParameterParserTests + [Theory] + [InlineData("=")] + [InlineData(":")] + public void ParseRouteParameter_WithoutADefaultValue(string parameterName) { - [Theory] - [InlineData("=")] - [InlineData(":")] - public void ParseRouteParameter_WithoutADefaultValue(string parameterName) - { - // Arrange & Act - var templatePart = ParseParameter(parameterName); + // Arrange & Act + var templatePart = ParseParameter(parameterName); - // Assert - Assert.Equal(parameterName, templatePart.Name); - Assert.Null(templatePart.DefaultValue); - Assert.Empty(templatePart.InlineConstraints); - } + // Assert + Assert.Equal(parameterName, templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.Empty(templatePart.InlineConstraints); + } - [Fact] - public void ParseRouteParameter_WithEmptyDefaultValue() - { - // Arrange & Act - var templatePart = ParseParameter("param="); + [Fact] + public void ParseRouteParameter_WithEmptyDefaultValue() + { + // Arrange & Act + var templatePart = ParseParameter("param="); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("", templatePart.DefaultValue); - Assert.Empty(templatePart.InlineConstraints); - } + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("", templatePart.DefaultValue); + Assert.Empty(templatePart.InlineConstraints); + } - [Fact] - public void ParseRouteParameter_WithoutAConstraintName() - { - // Arrange & Act - var templatePart = ParseParameter("param:"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Null(templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Empty(constraint.Constraint); - } + [Fact] + public void ParseRouteParameter_WithoutAConstraintName() + { + // Arrange & Act + var templatePart = ParseParameter("param:"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Empty(constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_WithoutAConstraintNameOrParameterName() - { - // Arrange & Act - var templatePart = ParseParameter("param:="); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Empty(constraint.Constraint); - } + [Fact] + public void ParseRouteParameter_WithoutAConstraintNameOrParameterName() + { + // Arrange & Act + var templatePart = ParseParameter("param:="); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("", templatePart.DefaultValue); + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Empty(constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_WithADefaultValueContainingConstraintSeparator() - { - // Arrange & Act - var templatePart = ParseParameter("param=:"); + [Fact] + public void ParseRouteParameter_WithADefaultValueContainingConstraintSeparator() + { + // Arrange & Act + var templatePart = ParseParameter("param=:"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal(":", templatePart.DefaultValue); - Assert.Empty(templatePart.InlineConstraints); - } + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal(":", templatePart.DefaultValue); + Assert.Empty(templatePart.InlineConstraints); + } - [Fact] - public void ParseRouteParameter_ConstraintAndDefault_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter("param:int=111111"); + [Fact] + public void ParseRouteParameter_ConstraintAndDefault_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter("param:int=111111"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("111111", templatePart.DefaultValue); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("111111", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("int", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("int", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithArgumentsAndDefault_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\d+)=111111"); + [Fact] + public void ParseRouteParameter_ConstraintWithArgumentsAndDefault_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+)=111111"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("111111", templatePart.DefaultValue); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("111111", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\d+)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\d+)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintAndOptional_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:int?"); + [Fact] + public void ParseRouteParameter_ConstraintAndOptional_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:int?"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.True(templatePart.IsOptional); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.True(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("int", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("int", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintAndOptional_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:int=12?"); + [Fact] + public void ParseRouteParameter_ConstraintAndOptional_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:int=12?"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("12", templatePart.DefaultValue); - Assert.True(templatePart.IsOptional); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("12", templatePart.DefaultValue); + Assert.True(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("int", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("int", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintAndOptional_WithDefaultValueWithQuestionMark_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:int=12??"); + [Fact] + public void ParseRouteParameter_ConstraintAndOptional_WithDefaultValueWithQuestionMark_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:int=12??"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("12?", templatePart.DefaultValue); - Assert.True(templatePart.IsOptional); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("12?", templatePart.DefaultValue); + Assert.True(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("int", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("int", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithArgumentsAndOptional_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\d+)?"); + [Fact] + public void ParseRouteParameter_ConstraintWithArgumentsAndOptional_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+)?"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.True(templatePart.IsOptional); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.True(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\d+)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\d+)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithArgumentsAndOptional_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\d+)=abc?"); + [Fact] + public void ParseRouteParameter_ConstraintWithArgumentsAndOptional_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+)=abc?"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.True(templatePart.IsOptional); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.True(templatePart.IsOptional); - Assert.Equal("abc", templatePart.DefaultValue); + Assert.Equal("abc", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\d+)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\d+)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ChainedConstraints_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(d+):test(w+)"); + [Fact] + public void ParseRouteParameter_ChainedConstraints_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(d+):test(w+)"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - Assert.Collection(templatePart.InlineConstraints, - constraint => Assert.Equal(@"test(d+)", constraint.Constraint), - constraint => Assert.Equal(@"test(w+)", constraint.Constraint)); - } + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(d+)", constraint.Constraint), + constraint => Assert.Equal(@"test(w+)", constraint.Constraint)); + } - [Fact] - public void ParseRouteParameter_ChainedConstraints_DoubleDelimiters_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param::test(d+)::test(w+)"); + [Fact] + public void ParseRouteParameter_ChainedConstraints_DoubleDelimiters_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param::test(d+)::test(w+)"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - Assert.Collection(templatePart.InlineConstraints, - constraint => Assert.Empty(constraint.Constraint), - constraint => Assert.Equal(@"test(d+)", constraint.Constraint), - constraint => Assert.Empty(constraint.Constraint), - constraint => Assert.Equal(@"test(w+)", constraint.Constraint)); - } + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Empty(constraint.Constraint), + constraint => Assert.Equal(@"test(d+)", constraint.Constraint), + constraint => Assert.Empty(constraint.Constraint), + constraint => Assert.Equal(@"test(w+)", constraint.Constraint)); + } - [Fact] - public void ParseRouteParameter_ChainedConstraints_ColonInPattern_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\d+):test(\w:+)"); + [Fact] + public void ParseRouteParameter_ChainedConstraints_ColonInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+):test(\w:+)"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - Assert.Collection(templatePart.InlineConstraints, - constraint => Assert.Equal(@"test(\d+)", constraint.Constraint), - constraint => Assert.Equal(@"test(\w:+)", constraint.Constraint)); - } + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(\d+)", constraint.Constraint), + constraint => Assert.Equal(@"test(\w:+)", constraint.Constraint)); + } - [Fact] - public void ParseRouteParameter_ChainedConstraints_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\d+):test(\w+)=qwer"); + [Fact] + public void ParseRouteParameter_ChainedConstraints_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+):test(\w+)=qwer"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - Assert.Equal("qwer", templatePart.DefaultValue); + Assert.Equal("qwer", templatePart.DefaultValue); - Assert.Collection(templatePart.InlineConstraints, - constraint => Assert.Equal(@"test(\d+)", constraint.Constraint), - constraint => Assert.Equal(@"test(\w+)", constraint.Constraint)); - } + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(\d+)", constraint.Constraint), + constraint => Assert.Equal(@"test(\w+)", constraint.Constraint)); + } - [Fact] - public void ParseRouteParameter_ChainedConstraints_WithDefaultValue_DoubleDelimiters_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\d+)::test(\w+)==qwer"); + [Fact] + public void ParseRouteParameter_ChainedConstraints_WithDefaultValue_DoubleDelimiters_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+)::test(\w+)==qwer"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - Assert.Equal("=qwer", templatePart.DefaultValue); + Assert.Equal("=qwer", templatePart.DefaultValue); - Assert.Collection(templatePart.InlineConstraints, - constraint => Assert.Equal(@"test(\d+)", constraint.Constraint), - constraint => Assert.Empty(constraint.Constraint), - constraint => Assert.Equal(@"test(\w+)", constraint.Constraint)); - } + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(\d+)", constraint.Constraint), + constraint => Assert.Empty(constraint.Constraint), + constraint => Assert.Equal(@"test(\w+)", constraint.Constraint)); + } - [Theory] - [InlineData("=")] - [InlineData("+=")] - [InlineData(">= || <= || ==")] - public void ParseRouteParameter_WithDefaultValue_ContainingDelimiter(string defaultValue) - { - // Arrange & Act - var templatePart = ParseParameter($"comparison-operator:length(6)={defaultValue}"); + [Theory] + [InlineData("=")] + [InlineData("+=")] + [InlineData(">= || <= || ==")] + public void ParseRouteParameter_WithDefaultValue_ContainingDelimiter(string defaultValue) + { + // Arrange & Act + var templatePart = ParseParameter($"comparison-operator:length(6)={defaultValue}"); - // Assert - Assert.Equal("comparison-operator", templatePart.Name); - Assert.Equal(defaultValue, templatePart.DefaultValue); + // Assert + Assert.Equal("comparison-operator", templatePart.Name); + Assert.Equal(defaultValue, templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("length(6)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("length(6)", constraint.Constraint); + } - [Fact] - public void ParseRouteTemplate_ConstraintsDefaultsAndOptionalsInMultipleSections_ParsedCorrectly() - { - // Arrange & Act - var template = ParseRouteTemplate(@"some/url-{p1:int:test(3)=hello}/{p2=abc}/{p3?}"); - - // Assert - var parameters = template.Parameters.ToArray(); - - var param1 = parameters[0]; - Assert.Equal("p1", param1.Name); - Assert.Equal("hello", param1.DefaultValue); - Assert.False(param1.IsOptional); - - Assert.Collection(param1.InlineConstraints, - constraint => Assert.Equal("int", constraint.Constraint), - constraint => Assert.Equal("test(3)", constraint.Constraint) - ); - - var param2 = parameters[1]; - Assert.Equal("p2", param2.Name); - Assert.Equal("abc", param2.DefaultValue); - Assert.False(param2.IsOptional); - - var param3 = parameters[2]; - Assert.Equal("p3", param3.Name); - Assert.True(param3.IsOptional); - } + [Fact] + public void ParseRouteTemplate_ConstraintsDefaultsAndOptionalsInMultipleSections_ParsedCorrectly() + { + // Arrange & Act + var template = ParseRouteTemplate(@"some/url-{p1:int:test(3)=hello}/{p2=abc}/{p3?}"); + + // Assert + var parameters = template.Parameters.ToArray(); + + var param1 = parameters[0]; + Assert.Equal("p1", param1.Name); + Assert.Equal("hello", param1.DefaultValue); + Assert.False(param1.IsOptional); + + Assert.Collection(param1.InlineConstraints, + constraint => Assert.Equal("int", constraint.Constraint), + constraint => Assert.Equal("test(3)", constraint.Constraint) + ); + + var param2 = parameters[1]; + Assert.Equal("p2", param2.Name); + Assert.Equal("abc", param2.DefaultValue); + Assert.False(param2.IsOptional); + + var param3 = parameters[2]; + Assert.Equal("p3", param3.Name); + Assert.True(param3.IsOptional); + } - [Fact] - public void ParseRouteParameter_NoTokens_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter("world"); + [Fact] + public void ParseRouteParameter_NoTokens_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter("world"); - // Assert - Assert.Equal("world", templatePart.Name); - } + // Assert + Assert.Equal("world", templatePart.Name); + } - [Fact] - public void ParseRouteParameter_ParamDefault_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter("param=world"); + [Fact] + public void ParseRouteParameter_ParamDefault_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter("param=world"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("world", templatePart.DefaultValue); - } + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("world", templatePart.DefaultValue); + } - [Fact] - public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_ClosingBraceIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\})"); + [Fact] + public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_ClosingBraceIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\})"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\})", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\})", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\})=wer"); + [Fact] + public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\})=wer"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - Assert.Equal("wer", templatePart.DefaultValue); + Assert.Equal("wer", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\})", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\})", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithClosingParenInPattern_ClosingParenIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\))"); + [Fact] + public void ParseRouteParameter_ConstraintWithClosingParenInPattern_ClosingParenIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\))"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\))", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\))", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithClosingParenInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\))=fsd"); + [Fact] + public void ParseRouteParameter_ConstraintWithClosingParenInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\))=fsd"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - Assert.Equal("fsd", templatePart.DefaultValue); + Assert.Equal("fsd", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\))", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\))", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithColonInPattern_ColonIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(:)"); + [Fact] + public void ParseRouteParameter_ConstraintWithColonInPattern_ColonIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(:)"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(:)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(:)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithColonInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(:)=mnf"); + [Fact] + public void ParseRouteParameter_ConstraintWithColonInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(:)=mnf"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - Assert.Equal("mnf", templatePart.DefaultValue); + Assert.Equal("mnf", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(:)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(:)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithColonsInPattern_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(a:b:c)"); + [Fact] + public void ParseRouteParameter_ConstraintWithColonsInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(a:b:c)"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(a:b:c)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(a:b:c)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithColonInParamName_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@":param:test=12"); + [Fact] + public void ParseRouteParameter_ConstraintWithColonInParamName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@":param:test=12"); - // Assert - Assert.Equal(":param", templatePart.Name); + // Assert + Assert.Equal(":param", templatePart.Name); - Assert.Equal("12", templatePart.DefaultValue); + Assert.Equal("12", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("test", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithTwoColonInParamName_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@":param::test=12"); + [Fact] + public void ParseRouteParameter_ConstraintWithTwoColonInParamName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@":param::test=12"); - // Assert - Assert.Equal(":param", templatePart.Name); + // Assert + Assert.Equal(":param", templatePart.Name); - Assert.Equal("12", templatePart.DefaultValue); + Assert.Equal("12", templatePart.DefaultValue); - Assert.Collection(templatePart.InlineConstraints, - constraint => Assert.Empty(constraint.Constraint), - constraint => Assert.Equal("test", constraint.Constraint)); - } + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Empty(constraint.Constraint), + constraint => Assert.Equal("test", constraint.Constraint)); + } - [Fact] - public void ParseRouteParameter_EmptyConstraint_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@":param:test:"); + [Fact] + public void ParseRouteParameter_EmptyConstraint_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@":param:test:"); - // Assert - Assert.Equal(":param", templatePart.Name); + // Assert + Assert.Equal(":param", templatePart.Name); - Assert.Collection(templatePart.InlineConstraints, - constraint => Assert.Equal("test", constraint.Constraint), - constraint => Assert.Empty(constraint.Constraint)); - } + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal("test", constraint.Constraint), + constraint => Assert.Empty(constraint.Constraint)); + } - [Fact] - public void ParseRouteParameter_ConstraintWithCommaInPattern_PatternIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\w,\w)"); + [Fact] + public void ParseRouteParameter_ConstraintWithCommaInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\w,\w)"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\w,\w)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\w,\w)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithCommaInName_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"par,am:test(\w)"); + [Fact] + public void ParseRouteParameter_ConstraintWithCommaInName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par,am:test(\w)"); - // Assert - Assert.Equal("par,am", templatePart.Name); + // Assert + Assert.Equal("par,am", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\w)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\w)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithCommaInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\w,\w)=jsd"); + [Fact] + public void ParseRouteParameter_ConstraintWithCommaInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\w,\w)=jsd"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - Assert.Equal("jsd", templatePart.DefaultValue); + Assert.Equal("jsd", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\w,\w)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\w,\w)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithEqualsFollowedByQuestionMark_PatternIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:int=?"); + [Fact] + public void ParseRouteParameter_ConstraintWithEqualsFollowedByQuestionMark_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:int=?"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("", templatePart.DefaultValue); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("", templatePart.DefaultValue); - Assert.True(templatePart.IsOptional); + Assert.True(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("int", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("int", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_PatternIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(=)"); + [Fact] + public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(=)"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Null(templatePart.DefaultValue); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("test(=)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test(=)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_EqualsSignInDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param=test=bar"); + [Fact] + public void ParseRouteParameter_EqualsSignInDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param=test=bar"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("test=bar", templatePart.DefaultValue); - } + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("test=bar", templatePart.DefaultValue); + } - [Fact] - public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(a==b)"); + [Fact] + public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(a==b)"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Null(templatePart.DefaultValue); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("test(a==b)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test(a==b)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(a==b)=dvds"); + [Fact] + public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(a==b)=dvds"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("dvds", templatePart.DefaultValue); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("dvds", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("test(a==b)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test(a==b)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_EqualEqualSignInName_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"par==am:test=dvds"); + [Fact] + public void ParseRouteParameter_EqualEqualSignInName_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par==am:test=dvds"); - // Assert - Assert.Equal("par", templatePart.Name); - Assert.Equal("=am:test=dvds", templatePart.DefaultValue); - } + // Assert + Assert.Equal("par", templatePart.Name); + Assert.Equal("=am:test=dvds", templatePart.DefaultValue); + } - [Fact] - public void ParseRouteParameter_EqualEqualSignInDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test==dvds"); + [Fact] + public void ParseRouteParameter_EqualEqualSignInDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test==dvds"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("=dvds", templatePart.DefaultValue); - } + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("=dvds", templatePart.DefaultValue); + } - [Fact] - public void ParseRouteParameter_DefaultValueWithColonAndParens_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"par=am:test(asd)"); + [Fact] + public void ParseRouteParameter_DefaultValueWithColonAndParens_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par=am:test(asd)"); - // Assert - Assert.Equal("par", templatePart.Name); - Assert.Equal("am:test(asd)", templatePart.DefaultValue); - } + // Assert + Assert.Equal("par", templatePart.Name); + Assert.Equal("am:test(asd)", templatePart.DefaultValue); + } - [Fact] - public void ParseRouteParameter_DefaultValueWithEqualsSignIn_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"par=test(am):est=asd"); + [Fact] + public void ParseRouteParameter_DefaultValueWithEqualsSignIn_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par=test(am):est=asd"); - // Assert - Assert.Equal("par", templatePart.Name); - Assert.Equal("test(am):est=asd", templatePart.DefaultValue); - } + // Assert + Assert.Equal("par", templatePart.Name); + Assert.Equal("test(am):est=asd", templatePart.DefaultValue); + } - [Fact] - public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(=)=sds"); + [Fact] + public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(=)=sds"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("sds", templatePart.DefaultValue); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("sds", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("test(=)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test(=)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_PatternIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\{)"); + [Fact] + public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\{)"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\{)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\{)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithOpenBraceInName_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"par{am:test(\sd)"); + [Fact] + public void ParseRouteParameter_ConstraintWithOpenBraceInName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par{am:test(\sd)"); - // Assert - Assert.Equal("par{am", templatePart.Name); + // Assert + Assert.Equal("par{am", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\sd)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\sd)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\{)=xvc"); + [Fact] + public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\{)=xvc"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - Assert.Equal("xvc", templatePart.DefaultValue); + Assert.Equal("xvc", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\{)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\{)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithOpenParenInName_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"par(am:test(\()"); + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenInName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par(am:test(\()"); - // Assert - Assert.Equal("par(am", templatePart.Name); + // Assert + Assert.Equal("par(am", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\()", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\()", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithOpenParenInPattern_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\()"); + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\()"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\()", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\()", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithOpenParenNoCloseParen_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(#$%"); + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenNoCloseParen_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(#$%"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal("test(#$%", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal("test(#$%", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithOpenParenAndColon_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(#:test1"); + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenAndColon_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(#:test1"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - Assert.Collection(templatePart.InlineConstraints, - constraint => Assert.Equal(@"test(#", constraint.Constraint), - constraint => Assert.Equal(@"test1", constraint.Constraint)); - } + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(#", constraint.Constraint), + constraint => Assert.Equal(@"test1", constraint.Constraint)); + } - [Fact] - public void ParseRouteParameter_ConstraintWithOpenParenAndColonWithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(abc:somevalue):name(test1:differentname=default-value"); + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenAndColonWithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(abc:somevalue):name(test1:differentname=default-value"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("default-value", templatePart.DefaultValue); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("default-value", templatePart.DefaultValue); - Assert.Collection(templatePart.InlineConstraints, - constraint => Assert.Equal(@"test(abc:somevalue)", constraint.Constraint), - constraint => Assert.Equal(@"name(test1", constraint.Constraint), - constraint => Assert.Equal(@"differentname", constraint.Constraint)); - } + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(abc:somevalue)", constraint.Constraint), + constraint => Assert.Equal(@"name(test1", constraint.Constraint), + constraint => Assert.Equal(@"differentname", constraint.Constraint)); + } - [Fact] - public void ParseRouteParameter_ConstraintWithOpenParenAndDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(constraintvalue=test1"); + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenAndDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(constraintvalue=test1"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("test1", templatePart.DefaultValue); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("test1", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(constraintvalue", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(constraintvalue", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithOpenParenInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\()=djk"); + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\()=djk"); - // Assert - Assert.Equal("param", templatePart.Name); + // Assert + Assert.Equal("param", templatePart.Name); - Assert.Equal("djk", templatePart.DefaultValue); + Assert.Equal("djk", templatePart.DefaultValue); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\()", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\()", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_PatternIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\?)"); + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\?)"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Null(templatePart.DefaultValue); - Assert.False(templatePart.IsOptional); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\?)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\?)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_PatternIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\?)?"); + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\?)?"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Null(templatePart.DefaultValue); - Assert.True(templatePart.IsOptional); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.True(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\?)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\?)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\?)=sdf"); + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\?)=sdf"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("sdf", templatePart.DefaultValue); - Assert.False(templatePart.IsOptional); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("sdf", templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\?)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\?)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\?)=sdf?"); + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\?)=sdf?"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("sdf", templatePart.DefaultValue); - Assert.True(templatePart.IsOptional); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("sdf", templatePart.DefaultValue); + Assert.True(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\?)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\?)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithQuestionMarkInName_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"par?am:test(\?)"); + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par?am:test(\?)"); - // Assert - Assert.Equal("par?am", templatePart.Name); - Assert.Null(templatePart.DefaultValue); - Assert.False(templatePart.IsOptional); + // Assert + Assert.Equal("par?am", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(\?)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(\?)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithClosedParenAndColonInPattern_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(#):$)"); + [Fact] + public void ParseRouteParameter_ConstraintWithClosedParenAndColonInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(#):$)"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Null(templatePart.DefaultValue); - Assert.False(templatePart.IsOptional); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); - Assert.Collection(templatePart.InlineConstraints, - constraint => Assert.Equal(@"test(#)", constraint.Constraint), - constraint => Assert.Equal(@"$)", constraint.Constraint)); - } + Assert.Collection(templatePart.InlineConstraints, + constraint => Assert.Equal(@"test(#)", constraint.Constraint), + constraint => Assert.Equal(@"$)", constraint.Constraint)); + } - [Fact] - public void ParseRouteParameter_ConstraintWithColonAndClosedParenInPattern_PatternIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(#:)$)"); + [Fact] + public void ParseRouteParameter_ConstraintWithColonAndClosedParenInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(#:)$)"); - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Null(templatePart.DefaultValue); - Assert.False(templatePart.IsOptional); + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"test(#:)$)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"test(#:)$)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ContainingMultipleUnclosedParenthesisInConstraint() - { - // Arrange & Act - var templatePart = ParseParameter(@"foo:regex(\\(\\(\\(\\()"); + [Fact] + public void ParseRouteParameter_ContainingMultipleUnclosedParenthesisInConstraint() + { + // Arrange & Act + var templatePart = ParseParameter(@"foo:regex(\\(\\(\\(\\()"); - // Assert - Assert.Equal("foo", templatePart.Name); - Assert.Null(templatePart.DefaultValue); - Assert.False(templatePart.IsOptional); + // Assert + Assert.Equal("foo", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"regex(\\(\\(\\(\\()", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"regex(\\(\\(\\(\\()", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithBraces_PatternIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)"); // ssn + [Fact] + public void ParseRouteParameter_ConstraintWithBraces_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)"); // ssn - // Assert - Assert.Equal("p1", templatePart.Name); - Assert.Null(templatePart.DefaultValue); - Assert.False(templatePart.IsOptional); + // Assert + Assert.Equal("p1", templatePart.Name); + Assert.Null(templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Constraint); + } - [Fact] - public void ParseRouteParameter_ConstraintWithBraces_WithDefaultValue() - { - // Arrange & Act - var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)=123-456-7890"); // ssn + [Fact] + public void ParseRouteParameter_ConstraintWithBraces_WithDefaultValue() + { + // Arrange & Act + var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)=123-456-7890"); // ssn - // Assert - Assert.Equal("p1", templatePart.Name); - Assert.Equal("123-456-7890", templatePart.DefaultValue); - Assert.False(templatePart.IsOptional); + // Assert + Assert.Equal("p1", templatePart.Name); + Assert.Equal("123-456-7890", templatePart.DefaultValue); + Assert.False(templatePart.IsOptional); - var constraint = Assert.Single(templatePart.InlineConstraints); - Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Constraint); - } + var constraint = Assert.Single(templatePart.InlineConstraints); + Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Constraint); + } - [Theory] - [InlineData("", "")] - [InlineData("?", "")] - [InlineData("*", "")] - [InlineData(" ", " ")] - [InlineData("\t", "\t")] - [InlineData("#!@#$%Q@#@%", "#!@#$%Q@#@%")] - [InlineData(",,,", ",,,")] - public void ParseRouteParameter_ParameterWithoutInlineConstraint_ReturnsTemplatePartWithEmptyInlineValues( - string parameter, - string expectedParameterName) - { - // Arrange & Act - var templatePart = ParseParameter(parameter); + [Theory] + [InlineData("", "")] + [InlineData("?", "")] + [InlineData("*", "")] + [InlineData(" ", " ")] + [InlineData("\t", "\t")] + [InlineData("#!@#$%Q@#@%", "#!@#$%Q@#@%")] + [InlineData(",,,", ",,,")] + public void ParseRouteParameter_ParameterWithoutInlineConstraint_ReturnsTemplatePartWithEmptyInlineValues( + string parameter, + string expectedParameterName) + { + // Arrange & Act + var templatePart = ParseParameter(parameter); - // Assert - Assert.Equal(expectedParameterName, templatePart.Name); - Assert.Empty(templatePart.InlineConstraints); - Assert.Null(templatePart.DefaultValue); - } + // Assert + Assert.Equal(expectedParameterName, templatePart.Name); + Assert.Empty(templatePart.InlineConstraints); + Assert.Null(templatePart.DefaultValue); + } - private TemplatePart ParseParameter(string routeParameter) - { - var _constraintResolver = GetConstraintResolver(); - var templatePart = InlineRouteParameterParser.ParseRouteParameter(routeParameter); - return templatePart; - } + private TemplatePart ParseParameter(string routeParameter) + { + var _constraintResolver = GetConstraintResolver(); + var templatePart = InlineRouteParameterParser.ParseRouteParameter(routeParameter); + return templatePart; + } - private static RouteTemplate ParseRouteTemplate(string template) - { - var _constraintResolver = GetConstraintResolver(); - return TemplateParser.Parse(template); - } + private static RouteTemplate ParseRouteTemplate(string template) + { + var _constraintResolver = GetConstraintResolver(); + return TemplateParser.Parse(template); + } - private static IInlineConstraintResolver GetConstraintResolver() + private static IInlineConstraintResolver GetConstraintResolver() + { + var services = new ServiceCollection().AddOptions(); + services.Configure(options => + options + .ConstraintMap + .Add("test", typeof(TestRouteConstraint))); + var serviceProvider = services.BuildServiceProvider(); + var accessor = serviceProvider.GetRequiredService>(); + return new DefaultInlineConstraintResolver(accessor, serviceProvider); + } + + private class TestRouteConstraint : IRouteConstraint + { + public TestRouteConstraint(string pattern) { - var services = new ServiceCollection().AddOptions(); - services.Configure(options => - options - .ConstraintMap - .Add("test", typeof(TestRouteConstraint))); - var serviceProvider = services.BuildServiceProvider(); - var accessor = serviceProvider.GetRequiredService>(); - return new DefaultInlineConstraintResolver(accessor, serviceProvider); + Pattern = pattern; } - private class TestRouteConstraint : IRouteConstraint + public string Pattern { get; private set; } + public bool Match(HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) { - public TestRouteConstraint(string pattern) - { - Pattern = pattern; - } - - public string Pattern { get; private set; } - public bool Match(HttpContext httpContext, - IRouter route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } } } diff --git a/src/Http/Routing/test/UnitTests/Internal/DfaGraphWriterTest.cs b/src/Http/Routing/test/UnitTests/Internal/DfaGraphWriterTest.cs index cd47f54953..1b55344ba6 100644 --- a/src/Http/Routing/test/UnitTests/Internal/DfaGraphWriterTest.cs +++ b/src/Http/Routing/test/UnitTests/Internal/DfaGraphWriterTest.cs @@ -8,85 +8,84 @@ using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Microsoft.AspNetCore.Routing.Internal +namespace Microsoft.AspNetCore.Routing.Internal; + +public class DfaGraphWriterTest { - public class DfaGraphWriterTest + private DfaGraphWriter CreateGraphWriter() { - private DfaGraphWriter CreateGraphWriter() - { - ServiceCollection services = new ServiceCollection(); - services.AddLogging(); - services.AddRouting(); + ServiceCollection services = new ServiceCollection(); + services.AddLogging(); + services.AddRouting(); - return new DfaGraphWriter(services.BuildServiceProvider()); - } + return new DfaGraphWriter(services.BuildServiceProvider()); + } - [Fact] - public void Write_ExcludeNonRouteEndpoint() - { - // Arrange - var graphWriter = CreateGraphWriter(); - var writer = new StringWriter(); - var endpointsDataSource = new DefaultEndpointDataSource(new Endpoint((context) => null, EndpointMetadataCollection.Empty, string.Empty)); + [Fact] + public void Write_ExcludeNonRouteEndpoint() + { + // Arrange + var graphWriter = CreateGraphWriter(); + var writer = new StringWriter(); + var endpointsDataSource = new DefaultEndpointDataSource(new Endpoint((context) => null, EndpointMetadataCollection.Empty, string.Empty)); - // Act - graphWriter.Write(endpointsDataSource, writer); + // Act + graphWriter.Write(endpointsDataSource, writer); - // Assert - Assert.Equal(String.Join(Environment.NewLine, @"digraph DFA {", + // Assert + Assert.Equal(String.Join(Environment.NewLine, @"digraph DFA {", @"0 [label=""/""]", "}") + Environment.NewLine, writer.ToString()); - } + } - [Fact] - public void Write_ExcludeRouteEndpointWithSuppressMatchingMetadata() - { - // Arrange - var graphWriter = CreateGraphWriter(); - var writer = new StringWriter(); - var endpointsDataSource = new DefaultEndpointDataSource( - new RouteEndpoint( - (context) => null, - RoutePatternFactory.Parse("/"), - 0, - new EndpointMetadataCollection(new SuppressMatchingMetadata()), - string.Empty)); + [Fact] + public void Write_ExcludeRouteEndpointWithSuppressMatchingMetadata() + { + // Arrange + var graphWriter = CreateGraphWriter(); + var writer = new StringWriter(); + var endpointsDataSource = new DefaultEndpointDataSource( + new RouteEndpoint( + (context) => null, + RoutePatternFactory.Parse("/"), + 0, + new EndpointMetadataCollection(new SuppressMatchingMetadata()), + string.Empty)); - // Act - graphWriter.Write(endpointsDataSource, writer); + // Act + graphWriter.Write(endpointsDataSource, writer); - // Assert - Assert.Equal(String.Join(Environment.NewLine, @"digraph DFA {", + // Assert + Assert.Equal(String.Join(Environment.NewLine, @"digraph DFA {", @"0 [label=""/""]", @"}") + Environment.NewLine, writer.ToString()); - } + } - [Fact] - public void Write_IncludeRouteEndpointWithPolicy() - { - // Arrange - var graphWriter = CreateGraphWriter(); - var writer = new StringWriter(); - var endpointsDataSource = new DefaultEndpointDataSource( - new RouteEndpoint( - (context) => null, - RoutePatternFactory.Parse("/"), - 0, - new EndpointMetadataCollection(new HttpMethodMetadata(new[] { "GET" })), - string.Empty)); + [Fact] + public void Write_IncludeRouteEndpointWithPolicy() + { + // Arrange + var graphWriter = CreateGraphWriter(); + var writer = new StringWriter(); + var endpointsDataSource = new DefaultEndpointDataSource( + new RouteEndpoint( + (context) => null, + RoutePatternFactory.Parse("/"), + 0, + new EndpointMetadataCollection(new HttpMethodMetadata(new[] { "GET" })), + string.Empty)); - // Act - graphWriter.Write(endpointsDataSource, writer); + // Act + graphWriter.Write(endpointsDataSource, writer); - // Assert - var sdf = writer.ToString(); - Assert.Equal(String.Join(Environment.NewLine, @"digraph DFA {", + // Assert + var sdf = writer.ToString(); + Assert.Equal(String.Join(Environment.NewLine, @"digraph DFA {", @"0 [label=""/ HTTP: GET""]", @"1 [label=""/ HTTP: *""]", @"2 -> 0 [label=""HTTP: GET""]", @"2 -> 1 [label=""HTTP: *""]", @"2 [label=""/""]", @"}") + Environment.NewLine, sdf); - } } } diff --git a/src/Http/Routing/test/UnitTests/LinkGeneratorEndpointNameExtensionsTest.cs b/src/Http/Routing/test/UnitTests/LinkGeneratorEndpointNameExtensionsTest.cs index 82d998d241..679a19963b 100644 --- a/src/Http/Routing/test/UnitTests/LinkGeneratorEndpointNameExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/LinkGeneratorEndpointNameExtensionsTest.cs @@ -6,144 +6,143 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +// Integration tests for GetXyzByName. These are basic because important behavioral details +// are covered elsewhere. +// +// Does not cover template processing in detail, those scenarios are validated by TemplateBinderTests +// and DefaultLinkGeneratorProcessTemplateTest +// +// Does not cover the EndpointNameAddressScheme in detail. see EndpointNameAddressSchemeTest +public class LinkGeneratorEndpointNameExtensionsTest : LinkGeneratorTestBase { - // Integration tests for GetXyzByName. These are basic because important behavioral details - // are covered elsewhere. - // - // Does not cover template processing in detail, those scenarios are validated by TemplateBinderTests - // and DefaultLinkGeneratorProcessTemplateTest - // - // Does not cover the EndpointNameAddressScheme in detail. see EndpointNameAddressSchemeTest - public class LinkGeneratorEndpointNameExtensionsTest : LinkGeneratorTestBase + [Fact] + public void GetPathByName_WithHttpContext_DoesNotUseAmbientValues() { - [Fact] - public void GetPathByName_WithHttpContext_DoesNotUseAmbientValues() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), }); - - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - - var httpContext = CreateHttpContext(); - httpContext.Request.RouteValues = new RouteValueDictionary(new { p = "5", }); - httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); - - var values = new { query = "some?query", }; - - // Act - var path = linkGenerator.GetPathByName( - httpContext, - endpointName: "name2", - values, - fragment: new FragmentString("#Fragment?"), - options: new LinkOptions() { AppendTrailingSlash = true, }); - - // Assert - Assert.Null(path); - } - - [Fact] - public void GetPathByName_WithoutHttpContext_WithPathBaseAndFragment() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), }); - - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - - var values = new { p = "In?dex", query = "some?query", }; - - // Act - var path = linkGenerator.GetPathByName( - endpointName: "name2", - values, - new PathString("/Foo/Bar?encodeme?"), - new FragmentString("#Fragment?"), - new LinkOptions() { AppendTrailingSlash = true, }); - - // Assert - Assert.Equal("/Foo/Bar%3Fencodeme%3F/some%23-other-endpoint/In%3Fdex/?query=some%3Fquery#Fragment?", path); - } - - [Fact] - public void GetPathByName_WithHttpContext_WithPathBaseAndFragment() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), }); - - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - - var httpContext = CreateHttpContext(); - httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); - - var values = new { p = "In?dex", query = "some?query", }; - - // Act - var path = linkGenerator.GetPathByName( - httpContext, - endpointName: "name2", - values, - fragment: new FragmentString("#Fragment?"), - options: new LinkOptions() { AppendTrailingSlash = true, }); - - // Assert - Assert.Equal("/Foo/Bar%3Fencodeme%3F/some%23-other-endpoint/In%3Fdex/?query=some%3Fquery#Fragment?", path); - } - - [Fact] - public void GetUriByRouteValues_WithoutHttpContext_WithPathBaseAndFragment() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), }); - - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - - var values = new { p = "In?dex", query = "some?query", }; - - // Act - var path = linkGenerator.GetUriByName( - endpointName: "name2", - values, - "http", - new HostString("example.com"), - new PathString("/Foo/Bar?encodeme?"), - new FragmentString("#Fragment?"), - new LinkOptions() { AppendTrailingSlash = true, }); - - // Assert - Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/some%23-other-endpoint/In%3Fdex/?query=some%3Fquery#Fragment?", path); - } - - [Fact] - public void GetUriByName_WithHttpContext_WithPathBaseAndFragment() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), }); - - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - - var httpContext = CreateHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("example.com"); - httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); - - var values = new { p = "In?dex", query = "some?query", }; - - // Act - var uri = linkGenerator.GetUriByName( - httpContext, - endpointName: "name2", - values, - fragment: new FragmentString("#Fragment?"), - options: new LinkOptions() { AppendTrailingSlash = true, }); - - // Assert - Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/some%23-other-endpoint/In%3Fdex/?query=some%3Fquery#Fragment?", uri); - } + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var httpContext = CreateHttpContext(); + httpContext.Request.RouteValues = new RouteValueDictionary(new { p = "5", }); + httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); + + var values = new { query = "some?query", }; + + // Act + var path = linkGenerator.GetPathByName( + httpContext, + endpointName: "name2", + values, + fragment: new FragmentString("#Fragment?"), + options: new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Null(path); + } + + [Fact] + public void GetPathByName_WithoutHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var values = new { p = "In?dex", query = "some?query", }; + + // Act + var path = linkGenerator.GetPathByName( + endpointName: "name2", + values, + new PathString("/Foo/Bar?encodeme?"), + new FragmentString("#Fragment?"), + new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("/Foo/Bar%3Fencodeme%3F/some%23-other-endpoint/In%3Fdex/?query=some%3Fquery#Fragment?", path); + } + + [Fact] + public void GetPathByName_WithHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var httpContext = CreateHttpContext(); + httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); + + var values = new { p = "In?dex", query = "some?query", }; + + // Act + var path = linkGenerator.GetPathByName( + httpContext, + endpointName: "name2", + values, + fragment: new FragmentString("#Fragment?"), + options: new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("/Foo/Bar%3Fencodeme%3F/some%23-other-endpoint/In%3Fdex/?query=some%3Fquery#Fragment?", path); + } + + [Fact] + public void GetUriByRouteValues_WithoutHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var values = new { p = "In?dex", query = "some?query", }; + + // Act + var path = linkGenerator.GetUriByName( + endpointName: "name2", + values, + "http", + new HostString("example.com"), + new PathString("/Foo/Bar?encodeme?"), + new FragmentString("#Fragment?"), + new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/some%23-other-endpoint/In%3Fdex/?query=some%3Fquery#Fragment?", path); + } + + [Fact] + public void GetUriByName_WithHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("some-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name1"), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("some#-other-endpoint/{p}", metadata: new[] { new EndpointNameMetadata("name2"), }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var httpContext = CreateHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("example.com"); + httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); + + var values = new { p = "In?dex", query = "some?query", }; + + // Act + var uri = linkGenerator.GetUriByName( + httpContext, + endpointName: "name2", + values, + fragment: new FragmentString("#Fragment?"), + options: new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/some%23-other-endpoint/In%3Fdex/?query=some%3Fquery#Fragment?", uri); } } diff --git a/src/Http/Routing/test/UnitTests/LinkGeneratorIntegrationTest.cs b/src/Http/Routing/test/UnitTests/LinkGeneratorIntegrationTest.cs index 056d644961..91695aed82 100644 --- a/src/Http/Routing/test/UnitTests/LinkGeneratorIntegrationTest.cs +++ b/src/Http/Routing/test/UnitTests/LinkGeneratorIntegrationTest.cs @@ -1,23 +1,23 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing.Patterns; -using System.Collections.Generic; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +// This is a set of integration tests that are similar to a typical MVC configuration. +// +// We're doing this here because it's relatively expensive to test these scenarios +// inside MVC - it requires creating actual controllers and pages. +public class LinkGeneratorIntegrationTest : LinkGeneratorTestBase { - // This is a set of integration tests that are similar to a typical MVC configuration. - // - // We're doing this here because it's relatively expensive to test these scenarios - // inside MVC - it requires creating actual controllers and pages. - public class LinkGeneratorIntegrationTest : LinkGeneratorTestBase + public LinkGeneratorIntegrationTest() { - public LinkGeneratorIntegrationTest() - { - var endpoints = new List() + var endpoints = new List() { // Attribute routed endpoint 1 EndpointFactory.CreateRouteEndpoint( @@ -202,513 +202,512 @@ namespace Microsoft.AspNetCore.Routing metadata: new object[] { new SuppressLinkGenerationMetadata(), }), }; - Endpoints = endpoints; - LinkGenerator = CreateLinkGenerator(endpoints.ToArray()); - } + Endpoints = endpoints; + LinkGenerator = CreateLinkGenerator(endpoints.ToArray()); + } - private IReadOnlyList Endpoints { get; } + private IReadOnlyList Endpoints { get; } - private LinkGenerator LinkGenerator { get; } + private LinkGenerator LinkGenerator { get; } - #region Without ambient values (simple cases) + #region Without ambient values (simple cases) - [Fact] - public void GetPathByAddress_LinkToAttributedAction_GeneratesPath() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { controller = "Pets", action = "GetById", id = "17", }; - var ambientValues = new { }; - var address = CreateAddress(values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/api/Pets/17", path); - } - - [Fact] - public void GetPathByAddress_LinkToConventionalAction_GeneratesPath() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { controller = "Home", action = "Index", }; - var ambientValues = new { }; - var address = CreateAddress(values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/", path); - } - - [Fact] - public void GetPathByAddress_LinkToConventionalActionInArea_GeneratesPath() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { area = "Admin", controller = "Users", action = "Add", }; - var ambientValues = new { }; - var address = CreateAddress(values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/Admin/Users/Add", path); - } - - [Fact] - public void GetPathByAddress_LinkToConventionalRoute_GeneratesPath() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { controller = "Store", id = "17", }; - var ambientValues = new { }; - var address = CreateAddress(routeName: "custom", values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/api/Store/17", path); - } - - [Fact] - public void GetPathByAddress_LinkToPage_GeneratesPath() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { page = "/Pages/Index", }; - var ambientValues = new { }; - var address = CreateAddress(values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/Pages", path); - } - - [Fact] - public void GetPathByAddress_LinkToPageInArea_GeneratesPath() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { area = "Admin", page = "/Pages/Index", }; - var ambientValues = new { }; - var address = CreateAddress(values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/Admin/Pages", path); - } - - [Fact] - public void GetPathByAddress_LinkToNonExistentAction_GeneratesPath() - { - // Arrange - var httpContext = CreateHttpContext(); + [Fact] + public void GetPathByAddress_LinkToAttributedAction_GeneratesPath() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { controller = "Pets", action = "GetById", id = "17", }; + var ambientValues = new { }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/api/Pets/17", path); + } + + [Fact] + public void GetPathByAddress_LinkToConventionalAction_GeneratesPath() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { controller = "Home", action = "Index", }; + var ambientValues = new { }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/", path); + } + + [Fact] + public void GetPathByAddress_LinkToConventionalActionInArea_GeneratesPath() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { area = "Admin", controller = "Users", action = "Add", }; + var ambientValues = new { }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/Admin/Users/Add", path); + } + + [Fact] + public void GetPathByAddress_LinkToConventionalRoute_GeneratesPath() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { controller = "Store", id = "17", }; + var ambientValues = new { }; + var address = CreateAddress(routeName: "custom", values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/api/Store/17", path); + } - var values = new { controller = "Home", action = "Fake", id = "17", }; - var ambientValues = new { }; - var address = CreateAddress(values: values, ambientValues: ambientValues); + [Fact] + public void GetPathByAddress_LinkToPage_GeneratesPath() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { page = "/Pages/Index", }; + var ambientValues = new { }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/Pages", path); + } - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); + [Fact] + public void GetPathByAddress_LinkToPageInArea_GeneratesPath() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { area = "Admin", page = "/Pages/Index", }; + var ambientValues = new { }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/Admin/Pages", path); + } - // Assert - Assert.Equal("/Home/Fake/17", path); - } + [Fact] + public void GetPathByAddress_LinkToNonExistentAction_GeneratesPath() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { controller = "Home", action = "Fake", id = "17", }; + var ambientValues = new { }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/Home/Fake/17", path); + } - #endregion + #endregion - #region With ambient values + #region With ambient values - [Fact] - public void GetPathByAddress_LinkToAttributedAction_FromSameAction_KeepsAmbientValues() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { controller = "Pets", action = "GetById", }; - var ambientValues = new { controller = "Pets", action = "GetById", id = "17", }; - var address = CreateAddress(values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/api/Pets/17", path); - } - - [Fact] - public void GetPathByAddress_LinkToAttributedAction_FromAnotherAction_DiscardsAmbientValues() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { controller = "Pets", action = "GetById", }; - var ambientValues = new { controller = "Pets", action = "Update", id = "17", }; - var address = CreateAddress(values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/Pets/GetById", path); - } - - [Fact] - public void GetPathByAddress_LinkToAttributedAction_FromPage_DiscardsAmbientValues() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { controller = "Pets", action = "GetById", }; - var ambientValues = new { page = "/Pages/Help", id = "17", }; - var address = CreateAddress(values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/Pets/GetById", path); - } - - [Fact] - public void GetPathByAddress_LinkToConventionalAction_FromSameAction_KeepsAmbientValues() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { controller = "Home", action = "Index", }; - var ambientValues = new { controller = "Home", action = "Index", id = "17", }; - var address = CreateAddress(values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/Home/Index/17", path); - } - - [Fact] - public void GetPathByAddress_LinkToConventionalAction_FromAnotherAction_DiscardsAmbientValues() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { controller = "Home", action = "Index", }; - var ambientValues = new { controller = "Pets", action = "Update", id = "17", }; - var address = CreateAddress(values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/", path); - } - - [Fact] - public void GetPathByAddress_LinkToConventionalAction_FromPage_DiscardsAmbientValues() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { controller = "Home", action = "Index", }; - var ambientValues = new { page = "/Pages/Help", id = "17", }; - var address = CreateAddress(values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/", path); - } - - [Fact] - public void GetPathByAddress_LinkToNonExistentConventionalAction_FromAnotherAction_DiscardsAmbientValues() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { controller = "Home", action = "Index11", }; - var ambientValues = new { controller = "Pets", action = "Update", id = "17", }; - var address = CreateAddress(values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/Home/Index11", path); - } - - [Fact] - public void GetPathByAddress_LinkToNonExistentAreaAction_FromAnotherAction_DiscardsAmbientValues() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { area = "Admin", controller = "Home", action = "Index11", }; - var ambientValues = new { controller = "Pets", action = "Update", id = "17", }; - var address = CreateAddress(values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/Admin/Home/Index11", path); - } - - [Fact] - public void GetPathByAddress_LinkToConventionalRoute_FromAction_DiscardsAmbientValues() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { controller = "Store", }; - var ambientValues = new { controller = "Home", action = "Index", id = "17", }; - var address = CreateAddress(routeName: "custom", values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/api/Store", path); - } - - [Fact] - public void GetPathByAddress_LinkToConventionalRoute_WithAmbientValues_GeneratesPath() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { controller = "Store", id = "17", }; - var ambientValues = new { controller = "Store", }; - var address = CreateAddress(routeName: "custom", values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/api/Store/17", path); - } - - [Fact] - public void GetPathByAddress_LinkToConventionalRouteWithoutSharedAmbientValues_WithAmbientValues_GeneratesPath() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { custom2 = "17", }; - var ambientValues = new { controller = "Store", }; - var address = CreateAddress(routeName: "custom2", values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/api/Foo/17", path); - } - - [Fact] - public void GetPathByAddress_LinkToPage_FromSamePage_KeepsAmbientValues() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { page = "/Pages/Help", }; - var ambientValues = new { page = "/Pages/Help", id = "17", }; - var address = CreateAddress(values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/Pages/Help/17", path); - } - - [Fact] - public void GetPathByAddress_LinkToPage_FromAction_DiscardsAmbientValues() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { page = "/Pages/Help", }; - var ambientValues = new { controller = "Pets", action = "Update", id = "17", }; - var address = CreateAddress(values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/Pages/Help", path); - } - - [Fact] - public void GetPathByAddress_LinkToPage_FromAnotherPage_DiscardsAmbientValues() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { page = "/Pages/Help", }; - var ambientValues = new { page = "/Pages/About", id = "17", }; - var address = CreateAddress(values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/Pages/Help", path); - } - - [Fact] - public void GetPathByAddress_LinkToNonExistentPage_FromAction_MatchesActionConventionalRoute() - { - // Arrange - var httpContext = CreateHttpContext(); - - var values = new { page = "/Pages/Help2", }; - var ambientValues = new { controller = "Pets", action = "Update", id = "17", }; - var address = CreateAddress(values: values, ambientValues: ambientValues); - - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); - - // Assert - Assert.Equal("/Pets/Update?page=%2FPages%2FHelp2", path); - } - - [Fact] - public void GetPathByAddress_LinkToPageInSameArea_FromAction_UsingAreaAmbientValue() - { - // Arrange - var httpContext = CreateHttpContext(); + [Fact] + public void GetPathByAddress_LinkToAttributedAction_FromSameAction_KeepsAmbientValues() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { controller = "Pets", action = "GetById", }; + var ambientValues = new { controller = "Pets", action = "GetById", id = "17", }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/api/Pets/17", path); + } + + [Fact] + public void GetPathByAddress_LinkToAttributedAction_FromAnotherAction_DiscardsAmbientValues() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { controller = "Pets", action = "GetById", }; + var ambientValues = new { controller = "Pets", action = "Update", id = "17", }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/Pets/GetById", path); + } + + [Fact] + public void GetPathByAddress_LinkToAttributedAction_FromPage_DiscardsAmbientValues() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { controller = "Pets", action = "GetById", }; + var ambientValues = new { page = "/Pages/Help", id = "17", }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/Pets/GetById", path); + } + + [Fact] + public void GetPathByAddress_LinkToConventionalAction_FromSameAction_KeepsAmbientValues() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { controller = "Home", action = "Index", }; + var ambientValues = new { controller = "Home", action = "Index", id = "17", }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/Home/Index/17", path); + } + + [Fact] + public void GetPathByAddress_LinkToConventionalAction_FromAnotherAction_DiscardsAmbientValues() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { controller = "Home", action = "Index", }; + var ambientValues = new { controller = "Pets", action = "Update", id = "17", }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/", path); + } + + [Fact] + public void GetPathByAddress_LinkToConventionalAction_FromPage_DiscardsAmbientValues() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { controller = "Home", action = "Index", }; + var ambientValues = new { page = "/Pages/Help", id = "17", }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/", path); + } + + [Fact] + public void GetPathByAddress_LinkToNonExistentConventionalAction_FromAnotherAction_DiscardsAmbientValues() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { controller = "Home", action = "Index11", }; + var ambientValues = new { controller = "Pets", action = "Update", id = "17", }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/Home/Index11", path); + } + + [Fact] + public void GetPathByAddress_LinkToNonExistentAreaAction_FromAnotherAction_DiscardsAmbientValues() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { area = "Admin", controller = "Home", action = "Index11", }; + var ambientValues = new { controller = "Pets", action = "Update", id = "17", }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/Admin/Home/Index11", path); + } + + [Fact] + public void GetPathByAddress_LinkToConventionalRoute_FromAction_DiscardsAmbientValues() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { controller = "Store", }; + var ambientValues = new { controller = "Home", action = "Index", id = "17", }; + var address = CreateAddress(routeName: "custom", values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/api/Store", path); + } - var values = new { page = "/Pages/Index", }; - var ambientValues = new { area = "Admin", controller = "Users", action = "Add", }; - var address = CreateAddress(values: values, ambientValues: ambientValues); + [Fact] + public void GetPathByAddress_LinkToConventionalRoute_WithAmbientValues_GeneratesPath() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { controller = "Store", id = "17", }; + var ambientValues = new { controller = "Store", }; + var address = CreateAddress(routeName: "custom", values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/api/Store/17", path); + } - // Act - var path = LinkGenerator.GetPathByAddress( - httpContext, - address, - address.ExplicitValues, - address.AmbientValues); + [Fact] + public void GetPathByAddress_LinkToConventionalRouteWithoutSharedAmbientValues_WithAmbientValues_GeneratesPath() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { custom2 = "17", }; + var ambientValues = new { controller = "Store", }; + var address = CreateAddress(routeName: "custom2", values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/api/Foo/17", path); + } - // Assert - Assert.Equal("/Admin/Pages", path); - } + [Fact] + public void GetPathByAddress_LinkToPage_FromSamePage_KeepsAmbientValues() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { page = "/Pages/Help", }; + var ambientValues = new { page = "/Pages/Help", id = "17", }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/Pages/Help/17", path); + } - #endregion + [Fact] + public void GetPathByAddress_LinkToPage_FromAction_DiscardsAmbientValues() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { page = "/Pages/Help", }; + var ambientValues = new { controller = "Pets", action = "Update", id = "17", }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/Pages/Help", path); + } - private static RouteValuesAddress CreateAddress(string routeName = null, object values = null, object ambientValues = null) + [Fact] + public void GetPathByAddress_LinkToPage_FromAnotherPage_DiscardsAmbientValues() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { page = "/Pages/Help", }; + var ambientValues = new { page = "/Pages/About", id = "17", }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/Pages/Help", path); + } + + [Fact] + public void GetPathByAddress_LinkToNonExistentPage_FromAction_MatchesActionConventionalRoute() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { page = "/Pages/Help2", }; + var ambientValues = new { controller = "Pets", action = "Update", id = "17", }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/Pets/Update?page=%2FPages%2FHelp2", path); + } + + [Fact] + public void GetPathByAddress_LinkToPageInSameArea_FromAction_UsingAreaAmbientValue() + { + // Arrange + var httpContext = CreateHttpContext(); + + var values = new { page = "/Pages/Index", }; + var ambientValues = new { area = "Admin", controller = "Users", action = "Add", }; + var address = CreateAddress(values: values, ambientValues: ambientValues); + + // Act + var path = LinkGenerator.GetPathByAddress( + httpContext, + address, + address.ExplicitValues, + address.AmbientValues); + + // Assert + Assert.Equal("/Admin/Pages", path); + } + + #endregion + + private static RouteValuesAddress CreateAddress(string routeName = null, object values = null, object ambientValues = null) + { + return new RouteValuesAddress() { - return new RouteValuesAddress() - { - RouteName = routeName, - ExplicitValues = new RouteValueDictionary(values), - AmbientValues = new RouteValueDictionary(ambientValues), - }; - } + RouteName = routeName, + ExplicitValues = new RouteValueDictionary(values), + AmbientValues = new RouteValueDictionary(ambientValues), + }; } } diff --git a/src/Http/Routing/test/UnitTests/LinkGeneratorRouteValuesAddressExtensionsTest.cs b/src/Http/Routing/test/UnitTests/LinkGeneratorRouteValuesAddressExtensionsTest.cs index 02c344b499..00e31d0ccb 100644 --- a/src/Http/Routing/test/UnitTests/LinkGeneratorRouteValuesAddressExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/LinkGeneratorRouteValuesAddressExtensionsTest.cs @@ -6,196 +6,195 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +// Integration tests for GetXyzByRouteValues. These are basic because important behavioral details +// are covered elsewhere. +// +// Does not cover template processing in detail, those scenarios are validated by TemplateBinderTests +// and DefaultLinkGeneratorProcessTemplateTest +// +// Does not cover the RouteValuesAddressScheme in detail. see RouteValuesAddressSchemeTest +public class LinkGeneratorRouteValuesAddressExtensionsTest : LinkGeneratorTestBase { - // Integration tests for GetXyzByRouteValues. These are basic because important behavioral details - // are covered elsewhere. - // - // Does not cover template processing in detail, those scenarios are validated by TemplateBinderTests - // and DefaultLinkGeneratorProcessTemplateTest - // - // Does not cover the RouteValuesAddressScheme in detail. see RouteValuesAddressSchemeTest - public class LinkGeneratorRouteValuesAddressExtensionsTest : LinkGeneratorTestBase + [Fact] + public void GetPathByRouteValues_WithHttpContext_UsesAmbientValues() { - [Fact] - public void GetPathByRouteValues_WithHttpContext_UsesAmbientValues() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint( - "Home/Index/{id}", - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint( - "Home/Index/{id?}", - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - - var httpContext = CreateHttpContext(); - httpContext.Request.RouteValues = new RouteValueDictionary(new { action = "Index", }); - httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); - - // Act - var path = linkGenerator.GetPathByRouteValues( - httpContext, - routeName: null, - values: new RouteValueDictionary(new { controller = "Home", query = "some?query" }), - fragment: new FragmentString("#Fragment?"), - options: new LinkOptions() { AppendTrailingSlash = true, }); - - // Assert - Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/Index/?query=some%3Fquery#Fragment?", path); - } - - [Fact] - public void GetPathByRouteValues_WithoutHttpContext_WithPathBaseAndFragment() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint( - "Home/Index/{id}", - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint( - "Home/Index/{id?}", - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - - // Act - var path = linkGenerator.GetPathByRouteValues( - routeName: null, - values: new RouteValueDictionary(new { controller = "Home", action = "Index", query = "some?query" }), - new PathString("/Foo/Bar?encodeme?"), - new FragmentString("#Fragment?"), - new LinkOptions() { AppendTrailingSlash = true, }); - - // Assert - Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/Index/?query=some%3Fquery#Fragment?", path); - } - - [Fact] - public void GetPathByRouteValues_WithHttpContext_WithPathBaseAndFragment() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint( - "Home/Index/{id}", - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint( - "Home/Index/{id?}", - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - - var httpContext = CreateHttpContext(); - httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); - - // Act - var path = linkGenerator.GetPathByRouteValues( - httpContext, - routeName: null, - values: new RouteValueDictionary(new { controller = "Home", action = "Index", query = "some?query" }), - fragment: new FragmentString("#Fragment?"), - options: new LinkOptions() { AppendTrailingSlash = true, }); - - // Assert - Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/Index/?query=some%3Fquery#Fragment?", path); - } - - [Fact] - public void GetUriByRouteValues_WithoutHttpContext_WithPathBaseAndFragment() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint( - "Home/Index/{id}", - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint( - "Home/Index/{id?}", - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - - // Act - var path = linkGenerator.GetUriByRouteValues( - routeName: null, - values: new RouteValueDictionary(new { controller = "Home", action = "Index", query = "some?query" }), - "http", - new HostString("example.com"), - new PathString("/Foo/Bar?encodeme?"), - new FragmentString("#Fragment?"), - new LinkOptions() { AppendTrailingSlash = true, }); - - // Assert - Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/Index/?query=some%3Fquery#Fragment?", path); - } - - [Fact] - public void GetUriByRouteValues_WithHttpContext_WithPathBaseAndFragment() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint( - "Home/Index/{id}", - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint( - "Home/Index/{id?}", - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - - var httpContext = CreateHttpContext(); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("example.com"); - httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); - - // Act - var uri = linkGenerator.GetUriByRouteValues( - httpContext, - routeName: null, - values: new RouteValueDictionary(new { controller = "Home", action = "Index", query = "some?query" }), - fragment: new FragmentString("#Fragment?"), - options: new LinkOptions() { AppendTrailingSlash = true, }); - - // Assert - Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/Index/?query=some%3Fquery#Fragment?", uri); - } - - [Fact] - public void GetUriByRouteValues_WithHttpContext_CanUseAmbientValues() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint( - "Home/Index/{id}", - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint( - "Home/Index/{id?}", - defaults: new { controller = "Home", action = "Index", }, - requiredValues: new { controller = "Home", action = "Index", }); - - var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); - - var httpContext = CreateHttpContext(new { controller = "Home", }); - httpContext.Request.Scheme = "http"; - httpContext.Request.Host = new HostString("example.com"); - httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); - - // Act - var uri = linkGenerator.GetUriByRouteValues( - httpContext, - routeName: null, - values: new RouteValueDictionary(new { action = "Index", query = "some?query" }), - fragment: new FragmentString("#Fragment?"), - options: new LinkOptions() { AppendTrailingSlash = true, }); - - // Assert - Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/Index/?query=some%3Fquery#Fragment?", uri); - } + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint( + "Home/Index/{id}", + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint( + "Home/Index/{id?}", + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var httpContext = CreateHttpContext(); + httpContext.Request.RouteValues = new RouteValueDictionary(new { action = "Index", }); + httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); + + // Act + var path = linkGenerator.GetPathByRouteValues( + httpContext, + routeName: null, + values: new RouteValueDictionary(new { controller = "Home", query = "some?query" }), + fragment: new FragmentString("#Fragment?"), + options: new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/Index/?query=some%3Fquery#Fragment?", path); + } + + [Fact] + public void GetPathByRouteValues_WithoutHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint( + "Home/Index/{id}", + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint( + "Home/Index/{id?}", + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + // Act + var path = linkGenerator.GetPathByRouteValues( + routeName: null, + values: new RouteValueDictionary(new { controller = "Home", action = "Index", query = "some?query" }), + new PathString("/Foo/Bar?encodeme?"), + new FragmentString("#Fragment?"), + new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/Index/?query=some%3Fquery#Fragment?", path); + } + + [Fact] + public void GetPathByRouteValues_WithHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint( + "Home/Index/{id}", + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint( + "Home/Index/{id?}", + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var httpContext = CreateHttpContext(); + httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); + + // Act + var path = linkGenerator.GetPathByRouteValues( + httpContext, + routeName: null, + values: new RouteValueDictionary(new { controller = "Home", action = "Index", query = "some?query" }), + fragment: new FragmentString("#Fragment?"), + options: new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("/Foo/Bar%3Fencodeme%3F/Home/Index/?query=some%3Fquery#Fragment?", path); + } + + [Fact] + public void GetUriByRouteValues_WithoutHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint( + "Home/Index/{id}", + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint( + "Home/Index/{id?}", + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + // Act + var path = linkGenerator.GetUriByRouteValues( + routeName: null, + values: new RouteValueDictionary(new { controller = "Home", action = "Index", query = "some?query" }), + "http", + new HostString("example.com"), + new PathString("/Foo/Bar?encodeme?"), + new FragmentString("#Fragment?"), + new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/Index/?query=some%3Fquery#Fragment?", path); + } + + [Fact] + public void GetUriByRouteValues_WithHttpContext_WithPathBaseAndFragment() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint( + "Home/Index/{id}", + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint( + "Home/Index/{id?}", + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var httpContext = CreateHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("example.com"); + httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); + + // Act + var uri = linkGenerator.GetUriByRouteValues( + httpContext, + routeName: null, + values: new RouteValueDictionary(new { controller = "Home", action = "Index", query = "some?query" }), + fragment: new FragmentString("#Fragment?"), + options: new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/Index/?query=some%3Fquery#Fragment?", uri); + } + + [Fact] + public void GetUriByRouteValues_WithHttpContext_CanUseAmbientValues() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint( + "Home/Index/{id}", + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint( + "Home/Index/{id?}", + defaults: new { controller = "Home", action = "Index", }, + requiredValues: new { controller = "Home", action = "Index", }); + + var linkGenerator = CreateLinkGenerator(endpoint1, endpoint2); + + var httpContext = CreateHttpContext(new { controller = "Home", }); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("example.com"); + httpContext.Request.PathBase = new PathString("/Foo/Bar?encodeme?"); + + // Act + var uri = linkGenerator.GetUriByRouteValues( + httpContext, + routeName: null, + values: new RouteValueDictionary(new { action = "Index", query = "some?query" }), + fragment: new FragmentString("#Fragment?"), + options: new LinkOptions() { AppendTrailingSlash = true, }); + + // Assert + Assert.Equal("http://example.com/Foo/Bar%3Fencodeme%3F/Home/Index/?query=some%3Fquery#Fragment?", uri); } } diff --git a/src/Http/Routing/test/UnitTests/LinkGeneratorTestBase.cs b/src/Http/Routing/test/UnitTests/LinkGeneratorTestBase.cs index 4dbced25f8..16d70ba912 100644 --- a/src/Http/Routing/test/UnitTests/LinkGeneratorTestBase.cs +++ b/src/Http/Routing/test/UnitTests/LinkGeneratorTestBase.cs @@ -8,76 +8,75 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public abstract class LinkGeneratorTestBase { - public abstract class LinkGeneratorTestBase + protected HttpContext CreateHttpContext(object ambientValues = null) { - protected HttpContext CreateHttpContext(object ambientValues = null) - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.RouteValues = new RouteValueDictionary(ambientValues); - return httpContext; - } + var httpContext = new DefaultHttpContext(); + httpContext.Request.RouteValues = new RouteValueDictionary(ambientValues); + return httpContext; + } - protected ServiceCollection GetBasicServices() - { - var services = new ServiceCollection(); - services.AddOptions(); - services.AddRouting(); - services.AddLogging(); - return services; - } + protected ServiceCollection GetBasicServices() + { + var services = new ServiceCollection(); + services.AddOptions(); + services.AddRouting(); + services.AddLogging(); + return services; + } - protected virtual void AddAdditionalServices(IServiceCollection services) - { - } + protected virtual void AddAdditionalServices(IServiceCollection services) + { + } - private protected DefaultLinkGenerator CreateLinkGenerator(params Endpoint[] endpoints) - { - return CreateLinkGenerator(configureServices: null, endpoints); - } + private protected DefaultLinkGenerator CreateLinkGenerator(params Endpoint[] endpoints) + { + return CreateLinkGenerator(configureServices: null, endpoints); + } - private protected DefaultLinkGenerator CreateLinkGenerator( - Action configureServices, - params Endpoint[] endpoints) - { - return CreateLinkGenerator(configureServices, new[] { new DefaultEndpointDataSource(endpoints ?? Array.Empty()) }); - } + private protected DefaultLinkGenerator CreateLinkGenerator( + Action configureServices, + params Endpoint[] endpoints) + { + return CreateLinkGenerator(configureServices, new[] { new DefaultEndpointDataSource(endpoints ?? Array.Empty()) }); + } - private protected DefaultLinkGenerator CreateLinkGenerator(EndpointDataSource[] dataSources) - { - return CreateLinkGenerator(configureServices: null, dataSources); - } + private protected DefaultLinkGenerator CreateLinkGenerator(EndpointDataSource[] dataSources) + { + return CreateLinkGenerator(configureServices: null, dataSources); + } - private protected DefaultLinkGenerator CreateLinkGenerator( - Action configureServices, - EndpointDataSource[] dataSources) - { - var services = GetBasicServices(); - AddAdditionalServices(services); - configureServices?.Invoke(services); + private protected DefaultLinkGenerator CreateLinkGenerator( + Action configureServices, + EndpointDataSource[] dataSources) + { + var services = GetBasicServices(); + AddAdditionalServices(services); + configureServices?.Invoke(services); - services.Configure(o => + services.Configure(o => + { + if (dataSources != null) { - if (dataSources != null) + foreach (var dataSource in dataSources) { - foreach (var dataSource in dataSources) - { - o.EndpointDataSources.Add(dataSource); - } + o.EndpointDataSources.Add(dataSource); } - }); + } + }); - var serviceProvider = services.BuildServiceProvider(); - var routeOptions = serviceProvider.GetRequiredService>(); + var serviceProvider = services.BuildServiceProvider(); + var routeOptions = serviceProvider.GetRequiredService>(); - return new DefaultLinkGenerator( - new DefaultParameterPolicyFactory(routeOptions, serviceProvider), - serviceProvider.GetRequiredService(), - new CompositeEndpointDataSource(routeOptions.Value.EndpointDataSources), - routeOptions, - NullLogger.Instance, - serviceProvider); - } + return new DefaultLinkGenerator( + new DefaultParameterPolicyFactory(routeOptions, serviceProvider), + serviceProvider.GetRequiredService(), + new CompositeEndpointDataSource(routeOptions.Value.EndpointDataSources), + routeOptions, + NullLogger.Instance, + serviceProvider); } } diff --git a/src/Http/Routing/test/UnitTests/LinkParserEndpointNameExtensionsTest.cs b/src/Http/Routing/test/UnitTests/LinkParserEndpointNameExtensionsTest.cs index 096cd45fb5..b6857a24b6 100644 --- a/src/Http/Routing/test/UnitTests/LinkParserEndpointNameExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/LinkParserEndpointNameExtensionsTest.cs @@ -4,54 +4,53 @@ using Microsoft.AspNetCore.Routing.Matching; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class LinkParserEndpointNameExtensionsTest : LinkParserTestBase { - public class LinkParserEndpointNameExtensionsTest : LinkParserTestBase + [Fact] + public void ParsePathByAddresss_NoMatchingEndpoint_ReturnsNull() { - [Fact] - public void ParsePathByAddresss_NoMatchingEndpoint_ReturnsNull() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new EndpointNameMetadata("Test2"), }); + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id?}", metadata: new object[] { new EndpointNameMetadata("Test2"), }); - var parser = CreateLinkParser(endpoint); + var parser = CreateLinkParser(endpoint); - // Act - var values = parser.ParsePathByEndpointName("Test", "/Home/Index/17"); + // Act + var values = parser.ParsePathByEndpointName("Test", "/Home/Index/17"); - // Assert - Assert.Null(values); - } + // Assert + Assert.Null(values); + } - [Fact] - public void ParsePathByAddresss_HasMatches_ReturnsNullWhenParsingFails() - { - // Arrange - var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new EndpointNameMetadata("Test2"), }); - var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id2}", metadata: new object[] { new EndpointNameMetadata("Test"), }); + [Fact] + public void ParsePathByAddresss_HasMatches_ReturnsNullWhenParsingFails() + { + // Arrange + var endpoint1 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new EndpointNameMetadata("Test2"), }); + var endpoint2 = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id2}", metadata: new object[] { new EndpointNameMetadata("Test"), }); - var parser = CreateLinkParser(endpoint1, endpoint2); + var parser = CreateLinkParser(endpoint1, endpoint2); - // Act - var values = parser.ParsePathByEndpointName("Test", "/"); + // Act + var values = parser.ParsePathByEndpointName("Test", "/"); - // Assert - Assert.Null(values); - } + // Assert + Assert.Null(values); + } - [Fact] // Endpoint name does not support multiple matches - public void ParsePathByAddresss_HasMatches_ReturnsFirstSuccessfulParse() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new EndpointNameMetadata("Test"), }); + [Fact] // Endpoint name does not support multiple matches + public void ParsePathByAddresss_HasMatches_ReturnsFirstSuccessfulParse() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint("{controller}/{action}/{id}", metadata: new object[] { new EndpointNameMetadata("Test"), }); - var parser = CreateLinkParser(endpoint); + var parser = CreateLinkParser(endpoint); - // Act - var values = parser.ParsePathByEndpointName("Test", "/Home/Index/17"); + // Act + var values = parser.ParsePathByEndpointName("Test", "/Home/Index/17"); - // Assert - MatcherAssert.AssertRouteValuesEqual(new { controller = "Home", action = "Index", id = "17" }, values); - } + // Assert + MatcherAssert.AssertRouteValuesEqual(new { controller = "Home", action = "Index", id = "17" }, values); } } diff --git a/src/Http/Routing/test/UnitTests/LinkParserTestBase.cs b/src/Http/Routing/test/UnitTests/LinkParserTestBase.cs index b2c6d46b30..571ad7e324 100644 --- a/src/Http/Routing/test/UnitTests/LinkParserTestBase.cs +++ b/src/Http/Routing/test/UnitTests/LinkParserTestBase.cs @@ -8,67 +8,66 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public abstract class LinkParserTestBase { - public abstract class LinkParserTestBase + protected ServiceCollection GetBasicServices() { - protected ServiceCollection GetBasicServices() - { - var services = new ServiceCollection(); - services.AddOptions(); - services.AddRouting(); - services.AddLogging(); - return services; - } + var services = new ServiceCollection(); + services.AddOptions(); + services.AddRouting(); + services.AddLogging(); + return services; + } - protected virtual void AddAdditionalServices(IServiceCollection services) - { - } + protected virtual void AddAdditionalServices(IServiceCollection services) + { + } - private protected DefaultLinkParser CreateLinkParser(params Endpoint[] endpoints) - { - return CreateLinkParser(configureServices: null, endpoints); - } + private protected DefaultLinkParser CreateLinkParser(params Endpoint[] endpoints) + { + return CreateLinkParser(configureServices: null, endpoints); + } - private protected DefaultLinkParser CreateLinkParser( - Action configureServices, - params Endpoint[] endpoints) - { - return CreateLinkParser(configureServices, new[] { new DefaultEndpointDataSource(endpoints ?? Array.Empty()) }); - } + private protected DefaultLinkParser CreateLinkParser( + Action configureServices, + params Endpoint[] endpoints) + { + return CreateLinkParser(configureServices, new[] { new DefaultEndpointDataSource(endpoints ?? Array.Empty()) }); + } - private protected DefaultLinkParser CreateLinkParser(EndpointDataSource[] dataSources) - { - return CreateLinkParser(configureServices: null, dataSources); - } + private protected DefaultLinkParser CreateLinkParser(EndpointDataSource[] dataSources) + { + return CreateLinkParser(configureServices: null, dataSources); + } - private protected DefaultLinkParser CreateLinkParser( - Action configureServices, - EndpointDataSource[] dataSources) - { - var services = GetBasicServices(); - AddAdditionalServices(services); - configureServices?.Invoke(services); + private protected DefaultLinkParser CreateLinkParser( + Action configureServices, + EndpointDataSource[] dataSources) + { + var services = GetBasicServices(); + AddAdditionalServices(services); + configureServices?.Invoke(services); - services.Configure(o => + services.Configure(o => + { + if (dataSources != null) { - if (dataSources != null) + foreach (var dataSource in dataSources) { - foreach (var dataSource in dataSources) - { - o.EndpointDataSources.Add(dataSource); - } + o.EndpointDataSources.Add(dataSource); } - }); + } + }); - var serviceProvider = services.BuildServiceProvider(); - var routeOptions = serviceProvider.GetRequiredService>(); + var serviceProvider = services.BuildServiceProvider(); + var routeOptions = serviceProvider.GetRequiredService>(); - return new DefaultLinkParser( - new DefaultParameterPolicyFactory(routeOptions, serviceProvider), - new CompositeEndpointDataSource(routeOptions.Value.EndpointDataSources), - serviceProvider.GetRequiredService().CreateLogger(), - serviceProvider); - } + return new DefaultLinkParser( + new DefaultParameterPolicyFactory(routeOptions, serviceProvider), + new CompositeEndpointDataSource(routeOptions.Value.EndpointDataSources), + serviceProvider.GetRequiredService().CreateLogger(), + serviceProvider); } } diff --git a/src/Http/Routing/test/UnitTests/Logging/WriteContext.cs b/src/Http/Routing/test/UnitTests/Logging/WriteContext.cs index 4d971f1208..96fd8cf104 100644 --- a/src/Http/Routing/test/UnitTests/Logging/WriteContext.cs +++ b/src/Http/Routing/test/UnitTests/Logging/WriteContext.cs @@ -4,22 +4,21 @@ using System; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class WriteContext { - public class WriteContext - { - public LogLevel LogLevel { get; set; } + public LogLevel LogLevel { get; set; } - public int EventId { get; set; } + public int EventId { get; set; } - public object State { get; set; } + public object State { get; set; } - public Exception Exception { get; set; } + public Exception Exception { get; set; } - public Func Formatter { get; set; } + public Func Formatter { get; set; } - public object Scope { get; set; } + public object Scope { get; set; } - public string LoggerName { get; set; } - } -} \ No newline at end of file + public string LoggerName { get; set; } +} diff --git a/src/Http/Routing/test/UnitTests/MatcherPolicyTest.cs b/src/Http/Routing/test/UnitTests/MatcherPolicyTest.cs index cb462e0e90..135cb97549 100644 --- a/src/Http/Routing/test/UnitTests/MatcherPolicyTest.cs +++ b/src/Http/Routing/test/UnitTests/MatcherPolicyTest.cs @@ -6,87 +6,86 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Patterns; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class MatcherPolicyTest { - public class MatcherPolicyTest + [Fact] + public void ContainsDynamicEndpoint_FindsDynamicEndpoint() { - [Fact] - public void ContainsDynamicEndpoint_FindsDynamicEndpoint() + // Arrange + var endpoints = new Endpoint[] { - // Arrange - var endpoints = new Endpoint[] - { CreateEndpoint("1"), CreateEndpoint("2"), CreateEndpoint("3", new DynamicEndpointMetadata(isDynamic: true)), - }; + }; - // Act - var result = TestMatcherPolicy.ContainsDynamicEndpoints(endpoints); + // Act + var result = TestMatcherPolicy.ContainsDynamicEndpoints(endpoints); - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void ContainsDynamicEndpoint_DoesNotFindDynamicEndpoint() + [Fact] + public void ContainsDynamicEndpoint_DoesNotFindDynamicEndpoint() + { + // Arrange + var endpoints = new Endpoint[] { - // Arrange - var endpoints = new Endpoint[] - { CreateEndpoint("1"), CreateEndpoint("2"), CreateEndpoint("3", new DynamicEndpointMetadata(isDynamic: false)), - }; + }; - // Act - var result = TestMatcherPolicy.ContainsDynamicEndpoints(endpoints); + // Act + var result = TestMatcherPolicy.ContainsDynamicEndpoints(endpoints); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Fact] - public void ContainsDynamicEndpoint_DoesNotFindDynamicEndpoint_Empty() - { - // Arrange - var endpoints = new Endpoint[]{ }; + [Fact] + public void ContainsDynamicEndpoint_DoesNotFindDynamicEndpoint_Empty() + { + // Arrange + var endpoints = new Endpoint[] { }; - // Act - var result = TestMatcherPolicy.ContainsDynamicEndpoints(endpoints); + // Act + var result = TestMatcherPolicy.ContainsDynamicEndpoints(endpoints); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - private RouteEndpoint CreateEndpoint(string template, params object[] metadata) + private RouteEndpoint CreateEndpoint(string template, params object[] metadata) + { + return new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse(template), + 0, + new EndpointMetadataCollection(metadata), + "test"); + } + + private class DynamicEndpointMetadata : IDynamicEndpointMetadata + { + public DynamicEndpointMetadata(bool isDynamic) { - return new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse(template), - 0, - new EndpointMetadataCollection(metadata), - "test"); + IsDynamic = isDynamic; } - private class DynamicEndpointMetadata : IDynamicEndpointMetadata - { - public DynamicEndpointMetadata(bool isDynamic) - { - IsDynamic = isDynamic; - } + public bool IsDynamic { get; } + } - public bool IsDynamic { get; } - } + private class TestMatcherPolicy : MatcherPolicy + { + public override int Order => throw new System.NotImplementedException(); - private class TestMatcherPolicy : MatcherPolicy + public static new bool ContainsDynamicEndpoints(IReadOnlyList endpoints) { - public override int Order => throw new System.NotImplementedException(); - - public static new bool ContainsDynamicEndpoints(IReadOnlyList endpoints) - { - return MatcherPolicy.ContainsDynamicEndpoints(endpoints); - } + return MatcherPolicy.ContainsDynamicEndpoints(endpoints); } } } diff --git a/src/Http/Routing/test/UnitTests/Matching/AcceptsMatcherPolicyTest.cs b/src/Http/Routing/test/UnitTests/Matching/AcceptsMatcherPolicyTest.cs index dea6bc6bd1..0a86e7144f 100644 --- a/src/Http/Routing/test/UnitTests/Matching/AcceptsMatcherPolicyTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/AcceptsMatcherPolicyTest.cs @@ -11,160 +11,160 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// There are some unit tests here for the IEndpointSelectorPolicy implementation. +// The INodeBuilderPolicy implementation is well-tested by functional tests. +public class AcceptsMatcherPolicyTest { - // There are some unit tests here for the IEndpointSelectorPolicy implementation. - // The INodeBuilderPolicy implementation is well-tested by functional tests. - public class AcceptsMatcherPolicyTest + [Fact] + public void INodeBuilderPolicy_AppliesToEndpoints_EndpointWithoutMetadata_ReturnsFalse() { - [Fact] - public void INodeBuilderPolicy_AppliesToEndpoints_EndpointWithoutMetadata_ReturnsFalse() - { - // Arrange - var endpoints = new[] { CreateEndpoint("/", null), }; + // Arrange + var endpoints = new[] { CreateEndpoint("/", null), }; - var policy = (INodeBuilderPolicy)CreatePolicy(); + var policy = (INodeBuilderPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Fact] - public void INodeBuilderPolicy_AppliesToEndpoints_EndpointWithoutContentTypes_ReturnsFalse() + [Fact] + public void INodeBuilderPolicy_AppliesToEndpoints_EndpointWithoutContentTypes_ReturnsFalse() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new AcceptsMetadata(Array.Empty())), }; - var policy = (INodeBuilderPolicy)CreatePolicy(); + var policy = (INodeBuilderPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Fact] - public void INodeBuilderPolicy_AppliesToEndpoints_EndpointHasContentTypes_ReturnsTrue() + [Fact] + public void INodeBuilderPolicy_AppliesToEndpoints_EndpointHasContentTypes_ReturnsTrue() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new AcceptsMetadata(Array.Empty())), CreateEndpoint("/", new AcceptsMetadata(new[] { "application/json", })), }; - var policy = (INodeBuilderPolicy)CreatePolicy(); + var policy = (INodeBuilderPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void INodeBuilderPolicy_AppliesToEndpoints_WithDynamicMetadata_ReturnsFalse() + [Fact] + public void INodeBuilderPolicy_AppliesToEndpoints_WithDynamicMetadata_ReturnsFalse() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new AcceptsMetadata(Array.Empty()), new DynamicEndpointMetadata()), CreateEndpoint("/", new AcceptsMetadata(new[] { "application/json", })), }; - var policy = (INodeBuilderPolicy)CreatePolicy(); + var policy = (INodeBuilderPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Fact] - public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointWithoutMetadata_ReturnsTrue() - { - // Arrange - var endpoints = new[] { CreateEndpoint("/", null, new DynamicEndpointMetadata()), }; + [Fact] + public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointWithoutMetadata_ReturnsTrue() + { + // Arrange + var endpoints = new[] { CreateEndpoint("/", null, new DynamicEndpointMetadata()), }; - var policy = (IEndpointSelectorPolicy)CreatePolicy(); + var policy = (IEndpointSelectorPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointWithoutContentTypes_ReturnsTrue() + [Fact] + public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointWithoutContentTypes_ReturnsTrue() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new AcceptsMetadata(Array.Empty()), new DynamicEndpointMetadata()), }; - var policy = (IEndpointSelectorPolicy)CreatePolicy(); + var policy = (IEndpointSelectorPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointHasContentTypes_ReturnsTrue() + [Fact] + public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointHasContentTypes_ReturnsTrue() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new AcceptsMetadata(Array.Empty()), new DynamicEndpointMetadata()), CreateEndpoint("/", new AcceptsMetadata(new[] { "application/json", })), }; - var policy = (IEndpointSelectorPolicy)CreatePolicy(); + var policy = (IEndpointSelectorPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void IEndpointSelectorPolicy_AppliesToEndpoints_WithoutDynamicMetadata_ReturnsFalse() + [Fact] + public void IEndpointSelectorPolicy_AppliesToEndpoints_WithoutDynamicMetadata_ReturnsFalse() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new AcceptsMetadata(Array.Empty())), CreateEndpoint("/", new AcceptsMetadata(new[] { "application/json", })), }; - var policy = (IEndpointSelectorPolicy)CreatePolicy(); + var policy = (IEndpointSelectorPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Fact] - public void GetEdges_GroupsByContentType() + [Fact] + public void GetEdges_GroupsByContentType() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { // These are arrange in an order that we won't actually see in a product scenario. It's done // this way so we can verify that ordering is preserved by GetEdges. CreateEndpoint("/", new AcceptsMetadata(new[] { "application/json", "application/*+json", })), @@ -174,57 +174,57 @@ namespace Microsoft.AspNetCore.Routing.Matching CreateEndpoint("/", new AcceptsMetadata(new[]{ "*/*", })), }; - var policy = CreatePolicy(); + var policy = CreatePolicy(); - // Act - var edges = policy.GetEdges(endpoints); + // Act + var edges = policy.GetEdges(endpoints); - // Assert - Assert.Collection( - edges.OrderBy(e => e.State), - e => - { - Assert.Equal(string.Empty, e.State); - Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal("*/*", e.State); - Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal("application/*", e.State); - Assert.Equal(new[] { endpoints[1], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal("application/*+json", e.State); - Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal("application/*+xml", e.State); - Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal("application/json", e.State); - Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal("application/xml", e.State); - Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); - }); - } + // Assert + Assert.Collection( + edges.OrderBy(e => e.State), + e => + { + Assert.Equal(string.Empty, e.State); + Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("*/*", e.State); + Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("application/*", e.State); + Assert.Equal(new[] { endpoints[1], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("application/*+json", e.State); + Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("application/*+xml", e.State); + Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("application/json", e.State); + Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("application/xml", e.State); + Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); + }); + } - [Fact] // See explanation in GetEdges for how this case is different - public void GetEdges_GroupsByContentType_CreatesHttp415Endpoint() + [Fact] // See explanation in GetEdges for how this case is different + public void GetEdges_GroupsByContentType_CreatesHttp415Endpoint() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { // These are arrange in an order that we won't actually see in a product scenario. It's done // this way so we can verify that ordering is preserved by GetEdges. CreateEndpoint("/", new AcceptsMetadata(new[] { "application/json", "application/*+json", })), @@ -232,65 +232,65 @@ namespace Microsoft.AspNetCore.Routing.Matching CreateEndpoint("/", new AcceptsMetadata(new[] { "application/*", })), }; - var policy = CreatePolicy(); + var policy = CreatePolicy(); - // Act - var edges = policy.GetEdges(endpoints); + // Act + var edges = policy.GetEdges(endpoints); - // Assert - Assert.Collection( - edges.OrderBy(e => e.State), - e => - { - Assert.Equal(string.Empty, e.State); - Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal("*/*", e.State); - Assert.Equal(AcceptsMatcherPolicy.Http415EndpointDisplayName, Assert.Single(e.Endpoints).DisplayName); - }, - e => - { - Assert.Equal("application/*", e.State); - Assert.Equal(new[] { endpoints[2], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal("application/*+json", e.State); - Assert.Equal(new[] { endpoints[0], endpoints[2], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal("application/*+xml", e.State); - Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal("application/json", e.State); - Assert.Equal(new[] { endpoints[0], endpoints[2], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal("application/xml", e.State); - Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); - }); + // Assert + Assert.Collection( + edges.OrderBy(e => e.State), + e => + { + Assert.Equal(string.Empty, e.State); + Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("*/*", e.State); + Assert.Equal(AcceptsMatcherPolicy.Http415EndpointDisplayName, Assert.Single(e.Endpoints).DisplayName); + }, + e => + { + Assert.Equal("application/*", e.State); + Assert.Equal(new[] { endpoints[2], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("application/*+json", e.State); + Assert.Equal(new[] { endpoints[0], endpoints[2], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("application/*+xml", e.State); + Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("application/json", e.State); + Assert.Equal(new[] { endpoints[0], endpoints[2], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("application/xml", e.State); + Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); + }); - } + } - [Theory] - [InlineData("image/png", 1)] - [InlineData("application/foo", 2)] - [InlineData("text/xml", 3)] - [InlineData("application/product+json", 6)] // application/json will match this - [InlineData("application/product+xml", 7)] // application/xml will match this - [InlineData("application/json", 6)] - [InlineData("application/xml", 7)] - public void BuildJumpTable_SortsEdgesByPriority(string contentType, int expected) + [Theory] + [InlineData("image/png", 1)] + [InlineData("application/foo", 2)] + [InlineData("text/xml", 3)] + [InlineData("application/product+json", 6)] // application/json will match this + [InlineData("application/product+xml", 7)] // application/xml will match this + [InlineData("application/json", 6)] + [InlineData("application/xml", 7)] + public void BuildJumpTable_SortsEdgesByPriority(string contentType, int expected) + { + // Arrange + var edges = new PolicyJumpTableEdge[] { - // Arrange - var edges = new PolicyJumpTableEdge[] - { // In reverse order of how they should be processed new PolicyJumpTableEdge(string.Empty, 0), new PolicyJumpTableEdge("*/*", 1), @@ -300,341 +300,340 @@ namespace Microsoft.AspNetCore.Routing.Matching new PolicyJumpTableEdge("application/*+json", 5), new PolicyJumpTableEdge("application/json", 6), new PolicyJumpTableEdge("application/xml", 7), - }; + }; - var policy = CreatePolicy(); + var policy = CreatePolicy(); - var jumpTable = policy.BuildJumpTable(-1, edges); + var jumpTable = policy.BuildJumpTable(-1, edges); - var httpContext = new DefaultHttpContext(); - httpContext.Request.ContentType = contentType; + var httpContext = new DefaultHttpContext(); + httpContext.Request.ContentType = contentType; - // Act - var actual = jumpTable.GetDestination(httpContext); + // Act + var actual = jumpTable.GetDestination(httpContext); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); + } - [Fact] - public async Task ApplyAsync_EndpointWithoutMetadata_MatchWithoutContentType() + [Fact] + public async Task ApplyAsync_EndpointWithoutMetadata_MatchWithoutContentType() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", null), }; - var candidates = CreateCandidateSet(endpoints); - var httpContext = new DefaultHttpContext(); + var candidates = CreateCandidateSet(endpoints); + var httpContext = new DefaultHttpContext(); - var policy = CreatePolicy(); + var policy = CreatePolicy(); - // Act - await policy.ApplyAsync(httpContext, candidates); + // Act + await policy.ApplyAsync(httpContext, candidates); - // Assert - Assert.True(candidates.IsValidCandidate(0)); - } + // Assert + Assert.True(candidates.IsValidCandidate(0)); + } - [Fact] - public async Task ApplyAsync_EndpointAllowsAnyContentType_MatchWithoutContentType() + [Fact] + public async Task ApplyAsync_EndpointAllowsAnyContentType_MatchWithoutContentType() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new AcceptsMetadata(Array.Empty())), }; - var candidates = CreateCandidateSet(endpoints); - var httpContext = new DefaultHttpContext(); + var candidates = CreateCandidateSet(endpoints); + var httpContext = new DefaultHttpContext(); - var policy = CreatePolicy(); + var policy = CreatePolicy(); - // Act - await policy.ApplyAsync(httpContext, candidates); + // Act + await policy.ApplyAsync(httpContext, candidates); - // Assert - Assert.True(candidates.IsValidCandidate(0)); - } + // Assert + Assert.True(candidates.IsValidCandidate(0)); + } - [Fact] - public async Task ApplyAsync_EndpointHasWildcardContentType_MatchWithoutContentType() + [Fact] + public async Task ApplyAsync_EndpointHasWildcardContentType_MatchWithoutContentType() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new AcceptsMetadata(new string[] { "*/*" })), }; - var candidates = CreateCandidateSet(endpoints); - var httpContext = new DefaultHttpContext(); + var candidates = CreateCandidateSet(endpoints); + var httpContext = new DefaultHttpContext(); - var policy = CreatePolicy(); + var policy = CreatePolicy(); - // Act - await policy.ApplyAsync(httpContext, candidates); + // Act + await policy.ApplyAsync(httpContext, candidates); - // Assert - Assert.True(candidates.IsValidCandidate(0)); - } + // Assert + Assert.True(candidates.IsValidCandidate(0)); + } - [Fact] - public async Task ApplyAsync_EndpointWithoutMetadata_MatchWithAnyContentType() + [Fact] + public async Task ApplyAsync_EndpointWithoutMetadata_MatchWithAnyContentType() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", null), }; - var candidates = CreateCandidateSet(endpoints); - var httpContext = new DefaultHttpContext() - { - Request = + var candidates = CreateCandidateSet(endpoints); + var httpContext = new DefaultHttpContext() + { + Request = { ContentType = "text/plain", }, - }; + }; - var policy = CreatePolicy(); + var policy = CreatePolicy(); - // Act - await policy.ApplyAsync(httpContext, candidates); + // Act + await policy.ApplyAsync(httpContext, candidates); - // Assert - Assert.True(candidates.IsValidCandidate(0)); - } + // Assert + Assert.True(candidates.IsValidCandidate(0)); + } - [Fact] - public async Task ApplyAsync_EndpointAllowsAnyContentType_MatchWithAnyContentType() + [Fact] + public async Task ApplyAsync_EndpointAllowsAnyContentType_MatchWithAnyContentType() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new AcceptsMetadata(Array.Empty())), }; - var candidates = CreateCandidateSet(endpoints); - var httpContext = new DefaultHttpContext() - { - Request = + var candidates = CreateCandidateSet(endpoints); + var httpContext = new DefaultHttpContext() + { + Request = { ContentType = "text/plain", }, - }; + }; - var policy = CreatePolicy(); + var policy = CreatePolicy(); - // Act - await policy.ApplyAsync(httpContext, candidates); + // Act + await policy.ApplyAsync(httpContext, candidates); - // Assert - Assert.True(candidates.IsValidCandidate(0)); - } + // Assert + Assert.True(candidates.IsValidCandidate(0)); + } - [Fact] - public async Task ApplyAsync_EndpointHasWildcardContentType_MatchWithAnyContentType() + [Fact] + public async Task ApplyAsync_EndpointHasWildcardContentType_MatchWithAnyContentType() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new AcceptsMetadata(new string[] { "*/*" })), }; - var candidates = CreateCandidateSet(endpoints); - var httpContext = new DefaultHttpContext() - { - Request = + var candidates = CreateCandidateSet(endpoints); + var httpContext = new DefaultHttpContext() + { + Request = { ContentType = "text/plain", }, - }; + }; - var policy = CreatePolicy(); + var policy = CreatePolicy(); - // Act - await policy.ApplyAsync(httpContext, candidates); + // Act + await policy.ApplyAsync(httpContext, candidates); - // Assert - Assert.True(candidates.IsValidCandidate(0)); - } + // Assert + Assert.True(candidates.IsValidCandidate(0)); + } - [Fact] - public async Task ApplyAsync_EndpointHasSubTypeWildcard_MatchWithValidContentType() + [Fact] + public async Task ApplyAsync_EndpointHasSubTypeWildcard_MatchWithValidContentType() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new AcceptsMetadata(new string[] { "application/*+json", })), }; - var candidates = CreateCandidateSet(endpoints); - var httpContext = new DefaultHttpContext() - { - Request = + var candidates = CreateCandidateSet(endpoints); + var httpContext = new DefaultHttpContext() + { + Request = { ContentType = "application/project+json", }, - }; + }; - var policy = CreatePolicy(); + var policy = CreatePolicy(); - // Act - await policy.ApplyAsync(httpContext, candidates); + // Act + await policy.ApplyAsync(httpContext, candidates); - // Assert - Assert.True(candidates.IsValidCandidate(0)); - } + // Assert + Assert.True(candidates.IsValidCandidate(0)); + } - [Fact] - public async Task ApplyAsync_EndpointHasMultipleContentType_MatchWithValidContentType() + [Fact] + public async Task ApplyAsync_EndpointHasMultipleContentType_MatchWithValidContentType() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new AcceptsMetadata(new string[] { "text/xml", "application/xml", })), }; - var candidates = CreateCandidateSet(endpoints); - var httpContext = new DefaultHttpContext() - { - Request = + var candidates = CreateCandidateSet(endpoints); + var httpContext = new DefaultHttpContext() + { + Request = { ContentType = "application/xml", }, - }; + }; - var policy = CreatePolicy(); + var policy = CreatePolicy(); - // Act - await policy.ApplyAsync(httpContext, candidates); + // Act + await policy.ApplyAsync(httpContext, candidates); - // Assert - Assert.True(candidates.IsValidCandidate(0)); - } + // Assert + Assert.True(candidates.IsValidCandidate(0)); + } - [Fact] - public async Task ApplyAsync_EndpointDoesNotMatch_Returns415() + [Fact] + public async Task ApplyAsync_EndpointDoesNotMatch_Returns415() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new AcceptsMetadata(new string[] { "text/xml", "application/xml", })), }; - var candidates = CreateCandidateSet(endpoints); - var httpContext = new DefaultHttpContext() - { - Request = + var candidates = CreateCandidateSet(endpoints); + var httpContext = new DefaultHttpContext() + { + Request = { ContentType = "application/json", }, - }; + }; - var policy = CreatePolicy(); + var policy = CreatePolicy(); - // Act - await policy.ApplyAsync(httpContext, candidates); + // Act + await policy.ApplyAsync(httpContext, candidates); - // Assert - Assert.False(candidates.IsValidCandidate(0)); - Assert.NotNull(httpContext.GetEndpoint()); - } + // Assert + Assert.False(candidates.IsValidCandidate(0)); + Assert.NotNull(httpContext.GetEndpoint()); + } - [Fact] - public async Task ApplyAsync_EndpointDoesNotMatch_DoesNotReturns415WithContentTypeObliviousEndpoint() + [Fact] + public async Task ApplyAsync_EndpointDoesNotMatch_DoesNotReturns415WithContentTypeObliviousEndpoint() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new AcceptsMetadata(new string[] { "text/xml", "application/xml", })), CreateEndpoint("/", null) }; - var candidates = CreateCandidateSet(endpoints); - var httpContext = new DefaultHttpContext() - { - Request = + var candidates = CreateCandidateSet(endpoints); + var httpContext = new DefaultHttpContext() + { + Request = { ContentType = "application/json", }, - }; + }; - var policy = CreatePolicy(); + var policy = CreatePolicy(); - // Act - await policy.ApplyAsync(httpContext, candidates); + // Act + await policy.ApplyAsync(httpContext, candidates); - // Assert - Assert.False(candidates.IsValidCandidate(0)); - Assert.Null(httpContext.GetEndpoint()); - } + // Assert + Assert.False(candidates.IsValidCandidate(0)); + Assert.Null(httpContext.GetEndpoint()); + } - [Fact] - public async Task ApplyAsync_EndpointDoesNotMatch_DoesNotReturns415WithContentTypeWildcardEndpoint() + [Fact] + public async Task ApplyAsync_EndpointDoesNotMatch_DoesNotReturns415WithContentTypeWildcardEndpoint() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new AcceptsMetadata(new string[] { "text/xml", "application/xml", })), CreateEndpoint("/", new AcceptsMetadata(new string[] { "*/*", })) }; - var candidates = CreateCandidateSet(endpoints); - var httpContext = new DefaultHttpContext() - { - Request = + var candidates = CreateCandidateSet(endpoints); + var httpContext = new DefaultHttpContext() + { + Request = { ContentType = "application/json", }, - }; + }; - var policy = CreatePolicy(); + var policy = CreatePolicy(); - // Act - await policy.ApplyAsync(httpContext, candidates); + // Act + await policy.ApplyAsync(httpContext, candidates); - // Assert - Assert.False(candidates.IsValidCandidate(0)); - Assert.True(candidates.IsValidCandidate(1)); - Assert.Null(httpContext.GetEndpoint()); - } + // Assert + Assert.False(candidates.IsValidCandidate(0)); + Assert.True(candidates.IsValidCandidate(1)); + Assert.Null(httpContext.GetEndpoint()); + } - private static RouteEndpoint CreateEndpoint(string template, AcceptsMetadata consumesMetadata, params object[] more) + private static RouteEndpoint CreateEndpoint(string template, AcceptsMetadata consumesMetadata, params object[] more) + { + var metadata = new List(); + if (consumesMetadata != null) { - var metadata = new List(); - if (consumesMetadata != null) - { - metadata.Add(consumesMetadata); - } - - if (more != null) - { - metadata.AddRange(more); - } - - return new RouteEndpoint( - (context) => Task.CompletedTask, - RoutePatternFactory.Parse(template), - 0, - new EndpointMetadataCollection(metadata), - $"test: {template} - {string.Join(", ", consumesMetadata?.ContentTypes ?? Array.Empty())}"); + metadata.Add(consumesMetadata); } - private static CandidateSet CreateCandidateSet(Endpoint[] endpoints) + if (more != null) { - return new CandidateSet(endpoints, new RouteValueDictionary[endpoints.Length], new int[endpoints.Length]); + metadata.AddRange(more); } - private static AcceptsMatcherPolicy CreatePolicy() - { - return new AcceptsMatcherPolicy(); - } + return new RouteEndpoint( + (context) => Task.CompletedTask, + RoutePatternFactory.Parse(template), + 0, + new EndpointMetadataCollection(metadata), + $"test: {template} - {string.Join(", ", consumesMetadata?.ContentTypes ?? Array.Empty())}"); + } - private class DynamicEndpointMetadata : IDynamicEndpointMetadata - { - public bool IsDynamic => true; - } + private static CandidateSet CreateCandidateSet(Endpoint[] endpoints) + { + return new CandidateSet(endpoints, new RouteValueDictionary[endpoints.Length], new int[endpoints.Length]); + } + + private static AcceptsMatcherPolicy CreatePolicy() + { + return new AcceptsMatcherPolicy(); + } + + private class DynamicEndpointMetadata : IDynamicEndpointMetadata + { + public bool IsDynamic => true; } } diff --git a/src/Http/Routing/test/UnitTests/Matching/AsciiTest.cs b/src/Http/Routing/test/UnitTests/Matching/AsciiTest.cs index 9ca3ba50ee..fcd2ee5756 100644 --- a/src/Http/Routing/test/UnitTests/Matching/AsciiTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/AsciiTest.cs @@ -4,111 +4,110 @@ using System; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// Note that while we don't intend for this code to be used with non-ASCII test, +// we still call into these methods with some non-ASCII characters so that +// we are sure of how it behaves. +public class AsciiTest { - // Note that while we don't intend for this code to be used with non-ASCII test, - // we still call into these methods with some non-ASCII characters so that - // we are sure of how it behaves. - public class AsciiTest + [Fact] + public void IsAscii_ReturnsTrueForAscii() + { + // Arrange + var text = "abcd\u007F"; + + // Act + var result = Ascii.IsAscii(text); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsAscii_ReturnsFalseForNonAscii() + { + // Arrange + var text = "abcd\u0080"; + + // Act + var result = Ascii.IsAscii(text); + + // Assert + Assert.False(result); + } + + [Theory] + + // Identity + [InlineData('c', 'c')] + [InlineData('C', 'C')] + [InlineData('#', '#')] + [InlineData('\u0080', '\u0080')] + + // Case-insensitive + [InlineData('c', 'C')] + public void AsciiIgnoreCaseEquals_ReturnsTrue(char x, char y) + { + // Arrange + + // Act + var result = Ascii.AsciiIgnoreCaseEquals(x, y); + + // Assert + Assert.True(result); + } + + [Theory] + + // Different letter + [InlineData('c', 'd')] + [InlineData('C', 'D')] + + // Non-letter + casing difference - 'a' and 'A' are 32 bits apart and so are ' ' and '@' + [InlineData(' ', '@')] + [InlineData('\u0080', '\u0080' + 32)] // Outside of ASCII range + public void AsciiIgnoreCaseEquals_ReturnsFalse(char x, char y) + { + // Arrange + + // Act + var result = Ascii.AsciiIgnoreCaseEquals(x, y); + + // Assert + Assert.False(result); + } + + [Theory] + [InlineData("", "", 0)] + [InlineData("abCD", "abcF", 3)] + [InlineData("ab#\u0080-$%", "Ab#\u0080-$%", 7)] + public void UnsafeAsciiIgnoreCaseEquals_ReturnsTrue(string x, string y, int length) + { + // Arrange + var spanX = x.AsSpan(); + var spanY = y.AsSpan(); + + // Act + var result = Ascii.AsciiIgnoreCaseEquals(spanX, spanY, length); + + // Assert + Assert.True(result); + } + + [Theory] + [InlineData("abcD", "abCE", 4)] + [InlineData("ab#\u0080-$%", "Ab#\u0081-$%", 7)] + public void UnsafeAsciiIgnoreCaseEquals_ReturnsFalse(string x, string y, int length) { - [Fact] - public void IsAscii_ReturnsTrueForAscii() - { - // Arrange - var text = "abcd\u007F"; - - // Act - var result = Ascii.IsAscii(text); - - // Assert - Assert.True(result); - } - - [Fact] - public void IsAscii_ReturnsFalseForNonAscii() - { - // Arrange - var text = "abcd\u0080"; - - // Act - var result = Ascii.IsAscii(text); - - // Assert - Assert.False(result); - } - - [Theory] - - // Identity - [InlineData('c', 'c')] - [InlineData('C', 'C')] - [InlineData('#', '#')] - [InlineData('\u0080', '\u0080')] - - // Case-insensitive - [InlineData('c', 'C')] - public void AsciiIgnoreCaseEquals_ReturnsTrue(char x, char y) - { - // Arrange - - // Act - var result = Ascii.AsciiIgnoreCaseEquals(x, y); - - // Assert - Assert.True(result); - } - - [Theory] - - // Different letter - [InlineData('c', 'd')] - [InlineData('C', 'D')] - - // Non-letter + casing difference - 'a' and 'A' are 32 bits apart and so are ' ' and '@' - [InlineData(' ', '@')] - [InlineData('\u0080', '\u0080' + 32)] // Outside of ASCII range - public void AsciiIgnoreCaseEquals_ReturnsFalse(char x, char y) - { - // Arrange - - // Act - var result = Ascii.AsciiIgnoreCaseEquals(x, y); - - // Assert - Assert.False(result); - } - - [Theory] - [InlineData("", "", 0)] - [InlineData("abCD", "abcF", 3)] - [InlineData("ab#\u0080-$%", "Ab#\u0080-$%", 7)] - public void UnsafeAsciiIgnoreCaseEquals_ReturnsTrue(string x, string y, int length) - { - // Arrange - var spanX = x.AsSpan(); - var spanY = y.AsSpan(); - - // Act - var result = Ascii.AsciiIgnoreCaseEquals(spanX, spanY, length); - - // Assert - Assert.True(result); - } - - [Theory] - [InlineData("abcD", "abCE", 4)] - [InlineData("ab#\u0080-$%", "Ab#\u0081-$%", 7)] - public void UnsafeAsciiIgnoreCaseEquals_ReturnsFalse(string x, string y, int length) - { - // Arrange - var spanX = x.AsSpan(); - var spanY = y.AsSpan(); - - // Act - var result = Ascii.AsciiIgnoreCaseEquals(spanX, spanY, length); - - // Assert - Assert.False(result); - } + // Arrange + var spanX = x.AsSpan(); + var spanY = y.AsSpan(); + + // Act + var result = Ascii.AsciiIgnoreCaseEquals(spanX, spanY, length); + + // Assert + Assert.False(result); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/BarebonesMatcher.cs b/src/Http/Routing/test/UnitTests/Matching/BarebonesMatcher.cs index 5473876c2a..710a3be639 100644 --- a/src/Http/Routing/test/UnitTests/Matching/BarebonesMatcher.cs +++ b/src/Http/Routing/test/UnitTests/Matching/BarebonesMatcher.cs @@ -6,126 +6,125 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// A test-only matcher implementation - used as a baseline for more compilated +// perf tests. The idea with this matcher is that we can cheat on the requirements +// to establish a lower bound for perf comparisons. +internal class BarebonesMatcher : Matcher { - // A test-only matcher implementation - used as a baseline for more compilated - // perf tests. The idea with this matcher is that we can cheat on the requirements - // to establish a lower bound for perf comparisons. - internal class BarebonesMatcher : Matcher + public readonly InnerMatcher[] Matchers; + + public BarebonesMatcher(InnerMatcher[] matchers) { - public readonly InnerMatcher[] Matchers; + Matchers = matchers; + } - public BarebonesMatcher(InnerMatcher[] matchers) + public override Task MatchAsync(HttpContext httpContext) + { + if (httpContext == null) { - Matchers = matchers; + throw new ArgumentNullException(nameof(httpContext)); } - public override Task MatchAsync(HttpContext httpContext) + var path = httpContext.Request.Path.Value; + for (var i = 0; i < Matchers.Length; i++) { - if (httpContext == null) + if (Matchers[i].TryMatch(path)) { - throw new ArgumentNullException(nameof(httpContext)); - } - - var path = httpContext.Request.Path.Value; - for (var i = 0; i < Matchers.Length; i++) - { - if (Matchers[i].TryMatch(path)) - { - httpContext.SetEndpoint(Matchers[i].Endpoint); - httpContext.Request.RouteValues = new RouteValueDictionary(); - } + httpContext.SetEndpoint(Matchers[i].Endpoint); + httpContext.Request.RouteValues = new RouteValueDictionary(); } - - return Task.CompletedTask; } - public sealed class InnerMatcher : Matcher - { - public readonly RouteEndpoint Endpoint; + return Task.CompletedTask; + } - private readonly string[] _segments; - private readonly Candidate[] _candidates; + public sealed class InnerMatcher : Matcher + { + public readonly RouteEndpoint Endpoint; - public InnerMatcher(string[] segments, RouteEndpoint endpoint) - { - _segments = segments; - Endpoint = endpoint; + private readonly string[] _segments; + private readonly Candidate[] _candidates; - _candidates = new Candidate[] { new Candidate(endpoint), }; - } + public InnerMatcher(string[] segments, RouteEndpoint endpoint) + { + _segments = segments; + Endpoint = endpoint; - public bool TryMatch(string path) - { - var segment = 0; + _candidates = new Candidate[] { new Candidate(endpoint), }; + } - var start = 1; // PathString always has a leading slash - var end = 0; - while ((end = path.IndexOf('/', start)) >= 0) - { - var comparand = _segments.Length > segment ? _segments[segment] : null; - if ((comparand == null && end - start == 0) || - (comparand != null && - (comparand.Length != end - start || - string.Compare( - path, - start, - comparand, - 0, - comparand.Length, - StringComparison.OrdinalIgnoreCase) != 0))) - { - return false; - } - - start = end + 1; - segment++; - } + public bool TryMatch(string path) + { + var segment = 0; - // residue - var length = path.Length - start; - if (length > 0) - { - var comparand = _segments.Length > segment ? _segments[segment] : null; - if (comparand != null && - (comparand.Length != length || + var start = 1; // PathString always has a leading slash + var end = 0; + while ((end = path.IndexOf('/', start)) >= 0) + { + var comparand = _segments.Length > segment ? _segments[segment] : null; + if ((comparand == null && end - start == 0) || + (comparand != null && + (comparand.Length != end - start || string.Compare( path, start, comparand, 0, comparand.Length, - StringComparison.OrdinalIgnoreCase) != 0)) - { - return false; - } - - segment++; + StringComparison.OrdinalIgnoreCase) != 0))) + { + return false; } - return segment == _segments.Length; + start = end + 1; + segment++; } - internal Candidate[] FindCandidateSet(string path, ReadOnlySpan segments) + // residue + var length = path.Length - start; + if (length > 0) { - if (TryMatch(path)) + var comparand = _segments.Length > segment ? _segments[segment] : null; + if (comparand != null && + (comparand.Length != length || + string.Compare( + path, + start, + comparand, + 0, + comparand.Length, + StringComparison.OrdinalIgnoreCase) != 0)) { - return _candidates; + return false; } - return Array.Empty(); + segment++; } - public override Task MatchAsync(HttpContext httpContext) + return segment == _segments.Length; + } + + internal Candidate[] FindCandidateSet(string path, ReadOnlySpan segments) + { + if (TryMatch(path)) { - if (TryMatch(httpContext.Request.Path.Value)) - { - httpContext.SetEndpoint(Endpoint); - httpContext.Request.RouteValues = new RouteValueDictionary(); - } + return _candidates; + } + + return Array.Empty(); + } - return Task.CompletedTask; + public override Task MatchAsync(HttpContext httpContext) + { + if (TryMatch(httpContext.Request.Path.Value)) + { + httpContext.SetEndpoint(Endpoint); + httpContext.Request.RouteValues = new RouteValueDictionary(); } + + return Task.CompletedTask; } } } diff --git a/src/Http/Routing/test/UnitTests/Matching/BarebonesMatcherBuilder.cs b/src/Http/Routing/test/UnitTests/Matching/BarebonesMatcherBuilder.cs index cd295f21a2..0c005ffff2 100644 --- a/src/Http/Routing/test/UnitTests/Matching/BarebonesMatcherBuilder.cs +++ b/src/Http/Routing/test/UnitTests/Matching/BarebonesMatcherBuilder.cs @@ -7,30 +7,29 @@ using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Template; using static Microsoft.AspNetCore.Routing.Matching.BarebonesMatcher; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal class BarebonesMatcherBuilder : MatcherBuilder { - internal class BarebonesMatcherBuilder : MatcherBuilder + private readonly List _endpoints = new List(); + + public override void AddEndpoint(RouteEndpoint endpoint) { - private readonly List _endpoints = new List(); + _endpoints.Add(endpoint); + } - public override void AddEndpoint(RouteEndpoint endpoint) + public override Matcher Build() + { + var matchers = new InnerMatcher[_endpoints.Count]; + for (var i = 0; i < _endpoints.Count; i++) { - _endpoints.Add(endpoint); + var endpoint = _endpoints[i]; + var pathSegments = endpoint.RoutePattern.PathSegments + .Select(s => s.IsSimple && s.Parts[0] is RoutePatternLiteralPart literalPart ? literalPart.Content : null) + .ToArray(); + matchers[i] = new InnerMatcher(pathSegments, _endpoints[i]); } - public override Matcher Build() - { - var matchers = new InnerMatcher[_endpoints.Count]; - for (var i = 0; i < _endpoints.Count; i++) - { - var endpoint = _endpoints[i]; - var pathSegments = endpoint.RoutePattern.PathSegments - .Select(s => s.IsSimple && s.Parts[0] is RoutePatternLiteralPart literalPart ? literalPart.Content : null) - .ToArray(); - matchers[i] = new InnerMatcher(pathSegments, _endpoints[i]); - } - - return new BarebonesMatcher(matchers); - } + return new BarebonesMatcher(matchers); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/BarebonesMatcherConformanceTest.cs b/src/Http/Routing/test/UnitTests/Matching/BarebonesMatcherConformanceTest.cs index f3cd6b4be6..964a3d221e 100644 --- a/src/Http/Routing/test/UnitTests/Matching/BarebonesMatcherConformanceTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/BarebonesMatcherConformanceTest.cs @@ -5,55 +5,54 @@ using System; using System.Threading.Tasks; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class BarebonesMatcherConformanceTest : MatcherConformanceTest { - public class BarebonesMatcherConformanceTest : MatcherConformanceTest + // Route values not supported + [Fact] + public override Task Match_SingleParameter() { - // Route values not supported - [Fact] - public override Task Match_SingleParameter() - { - return Task.CompletedTask; - } + return Task.CompletedTask; + } - // Route values not supported - [Fact] - public override Task Match_SingleParameter_TrailingSlash() - { - return Task.CompletedTask; - } + // Route values not supported + [Fact] + public override Task Match_SingleParameter_TrailingSlash() + { + return Task.CompletedTask; + } - // Route values not supported - [Fact] - public override Task Match_SingleParameter_WeirdNames() - { - return Task.CompletedTask; - } + // Route values not supported + [Fact] + public override Task Match_SingleParameter_WeirdNames() + { + return Task.CompletedTask; + } - // Route values not supported - [Theory] - [InlineData(null, null, null, null)] - public override Task Match_MultipleParameters(string template, string path, string[] keys, string[] values) - { - GC.KeepAlive(new object[] { template, path, keys, values }); - return Task.CompletedTask; - } + // Route values not supported + [Theory] + [InlineData(null, null, null, null)] + public override Task Match_MultipleParameters(string template, string path, string[] keys, string[] values) + { + GC.KeepAlive(new object[] { template, path, keys, values }); + return Task.CompletedTask; + } - // Route constraints not supported - [Fact] - public override Task Match_Constraint() - { - return Task.CompletedTask; - } + // Route constraints not supported + [Fact] + public override Task Match_Constraint() + { + return Task.CompletedTask; + } - internal override Matcher CreateMatcher(params RouteEndpoint[] endpoints) + internal override Matcher CreateMatcher(params RouteEndpoint[] endpoints) + { + var builder = new BarebonesMatcherBuilder(); + for (var i = 0; i < endpoints.Length; i++) { - var builder = new BarebonesMatcherBuilder(); - for (var i = 0; i < endpoints.Length; i++) - { - builder.AddEndpoint(endpoints[i]); - } - return builder.Build(); + builder.AddEndpoint(endpoints[i]); } + return builder.Build(); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/CandidateSetTest.cs b/src/Http/Routing/test/UnitTests/Matching/CandidateSetTest.cs index fef7ab0f8e..923180ee8b 100644 --- a/src/Http/Routing/test/UnitTests/Matching/CandidateSetTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/CandidateSetTest.cs @@ -10,397 +10,396 @@ using Microsoft.Extensions.Logging.Abstractions; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class CandidateSetTest { - public class CandidateSetTest + [Fact] + public void Create_CreatesCandidateSet() { - [Fact] - public void Create_CreatesCandidateSet() + // Arrange + var count = 10; + var endpoints = new RouteEndpoint[count]; + for (var i = 0; i < endpoints.Length; i++) { - // Arrange - var count = 10; - var endpoints = new RouteEndpoint[count]; - for (var i = 0; i < endpoints.Length; i++) - { - endpoints[i] = CreateEndpoint($"/{i}"); - } + endpoints[i] = CreateEndpoint($"/{i}"); + } - var builder = CreateDfaMatcherBuilder(); - var candidates = builder.CreateCandidates(endpoints); + var builder = CreateDfaMatcherBuilder(); + var candidates = builder.CreateCandidates(endpoints); - // Act - var candidateSet = new CandidateSet(candidates); + // Act + var candidateSet = new CandidateSet(candidates); - // Assert - for (var i = 0; i < candidateSet.Count; i++) - { - ref var state = ref candidateSet[i]; - Assert.True(candidateSet.IsValidCandidate(i)); - Assert.Same(endpoints[i], state.Endpoint); - Assert.Equal(candidates[i].Score, state.Score); - Assert.Null(state.Values); - - candidateSet.SetValidity(i, false); - Assert.False(candidateSet.IsValidCandidate(i)); - } + // Assert + for (var i = 0; i < candidateSet.Count; i++) + { + ref var state = ref candidateSet[i]; + Assert.True(candidateSet.IsValidCandidate(i)); + Assert.Same(endpoints[i], state.Endpoint); + Assert.Equal(candidates[i].Score, state.Score); + Assert.Null(state.Values); + + candidateSet.SetValidity(i, false); + Assert.False(candidateSet.IsValidCandidate(i)); } + } - [Fact] - public void ReplaceEndpoint_WithEndpoint() + [Fact] + public void ReplaceEndpoint_WithEndpoint() + { + // Arrange + var count = 10; + var endpoints = new RouteEndpoint[count]; + for (var i = 0; i < endpoints.Length; i++) { - // Arrange - var count = 10; - var endpoints = new RouteEndpoint[count]; - for (var i = 0; i < endpoints.Length; i++) - { - endpoints[i] = CreateEndpoint($"/{i}"); - } - - var builder = CreateDfaMatcherBuilder(); - var candidates = builder.CreateCandidates(endpoints); - - var candidateSet = new CandidateSet(candidates); - - for (var i = 0; i < candidateSet.Count; i++) - { - ref var state = ref candidateSet[i]; - - var endpoint = CreateEndpoint($"/test{i}"); - var values = new RouteValueDictionary(); - - // Act - candidateSet.ReplaceEndpoint(i, endpoint, values); - - // Assert - Assert.Same(endpoint, state.Endpoint); - Assert.Same(values, state.Values); - Assert.True(candidateSet.IsValidCandidate(i)); - } + endpoints[i] = CreateEndpoint($"/{i}"); } - [Fact] - public void ReplaceEndpoint_WithEndpoint_Null() + var builder = CreateDfaMatcherBuilder(); + var candidates = builder.CreateCandidates(endpoints); + + var candidateSet = new CandidateSet(candidates); + + for (var i = 0; i < candidateSet.Count; i++) { - // Arrange - var count = 10; - var endpoints = new RouteEndpoint[count]; - for (var i = 0; i < endpoints.Length; i++) - { - endpoints[i] = CreateEndpoint($"/{i}"); - } - - var builder = CreateDfaMatcherBuilder(); - var candidates = builder.CreateCandidates(endpoints); - - var candidateSet = new CandidateSet(candidates); - - for (var i = 0; i < candidateSet.Count; i++) - { - ref var state = ref candidateSet[i]; - - // Act - candidateSet.ReplaceEndpoint(i, (Endpoint)null, null); - - // Assert - Assert.Null(state.Endpoint); - Assert.Null(state.Values); - Assert.False(candidateSet.IsValidCandidate(i)); - } + ref var state = ref candidateSet[i]; + + var endpoint = CreateEndpoint($"/test{i}"); + var values = new RouteValueDictionary(); + + // Act + candidateSet.ReplaceEndpoint(i, endpoint, values); + + // Assert + Assert.Same(endpoint, state.Endpoint); + Assert.Same(values, state.Values); + Assert.True(candidateSet.IsValidCandidate(i)); } + } - [Fact] - public void ExpandEndpoint_EmptyList() + [Fact] + public void ReplaceEndpoint_WithEndpoint_Null() + { + // Arrange + var count = 10; + var endpoints = new RouteEndpoint[count]; + for (var i = 0; i < endpoints.Length; i++) { - // Arrange - var count = 10; - var endpoints = new RouteEndpoint[count]; - for (var i = 0; i < endpoints.Length; i++) - { - endpoints[i] = CreateEndpoint($"/{i}", order: i); - } + endpoints[i] = CreateEndpoint($"/{i}"); + } - var builder = CreateDfaMatcherBuilder(); - var candidates = builder.CreateCandidates(endpoints); + var builder = CreateDfaMatcherBuilder(); + var candidates = builder.CreateCandidates(endpoints); - var candidateSet = new CandidateSet(candidates); + var candidateSet = new CandidateSet(candidates); - var services = new Mock(); - services.Setup(s => s.GetService(typeof(IEnumerable))).Returns(new[] { new TestMetadataMatcherPolicy(), }); - var comparer = new EndpointMetadataComparer(services.Object); + for (var i = 0; i < candidateSet.Count; i++) + { + ref var state = ref candidateSet[i]; // Act - candidateSet.ExpandEndpoint(0, Array.Empty(), comparer); + candidateSet.ReplaceEndpoint(i, (Endpoint)null, null); // Assert + Assert.Null(state.Endpoint); + Assert.Null(state.Values); + Assert.False(candidateSet.IsValidCandidate(i)); + } + } + + [Fact] + public void ExpandEndpoint_EmptyList() + { + // Arrange + var count = 10; + var endpoints = new RouteEndpoint[count]; + for (var i = 0; i < endpoints.Length; i++) + { + endpoints[i] = CreateEndpoint($"/{i}", order: i); + } + + var builder = CreateDfaMatcherBuilder(); + var candidates = builder.CreateCandidates(endpoints); + + var candidateSet = new CandidateSet(candidates); + + var services = new Mock(); + services.Setup(s => s.GetService(typeof(IEnumerable))).Returns(new[] { new TestMetadataMatcherPolicy(), }); + var comparer = new EndpointMetadataComparer(services.Object); + + // Act + candidateSet.ExpandEndpoint(0, Array.Empty(), comparer); - Assert.Null(candidateSet[0].Endpoint); - Assert.False(candidateSet.IsValidCandidate(0)); + // Assert - for (var i = 1; i < candidateSet.Count; i++) - { - ref var state = ref candidateSet[i]; + Assert.Null(candidateSet[0].Endpoint); + Assert.False(candidateSet.IsValidCandidate(0)); - Assert.Same(endpoints[i], state.Endpoint); - } + for (var i = 1; i < candidateSet.Count; i++) + { + ref var state = ref candidateSet[i]; + + Assert.Same(endpoints[i], state.Endpoint); } + } - [Fact] - public void ExpandEndpoint_Beginning() + [Fact] + public void ExpandEndpoint_Beginning() + { + // Arrange + var count = 10; + var endpoints = new RouteEndpoint[count]; + for (var i = 0; i < endpoints.Length; i++) { - // Arrange - var count = 10; - var endpoints = new RouteEndpoint[count]; - for (var i = 0; i < endpoints.Length; i++) - { - endpoints[i] = CreateEndpoint($"/{i}", order: i); - } + endpoints[i] = CreateEndpoint($"/{i}", order: i); + } - var builder = CreateDfaMatcherBuilder(); - var candidates = builder.CreateCandidates(endpoints); + var builder = CreateDfaMatcherBuilder(); + var candidates = builder.CreateCandidates(endpoints); - var candidateSet = new CandidateSet(candidates); + var candidateSet = new CandidateSet(candidates); - var replacements = new RouteEndpoint[3] - { + var replacements = new RouteEndpoint[3] + { CreateEndpoint($"new /A", metadata: new object[]{ new TestMetadata(), }), CreateEndpoint($"new /B", metadata: new object[]{ }), CreateEndpoint($"new /C", metadata: new object[]{ new TestMetadata(), }), - }; + }; - var services = new Mock(); - services.Setup(s => s.GetService(typeof(IEnumerable))).Returns(new[] { new TestMetadataMatcherPolicy(), }); - var comparer = new EndpointMetadataComparer(services.Object); + var services = new Mock(); + services.Setup(s => s.GetService(typeof(IEnumerable))).Returns(new[] { new TestMetadataMatcherPolicy(), }); + var comparer = new EndpointMetadataComparer(services.Object); - candidateSet.SetValidity(0, false); // Has no effect. We always count new stuff as valid by default. + candidateSet.SetValidity(0, false); // Has no effect. We always count new stuff as valid by default. - // Act - candidateSet.ExpandEndpoint(0, replacements, comparer); + // Act + candidateSet.ExpandEndpoint(0, replacements, comparer); - // Assert - Assert.Equal(12, candidateSet.Count); - - Assert.Same(replacements[0], candidateSet[0].Endpoint); - Assert.Equal(0, candidateSet[0].Score); - Assert.Same(replacements[2], candidateSet[1].Endpoint); - Assert.Equal(0, candidateSet[1].Score); - Assert.Same(replacements[1], candidateSet[2].Endpoint); - Assert.Equal(1, candidateSet[2].Score); - - for (var i = 3; i < candidateSet.Count; i++) - { - ref var state = ref candidateSet[i]; - Assert.Same(endpoints[i - 2], state.Endpoint); - Assert.Equal(i - 1, candidateSet[i].Score); - } + // Assert + Assert.Equal(12, candidateSet.Count); + + Assert.Same(replacements[0], candidateSet[0].Endpoint); + Assert.Equal(0, candidateSet[0].Score); + Assert.Same(replacements[2], candidateSet[1].Endpoint); + Assert.Equal(0, candidateSet[1].Score); + Assert.Same(replacements[1], candidateSet[2].Endpoint); + Assert.Equal(1, candidateSet[2].Score); + + for (var i = 3; i < candidateSet.Count; i++) + { + ref var state = ref candidateSet[i]; + Assert.Same(endpoints[i - 2], state.Endpoint); + Assert.Equal(i - 1, candidateSet[i].Score); } + } - [Fact] - public void ExpandEndpoint_Middle() + [Fact] + public void ExpandEndpoint_Middle() + { + // Arrange + var count = 10; + var endpoints = new RouteEndpoint[count]; + for (var i = 0; i < endpoints.Length; i++) { - // Arrange - var count = 10; - var endpoints = new RouteEndpoint[count]; - for (var i = 0; i < endpoints.Length; i++) - { - endpoints[i] = CreateEndpoint($"/{i}", order: i); - } + endpoints[i] = CreateEndpoint($"/{i}", order: i); + } - var builder = CreateDfaMatcherBuilder(); - var candidates = builder.CreateCandidates(endpoints); + var builder = CreateDfaMatcherBuilder(); + var candidates = builder.CreateCandidates(endpoints); - var candidateSet = new CandidateSet(candidates); + var candidateSet = new CandidateSet(candidates); - var replacements = new RouteEndpoint[3] - { + var replacements = new RouteEndpoint[3] + { CreateEndpoint($"new /A", metadata: new object[]{ new TestMetadata(), }), CreateEndpoint($"new /B", metadata: new object[]{ }), CreateEndpoint($"new /C", metadata: new object[]{ new TestMetadata(), }), - }; + }; - var services = new Mock(); - services.Setup(s => s.GetService(typeof(IEnumerable))).Returns(new[] { new TestMetadataMatcherPolicy(), }); - var comparer = new EndpointMetadataComparer(services.Object); + var services = new Mock(); + services.Setup(s => s.GetService(typeof(IEnumerable))).Returns(new[] { new TestMetadataMatcherPolicy(), }); + var comparer = new EndpointMetadataComparer(services.Object); - candidateSet.SetValidity(5, false); // Has no effect. We always count new stuff as valid by default. + candidateSet.SetValidity(5, false); // Has no effect. We always count new stuff as valid by default. - // Act - candidateSet.ExpandEndpoint(5, replacements, comparer); + // Act + candidateSet.ExpandEndpoint(5, replacements, comparer); - // Assert - Assert.Equal(12, candidateSet.Count); - - for (var i = 0; i < 5; i++) - { - ref var state = ref candidateSet[i]; - Assert.Same(endpoints[i], state.Endpoint); - Assert.Equal(i, candidateSet[i].Score); - } - - Assert.Same(replacements[0], candidateSet[5].Endpoint); - Assert.Equal(5, candidateSet[5].Score); - Assert.Same(replacements[2], candidateSet[6].Endpoint); - Assert.Equal(5, candidateSet[6].Score); - Assert.Same(replacements[1], candidateSet[7].Endpoint); - Assert.Equal(6, candidateSet[7].Score); - - for (var i = 8; i < candidateSet.Count; i++) - { - ref var state = ref candidateSet[i]; - Assert.Same(endpoints[i - 2], state.Endpoint); - Assert.Equal(i - 1, candidateSet[i].Score); - } + // Assert + Assert.Equal(12, candidateSet.Count); + + for (var i = 0; i < 5; i++) + { + ref var state = ref candidateSet[i]; + Assert.Same(endpoints[i], state.Endpoint); + Assert.Equal(i, candidateSet[i].Score); } - [Fact] - public void ExpandEndpoint_End() + Assert.Same(replacements[0], candidateSet[5].Endpoint); + Assert.Equal(5, candidateSet[5].Score); + Assert.Same(replacements[2], candidateSet[6].Endpoint); + Assert.Equal(5, candidateSet[6].Score); + Assert.Same(replacements[1], candidateSet[7].Endpoint); + Assert.Equal(6, candidateSet[7].Score); + + for (var i = 8; i < candidateSet.Count; i++) { - // Arrange - var count = 10; - var endpoints = new RouteEndpoint[count]; - for (var i = 0; i < endpoints.Length; i++) - { - endpoints[i] = CreateEndpoint($"/{i}", order: i); - } + ref var state = ref candidateSet[i]; + Assert.Same(endpoints[i - 2], state.Endpoint); + Assert.Equal(i - 1, candidateSet[i].Score); + } + } - var builder = CreateDfaMatcherBuilder(); - var candidates = builder.CreateCandidates(endpoints); + [Fact] + public void ExpandEndpoint_End() + { + // Arrange + var count = 10; + var endpoints = new RouteEndpoint[count]; + for (var i = 0; i < endpoints.Length; i++) + { + endpoints[i] = CreateEndpoint($"/{i}", order: i); + } - var candidateSet = new CandidateSet(candidates); + var builder = CreateDfaMatcherBuilder(); + var candidates = builder.CreateCandidates(endpoints); - var replacements = new RouteEndpoint[3] - { + var candidateSet = new CandidateSet(candidates); + + var replacements = new RouteEndpoint[3] + { CreateEndpoint($"new /A", metadata: new object[]{ new TestMetadata(), }), CreateEndpoint($"new /B", metadata: new object[]{ }), CreateEndpoint($"new /C", metadata: new object[]{ new TestMetadata(), }), - }; + }; - var services = new Mock(); - services.Setup(s => s.GetService(typeof(IEnumerable))).Returns(new[] { new TestMetadataMatcherPolicy(), }); - var comparer = new EndpointMetadataComparer(services.Object); + var services = new Mock(); + services.Setup(s => s.GetService(typeof(IEnumerable))).Returns(new[] { new TestMetadataMatcherPolicy(), }); + var comparer = new EndpointMetadataComparer(services.Object); - candidateSet.SetValidity(9, false); // Has no effect. We always count new stuff as valid by default. + candidateSet.SetValidity(9, false); // Has no effect. We always count new stuff as valid by default. - // Act - candidateSet.ExpandEndpoint(9, replacements, comparer); + // Act + candidateSet.ExpandEndpoint(9, replacements, comparer); - // Assert - Assert.Equal(12, candidateSet.Count); - - for (var i = 0; i < 9; i++) - { - ref var state = ref candidateSet[i]; - Assert.Same(endpoints[i], state.Endpoint); - Assert.Equal(i, candidateSet[i].Score); - } - - Assert.Same(replacements[0], candidateSet[9].Endpoint); - Assert.Equal(9, candidateSet[9].Score); - Assert.Same(replacements[2], candidateSet[10].Endpoint); - Assert.Equal(9, candidateSet[10].Score); - Assert.Same(replacements[1], candidateSet[11].Endpoint); - Assert.Equal(10, candidateSet[11].Score); + // Assert + Assert.Equal(12, candidateSet.Count); + + for (var i = 0; i < 9; i++) + { + ref var state = ref candidateSet[i]; + Assert.Same(endpoints[i], state.Endpoint); + Assert.Equal(i, candidateSet[i].Score); } - [Fact] - public void ExpandEndpoint_ThrowsForDuplicateScore() + Assert.Same(replacements[0], candidateSet[9].Endpoint); + Assert.Equal(9, candidateSet[9].Score); + Assert.Same(replacements[2], candidateSet[10].Endpoint); + Assert.Equal(9, candidateSet[10].Score); + Assert.Same(replacements[1], candidateSet[11].Endpoint); + Assert.Equal(10, candidateSet[11].Score); + } + + [Fact] + public void ExpandEndpoint_ThrowsForDuplicateScore() + { + // Arrange + var count = 2; + var endpoints = new RouteEndpoint[count]; + for (var i = 0; i < endpoints.Length; i++) { - // Arrange - var count = 2; - var endpoints = new RouteEndpoint[count]; - for (var i = 0; i < endpoints.Length; i++) - { - endpoints[i] = CreateEndpoint($"/{i}", order: 0); - } + endpoints[i] = CreateEndpoint($"/{i}", order: 0); + } - var builder = CreateDfaMatcherBuilder(); - var candidates = builder.CreateCandidates(endpoints); + var builder = CreateDfaMatcherBuilder(); + var candidates = builder.CreateCandidates(endpoints); - var candidateSet = new CandidateSet(candidates); + var candidateSet = new CandidateSet(candidates); - var services = new Mock(); - services.Setup(s => s.GetService(typeof(IEnumerable))).Returns(new[] { new TestMetadataMatcherPolicy(), }); - var comparer = new EndpointMetadataComparer(services.Object); + var services = new Mock(); + services.Setup(s => s.GetService(typeof(IEnumerable))).Returns(new[] { new TestMetadataMatcherPolicy(), }); + var comparer = new EndpointMetadataComparer(services.Object); - // Act - var ex = Assert.Throws(() => candidateSet.ExpandEndpoint(0, Array.Empty(), comparer)); + // Act + var ex = Assert.Throws(() => candidateSet.ExpandEndpoint(0, Array.Empty(), comparer)); - // Assert - Assert.Equal(@"Using ExpandEndpoint requires that the replaced endpoint have a unique priority. The following endpoints were found with the same priority:" + - Environment.NewLine + - "test: /0" + - Environment.NewLine + - "test: /1" - .TrimStart(), ex.Message); + // Assert + Assert.Equal(@"Using ExpandEndpoint requires that the replaced endpoint have a unique priority. The following endpoints were found with the same priority:" + + Environment.NewLine + + "test: /0" + + Environment.NewLine + + "test: /1" + .TrimStart(), ex.Message); + } + + [Fact] + public void Create_CreatesCandidateSet_TestConstructor() + { + // Arrange + var count = 10; + var endpoints = new RouteEndpoint[count]; + for (var i = 0; i < endpoints.Length; i++) + { + endpoints[i] = CreateEndpoint($"/{i}"); } - [Fact] - public void Create_CreatesCandidateSet_TestConstructor() + var values = new RouteValueDictionary[count]; + for (var i = 0; i < endpoints.Length; i++) { - // Arrange - var count = 10; - var endpoints = new RouteEndpoint[count]; - for (var i = 0; i < endpoints.Length; i++) - { - endpoints[i] = CreateEndpoint($"/{i}"); - } - - var values = new RouteValueDictionary[count]; - for (var i = 0; i < endpoints.Length; i++) - { - values[i] = new RouteValueDictionary() + values[i] = new RouteValueDictionary() { { "i", i } }; - } - - // Act - var candidateSet = new CandidateSet(endpoints, values, Enumerable.Range(0, count).ToArray()); - - // Assert - for (var i = 0; i < candidateSet.Count; i++) - { - ref var state = ref candidateSet[i]; - Assert.True(candidateSet.IsValidCandidate(i)); - Assert.Same(endpoints[i], state.Endpoint); - Assert.Equal(i, state.Score); - Assert.NotNull(state.Values); - Assert.Equal(i, state.Values["i"]); - - candidateSet.SetValidity(i, false); - Assert.False(candidateSet.IsValidCandidate(i)); - } } - private RouteEndpoint CreateEndpoint(string template, int order = 0, params object[] metadata) - { - var builder = new RouteEndpointBuilder(TestConstants.EmptyRequestDelegate, RoutePatternFactory.Parse(template), order); - for (var i = 0; i < metadata.Length; i++) - { - builder.Metadata.Add(metadata[i]); - } - - builder.DisplayName = "test: " + template; - return (RouteEndpoint)builder.Build(); - } + // Act + var candidateSet = new CandidateSet(endpoints, values, Enumerable.Range(0, count).ToArray()); - private static DfaMatcherBuilder CreateDfaMatcherBuilder(params MatcherPolicy[] policies) + // Assert + for (var i = 0; i < candidateSet.Count; i++) { - var dataSource = new CompositeEndpointDataSource(Array.Empty()); - return new DfaMatcherBuilder( - NullLoggerFactory.Instance, - Mock.Of(), - Mock.Of(), - policies); + ref var state = ref candidateSet[i]; + Assert.True(candidateSet.IsValidCandidate(i)); + Assert.Same(endpoints[i], state.Endpoint); + Assert.Equal(i, state.Score); + Assert.NotNull(state.Values); + Assert.Equal(i, state.Values["i"]); + + candidateSet.SetValidity(i, false); + Assert.False(candidateSet.IsValidCandidate(i)); } + } - private class TestMetadata + private RouteEndpoint CreateEndpoint(string template, int order = 0, params object[] metadata) + { + var builder = new RouteEndpointBuilder(TestConstants.EmptyRequestDelegate, RoutePatternFactory.Parse(template), order); + for (var i = 0; i < metadata.Length; i++) { + builder.Metadata.Add(metadata[i]); } - private class TestMetadataMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy - { - public override int Order { get; } - public IComparer Comparer => EndpointMetadataComparer.Default; - } + builder.DisplayName = "test: " + template; + return (RouteEndpoint)builder.Build(); + } + + private static DfaMatcherBuilder CreateDfaMatcherBuilder(params MatcherPolicy[] policies) + { + var dataSource = new CompositeEndpointDataSource(Array.Empty()); + return new DfaMatcherBuilder( + NullLoggerFactory.Instance, + Mock.Of(), + Mock.Of(), + policies); + } + + private class TestMetadata + { + } + + private class TestMetadataMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy + { + public override int Order { get; } + public IComparer Comparer => EndpointMetadataComparer.Default; } } diff --git a/src/Http/Routing/test/UnitTests/Matching/DataSourceDependentMatcherTest.cs b/src/Http/Routing/test/UnitTests/Matching/DataSourceDependentMatcherTest.cs index 88563ba750..f03bd7272f 100644 --- a/src/Http/Routing/test/UnitTests/Matching/DataSourceDependentMatcherTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/DataSourceDependentMatcherTest.cs @@ -9,264 +9,263 @@ using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.TestObjects; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class DataSourceDependentMatcherTest { - public class DataSourceDependentMatcherTest + [Fact] + public void Matcher_Initializes_InConstructor() { - [Fact] - public void Matcher_Initializes_InConstructor() - { - // Arrange - var dataSource = new DynamicEndpointDataSource(); - var lifetime = new DataSourceDependentMatcher.Lifetime(); - - // Act - var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create); + // Arrange + var dataSource = new DynamicEndpointDataSource(); + var lifetime = new DataSourceDependentMatcher.Lifetime(); - // Assert - var inner = Assert.IsType(matcher.CurrentMatcher); - Assert.Empty(inner.Endpoints); + // Act + var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create); - Assert.NotNull(lifetime.Cache); - } - - [Fact] - public void Matcher_Reinitializes_WhenDataSourceChanges() - { - // Arrange - var dataSource = new DynamicEndpointDataSource(); - var lifetime = new DataSourceDependentMatcher.Lifetime(); - var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create); + // Assert + var inner = Assert.IsType(matcher.CurrentMatcher); + Assert.Empty(inner.Endpoints); - var endpoint = new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse("a/b/c"), - 0, - EndpointMetadataCollection.Empty, - "test"); - - // Act - dataSource.AddEndpoint(endpoint); - - // Assert - var inner = Assert.IsType(matcher.CurrentMatcher); - Assert.Collection( - inner.Endpoints, - e => Assert.Same(endpoint, e)); - } - - [Fact] - public void Matcher_IgnoresUpdate_WhenDisposed() - { - // Arrange - var dataSource = new DynamicEndpointDataSource(); - var lifetime = new DataSourceDependentMatcher.Lifetime(); - var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create); - - var endpoint = new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse("a/b/c"), - 0, - EndpointMetadataCollection.Empty, - "test"); - - lifetime.Dispose(); - - // Act - dataSource.AddEndpoint(endpoint); - - // Assert - var inner = Assert.IsType(matcher.CurrentMatcher); - Assert.Empty(inner.Endpoints); - } + Assert.NotNull(lifetime.Cache); + } - [Fact] - public void Matcher_Ignores_NonRouteEndpoint() - { - // Arrange - var dataSource = new DynamicEndpointDataSource(); - var lifetime = new DataSourceDependentMatcher.Lifetime(); - var endpoint = new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "test"); - dataSource.AddEndpoint(endpoint); - - // Act - var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create); - - // Assert - var inner = Assert.IsType(matcher.CurrentMatcher); - Assert.Empty(inner.Endpoints); - } + [Fact] + public void Matcher_Reinitializes_WhenDataSourceChanges() + { + // Arrange + var dataSource = new DynamicEndpointDataSource(); + var lifetime = new DataSourceDependentMatcher.Lifetime(); + var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create); + + var endpoint = new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse("a/b/c"), + 0, + EndpointMetadataCollection.Empty, + "test"); + + // Act + dataSource.AddEndpoint(endpoint); + + // Assert + var inner = Assert.IsType(matcher.CurrentMatcher); + Assert.Collection( + inner.Endpoints, + e => Assert.Same(endpoint, e)); + } - [Fact] - public void Matcher_Ignores_SuppressedEndpoint() - { - // Arrange - var dataSource = new DynamicEndpointDataSource(); - var lifetime = new DataSourceDependentMatcher.Lifetime(); - var endpoint = new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse("/"), - 0, - new EndpointMetadataCollection(new SuppressMatchingMetadata()), - "test"); - dataSource.AddEndpoint(endpoint); + [Fact] + public void Matcher_IgnoresUpdate_WhenDisposed() + { + // Arrange + var dataSource = new DynamicEndpointDataSource(); + var lifetime = new DataSourceDependentMatcher.Lifetime(); + var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create); + + var endpoint = new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse("a/b/c"), + 0, + EndpointMetadataCollection.Empty, + "test"); + + lifetime.Dispose(); + + // Act + dataSource.AddEndpoint(endpoint); + + // Assert + var inner = Assert.IsType(matcher.CurrentMatcher); + Assert.Empty(inner.Endpoints); + } - // Act - var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create); + [Fact] + public void Matcher_Ignores_NonRouteEndpoint() + { + // Arrange + var dataSource = new DynamicEndpointDataSource(); + var lifetime = new DataSourceDependentMatcher.Lifetime(); + var endpoint = new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "test"); + dataSource.AddEndpoint(endpoint); + + // Act + var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create); + + // Assert + var inner = Assert.IsType(matcher.CurrentMatcher); + Assert.Empty(inner.Endpoints); + } - // Assert - var inner = Assert.IsType(matcher.CurrentMatcher); - Assert.Empty(inner.Endpoints); - } + [Fact] + public void Matcher_Ignores_SuppressedEndpoint() + { + // Arrange + var dataSource = new DynamicEndpointDataSource(); + var lifetime = new DataSourceDependentMatcher.Lifetime(); + var endpoint = new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse("/"), + 0, + new EndpointMetadataCollection(new SuppressMatchingMetadata()), + "test"); + dataSource.AddEndpoint(endpoint); + + // Act + var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create); + + // Assert + var inner = Assert.IsType(matcher.CurrentMatcher); + Assert.Empty(inner.Endpoints); + } - [Fact] - public void Matcher_UnsuppressedEndpoint_IsUsed() - { - // Arrange - var dataSource = new DynamicEndpointDataSource(); - var lifetime = new DataSourceDependentMatcher.Lifetime(); - var endpoint = new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse("/"), - 0, - new EndpointMetadataCollection(new SuppressMatchingMetadata(), new EncourageMatchingMetadata()), - "test"); - dataSource.AddEndpoint(endpoint); + [Fact] + public void Matcher_UnsuppressedEndpoint_IsUsed() + { + // Arrange + var dataSource = new DynamicEndpointDataSource(); + var lifetime = new DataSourceDependentMatcher.Lifetime(); + var endpoint = new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse("/"), + 0, + new EndpointMetadataCollection(new SuppressMatchingMetadata(), new EncourageMatchingMetadata()), + "test"); + dataSource.AddEndpoint(endpoint); + + // Act + var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create); + + // Assert + var inner = Assert.IsType(matcher.CurrentMatcher); + Assert.Same(endpoint, Assert.Single(inner.Endpoints)); + } - // Act - var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create); + [Fact] + public void Matcher_ThrowsOnDuplicateEndpoints() + { + // Arrange + var expectedError = "Duplicate endpoint name 'Foo' found on '/bar' and '/foo'. Endpoint names must be globally unique."; + var dataSource = new DynamicEndpointDataSource(); + var lifetime = new DataSourceDependentMatcher.Lifetime(); + dataSource.AddEndpoint(new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse("/foo"), + 0, + new EndpointMetadataCollection(new EndpointNameMetadata("Foo")), + "/foo" + )); + dataSource.AddEndpoint(new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse("/bar"), + 0, + new EndpointMetadataCollection(new EndpointNameMetadata("Foo")), + "/bar" + )); + + // Assert + var exception = Assert.Throws( + () => new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create)); + Assert.Equal(expectedError, exception.Message); + } - // Assert - var inner = Assert.IsType(matcher.CurrentMatcher); - Assert.Same(endpoint, Assert.Single(inner.Endpoints)); - } + [Fact] + public void Matcher_ThrowsOnDuplicateEndpointsFromMultipleSources() + { + // Arrange + var expectedError = "Duplicate endpoint name 'Foo' found on '/foo2' and '/foo'. Endpoint names must be globally unique."; + var dataSource = new DynamicEndpointDataSource(); + var lifetime = new DataSourceDependentMatcher.Lifetime(); + dataSource.AddEndpoint(new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse("/foo"), + 0, + new EndpointMetadataCollection(new EndpointNameMetadata("Foo")), + "/foo" + )); + dataSource.AddEndpoint(new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse("/bar"), + 0, + new EndpointMetadataCollection(new EndpointNameMetadata("Bar")), + "/bar" + )); + var anotherDataSource = new DynamicEndpointDataSource(); + anotherDataSource.AddEndpoint(new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse("/foo2"), + 0, + new EndpointMetadataCollection(new EndpointNameMetadata("Foo")), + "/foo2" + )); + + var compositeDataSource = new CompositeEndpointDataSource(new[] { dataSource, anotherDataSource }); + + // Assert + var exception = Assert.Throws( + () => new DataSourceDependentMatcher(compositeDataSource, lifetime, TestMatcherBuilder.Create)); + Assert.Equal(expectedError, exception.Message); + } - [Fact] - public void Matcher_ThrowsOnDuplicateEndpoints() - { - // Arrange - var expectedError = "Duplicate endpoint name 'Foo' found on '/bar' and '/foo'. Endpoint names must be globally unique."; - var dataSource = new DynamicEndpointDataSource(); - var lifetime = new DataSourceDependentMatcher.Lifetime(); - dataSource.AddEndpoint(new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse("/foo"), - 0, - new EndpointMetadataCollection(new EndpointNameMetadata("Foo")), - "/foo" - )); - dataSource.AddEndpoint(new RouteEndpoint( + [Fact] + public void Matcher_ThrowsOnDuplicateEndpointAddedLater() + { + // Arrange + var expectedError = "Duplicate endpoint name 'Foo' found on '/bar' and '/foo'. Endpoint names must be globally unique."; + var dataSource = new DynamicEndpointDataSource(); + var lifetime = new DataSourceDependentMatcher.Lifetime(); + dataSource.AddEndpoint(new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse("/foo"), + 0, + new EndpointMetadataCollection(new EndpointNameMetadata("Foo")), + "/foo" + )); + + // Act (should be all good since no duplicate has been added yet) + var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create); + + // Assert that rerunning initializer throws AggregateException + var exception = Assert.Throws( + () => dataSource.AddEndpoint(new RouteEndpoint( TestConstants.EmptyRequestDelegate, RoutePatternFactory.Parse("/bar"), 0, new EndpointMetadataCollection(new EndpointNameMetadata("Foo")), "/bar" - )); - - // Assert - var exception = Assert.Throws( - () => new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create)); - Assert.Equal(expectedError, exception.Message); - } - - [Fact] - public void Matcher_ThrowsOnDuplicateEndpointsFromMultipleSources() - { - // Arrange - var expectedError = "Duplicate endpoint name 'Foo' found on '/foo2' and '/foo'. Endpoint names must be globally unique."; - var dataSource = new DynamicEndpointDataSource(); - var lifetime = new DataSourceDependentMatcher.Lifetime(); - dataSource.AddEndpoint(new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse("/foo"), - 0, - new EndpointMetadataCollection(new EndpointNameMetadata("Foo")), - "/foo" - )); - dataSource.AddEndpoint(new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse("/bar"), - 0, - new EndpointMetadataCollection(new EndpointNameMetadata("Bar")), - "/bar" - )); - var anotherDataSource = new DynamicEndpointDataSource(); - anotherDataSource.AddEndpoint(new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse("/foo2"), - 0, - new EndpointMetadataCollection(new EndpointNameMetadata("Foo")), - "/foo2" - )); + ))); + Assert.Equal(expectedError, exception.InnerException.Message); + } - var compositeDataSource = new CompositeEndpointDataSource(new[] { dataSource, anotherDataSource }); + private class TestMatcherBuilder : MatcherBuilder + { + public static Func Create = () => new TestMatcherBuilder(); - // Assert - var exception = Assert.Throws( - () => new DataSourceDependentMatcher(compositeDataSource, lifetime, TestMatcherBuilder.Create)); - Assert.Equal(expectedError, exception.Message); - } + private List Endpoints { get; } = new List(); - [Fact] - public void Matcher_ThrowsOnDuplicateEndpointAddedLater() + public override void AddEndpoint(RouteEndpoint endpoint) { - // Arrange - var expectedError = "Duplicate endpoint name 'Foo' found on '/bar' and '/foo'. Endpoint names must be globally unique."; - var dataSource = new DynamicEndpointDataSource(); - var lifetime = new DataSourceDependentMatcher.Lifetime(); - dataSource.AddEndpoint(new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse("/foo"), - 0, - new EndpointMetadataCollection(new EndpointNameMetadata("Foo")), - "/foo" - )); - - // Act (should be all good since no duplicate has been added yet) - var matcher = new DataSourceDependentMatcher(dataSource, lifetime, TestMatcherBuilder.Create); - - // Assert that rerunning initializer throws AggregateException - var exception = Assert.Throws( - () => dataSource.AddEndpoint(new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse("/bar"), - 0, - new EndpointMetadataCollection(new EndpointNameMetadata("Foo")), - "/bar" - ))); - Assert.Equal(expectedError, exception.InnerException.Message); + Endpoints.Add(endpoint); } - private class TestMatcherBuilder : MatcherBuilder + public override Matcher Build() { - public static Func Create = () => new TestMatcherBuilder(); - - private List Endpoints { get; } = new List(); - - public override void AddEndpoint(RouteEndpoint endpoint) - { - Endpoints.Add(endpoint); - } - - public override Matcher Build() - { - return new TestMatcher() { Endpoints = Endpoints, }; - } + return new TestMatcher() { Endpoints = Endpoints, }; } + } - private class TestMatcher : Matcher - { - public IReadOnlyList Endpoints { get; set; } - - public override Task MatchAsync(HttpContext httpContext) - { - throw new NotImplementedException(); - } - } + private class TestMatcher : Matcher + { + public IReadOnlyList Endpoints { get; set; } - private class EncourageMatchingMetadata : ISuppressMatchingMetadata + public override Task MatchAsync(HttpContext httpContext) { - public bool SuppressMatching => false; + throw new NotImplementedException(); } } + + private class EncourageMatchingMetadata : ISuppressMatchingMetadata + { + public bool SuppressMatching => false; + } } diff --git a/src/Http/Routing/test/UnitTests/Matching/DefaultEndpointSelectorTest.cs b/src/Http/Routing/test/UnitTests/Matching/DefaultEndpointSelectorTest.cs index a90e481382..d421bb4c94 100644 --- a/src/Http/Routing/test/UnitTests/Matching/DefaultEndpointSelectorTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/DefaultEndpointSelectorTest.cs @@ -9,192 +9,191 @@ using Microsoft.AspNetCore.Routing.Patterns; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class DefaultEndpointSelectorTest { - public class DefaultEndpointSelectorTest + [Fact] + public async Task SelectAsync_NoCandidates_DoesNothing() { - [Fact] - public async Task SelectAsync_NoCandidates_DoesNothing() - { - // Arrange - var endpoints = new RouteEndpoint[] { }; - var scores = new int[] { }; - var candidateSet = CreateCandidateSet(endpoints, scores); + // Arrange + var endpoints = new RouteEndpoint[] { }; + var scores = new int[] { }; + var candidateSet = CreateCandidateSet(endpoints, scores); - var httpContext = CreateContext(); - var selector = CreateSelector(); + var httpContext = CreateContext(); + var selector = CreateSelector(); - // Act - await selector.SelectAsync(httpContext, candidateSet); + // Act + await selector.SelectAsync(httpContext, candidateSet); - // Assert - Assert.Null(httpContext.GetEndpoint()); - } + // Assert + Assert.Null(httpContext.GetEndpoint()); + } - [Fact] - public async Task SelectAsync_NoValidCandidates_DoesNothing() - { - // Arrange - var endpoints = new RouteEndpoint[] { CreateEndpoint("/test"), }; - var scores = new int[] { 0, }; - var candidateSet = CreateCandidateSet(endpoints, scores); + [Fact] + public async Task SelectAsync_NoValidCandidates_DoesNothing() + { + // Arrange + var endpoints = new RouteEndpoint[] { CreateEndpoint("/test"), }; + var scores = new int[] { 0, }; + var candidateSet = CreateCandidateSet(endpoints, scores); - candidateSet[0].Values = new RouteValueDictionary(); - candidateSet.SetValidity(0, false); + candidateSet[0].Values = new RouteValueDictionary(); + candidateSet.SetValidity(0, false); - var httpContext = CreateContext(); - var selector = CreateSelector(); + var httpContext = CreateContext(); + var selector = CreateSelector(); - // Act - await selector.SelectAsync(httpContext, candidateSet); + // Act + await selector.SelectAsync(httpContext, candidateSet); - // Assert - Assert.Null(httpContext.GetEndpoint()); - } + // Assert + Assert.Null(httpContext.GetEndpoint()); + } - [Fact] - public async Task SelectAsync_SingleCandidate_ChoosesCandidate() - { - // Arrange - var endpoints = new RouteEndpoint[] { CreateEndpoint("/test"), }; - var scores = new int[] { 0, }; - var candidateSet = CreateCandidateSet(endpoints, scores); + [Fact] + public async Task SelectAsync_SingleCandidate_ChoosesCandidate() + { + // Arrange + var endpoints = new RouteEndpoint[] { CreateEndpoint("/test"), }; + var scores = new int[] { 0, }; + var candidateSet = CreateCandidateSet(endpoints, scores); - candidateSet[0].Values = new RouteValueDictionary(); - candidateSet.SetValidity(0, true); + candidateSet[0].Values = new RouteValueDictionary(); + candidateSet.SetValidity(0, true); - var httpContext = CreateContext(); - var selector = CreateSelector(); + var httpContext = CreateContext(); + var selector = CreateSelector(); - // Act - await selector.SelectAsync(httpContext, candidateSet); + // Act + await selector.SelectAsync(httpContext, candidateSet); - // Assert - Assert.Same(endpoints[0], httpContext.GetEndpoint()); - } + // Assert + Assert.Same(endpoints[0], httpContext.GetEndpoint()); + } - [Fact] - public async Task SelectAsync_SingleValidCandidate_ChoosesCandidate() - { - // Arrange - var endpoints = new RouteEndpoint[] { CreateEndpoint("/test1"), CreateEndpoint("/test2"), }; - var scores = new int[] { 0, 0 }; - var candidateSet = CreateCandidateSet(endpoints, scores); + [Fact] + public async Task SelectAsync_SingleValidCandidate_ChoosesCandidate() + { + // Arrange + var endpoints = new RouteEndpoint[] { CreateEndpoint("/test1"), CreateEndpoint("/test2"), }; + var scores = new int[] { 0, 0 }; + var candidateSet = CreateCandidateSet(endpoints, scores); - candidateSet.SetValidity(0, false); - candidateSet.SetValidity(1, true); + candidateSet.SetValidity(0, false); + candidateSet.SetValidity(1, true); - var httpContext = CreateContext(); - var selector = CreateSelector(); + var httpContext = CreateContext(); + var selector = CreateSelector(); - // Act - await selector.SelectAsync(httpContext, candidateSet); + // Act + await selector.SelectAsync(httpContext, candidateSet); - // Assert - Assert.Same(endpoints[1], httpContext.GetEndpoint()); - } + // Assert + Assert.Same(endpoints[1], httpContext.GetEndpoint()); + } - [Fact] - public async Task SelectAsync_SingleValidCandidateInGroup_ChoosesCandidate() - { - // Arrange - var endpoints = new RouteEndpoint[] { CreateEndpoint("/test1"), CreateEndpoint("/test2"), CreateEndpoint("/test3"), }; - var scores = new int[] { 0, 0, 1 }; - var candidateSet = CreateCandidateSet(endpoints, scores); + [Fact] + public async Task SelectAsync_SingleValidCandidateInGroup_ChoosesCandidate() + { + // Arrange + var endpoints = new RouteEndpoint[] { CreateEndpoint("/test1"), CreateEndpoint("/test2"), CreateEndpoint("/test3"), }; + var scores = new int[] { 0, 0, 1 }; + var candidateSet = CreateCandidateSet(endpoints, scores); - candidateSet.SetValidity(0, false); - candidateSet.SetValidity(1, true); - candidateSet.SetValidity(2, true); + candidateSet.SetValidity(0, false); + candidateSet.SetValidity(1, true); + candidateSet.SetValidity(2, true); - var httpContext = CreateContext(); - var selector = CreateSelector(); + var httpContext = CreateContext(); + var selector = CreateSelector(); - // Act - await selector.SelectAsync(httpContext, candidateSet); + // Act + await selector.SelectAsync(httpContext, candidateSet); - // Assert - Assert.Same(endpoints[1], httpContext.GetEndpoint()); - } + // Assert + Assert.Same(endpoints[1], httpContext.GetEndpoint()); + } - [Fact] - public async Task SelectAsync_ManyGroupsLastCandidate_ChoosesCandidate() + [Fact] + public async Task SelectAsync_ManyGroupsLastCandidate_ChoosesCandidate() + { + // Arrange + var endpoints = new RouteEndpoint[] { - // Arrange - var endpoints = new RouteEndpoint[] - { CreateEndpoint("/test1"), CreateEndpoint("/test2"), CreateEndpoint("/test3"), CreateEndpoint("/test4"), CreateEndpoint("/test5"), - }; - var scores = new int[] { 0, 1, 2, 3, 4 }; - var candidateSet = CreateCandidateSet(endpoints, scores); + }; + var scores = new int[] { 0, 1, 2, 3, 4 }; + var candidateSet = CreateCandidateSet(endpoints, scores); - candidateSet.SetValidity(0, false); - candidateSet.SetValidity(1, false); - candidateSet.SetValidity(2, false); - candidateSet.SetValidity(3, false); - candidateSet.SetValidity(4, true); + candidateSet.SetValidity(0, false); + candidateSet.SetValidity(1, false); + candidateSet.SetValidity(2, false); + candidateSet.SetValidity(3, false); + candidateSet.SetValidity(4, true); - var httpContext = CreateContext(); - var selector = CreateSelector(); + var httpContext = CreateContext(); + var selector = CreateSelector(); - // Act - await selector.SelectAsync(httpContext, candidateSet); + // Act + await selector.SelectAsync(httpContext, candidateSet); - // Assert - Assert.Same(endpoints[4], httpContext.GetEndpoint()); - } + // Assert + Assert.Same(endpoints[4], httpContext.GetEndpoint()); + } - [Fact] - public async Task SelectAsync_MultipleValidCandidatesInGroup_ReportsAmbiguity() - { - // Arrange - var endpoints = new RouteEndpoint[] { CreateEndpoint("/test1"), CreateEndpoint("/test2"), CreateEndpoint("/test3"), }; - var scores = new int[] { 0, 1, 1 }; - var candidateSet = CreateCandidateSet(endpoints, scores); + [Fact] + public async Task SelectAsync_MultipleValidCandidatesInGroup_ReportsAmbiguity() + { + // Arrange + var endpoints = new RouteEndpoint[] { CreateEndpoint("/test1"), CreateEndpoint("/test2"), CreateEndpoint("/test3"), }; + var scores = new int[] { 0, 1, 1 }; + var candidateSet = CreateCandidateSet(endpoints, scores); - candidateSet.SetValidity(0, false); - candidateSet.SetValidity(1, true); - candidateSet.SetValidity(2, true); + candidateSet.SetValidity(0, false); + candidateSet.SetValidity(1, true); + candidateSet.SetValidity(2, true); - var httpContext = CreateContext(); - var selector = CreateSelector(); + var httpContext = CreateContext(); + var selector = CreateSelector(); - // Act - var ex = await Assert.ThrowsAsync(() => selector.SelectAsync(httpContext, candidateSet)); + // Act + var ex = await Assert.ThrowsAsync(() => selector.SelectAsync(httpContext, candidateSet)); - // Assert - Assert.Equal( + // Assert + Assert.Equal( @"The request matched multiple endpoints. Matches: " + Environment.NewLine + Environment.NewLine + "test: /test2" + Environment.NewLine + "test: /test3", ex.Message); - Assert.Null(httpContext.GetEndpoint()); - } + Assert.Null(httpContext.GetEndpoint()); + } - private static HttpContext CreateContext() - { - return new DefaultHttpContext(); - } + private static HttpContext CreateContext() + { + return new DefaultHttpContext(); + } - private static RouteEndpoint CreateEndpoint(string template) - { - return new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse(template), - 0, - EndpointMetadataCollection.Empty, - $"test: {template}"); - } - - private static CandidateSet CreateCandidateSet(RouteEndpoint[] endpoints, int[] scores) - { - return new CandidateSet(endpoints, new RouteValueDictionary[endpoints.Length], scores); - } + private static RouteEndpoint CreateEndpoint(string template) + { + return new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse(template), + 0, + EndpointMetadataCollection.Empty, + $"test: {template}"); + } - private static DefaultEndpointSelector CreateSelector() - { - return new DefaultEndpointSelector(); - } + private static CandidateSet CreateCandidateSet(RouteEndpoint[] endpoints, int[] scores) + { + return new CandidateSet(endpoints, new RouteValueDictionary[endpoints.Length], scores); + } + + private static DefaultEndpointSelector CreateSelector() + { + return new DefaultEndpointSelector(); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/DfaMatcherBuilderTest.cs b/src/Http/Routing/test/UnitTests/Matching/DfaMatcherBuilderTest.cs index 64487a90c6..334fbd268f 100644 --- a/src/Http/Routing/test/UnitTests/Matching/DfaMatcherBuilderTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/DfaMatcherBuilderTest.cs @@ -14,1059 +14,1059 @@ using Microsoft.Extensions.Options; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class DfaMatcherBuilderTest { - public class DfaMatcherBuilderTest + [Fact] + public void BuildDfaTree_SingleEndpoint_Empty() { - [Fact] - public void BuildDfaTree_SingleEndpoint_Empty() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint = CreateEndpoint("/"); - builder.AddEndpoint(endpoint); + var endpoint = CreateEndpoint("/"); + builder.AddEndpoint(endpoint); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Same(endpoint, Assert.Single(root.Matches)); - Assert.Null(root.Parameters); - Assert.Null(root.Literals); - } + // Assert + Assert.Same(endpoint, Assert.Single(root.Matches)); + Assert.Null(root.Parameters); + Assert.Null(root.Literals); + } - [Fact] - public void BuildDfaTree_SingleEndpoint_Literals() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_SingleEndpoint_Literals() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint = CreateEndpoint("a/b/c"); - builder.AddEndpoint(endpoint); + var endpoint = CreateEndpoint("a/b/c"); + builder.AddEndpoint(endpoint); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); - var a = next.Value; - Assert.Null(a.Matches); - Assert.Null(a.Parameters); + var a = next.Value; + Assert.Null(a.Matches); + Assert.Null(a.Parameters); - next = Assert.Single(a.Literals); - Assert.Equal("b", next.Key); + next = Assert.Single(a.Literals); + Assert.Equal("b", next.Key); - var b = next.Value; - Assert.Null(b.Matches); - Assert.Null(b.Parameters); + var b = next.Value; + Assert.Null(b.Matches); + Assert.Null(b.Parameters); - next = Assert.Single(b.Literals); - Assert.Equal("c", next.Key); + next = Assert.Single(b.Literals); + Assert.Equal("c", next.Key); - var c = next.Value; - Assert.Same(endpoint, Assert.Single(c.Matches)); - Assert.Null(c.Parameters); - Assert.Null(c.Literals); - } + var c = next.Value; + Assert.Same(endpoint, Assert.Single(c.Matches)); + Assert.Null(c.Parameters); + Assert.Null(c.Literals); + } - [Fact] - public void BuildDfaTree_SingleEndpoint_Parameters() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_SingleEndpoint_Parameters() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint = CreateEndpoint("{a}/{b}/{c}"); - builder.AddEndpoint(endpoint); + var endpoint = CreateEndpoint("{a}/{b}/{c}"); + builder.AddEndpoint(endpoint); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Literals); + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Literals); - var a = root.Parameters; - Assert.Null(a.Matches); - Assert.Null(a.Literals); + var a = root.Parameters; + Assert.Null(a.Matches); + Assert.Null(a.Literals); - var b = a.Parameters; - Assert.Null(b.Matches); - Assert.Null(b.Literals); + var b = a.Parameters; + Assert.Null(b.Matches); + Assert.Null(b.Literals); - var c = b.Parameters; - Assert.Same(endpoint, Assert.Single(c.Matches)); - Assert.Null(c.Parameters); - Assert.Null(c.Literals); - } + var c = b.Parameters; + Assert.Same(endpoint, Assert.Single(c.Matches)); + Assert.Null(c.Parameters); + Assert.Null(c.Literals); + } - [Fact] - public void BuildDfaTree_SingleEndpoint_CatchAll() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_SingleEndpoint_CatchAll() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint = CreateEndpoint("{a}/{*b}"); - builder.AddEndpoint(endpoint); + var endpoint = CreateEndpoint("{a}/{*b}"); + builder.AddEndpoint(endpoint); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Literals); + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Literals); - var a = root.Parameters; + var a = root.Parameters; - // The catch all can match a path like '/a' - Assert.Same(endpoint, Assert.Single(a.Matches)); - Assert.Null(a.Literals); - Assert.Null(a.Parameters); + // The catch all can match a path like '/a' + Assert.Same(endpoint, Assert.Single(a.Matches)); + Assert.Null(a.Literals); + Assert.Null(a.Parameters); - // Catch-all nodes include an extra transition that loops to process - // extra segments. - var catchAll = a.CatchAll; - Assert.Same(endpoint, Assert.Single(catchAll.Matches)); - Assert.Null(catchAll.Literals); - Assert.Same(catchAll, catchAll.Parameters); - Assert.Same(catchAll, catchAll.CatchAll); - } + // Catch-all nodes include an extra transition that loops to process + // extra segments. + var catchAll = a.CatchAll; + Assert.Same(endpoint, Assert.Single(catchAll.Matches)); + Assert.Null(catchAll.Literals); + Assert.Same(catchAll, catchAll.Parameters); + Assert.Same(catchAll, catchAll.CatchAll); + } - [Fact] - public void BuildDfaTree_SingleEndpoint_CatchAllAtRoot() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_SingleEndpoint_CatchAllAtRoot() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint = CreateEndpoint("{*a}"); - builder.AddEndpoint(endpoint); + var endpoint = CreateEndpoint("{*a}"); + builder.AddEndpoint(endpoint); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Same(endpoint, Assert.Single(root.Matches)); - Assert.Null(root.Literals); + // Assert + Assert.Same(endpoint, Assert.Single(root.Matches)); + Assert.Null(root.Literals); - // Catch-all nodes include an extra transition that loops to process - // extra segments. - var catchAll = root.CatchAll; - Assert.Same(endpoint, Assert.Single(catchAll.Matches)); - Assert.Null(catchAll.Literals); - Assert.Same(catchAll, catchAll.Parameters); - } + // Catch-all nodes include an extra transition that loops to process + // extra segments. + var catchAll = root.CatchAll; + Assert.Same(endpoint, Assert.Single(catchAll.Matches)); + Assert.Null(catchAll.Literals); + Assert.Same(catchAll, catchAll.Parameters); + } - [Fact] - public void BuildDfaTree_MultipleEndpoint_LiteralAndLiteral() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_MultipleEndpoint_LiteralAndLiteral() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint1 = CreateEndpoint("a/b1/c"); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint("a/b1/c"); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("a/b2/c"); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("a/b2/c"); + builder.AddEndpoint(endpoint2); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); - var a = next.Value; - Assert.Null(a.Matches); + var a = next.Value; + Assert.Null(a.Matches); - Assert.Equal(2, a.Literals.Count); + Assert.Equal(2, a.Literals.Count); - var b1 = a.Literals["b1"]; - Assert.Null(b1.Matches); - Assert.Null(b1.Parameters); + var b1 = a.Literals["b1"]; + Assert.Null(b1.Matches); + Assert.Null(b1.Parameters); - next = Assert.Single(b1.Literals); - Assert.Equal("c", next.Key); + next = Assert.Single(b1.Literals); + Assert.Equal("c", next.Key); - var c1 = next.Value; - Assert.Same(endpoint1, Assert.Single(c1.Matches)); - Assert.Null(c1.Parameters); - Assert.Null(c1.Literals); + var c1 = next.Value; + Assert.Same(endpoint1, Assert.Single(c1.Matches)); + Assert.Null(c1.Parameters); + Assert.Null(c1.Literals); - var b2 = a.Literals["b2"]; - Assert.Null(b2.Matches); - Assert.Null(b2.Parameters); + var b2 = a.Literals["b2"]; + Assert.Null(b2.Matches); + Assert.Null(b2.Parameters); - next = Assert.Single(b2.Literals); - Assert.Equal("c", next.Key); + next = Assert.Single(b2.Literals); + Assert.Equal("c", next.Key); - var c2 = next.Value; - Assert.Same(endpoint2, Assert.Single(c2.Matches)); - Assert.Null(c2.Parameters); - Assert.Null(c2.Literals); - } + var c2 = next.Value; + Assert.Same(endpoint2, Assert.Single(c2.Matches)); + Assert.Null(c2.Parameters); + Assert.Null(c2.Literals); + } - [Fact] - public void BuildDfaTree_MultipleEndpoint_LiteralDifferentCase() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_MultipleEndpoint_LiteralDifferentCase() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint1 = CreateEndpoint("a/b1/c"); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint("a/b1/c"); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("A/b2/c"); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("A/b2/c"); + builder.AddEndpoint(endpoint2); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); - var a = next.Value; - Assert.Null(a.Matches); + var a = next.Value; + Assert.Null(a.Matches); - Assert.Equal(2, a.Literals.Count); + Assert.Equal(2, a.Literals.Count); - var b1 = a.Literals["b1"]; - Assert.Null(b1.Matches); - Assert.Null(b1.Parameters); + var b1 = a.Literals["b1"]; + Assert.Null(b1.Matches); + Assert.Null(b1.Parameters); - next = Assert.Single(b1.Literals); - Assert.Equal("c", next.Key); + next = Assert.Single(b1.Literals); + Assert.Equal("c", next.Key); - var c1 = next.Value; - Assert.Same(endpoint1, Assert.Single(c1.Matches)); - Assert.Null(c1.Parameters); - Assert.Null(c1.Literals); + var c1 = next.Value; + Assert.Same(endpoint1, Assert.Single(c1.Matches)); + Assert.Null(c1.Parameters); + Assert.Null(c1.Literals); - var b2 = a.Literals["b2"]; - Assert.Null(b2.Matches); - Assert.Null(b2.Parameters); + var b2 = a.Literals["b2"]; + Assert.Null(b2.Matches); + Assert.Null(b2.Parameters); - next = Assert.Single(b2.Literals); - Assert.Equal("c", next.Key); + next = Assert.Single(b2.Literals); + Assert.Equal("c", next.Key); - var c2 = next.Value; - Assert.Same(endpoint2, Assert.Single(c2.Matches)); - Assert.Null(c2.Parameters); - Assert.Null(c2.Literals); - } + var c2 = next.Value; + Assert.Same(endpoint2, Assert.Single(c2.Matches)); + Assert.Null(c2.Parameters); + Assert.Null(c2.Literals); + } - [Fact] - public void BuildDfaTree_MultipleEndpoint_LiteralAndParameter() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_MultipleEndpoint_LiteralAndParameter() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint1 = CreateEndpoint("a/b/c"); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint("a/b/c"); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("a/{b}/c"); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("a/{b}/c"); + builder.AddEndpoint(endpoint2); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); - var a = next.Value; - Assert.Null(a.Matches); + var a = next.Value; + Assert.Null(a.Matches); - next = Assert.Single(a.Literals); - Assert.Equal("b", next.Key); + next = Assert.Single(a.Literals); + Assert.Equal("b", next.Key); - var b = next.Value; - Assert.Null(b.Matches); - Assert.Null(b.Parameters); + var b = next.Value; + Assert.Null(b.Matches); + Assert.Null(b.Parameters); - next = Assert.Single(b.Literals); - Assert.Equal("c", next.Key); + next = Assert.Single(b.Literals); + Assert.Equal("c", next.Key); - var c1 = next.Value; - Assert.Collection( - c1.Matches, - e => Assert.Same(endpoint1, e), - e => Assert.Same(endpoint2, e)); - Assert.Null(c1.Parameters); - Assert.Null(c1.Literals); + var c1 = next.Value; + Assert.Collection( + c1.Matches, + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e)); + Assert.Null(c1.Parameters); + Assert.Null(c1.Literals); - var b2 = a.Parameters; - Assert.Null(b2.Matches); - Assert.Null(b2.Parameters); + var b2 = a.Parameters; + Assert.Null(b2.Matches); + Assert.Null(b2.Parameters); - next = Assert.Single(b2.Literals); - Assert.Equal("c", next.Key); + next = Assert.Single(b2.Literals); + Assert.Equal("c", next.Key); - var c2 = next.Value; - Assert.Same(endpoint2, Assert.Single(c2.Matches)); - Assert.Null(c2.Parameters); - Assert.Null(c2.Literals); - } + var c2 = next.Value; + Assert.Same(endpoint2, Assert.Single(c2.Matches)); + Assert.Null(c2.Parameters); + Assert.Null(c2.Literals); + } - [Fact] - public void BuildDfaTree_MultipleEndpoint_ParameterAndParameter() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_MultipleEndpoint_ParameterAndParameter() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint1 = CreateEndpoint("a/{b1}/c"); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint("a/{b1}/c"); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("a/{b2}/c"); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("a/{b2}/c"); + builder.AddEndpoint(endpoint2); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); - var a = next.Value; - Assert.Null(a.Matches); - Assert.Null(a.Literals); + var a = next.Value; + Assert.Null(a.Matches); + Assert.Null(a.Literals); - var b = a.Parameters; - Assert.Null(b.Matches); - Assert.Null(b.Parameters); + var b = a.Parameters; + Assert.Null(b.Matches); + Assert.Null(b.Parameters); - next = Assert.Single(b.Literals); - Assert.Equal("c", next.Key); + next = Assert.Single(b.Literals); + Assert.Equal("c", next.Key); - var c = next.Value; - Assert.Collection( - c.Matches, - e => Assert.Same(endpoint1, e), - e => Assert.Same(endpoint2, e)); - Assert.Null(c.Parameters); - Assert.Null(c.Literals); - } + var c = next.Value; + Assert.Collection( + c.Matches, + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e)); + Assert.Null(c.Parameters); + Assert.Null(c.Literals); + } - [Fact] - public void BuildDfaTree_MultipleEndpoint_LiteralAndCatchAll() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_MultipleEndpoint_LiteralAndCatchAll() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint1 = CreateEndpoint("a/b/c"); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint("a/b/c"); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("a/{*b}"); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("a/{*b}"); + builder.AddEndpoint(endpoint2); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); - var a = next.Value; - Assert.Same(endpoint2, Assert.Single(a.Matches)); + var a = next.Value; + Assert.Same(endpoint2, Assert.Single(a.Matches)); - next = Assert.Single(a.Literals); - Assert.Equal("b", next.Key); + next = Assert.Single(a.Literals); + Assert.Equal("b", next.Key); - var b1 = next.Value; - Assert.Same(endpoint2, Assert.Single(a.Matches)); - Assert.Null(b1.Parameters); + var b1 = next.Value; + Assert.Same(endpoint2, Assert.Single(a.Matches)); + Assert.Null(b1.Parameters); - next = Assert.Single(b1.Literals); - Assert.Equal("c", next.Key); + next = Assert.Single(b1.Literals); + Assert.Equal("c", next.Key); - var c1 = next.Value; - Assert.Collection( - c1.Matches, - e => Assert.Same(endpoint1, e), - e => Assert.Same(endpoint2, e)); - Assert.Null(c1.Parameters); - Assert.Null(c1.Literals); + var c1 = next.Value; + Assert.Collection( + c1.Matches, + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e)); + Assert.Null(c1.Parameters); + Assert.Null(c1.Literals); - var catchAll = a.CatchAll; - Assert.Same(endpoint2, Assert.Single(catchAll.Matches)); - Assert.Same(catchAll, catchAll.Parameters); - Assert.Same(catchAll, catchAll.CatchAll); - } + var catchAll = a.CatchAll; + Assert.Same(endpoint2, Assert.Single(catchAll.Matches)); + Assert.Same(catchAll, catchAll.Parameters); + Assert.Same(catchAll, catchAll.CatchAll); + } - [Fact] - public void BuildDfaTree_MultipleEndpoint_ParameterAndCatchAll() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_MultipleEndpoint_ParameterAndCatchAll() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint1 = CreateEndpoint("a/{b}/c"); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint("a/{b}/c"); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("a/{*b}"); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("a/{*b}"); + builder.AddEndpoint(endpoint2); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); - var a = next.Value; - Assert.Same(endpoint2, Assert.Single(a.Matches)); - Assert.Null(a.Literals); + var a = next.Value; + Assert.Same(endpoint2, Assert.Single(a.Matches)); + Assert.Null(a.Literals); - var b1 = a.Parameters; - Assert.Same(endpoint2, Assert.Single(a.Matches)); - Assert.Null(b1.Parameters); + var b1 = a.Parameters; + Assert.Same(endpoint2, Assert.Single(a.Matches)); + Assert.Null(b1.Parameters); - next = Assert.Single(b1.Literals); - Assert.Equal("c", next.Key); + next = Assert.Single(b1.Literals); + Assert.Equal("c", next.Key); - var c1 = next.Value; - Assert.Collection( - c1.Matches, - e => Assert.Same(endpoint1, e), - e => Assert.Same(endpoint2, e)); - Assert.Null(c1.Parameters); - Assert.Null(c1.Literals); + var c1 = next.Value; + Assert.Collection( + c1.Matches, + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e)); + Assert.Null(c1.Parameters); + Assert.Null(c1.Literals); - var catchAll = a.CatchAll; - Assert.Same(endpoint2, Assert.Single(catchAll.Matches)); - Assert.Same(catchAll, catchAll.Parameters); - Assert.Same(catchAll, catchAll.CatchAll); - } + var catchAll = a.CatchAll; + Assert.Same(endpoint2, Assert.Single(catchAll.Matches)); + Assert.Same(catchAll, catchAll.Parameters); + Assert.Same(catchAll, catchAll.CatchAll); + } - [Fact] - public void BuildDfaTree_MultipleEndpoint_ConstrainedParameterTrimming_DoesNotMeetConstraint() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); - - var endpoint1 = CreateEndpoint("a/c"); - builder.AddEndpoint(endpoint1); - - var endpoint2 = CreateEndpoint("{a:length(2)}/b/c"); - builder.AddEndpoint(endpoint2); - - // Act - var root = builder.BuildDfaTree(); - - // Assert - Assert.Null(root.Matches); - Assert.NotNull(root.Parameters); - - var aNodeKvp = Assert.Single(root.Literals); - Assert.Equal("a", aNodeKvp.Key); - - var aNodeValue = aNodeKvp.Value; - var cNodeKvp = Assert.Single(aNodeValue.Literals); - Assert.Equal("c", cNodeKvp.Key); - var cNode = cNodeKvp.Value; - - Assert.Same(endpoint1, Assert.Single(cNode.Matches)); - Assert.Null(cNode.Literals); - Assert.Null(cNode.Parameters); - - var bNodeKvp = Assert.Single(root.Parameters.Literals); - Assert.Equal("b", bNodeKvp.Key); - var bNode = bNodeKvp.Value; - Assert.Null(bNode.Parameters); - Assert.Null(bNode.Matches); - var paramCNodeKvp = Assert.Single(bNode.Literals); - - Assert.Equal("c", paramCNodeKvp.Key); - var paramCNode = paramCNodeKvp.Value; - Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); - Assert.Null(paramCNode.Literals); - Assert.Null(paramCNode.Parameters); - } + [Fact] + public void BuildDfaTree_MultipleEndpoint_ConstrainedParameterTrimming_DoesNotMeetConstraint() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); + + var endpoint1 = CreateEndpoint("a/c"); + builder.AddEndpoint(endpoint1); + + var endpoint2 = CreateEndpoint("{a:length(2)}/b/c"); + builder.AddEndpoint(endpoint2); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Null(root.Matches); + Assert.NotNull(root.Parameters); + + var aNodeKvp = Assert.Single(root.Literals); + Assert.Equal("a", aNodeKvp.Key); + + var aNodeValue = aNodeKvp.Value; + var cNodeKvp = Assert.Single(aNodeValue.Literals); + Assert.Equal("c", cNodeKvp.Key); + var cNode = cNodeKvp.Value; + + Assert.Same(endpoint1, Assert.Single(cNode.Matches)); + Assert.Null(cNode.Literals); + Assert.Null(cNode.Parameters); + + var bNodeKvp = Assert.Single(root.Parameters.Literals); + Assert.Equal("b", bNodeKvp.Key); + var bNode = bNodeKvp.Value; + Assert.Null(bNode.Parameters); + Assert.Null(bNode.Matches); + var paramCNodeKvp = Assert.Single(bNode.Literals); + + Assert.Equal("c", paramCNodeKvp.Key); + var paramCNode = paramCNodeKvp.Value; + Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); + Assert.Null(paramCNode.Literals); + Assert.Null(paramCNode.Parameters); + } - [Theory] - [InlineData("aa/c", "aa", "c")] - [InlineData("1/c", "1", "c")] - public void BuildDfaTree_MultipleEndpoint_ConstrainedParameterTrimming_EvaluatesAllConstraints(string candidate, string firstSegment, string secondSegment) - { - // Arrange - var builder = CreateDfaMatcherBuilder(); - - var endpoint1 = CreateEndpoint(candidate); - builder.AddEndpoint(endpoint1); - - var endpoint2 = CreateEndpoint("{a:int:length(2)}/b/c"); - builder.AddEndpoint(endpoint2); - - // Act - var root = builder.BuildDfaTree(); - - // Assert - Assert.Null(root.Matches); - Assert.NotNull(root.Parameters); - - var aNodeKvp = Assert.Single(root.Literals); - Assert.Equal(firstSegment, aNodeKvp.Key); - - var aNodeValue = aNodeKvp.Value; - var cNodeKvp = Assert.Single(aNodeValue.Literals); - Assert.Equal(secondSegment, cNodeKvp.Key); - var cNode = cNodeKvp.Value; - - Assert.Same(endpoint1, Assert.Single(cNode.Matches)); - Assert.Null(cNode.Literals); - Assert.Null(cNode.Parameters); - - var bNodeKvp = Assert.Single(root.Parameters.Literals); - Assert.Equal("b", bNodeKvp.Key); - var bNode = bNodeKvp.Value; - Assert.Null(bNode.Parameters); - Assert.Null(bNode.Matches); - var paramCNodeKvp = Assert.Single(bNode.Literals); - - Assert.Equal("c", paramCNodeKvp.Key); - var paramCNode = paramCNodeKvp.Value; - Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); - Assert.Null(paramCNode.Literals); - Assert.Null(paramCNode.Parameters); - } + [Theory] + [InlineData("aa/c", "aa", "c")] + [InlineData("1/c", "1", "c")] + public void BuildDfaTree_MultipleEndpoint_ConstrainedParameterTrimming_EvaluatesAllConstraints(string candidate, string firstSegment, string secondSegment) + { + // Arrange + var builder = CreateDfaMatcherBuilder(); + + var endpoint1 = CreateEndpoint(candidate); + builder.AddEndpoint(endpoint1); + + var endpoint2 = CreateEndpoint("{a:int:length(2)}/b/c"); + builder.AddEndpoint(endpoint2); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Null(root.Matches); + Assert.NotNull(root.Parameters); + + var aNodeKvp = Assert.Single(root.Literals); + Assert.Equal(firstSegment, aNodeKvp.Key); + + var aNodeValue = aNodeKvp.Value; + var cNodeKvp = Assert.Single(aNodeValue.Literals); + Assert.Equal(secondSegment, cNodeKvp.Key); + var cNode = cNodeKvp.Value; + + Assert.Same(endpoint1, Assert.Single(cNode.Matches)); + Assert.Null(cNode.Literals); + Assert.Null(cNode.Parameters); + + var bNodeKvp = Assert.Single(root.Parameters.Literals); + Assert.Equal("b", bNodeKvp.Key); + var bNode = bNodeKvp.Value; + Assert.Null(bNode.Parameters); + Assert.Null(bNode.Matches); + var paramCNodeKvp = Assert.Single(bNode.Literals); + + Assert.Equal("c", paramCNodeKvp.Key); + var paramCNode = paramCNodeKvp.Value; + Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); + Assert.Null(paramCNode.Literals); + Assert.Null(paramCNode.Parameters); + } - [Fact] - public void BuildDfaTree_MultipleEndpoint_ConstrainedParameterTrimming_MeetsConstraint() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_MultipleEndpoint_ConstrainedParameterTrimming_MeetsConstraint() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint1 = CreateEndpoint("aa/c"); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint("aa/c"); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("{a:length(2)}/b/c"); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("{a:length(2)}/b/c"); + builder.AddEndpoint(endpoint2); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.NotNull(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.NotNull(root.Parameters); - // Branch aa -> c = (aa/c) + // Branch aa -> c = (aa/c) - var aNodeKvp = Assert.Single(root.Literals); - Assert.Equal("aa", aNodeKvp.Key); + var aNodeKvp = Assert.Single(root.Literals); + Assert.Equal("aa", aNodeKvp.Key); - var aNodeValue = aNodeKvp.Value; - Assert.True(aNodeValue.Literals.TryGetValue("c", out var cNode)); + var aNodeValue = aNodeKvp.Value; + Assert.True(aNodeValue.Literals.TryGetValue("c", out var cNode)); - Assert.Same(endpoint1, Assert.Single(cNode.Matches)); - Assert.Null(cNode.Literals); - Assert.Null(cNode.Parameters); + Assert.Same(endpoint1, Assert.Single(cNode.Matches)); + Assert.Null(cNode.Literals); + Assert.Null(cNode.Parameters); - // Branch (aa) -> b -> c = ({a:length(2)}/b/c) + // Branch (aa) -> b -> c = ({a:length(2)}/b/c) - Assert.True(aNodeValue.Literals.TryGetValue("b", out var bNode)); - Assert.Null(bNode.Parameters); - Assert.Null(bNode.Matches); - var paramBCNodeKvp = Assert.Single(bNode.Literals); - Assert.Equal("c", paramBCNodeKvp.Key); - var paramBCNode = paramBCNodeKvp.Value; + Assert.True(aNodeValue.Literals.TryGetValue("b", out var bNode)); + Assert.Null(bNode.Parameters); + Assert.Null(bNode.Matches); + var paramBCNodeKvp = Assert.Single(bNode.Literals); + Assert.Equal("c", paramBCNodeKvp.Key); + var paramBCNode = paramBCNodeKvp.Value; - Assert.Same(endpoint2, Assert.Single(paramBCNode.Matches)); - Assert.Null(cNode.Literals); - Assert.Null(cNode.Parameters); + Assert.Same(endpoint2, Assert.Single(paramBCNode.Matches)); + Assert.Null(cNode.Literals); + Assert.Null(cNode.Parameters); - // Branch {param} -> b -> c = ({a:length(2)}/b/c) + // Branch {param} -> b -> c = ({a:length(2)}/b/c) - var bParamNodeKvp = Assert.Single(root.Parameters.Literals); - Assert.Equal("b", bParamNodeKvp.Key); - var bParamNode = bParamNodeKvp.Value; - Assert.Null(bParamNode.Parameters); - Assert.Null(bParamNode.Matches); - var paramCNodeKvp = Assert.Single(bParamNode.Literals); + var bParamNodeKvp = Assert.Single(root.Parameters.Literals); + Assert.Equal("b", bParamNodeKvp.Key); + var bParamNode = bParamNodeKvp.Value; + Assert.Null(bParamNode.Parameters); + Assert.Null(bParamNode.Matches); + var paramCNodeKvp = Assert.Single(bParamNode.Literals); - Assert.Equal("c", paramCNodeKvp.Key); - var paramCNode = paramCNodeKvp.Value; - Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); - Assert.Null(paramCNode.Literals); - Assert.Null(paramCNode.Parameters); - } + Assert.Equal("c", paramCNodeKvp.Key); + var paramCNode = paramCNodeKvp.Value; + Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); + Assert.Null(paramCNode.Literals); + Assert.Null(paramCNode.Parameters); + } - [Fact] - public void BuildDfaTree_MultipleEndpoint_ConstrainedParameterTrimming_BothCandidates_WhenLitteralPatternMeetsConstraintAndRoutePattern() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_MultipleEndpoint_ConstrainedParameterTrimming_BothCandidates_WhenLitteralPatternMeetsConstraintAndRoutePattern() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint1 = CreateEndpoint("aa/b/c"); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint("aa/b/c"); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("{a:length(2)}/b/c"); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("{a:length(2)}/b/c"); + builder.AddEndpoint(endpoint2); - var endpoint3 = CreateEndpoint("aa/c"); - builder.AddEndpoint(endpoint3); + var endpoint3 = CreateEndpoint("aa/c"); + builder.AddEndpoint(endpoint3); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.NotNull(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.NotNull(root.Parameters); - // Branch aa -> c = (aa/c) + // Branch aa -> c = (aa/c) - var aNodeKvp = Assert.Single(root.Literals); - Assert.Equal("aa", aNodeKvp.Key); + var aNodeKvp = Assert.Single(root.Literals); + Assert.Equal("aa", aNodeKvp.Key); - var aNodeValue = aNodeKvp.Value; - Assert.True(aNodeValue.Literals.TryGetValue("c", out var cNode)); + var aNodeValue = aNodeKvp.Value; + Assert.True(aNodeValue.Literals.TryGetValue("c", out var cNode)); - Assert.Same(endpoint3, Assert.Single(cNode.Matches)); - Assert.Null(cNode.Literals); - Assert.Null(cNode.Parameters); + Assert.Same(endpoint3, Assert.Single(cNode.Matches)); + Assert.Null(cNode.Literals); + Assert.Null(cNode.Parameters); - // Branch (aa) -> b -> c = (aa/b/c, {a:length(2)}/b/c) + // Branch (aa) -> b -> c = (aa/b/c, {a:length(2)}/b/c) - Assert.True(aNodeValue.Literals.TryGetValue("b", out var bNode)); - Assert.Null(bNode.Parameters); - Assert.Null(bNode.Matches); - var paramBCNodeKvp = Assert.Single(bNode.Literals); - Assert.Equal("c", paramBCNodeKvp.Key); - var paramBCNode = paramBCNodeKvp.Value; + Assert.True(aNodeValue.Literals.TryGetValue("b", out var bNode)); + Assert.Null(bNode.Parameters); + Assert.Null(bNode.Matches); + var paramBCNodeKvp = Assert.Single(bNode.Literals); + Assert.Equal("c", paramBCNodeKvp.Key); + var paramBCNode = paramBCNodeKvp.Value; - Assert.Equal(new[] { endpoint1, endpoint2 }, paramBCNode.Matches.ToArray()); - Assert.Null(cNode.Literals); - Assert.Null(cNode.Parameters); + Assert.Equal(new[] { endpoint1, endpoint2 }, paramBCNode.Matches.ToArray()); + Assert.Null(cNode.Literals); + Assert.Null(cNode.Parameters); - // Branch {param} -> b -> c = ({a:length(2)}/b/c) + // Branch {param} -> b -> c = ({a:length(2)}/b/c) - var bParamNodeKvp = Assert.Single(root.Parameters.Literals); - Assert.Equal("b", bParamNodeKvp.Key); - var bParamNode = bParamNodeKvp.Value; - Assert.Null(bParamNode.Parameters); - Assert.Null(bParamNode.Matches); - var paramCNodeKvp = Assert.Single(bParamNode.Literals); + var bParamNodeKvp = Assert.Single(root.Parameters.Literals); + Assert.Equal("b", bParamNodeKvp.Key); + var bParamNode = bParamNodeKvp.Value; + Assert.Null(bParamNode.Parameters); + Assert.Null(bParamNode.Matches); + var paramCNodeKvp = Assert.Single(bParamNode.Literals); - Assert.Equal("c", paramCNodeKvp.Key); - var paramCNode = paramCNodeKvp.Value; - Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); - Assert.Null(paramCNode.Literals); - Assert.Null(paramCNode.Parameters); - } + Assert.Equal("c", paramCNodeKvp.Key); + var paramCNode = paramCNodeKvp.Value; + Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); + Assert.Null(paramCNode.Literals); + Assert.Null(paramCNode.Parameters); + } - [Fact] - public void BuildDfaTree_MultipleEndpoint_ComplexParameter_LiteralDoesNotMatchComplexParameter() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); - - var endpoint1 = CreateEndpoint("a/c"); - builder.AddEndpoint(endpoint1); - - var endpoint2 = CreateEndpoint("a{value}/b/c"); - builder.AddEndpoint(endpoint2); - - // Act - var root = builder.BuildDfaTree(); - - // Assert - Assert.Null(root.Matches); - Assert.NotNull(root.Parameters); - - var aNodeKvp = Assert.Single(root.Literals); - Assert.Equal("a", aNodeKvp.Key); - - var aNodeValue = aNodeKvp.Value; - var cNodeKvp = Assert.Single(aNodeValue.Literals); - Assert.Equal("c", cNodeKvp.Key); - var cNode = cNodeKvp.Value; - - Assert.Same(endpoint1, Assert.Single(cNode.Matches)); - Assert.Null(cNode.Literals); - Assert.Null(cNode.Parameters); - - var bNodeKvp = Assert.Single(root.Parameters.Literals); - Assert.Equal("b", bNodeKvp.Key); - var bNode = bNodeKvp.Value; - Assert.Null(bNode.Parameters); - Assert.Null(bNode.Matches); - var paramCNodeKvp = Assert.Single(bNode.Literals); - - Assert.Equal("c", paramCNodeKvp.Key); - var paramCNode = paramCNodeKvp.Value; - Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); - Assert.Null(paramCNode.Literals); - Assert.Null(paramCNode.Parameters); - } + [Fact] + public void BuildDfaTree_MultipleEndpoint_ComplexParameter_LiteralDoesNotMatchComplexParameter() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); + + var endpoint1 = CreateEndpoint("a/c"); + builder.AddEndpoint(endpoint1); + + var endpoint2 = CreateEndpoint("a{value}/b/c"); + builder.AddEndpoint(endpoint2); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Null(root.Matches); + Assert.NotNull(root.Parameters); + + var aNodeKvp = Assert.Single(root.Literals); + Assert.Equal("a", aNodeKvp.Key); + + var aNodeValue = aNodeKvp.Value; + var cNodeKvp = Assert.Single(aNodeValue.Literals); + Assert.Equal("c", cNodeKvp.Key); + var cNode = cNodeKvp.Value; + + Assert.Same(endpoint1, Assert.Single(cNode.Matches)); + Assert.Null(cNode.Literals); + Assert.Null(cNode.Parameters); + + var bNodeKvp = Assert.Single(root.Parameters.Literals); + Assert.Equal("b", bNodeKvp.Key); + var bNode = bNodeKvp.Value; + Assert.Null(bNode.Parameters); + Assert.Null(bNode.Matches); + var paramCNodeKvp = Assert.Single(bNode.Literals); + + Assert.Equal("c", paramCNodeKvp.Key); + var paramCNode = paramCNodeKvp.Value; + Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); + Assert.Null(paramCNode.Literals); + Assert.Null(paramCNode.Parameters); + } - [Fact] - public void BuildDfaTree_MultipleEndpoint_ComplexParameter_LiteralMatchesComplexParameter() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_MultipleEndpoint_ComplexParameter_LiteralMatchesComplexParameter() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint1 = CreateEndpoint("aa/c"); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint("aa/c"); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("a{value}/b/c"); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("a{value}/b/c"); + builder.AddEndpoint(endpoint2); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.NotNull(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.NotNull(root.Parameters); - // Branch aa -> c = (aa/c) + // Branch aa -> c = (aa/c) - var aNodeKvp = Assert.Single(root.Literals); - Assert.Equal("aa", aNodeKvp.Key); + var aNodeKvp = Assert.Single(root.Literals); + Assert.Equal("aa", aNodeKvp.Key); - var aNodeValue = aNodeKvp.Value; - Assert.True(aNodeValue.Literals.TryGetValue("c", out var cNode)); + var aNodeValue = aNodeKvp.Value; + Assert.True(aNodeValue.Literals.TryGetValue("c", out var cNode)); - Assert.Same(endpoint1, Assert.Single(cNode.Matches)); - Assert.Null(cNode.Literals); - Assert.Null(cNode.Parameters); + Assert.Same(endpoint1, Assert.Single(cNode.Matches)); + Assert.Null(cNode.Literals); + Assert.Null(cNode.Parameters); - // Branch (aa) -> b -> c = (a{value}/b/c) + // Branch (aa) -> b -> c = (a{value}/b/c) - Assert.True(aNodeValue.Literals.TryGetValue("b", out var bNode)); - Assert.Null(bNode.Parameters); - Assert.Null(bNode.Matches); - var paramBCNodeKvp = Assert.Single(bNode.Literals); - Assert.Equal("c", paramBCNodeKvp.Key); - var paramBCNode = paramBCNodeKvp.Value; + Assert.True(aNodeValue.Literals.TryGetValue("b", out var bNode)); + Assert.Null(bNode.Parameters); + Assert.Null(bNode.Matches); + var paramBCNodeKvp = Assert.Single(bNode.Literals); + Assert.Equal("c", paramBCNodeKvp.Key); + var paramBCNode = paramBCNodeKvp.Value; - Assert.Same(endpoint2, Assert.Single(paramBCNode.Matches)); - Assert.Null(cNode.Literals); - Assert.Null(cNode.Parameters); + Assert.Same(endpoint2, Assert.Single(paramBCNode.Matches)); + Assert.Null(cNode.Literals); + Assert.Null(cNode.Parameters); - // Branch {param} -> b -> c = (a{value}/b/c) + // Branch {param} -> b -> c = (a{value}/b/c) - var bParamNodeKvp = Assert.Single(root.Parameters.Literals); - Assert.Equal("b", bParamNodeKvp.Key); - var bParamNode = bParamNodeKvp.Value; - Assert.Null(bParamNode.Parameters); - Assert.Null(bParamNode.Matches); - var paramCNodeKvp = Assert.Single(bParamNode.Literals); + var bParamNodeKvp = Assert.Single(root.Parameters.Literals); + Assert.Equal("b", bParamNodeKvp.Key); + var bParamNode = bParamNodeKvp.Value; + Assert.Null(bParamNode.Parameters); + Assert.Null(bParamNode.Matches); + var paramCNodeKvp = Assert.Single(bParamNode.Literals); - Assert.Equal("c", paramCNodeKvp.Key); - var paramCNode = paramCNodeKvp.Value; - Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); - Assert.Null(paramCNode.Literals); - Assert.Null(paramCNode.Parameters); - } + Assert.Equal("c", paramCNodeKvp.Key); + var paramCNode = paramCNodeKvp.Value; + Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); + Assert.Null(paramCNode.Literals); + Assert.Null(paramCNode.Parameters); + } - [Fact] - public void BuildDfaTree_MultipleEndpoint_ConstrainedComplexParameter_LiteralMatchesComplexParameterButNotConstraint() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); - - var endpoint1 = CreateEndpoint("aa/c"); - builder.AddEndpoint(endpoint1); - - var endpoint2 = CreateEndpoint("a{value:int}/b/c"); - builder.AddEndpoint(endpoint2); - - // Act - var root = builder.BuildDfaTree(); - - // Assert - Assert.Null(root.Matches); - Assert.NotNull(root.Parameters); - - var aNodeKvp = Assert.Single(root.Literals); - Assert.Equal("aa", aNodeKvp.Key); - - var aNodeValue = aNodeKvp.Value; - var cNodeKvp = Assert.Single(aNodeValue.Literals); - Assert.Equal("c", cNodeKvp.Key); - var cNode = cNodeKvp.Value; - - Assert.Same(endpoint1, Assert.Single(cNode.Matches)); - Assert.Null(cNode.Literals); - Assert.Null(cNode.Parameters); - - var bNodeKvp = Assert.Single(root.Parameters.Literals); - Assert.Equal("b", bNodeKvp.Key); - var bNode = bNodeKvp.Value; - Assert.Null(bNode.Parameters); - Assert.Null(bNode.Matches); - var paramCNodeKvp = Assert.Single(bNode.Literals); - - Assert.Equal("c", paramCNodeKvp.Key); - var paramCNode = paramCNodeKvp.Value; - Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); - Assert.Null(paramCNode.Literals); - Assert.Null(paramCNode.Parameters); - } + [Fact] + public void BuildDfaTree_MultipleEndpoint_ConstrainedComplexParameter_LiteralMatchesComplexParameterButNotConstraint() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); + + var endpoint1 = CreateEndpoint("aa/c"); + builder.AddEndpoint(endpoint1); + + var endpoint2 = CreateEndpoint("a{value:int}/b/c"); + builder.AddEndpoint(endpoint2); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Null(root.Matches); + Assert.NotNull(root.Parameters); + + var aNodeKvp = Assert.Single(root.Literals); + Assert.Equal("aa", aNodeKvp.Key); + + var aNodeValue = aNodeKvp.Value; + var cNodeKvp = Assert.Single(aNodeValue.Literals); + Assert.Equal("c", cNodeKvp.Key); + var cNode = cNodeKvp.Value; + + Assert.Same(endpoint1, Assert.Single(cNode.Matches)); + Assert.Null(cNode.Literals); + Assert.Null(cNode.Parameters); + + var bNodeKvp = Assert.Single(root.Parameters.Literals); + Assert.Equal("b", bNodeKvp.Key); + var bNode = bNodeKvp.Value; + Assert.Null(bNode.Parameters); + Assert.Null(bNode.Matches); + var paramCNodeKvp = Assert.Single(bNode.Literals); + + Assert.Equal("c", paramCNodeKvp.Key); + var paramCNode = paramCNodeKvp.Value; + Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); + Assert.Null(paramCNode.Literals); + Assert.Null(paramCNode.Parameters); + } - [Fact] - public void BuildDfaTree_MultipleEndpoint_ComplexParameter_LiteralMatchesComplexParameterAndPartConstraint() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_MultipleEndpoint_ComplexParameter_LiteralMatchesComplexParameterAndPartConstraint() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint1 = CreateEndpoint("a1/c"); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint("a1/c"); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("a{value:int}/b/c"); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("a{value:int}/b/c"); + builder.AddEndpoint(endpoint2); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.NotNull(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.NotNull(root.Parameters); - // Branch aa -> c = (a1/c) + // Branch aa -> c = (a1/c) - var aNodeKvp = Assert.Single(root.Literals); - Assert.Equal("a1", aNodeKvp.Key); + var aNodeKvp = Assert.Single(root.Literals); + Assert.Equal("a1", aNodeKvp.Key); - var aNodeValue = aNodeKvp.Value; - Assert.True(aNodeValue.Literals.TryGetValue("c", out var cNode)); + var aNodeValue = aNodeKvp.Value; + Assert.True(aNodeValue.Literals.TryGetValue("c", out var cNode)); - Assert.Same(endpoint1, Assert.Single(cNode.Matches)); - Assert.Null(cNode.Literals); - Assert.Null(cNode.Parameters); + Assert.Same(endpoint1, Assert.Single(cNode.Matches)); + Assert.Null(cNode.Literals); + Assert.Null(cNode.Parameters); - // Branch (a1) -> b -> c = (a{value:int}/b/c) + // Branch (a1) -> b -> c = (a{value:int}/b/c) - Assert.True(aNodeValue.Literals.TryGetValue("b", out var bNode)); - Assert.Null(bNode.Parameters); - Assert.Null(bNode.Matches); - var paramBCNodeKvp = Assert.Single(bNode.Literals); - Assert.Equal("c", paramBCNodeKvp.Key); - var paramBCNode = paramBCNodeKvp.Value; + Assert.True(aNodeValue.Literals.TryGetValue("b", out var bNode)); + Assert.Null(bNode.Parameters); + Assert.Null(bNode.Matches); + var paramBCNodeKvp = Assert.Single(bNode.Literals); + Assert.Equal("c", paramBCNodeKvp.Key); + var paramBCNode = paramBCNodeKvp.Value; - Assert.Same(endpoint2, Assert.Single(paramBCNode.Matches)); - Assert.Null(cNode.Literals); - Assert.Null(cNode.Parameters); + Assert.Same(endpoint2, Assert.Single(paramBCNode.Matches)); + Assert.Null(cNode.Literals); + Assert.Null(cNode.Parameters); - // Branch {param} -> b -> c = (a{value:int}/b/c) + // Branch {param} -> b -> c = (a{value:int}/b/c) - var bParamNodeKvp = Assert.Single(root.Parameters.Literals); - Assert.Equal("b", bParamNodeKvp.Key); - var bParamNode = bParamNodeKvp.Value; - Assert.Null(bParamNode.Parameters); - Assert.Null(bParamNode.Matches); - var paramCNodeKvp = Assert.Single(bParamNode.Literals); + var bParamNodeKvp = Assert.Single(root.Parameters.Literals); + Assert.Equal("b", bParamNodeKvp.Key); + var bParamNode = bParamNodeKvp.Value; + Assert.Null(bParamNode.Parameters); + Assert.Null(bParamNode.Matches); + var paramCNodeKvp = Assert.Single(bParamNode.Literals); - Assert.Equal("c", paramCNodeKvp.Key); - var paramCNode = paramCNodeKvp.Value; - Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); - Assert.Null(paramCNode.Literals); - Assert.Null(paramCNode.Parameters); - } + Assert.Equal("c", paramCNodeKvp.Key); + var paramCNode = paramCNodeKvp.Value; + Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); + Assert.Null(paramCNode.Literals); + Assert.Null(paramCNode.Parameters); + } - [Fact] - public void BuildDfaTree_MultipleEndpoint_ComplexParameter_EvaluatesAllPartsAndConstraints() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_MultipleEndpoint_ComplexParameter_EvaluatesAllPartsAndConstraints() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint1 = CreateEndpoint("a-11-b-true/c"); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint("a-11-b-true/c"); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("a-{value:int:length(2)}-b-{other:bool}/b/c"); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("a-{value:int:length(2)}-b-{other:bool}/b/c"); + builder.AddEndpoint(endpoint2); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.NotNull(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.NotNull(root.Parameters); - // Branch a11-b-true -> c = (a11-b-true/c) + // Branch a11-b-true -> c = (a11-b-true/c) - var aNodeKvp = Assert.Single(root.Literals); - Assert.Equal("a-11-b-true", aNodeKvp.Key); + var aNodeKvp = Assert.Single(root.Literals); + Assert.Equal("a-11-b-true", aNodeKvp.Key); - var aNodeValue = aNodeKvp.Value; - Assert.True(aNodeValue.Literals.TryGetValue("c", out var cNode)); + var aNodeValue = aNodeKvp.Value; + Assert.True(aNodeValue.Literals.TryGetValue("c", out var cNode)); - Assert.Same(endpoint1, Assert.Single(cNode.Matches)); - Assert.Null(cNode.Literals); - Assert.Null(cNode.Parameters); + Assert.Same(endpoint1, Assert.Single(cNode.Matches)); + Assert.Null(cNode.Literals); + Assert.Null(cNode.Parameters); - // Branch (a-11-b-true) -> b -> c = (a-{value:int:length(2)}-b-{other:bool}/b/c) + // Branch (a-11-b-true) -> b -> c = (a-{value:int:length(2)}-b-{other:bool}/b/c) - Assert.True(aNodeValue.Literals.TryGetValue("b", out var bNode)); - Assert.Null(bNode.Parameters); - Assert.Null(bNode.Matches); - var paramBCNodeKvp = Assert.Single(bNode.Literals); - Assert.Equal("c", paramBCNodeKvp.Key); - var paramBCNode = paramBCNodeKvp.Value; + Assert.True(aNodeValue.Literals.TryGetValue("b", out var bNode)); + Assert.Null(bNode.Parameters); + Assert.Null(bNode.Matches); + var paramBCNodeKvp = Assert.Single(bNode.Literals); + Assert.Equal("c", paramBCNodeKvp.Key); + var paramBCNode = paramBCNodeKvp.Value; - Assert.Same(endpoint2, Assert.Single(paramBCNode.Matches)); - Assert.Null(cNode.Literals); - Assert.Null(cNode.Parameters); + Assert.Same(endpoint2, Assert.Single(paramBCNode.Matches)); + Assert.Null(cNode.Literals); + Assert.Null(cNode.Parameters); - // Branch {param} -> b -> c = (a-{value:int:length(2)}-b-{other:bool}/b/c) + // Branch {param} -> b -> c = (a-{value:int:length(2)}-b-{other:bool}/b/c) - var bParamNodeKvp = Assert.Single(root.Parameters.Literals); - Assert.Equal("b", bParamNodeKvp.Key); - var bParamNode = bParamNodeKvp.Value; - Assert.Null(bParamNode.Parameters); - Assert.Null(bParamNode.Matches); - var paramCNodeKvp = Assert.Single(bParamNode.Literals); + var bParamNodeKvp = Assert.Single(root.Parameters.Literals); + Assert.Equal("b", bParamNodeKvp.Key); + var bParamNode = bParamNodeKvp.Value; + Assert.Null(bParamNode.Parameters); + Assert.Null(bParamNode.Matches); + var paramCNodeKvp = Assert.Single(bParamNode.Literals); - Assert.Equal("c", paramCNodeKvp.Key); - var paramCNode = paramCNodeKvp.Value; - Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); - Assert.Null(paramCNode.Literals); - Assert.Null(paramCNode.Parameters); - } + Assert.Equal("c", paramCNodeKvp.Key); + var paramCNode = paramCNodeKvp.Value; + Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); + Assert.Null(paramCNode.Literals); + Assert.Null(paramCNode.Parameters); + } - [Theory] - [InlineData("a-11-b-true/c", "a-11-b-true", "c")] - [InlineData("a-ddd-b-true/c", "a-ddd-b-true", "c")] - [InlineData("a-111-b-0/c", "a-111-b-0", "c")] - public void BuildDfaTree_MultipleEndpoint_ComplexParameter_Trims_When_OneConstraintFails(string candidate, string firstSegment, string secondSegment) - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Theory] + [InlineData("a-11-b-true/c", "a-11-b-true", "c")] + [InlineData("a-ddd-b-true/c", "a-ddd-b-true", "c")] + [InlineData("a-111-b-0/c", "a-111-b-0", "c")] + public void BuildDfaTree_MultipleEndpoint_ComplexParameter_Trims_When_OneConstraintFails(string candidate, string firstSegment, string secondSegment) + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint1 = CreateEndpoint(candidate); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint(candidate); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("a-{value:int:length(3)}-b-{other:bool}/b/c"); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("a-{value:int:length(3)}-b-{other:bool}/b/c"); + builder.AddEndpoint(endpoint2); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.NotNull(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.NotNull(root.Parameters); - // Branch a-11-b-true -> c = (a11-b-true/c) + // Branch a-11-b-true -> c = (a11-b-true/c) - var aNodeKvp = Assert.Single(root.Literals); - Assert.Equal(firstSegment, aNodeKvp.Key); + var aNodeKvp = Assert.Single(root.Literals); + Assert.Equal(firstSegment, aNodeKvp.Key); - var aNodeValue = aNodeKvp.Value; - var cNodeKvp = aNodeValue.Literals.Single(); - Assert.Equal(secondSegment, cNodeKvp.Key); - var cNode = cNodeKvp.Value; + var aNodeValue = aNodeKvp.Value; + var cNodeKvp = aNodeValue.Literals.Single(); + Assert.Equal(secondSegment, cNodeKvp.Key); + var cNode = cNodeKvp.Value; - Assert.Same(endpoint1, Assert.Single(cNode.Matches)); - Assert.Null(cNode.Literals); - Assert.Null(cNode.Parameters); + Assert.Same(endpoint1, Assert.Single(cNode.Matches)); + Assert.Null(cNode.Literals); + Assert.Null(cNode.Parameters); - // Branch {param} -> b -> c = (a-{value:int:length(2)}-b-{other:bool}/b/c) + // Branch {param} -> b -> c = (a-{value:int:length(2)}-b-{other:bool}/b/c) - var bParamNodeKvp = Assert.Single(root.Parameters.Literals); - Assert.Equal("b", bParamNodeKvp.Key); - var bParamNode = bParamNodeKvp.Value; - Assert.Null(bParamNode.Parameters); - Assert.Null(bParamNode.Matches); - var paramCNodeKvp = Assert.Single(bParamNode.Literals); + var bParamNodeKvp = Assert.Single(root.Parameters.Literals); + Assert.Equal("b", bParamNodeKvp.Key); + var bParamNode = bParamNodeKvp.Value; + Assert.Null(bParamNode.Parameters); + Assert.Null(bParamNode.Matches); + var paramCNodeKvp = Assert.Single(bParamNode.Literals); - Assert.Equal("c", paramCNodeKvp.Key); - var paramCNode = paramCNodeKvp.Value; - Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); - Assert.Null(paramCNode.Literals); - Assert.Null(paramCNode.Parameters); - } + Assert.Equal("c", paramCNodeKvp.Key); + var paramCNode = paramCNodeKvp.Value; + Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); + Assert.Null(paramCNode.Literals); + Assert.Null(paramCNode.Parameters); + } - [Fact] - public void BuildDfaTree_MultipleEndpoint_ComplexParameter_BothCandidates_WhenLitteralPatternMatchesComplexParameterAndRoutePattern() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_MultipleEndpoint_ComplexParameter_BothCandidates_WhenLitteralPatternMatchesComplexParameterAndRoutePattern() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint1 = CreateEndpoint("aa/b/c"); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint("aa/b/c"); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("a{value}/b/c"); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("a{value}/b/c"); + builder.AddEndpoint(endpoint2); - var endpoint3 = CreateEndpoint("aa/c"); - builder.AddEndpoint(endpoint3); + var endpoint3 = CreateEndpoint("aa/c"); + builder.AddEndpoint(endpoint3); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.NotNull(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.NotNull(root.Parameters); - // Branch aa -> c = (aa/c) + // Branch aa -> c = (aa/c) - var aNodeKvp = Assert.Single(root.Literals); - Assert.Equal("aa", aNodeKvp.Key); + var aNodeKvp = Assert.Single(root.Literals); + Assert.Equal("aa", aNodeKvp.Key); - var aNodeValue = aNodeKvp.Value; - Assert.True(aNodeValue.Literals.TryGetValue("c", out var cNode)); + var aNodeValue = aNodeKvp.Value; + Assert.True(aNodeValue.Literals.TryGetValue("c", out var cNode)); - Assert.Same(endpoint3, Assert.Single(cNode.Matches)); - Assert.Null(cNode.Literals); - Assert.Null(cNode.Parameters); + Assert.Same(endpoint3, Assert.Single(cNode.Matches)); + Assert.Null(cNode.Literals); + Assert.Null(cNode.Parameters); - // Branch (aa) -> b -> c = (aa/b/c, a{value}/b/c) + // Branch (aa) -> b -> c = (aa/b/c, a{value}/b/c) - Assert.True(aNodeValue.Literals.TryGetValue("b", out var bNode)); - Assert.Null(bNode.Parameters); - Assert.Null(bNode.Matches); - var paramBCNodeKvp = Assert.Single(bNode.Literals); - Assert.Equal("c", paramBCNodeKvp.Key); - var paramBCNode = paramBCNodeKvp.Value; + Assert.True(aNodeValue.Literals.TryGetValue("b", out var bNode)); + Assert.Null(bNode.Parameters); + Assert.Null(bNode.Matches); + var paramBCNodeKvp = Assert.Single(bNode.Literals); + Assert.Equal("c", paramBCNodeKvp.Key); + var paramBCNode = paramBCNodeKvp.Value; - Assert.Equal(new[] { endpoint1, endpoint2 }, paramBCNode.Matches.ToArray()); - Assert.Null(cNode.Literals); - Assert.Null(cNode.Parameters); + Assert.Equal(new[] { endpoint1, endpoint2 }, paramBCNode.Matches.ToArray()); + Assert.Null(cNode.Literals); + Assert.Null(cNode.Parameters); - // Branch {param} -> b -> c = (a{value}/b/c) + // Branch {param} -> b -> c = (a{value}/b/c) - var bParamNodeKvp = Assert.Single(root.Parameters.Literals); - Assert.Equal("b", bParamNodeKvp.Key); - var bParamNode = bParamNodeKvp.Value; - Assert.Null(bParamNode.Parameters); - Assert.Null(bParamNode.Matches); - var paramCNodeKvp = Assert.Single(bParamNode.Literals); + var bParamNodeKvp = Assert.Single(root.Parameters.Literals); + Assert.Equal("b", bParamNodeKvp.Key); + var bParamNode = bParamNodeKvp.Value; + Assert.Null(bParamNode.Parameters); + Assert.Null(bParamNode.Matches); + var paramCNodeKvp = Assert.Single(bParamNode.Literals); - Assert.Equal("c", paramCNodeKvp.Key); - var paramCNode = paramCNodeKvp.Value; - Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); - Assert.Null(paramCNode.Literals); - Assert.Null(paramCNode.Parameters); - } + Assert.Equal("c", paramCNodeKvp.Key); + var paramCNode = paramCNodeKvp.Value; + Assert.Same(endpoint2, Assert.Single(paramCNode.Matches)); + Assert.Null(paramCNode.Literals); + Assert.Null(paramCNode.Parameters); + } - // Regression test for excessive memory usage https://github.com/dotnet/aspnetcore/issues/23850 - [Fact] - public void BuildDfaTree_CanHandle_LargeAmountOfRoutes_WithConstraints() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + // Regression test for excessive memory usage https://github.com/dotnet/aspnetcore/issues/23850 + [Fact] + public void BuildDfaTree_CanHandle_LargeAmountOfRoutes_WithConstraints() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoints = new[]{ + var endpoints = new[]{ CreateEndpoint("test1/method-1", new HttpMethodMetadata(new[] { "GET" })), CreateEndpoint("{language:length(2)}/test1/method-1", new HttpMethodMetadata(new[] { "GET" })), CreateEndpoint("{version:int}/{language:length(2)}/test1/method-1", new HttpMethodMetadata(new[] { "GET" })), @@ -1636,32 +1636,32 @@ namespace Microsoft.AspNetCore.Routing.Matching CreateEndpoint("{version:int}/{language:length(2)}/test63/method-3", new HttpMethodMetadata(new[] { "POST" })) }; - foreach (var endpoint in endpoints) - { - builder.AddEndpoint(endpoint); - } + foreach (var endpoint in endpoints) + { + builder.AddEndpoint(endpoint); + } - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.NotNull(root); - var count = 0; - root.Visit(node => count++); + // Assert + Assert.NotNull(root); + var count = 0; + root.Visit(node => count++); - // Without filtering it would have resulted in millions of nodes, several GB of memory and minutes - Assert.Equal(759, count); - } + // Without filtering it would have resulted in millions of nodes, several GB of memory and minutes + Assert.Equal(759, count); + } - // Regression test for excessive memory usage https://github.com/dotnet/aspnetcore/issues/33735 - [Fact] - public void BuildDfaTree_Regression_33735() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + // Regression test for excessive memory usage https://github.com/dotnet/aspnetcore/issues/33735 + [Fact] + public void BuildDfaTree_Regression_33735() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoints = new[] - { + var endpoints = new[] + { CreateEndpoint("api/{baseSpaceId:regex(Spaces-\\d\u002B)}/workers/{id}", new HttpMethodMetadata(new[] { "DELETE" })), CreateEndpoint("api/workers/{id}", new HttpMethodMetadata(new[] { "DELETE" })), CreateEndpoint("api/{baseSpaceId:regex(Spaces-\\d\u002B)}/workers/all", new HttpMethodMetadata(new[] { "GET" })), @@ -2156,31 +2156,31 @@ namespace Microsoft.AspNetCore.Routing.Matching CreateEndpoint("api/diagnostics/throw", new HttpMethodMetadata(new[] { "GET" })), }; - foreach (var endpoint in endpoints) - { - builder.AddEndpoint(endpoint); - } + foreach (var endpoint in endpoints) + { + builder.AddEndpoint(endpoint); + } - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.NotNull(root); - var count = 0; - root.Visit(node => count++); + // Assert + Assert.NotNull(root); + var count = 0; + root.Visit(node => count++); - // Without filtering it would have resulted in several order of magnitudes more nodes and much more memory - Assert.Equal(2964, count); - } + // Without filtering it would have resulted in several order of magnitudes more nodes and much more memory + Assert.Equal(2964, count); + } - // Another regression test based on OData models - [Fact] - public void BuildDfaTree_CanHandle_LargeAmountOfRoutes_WithComplexParameters() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + // Another regression test based on OData models + [Fact] + public void BuildDfaTree_CanHandle_LargeAmountOfRoutes_WithComplexParameters() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoints = new[]{ + var endpoints = new[]{ CreateEndpoint("Student", new HttpMethodMetadata(new[] { "GET" })), CreateEndpoint("{contextToken}/Student", new HttpMethodMetadata(new[] { "GET" })), CreateEndpoint("{contextToken}/Student/$count", new HttpMethodMetadata(new[] { "GET" })), @@ -2374,1039 +2374,1039 @@ namespace Microsoft.AspNetCore.Routing.Matching CreateEndpoint("{contextToken}", new HttpMethodMetadata(new[] { "GET" })), }; - foreach (var endpoint in endpoints) - { - builder.AddEndpoint(endpoint); - } + foreach (var endpoint in endpoints) + { + builder.AddEndpoint(endpoint); + } - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.NotNull(root); - var count = 0; - root.Visit(node => count++); + // Assert + Assert.NotNull(root); + var count = 0; + root.Visit(node => count++); - // Without filtering it would have resulted in several order of magnitudes more nodes and much more memory - Assert.Equal(1453, count); - } + // Without filtering it would have resulted in several order of magnitudes more nodes and much more memory + Assert.Equal(1453, count); + } - // Regression test for https://github.com/dotnet/aspnetcore/issues/16579 - // - // This case behaves the same for all combinations. - [Fact] - public void BuildDfaTree_MultipleEndpoint_ParameterAndCatchAll_OnSameNode_Order1() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); - - var endpoint1 = CreateEndpoint("a/{b}", order: 0); - builder.AddEndpoint(endpoint1); - - var endpoint2 = CreateEndpoint("a/{*b}", order: 1); - builder.AddEndpoint(endpoint2); - - // Act - var root = builder.BuildDfaTree(); - - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); - - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); - - var a = next.Value; - Assert.Same(endpoint2, Assert.Single(a.Matches)); - Assert.Null(a.Literals); - - var b = a.Parameters; - Assert.Collection( - b.Matches, - e => Assert.Same(endpoint1, e), - e => Assert.Same(endpoint2, e)); - Assert.Null(b.Literals); - Assert.Null(b.Parameters); - Assert.NotNull(b.CatchAll); - - var catchAll = b.CatchAll; - Assert.Same(endpoint2, Assert.Single(catchAll.Matches)); - Assert.Null(catchAll.Literals); - Assert.Same(catchAll, catchAll.Parameters); - Assert.Same(catchAll, catchAll.CatchAll); - } + // Regression test for https://github.com/dotnet/aspnetcore/issues/16579 + // + // This case behaves the same for all combinations. + [Fact] + public void BuildDfaTree_MultipleEndpoint_ParameterAndCatchAll_OnSameNode_Order1() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); + + var endpoint1 = CreateEndpoint("a/{b}", order: 0); + builder.AddEndpoint(endpoint1); + + var endpoint2 = CreateEndpoint("a/{*b}", order: 1); + builder.AddEndpoint(endpoint2); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); + + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); + + var a = next.Value; + Assert.Same(endpoint2, Assert.Single(a.Matches)); + Assert.Null(a.Literals); + + var b = a.Parameters; + Assert.Collection( + b.Matches, + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e)); + Assert.Null(b.Literals); + Assert.Null(b.Parameters); + Assert.NotNull(b.CatchAll); + + var catchAll = b.CatchAll; + Assert.Same(endpoint2, Assert.Single(catchAll.Matches)); + Assert.Null(catchAll.Literals); + Assert.Same(catchAll, catchAll.Parameters); + Assert.Same(catchAll, catchAll.CatchAll); + } - // Regression test for https://github.com/dotnet/aspnetcore/issues/16579 - [Fact] - public void BuildDfaTree_MultipleEndpoint_ParameterAndCatchAll_OnSameNode_Order2() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); - - var endpoint1 = CreateEndpoint("a/{*b}", order: 0); - builder.AddEndpoint(endpoint1); - - var endpoint2 = CreateEndpoint("a/{b}", order: 1); - builder.AddEndpoint(endpoint2); - - // Act - var root = builder.BuildDfaTree(); - - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); - - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); - - var a = next.Value; - Assert.Same(endpoint1, Assert.Single(a.Matches)); - Assert.Null(a.Literals); - - var b = a.Parameters; - Assert.Collection( - b.Matches, - e => Assert.Same(endpoint1, e), - e => Assert.Same(endpoint2, e)); - Assert.Null(b.Literals); - Assert.Null(b.Parameters); - Assert.NotNull(b.CatchAll); - - var catchAll = b.CatchAll; - Assert.Same(endpoint1, Assert.Single(catchAll.Matches)); - Assert.Null(catchAll.Literals); - Assert.Same(catchAll, catchAll.Parameters); - Assert.Same(catchAll, catchAll.CatchAll); - } + // Regression test for https://github.com/dotnet/aspnetcore/issues/16579 + [Fact] + public void BuildDfaTree_MultipleEndpoint_ParameterAndCatchAll_OnSameNode_Order2() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); + + var endpoint1 = CreateEndpoint("a/{*b}", order: 0); + builder.AddEndpoint(endpoint1); + + var endpoint2 = CreateEndpoint("a/{b}", order: 1); + builder.AddEndpoint(endpoint2); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); + + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); + + var a = next.Value; + Assert.Same(endpoint1, Assert.Single(a.Matches)); + Assert.Null(a.Literals); + + var b = a.Parameters; + Assert.Collection( + b.Matches, + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e)); + Assert.Null(b.Literals); + Assert.Null(b.Parameters); + Assert.NotNull(b.CatchAll); + + var catchAll = b.CatchAll; + Assert.Same(endpoint1, Assert.Single(catchAll.Matches)); + Assert.Null(catchAll.Literals); + Assert.Same(catchAll, catchAll.Parameters); + Assert.Same(catchAll, catchAll.CatchAll); + } - // Regression test for https://github.com/dotnet/aspnetcore/issues/18677 - [Fact] - public void BuildDfaTree_MultipleEndpoint_CatchAllWithHigherPrecedenceThanParameter_Order1() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); - - var endpoint1 = CreateEndpoint("{a}/{b}", order: 0); - builder.AddEndpoint(endpoint1); - - var endpoint2 = CreateEndpoint("a/{*b}", order: 1); - builder.AddEndpoint(endpoint2); - - // Act - var root = builder.BuildDfaTree(); - - // Assert - Assert.Null(root.Matches); - - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); - - var a1 = next.Value; - Assert.Same(endpoint2, Assert.Single(a1.Matches)); - Assert.Null(a1.Literals); - - var b1 = a1.Parameters; - Assert.Collection( - b1.Matches, - e => Assert.Same(endpoint1, e), - e => Assert.Same(endpoint2, e)); - Assert.Null(b1.Literals); - Assert.Null(b1.Parameters); - Assert.NotNull(b1.CatchAll); - - var catchAll1 = b1.CatchAll; - Assert.Same(endpoint2, Assert.Single(catchAll1.Matches)); - Assert.Null(catchAll1.Literals); - Assert.Same(catchAll1, catchAll1.Parameters); - Assert.Same(catchAll1, catchAll1.CatchAll); - - var a2 = root.Parameters; - Assert.Null(a2.Matches); - Assert.Null(a2.Literals); - - var b2 = a2.Parameters; - Assert.Collection( - b2.Matches, - e => Assert.Same(endpoint1, e)); - Assert.Null(b2.Literals); - Assert.Null(b2.Parameters); - Assert.Null(b2.CatchAll); - } + // Regression test for https://github.com/dotnet/aspnetcore/issues/18677 + [Fact] + public void BuildDfaTree_MultipleEndpoint_CatchAllWithHigherPrecedenceThanParameter_Order1() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); + + var endpoint1 = CreateEndpoint("{a}/{b}", order: 0); + builder.AddEndpoint(endpoint1); + + var endpoint2 = CreateEndpoint("a/{*b}", order: 1); + builder.AddEndpoint(endpoint2); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Null(root.Matches); + + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); + + var a1 = next.Value; + Assert.Same(endpoint2, Assert.Single(a1.Matches)); + Assert.Null(a1.Literals); + + var b1 = a1.Parameters; + Assert.Collection( + b1.Matches, + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e)); + Assert.Null(b1.Literals); + Assert.Null(b1.Parameters); + Assert.NotNull(b1.CatchAll); + + var catchAll1 = b1.CatchAll; + Assert.Same(endpoint2, Assert.Single(catchAll1.Matches)); + Assert.Null(catchAll1.Literals); + Assert.Same(catchAll1, catchAll1.Parameters); + Assert.Same(catchAll1, catchAll1.CatchAll); + + var a2 = root.Parameters; + Assert.Null(a2.Matches); + Assert.Null(a2.Literals); + + var b2 = a2.Parameters; + Assert.Collection( + b2.Matches, + e => Assert.Same(endpoint1, e)); + Assert.Null(b2.Literals); + Assert.Null(b2.Parameters); + Assert.Null(b2.CatchAll); + } - // Regression test for https://github.com/dotnet/aspnetcore/issues/18677 - [Fact] - public void BuildDfaTree_MultipleEndpoint_CatchAllWithHigherPrecedenceThanParameter_Order2() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); - - var endpoint1 = CreateEndpoint("a/{*b}", order: 0); - builder.AddEndpoint(endpoint1); - - var endpoint2 = CreateEndpoint("{a}/{b}", order: 1); - builder.AddEndpoint(endpoint2); - - // Act - var root = builder.BuildDfaTree(); - - // Assert - Assert.Null(root.Matches); - - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); - - var a1 = next.Value; - Assert.Same(endpoint1, Assert.Single(a1.Matches)); - Assert.Null(a1.Literals); - - var b1 = a1.Parameters; - Assert.Collection( - b1.Matches, - e => Assert.Same(endpoint1, e), - e => Assert.Same(endpoint2, e)); - Assert.Null(b1.Literals); - Assert.Null(b1.Parameters); - Assert.NotNull(b1.CatchAll); - - var catchAll1 = b1.CatchAll; - Assert.Same(endpoint1, Assert.Single(catchAll1.Matches)); - Assert.Null(catchAll1.Literals); - Assert.Same(catchAll1, catchAll1.Parameters); - Assert.Same(catchAll1, catchAll1.CatchAll); - - var a2 = root.Parameters; - Assert.Null(a2.Matches); - Assert.Null(a2.Literals); - - var b2 = a2.Parameters; - Assert.Collection( - b2.Matches, - e => Assert.Same(endpoint2, e)); - Assert.Null(b2.Literals); - Assert.Null(b2.Parameters); - Assert.Null(b2.CatchAll); - } + // Regression test for https://github.com/dotnet/aspnetcore/issues/18677 + [Fact] + public void BuildDfaTree_MultipleEndpoint_CatchAllWithHigherPrecedenceThanParameter_Order2() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); + + var endpoint1 = CreateEndpoint("a/{*b}", order: 0); + builder.AddEndpoint(endpoint1); + + var endpoint2 = CreateEndpoint("{a}/{b}", order: 1); + builder.AddEndpoint(endpoint2); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Null(root.Matches); + + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); + + var a1 = next.Value; + Assert.Same(endpoint1, Assert.Single(a1.Matches)); + Assert.Null(a1.Literals); + + var b1 = a1.Parameters; + Assert.Collection( + b1.Matches, + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e)); + Assert.Null(b1.Literals); + Assert.Null(b1.Parameters); + Assert.NotNull(b1.CatchAll); + + var catchAll1 = b1.CatchAll; + Assert.Same(endpoint1, Assert.Single(catchAll1.Matches)); + Assert.Null(catchAll1.Literals); + Assert.Same(catchAll1, catchAll1.Parameters); + Assert.Same(catchAll1, catchAll1.CatchAll); + + var a2 = root.Parameters; + Assert.Null(a2.Matches); + Assert.Null(a2.Literals); + + var b2 = a2.Parameters; + Assert.Collection( + b2.Matches, + e => Assert.Same(endpoint2, e)); + Assert.Null(b2.Literals); + Assert.Null(b2.Parameters); + Assert.Null(b2.CatchAll); + } - private void BuildDfaTree_MultipleEndpoint_CatchAllWithHigherPrecedenceThanParameter_Order2_Legacy30Behavior_Core(DfaMatcherBuilder builder) - { - // Arrange - var endpoint1 = CreateEndpoint("a/{*b}", order: 0); - builder.AddEndpoint(endpoint1); - - var endpoint2 = CreateEndpoint("{a}/{b}", order: 1); - builder.AddEndpoint(endpoint2); - - // Act - var root = builder.BuildDfaTree(); - - // Assert - Assert.Null(root.Matches); - - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); - - var a1 = next.Value; - Assert.Same(endpoint1, Assert.Single(a1.Matches)); - Assert.Null(a1.Literals); - - var b1 = a1.Parameters; - Assert.Same(endpoint2, Assert.Single(b1.Matches)); - Assert.Null(b1.Literals); - Assert.Null(b1.Parameters); - Assert.Null(b1.CatchAll); - - var a2 = root.Parameters; - Assert.Null(a2.Matches); - Assert.Null(a2.Literals); - - var b2 = a2.Parameters; - Assert.Collection( - b2.Matches, - e => Assert.Same(endpoint2, e)); - Assert.Null(b2.Literals); - Assert.Null(b2.Parameters); - Assert.Null(b2.CatchAll); - } + private void BuildDfaTree_MultipleEndpoint_CatchAllWithHigherPrecedenceThanParameter_Order2_Legacy30Behavior_Core(DfaMatcherBuilder builder) + { + // Arrange + var endpoint1 = CreateEndpoint("a/{*b}", order: 0); + builder.AddEndpoint(endpoint1); + + var endpoint2 = CreateEndpoint("{a}/{b}", order: 1); + builder.AddEndpoint(endpoint2); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Null(root.Matches); + + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); + + var a1 = next.Value; + Assert.Same(endpoint1, Assert.Single(a1.Matches)); + Assert.Null(a1.Literals); + + var b1 = a1.Parameters; + Assert.Same(endpoint2, Assert.Single(b1.Matches)); + Assert.Null(b1.Literals); + Assert.Null(b1.Parameters); + Assert.Null(b1.CatchAll); + + var a2 = root.Parameters; + Assert.Null(a2.Matches); + Assert.Null(a2.Literals); + + var b2 = a2.Parameters; + Assert.Collection( + b2.Matches, + e => Assert.Same(endpoint2, e)); + Assert.Null(b2.Literals); + Assert.Null(b2.Parameters); + Assert.Null(b2.CatchAll); + } - [Fact] - public void BuildDfaTree_WithPolicies() - { - // Arrange - var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); - - var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(0), new TestMetadata2(true), }); - builder.AddEndpoint(endpoint1); - - // Act - var root = builder.BuildDfaTree(); - - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); - - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); - - var a = next.Value; - Assert.Empty(a.Matches); - Assert.IsType(a.NodeBuilder); - Assert.Collection( - a.PolicyEdges.OrderBy(e => e.Key), - e => Assert.Equal(0, e.Key)); - - var test1_0 = a.PolicyEdges[0]; - Assert.Empty(a.Matches); - Assert.IsType(test1_0.NodeBuilder); - Assert.Collection( - test1_0.PolicyEdges.OrderBy(e => e.Key), - e => Assert.Equal(true, e.Key)); - - var test2_true = test1_0.PolicyEdges[true]; - Assert.Same(endpoint1, Assert.Single(test2_true.Matches)); - Assert.Null(test2_true.NodeBuilder); - Assert.Null(test2_true.PolicyEdges); - } + [Fact] + public void BuildDfaTree_WithPolicies() + { + // Arrange + var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); + + var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(0), new TestMetadata2(true), }); + builder.AddEndpoint(endpoint1); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); + + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); + + var a = next.Value; + Assert.Empty(a.Matches); + Assert.IsType(a.NodeBuilder); + Assert.Collection( + a.PolicyEdges.OrderBy(e => e.Key), + e => Assert.Equal(0, e.Key)); + + var test1_0 = a.PolicyEdges[0]; + Assert.Empty(a.Matches); + Assert.IsType(test1_0.NodeBuilder); + Assert.Collection( + test1_0.PolicyEdges.OrderBy(e => e.Key), + e => Assert.Equal(true, e.Key)); + + var test2_true = test1_0.PolicyEdges[true]; + Assert.Same(endpoint1, Assert.Single(test2_true.Matches)); + Assert.Null(test2_true.NodeBuilder); + Assert.Null(test2_true.PolicyEdges); + } - [Fact] - public void BuildDfaTree_WithPolicies_AndBranches() - { - // Arrange - var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); - - var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(0), new TestMetadata2(true), }); - builder.AddEndpoint(endpoint1); - - var endpoint2 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), new TestMetadata2(true), }); - builder.AddEndpoint(endpoint2); - - var endpoint3 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), new TestMetadata2(false), }); - builder.AddEndpoint(endpoint3); - - // Act - var root = builder.BuildDfaTree(); - - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); - - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); - - var a = next.Value; - Assert.Empty(a.Matches); - Assert.IsType(a.NodeBuilder); - Assert.Collection( - a.PolicyEdges.OrderBy(e => e.Key), - e => Assert.Equal(0, e.Key), - e => Assert.Equal(1, e.Key)); - - var test1_0 = a.PolicyEdges[0]; - Assert.Empty(test1_0.Matches); - Assert.IsType(test1_0.NodeBuilder); - Assert.Collection( - test1_0.PolicyEdges.OrderBy(e => e.Key), - e => Assert.Equal(true, e.Key)); - - var test2_true = test1_0.PolicyEdges[true]; - Assert.Same(endpoint1, Assert.Single(test2_true.Matches)); - Assert.Null(test2_true.NodeBuilder); - Assert.Null(test2_true.PolicyEdges); - - var test1_1 = a.PolicyEdges[1]; - Assert.Empty(test1_1.Matches); - Assert.IsType(test1_1.NodeBuilder); - Assert.Collection( - test1_1.PolicyEdges.OrderBy(e => e.Key), - e => Assert.Equal(false, e.Key), - e => Assert.Equal(true, e.Key)); - - test2_true = test1_1.PolicyEdges[true]; - Assert.Same(endpoint2, Assert.Single(test2_true.Matches)); - Assert.Null(test2_true.NodeBuilder); - Assert.Null(test2_true.PolicyEdges); - - var test2_false = test1_1.PolicyEdges[false]; - Assert.Same(endpoint3, Assert.Single(test2_false.Matches)); - Assert.Null(test2_false.NodeBuilder); - Assert.Null(test2_false.PolicyEdges); - } + [Fact] + public void BuildDfaTree_WithPolicies_AndBranches() + { + // Arrange + var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); + + var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(0), new TestMetadata2(true), }); + builder.AddEndpoint(endpoint1); + + var endpoint2 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), new TestMetadata2(true), }); + builder.AddEndpoint(endpoint2); + + var endpoint3 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), new TestMetadata2(false), }); + builder.AddEndpoint(endpoint3); + + // Act + var root = builder.BuildDfaTree(); + + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); + + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); + + var a = next.Value; + Assert.Empty(a.Matches); + Assert.IsType(a.NodeBuilder); + Assert.Collection( + a.PolicyEdges.OrderBy(e => e.Key), + e => Assert.Equal(0, e.Key), + e => Assert.Equal(1, e.Key)); + + var test1_0 = a.PolicyEdges[0]; + Assert.Empty(test1_0.Matches); + Assert.IsType(test1_0.NodeBuilder); + Assert.Collection( + test1_0.PolicyEdges.OrderBy(e => e.Key), + e => Assert.Equal(true, e.Key)); + + var test2_true = test1_0.PolicyEdges[true]; + Assert.Same(endpoint1, Assert.Single(test2_true.Matches)); + Assert.Null(test2_true.NodeBuilder); + Assert.Null(test2_true.PolicyEdges); + + var test1_1 = a.PolicyEdges[1]; + Assert.Empty(test1_1.Matches); + Assert.IsType(test1_1.NodeBuilder); + Assert.Collection( + test1_1.PolicyEdges.OrderBy(e => e.Key), + e => Assert.Equal(false, e.Key), + e => Assert.Equal(true, e.Key)); + + test2_true = test1_1.PolicyEdges[true]; + Assert.Same(endpoint2, Assert.Single(test2_true.Matches)); + Assert.Null(test2_true.NodeBuilder); + Assert.Null(test2_true.PolicyEdges); + + var test2_false = test1_1.PolicyEdges[false]; + Assert.Same(endpoint3, Assert.Single(test2_false.Matches)); + Assert.Null(test2_false.NodeBuilder); + Assert.Null(test2_false.PolicyEdges); + } - [Fact] - public void BuildDfaTree_WithPolicies_AndBranches_FirstPolicySkipped() - { - // Arrange - var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); + [Fact] + public void BuildDfaTree_WithPolicies_AndBranches_FirstPolicySkipped() + { + // Arrange + var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); - var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata2(true), }); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata2(true), }); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata2(true), }); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata2(true), }); + builder.AddEndpoint(endpoint2); - var endpoint3 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata2(false), }); - builder.AddEndpoint(endpoint3); + var endpoint3 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata2(false), }); + builder.AddEndpoint(endpoint3); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); - var a = next.Value; - Assert.Empty(a.Matches); - Assert.IsType(a.NodeBuilder); - Assert.Collection( - a.PolicyEdges.OrderBy(e => e.Key), - e => Assert.Equal(false, e.Key), - e => Assert.Equal(true, e.Key)); + var a = next.Value; + Assert.Empty(a.Matches); + Assert.IsType(a.NodeBuilder); + Assert.Collection( + a.PolicyEdges.OrderBy(e => e.Key), + e => Assert.Equal(false, e.Key), + e => Assert.Equal(true, e.Key)); - var test2_true = a.PolicyEdges[true]; - Assert.Equal(new[] { endpoint1, endpoint2, }, test2_true.Matches); - Assert.Null(test2_true.NodeBuilder); - Assert.Null(test2_true.PolicyEdges); + var test2_true = a.PolicyEdges[true]; + Assert.Equal(new[] { endpoint1, endpoint2, }, test2_true.Matches); + Assert.Null(test2_true.NodeBuilder); + Assert.Null(test2_true.PolicyEdges); - var test2_false = a.PolicyEdges[false]; - Assert.Equal(new[] { endpoint3, }, test2_false.Matches); - Assert.Null(test2_false.NodeBuilder); - Assert.Null(test2_false.PolicyEdges); - } + var test2_false = a.PolicyEdges[false]; + Assert.Equal(new[] { endpoint3, }, test2_false.Matches); + Assert.Null(test2_false.NodeBuilder); + Assert.Null(test2_false.PolicyEdges); + } - [Fact] - public void BuildDfaTree_WithPolicies_AndBranches_SecondSkipped() - { - // Arrange - var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); + [Fact] + public void BuildDfaTree_WithPolicies_AndBranches_SecondSkipped() + { + // Arrange + var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); - var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(0), }); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(0), }); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), }); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), }); + builder.AddEndpoint(endpoint2); - var endpoint3 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), }); - builder.AddEndpoint(endpoint3); + var endpoint3 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), }); + builder.AddEndpoint(endpoint3); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); - var a = next.Value; - Assert.Empty(a.Matches); - Assert.IsType(a.NodeBuilder); - Assert.Collection( - a.PolicyEdges.OrderBy(e => e.Key), - e => Assert.Equal(0, e.Key), - e => Assert.Equal(1, e.Key)); + var a = next.Value; + Assert.Empty(a.Matches); + Assert.IsType(a.NodeBuilder); + Assert.Collection( + a.PolicyEdges.OrderBy(e => e.Key), + e => Assert.Equal(0, e.Key), + e => Assert.Equal(1, e.Key)); - var test1_0 = a.PolicyEdges[0]; - Assert.Equal(new[] { endpoint1, }, test1_0.Matches); - Assert.Null(test1_0.NodeBuilder); - Assert.Null(test1_0.PolicyEdges); + var test1_0 = a.PolicyEdges[0]; + Assert.Equal(new[] { endpoint1, }, test1_0.Matches); + Assert.Null(test1_0.NodeBuilder); + Assert.Null(test1_0.PolicyEdges); - var test1_1 = a.PolicyEdges[1]; - Assert.Equal(new[] { endpoint2, endpoint3, }, test1_1.Matches); - Assert.Null(test1_1.NodeBuilder); - Assert.Null(test1_1.PolicyEdges); - } + var test1_1 = a.PolicyEdges[1]; + Assert.Equal(new[] { endpoint2, endpoint3, }, test1_1.Matches); + Assert.Null(test1_1.NodeBuilder); + Assert.Null(test1_1.PolicyEdges); + } - [Fact] - public void BuildDfaTree_WithPolicies_AndBranches_NonRouteEndpoint() - { - // Arrange - var builder = CreateDfaMatcherBuilder(new TestNonRoutePatternMatcherPolicy()); + [Fact] + public void BuildDfaTree_WithPolicies_AndBranches_NonRouteEndpoint() + { + // Arrange + var builder = CreateDfaMatcherBuilder(new TestNonRoutePatternMatcherPolicy()); - var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(0), }); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(0), }); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), }); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), }); + builder.AddEndpoint(endpoint2); - var endpoint3 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), }); - builder.AddEndpoint(endpoint3); + var endpoint3 = CreateEndpoint("/a", metadata: new object[] { new TestMetadata1(1), }); + builder.AddEndpoint(endpoint3); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); - var a = next.Value; - Assert.Empty(a.Matches); - Assert.IsType(a.NodeBuilder); - Assert.Collection( - a.PolicyEdges.OrderBy(e => e.Key), - e => Assert.Equal(0, e.Key), - e => Assert.Equal(1, e.Key), - e => Assert.Equal(int.MaxValue, e.Key)); + var a = next.Value; + Assert.Empty(a.Matches); + Assert.IsType(a.NodeBuilder); + Assert.Collection( + a.PolicyEdges.OrderBy(e => e.Key), + e => Assert.Equal(0, e.Key), + e => Assert.Equal(1, e.Key), + e => Assert.Equal(int.MaxValue, e.Key)); - var test1_0 = a.PolicyEdges[0]; - Assert.Equal(new[] { endpoint1, }, test1_0.Matches); - Assert.Null(test1_0.NodeBuilder); - Assert.Null(test1_0.PolicyEdges); + var test1_0 = a.PolicyEdges[0]; + Assert.Equal(new[] { endpoint1, }, test1_0.Matches); + Assert.Null(test1_0.NodeBuilder); + Assert.Null(test1_0.PolicyEdges); - var test1_1 = a.PolicyEdges[1]; - Assert.Equal(new[] { endpoint2, endpoint3, }, test1_1.Matches); - Assert.Null(test1_1.NodeBuilder); - Assert.Null(test1_1.PolicyEdges); + var test1_1 = a.PolicyEdges[1]; + Assert.Equal(new[] { endpoint2, endpoint3, }, test1_1.Matches); + Assert.Null(test1_1.NodeBuilder); + Assert.Null(test1_1.PolicyEdges); - var nonRouteEndpoint = a.PolicyEdges[int.MaxValue]; - Assert.Equal("MaxValueEndpoint", Assert.Single(nonRouteEndpoint.Matches).DisplayName); - } + var nonRouteEndpoint = a.PolicyEdges[int.MaxValue]; + Assert.Equal("MaxValueEndpoint", Assert.Single(nonRouteEndpoint.Matches).DisplayName); + } - [Fact] - public void BuildDfaTree_WithPolicies_AndBranches_BothPoliciesSkipped() - { - // Arrange - var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); + [Fact] + public void BuildDfaTree_WithPolicies_AndBranches_BothPoliciesSkipped() + { + // Arrange + var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); - var endpoint1 = CreateEndpoint("/a", metadata: new object[] { }); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint("/a", metadata: new object[] { }); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("/a", metadata: new object[] { }); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("/a", metadata: new object[] { }); + builder.AddEndpoint(endpoint2); - var endpoint3 = CreateEndpoint("/a", metadata: new object[] { }); - builder.AddEndpoint(endpoint3); + var endpoint3 = CreateEndpoint("/a", metadata: new object[] { }); + builder.AddEndpoint(endpoint3); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); - var next = Assert.Single(root.Literals); - Assert.Equal("a", next.Key); + var next = Assert.Single(root.Literals); + Assert.Equal("a", next.Key); - var a = next.Value; - Assert.Equal(new[] { endpoint1, endpoint2, endpoint3, }, a.Matches); - Assert.Null(a.NodeBuilder); - Assert.Null(a.PolicyEdges); - } + var a = next.Value; + Assert.Equal(new[] { endpoint1, endpoint2, endpoint3, }, a.Matches); + Assert.Null(a.NodeBuilder); + Assert.Null(a.PolicyEdges); + } - // Verifies that we sort the endpoints before calling into policies. + // Verifies that we sort the endpoints before calling into policies. + // + // The builder uses a different sort order when building the tree, vs when building the policy nodes. Policy + // nodes should see an "absolute" order. + [Fact] + public void BuildDfaTree_WithPolicies_SortedAccordingToScore() + { + // Arrange // - // The builder uses a different sort order when building the tree, vs when building the policy nodes. Policy - // nodes should see an "absolute" order. - [Fact] - public void BuildDfaTree_WithPolicies_SortedAccordingToScore() + // These cases where chosen where the absolute order incontrolled explicitly by setting .Order, but + // the precedence of segments is different, so these will be sorted into different orders when building + // the tree. + var policies = new MatcherPolicy[] { - // Arrange - // - // These cases where chosen where the absolute order incontrolled explicitly by setting .Order, but - // the precedence of segments is different, so these will be sorted into different orders when building - // the tree. - var policies = new MatcherPolicy[] - { new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy(), - }; + }; - var comparer = new EndpointComparer(policies.OrderBy(p => p.Order).OfType().ToArray()); + var comparer = new EndpointComparer(policies.OrderBy(p => p.Order).OfType().ToArray()); - var builder = CreateDfaMatcherBuilder(policies); + var builder = CreateDfaMatcherBuilder(policies); - ((TestMetadata1MatcherPolicy)policies[0]).OnGetEdges = VerifyOrder; - ((TestMetadata2MatcherPolicy)policies[1]).OnGetEdges = VerifyOrder; + ((TestMetadata1MatcherPolicy)policies[0]).OnGetEdges = VerifyOrder; + ((TestMetadata2MatcherPolicy)policies[1]).OnGetEdges = VerifyOrder; - var endpoint1 = CreateEndpoint("/a/{**b}", order: -1, metadata: new object[] { new TestMetadata1(0), new TestMetadata2(true), }); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateEndpoint("/a/{**b}", order: -1, metadata: new object[] { new TestMetadata1(0), new TestMetadata2(true), }); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateEndpoint("/a/{b}/{**c}", order: 0, metadata: new object[] { new TestMetadata1(1), new TestMetadata2(true), }); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateEndpoint("/a/{b}/{**c}", order: 0, metadata: new object[] { new TestMetadata1(1), new TestMetadata2(true), }); + builder.AddEndpoint(endpoint2); - var endpoint3 = CreateEndpoint("/a/b/{c}", order: 1, metadata: new object[] { new TestMetadata1(1), new TestMetadata2(false), }); - builder.AddEndpoint(endpoint3); + var endpoint3 = CreateEndpoint("/a/b/{c}", order: 1, metadata: new object[] { new TestMetadata1(1), new TestMetadata2(false), }); + builder.AddEndpoint(endpoint3); - // Act & Assert - _ = builder.BuildDfaTree(); + // Act & Assert + _ = builder.BuildDfaTree(); - void VerifyOrder(IReadOnlyList endpoints) - { - // The list should already be in sorted order, every time build is called. - Assert.Equal(endpoints, endpoints.OrderBy(e => e, comparer)); - } + void VerifyOrder(IReadOnlyList endpoints) + { + // The list should already be in sorted order, every time build is called. + Assert.Equal(endpoints, endpoints.OrderBy(e => e, comparer)); } + } - [Fact] - public void BuildDfaTree_RequiredValues() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_RequiredValues() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint = CreateEndpoint("{controller}/{action}", requiredValues: new { controller = "Home", action = "Index" }); - builder.AddEndpoint(endpoint); + var endpoint = CreateEndpoint("{controller}/{action}", requiredValues: new { controller = "Home", action = "Index" }); + builder.AddEndpoint(endpoint); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); - var next = Assert.Single(root.Literals); - Assert.Equal("Home", next.Key); + var next = Assert.Single(root.Literals); + Assert.Equal("Home", next.Key); - var home = next.Value; - Assert.Null(home.Matches); - Assert.Null(home.Parameters); + var home = next.Value; + Assert.Null(home.Matches); + Assert.Null(home.Parameters); - next = Assert.Single(home.Literals); - Assert.Equal("Index", next.Key); + next = Assert.Single(home.Literals); + Assert.Equal("Index", next.Key); - var index = next.Value; - Assert.Same(endpoint, Assert.Single(index.Matches)); - Assert.Null(index.Literals); - } + var index = next.Value; + Assert.Same(endpoint, Assert.Single(index.Matches)); + Assert.Null(index.Literals); + } - [Fact] - public void BuildDfaTree_RequiredValues_AndMatchingDefaults() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_RequiredValues_AndMatchingDefaults() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint = CreateEndpoint( - "{controller}/{action}", - defaults: new { controller = "Home", action = "Index" }, - requiredValues: new { controller = "Home", action = "Index" }); - builder.AddEndpoint(endpoint); + var endpoint = CreateEndpoint( + "{controller}/{action}", + defaults: new { controller = "Home", action = "Index" }, + requiredValues: new { controller = "Home", action = "Index" }); + builder.AddEndpoint(endpoint); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Same(endpoint, Assert.Single(root.Matches)); - Assert.Null(root.Parameters); + // Assert + Assert.Same(endpoint, Assert.Single(root.Matches)); + Assert.Null(root.Parameters); - var next = Assert.Single(root.Literals); - Assert.Equal("Home", next.Key); + var next = Assert.Single(root.Literals); + Assert.Equal("Home", next.Key); - var home = next.Value; - Assert.Same(endpoint, Assert.Single(home.Matches)); - Assert.Null(home.Parameters); + var home = next.Value; + Assert.Same(endpoint, Assert.Single(home.Matches)); + Assert.Null(home.Parameters); - next = Assert.Single(home.Literals); - Assert.Equal("Index", next.Key); + next = Assert.Single(home.Literals); + Assert.Equal("Index", next.Key); - var index = next.Value; - Assert.Same(endpoint, Assert.Single(index.Matches)); - Assert.Null(index.Literals); - } + var index = next.Value; + Assert.Same(endpoint, Assert.Single(index.Matches)); + Assert.Null(index.Literals); + } - [Fact] - public void BuildDfaTree_RequiredValues_AndDifferentDefaults() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_RequiredValues_AndDifferentDefaults() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint = CreateSubsitutedEndpoint( - "{controller}/{action}", - defaults: new { controller = "Home", action = "Index" }, - requiredValues: new { controller = "Login", action = "Index" }); - builder.AddEndpoint(endpoint); + var endpoint = CreateSubsitutedEndpoint( + "{controller}/{action}", + defaults: new { controller = "Home", action = "Index" }, + requiredValues: new { controller = "Login", action = "Index" }); + builder.AddEndpoint(endpoint); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); - var next = Assert.Single(root.Literals); - Assert.Equal("Login", next.Key); + var next = Assert.Single(root.Literals); + Assert.Equal("Login", next.Key); - var login = next.Value; - Assert.Same(endpoint, Assert.Single(login.Matches)); - Assert.Null(login.Parameters); + var login = next.Value; + Assert.Same(endpoint, Assert.Single(login.Matches)); + Assert.Null(login.Parameters); - next = Assert.Single(login.Literals); - Assert.Equal("Index", next.Key); + next = Assert.Single(login.Literals); + Assert.Equal("Index", next.Key); - var index = next.Value; - Assert.Same(endpoint, Assert.Single(index.Matches)); - Assert.Null(index.Literals); - } + var index = next.Value; + Assert.Same(endpoint, Assert.Single(index.Matches)); + Assert.Null(index.Literals); + } - [Fact] - public void BuildDfaTree_RequiredValues_Multiple() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_RequiredValues_Multiple() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint1 = CreateSubsitutedEndpoint( - "{controller}/{action}/{id?}", - defaults: new { controller = "Home", action = "Index" }, - requiredValues: new { controller = "Home", action = "Index" }); - builder.AddEndpoint(endpoint1); + var endpoint1 = CreateSubsitutedEndpoint( + "{controller}/{action}/{id?}", + defaults: new { controller = "Home", action = "Index" }, + requiredValues: new { controller = "Home", action = "Index" }); + builder.AddEndpoint(endpoint1); - var endpoint2 = CreateSubsitutedEndpoint( - "{controller}/{action}/{id?}", - defaults: new { controller = "Home", action = "Index" }, - requiredValues: new { controller = "Login", action = "Index" }); - builder.AddEndpoint(endpoint2); + var endpoint2 = CreateSubsitutedEndpoint( + "{controller}/{action}/{id?}", + defaults: new { controller = "Home", action = "Index" }, + requiredValues: new { controller = "Login", action = "Index" }); + builder.AddEndpoint(endpoint2); - var endpoint3 = CreateSubsitutedEndpoint( - "{controller}/{action}/{id?}", - defaults: new { controller = "Home", action = "Index" }, - requiredValues: new { controller = "Login", action = "ChangePassword" }); - builder.AddEndpoint(endpoint3); + var endpoint3 = CreateSubsitutedEndpoint( + "{controller}/{action}/{id?}", + defaults: new { controller = "Home", action = "Index" }, + requiredValues: new { controller = "Login", action = "ChangePassword" }); + builder.AddEndpoint(endpoint3); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Same(endpoint1, Assert.Single(root.Matches)); - Assert.Null(root.Parameters); + // Assert + Assert.Same(endpoint1, Assert.Single(root.Matches)); + Assert.Null(root.Parameters); - Assert.Equal(2, root.Literals.Count); + Assert.Equal(2, root.Literals.Count); - var home = root.Literals["Home"]; + var home = root.Literals["Home"]; - Assert.Same(endpoint1, Assert.Single(home.Matches)); - Assert.Null(home.Parameters); + Assert.Same(endpoint1, Assert.Single(home.Matches)); + Assert.Null(home.Parameters); - var next = Assert.Single(home.Literals); - Assert.Equal("Index", next.Key); + var next = Assert.Single(home.Literals); + Assert.Equal("Index", next.Key); - var homeIndex = next.Value; - Assert.Same(endpoint1, Assert.Single(homeIndex.Matches)); - Assert.Null(homeIndex.Literals); - Assert.NotNull(homeIndex.Parameters); + var homeIndex = next.Value; + Assert.Same(endpoint1, Assert.Single(homeIndex.Matches)); + Assert.Null(homeIndex.Literals); + Assert.NotNull(homeIndex.Parameters); - Assert.Same(endpoint1, Assert.Single(homeIndex.Parameters.Matches)); + Assert.Same(endpoint1, Assert.Single(homeIndex.Parameters.Matches)); - var login = root.Literals["Login"]; + var login = root.Literals["Login"]; - Assert.Same(endpoint2, Assert.Single(login.Matches)); - Assert.Null(login.Parameters); + Assert.Same(endpoint2, Assert.Single(login.Matches)); + Assert.Null(login.Parameters); - Assert.Equal(2, login.Literals.Count); + Assert.Equal(2, login.Literals.Count); - var loginIndex = login.Literals["Index"]; + var loginIndex = login.Literals["Index"]; - Assert.Same(endpoint2, Assert.Single(loginIndex.Matches)); - Assert.Null(loginIndex.Literals); - Assert.NotNull(loginIndex.Parameters); + Assert.Same(endpoint2, Assert.Single(loginIndex.Matches)); + Assert.Null(loginIndex.Literals); + Assert.NotNull(loginIndex.Parameters); - Assert.Same(endpoint2, Assert.Single(loginIndex.Parameters.Matches)); + Assert.Same(endpoint2, Assert.Single(loginIndex.Parameters.Matches)); - var loginChangePassword = login.Literals["ChangePassword"]; + var loginChangePassword = login.Literals["ChangePassword"]; - Assert.Same(endpoint3, Assert.Single(loginChangePassword.Matches)); - Assert.Null(loginChangePassword.Literals); - Assert.NotNull(loginChangePassword.Parameters); + Assert.Same(endpoint3, Assert.Single(loginChangePassword.Matches)); + Assert.Null(loginChangePassword.Literals); + Assert.NotNull(loginChangePassword.Parameters); - Assert.Same(endpoint3, Assert.Single(loginChangePassword.Parameters.Matches)); - } + Assert.Same(endpoint3, Assert.Single(loginChangePassword.Parameters.Matches)); + } - [Fact] - public void BuildDfaTree_RequiredValues_AndParameterTransformer() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_RequiredValues_AndParameterTransformer() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint = CreateEndpoint( - "{controller:slugify}/{action:slugify}", - defaults: new { controller = "RecentProducts", action = "ViewAll" }, - requiredValues: new { controller = "RecentProducts", action = "ViewAll" }); - builder.AddEndpoint(endpoint); + var endpoint = CreateEndpoint( + "{controller:slugify}/{action:slugify}", + defaults: new { controller = "RecentProducts", action = "ViewAll" }, + requiredValues: new { controller = "RecentProducts", action = "ViewAll" }); + builder.AddEndpoint(endpoint); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Same(endpoint, Assert.Single(root.Matches)); - Assert.Null(root.Parameters); + // Assert + Assert.Same(endpoint, Assert.Single(root.Matches)); + Assert.Null(root.Parameters); - var next = Assert.Single(root.Literals); - Assert.Equal("recent-products", next.Key); + var next = Assert.Single(root.Literals); + Assert.Equal("recent-products", next.Key); - var home = next.Value; - Assert.Same(endpoint, Assert.Single(home.Matches)); - Assert.Null(home.Parameters); + var home = next.Value; + Assert.Same(endpoint, Assert.Single(home.Matches)); + Assert.Null(home.Parameters); - next = Assert.Single(home.Literals); - Assert.Equal("view-all", next.Key); + next = Assert.Single(home.Literals); + Assert.Equal("view-all", next.Key); - var index = next.Value; - Assert.Same(endpoint, Assert.Single(index.Matches)); - Assert.Null(index.Literals); - } + var index = next.Value; + Assert.Same(endpoint, Assert.Single(index.Matches)); + Assert.Null(index.Literals); + } - [Fact] - public void BuildDfaTree_RequiredValues_AndDefaults_AndParameterTransformer() - { - // Arrange - var builder = CreateDfaMatcherBuilder(); + [Fact] + public void BuildDfaTree_RequiredValues_AndDefaults_AndParameterTransformer() + { + // Arrange + var builder = CreateDfaMatcherBuilder(); - var endpoint = CreateEndpoint( - "ConventionalTransformerRoute/{controller:slugify}/{action=Index}/{param:slugify?}", - requiredValues: new { controller = "ConventionalTransformer", action = "Index", area = (string)null, page = (string)null }); - builder.AddEndpoint(endpoint); + var endpoint = CreateEndpoint( + "ConventionalTransformerRoute/{controller:slugify}/{action=Index}/{param:slugify?}", + requiredValues: new { controller = "ConventionalTransformer", action = "Index", area = (string)null, page = (string)null }); + builder.AddEndpoint(endpoint); - // Act - var root = builder.BuildDfaTree(); + // Act + var root = builder.BuildDfaTree(); - // Assert - Assert.Null(root.Matches); - Assert.Null(root.Parameters); + // Assert + Assert.Null(root.Matches); + Assert.Null(root.Parameters); - var next = Assert.Single(root.Literals); - Assert.Equal("ConventionalTransformerRoute", next.Key); + var next = Assert.Single(root.Literals); + Assert.Equal("ConventionalTransformerRoute", next.Key); - var conventionalTransformerRoute = next.Value; - Assert.Null(conventionalTransformerRoute.Matches); - Assert.Null(conventionalTransformerRoute.Parameters); + var conventionalTransformerRoute = next.Value; + Assert.Null(conventionalTransformerRoute.Matches); + Assert.Null(conventionalTransformerRoute.Parameters); - next = Assert.Single(conventionalTransformerRoute.Literals); - Assert.Equal("conventional-transformer", next.Key); + next = Assert.Single(conventionalTransformerRoute.Literals); + Assert.Equal("conventional-transformer", next.Key); - var conventionalTransformer = next.Value; - Assert.Same(endpoint, Assert.Single(conventionalTransformer.Matches)); + var conventionalTransformer = next.Value; + Assert.Same(endpoint, Assert.Single(conventionalTransformer.Matches)); - next = Assert.Single(conventionalTransformer.Literals); - Assert.Equal("Index", next.Key); + next = Assert.Single(conventionalTransformer.Literals); + Assert.Equal("Index", next.Key); - var index = next.Value; - Assert.Same(endpoint, Assert.Single(index.Matches)); + var index = next.Value; + Assert.Same(endpoint, Assert.Single(index.Matches)); - Assert.NotNull(index.Parameters); + Assert.NotNull(index.Parameters); - Assert.Same(endpoint, Assert.Single(index.Parameters.Matches)); - } + Assert.Same(endpoint, Assert.Single(index.Parameters.Matches)); + } - [Fact] - public void CreateCandidate_JustLiterals() - { - // Arrange - var endpoint = CreateEndpoint("/a/b/c"); + [Fact] + public void CreateCandidate_JustLiterals() + { + // Arrange + var endpoint = CreateEndpoint("/a/b/c"); - var builder = CreateDfaMatcherBuilder(); + var builder = CreateDfaMatcherBuilder(); - // Act - var candidate = builder.CreateCandidate(endpoint, score: 0); + // Act + var candidate = builder.CreateCandidate(endpoint, score: 0); - // Assert - Assert.Equal(Candidate.CandidateFlags.None, candidate.Flags); - Assert.Empty(candidate.Slots); - Assert.Empty(candidate.Captures); - Assert.Equal(default, candidate.CatchAll); - Assert.Empty(candidate.ComplexSegments); - Assert.Empty(candidate.Constraints); - } + // Assert + Assert.Equal(Candidate.CandidateFlags.None, candidate.Flags); + Assert.Empty(candidate.Slots); + Assert.Empty(candidate.Captures); + Assert.Equal(default, candidate.CatchAll); + Assert.Empty(candidate.ComplexSegments); + Assert.Empty(candidate.Constraints); + } - [Fact] - public void CreateCandidate_Parameters() - { - // Arrange - var endpoint = CreateEndpoint("/{a}/{b}/{c}"); - - var builder = CreateDfaMatcherBuilder(); - - // Act - var candidate = builder.CreateCandidate(endpoint, score: 0); - - // Assert - Assert.Equal(Candidate.CandidateFlags.HasCaptures, candidate.Flags); - Assert.Equal(3, candidate.Slots.Length); - Assert.Collection( - candidate.Captures, - c => Assert.Equal(("a", 0, 0), c), - c => Assert.Equal(("b", 1, 1), c), - c => Assert.Equal(("c", 2, 2), c)); - Assert.Equal(default, candidate.CatchAll); - Assert.Empty(candidate.ComplexSegments); - Assert.Empty(candidate.Constraints); - } + [Fact] + public void CreateCandidate_Parameters() + { + // Arrange + var endpoint = CreateEndpoint("/{a}/{b}/{c}"); + + var builder = CreateDfaMatcherBuilder(); + + // Act + var candidate = builder.CreateCandidate(endpoint, score: 0); + + // Assert + Assert.Equal(Candidate.CandidateFlags.HasCaptures, candidate.Flags); + Assert.Equal(3, candidate.Slots.Length); + Assert.Collection( + candidate.Captures, + c => Assert.Equal(("a", 0, 0), c), + c => Assert.Equal(("b", 1, 1), c), + c => Assert.Equal(("c", 2, 2), c)); + Assert.Equal(default, candidate.CatchAll); + Assert.Empty(candidate.ComplexSegments); + Assert.Empty(candidate.Constraints); + } - [Fact] - public void CreateCandidate_Parameters_WithDefaults() - { - // Arrange - var endpoint = CreateEndpoint("/{a=aa}/{b=bb}/{c=cc}"); - - var builder = CreateDfaMatcherBuilder(); - - // Act - var candidate = builder.CreateCandidate(endpoint, score: 0); - - // Assert - Assert.Equal( - Candidate.CandidateFlags.HasDefaults | Candidate.CandidateFlags.HasCaptures, - candidate.Flags); - Assert.Collection( - candidate.Slots, - s => Assert.Equal(new KeyValuePair("a", "aa"), s), - s => Assert.Equal(new KeyValuePair("b", "bb"), s), - s => Assert.Equal(new KeyValuePair("c", "cc"), s)); - Assert.Collection( - candidate.Captures, - c => Assert.Equal(("a", 0, 0), c), - c => Assert.Equal(("b", 1, 1), c), - c => Assert.Equal(("c", 2, 2), c)); - Assert.Equal(default, candidate.CatchAll); - Assert.Empty(candidate.ComplexSegments); - Assert.Empty(candidate.Constraints); - } + [Fact] + public void CreateCandidate_Parameters_WithDefaults() + { + // Arrange + var endpoint = CreateEndpoint("/{a=aa}/{b=bb}/{c=cc}"); + + var builder = CreateDfaMatcherBuilder(); + + // Act + var candidate = builder.CreateCandidate(endpoint, score: 0); + + // Assert + Assert.Equal( + Candidate.CandidateFlags.HasDefaults | Candidate.CandidateFlags.HasCaptures, + candidate.Flags); + Assert.Collection( + candidate.Slots, + s => Assert.Equal(new KeyValuePair("a", "aa"), s), + s => Assert.Equal(new KeyValuePair("b", "bb"), s), + s => Assert.Equal(new KeyValuePair("c", "cc"), s)); + Assert.Collection( + candidate.Captures, + c => Assert.Equal(("a", 0, 0), c), + c => Assert.Equal(("b", 1, 1), c), + c => Assert.Equal(("c", 2, 2), c)); + Assert.Equal(default, candidate.CatchAll); + Assert.Empty(candidate.ComplexSegments); + Assert.Empty(candidate.Constraints); + } - [Fact] - public void CreateCandidate_Parameters_CatchAll() - { - // Arrange - var endpoint = CreateEndpoint("/{a}/{b}/{*c=cc}"); - - var builder = CreateDfaMatcherBuilder(); - - // Act - var candidate = builder.CreateCandidate(endpoint, score: 0); - - // Assert - Assert.Equal( - Candidate.CandidateFlags.HasDefaults | - Candidate.CandidateFlags.HasCaptures | - Candidate.CandidateFlags.HasCatchAll, - candidate.Flags); - Assert.Collection( - candidate.Slots, - s => Assert.Equal(new KeyValuePair("c", "cc"), s), - s => Assert.Equal(new KeyValuePair(null, null), s), - s => Assert.Equal(new KeyValuePair(null, null), s)); - Assert.Collection( - candidate.Captures, - c => Assert.Equal(("a", 0, 1), c), - c => Assert.Equal(("b", 1, 2), c)); - Assert.Equal(("c", 2, 0), candidate.CatchAll); - Assert.Empty(candidate.ComplexSegments); - Assert.Empty(candidate.Constraints); - } + [Fact] + public void CreateCandidate_Parameters_CatchAll() + { + // Arrange + var endpoint = CreateEndpoint("/{a}/{b}/{*c=cc}"); + + var builder = CreateDfaMatcherBuilder(); + + // Act + var candidate = builder.CreateCandidate(endpoint, score: 0); + + // Assert + Assert.Equal( + Candidate.CandidateFlags.HasDefaults | + Candidate.CandidateFlags.HasCaptures | + Candidate.CandidateFlags.HasCatchAll, + candidate.Flags); + Assert.Collection( + candidate.Slots, + s => Assert.Equal(new KeyValuePair("c", "cc"), s), + s => Assert.Equal(new KeyValuePair(null, null), s), + s => Assert.Equal(new KeyValuePair(null, null), s)); + Assert.Collection( + candidate.Captures, + c => Assert.Equal(("a", 0, 1), c), + c => Assert.Equal(("b", 1, 2), c)); + Assert.Equal(("c", 2, 0), candidate.CatchAll); + Assert.Empty(candidate.ComplexSegments); + Assert.Empty(candidate.Constraints); + } - // Defaults are processed first, which affects the slot ordering. - [Fact] - public void CreateCandidate_Parameters_OutOfLineDefaults() - { - // Arrange - var endpoint = CreateEndpoint("/{a}/{b}/{c=cc}", new { a = "aa", d = "dd", }); - - var builder = CreateDfaMatcherBuilder(); - - // Act - var candidate = builder.CreateCandidate(endpoint, score: 0); - - // Assert - Assert.Equal( - Candidate.CandidateFlags.HasDefaults | Candidate.CandidateFlags.HasCaptures, - candidate.Flags); - Assert.Collection( - candidate.Slots, - s => Assert.Equal(new KeyValuePair("a", "aa"), s), - s => Assert.Equal(new KeyValuePair("d", "dd"), s), - s => Assert.Equal(new KeyValuePair("c", "cc"), s), - s => Assert.Equal(new KeyValuePair(null, null), s)); - Assert.Collection( - candidate.Captures, - c => Assert.Equal(("a", 0, 0), c), - c => Assert.Equal(("b", 1, 3), c), - c => Assert.Equal(("c", 2, 2), c)); - Assert.Equal(default, candidate.CatchAll); - Assert.Empty(candidate.ComplexSegments); - Assert.Empty(candidate.Constraints); - } + // Defaults are processed first, which affects the slot ordering. + [Fact] + public void CreateCandidate_Parameters_OutOfLineDefaults() + { + // Arrange + var endpoint = CreateEndpoint("/{a}/{b}/{c=cc}", new { a = "aa", d = "dd", }); + + var builder = CreateDfaMatcherBuilder(); + + // Act + var candidate = builder.CreateCandidate(endpoint, score: 0); + + // Assert + Assert.Equal( + Candidate.CandidateFlags.HasDefaults | Candidate.CandidateFlags.HasCaptures, + candidate.Flags); + Assert.Collection( + candidate.Slots, + s => Assert.Equal(new KeyValuePair("a", "aa"), s), + s => Assert.Equal(new KeyValuePair("d", "dd"), s), + s => Assert.Equal(new KeyValuePair("c", "cc"), s), + s => Assert.Equal(new KeyValuePair(null, null), s)); + Assert.Collection( + candidate.Captures, + c => Assert.Equal(("a", 0, 0), c), + c => Assert.Equal(("b", 1, 3), c), + c => Assert.Equal(("c", 2, 2), c)); + Assert.Equal(default, candidate.CatchAll); + Assert.Empty(candidate.ComplexSegments); + Assert.Empty(candidate.Constraints); + } - [Fact] - public void CreateCandidate_Parameters_ComplexSegments() - { - // Arrange - var endpoint = CreateEndpoint("/{a}-{b=bb}/{c}"); - - var builder = CreateDfaMatcherBuilder(); - - // Act - var candidate = builder.CreateCandidate(endpoint, score: 0); - - // Assert - Assert.Equal( - Candidate.CandidateFlags.HasDefaults | - Candidate.CandidateFlags.HasCaptures | - Candidate.CandidateFlags.HasComplexSegments, - candidate.Flags); - Assert.Collection( - candidate.Slots, - s => Assert.Equal(new KeyValuePair("b", "bb"), s), - s => Assert.Equal(new KeyValuePair(null, null), s)); - Assert.Collection( - candidate.Captures, - c => Assert.Equal(("c", 1, 1), c)); - Assert.Equal(default, candidate.CatchAll); - Assert.Collection( - candidate.ComplexSegments, - s => Assert.Equal(0, s.segmentIndex)); - Assert.Empty(candidate.Constraints); - } + [Fact] + public void CreateCandidate_Parameters_ComplexSegments() + { + // Arrange + var endpoint = CreateEndpoint("/{a}-{b=bb}/{c}"); + + var builder = CreateDfaMatcherBuilder(); + + // Act + var candidate = builder.CreateCandidate(endpoint, score: 0); + + // Assert + Assert.Equal( + Candidate.CandidateFlags.HasDefaults | + Candidate.CandidateFlags.HasCaptures | + Candidate.CandidateFlags.HasComplexSegments, + candidate.Flags); + Assert.Collection( + candidate.Slots, + s => Assert.Equal(new KeyValuePair("b", "bb"), s), + s => Assert.Equal(new KeyValuePair(null, null), s)); + Assert.Collection( + candidate.Captures, + c => Assert.Equal(("c", 1, 1), c)); + Assert.Equal(default, candidate.CatchAll); + Assert.Collection( + candidate.ComplexSegments, + s => Assert.Equal(0, s.segmentIndex)); + Assert.Empty(candidate.Constraints); + } - [Fact] - public void CreateCandidate_RouteConstraints() - { - // Arrange - var endpoint = CreateEndpoint("/a/b/c", constraints: new { a = new IntRouteConstraint(), }); + [Fact] + public void CreateCandidate_RouteConstraints() + { + // Arrange + var endpoint = CreateEndpoint("/a/b/c", constraints: new { a = new IntRouteConstraint(), }); - var builder = CreateDfaMatcherBuilder(); + var builder = CreateDfaMatcherBuilder(); - // Act - var candidate = builder.CreateCandidate(endpoint, score: 0); + // Act + var candidate = builder.CreateCandidate(endpoint, score: 0); - // Assert - Assert.Equal(Candidate.CandidateFlags.HasConstraints, candidate.Flags); - Assert.Empty(candidate.Slots); - Assert.Empty(candidate.Captures); - Assert.Equal(default, candidate.CatchAll); - Assert.Empty(candidate.ComplexSegments); - Assert.Single(candidate.Constraints); - } + // Assert + Assert.Equal(Candidate.CandidateFlags.HasConstraints, candidate.Flags); + Assert.Empty(candidate.Slots); + Assert.Empty(candidate.Captures); + Assert.Equal(default, candidate.CatchAll); + Assert.Empty(candidate.ComplexSegments); + Assert.Single(candidate.Constraints); + } - [Fact] - public void CreateCandidate_CustomParameterPolicy() - { - // Arrange - var endpoint = CreateEndpoint("/a/b/c", constraints: new { a = new CustomParameterPolicy(), }); + [Fact] + public void CreateCandidate_CustomParameterPolicy() + { + // Arrange + var endpoint = CreateEndpoint("/a/b/c", constraints: new { a = new CustomParameterPolicy(), }); - var builder = CreateDfaMatcherBuilder(); + var builder = CreateDfaMatcherBuilder(); - // Act - var candidate = builder.CreateCandidate(endpoint, score: 0); + // Act + var candidate = builder.CreateCandidate(endpoint, score: 0); - // Assert - Assert.Equal(Candidate.CandidateFlags.None, candidate.Flags); - Assert.Empty(candidate.Slots); - Assert.Empty(candidate.Captures); - Assert.Equal(default, candidate.CatchAll); - Assert.Empty(candidate.ComplexSegments); - Assert.Empty(candidate.Constraints); - } + // Assert + Assert.Equal(Candidate.CandidateFlags.None, candidate.Flags); + Assert.Empty(candidate.Slots); + Assert.Empty(candidate.Captures); + Assert.Equal(default, candidate.CatchAll); + Assert.Empty(candidate.ComplexSegments); + Assert.Empty(candidate.Constraints); + } - private class CustomParameterPolicy : IParameterPolicy - { - } + private class CustomParameterPolicy : IParameterPolicy + { + } - [Fact] - public void CreateCandidates_CreatesScoresCorrectly() + [Fact] + public void CreateCandidates_CreatesScoresCorrectly() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/a/b/c", constraints: new { a = new IntRouteConstraint(), }, metadata: new object[] { new TestMetadata1(), new TestMetadata2(), }), CreateEndpoint("/a/b/c", constraints: new { a = new AlphaRouteConstraint(), }, metadata: new object[] { new TestMetadata1(), new TestMetadata2(), }), CreateEndpoint("/a/b/c", constraints: new { a = new IntRouteConstraint(), }, metadata: new object[] { new TestMetadata1(), }), @@ -3415,204 +3415,203 @@ namespace Microsoft.AspNetCore.Routing.Matching CreateEndpoint("/a/b/c", constraints: new { }, metadata: new object[] { }), }; - var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); + var builder = CreateDfaMatcherBuilder(new TestMetadata1MatcherPolicy(), new TestMetadata2MatcherPolicy()); - // Act - var candidates = builder.CreateCandidates(endpoints); + // Act + var candidates = builder.CreateCandidates(endpoints); - // Assert - Assert.Collection( - candidates, - c => Assert.Equal(0, c.Score), - c => Assert.Equal(0, c.Score), - c => Assert.Equal(1, c.Score), - c => Assert.Equal(2, c.Score), - c => Assert.Equal(3, c.Score), - c => Assert.Equal(3, c.Score)); - } + // Assert + Assert.Collection( + candidates, + c => Assert.Equal(0, c.Score), + c => Assert.Equal(0, c.Score), + c => Assert.Equal(1, c.Score), + c => Assert.Equal(2, c.Score), + c => Assert.Equal(3, c.Score), + c => Assert.Equal(3, c.Score)); + } - private static DfaMatcherBuilder CreateDfaMatcherBuilder(params MatcherPolicy[] policies) - { - var policyFactory = CreateParameterPolicyFactory(); - var dataSource = new CompositeEndpointDataSource(Array.Empty()); - return new DfaMatcherBuilder( - NullLoggerFactory.Instance, - policyFactory, - Mock.Of(), - policies); - } + private static DfaMatcherBuilder CreateDfaMatcherBuilder(params MatcherPolicy[] policies) + { + var policyFactory = CreateParameterPolicyFactory(); + var dataSource = new CompositeEndpointDataSource(Array.Empty()); + return new DfaMatcherBuilder( + NullLoggerFactory.Instance, + policyFactory, + Mock.Of(), + policies); + } - private static RouteEndpoint CreateSubsitutedEndpoint( - string template, - object defaults = null, - object constraints = null, - object requiredValues = null, - params object[] metadata) - { - var routePattern = RoutePatternFactory.Parse(template, defaults, constraints); + private static RouteEndpoint CreateSubsitutedEndpoint( + string template, + object defaults = null, + object constraints = null, + object requiredValues = null, + params object[] metadata) + { + var routePattern = RoutePatternFactory.Parse(template, defaults, constraints); - var policyFactory = CreateParameterPolicyFactory(); - var defaultRoutePatternTransformer = new DefaultRoutePatternTransformer(policyFactory); + var policyFactory = CreateParameterPolicyFactory(); + var defaultRoutePatternTransformer = new DefaultRoutePatternTransformer(policyFactory); - routePattern = defaultRoutePatternTransformer.SubstituteRequiredValues(routePattern, requiredValues); + routePattern = defaultRoutePatternTransformer.SubstituteRequiredValues(routePattern, requiredValues); - return EndpointFactory.CreateRouteEndpoint(routePattern, metadata: metadata); - } + return EndpointFactory.CreateRouteEndpoint(routePattern, metadata: metadata); + } - public static RoutePattern CreateRoutePattern(RoutePattern routePattern, object requiredValues) + public static RoutePattern CreateRoutePattern(RoutePattern routePattern, object requiredValues) + { + if (requiredValues != null) { - if (requiredValues != null) - { - var policyFactory = CreateParameterPolicyFactory(); - var defaultRoutePatternTransformer = new DefaultRoutePatternTransformer(policyFactory); - - routePattern = defaultRoutePatternTransformer.SubstituteRequiredValues(routePattern, requiredValues); - } + var policyFactory = CreateParameterPolicyFactory(); + var defaultRoutePatternTransformer = new DefaultRoutePatternTransformer(policyFactory); - return routePattern; + routePattern = defaultRoutePatternTransformer.SubstituteRequiredValues(routePattern, requiredValues); } - private static DefaultParameterPolicyFactory CreateParameterPolicyFactory() - { - var serviceCollection = new ServiceCollection(); - var policyFactory = new DefaultParameterPolicyFactory( - Options.Create(new RouteOptions + return routePattern; + } + + private static DefaultParameterPolicyFactory CreateParameterPolicyFactory() + { + var serviceCollection = new ServiceCollection(); + var policyFactory = new DefaultParameterPolicyFactory( + Options.Create(new RouteOptions + { + ConstraintMap = { - ConstraintMap = - { ["slugify"] = typeof(SlugifyParameterTransformer), ["upper-case"] = typeof(UpperCaseParameterTransform) - } - }), - serviceCollection.BuildServiceProvider()); + } + }), + serviceCollection.BuildServiceProvider()); - return policyFactory; - } + return policyFactory; + } - private static RouteEndpoint CreateEndpoint( - string template, - object defaults = null, - object constraints = null, - object requiredValues = null, - int order = 0, - params object[] metadata) - { - return EndpointFactory.CreateRouteEndpoint(template, defaults, constraints, requiredValues, order: order, metadata: metadata); - } + private static RouteEndpoint CreateEndpoint( + string template, + object defaults = null, + object constraints = null, + object requiredValues = null, + int order = 0, + params object[] metadata) + { + return EndpointFactory.CreateRouteEndpoint(template, defaults, constraints, requiredValues, order: order, metadata: metadata); + } - private class TestMetadata1 + private class TestMetadata1 + { + public TestMetadata1() { - public TestMetadata1() - { - } - - public TestMetadata1(int state) - { - State = state; - } - - public int State { get; set; } } - private class TestMetadata1MatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy + public TestMetadata1(int state) { - public override int Order => 100; + State = state; + } - public IComparer Comparer => EndpointMetadataComparer.Default; + public int State { get; set; } + } - public Action> OnGetEdges { get; set; } + private class TestMetadata1MatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy + { + public override int Order => 100; - public bool AppliesToEndpoints(IReadOnlyList endpoints) - { - return endpoints.Any(e => e.Metadata.GetMetadata() != null); - } + public IComparer Comparer => EndpointMetadataComparer.Default; - public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) - { - throw new NotImplementedException(); - } + public Action> OnGetEdges { get; set; } - public IReadOnlyList GetEdges(IReadOnlyList endpoints) - { - OnGetEdges?.Invoke(endpoints); - return endpoints - .GroupBy(e => e.Metadata.GetMetadata().State) - .Select(g => new PolicyNodeEdge(g.Key, g.ToArray())) - .ToArray(); - } + public bool AppliesToEndpoints(IReadOnlyList endpoints) + { + return endpoints.Any(e => e.Metadata.GetMetadata() != null); } - private class TestMetadata2 + public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) { - public TestMetadata2() - { - } + throw new NotImplementedException(); + } - public TestMetadata2(bool state) - { - State = state; - } + public IReadOnlyList GetEdges(IReadOnlyList endpoints) + { + OnGetEdges?.Invoke(endpoints); + return endpoints + .GroupBy(e => e.Metadata.GetMetadata().State) + .Select(g => new PolicyNodeEdge(g.Key, g.ToArray())) + .ToArray(); + } + } - public bool State { get; set; } + private class TestMetadata2 + { + public TestMetadata2() + { } - private class TestMetadata2MatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy + public TestMetadata2(bool state) { - public override int Order => 101; + State = state; + } + + public bool State { get; set; } + } - public IComparer Comparer => EndpointMetadataComparer.Default; + private class TestMetadata2MatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy + { + public override int Order => 101; - public Action> OnGetEdges { get; set; } + public IComparer Comparer => EndpointMetadataComparer.Default; + public Action> OnGetEdges { get; set; } - public bool AppliesToEndpoints(IReadOnlyList endpoints) - { - return endpoints.Any(e => e.Metadata.GetMetadata() != null); - } - public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) - { - throw new NotImplementedException(); - } + public bool AppliesToEndpoints(IReadOnlyList endpoints) + { + return endpoints.Any(e => e.Metadata.GetMetadata() != null); + } - public IReadOnlyList GetEdges(IReadOnlyList endpoints) - { - OnGetEdges?.Invoke(endpoints); - return endpoints - .GroupBy(e => e.Metadata.GetMetadata().State) - .Select(g => new PolicyNodeEdge(g.Key, g.ToArray())) - .ToArray(); - } + public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) + { + throw new NotImplementedException(); } - private class TestNonRoutePatternMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy + public IReadOnlyList GetEdges(IReadOnlyList endpoints) { - public override int Order => 100; + OnGetEdges?.Invoke(endpoints); + return endpoints + .GroupBy(e => e.Metadata.GetMetadata().State) + .Select(g => new PolicyNodeEdge(g.Key, g.ToArray())) + .ToArray(); + } + } - public IComparer Comparer => EndpointMetadataComparer.Default; + private class TestNonRoutePatternMatcherPolicy : MatcherPolicy, IEndpointComparerPolicy, INodeBuilderPolicy + { + public override int Order => 100; - public bool AppliesToEndpoints(IReadOnlyList endpoints) - { - return endpoints.Any(e => e.Metadata.GetMetadata() != null); - } + public IComparer Comparer => EndpointMetadataComparer.Default; - public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) - { - throw new NotImplementedException(); - } + public bool AppliesToEndpoints(IReadOnlyList endpoints) + { + return endpoints.Any(e => e.Metadata.GetMetadata() != null); + } - public IReadOnlyList GetEdges(IReadOnlyList endpoints) - { - var edges = endpoints - .GroupBy(e => e.Metadata.GetMetadata().State) - .Select(g => new PolicyNodeEdge(g.Key, g.ToArray())) - .ToList(); + public PolicyJumpTable BuildJumpTable(int exitDestination, IReadOnlyList edges) + { + throw new NotImplementedException(); + } + + public IReadOnlyList GetEdges(IReadOnlyList endpoints) + { + var edges = endpoints + .GroupBy(e => e.Metadata.GetMetadata().State) + .Select(g => new PolicyNodeEdge(g.Key, g.ToArray())) + .ToList(); - var maxValueEndpoint = new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "MaxValueEndpoint"); - edges.Add(new PolicyNodeEdge(int.MaxValue, new[] { maxValueEndpoint })); + var maxValueEndpoint = new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "MaxValueEndpoint"); + edges.Add(new PolicyNodeEdge(int.MaxValue, new[] { maxValueEndpoint })); - return edges; - } + return edges; } } } diff --git a/src/Http/Routing/test/UnitTests/Matching/DfaMatcherConformanceTest.cs b/src/Http/Routing/test/UnitTests/Matching/DfaMatcherConformanceTest.cs index 9a3202ec01..fc9cf62847 100644 --- a/src/Http/Routing/test/UnitTests/Matching/DfaMatcherConformanceTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/DfaMatcherConformanceTest.cs @@ -5,35 +5,35 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class DfaMatcherConformanceTest : FullFeaturedMatcherConformanceTest { - public class DfaMatcherConformanceTest : FullFeaturedMatcherConformanceTest + // See the comments in the base class. DfaMatcher fixes a long-standing bug + // with catchall parameters and empty segments. + public override async Task Quirks_CatchAllParameter(string template, string path, string[] keys, string[] values) { - // See the comments in the base class. DfaMatcher fixes a long-standing bug - // with catchall parameters and empty segments. - public override async Task Quirks_CatchAllParameter(string template, string path, string[] keys, string[] values) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint, keys, values); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint, keys, values); + } - // https://github.com/dotnet/aspnetcore/issues/18677 - [Theory] - [InlineData("/middleware", 1)] - [InlineData("/middleware/test", 1)] - [InlineData("/middleware/test1/test2", 1)] - [InlineData("/bill/boga", 0)] - public async Task Match_Regression_1867_CorrectBehavior(string path, int endpointIndex) + // https://github.com/dotnet/aspnetcore/issues/18677 + [Theory] + [InlineData("/middleware", 1)] + [InlineData("/middleware/test", 1)] + [InlineData("/middleware/test1/test2", 1)] + [InlineData("/bill/boga", 0)] + public async Task Match_Regression_1867_CorrectBehavior(string path, int endpointIndex) + { + var endpoints = new RouteEndpoint[] { - var endpoints = new RouteEndpoint[] - { EndpointFactory.CreateRouteEndpoint( "{firstName}/{lastName}", order: 0, @@ -42,40 +42,39 @@ namespace Microsoft.AspNetCore.Routing.Matching EndpointFactory.CreateRouteEndpoint( "middleware/{**_}", order: 0), - }; + }; - var expected = endpoints[endpointIndex]; + var expected = endpoints[endpointIndex]; - var matcher = CreateMatcherCore(endpoints); - var httpContext = CreateContext(path); + var matcher = CreateMatcherCore(endpoints); + var httpContext = CreateContext(path); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true); - } + // Assert + MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true); + } - internal override Matcher CreateMatcher(params RouteEndpoint[] endpoints) - { - return CreateMatcherCore(endpoints); - } + internal override Matcher CreateMatcher(params RouteEndpoint[] endpoints) + { + return CreateMatcherCore(endpoints); + } - internal Matcher CreateMatcherCore(params RouteEndpoint[] endpoints) - { - var services = new ServiceCollection() - .AddLogging() - .AddOptions() - .AddRouting() - .BuildServiceProvider(); + internal Matcher CreateMatcherCore(params RouteEndpoint[] endpoints) + { + var services = new ServiceCollection() + .AddLogging() + .AddOptions() + .AddRouting() + .BuildServiceProvider(); - var builder = services.GetRequiredService(); + var builder = services.GetRequiredService(); - for (var i = 0; i < endpoints.Length; i++) - { - builder.AddEndpoint(endpoints[i]); - } - return builder.Build(); + for (var i = 0; i < endpoints.Length; i++) + { + builder.AddEndpoint(endpoints[i]); } + return builder.Build(); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/DfaMatcherTest.cs b/src/Http/Routing/test/UnitTests/Matching/DfaMatcherTest.cs index bdd1850671..bf9af68b30 100644 --- a/src/Http/Routing/test/UnitTests/Matching/DfaMatcherTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/DfaMatcherTest.cs @@ -14,1033 +14,1032 @@ using Microsoft.Extensions.Logging.Testing; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// Many of these are integration tests that exercise the system end to end, +// so we're reusing the services here. +public class DfaMatcherTest { - // Many of these are integration tests that exercise the system end to end, - // so we're reusing the services here. - public class DfaMatcherTest + private RouteEndpoint CreateEndpoint(string template, int order, object defaults = null, object requiredValues = null, object policies = null) { - private RouteEndpoint CreateEndpoint(string template, int order, object defaults = null, object requiredValues = null, object policies = null) - { - return EndpointFactory.CreateRouteEndpoint(template, defaults, policies, requiredValues, order, displayName: template); - } - - private DataSourceDependentMatcher CreateDfaMatcher( - EndpointDataSource dataSource, - MatcherPolicy[] policies = null, - EndpointSelector endpointSelector = null, - ILoggerFactory loggerFactory = null) - { - var serviceCollection = new ServiceCollection() - .AddLogging() - .AddOptions() - .AddRouting(options => - { - options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); - }); - - if (policies != null) - { - for (var i = 0; i < policies.Length; i++) - { - serviceCollection.AddSingleton(policies[i]); - } - } + return EndpointFactory.CreateRouteEndpoint(template, defaults, policies, requiredValues, order, displayName: template); + } - if (endpointSelector != null) + private DataSourceDependentMatcher CreateDfaMatcher( + EndpointDataSource dataSource, + MatcherPolicy[] policies = null, + EndpointSelector endpointSelector = null, + ILoggerFactory loggerFactory = null) + { + var serviceCollection = new ServiceCollection() + .AddLogging() + .AddOptions() + .AddRouting(options => { - serviceCollection.AddSingleton(endpointSelector); - } + options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); + }); - if (loggerFactory != null) + if (policies != null) + { + for (var i = 0; i < policies.Length; i++) { - serviceCollection.AddSingleton(loggerFactory); + serviceCollection.AddSingleton(policies[i]); } + } - var services = serviceCollection.BuildServiceProvider(); - - var factory = services.GetRequiredService(); - return Assert.IsType(factory.CreateMatcher(dataSource)); + if (endpointSelector != null) + { + serviceCollection.AddSingleton(endpointSelector); } - [Fact] - public async Task MatchAsync_ValidRouteConstraint_EndpointMatched() + if (loggerFactory != null) { - // Arrange - var endpointDataSource = new DefaultEndpointDataSource(new List + serviceCollection.AddSingleton(loggerFactory); + } + + var services = serviceCollection.BuildServiceProvider(); + + var factory = services.GetRequiredService(); + return Assert.IsType(factory.CreateMatcher(dataSource)); + } + + [Fact] + public async Task MatchAsync_ValidRouteConstraint_EndpointMatched() + { + // Arrange + var endpointDataSource = new DefaultEndpointDataSource(new List { CreateEndpoint("/{p:int}", 0) }); - var matcher = CreateDfaMatcher(endpointDataSource); + var matcher = CreateDfaMatcher(endpointDataSource); - var httpContext = CreateContext(); - httpContext.Request.Path = "/1"; + var httpContext = CreateContext(); + httpContext.Request.Path = "/1"; - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - Assert.NotNull(httpContext.GetEndpoint()); - } + // Assert + Assert.NotNull(httpContext.GetEndpoint()); + } - [Fact] - public async Task MatchAsync_InvalidRouteConstraint_NoEndpointMatched() - { - // Arrange - var endpointDataSource = new DefaultEndpointDataSource(new List + [Fact] + public async Task MatchAsync_InvalidRouteConstraint_NoEndpointMatched() + { + // Arrange + var endpointDataSource = new DefaultEndpointDataSource(new List { CreateEndpoint("/{p:int}", 0) }); - var matcher = CreateDfaMatcher(endpointDataSource); + var matcher = CreateDfaMatcher(endpointDataSource); - var httpContext = CreateContext(); - httpContext.Request.Path = "/One"; + var httpContext = CreateContext(); + httpContext.Request.Path = "/One"; - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - Assert.Null(httpContext.GetEndpoint()); - } + // Assert + Assert.Null(httpContext.GetEndpoint()); + } - [Fact] - public async Task MatchAsync_RequireValuesAndDefaultValues_EndpointMatched() - { - // Arrange - var endpoint = CreateEndpoint( - "{controller=Home}/{action=Index}/{id?}", - 0, - requiredValues: new { controller = "Home", action = "Index", area = (string)null, page = (string)null }); + [Fact] + public async Task MatchAsync_RequireValuesAndDefaultValues_EndpointMatched() + { + // Arrange + var endpoint = CreateEndpoint( + "{controller=Home}/{action=Index}/{id?}", + 0, + requiredValues: new { controller = "Home", action = "Index", area = (string)null, page = (string)null }); - var dataSource = new DefaultEndpointDataSource(new List + var dataSource = new DefaultEndpointDataSource(new List { endpoint }); - var matcher = CreateDfaMatcher(dataSource); + var matcher = CreateDfaMatcher(dataSource); - var httpContext = CreateContext(); - httpContext.Request.Path = "/"; + var httpContext = CreateContext(); + httpContext.Request.Path = "/"; - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - Assert.Same(endpoint, httpContext.GetEndpoint()); + // Assert + Assert.Same(endpoint, httpContext.GetEndpoint()); - Assert.Collection( - httpContext.Request.RouteValues.OrderBy(kvp => kvp.Key), - (kvp) => - { - Assert.Equal("action", kvp.Key); - Assert.Equal("Index", kvp.Value); - }, - (kvp) => - { - Assert.Equal("controller", kvp.Key); - Assert.Equal("Home", kvp.Value); - }); - } + Assert.Collection( + httpContext.Request.RouteValues.OrderBy(kvp => kvp.Key), + (kvp) => + { + Assert.Equal("action", kvp.Key); + Assert.Equal("Index", kvp.Value); + }, + (kvp) => + { + Assert.Equal("controller", kvp.Key); + Assert.Equal("Home", kvp.Value); + }); + } - [Fact] - public async Task MatchAsync_RequireValuesAndDifferentPath_NoEndpointMatched() - { - // Arrange - var endpoint = CreateEndpoint( - "{controller}/{action}", - 0, - requiredValues: new { controller = "Home", action = "Index", area = (string)null, page = (string)null }); + [Fact] + public async Task MatchAsync_RequireValuesAndDifferentPath_NoEndpointMatched() + { + // Arrange + var endpoint = CreateEndpoint( + "{controller}/{action}", + 0, + requiredValues: new { controller = "Home", action = "Index", area = (string)null, page = (string)null }); - var dataSource = new DefaultEndpointDataSource(new List + var dataSource = new DefaultEndpointDataSource(new List { endpoint }); - var matcher = CreateDfaMatcher(dataSource); + var matcher = CreateDfaMatcher(dataSource); - var httpContext = CreateContext(); - httpContext.Request.Path = "/Login/Index"; + var httpContext = CreateContext(); + httpContext.Request.Path = "/Login/Index"; - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - Assert.Null(httpContext.GetEndpoint()); - } + // Assert + Assert.Null(httpContext.GetEndpoint()); + } - [Fact] - public async Task MatchAsync_RequireValuesAndOptionalParameter_EndpointMatched() - { - // Arrange - var endpoint = CreateEndpoint( - "{controller}/{action}/{id?}", - 0, - requiredValues: new { controller = "Home", action = "Index", area = (string)null, page = (string)null }); + [Fact] + public async Task MatchAsync_RequireValuesAndOptionalParameter_EndpointMatched() + { + // Arrange + var endpoint = CreateEndpoint( + "{controller}/{action}/{id?}", + 0, + requiredValues: new { controller = "Home", action = "Index", area = (string)null, page = (string)null }); - var dataSource = new DefaultEndpointDataSource(new List + var dataSource = new DefaultEndpointDataSource(new List { endpoint }); - var matcher = CreateDfaMatcher(dataSource); - - var httpContext = CreateContext(); - httpContext.Request.Path = "/Home/Index/123"; - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - Assert.Same(endpoint, httpContext.GetEndpoint()); - - Assert.Collection( - httpContext.Request.RouteValues.OrderBy(kvp => kvp.Key), - (kvp) => - { - Assert.Equal("action", kvp.Key); - Assert.Equal("Index", kvp.Value); - }, - (kvp) => - { - Assert.Equal("controller", kvp.Key); - Assert.Equal("Home", kvp.Value); - }, - (kvp) => - { - Assert.Equal("id", kvp.Key); - Assert.Equal("123", kvp.Value); - }); - } + var matcher = CreateDfaMatcher(dataSource); - [Theory] - [InlineData("/")] - [InlineData("/TestController")] - [InlineData("/TestController/TestAction")] - [InlineData("/TestController/TestAction/17")] - [InlineData("/TestController/TestAction/17/catchAll")] - public async Task MatchAsync_ShortenedPattern_EndpointMatched(string path) - { - // Arrange - var endpoint = CreateEndpoint( - "{controller=TestController}/{action=TestAction}/{id=17}/{**catchAll}", - 0, - requiredValues: new { controller = "TestController", action = "TestAction", area = (string)null, page = (string)null }); + var httpContext = CreateContext(); + httpContext.Request.Path = "/Home/Index/123"; + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + Assert.Same(endpoint, httpContext.GetEndpoint()); + + Assert.Collection( + httpContext.Request.RouteValues.OrderBy(kvp => kvp.Key), + (kvp) => + { + Assert.Equal("action", kvp.Key); + Assert.Equal("Index", kvp.Value); + }, + (kvp) => + { + Assert.Equal("controller", kvp.Key); + Assert.Equal("Home", kvp.Value); + }, + (kvp) => + { + Assert.Equal("id", kvp.Key); + Assert.Equal("123", kvp.Value); + }); + } - var dataSource = new DefaultEndpointDataSource(new List + [Theory] + [InlineData("/")] + [InlineData("/TestController")] + [InlineData("/TestController/TestAction")] + [InlineData("/TestController/TestAction/17")] + [InlineData("/TestController/TestAction/17/catchAll")] + public async Task MatchAsync_ShortenedPattern_EndpointMatched(string path) + { + // Arrange + var endpoint = CreateEndpoint( + "{controller=TestController}/{action=TestAction}/{id=17}/{**catchAll}", + 0, + requiredValues: new { controller = "TestController", action = "TestAction", area = (string)null, page = (string)null }); + + var dataSource = new DefaultEndpointDataSource(new List { endpoint }); - var matcher = CreateDfaMatcher(dataSource); + var matcher = CreateDfaMatcher(dataSource); - var httpContext = CreateContext(); - httpContext.Request.Path = path; + var httpContext = CreateContext(); + httpContext.Request.Path = path; - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - Assert.Same(endpoint, httpContext.GetEndpoint()); + // Assert + Assert.Same(endpoint, httpContext.GetEndpoint()); - Assert.Equal("TestAction", httpContext.Request.RouteValues["action"]); - Assert.Equal("TestController", httpContext.Request.RouteValues["controller"]); - Assert.Equal("17", httpContext.Request.RouteValues["id"]); - } + Assert.Equal("TestAction", httpContext.Request.RouteValues["action"]); + Assert.Equal("TestController", httpContext.Request.RouteValues["controller"]); + Assert.Equal("17", httpContext.Request.RouteValues["id"]); + } - [Fact] - public async Task MatchAsync_MultipleEndpointsWithDifferentRequiredValues_EndpointMatched() - { - // Arrange - var endpoint1 = CreateEndpoint( - "{controller}/{action}/{id?}", - 0, - requiredValues: new { controller = "Home", action = "Index", area = (string)null, page = (string)null }); - var endpoint2 = CreateEndpoint( - "{controller}/{action}/{id?}", - 0, - requiredValues: new { controller = "Login", action = "Index", area = (string)null, page = (string)null }); - - var dataSource = new DefaultEndpointDataSource(new List + [Fact] + public async Task MatchAsync_MultipleEndpointsWithDifferentRequiredValues_EndpointMatched() + { + // Arrange + var endpoint1 = CreateEndpoint( + "{controller}/{action}/{id?}", + 0, + requiredValues: new { controller = "Home", action = "Index", area = (string)null, page = (string)null }); + var endpoint2 = CreateEndpoint( + "{controller}/{action}/{id?}", + 0, + requiredValues: new { controller = "Login", action = "Index", area = (string)null, page = (string)null }); + + var dataSource = new DefaultEndpointDataSource(new List { endpoint1, endpoint2 }); - var matcher = CreateDfaMatcher(dataSource); + var matcher = CreateDfaMatcher(dataSource); - var httpContext = CreateContext(); - httpContext.Request.Path = "/Home/Index/123"; + var httpContext = CreateContext(); + httpContext.Request.Path = "/Home/Index/123"; - // Act 1 - await matcher.MatchAsync(httpContext); + // Act 1 + await matcher.MatchAsync(httpContext); - // Assert 1 - Assert.Same(endpoint1, httpContext.GetEndpoint()); + // Assert 1 + Assert.Same(endpoint1, httpContext.GetEndpoint()); - httpContext.Request.Path = "/Login/Index/123"; + httpContext.Request.Path = "/Login/Index/123"; - // Act 2 - await matcher.MatchAsync(httpContext); + // Act 2 + await matcher.MatchAsync(httpContext); - // Assert 2 - Assert.Same(endpoint2, httpContext.GetEndpoint()); - } + // Assert 2 + Assert.Same(endpoint2, httpContext.GetEndpoint()); + } - [Fact] - public async Task MatchAsync_ParameterTransformer_EndpointMatched() - { - // Arrange - var endpoint = CreateEndpoint( - "ConventionalTransformerRoute/{controller:slugify}/{action=Index}/{param:slugify?}", - 0, - requiredValues: new { controller = "ConventionalTransformer", action = "Index", area = (string)null, page = (string)null }); + [Fact] + public async Task MatchAsync_ParameterTransformer_EndpointMatched() + { + // Arrange + var endpoint = CreateEndpoint( + "ConventionalTransformerRoute/{controller:slugify}/{action=Index}/{param:slugify?}", + 0, + requiredValues: new { controller = "ConventionalTransformer", action = "Index", area = (string)null, page = (string)null }); - var dataSource = new DefaultEndpointDataSource(new List + var dataSource = new DefaultEndpointDataSource(new List { endpoint }); - var matcher = CreateDfaMatcher(dataSource); + var matcher = CreateDfaMatcher(dataSource); - var httpContext = CreateContext(); - httpContext.Request.Path = "/ConventionalTransformerRoute/conventional-transformer/Index"; + var httpContext = CreateContext(); + httpContext.Request.Path = "/ConventionalTransformerRoute/conventional-transformer/Index"; - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - Assert.Same(endpoint, httpContext.GetEndpoint()); + // Assert + Assert.Same(endpoint, httpContext.GetEndpoint()); - Assert.Collection( - httpContext.Request.RouteValues.OrderBy(kvp => kvp.Key), - (kvp) => - { - Assert.Equal("action", kvp.Key); - Assert.Equal("Index", kvp.Value); - }, - (kvp) => - { - Assert.Equal("controller", kvp.Key); - Assert.Equal("ConventionalTransformer", kvp.Value); - }); - } + Assert.Collection( + httpContext.Request.RouteValues.OrderBy(kvp => kvp.Key), + (kvp) => + { + Assert.Equal("action", kvp.Key); + Assert.Equal("Index", kvp.Value); + }, + (kvp) => + { + Assert.Equal("controller", kvp.Key); + Assert.Equal("ConventionalTransformer", kvp.Value); + }); + } - [Fact] - public void MatchAsync_ConstrainedParameter_EndpointMatched() - { - // Arrange - var endpoint1 = CreateEndpoint("a/c", 0); - var endpoint2 = CreateEndpoint("{param:length(2)}/b/c", 0); + [Fact] + public void MatchAsync_ConstrainedParameter_EndpointMatched() + { + // Arrange + var endpoint1 = CreateEndpoint("a/c", 0); + var endpoint2 = CreateEndpoint("{param:length(2)}/b/c", 0); - var dataSource = new DefaultEndpointDataSource(new List + var dataSource = new DefaultEndpointDataSource(new List { endpoint1, endpoint2 }); - var matcher = (DfaMatcher)CreateDfaMatcher(dataSource).CurrentMatcher; - var buffer = new PathSegment[3]; - var (context, path, count) = CreateMatchingContext("/aa/b/c", buffer); + var matcher = (DfaMatcher)CreateDfaMatcher(dataSource).CurrentMatcher; + var buffer = new PathSegment[3]; + var (context, path, count) = CreateMatchingContext("/aa/b/c", buffer); - // Act - var set = matcher.FindCandidateSet(context, path, buffer.AsSpan().Slice(0, count)); + // Act + var set = matcher.FindCandidateSet(context, path, buffer.AsSpan().Slice(0, count)); - // Assert - // We expect endpoint2 to match here since we trimmed the branch for the parameter based on `a` not meeting - // the constraints. - var candidate = Assert.Single(set.candidates); - Assert.Same(endpoint2, candidate.Endpoint); - } + // Assert + // We expect endpoint2 to match here since we trimmed the branch for the parameter based on `a` not meeting + // the constraints. + var candidate = Assert.Single(set.candidates); + Assert.Same(endpoint2, candidate.Endpoint); + } - [Fact] - public void MatchAsync_ConstrainedParameter_EndpointNotMatched() - { - // Arrange - var endpoint1 = CreateEndpoint("a/c", 0); - var endpoint2 = CreateEndpoint("{param:length(2)}/b/c", 0); + [Fact] + public void MatchAsync_ConstrainedParameter_EndpointNotMatched() + { + // Arrange + var endpoint1 = CreateEndpoint("a/c", 0); + var endpoint2 = CreateEndpoint("{param:length(2)}/b/c", 0); - var dataSource = new DefaultEndpointDataSource(new List + var dataSource = new DefaultEndpointDataSource(new List { endpoint1, endpoint2 }); - var matcher = (DfaMatcher)CreateDfaMatcher(dataSource).CurrentMatcher; - var buffer = new PathSegment[3]; - var (context, path, count) = CreateMatchingContext("/a/b/c", buffer); + var matcher = (DfaMatcher)CreateDfaMatcher(dataSource).CurrentMatcher; + var buffer = new PathSegment[3]; + var (context, path, count) = CreateMatchingContext("/a/b/c", buffer); - // Act - var set = matcher.FindCandidateSet(context, path, buffer.AsSpan().Slice(0, count)); + // Act + var set = matcher.FindCandidateSet(context, path, buffer.AsSpan().Slice(0, count)); - // Assert - // We expect no candidates here, since the path on the tree (a -> b -> c = ({param:length(2)}/b/c)) for not meeting the length(2) constraint. - Assert.Empty(set.candidates); - } + // Assert + // We expect no candidates here, since the path on the tree (a -> b -> c = ({param:length(2)}/b/c)) for not meeting the length(2) constraint. + Assert.Empty(set.candidates); + } - [Fact] - public void MatchAsync_ConstrainedParameter_EndpointMatched_WhenExplicitRouteExists() - { - // Arrange - // Note that there is now an explicit branch created by the first endpoint, however endpoint 2 will - // be filtered out of the candidates list because it didn't meet the constraint. - var endpoint1 = CreateEndpoint("a/b/c", 0); - var endpoint2 = CreateEndpoint("{param:length(2)}/b/c", 0); + [Fact] + public void MatchAsync_ConstrainedParameter_EndpointMatched_WhenExplicitRouteExists() + { + // Arrange + // Note that there is now an explicit branch created by the first endpoint, however endpoint 2 will + // be filtered out of the candidates list because it didn't meet the constraint. + var endpoint1 = CreateEndpoint("a/b/c", 0); + var endpoint2 = CreateEndpoint("{param:length(2)}/b/c", 0); - var dataSource = new DefaultEndpointDataSource(new List + var dataSource = new DefaultEndpointDataSource(new List { endpoint1, endpoint2 }); - var matcher = (DfaMatcher)CreateDfaMatcher(dataSource).CurrentMatcher; - var buffer = new PathSegment[3]; - var (context, path, count) = CreateMatchingContext("/a/b/c", buffer); + var matcher = (DfaMatcher)CreateDfaMatcher(dataSource).CurrentMatcher; + var buffer = new PathSegment[3]; + var (context, path, count) = CreateMatchingContext("/a/b/c", buffer); - // Act - var set = matcher.FindCandidateSet(context, path, buffer.AsSpan().Slice(0, count)); + // Act + var set = matcher.FindCandidateSet(context, path, buffer.AsSpan().Slice(0, count)); - // Assert - // We expect only one candidate, since the path on the tree (a -> b -> c = ({param:length(2)}/b/c)) does not meet the length(2) constraint. - var candidate = Assert.Single(set.candidates); - Assert.Same(endpoint1, candidate.Endpoint); - } + // Assert + // We expect only one candidate, since the path on the tree (a -> b -> c = ({param:length(2)}/b/c)) does not meet the length(2) constraint. + var candidate = Assert.Single(set.candidates); + Assert.Same(endpoint1, candidate.Endpoint); + } - [Fact] - public void MatchAsync_ConstrainedParameter_EndpointMatchedWithTwoCandidates_WhenLiteralMeetsConstraint() - { - // Arrange - // Note that the literal now meets the constraint, so there will be an explicit branch and two candidates - var endpoint1 = CreateEndpoint("aa/b/c", 0); - var endpoint2 = CreateEndpoint("{param:length(2)}/b/c", 0); - var endpoints = new List + [Fact] + public void MatchAsync_ConstrainedParameter_EndpointMatchedWithTwoCandidates_WhenLiteralMeetsConstraint() + { + // Arrange + // Note that the literal now meets the constraint, so there will be an explicit branch and two candidates + var endpoint1 = CreateEndpoint("aa/b/c", 0); + var endpoint2 = CreateEndpoint("{param:length(2)}/b/c", 0); + var endpoints = new List { endpoint2, endpoint1, }; - var dataSource = new DefaultEndpointDataSource(endpoints); + var dataSource = new DefaultEndpointDataSource(endpoints); - var matcher = (DfaMatcher)CreateDfaMatcher(dataSource).CurrentMatcher; - var buffer = new PathSegment[3]; - var (context, path, count) = CreateMatchingContext("/aa/b/c", buffer); + var matcher = (DfaMatcher)CreateDfaMatcher(dataSource).CurrentMatcher; + var buffer = new PathSegment[3]; + var (context, path, count) = CreateMatchingContext("/aa/b/c", buffer); - // Act - var set = matcher.FindCandidateSet(context, path, buffer.AsSpan().Slice(0, count)); + // Act + var set = matcher.FindCandidateSet(context, path, buffer.AsSpan().Slice(0, count)); - // Assert - // We expect 2 candidates, since the path on the tree (aa -> b -> c = ({param:length(2)}/b/c)) meets the length(2) constraint. - Assert.Equal(endpoints.ToArray(), set.candidates.Select(e => e.Endpoint).OrderBy(e => ((RouteEndpoint)e).RoutePattern.RawText).ToArray()); - } + // Assert + // We expect 2 candidates, since the path on the tree (aa -> b -> c = ({param:length(2)}/b/c)) meets the length(2) constraint. + Assert.Equal(endpoints.ToArray(), set.candidates.Select(e => e.Endpoint).OrderBy(e => ((RouteEndpoint)e).RoutePattern.RawText).ToArray()); + } - [Fact] - public void MatchAsync_ConstrainedParameter_MiddleSegment_EndpointMatched() - { - // Arrange - var endpoint1 = CreateEndpoint("a/b/c", 0); - var endpoint2 = CreateEndpoint("a/{param:length(2)}/c", 0); + [Fact] + public void MatchAsync_ConstrainedParameter_MiddleSegment_EndpointMatched() + { + // Arrange + var endpoint1 = CreateEndpoint("a/b/c", 0); + var endpoint2 = CreateEndpoint("a/{param:length(2)}/c", 0); - var dataSource = new DefaultEndpointDataSource(new List + var dataSource = new DefaultEndpointDataSource(new List { endpoint1, endpoint2 }); - var matcher = (DfaMatcher)CreateDfaMatcher(dataSource).CurrentMatcher; - var buffer = new PathSegment[3]; - var (context, path, count) = CreateMatchingContext("/a/bb/c", buffer); + var matcher = (DfaMatcher)CreateDfaMatcher(dataSource).CurrentMatcher; + var buffer = new PathSegment[3]; + var (context, path, count) = CreateMatchingContext("/a/bb/c", buffer); - // Act - var set = matcher.FindCandidateSet(context, path, buffer.AsSpan().Slice(0, count)); + // Act + var set = matcher.FindCandidateSet(context, path, buffer.AsSpan().Slice(0, count)); - // Assert - // We expect endpoint2 to match here since we trimmed the branch (a -> b -> c = (a/{param:length(2)}/c)) for the parameter based on `b` not meeting the length(2) constraint. - var candidate = Assert.Single(set.candidates); - Assert.Same(endpoint2, candidate.Endpoint); - } + // Assert + // We expect endpoint2 to match here since we trimmed the branch (a -> b -> c = (a/{param:length(2)}/c)) for the parameter based on `b` not meeting the length(2) constraint. + var candidate = Assert.Single(set.candidates); + Assert.Same(endpoint2, candidate.Endpoint); + } - [Fact] - public void MatchAsync_ConstrainedParameter_MiddleSegment_EndpointNotMatched() - { - // Arrange - var endpoint1 = CreateEndpoint("a/b/d", 0); - var endpoint2 = CreateEndpoint("a/{param:length(2)}/c", 0); + [Fact] + public void MatchAsync_ConstrainedParameter_MiddleSegment_EndpointNotMatched() + { + // Arrange + var endpoint1 = CreateEndpoint("a/b/d", 0); + var endpoint2 = CreateEndpoint("a/{param:length(2)}/c", 0); - var dataSource = new DefaultEndpointDataSource(new List + var dataSource = new DefaultEndpointDataSource(new List { endpoint1, endpoint2 }); - var matcher = (DfaMatcher)CreateDfaMatcher(dataSource).CurrentMatcher; - var buffer = new PathSegment[3]; - var (context, path, count) = CreateMatchingContext("/a/b/c", buffer); + var matcher = (DfaMatcher)CreateDfaMatcher(dataSource).CurrentMatcher; + var buffer = new PathSegment[3]; + var (context, path, count) = CreateMatchingContext("/a/b/c", buffer); - // Act - var set = matcher.FindCandidateSet(context, path, buffer.AsSpan().Slice(0, count)); + // Act + var set = matcher.FindCandidateSet(context, path, buffer.AsSpan().Slice(0, count)); - // Assert - // We expect no candidates here since we trimmed the branch (a -> b -> c = (a/{param:length(2)}/c)) for the parameter based on `b` not meeting the length(2) constraint. - Assert.Empty(set.candidates); - } + // Assert + // We expect no candidates here since we trimmed the branch (a -> b -> c = (a/{param:length(2)}/c)) for the parameter based on `b` not meeting the length(2) constraint. + Assert.Empty(set.candidates); + } - [Fact] - public void MatchAsync_ConstrainedParameter_MiddleSegment_EndpointMatched_WhenExplicitRouteExists() - { - // Arrange - // Note that there is now an explicit branch created by the first endpoint, however endpoint 2 will - // be filtered out of the candidates list because it didn't meet the constraint. - var endpoint1 = CreateEndpoint("a/b/c", 0); - var endpoint2 = CreateEndpoint("a/{param:length(2)}/c", 0); + [Fact] + public void MatchAsync_ConstrainedParameter_MiddleSegment_EndpointMatched_WhenExplicitRouteExists() + { + // Arrange + // Note that there is now an explicit branch created by the first endpoint, however endpoint 2 will + // be filtered out of the candidates list because it didn't meet the constraint. + var endpoint1 = CreateEndpoint("a/b/c", 0); + var endpoint2 = CreateEndpoint("a/{param:length(2)}/c", 0); - var dataSource = new DefaultEndpointDataSource(new List + var dataSource = new DefaultEndpointDataSource(new List { endpoint1, endpoint2 }); - var matcher = (DfaMatcher)CreateDfaMatcher(dataSource).CurrentMatcher; - var buffer = new PathSegment[3]; - var (context, path, count) = CreateMatchingContext("/a/b/c", buffer); + var matcher = (DfaMatcher)CreateDfaMatcher(dataSource).CurrentMatcher; + var buffer = new PathSegment[3]; + var (context, path, count) = CreateMatchingContext("/a/b/c", buffer); - // Act - var set = matcher.FindCandidateSet(context, path, buffer.AsSpan().Slice(0, count)); + // Act + var set = matcher.FindCandidateSet(context, path, buffer.AsSpan().Slice(0, count)); - // Assert - // We expect only one candidate, since the path on the tree (a -> b -> c = (a/{param:length(2)}/c)) does not meet the length(2) constraint. - var candidate = Assert.Single(set.candidates); - Assert.Same(endpoint1, candidate.Endpoint); - } + // Assert + // We expect only one candidate, since the path on the tree (a -> b -> c = (a/{param:length(2)}/c)) does not meet the length(2) constraint. + var candidate = Assert.Single(set.candidates); + Assert.Same(endpoint1, candidate.Endpoint); + } - [Fact] - public void MatchAsync_ConstrainedParameter_MiddleSegment_EndpointMatchedWithTwoCandidates_WhenLiteralMeetsConstraint() - { - // Arrange - // Note that the literal now meets the constraint, so there will be an explicit branch and two candidates - var endpoint1 = CreateEndpoint("a/bb/c", 0); - var endpoint2 = CreateEndpoint("a/{param:length(2)}/c", 0); - var endpoints = new List + [Fact] + public void MatchAsync_ConstrainedParameter_MiddleSegment_EndpointMatchedWithTwoCandidates_WhenLiteralMeetsConstraint() + { + // Arrange + // Note that the literal now meets the constraint, so there will be an explicit branch and two candidates + var endpoint1 = CreateEndpoint("a/bb/c", 0); + var endpoint2 = CreateEndpoint("a/{param:length(2)}/c", 0); + var endpoints = new List { endpoint2, endpoint1, }; - var dataSource = new DefaultEndpointDataSource(endpoints); + var dataSource = new DefaultEndpointDataSource(endpoints); - var matcher = (DfaMatcher)CreateDfaMatcher(dataSource).CurrentMatcher; - var buffer = new PathSegment[3]; - var (context, path, count) = CreateMatchingContext("/a/bb/c", buffer); + var matcher = (DfaMatcher)CreateDfaMatcher(dataSource).CurrentMatcher; + var buffer = new PathSegment[3]; + var (context, path, count) = CreateMatchingContext("/a/bb/c", buffer); - // Act - var set = matcher.FindCandidateSet(context, path, buffer.AsSpan().Slice(0, count)); + // Act + var set = matcher.FindCandidateSet(context, path, buffer.AsSpan().Slice(0, count)); - // Assert - // We expect 2 candidates, since the path on the tree (aa -> b -> c = ({param:length(2)}/b/c)) meets the length(2) constraint. - Assert.Equal(endpoints.ToArray(), set.candidates.Select(e => e.Endpoint).OrderBy(e => ((RouteEndpoint)e).RoutePattern.RawText).ToArray()); - } + // Assert + // We expect 2 candidates, since the path on the tree (aa -> b -> c = ({param:length(2)}/b/c)) meets the length(2) constraint. + Assert.Equal(endpoints.ToArray(), set.candidates.Select(e => e.Endpoint).OrderBy(e => ((RouteEndpoint)e).RoutePattern.RawText).ToArray()); + } - private (HttpContext context, string path, int count) CreateMatchingContext(string requestPath, PathSegment[] buffer) - { - var context = CreateContext(); - context.Request.Path = requestPath; + private (HttpContext context, string path, int count) CreateMatchingContext(string requestPath, PathSegment[] buffer) + { + var context = CreateContext(); + context.Request.Path = requestPath; - // First tokenize the path into series of segments. - var count = FastPathTokenizer.Tokenize(requestPath, buffer); - return (context, requestPath, count); - } + // First tokenize the path into series of segments. + var count = FastPathTokenizer.Tokenize(requestPath, buffer); + return (context, requestPath, count); + } - [Fact] - public async Task MatchAsync_DifferentDefaultCase_RouteValueUsesDefaultCase() - { - // Arrange - var endpoint = CreateEndpoint( - "{controller}/{action=TESTACTION}/{id?}", - 0, - requiredValues: new { controller = "TestController", action = "TestAction" }); + [Fact] + public async Task MatchAsync_DifferentDefaultCase_RouteValueUsesDefaultCase() + { + // Arrange + var endpoint = CreateEndpoint( + "{controller}/{action=TESTACTION}/{id?}", + 0, + requiredValues: new { controller = "TestController", action = "TestAction" }); - var dataSource = new DefaultEndpointDataSource(new List + var dataSource = new DefaultEndpointDataSource(new List { endpoint }); - var matcher = CreateDfaMatcher(dataSource); + var matcher = CreateDfaMatcher(dataSource); - var httpContext = CreateContext(); - httpContext.Request.Path = "/TestController"; + var httpContext = CreateContext(); + httpContext.Request.Path = "/TestController"; - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - Assert.Same(endpoint, httpContext.GetEndpoint()); + // Assert + Assert.Same(endpoint, httpContext.GetEndpoint()); - Assert.Collection( - httpContext.Request.RouteValues.OrderBy(kvp => kvp.Key), - (kvp) => - { - Assert.Equal("action", kvp.Key); - Assert.Equal("TESTACTION", kvp.Value); - }, - (kvp) => - { - Assert.Equal("controller", kvp.Key); - Assert.Equal("TestController", kvp.Value); - }); - } + Assert.Collection( + httpContext.Request.RouteValues.OrderBy(kvp => kvp.Key), + (kvp) => + { + Assert.Equal("action", kvp.Key); + Assert.Equal("TESTACTION", kvp.Value); + }, + (kvp) => + { + Assert.Equal("controller", kvp.Key); + Assert.Equal("TestController", kvp.Value); + }); + } - [Fact] - public async Task MatchAsync_DuplicateTemplatesAndDifferentOrder_LowerOrderEndpointMatched() - { - // Arrange - var higherOrderEndpoint = CreateEndpoint("/Teams", 1); - var lowerOrderEndpoint = CreateEndpoint("/Teams", 0); + [Fact] + public async Task MatchAsync_DuplicateTemplatesAndDifferentOrder_LowerOrderEndpointMatched() + { + // Arrange + var higherOrderEndpoint = CreateEndpoint("/Teams", 1); + var lowerOrderEndpoint = CreateEndpoint("/Teams", 0); - var endpointDataSource = new DefaultEndpointDataSource(new List + var endpointDataSource = new DefaultEndpointDataSource(new List { higherOrderEndpoint, lowerOrderEndpoint }); - var matcher = CreateDfaMatcher(endpointDataSource); + var matcher = CreateDfaMatcher(endpointDataSource); - var httpContext = CreateContext(); - httpContext.Request.Path = "/Teams"; + var httpContext = CreateContext(); + httpContext.Request.Path = "/Teams"; - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - Assert.Equal(lowerOrderEndpoint, httpContext.GetEndpoint()); - } + // Assert + Assert.Equal(lowerOrderEndpoint, httpContext.GetEndpoint()); + } - [Fact] - public async Task MatchAsync_MultipleMatches_EndpointSelectorCalled() - { - // Arrange - var endpoint1 = CreateEndpoint("/Teams", 0); - var endpoint2 = CreateEndpoint("/Teams", 1); - - var endpointSelector = new Mock(); - endpointSelector - .Setup(s => s.SelectAsync(It.IsAny(), It.IsAny())) - .Callback((c, cs) => - { - Assert.Equal(2, cs.Count); - - Assert.Same(endpoint1, cs[0].Endpoint); - Assert.True(cs.IsValidCandidate(0)); - Assert.Equal(0, cs[0].Score); - Assert.Null(cs[0].Values); - - Assert.Same(endpoint2, cs[1].Endpoint); - Assert.True(cs.IsValidCandidate(1)); - Assert.Equal(1, cs[1].Score); - Assert.Null(cs[1].Values); - - c.SetEndpoint(endpoint2); - }) - .Returns(Task.CompletedTask); - - var endpointDataSource = new DefaultEndpointDataSource(new List + [Fact] + public async Task MatchAsync_MultipleMatches_EndpointSelectorCalled() + { + // Arrange + var endpoint1 = CreateEndpoint("/Teams", 0); + var endpoint2 = CreateEndpoint("/Teams", 1); + + var endpointSelector = new Mock(); + endpointSelector + .Setup(s => s.SelectAsync(It.IsAny(), It.IsAny())) + .Callback((c, cs) => + { + Assert.Equal(2, cs.Count); + + Assert.Same(endpoint1, cs[0].Endpoint); + Assert.True(cs.IsValidCandidate(0)); + Assert.Equal(0, cs[0].Score); + Assert.Null(cs[0].Values); + + Assert.Same(endpoint2, cs[1].Endpoint); + Assert.True(cs.IsValidCandidate(1)); + Assert.Equal(1, cs[1].Score); + Assert.Null(cs[1].Values); + + c.SetEndpoint(endpoint2); + }) + .Returns(Task.CompletedTask); + + var endpointDataSource = new DefaultEndpointDataSource(new List { endpoint1, endpoint2 }); - var matcher = CreateDfaMatcher(endpointDataSource, endpointSelector: endpointSelector.Object); + var matcher = CreateDfaMatcher(endpointDataSource, endpointSelector: endpointSelector.Object); - var httpContext = CreateContext(); - httpContext.Request.Path = "/Teams"; + var httpContext = CreateContext(); + httpContext.Request.Path = "/Teams"; - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - Assert.Equal(endpoint2, httpContext.GetEndpoint()); - } + // Assert + Assert.Equal(endpoint2, httpContext.GetEndpoint()); + } - [Fact] - public async Task MatchAsync_MultipleMatches_EndpointSelectorCalled_AllocatesDictionaryForRouteParameter() - { - // Arrange - var endpoint1 = CreateEndpoint("/Teams/{x?}", 0); - var endpoint2 = CreateEndpoint("/Teams/{x?}", 1); - - var endpointSelector = new Mock(); - endpointSelector - .Setup(s => s.SelectAsync(It.IsAny(), It.IsAny())) - .Callback((c, cs) => - { - Assert.Equal(2, cs.Count); - - Assert.Same(endpoint1, cs[0].Endpoint); - Assert.True(cs.IsValidCandidate(0)); - Assert.Equal(0, cs[0].Score); - Assert.Empty(cs[0].Values); - - Assert.Same(endpoint2, cs[1].Endpoint); - Assert.True(cs.IsValidCandidate(1)); - Assert.Equal(1, cs[1].Score); - Assert.Empty(cs[1].Values); - - c.SetEndpoint(endpoint2); - }) - .Returns(Task.CompletedTask); - - var endpointDataSource = new DefaultEndpointDataSource(new List + [Fact] + public async Task MatchAsync_MultipleMatches_EndpointSelectorCalled_AllocatesDictionaryForRouteParameter() + { + // Arrange + var endpoint1 = CreateEndpoint("/Teams/{x?}", 0); + var endpoint2 = CreateEndpoint("/Teams/{x?}", 1); + + var endpointSelector = new Mock(); + endpointSelector + .Setup(s => s.SelectAsync(It.IsAny(), It.IsAny())) + .Callback((c, cs) => + { + Assert.Equal(2, cs.Count); + + Assert.Same(endpoint1, cs[0].Endpoint); + Assert.True(cs.IsValidCandidate(0)); + Assert.Equal(0, cs[0].Score); + Assert.Empty(cs[0].Values); + + Assert.Same(endpoint2, cs[1].Endpoint); + Assert.True(cs.IsValidCandidate(1)); + Assert.Equal(1, cs[1].Score); + Assert.Empty(cs[1].Values); + + c.SetEndpoint(endpoint2); + }) + .Returns(Task.CompletedTask); + + var endpointDataSource = new DefaultEndpointDataSource(new List { endpoint1, endpoint2 }); - var matcher = CreateDfaMatcher(endpointDataSource, endpointSelector: endpointSelector.Object); + var matcher = CreateDfaMatcher(endpointDataSource, endpointSelector: endpointSelector.Object); - var httpContext = CreateContext(); - httpContext.Request.Path = "/Teams"; + var httpContext = CreateContext(); + httpContext.Request.Path = "/Teams"; - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - Assert.Equal(endpoint2, httpContext.GetEndpoint()); - } + // Assert + Assert.Equal(endpoint2, httpContext.GetEndpoint()); + } - [Fact] - public async Task MatchAsync_MultipleMatches_EndpointSelectorCalled_AllocatesDictionaryForRouteConstraint() - { - // Arrange - var constraint = new OptionalRouteConstraint(new IntRouteConstraint()); - var endpoint1 = CreateEndpoint("/Teams", 0, policies: new { x = constraint, }); - var endpoint2 = CreateEndpoint("/Teams", 1, policies: new { x = constraint, }); - - var endpointSelector = new Mock(); - endpointSelector - .Setup(s => s.SelectAsync(It.IsAny(), It.IsAny())) - .Callback((c, cs) => - { - Assert.Equal(2, cs.Count); - - Assert.Same(endpoint1, cs[0].Endpoint); - Assert.True(cs.IsValidCandidate(0)); - Assert.Equal(0, cs[0].Score); - Assert.Empty(cs[0].Values); - - Assert.Same(endpoint2, cs[1].Endpoint); - Assert.True(cs.IsValidCandidate(1)); - Assert.Equal(1, cs[1].Score); - Assert.Empty(cs[1].Values); - - c.SetEndpoint(endpoint2); - }) - .Returns(Task.CompletedTask); - - var endpointDataSource = new DefaultEndpointDataSource(new List + [Fact] + public async Task MatchAsync_MultipleMatches_EndpointSelectorCalled_AllocatesDictionaryForRouteConstraint() + { + // Arrange + var constraint = new OptionalRouteConstraint(new IntRouteConstraint()); + var endpoint1 = CreateEndpoint("/Teams", 0, policies: new { x = constraint, }); + var endpoint2 = CreateEndpoint("/Teams", 1, policies: new { x = constraint, }); + + var endpointSelector = new Mock(); + endpointSelector + .Setup(s => s.SelectAsync(It.IsAny(), It.IsAny())) + .Callback((c, cs) => + { + Assert.Equal(2, cs.Count); + + Assert.Same(endpoint1, cs[0].Endpoint); + Assert.True(cs.IsValidCandidate(0)); + Assert.Equal(0, cs[0].Score); + Assert.Empty(cs[0].Values); + + Assert.Same(endpoint2, cs[1].Endpoint); + Assert.True(cs.IsValidCandidate(1)); + Assert.Equal(1, cs[1].Score); + Assert.Empty(cs[1].Values); + + c.SetEndpoint(endpoint2); + }) + .Returns(Task.CompletedTask); + + var endpointDataSource = new DefaultEndpointDataSource(new List { endpoint1, endpoint2 }); - var matcher = CreateDfaMatcher(endpointDataSource, endpointSelector: endpointSelector.Object); + var matcher = CreateDfaMatcher(endpointDataSource, endpointSelector: endpointSelector.Object); - var httpContext = CreateContext(); - httpContext.Request.Path = "/Teams"; + var httpContext = CreateContext(); + httpContext.Request.Path = "/Teams"; - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - Assert.Equal(endpoint2, httpContext.GetEndpoint()); - } + // Assert + Assert.Equal(endpoint2, httpContext.GetEndpoint()); + } - [Fact] - public async Task MatchAsync_NoCandidates_Logging() - { - // Arrange - var endpointDataSource = new DefaultEndpointDataSource(new List + [Fact] + public async Task MatchAsync_NoCandidates_Logging() + { + // Arrange + var endpointDataSource = new DefaultEndpointDataSource(new List { CreateEndpoint("/{p:int}", 0) }); - var sink = new TestSink(); - var matcher = CreateDfaMatcher(endpointDataSource, loggerFactory: new TestLoggerFactory(sink, enabled: true)); + var sink = new TestSink(); + var matcher = CreateDfaMatcher(endpointDataSource, loggerFactory: new TestLoggerFactory(sink, enabled: true)); - var httpContext = CreateContext(); - httpContext.Request.Path = "/"; + var httpContext = CreateContext(); + httpContext.Request.Path = "/"; - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - Assert.Null(httpContext.GetEndpoint()); + // Assert + Assert.Null(httpContext.GetEndpoint()); - Assert.Collection( - sink.Writes, - (log) => - { - Assert.Equal(1000, log.EventId); - Assert.Equal("No candidates found for the request path '/'", log.Message); - }); - } + Assert.Collection( + sink.Writes, + (log) => + { + Assert.Equal(1000, log.EventId); + Assert.Equal("No candidates found for the request path '/'", log.Message); + }); + } - [Fact] - public async Task MatchAsync_ConstraintRejectsEndpoint_Logging() - { - // Arrange - var endpointDataSource = new DefaultEndpointDataSource(new List + [Fact] + public async Task MatchAsync_ConstraintRejectsEndpoint_Logging() + { + // Arrange + var endpointDataSource = new DefaultEndpointDataSource(new List { CreateEndpoint("/{p:int}", 0) }); - var sink = new TestSink(); - var matcher = CreateDfaMatcher(endpointDataSource, loggerFactory: new TestLoggerFactory(sink, enabled: true)); - - var httpContext = CreateContext(); - httpContext.Request.Path = "/One"; - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - Assert.Null(httpContext.GetEndpoint()); - - Assert.Collection( - sink.Writes, - (log) => - { - Assert.Equal(1001, log.EventId); - Assert.Equal("1 candidate(s) found for the request path '/One'", log.Message); - }, - (log) => - { - Assert.Equal(1003, log.EventId); - Assert.Equal("Endpoint '/{p:int}' with route pattern '/{p:int}' was rejected by constraint 'p':'Microsoft.AspNetCore.Routing.Constraints.IntRouteConstraint' with value 'One' for the request path '/One'", log.Message); - }, - (log) => - { - Assert.Equal(1004, log.EventId); - Assert.Equal("Endpoint '/{p:int}' with route pattern '/{p:int}' is not valid for the request path '/One'", log.Message); - }); - } + var sink = new TestSink(); + var matcher = CreateDfaMatcher(endpointDataSource, loggerFactory: new TestLoggerFactory(sink, enabled: true)); - [Fact] - public async Task MatchAsync_ComplexSegmentRejectsEndpoint_Logging() - { - // Arrange - var endpointDataSource = new DefaultEndpointDataSource(new List + var httpContext = CreateContext(); + httpContext.Request.Path = "/One"; + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + Assert.Null(httpContext.GetEndpoint()); + + Assert.Collection( + sink.Writes, + (log) => + { + Assert.Equal(1001, log.EventId); + Assert.Equal("1 candidate(s) found for the request path '/One'", log.Message); + }, + (log) => + { + Assert.Equal(1003, log.EventId); + Assert.Equal("Endpoint '/{p:int}' with route pattern '/{p:int}' was rejected by constraint 'p':'Microsoft.AspNetCore.Routing.Constraints.IntRouteConstraint' with value 'One' for the request path '/One'", log.Message); + }, + (log) => + { + Assert.Equal(1004, log.EventId); + Assert.Equal("Endpoint '/{p:int}' with route pattern '/{p:int}' is not valid for the request path '/One'", log.Message); + }); + } + + [Fact] + public async Task MatchAsync_ComplexSegmentRejectsEndpoint_Logging() + { + // Arrange + var endpointDataSource = new DefaultEndpointDataSource(new List { CreateEndpoint("/x-{id}-y", 0) }); - var sink = new TestSink(); - var matcher = CreateDfaMatcher(endpointDataSource, loggerFactory: new TestLoggerFactory(sink, enabled: true)); - - var httpContext = CreateContext(); - httpContext.Request.Path = "/One"; - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - Assert.Null(httpContext.GetEndpoint()); - - Assert.Collection( - sink.Writes, - (log) => - { - Assert.Equal(1001, log.EventId); - Assert.Equal("1 candidate(s) found for the request path '/One'", log.Message); - }, - (log) => - { - Assert.Equal(1002, log.EventId); - Assert.Equal("Endpoint '/x-{id}-y' with route pattern '/x-{id}-y' was rejected by complex segment 'x-{id}-y' for the request path '/One'", log.Message); - }, - (log) => - { - Assert.Equal(1004, log.EventId); - Assert.Equal("Endpoint '/x-{id}-y' with route pattern '/x-{id}-y' is not valid for the request path '/One'", log.Message); - }); - } + var sink = new TestSink(); + var matcher = CreateDfaMatcher(endpointDataSource, loggerFactory: new TestLoggerFactory(sink, enabled: true)); - [Fact] - public async Task MatchAsync_MultipleCandidates_Logging() - { - // Arrange - var endpointDataSource = new DefaultEndpointDataSource(new List + var httpContext = CreateContext(); + httpContext.Request.Path = "/One"; + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + Assert.Null(httpContext.GetEndpoint()); + + Assert.Collection( + sink.Writes, + (log) => + { + Assert.Equal(1001, log.EventId); + Assert.Equal("1 candidate(s) found for the request path '/One'", log.Message); + }, + (log) => + { + Assert.Equal(1002, log.EventId); + Assert.Equal("Endpoint '/x-{id}-y' with route pattern '/x-{id}-y' was rejected by complex segment 'x-{id}-y' for the request path '/One'", log.Message); + }, + (log) => + { + Assert.Equal(1004, log.EventId); + Assert.Equal("Endpoint '/x-{id}-y' with route pattern '/x-{id}-y' is not valid for the request path '/One'", log.Message); + }); + } + + [Fact] + public async Task MatchAsync_MultipleCandidates_Logging() + { + // Arrange + var endpointDataSource = new DefaultEndpointDataSource(new List { CreateEndpoint("/{one}", 0), CreateEndpoint("/{p:int}", 1), CreateEndpoint("/x-{id}-y", 2), }); - var sink = new TestSink(); - var matcher = CreateDfaMatcher(endpointDataSource, loggerFactory: new TestLoggerFactory(sink, enabled: true)); - - var httpContext = CreateContext(); - httpContext.Request.Path = "/One"; - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - Assert.Same(endpointDataSource.Endpoints[0], httpContext.GetEndpoint()); - - Assert.Collection( - sink.Writes, - (log) => - { - Assert.Equal(1001, log.EventId); - Assert.Equal("3 candidate(s) found for the request path '/One'", log.Message); - }, - (log) => - { - Assert.Equal(1005, log.EventId); - Assert.Equal("Endpoint '/{one}' with route pattern '/{one}' is valid for the request path '/One'", log.Message); - }, - (log) => - { - Assert.Equal(1003, log.EventId); - Assert.Equal("Endpoint '/{p:int}' with route pattern '/{p:int}' was rejected by constraint 'p':'Microsoft.AspNetCore.Routing.Constraints.IntRouteConstraint' with value 'One' for the request path '/One'", log.Message); - }, - (log) => - { - Assert.Equal(1004, log.EventId); - Assert.Equal("Endpoint '/{p:int}' with route pattern '/{p:int}' is not valid for the request path '/One'", log.Message); - }, - (log) => - { - Assert.Equal(1002, log.EventId); - Assert.Equal("Endpoint '/x-{id}-y' with route pattern '/x-{id}-y' was rejected by complex segment 'x-{id}-y' for the request path '/One'", log.Message); - }, - (log) => - { - Assert.Equal(1004, log.EventId); - Assert.Equal("Endpoint '/x-{id}-y' with route pattern '/x-{id}-y' is not valid for the request path '/One'", log.Message); - }); - } + var sink = new TestSink(); + var matcher = CreateDfaMatcher(endpointDataSource, loggerFactory: new TestLoggerFactory(sink, enabled: true)); - [Fact] - public async Task MatchAsync_RunsApplicableEndpointSelectorPolicies() - { - // Arrange - var dataSource = new DefaultEndpointDataSource(new List + var httpContext = CreateContext(); + httpContext.Request.Path = "/One"; + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + Assert.Same(endpointDataSource.Endpoints[0], httpContext.GetEndpoint()); + + Assert.Collection( + sink.Writes, + (log) => + { + Assert.Equal(1001, log.EventId); + Assert.Equal("3 candidate(s) found for the request path '/One'", log.Message); + }, + (log) => + { + Assert.Equal(1005, log.EventId); + Assert.Equal("Endpoint '/{one}' with route pattern '/{one}' is valid for the request path '/One'", log.Message); + }, + (log) => + { + Assert.Equal(1003, log.EventId); + Assert.Equal("Endpoint '/{p:int}' with route pattern '/{p:int}' was rejected by constraint 'p':'Microsoft.AspNetCore.Routing.Constraints.IntRouteConstraint' with value 'One' for the request path '/One'", log.Message); + }, + (log) => + { + Assert.Equal(1004, log.EventId); + Assert.Equal("Endpoint '/{p:int}' with route pattern '/{p:int}' is not valid for the request path '/One'", log.Message); + }, + (log) => + { + Assert.Equal(1002, log.EventId); + Assert.Equal("Endpoint '/x-{id}-y' with route pattern '/x-{id}-y' was rejected by complex segment 'x-{id}-y' for the request path '/One'", log.Message); + }, + (log) => + { + Assert.Equal(1004, log.EventId); + Assert.Equal("Endpoint '/x-{id}-y' with route pattern '/x-{id}-y' is not valid for the request path '/One'", log.Message); + }); + } + + [Fact] + public async Task MatchAsync_RunsApplicableEndpointSelectorPolicies() + { + // Arrange + var dataSource = new DefaultEndpointDataSource(new List { CreateEndpoint("/test/{id:alpha}", 0), CreateEndpoint("/test/{id:int}", 0), CreateEndpoint("/test/{id}", 0), }); - var policy = new Mock(); - policy - .As() - .Setup(p => p.AppliesToEndpoints(It.IsAny>())).Returns(true); - policy - .As() - .Setup(p => p.ApplyAsync(It.IsAny(), It.IsAny())) - .Returns((c, cs) => - { - cs.SetValidity(1, false); - return Task.CompletedTask; - }); + var policy = new Mock(); + policy + .As() + .Setup(p => p.AppliesToEndpoints(It.IsAny>())).Returns(true); + policy + .As() + .Setup(p => p.ApplyAsync(It.IsAny(), It.IsAny())) + .Returns((c, cs) => + { + cs.SetValidity(1, false); + return Task.CompletedTask; + }); - var matcher = CreateDfaMatcher(dataSource, policies: new[] { policy.Object, }); + var matcher = CreateDfaMatcher(dataSource, policies: new[] { policy.Object, }); - var httpContext = CreateContext(); - httpContext.Request.Path = "/test/17"; + var httpContext = CreateContext(); + httpContext.Request.Path = "/test/17"; - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - Assert.Same(dataSource.Endpoints[2], httpContext.GetEndpoint()); - } + // Assert + Assert.Same(dataSource.Endpoints[2], httpContext.GetEndpoint()); + } - [Fact] - public async Task MatchAsync_SkipsNonApplicableEndpointSelectorPolicies() - { - // Arrange - var dataSource = new DefaultEndpointDataSource(new List + [Fact] + public async Task MatchAsync_SkipsNonApplicableEndpointSelectorPolicies() + { + // Arrange + var dataSource = new DefaultEndpointDataSource(new List { CreateEndpoint("/test/{id:alpha}", 0), CreateEndpoint("/test/{id:int}", 0), CreateEndpoint("/test/{id}", 0), }); - var policy = new Mock(); - policy - .As() - .Setup(p => p.AppliesToEndpoints(It.IsAny>())).Returns(false); - policy - .As() - .Setup(p => p.ApplyAsync(It.IsAny(), It.IsAny())) - .Returns((c, cs) => - { - throw null; // Won't be called. + var policy = new Mock(); + policy + .As() + .Setup(p => p.AppliesToEndpoints(It.IsAny>())).Returns(false); + policy + .As() + .Setup(p => p.ApplyAsync(It.IsAny(), It.IsAny())) + .Returns((c, cs) => + { + throw null; // Won't be called. }); - var matcher = CreateDfaMatcher(dataSource, policies: new[] { policy.Object, }); + var matcher = CreateDfaMatcher(dataSource, policies: new[] { policy.Object, }); - var httpContext = CreateContext(); - httpContext.Request.Path = "/test/17"; + var httpContext = CreateContext(); + httpContext.Request.Path = "/test/17"; - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - Assert.Same(dataSource.Endpoints[1], httpContext.GetEndpoint()); - } + // Assert + Assert.Same(dataSource.Endpoints[1], httpContext.GetEndpoint()); + } - [Fact] - public async Task MatchAsync_RunsEndpointSelectorPolicies_CanShortCircuit() - { - // Arrange - var dataSource = new DefaultEndpointDataSource(new List + [Fact] + public async Task MatchAsync_RunsEndpointSelectorPolicies_CanShortCircuit() + { + // Arrange + var dataSource = new DefaultEndpointDataSource(new List { CreateEndpoint("/test/{id:alpha}", 0), CreateEndpoint("/test/{id:int}", 0), CreateEndpoint("/test/{id}", 0), }); - var policy1 = new Mock(); - policy1 - .As() - .Setup(p => p.AppliesToEndpoints(It.IsAny>())).Returns(true); - policy1 - .As() - .Setup(p => p.ApplyAsync(It.IsAny(), It.IsAny())) - .Returns((c, cs) => - { - c.SetEndpoint(cs[0].Endpoint); - return Task.CompletedTask; - }); + var policy1 = new Mock(); + policy1 + .As() + .Setup(p => p.AppliesToEndpoints(It.IsAny>())).Returns(true); + policy1 + .As() + .Setup(p => p.ApplyAsync(It.IsAny(), It.IsAny())) + .Returns((c, cs) => + { + c.SetEndpoint(cs[0].Endpoint); + return Task.CompletedTask; + }); - // This should never run, it's after policy1 which short circuits - var policy2 = new Mock(); - policy2 - .SetupGet(p => p.Order) - .Returns(1000); - policy2 - .As() - .Setup(p => p.AppliesToEndpoints(It.IsAny>())).Returns(true); - policy2 - .As() - .Setup(p => p.ApplyAsync(It.IsAny(), It.IsAny())) - .Throws(new InvalidOperationException()); - - var matcher = CreateDfaMatcher(dataSource, policies: new[] { policy1.Object, policy2.Object, }); - - var httpContext = CreateContext(); - httpContext.Request.Path = "/test/17"; - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - Assert.Same(dataSource.Endpoints[0], httpContext.GetEndpoint()); - } + // This should never run, it's after policy1 which short circuits + var policy2 = new Mock(); + policy2 + .SetupGet(p => p.Order) + .Returns(1000); + policy2 + .As() + .Setup(p => p.AppliesToEndpoints(It.IsAny>())).Returns(true); + policy2 + .As() + .Setup(p => p.ApplyAsync(It.IsAny(), It.IsAny())) + .Throws(new InvalidOperationException()); + + var matcher = CreateDfaMatcher(dataSource, policies: new[] { policy1.Object, policy2.Object, }); + + var httpContext = CreateContext(); + httpContext.Request.Path = "/test/17"; + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + Assert.Same(dataSource.Endpoints[0], httpContext.GetEndpoint()); + } - private HttpContext CreateContext() - { - return new DefaultHttpContext(); - } + private HttpContext CreateContext() + { + return new DefaultHttpContext(); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/DictionaryJumpTableTest.cs b/src/Http/Routing/test/UnitTests/Matching/DictionaryJumpTableTest.cs index e057dedb32..416333ef5a 100644 --- a/src/Http/Routing/test/UnitTests/Matching/DictionaryJumpTableTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/DictionaryJumpTableTest.cs @@ -1,16 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class DictionaryJumpTableTest : MultipleEntryJumpTableTest { - public class DictionaryJumpTableTest : MultipleEntryJumpTableTest + internal override JumpTable CreateTable( + int defaultDestination, + int exitDestination, + params (string text, int destination)[] entries) { - internal override JumpTable CreateTable( - int defaultDestination, - int exitDestination, - params (string text, int destination)[] entries) - { - return new DictionaryJumpTable(defaultDestination, exitDestination, entries); - } + return new DictionaryJumpTable(defaultDestination, exitDestination, entries); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/EndpointComparerTest.cs b/src/Http/Routing/test/UnitTests/Matching/EndpointComparerTest.cs index a1364bebc5..24d504b2b9 100644 --- a/src/Http/Routing/test/UnitTests/Matching/EndpointComparerTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/EndpointComparerTest.cs @@ -6,272 +6,271 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Patterns; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class RouteEndpointComparerTest { - public class RouteEndpointComparerTest + [Fact] + public void Compare_PrefersOrder_IfDifferent() + { + // Arrange + var endpoint1 = CreateEndpoint("/", order: 1); + var endpoint2 = CreateEndpoint("/api/foo", order: -1); + + var comparer = CreateComparer(); + + // Act + var result = comparer.Compare(endpoint1, endpoint2); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void Compare_PrefersPrecedence_IfOrderIsSame() + { + // Arrange + var endpoint1 = CreateEndpoint("/api/foo", order: 1); + var endpoint2 = CreateEndpoint("/", order: 1); + + var comparer = CreateComparer(); + + // Act + var result = comparer.Compare(endpoint1, endpoint2); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void Compare_PrefersPolicy_IfPrecedenceIsSame() { - [Fact] - public void Compare_PrefersOrder_IfDifferent() - { - // Arrange - var endpoint1 = CreateEndpoint("/", order: 1); - var endpoint2 = CreateEndpoint("/api/foo", order: -1); + // Arrange + var endpoint1 = CreateEndpoint("/", order: 1, new TestMetadata1()); + var endpoint2 = CreateEndpoint("/", order: 1); - var comparer = CreateComparer(); + var comparer = CreateComparer(new TestMetadata1Policy()); - // Act - var result = comparer.Compare(endpoint1, endpoint2); + // Act + var result = comparer.Compare(endpoint1, endpoint2); + + // Assert + Assert.Equal(-1, result); + } - // Assert - Assert.Equal(1, result); - } - - [Fact] - public void Compare_PrefersPrecedence_IfOrderIsSame() - { - // Arrange - var endpoint1 = CreateEndpoint("/api/foo", order: 1); - var endpoint2 = CreateEndpoint("/", order: 1); + [Fact] + public void Compare_PrefersSecondPolicy_IfFirstPolicyIsSame() + { + // Arrange + var endpoint1 = CreateEndpoint("/", order: 1, new TestMetadata1()); + var endpoint2 = CreateEndpoint("/", order: 1, new TestMetadata1(), new TestMetadata2()); - var comparer = CreateComparer(); + var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); - // Act - var result = comparer.Compare(endpoint1, endpoint2); + // Act + var result = comparer.Compare(endpoint1, endpoint2); - // Assert - Assert.Equal(1, result); - } - - [Fact] - public void Compare_PrefersPolicy_IfPrecedenceIsSame() - { - // Arrange - var endpoint1 = CreateEndpoint("/", order: 1, new TestMetadata1()); - var endpoint2 = CreateEndpoint("/", order: 1); + // Assert + Assert.Equal(1, result); + } - var comparer = CreateComparer(new TestMetadata1Policy()); + [Fact] + public void Compare_PrefersTemplate_IfOtherCriteriaIsSame() + { + // Arrange + var endpoint1 = CreateEndpoint("/foo", order: 1, new TestMetadata1()); + var endpoint2 = CreateEndpoint("/bar", order: 1, new TestMetadata1()); - // Act - var result = comparer.Compare(endpoint1, endpoint2); + var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); - // Assert - Assert.Equal(-1, result); - } - - [Fact] - public void Compare_PrefersSecondPolicy_IfFirstPolicyIsSame() - { - // Arrange - var endpoint1 = CreateEndpoint("/", order: 1, new TestMetadata1()); - var endpoint2 = CreateEndpoint("/", order: 1, new TestMetadata1(), new TestMetadata2()); + // Act + var result = comparer.Compare(endpoint1, endpoint2); - var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); + // Assert + Assert.True(result > 0); + } - // Act - var result = comparer.Compare(endpoint1, endpoint2); + [Fact] + public void Compare_ReturnsZero_WhenIdentical() + { + // Arrange + var endpoint1 = CreateEndpoint("/foo", order: 1, new TestMetadata1()); + var endpoint2 = CreateEndpoint("/foo", order: 1, new TestMetadata1()); - // Assert - Assert.Equal(1, result); - } - - [Fact] - public void Compare_PrefersTemplate_IfOtherCriteriaIsSame() - { - // Arrange - var endpoint1 = CreateEndpoint("/foo", order: 1, new TestMetadata1()); - var endpoint2 = CreateEndpoint("/bar", order: 1, new TestMetadata1()); + var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); - var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); + // Act + var result = comparer.Compare(endpoint1, endpoint2); - // Act - var result = comparer.Compare(endpoint1, endpoint2); + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void Equals_NotEqual_IfOrderDifferent() + { + // Arrange + var endpoint1 = CreateEndpoint("/", order: 1); + var endpoint2 = CreateEndpoint("/api/foo", order: -1); + + var comparer = CreateComparer(); + + // Act + var result = comparer.Equals(endpoint1, endpoint2); + + // Assert + Assert.False(result); + } + + [Fact] + public void Equals_NotEqual_IfPrecedenceDifferent() + { + // Arrange + var endpoint1 = CreateEndpoint("/api/foo", order: 1); + var endpoint2 = CreateEndpoint("/", order: 1); + + var comparer = CreateComparer(); + + // Act + var result = comparer.Equals(endpoint1, endpoint2); + + // Assert + Assert.False(result); + } + + [Fact] + public void Equals_NotEqual_IfFirstPolicyDifferent() + { + // Arrange + var endpoint1 = CreateEndpoint("/", order: 1, new TestMetadata1()); + var endpoint2 = CreateEndpoint("/", order: 1); + + var comparer = CreateComparer(new TestMetadata1Policy()); + + // Act + var result = comparer.Equals(endpoint1, endpoint2); + + // Assert + Assert.False(result); + } - // Assert - Assert.True(result > 0); - } - - [Fact] - public void Compare_ReturnsZero_WhenIdentical() - { - // Arrange - var endpoint1 = CreateEndpoint("/foo", order: 1, new TestMetadata1()); - var endpoint2 = CreateEndpoint("/foo", order: 1, new TestMetadata1()); - - var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); - - // Act - var result = comparer.Compare(endpoint1, endpoint2); - - // Assert - Assert.Equal(0, result); - } - - [Fact] - public void Equals_NotEqual_IfOrderDifferent() - { - // Arrange - var endpoint1 = CreateEndpoint("/", order: 1); - var endpoint2 = CreateEndpoint("/api/foo", order: -1); - - var comparer = CreateComparer(); - - // Act - var result = comparer.Equals(endpoint1, endpoint2); - - // Assert - Assert.False(result); - } - - [Fact] - public void Equals_NotEqual_IfPrecedenceDifferent() - { - // Arrange - var endpoint1 = CreateEndpoint("/api/foo", order: 1); - var endpoint2 = CreateEndpoint("/", order: 1); - - var comparer = CreateComparer(); - - // Act - var result = comparer.Equals(endpoint1, endpoint2); - - // Assert - Assert.False(result); - } - - [Fact] - public void Equals_NotEqual_IfFirstPolicyDifferent() - { - // Arrange - var endpoint1 = CreateEndpoint("/", order: 1, new TestMetadata1()); - var endpoint2 = CreateEndpoint("/", order: 1); - - var comparer = CreateComparer(new TestMetadata1Policy()); - - // Act - var result = comparer.Equals(endpoint1, endpoint2); - - // Assert - Assert.False(result); - } - - [Fact] - public void Equals_NotEqual_IfSecondPolicyDifferent() - { - // Arrange - var endpoint1 = CreateEndpoint("/", order: 1, new TestMetadata1()); - var endpoint2 = CreateEndpoint("/", order: 1, new TestMetadata1(), new TestMetadata2()); - - var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); - - // Act - var result = comparer.Equals(endpoint1, endpoint2); - - // Assert - Assert.False(result); - } - - [Fact] - public void Equals_Equals_WhenTemplateIsDifferent() - { - // Arrange - var endpoint1 = CreateEndpoint("/foo", order: 1, new TestMetadata1()); - var endpoint2 = CreateEndpoint("/bar", order: 1, new TestMetadata1()); - - var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); - - // Act - var result = comparer.Equals(endpoint1, endpoint2); - - // Assert - Assert.True(result); - } - - [Fact] - public void Sort_MoreSpecific_FirstInList() - { - // Arrange - var endpoint1 = CreateEndpoint("/foo", order: -1); - var endpoint2 = CreateEndpoint("/bar/{baz}", order: -1); - var endpoint3 = CreateEndpoint("/bar", order: 0, new TestMetadata1()); - var endpoint4 = CreateEndpoint("/foo", order: 0, new TestMetadata2()); - var endpoint5 = CreateEndpoint("/foo", order: 0); - var endpoint6 = CreateEndpoint("/a{baz}", order: 0, new TestMetadata1(), new TestMetadata2()); - var endpoint7 = CreateEndpoint("/bar{baz}", order: 0, new TestMetadata1(), new TestMetadata2()); - - // Endpoints listed in reverse of the desired order. - var list = new List() { endpoint7, endpoint6, endpoint5, endpoint4, endpoint3, endpoint2, endpoint1, }; - - var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); - - // Act - list.Sort(comparer); - - // Assert - Assert.Collection( - list, - e => Assert.Same(endpoint1, e), - e => Assert.Same(endpoint2, e), - e => Assert.Same(endpoint3, e), - e => Assert.Same(endpoint4, e), - e => Assert.Same(endpoint5, e), - e => Assert.Same(endpoint6, e), - e => Assert.Same(endpoint7, e)); - } - - [Fact] - public void Compare_PatternOrder_OrdinalIgnoreCaseSort() - { - // Arrange - var endpoint1 = CreateEndpoint("/I", order: 0); - var endpoint2 = CreateEndpoint("/i", order: 0); - var endpoint3 = CreateEndpoint("/\u0131", order: 0); // Turkish lowercase i - - var list = new List() { endpoint1, endpoint2, endpoint3 }; - - var comparer = CreateComparer(); - - // Act - list.Sort(comparer); - - // Assert - Assert.Collection( - list, - e => Assert.Same(endpoint1, e), - e => Assert.Same(endpoint2, e), - e => Assert.Same(endpoint3, e)); - } - - private static RouteEndpoint CreateEndpoint(string template, int order, params object[] metadata) - { - return new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse(template), - order, - new EndpointMetadataCollection(metadata), - "test: " + template); - } - - private static EndpointComparer CreateComparer(params IEndpointComparerPolicy[] policies) - { - return new EndpointComparer(policies); - } - - private class TestMetadata1 - { - } - - private class TestMetadata1Policy : IEndpointComparerPolicy - { - public IComparer Comparer => EndpointMetadataComparer.Default; - } - - private class TestMetadata2 - { - } - - private class TestMetadata2Policy : IEndpointComparerPolicy - { - public IComparer Comparer => EndpointMetadataComparer.Default; - } + [Fact] + public void Equals_NotEqual_IfSecondPolicyDifferent() + { + // Arrange + var endpoint1 = CreateEndpoint("/", order: 1, new TestMetadata1()); + var endpoint2 = CreateEndpoint("/", order: 1, new TestMetadata1(), new TestMetadata2()); + + var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); + + // Act + var result = comparer.Equals(endpoint1, endpoint2); + + // Assert + Assert.False(result); + } + + [Fact] + public void Equals_Equals_WhenTemplateIsDifferent() + { + // Arrange + var endpoint1 = CreateEndpoint("/foo", order: 1, new TestMetadata1()); + var endpoint2 = CreateEndpoint("/bar", order: 1, new TestMetadata1()); + + var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); + + // Act + var result = comparer.Equals(endpoint1, endpoint2); + + // Assert + Assert.True(result); + } + + [Fact] + public void Sort_MoreSpecific_FirstInList() + { + // Arrange + var endpoint1 = CreateEndpoint("/foo", order: -1); + var endpoint2 = CreateEndpoint("/bar/{baz}", order: -1); + var endpoint3 = CreateEndpoint("/bar", order: 0, new TestMetadata1()); + var endpoint4 = CreateEndpoint("/foo", order: 0, new TestMetadata2()); + var endpoint5 = CreateEndpoint("/foo", order: 0); + var endpoint6 = CreateEndpoint("/a{baz}", order: 0, new TestMetadata1(), new TestMetadata2()); + var endpoint7 = CreateEndpoint("/bar{baz}", order: 0, new TestMetadata1(), new TestMetadata2()); + + // Endpoints listed in reverse of the desired order. + var list = new List() { endpoint7, endpoint6, endpoint5, endpoint4, endpoint3, endpoint2, endpoint1, }; + + var comparer = CreateComparer(new TestMetadata1Policy(), new TestMetadata2Policy()); + + // Act + list.Sort(comparer); + + // Assert + Assert.Collection( + list, + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e), + e => Assert.Same(endpoint3, e), + e => Assert.Same(endpoint4, e), + e => Assert.Same(endpoint5, e), + e => Assert.Same(endpoint6, e), + e => Assert.Same(endpoint7, e)); + } + + [Fact] + public void Compare_PatternOrder_OrdinalIgnoreCaseSort() + { + // Arrange + var endpoint1 = CreateEndpoint("/I", order: 0); + var endpoint2 = CreateEndpoint("/i", order: 0); + var endpoint3 = CreateEndpoint("/\u0131", order: 0); // Turkish lowercase i + + var list = new List() { endpoint1, endpoint2, endpoint3 }; + + var comparer = CreateComparer(); + + // Act + list.Sort(comparer); + + // Assert + Assert.Collection( + list, + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e), + e => Assert.Same(endpoint3, e)); + } + + private static RouteEndpoint CreateEndpoint(string template, int order, params object[] metadata) + { + return new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse(template), + order, + new EndpointMetadataCollection(metadata), + "test: " + template); + } + + private static EndpointComparer CreateComparer(params IEndpointComparerPolicy[] policies) + { + return new EndpointComparer(policies); + } + + private class TestMetadata1 + { + } + + private class TestMetadata1Policy : IEndpointComparerPolicy + { + public IComparer Comparer => EndpointMetadataComparer.Default; + } + + private class TestMetadata2 + { + } + + private class TestMetadata2Policy : IEndpointComparerPolicy + { + public IComparer Comparer => EndpointMetadataComparer.Default; } } diff --git a/src/Http/Routing/test/UnitTests/Matching/EndpointMetadataComparerTest.cs b/src/Http/Routing/test/UnitTests/Matching/EndpointMetadataComparerTest.cs index 25ba6e35ef..b4ed3ec2e8 100644 --- a/src/Http/Routing/test/UnitTests/Matching/EndpointMetadataComparerTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/EndpointMetadataComparerTest.cs @@ -5,87 +5,86 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class EndpointMetadataComparerTest { - public class EndpointMetadataComparerTest + [Fact] + public void Compare_EndpointWithMetadata_MoreSpecific() + { + // Arrange + var endpoint1 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test1"); + var endpoint2 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { }), "test2"); + + // Act + var result = EndpointMetadataComparer.Default.Compare(endpoint1, endpoint2); + + // Assert + Assert.Equal(-1, result); + } + + [Fact] + public void Compare_EndpointWithMetadata_ReverseOrder_MoreSpecific() + { + // Arrange + var endpoint1 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { }), "test1"); + var endpoint2 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test2"); + + // Act + var result = EndpointMetadataComparer.Default.Compare(endpoint1, endpoint2); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void Compare_BothEndpointsWithMetadata_Equal() + { + // Arrange + var endpoint1 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test1"); + var endpoint2 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test2"); + + // Act + var result = EndpointMetadataComparer.Default.Compare(endpoint1, endpoint2); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void Compare_BothEndpointsWithoutMetadata_Equal() + { + // Arrange + var endpoint1 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { }), "test1"); + var endpoint2 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { }), "test2"); + + // Act + var result = EndpointMetadataComparer.Default.Compare(endpoint1, endpoint2); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void Sort_EndpointWithMetadata_FirstInList() + { + // Arrange + var endpoint1 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test1"); + var endpoint2 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { }), "test2"); + + var list = new List() { endpoint2, endpoint1, }; + + // Act + list.Sort(EndpointMetadataComparer.Default); + + // Assert + Assert.Collection( + list, + e => Assert.Same(endpoint1, e), + e => Assert.Same(endpoint2, e)); + } + + private class TestMetadata { - [Fact] - public void Compare_EndpointWithMetadata_MoreSpecific() - { - // Arrange - var endpoint1 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test1"); - var endpoint2 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { }), "test2"); - - // Act - var result = EndpointMetadataComparer.Default.Compare(endpoint1, endpoint2); - - // Assert - Assert.Equal(-1, result); - } - - [Fact] - public void Compare_EndpointWithMetadata_ReverseOrder_MoreSpecific() - { - // Arrange - var endpoint1 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { }), "test1"); - var endpoint2 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test2"); - - // Act - var result = EndpointMetadataComparer.Default.Compare(endpoint1, endpoint2); - - // Assert - Assert.Equal(1, result); - } - - [Fact] - public void Compare_BothEndpointsWithMetadata_Equal() - { - // Arrange - var endpoint1 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test1"); - var endpoint2 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test2"); - - // Act - var result = EndpointMetadataComparer.Default.Compare(endpoint1, endpoint2); - - // Assert - Assert.Equal(0, result); - } - - [Fact] - public void Compare_BothEndpointsWithoutMetadata_Equal() - { - // Arrange - var endpoint1 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { }), "test1"); - var endpoint2 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { }), "test2"); - - // Act - var result = EndpointMetadataComparer.Default.Compare(endpoint1, endpoint2); - - // Assert - Assert.Equal(0, result); - } - - [Fact] - public void Sort_EndpointWithMetadata_FirstInList() - { - // Arrange - var endpoint1 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { new TestMetadata(), }), "test1"); - var endpoint2 = new Endpoint(TestConstants.EmptyRequestDelegate, new EndpointMetadataCollection(new object[] { }), "test2"); - - var list = new List() { endpoint2, endpoint1, }; - - // Act - list.Sort(EndpointMetadataComparer.Default); - - // Assert - Assert.Collection( - list, - e => Assert.Same(endpoint1, e), - e => Assert.Same(endpoint2, e)); - } - - private class TestMetadata - { - } } } diff --git a/src/Http/Routing/test/UnitTests/Matching/FastPathTokenizerTest.cs b/src/Http/Routing/test/UnitTests/Matching/FastPathTokenizerTest.cs index 7276f744b7..1086ee5018 100644 --- a/src/Http/Routing/test/UnitTests/Matching/FastPathTokenizerTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/FastPathTokenizerTest.cs @@ -4,130 +4,129 @@ using System; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class FastPathTokenizerTest { - public class FastPathTokenizerTest + // Generally this will only happen in tests when the HttpContext hasn't been + // initialized. We still don't want to crash in this case. + [Fact] + public void Tokenize_EmptyString() + { + // Arrange + Span segments = stackalloc PathSegment[1]; + + // Act + var count = FastPathTokenizer.Tokenize("", segments); + + // Assert + Assert.Equal(0, count); + } + + [Fact] + public void Tokenize_RootPath() + { + // Arrange + Span segments = stackalloc PathSegment[1]; + + // Act + var count = FastPathTokenizer.Tokenize("/", segments); + + // Assert + Assert.Equal(0, count); + } + + [Fact] + public void Tokenize_SingleSegment() + { + // Arrange + Span segments = stackalloc PathSegment[1]; + + // Act + var count = FastPathTokenizer.Tokenize("/abc", segments); + + // Assert + Assert.Equal(1, count); + Assert.Equal(new PathSegment(1, 3), segments[0]); + } + + [Fact] + public void Tokenize_WithSomeSegments() + { + // Arrange + Span segments = stackalloc PathSegment[3]; + + // Act + var count = FastPathTokenizer.Tokenize("/a/b/c", segments); + + // Assert + Assert.Equal(3, count); + Assert.Equal(new PathSegment(1, 1), segments[0]); + Assert.Equal(new PathSegment(3, 1), segments[1]); + Assert.Equal(new PathSegment(5, 1), segments[2]); + } + + [Fact] // Empty trailing / is ignored + public void Tokenize_WithSomeSegments_TrailingSlash() + { + // Arrange + Span segments = stackalloc PathSegment[3]; + + // Act + var count = FastPathTokenizer.Tokenize("/a/b/c/", segments); + + // Assert + Assert.Equal(3, count); + Assert.Equal(new PathSegment(1, 1), segments[0]); + Assert.Equal(new PathSegment(3, 1), segments[1]); + Assert.Equal(new PathSegment(5, 1), segments[2]); + } + + [Fact] + public void Tokenize_LongerSegments() { - // Generally this will only happen in tests when the HttpContext hasn't been - // initialized. We still don't want to crash in this case. - [Fact] - public void Tokenize_EmptyString() - { - // Arrange - Span segments = stackalloc PathSegment[1]; - - // Act - var count = FastPathTokenizer.Tokenize("", segments); - - // Assert - Assert.Equal(0, count); - } - - [Fact] - public void Tokenize_RootPath() - { - // Arrange - Span segments = stackalloc PathSegment[1]; - - // Act - var count = FastPathTokenizer.Tokenize("/", segments); - - // Assert - Assert.Equal(0, count); - } - - [Fact] - public void Tokenize_SingleSegment() - { - // Arrange - Span segments = stackalloc PathSegment[1]; - - // Act - var count = FastPathTokenizer.Tokenize("/abc", segments); - - // Assert - Assert.Equal(1, count); - Assert.Equal(new PathSegment(1, 3), segments[0]); - } - - [Fact] - public void Tokenize_WithSomeSegments() - { - // Arrange - Span segments = stackalloc PathSegment[3]; - - // Act - var count = FastPathTokenizer.Tokenize("/a/b/c", segments); - - // Assert - Assert.Equal(3, count); - Assert.Equal(new PathSegment(1, 1), segments[0]); - Assert.Equal(new PathSegment(3, 1), segments[1]); - Assert.Equal(new PathSegment(5, 1), segments[2]); - } - - [Fact] // Empty trailing / is ignored - public void Tokenize_WithSomeSegments_TrailingSlash() - { - // Arrange - Span segments = stackalloc PathSegment[3]; - - // Act - var count = FastPathTokenizer.Tokenize("/a/b/c/", segments); - - // Assert - Assert.Equal(3, count); - Assert.Equal(new PathSegment(1, 1), segments[0]); - Assert.Equal(new PathSegment(3, 1), segments[1]); - Assert.Equal(new PathSegment(5, 1), segments[2]); - } - - [Fact] - public void Tokenize_LongerSegments() - { - // Arrange - Span segments = stackalloc PathSegment[3]; - - // Act - var count = FastPathTokenizer.Tokenize("/aaa/bb/ccccc", segments); - - // Assert - Assert.Equal(3, count); - Assert.Equal(new PathSegment(1, 3), segments[0]); - Assert.Equal(new PathSegment(5, 2), segments[1]); - Assert.Equal(new PathSegment(8, 5), segments[2]); - } - - [Fact] - public void Tokenize_EmptySegments() - { - // Arrange - Span segments = stackalloc PathSegment[3]; - - // Act - var count = FastPathTokenizer.Tokenize("///c", segments); - - // Assert - Assert.Equal(3, count); - Assert.Equal(new PathSegment(1, 0), segments[0]); - Assert.Equal(new PathSegment(2, 0), segments[1]); - Assert.Equal(new PathSegment(3, 1), segments[2]); - } - - [Fact] - public void Tokenize_TooManySegments() - { - // Arrange - Span segments = stackalloc PathSegment[3]; - - // Act - var count = FastPathTokenizer.Tokenize("/a/b/c/d", segments); - - // Assert - Assert.Equal(3, count); - Assert.Equal(new PathSegment(1, 1), segments[0]); - Assert.Equal(new PathSegment(3, 1), segments[1]); - Assert.Equal(new PathSegment(5, 1), segments[2]); - } + // Arrange + Span segments = stackalloc PathSegment[3]; + + // Act + var count = FastPathTokenizer.Tokenize("/aaa/bb/ccccc", segments); + + // Assert + Assert.Equal(3, count); + Assert.Equal(new PathSegment(1, 3), segments[0]); + Assert.Equal(new PathSegment(5, 2), segments[1]); + Assert.Equal(new PathSegment(8, 5), segments[2]); + } + + [Fact] + public void Tokenize_EmptySegments() + { + // Arrange + Span segments = stackalloc PathSegment[3]; + + // Act + var count = FastPathTokenizer.Tokenize("///c", segments); + + // Assert + Assert.Equal(3, count); + Assert.Equal(new PathSegment(1, 0), segments[0]); + Assert.Equal(new PathSegment(2, 0), segments[1]); + Assert.Equal(new PathSegment(3, 1), segments[2]); + } + + [Fact] + public void Tokenize_TooManySegments() + { + // Arrange + Span segments = stackalloc PathSegment[3]; + + // Act + var count = FastPathTokenizer.Tokenize("/a/b/c/d", segments); + + // Assert + Assert.Equal(3, count); + Assert.Equal(new PathSegment(1, 1), segments[0]); + Assert.Equal(new PathSegment(3, 1), segments[1]); + Assert.Equal(new PathSegment(5, 1), segments[2]); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/FullFeaturedMatcherConformanceTest.cs b/src/Http/Routing/test/UnitTests/Matching/FullFeaturedMatcherConformanceTest.cs index eb5e343532..9123c397ca 100644 --- a/src/Http/Routing/test/UnitTests/Matching/FullFeaturedMatcherConformanceTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/FullFeaturedMatcherConformanceTest.cs @@ -7,412 +7,412 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// This class includes features that we have not yet implemented in the DFA +// and instruction matchers. +// +// As those matchers add features we can move tests from this class into +// MatcherConformanceTest and delete this. +public abstract class FullFeaturedMatcherConformanceTest : MatcherConformanceTest { - // This class includes features that we have not yet implemented in the DFA - // and instruction matchers. - // - // As those matchers add features we can move tests from this class into - // MatcherConformanceTest and delete this. - public abstract class FullFeaturedMatcherConformanceTest : MatcherConformanceTest + [Theory] + [InlineData("/a/{b=15}", "/a/b", new string[] { "b", }, new string[] { "b", })] + [InlineData("/a/{b=15}", "/a/", new string[] { "b", }, new string[] { "15", })] + [InlineData("/a/{b=15}", "/a", new string[] { "b", }, new string[] { "15", })] + [InlineData("/{a}/{b=15}", "/54/b", new string[] { "a", "b", }, new string[] { "54", "b", })] + [InlineData("/{a=19}/{b=15}", "/54/b", new string[] { "a", "b", }, new string[] { "54", "b", })] + [InlineData("/{a=19}/{b=15}", "/54/", new string[] { "a", "b", }, new string[] { "54", "15", })] + [InlineData("/{a=19}/{b=15}", "/54", new string[] { "a", "b", }, new string[] { "54", "15", })] + [InlineData("/{a=19}/{b=15}", "/", new string[] { "a", "b", }, new string[] { "19", "15", })] + public virtual async Task Match_DefaultValues(string template, string path, string[] keys, string[] values) { - [Theory] - [InlineData("/a/{b=15}", "/a/b", new string[] { "b", }, new string[] { "b", })] - [InlineData("/a/{b=15}", "/a/", new string[] { "b", }, new string[] { "15", })] - [InlineData("/a/{b=15}", "/a", new string[] { "b", }, new string[] { "15", })] - [InlineData("/{a}/{b=15}", "/54/b", new string[] { "a", "b", }, new string[] { "54", "b", })] - [InlineData("/{a=19}/{b=15}", "/54/b", new string[] { "a", "b", }, new string[] { "54", "b", })] - [InlineData("/{a=19}/{b=15}", "/54/", new string[] { "a", "b", }, new string[] { "54", "15", })] - [InlineData("/{a=19}/{b=15}", "/54", new string[] { "a", "b", }, new string[] { "54", "15", })] - [InlineData("/{a=19}/{b=15}", "/", new string[] { "a", "b", }, new string[] { "19", "15", })] - public virtual async Task Match_DefaultValues(string template, string path, string[] keys, string[] values) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint, keys, values); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint, keys, values); + } - [Fact] - public virtual async Task Match_NonInlineDefaultValues() - { - // Arrange - var endpoint = CreateEndpoint("/a/{b}/{c}", new { b = "17", c = "18", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/a"); + [Fact] + public virtual async Task Match_NonInlineDefaultValues() + { + // Arrange + var endpoint = CreateEndpoint("/a/{b}/{c}", new { b = "17", c = "18", }); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/a"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint, new { b = "17", c = "18", }); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint, new { b = "17", c = "18", }); + } - [Fact] - public virtual async Task Match_ExtraDefaultValues() - { - // Arrange - var endpoint = CreateEndpoint("/a/{b}/{c}", new { b = "17", c = "18", d = "19" }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/a"); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint, new { b = "17", c = "18", d = "19" }); - } - - [Theory] - [InlineData("/a/{b=15}", "/54/b")] - [InlineData("/a/{b=15}", "/54/")] - [InlineData("/a/{b=15}", "/54")] - [InlineData("/a/{b=15}", "/a//")] - [InlineData("/a/{b=15}", "/54/43/23")] - [InlineData("/{a=19}/{b=15}", "/54/b/c")] - [InlineData("/a/{b=15}/c", "/a/b")] // Intermediate default values don't act like optional segments - [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a")] - [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b")] - [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c")] - [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d")] - public virtual async Task NotMatch_DefaultValues(string template, string path) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertNotMatch(httpContext); - } - - [Theory] - [InlineData("/{a?}/{b?}/{c?}", "/", null, null)] - [InlineData("/{a?}/{b?}/{c?}", "/a", new[] { "a", }, new[] { "a", })] - [InlineData("/{a?}/{b?}/{c?}", "/a/", new[] { "a", }, new[] { "a", })] - [InlineData("/{a?}/{b?}/{c?}", "/a/b", new[] { "a", "b", }, new[] { "a", "b", })] - [InlineData("/{a?}/{b?}/{c?}", "/a/b/", new[] { "a", "b", }, new[] { "a", "b", })] - [InlineData("/{a?}/{b?}/{c?}", "/a/b/c", new[] { "a", "b", "c", }, new[] { "a", "b", "c", })] - [InlineData("/{a?}/{b?}/{c?}", "/a/b/c/", new[] { "a", "b", "c", }, new[] { "a", "b", "c", })] - [InlineData("/{c}/{a?}", "/h/i", new[] { "c", "a", }, new[] { "h", "i", })] - [InlineData("/{c}/{a?}", "/h/", new[] { "c", }, new[] { "h", })] - [InlineData("/{c}/{a?}", "/h", new[] { "c", }, new[] { "h", })] - [InlineData("/{c?}/{a?}", "/", null, null)] - [InlineData("/{c}/{a?}/{id?}", "/h/i/18", new[] { "c", "a", "id", }, new[] { "h", "i", "18", })] - [InlineData("/{c}/{a?}/{id?}", "/h/i", new[] { "c", "a", }, new[] { "h", "i", })] - [InlineData("/{c}/{a?}/{id?}", "/h", new[] { "c", }, new[] { "h", })] - [InlineData("template/{p:int?}", "/template/5", new[] { "p", }, new[] { "5", })] - [InlineData("template/{p:int?}", "/template", null, null)] - [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e", new[] { "b", "d", "f" }, new[] { "b", "d", null, })] - [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e/f", new[] { "b", "d", "f", }, new[] { "b", "d", "f", })] - public virtual async Task Match_OptionalParameter(string template, string path, string[] keys, string[] values) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint, keys, values); - } - - [Theory] - [InlineData("/{a?}/{b?}/{c?}", "///")] - [InlineData("/{a?}/{b?}/{c?}", "/a//")] - [InlineData("/{a?}/{b?}/{c?}", "/a/b//")] - [InlineData("/{a?}/{b?}/{c?}", "//b//")] - [InlineData("/{a?}/{b?}/{c?}", "///c")] - [InlineData("/{a?}/{b?}/{c?}", "///c/")] - [InlineData("/{a?}/{b?}/{c?}", "/a/b/c/d")] - [InlineData("/a/{b?}/{c?}", "/")] - [InlineData("template/{parameter:int?}", "/template/qwer")] - public virtual async Task NotMatch_OptionalParameter(string template, string path) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertNotMatch(httpContext); - } - - [Theory] - [InlineData("/{a}/{*b}", "/a", new[] { "a", "b", }, new[] { "a", null, })] - [InlineData("/{a}/{*b}", "/a/", new[] { "a", "b", }, new[] { "a", null, })] - [InlineData("/{a}/{*b=b}", "/a", new[] { "a", "b", }, new[] { "a", "b", })] - [InlineData("/{a}/{*b=b}", "/a/", new[] { "a", "b", }, new[] { "a", "b", })] - [InlineData("/{a}/{*b=b}", "/a/hello", new[] { "a", "b", }, new[] { "a", "hello", })] - [InlineData("/{a}/{*b=b}", "/a/hello/goodbye", new[] { "a", "b", }, new[] { "a", "hello/goodbye", })] - [InlineData("/{a}/{*b=b}", "/a/b//", new[] { "a", "b", }, new[] { "a", "b//", })] - [InlineData("/{a}/{*b=b}", "/a/b/c/", new[] { "a", "b", }, new[] { "a", "b/c/", })] - [InlineData("/{a=1}/{b=2}/{c=3}/{d=4}", "/a/b/c", new[] { "a", "b", "c", "d", }, new[] { "a", "b", "c", "4", })] - [InlineData("a/{*path:regex(10/20/30)}", "/a/10/20/30", new[] { "path", }, new[] { "10/20/30" })] - public virtual async Task Match_CatchAllParameter(string template, string path, string[] keys, string[] values) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint, keys, values); - } - - // Historically catchall segments don't match an empty segment, but only if it's - // the first one. So `/a/b//` would match, but `/a//` would not. This is pretty - // weird and inconsistent with the intent of using a catch all. The DfaMatcher - // fixes this issue. - [Theory] - [InlineData("/{a}/{*b=b}", "/a///", new[] { "a", "b", }, new[] { "a", "//" })] - [InlineData("/{a}/{*b=b}", "/a//c/", new[] { "a", "b", }, new[] { "a", "/c/" })] - public virtual async Task Quirks_CatchAllParameter(string template, string path, string[] keys, string[] values) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertNotMatch(httpContext); - - // Need to access these to prevent a warning from the xUnit analyzer. - // Some of these tests will match (and process the values) and some will not. - GC.KeepAlive(keys); - GC.KeepAlive(values); - } - - [Theory] - [InlineData("{p}x{s}", "/xxxxxxxxxx", new[] { "p", "s" }, new[] { "xxxxxxxx", "x", })] - [InlineData("{p}xyz{s}", "/xxxxyzxyzxxxxxxyz", new[] { "p", "s" }, new[] { "xxxxyz", "xxxxxxyz", })] - [InlineData("{p}xyz{s}", "/abcxxxxyzxyzxxxxxxyzxx", new[] { "p", "s" }, new[] { "abcxxxxyzxyzxxxxx", "xx", })] - [InlineData("{p}xyz{s}", "/xyzxyzxyzxyzxyz", new[] { "p", "s" }, new[] { "xyzxyzxyz", "xyz", })] - [InlineData("{p}xyz{s}", "/xyzxyzxyzxyzxyz1", new[] { "p", "s" }, new[] { "xyzxyzxyzxyz", "1", })] - [InlineData("{p}xyz{s}", "/xyzxyzxyz", new[] { "p", "s" }, new[] { "xyz", "xyz", })] - [InlineData("{p}aa{s}", "/aaaaa", new[] { "p", "s" }, new[] { "aa", "a", })] - [InlineData("{p}aaa{s}", "/aaaaa", new[] { "p", "s" }, new[] { "a", "a", })] - [InlineData("language/{lang=en}-{region=US}", "/language/xx-yy", new[] { "lang", "region" }, new[] { "xx", "yy", })] - [InlineData("language/{lang}-{region}", "/language/en-US", new[] { "lang", "region" }, new[] { "en", "US", })] - [InlineData("language/{lang}-{region}a", "/language/en-USa", new[] { "lang", "region" }, new[] { "en", "US", })] - [InlineData("language/a{lang}-{region}", "/language/aen-US", new[] { "lang", "region" }, new[] { "en", "US", })] - [InlineData("language/a{lang}-{region}a", "/language/aen-USa", new[] { "lang", "region" }, new[] { "en", "US", })] - [InlineData("language/{lang}-", "/language/en-", new[] { "lang", }, new[] { "en", })] - [InlineData("language/a{lang}", "/language/aen", new[] { "lang", }, new[] { "en", })] - [InlineData("language/a{lang}a", "/language/aena", new[] { "lang", }, new[] { "en", })] - public virtual async Task Match_ComplexSegment(string template, string path, string[] keys, string[] values) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint, keys, values); - } - - [Theory] - [InlineData("language/a{lang}-{region}a", "/language/a-USa")] - [InlineData("language/a{lang}-{region}a", "/language/aen-a")] - [InlineData("language/{lang=en}-{region=US}", "/language")] - [InlineData("language/{lang=en}-{region=US}", "/language/-")] - [InlineData("language/{lang=en}-{region=US}", "/language/xx-")] - [InlineData("language/{lang=en}-{region=US}", "/language/-xx")] - public virtual async Task NotMatch_ComplexSegment(string template, string path) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertNotMatch(httpContext); - } - - [Theory] - [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", new[] { "p1", "p2", }, new[] { "foo", "bar" })] - [InlineData("moo/{p1}.{p2?}", "/moo/foo", new[] { "p1", }, new[] { "foo", })] - [InlineData("moo/{p1}.{p2?}", "/moo/.foo", new[] { "p1", }, new[] { ".foo", })] - [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", new[] { "p1", "p2", }, new[] { "foo.", "bar" })] - [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", new[] { "p1", "p2", }, new[] { "foo.moo", "bar" })] - [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", new[] { "p1", }, new[] { "moo", })] - [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.foo.bar", new[] { "p1", "p2", }, new[] { "foo", "bar" })] - [InlineData("moo/.{p2?}", "/moo/.foo", new[] { "p2", }, new[] { "foo", })] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", new[] { "p1", "p2", "p3" }, new[] { "foo", "moo", "bar" })] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", new[] { "p1", "p2", }, new[] { "foo", "moo" })] - [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", new[] { "p1", "p2", "p3" }, new[] { "foo", "moo", "bar" })] - [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", new[] { "p1", "p2", "p3" }, new[] { "foo", "moo", "bar" })] - [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", new[] { "p1", "p3" }, new[] { "foo", "bar" })] - [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", new[] { "p1", "p3" }, new[] { ".foo", "bar" })] - [InlineData("{p1}/{p2}/{p3?}", "/foo/bar/baz", new[] { "p1", "p2", "p3" }, new[] { "foo", "bar", "baz" })] - public virtual async Task Match_OptionalSeparator(string template, string path, string[] keys, string[] values) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint, keys, values); - } - - [Theory] - [InlineData("moo/{p1}.{p2?}", "/moo/foo.")] - [InlineData("moo/{p1}.{p2?}", "/moo/.")] - [InlineData("moo/{p1}.{p2}", "/foo.")] - [InlineData("moo/{p1}.{p2}", "/foo")] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")] - [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")] - [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")] - [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")] - [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")] - [InlineData("moo/.{p2?}", "/moo/.")] - [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")] - public virtual async Task NotMatch_OptionalSeparator(string template, string path) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertNotMatch(httpContext); - } - - // Most of are copied from old routing tests that date back to the VS 2010 era. Enjoy! - [Theory] - [InlineData("{Controller}.mvc/../{action}", "/Home.mvc/../index", new string[] { "Controller", "action" }, new string[] { "Home", "index" })] - [InlineData("{Controller}.mvc/.../{action}", "/Home.mvc/.../index", new string[] { "Controller", "action" }, new string[] { "Home", "index" })] - [InlineData("{Controller}.mvc/../../../{action}", "/Home.mvc/../../../index", new string[] { "Controller", "action" }, new string[] { "Home", "index" })] - [InlineData("{Controller}.mvc!/{action}", "/Home.mvc!/index", new string[] { "Controller", "action" }, new string[] { "Home", "index" })] - [InlineData("../{Controller}.mvc", "/../Home.mvc", new string[] { "Controller", }, new string[] { "Home", })] - [InlineData(@"\{Controller}.mvc", @"/\Home.mvc", new string[] { "Controller", }, new string[] { "Home", })] - [InlineData(@"{Controller}.mvc\{id}\{Param1}", @"/Home.mvc\123\p1", new string[] { "Controller", "id", "Param1" }, new string[] { "Home", "123", "p1" })] - [InlineData("(Controller).mvc", "/(Controller).mvc", new string[] { }, new string[] { })] - [InlineData("Controller.mvc/ ", "/Controller.mvc/ ", new string[] { }, new string[] { })] - [InlineData("Controller.mvc ", "/Controller.mvc ", new string[] { }, new string[] { })] - public virtual async Task Match_WeirdCharacterCases(string template, string path, string[] keys, string[] values) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint, keys, values); - } - - [Theory] - [InlineData("template/5", "template/{parameter:int}")] - [InlineData("template/5", "template/{parameter}")] - [InlineData("template/5", "template/{*parameter:int}")] - [InlineData("template/5", "template/{*parameter}")] - [InlineData("template/{parameter}", "template/{parameter:alpha}")] // constraint doesn't match - [InlineData("template/{parameter:int}", "template/{parameter}")] - [InlineData("template/{parameter:int}", "template/{*parameter:int}")] - [InlineData("template/{parameter:int}", "template/{*parameter}")] - [InlineData("template/{parameter}", "template/{*parameter:int}")] - [InlineData("template/{parameter}", "template/{*parameter}")] - [InlineData("template/{*parameter:int}", "template/{*parameter}")] - public virtual async Task Match_SelectEndpoint_BasedOnPrecedence(string template1, string template2) - { - // Arrange - var expected = CreateEndpoint(template1); - var other = CreateEndpoint(template2); - var path = "/template/5"; - - // Arrange - var matcher = CreateMatcher(other, expected); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true); - } - - [Theory] - [InlineData("template/5", "template/{parameter:int}")] - [InlineData("template/5", "template/{parameter}")] - [InlineData("template/5", "template/{*parameter:int}")] - [InlineData("template/5", "template/{*parameter}")] - [InlineData("template/{parameter:int}", "template/{parameter}")] - [InlineData("template/{parameter:int}", "template/{*parameter:int}")] - [InlineData("template/{parameter:int}", "template/{*parameter}")] - [InlineData("template/{parameter}", "template/{*parameter:int}")] - [InlineData("template/{parameter}", "template/{*parameter}")] - [InlineData("template/{*parameter:int}", "template/{*parameter}")] - [InlineData("template/5", "template/5")] - [InlineData("template/{parameter:int}", "template/{parameter:int}")] - [InlineData("template/{parameter}", "template/{parameter}")] - [InlineData("template/{*parameter:int}", "template/{*parameter:int}")] - [InlineData("template/{*parameter}", "template/{*parameter}")] - public virtual async Task Match_SelectEndpoint_BasedOnOrder(string template1, string template2) - { - // Arrange - var expected = CreateEndpoint(template1, order: 0); - var other = CreateEndpoint(template2, order: 1); - var path = "/template/5"; - - // Arrange - var matcher = CreateMatcher(other, expected); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true); - } - - [Theory] - [InlineData("/", "")] - [InlineData("/Literal1", "Literal1")] - [InlineData("/Literal1/Literal2", "Literal1/Literal2")] - [InlineData("/Literal1/Literal2/Literal3", "Literal1/Literal2/Literal3")] - [InlineData("/Literal1/Literal2/Literal3/4", "Literal1/Literal2/Literal3/{*constrainedCatchAll:int}")] - [InlineData("/Literal1/Literal2/Literal3/Literal4", "Literal1/Literal2/Literal3/{*catchAll}")] - [InlineData("/1", "{constrained1:int}")] - [InlineData("/1/2", "{constrained1:int}/{constrained2:int}")] - [InlineData("/1/2/3", "{constrained1:int}/{constrained2:int}/{constrained3:int}")] - [InlineData("/1/2/3/4", "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*constrainedCatchAll:int}")] - [InlineData("/1/2/3/CatchAll4", "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*catchAll}")] - [InlineData("/parameter1", "{parameter1}")] - [InlineData("/parameter1/parameter2", "{parameter1}/{parameter2}")] - [InlineData("/parameter1/parameter2/parameter3", "{parameter1}/{parameter2}/{parameter3}")] - [InlineData("/parameter1/parameter2/parameter3/4", "{parameter1}/{parameter2}/{parameter3}/{*constrainedCatchAll:int}")] - [InlineData("/parameter1/parameter2/parameter3/CatchAll4", "{parameter1}/{parameter2}/{parameter3}/{*catchAll}")] - public virtual async Task Match_IntegrationTest_MultipleEndpoints(string path, string expectedTemplate) + [Fact] + public virtual async Task Match_ExtraDefaultValues() + { + // Arrange + var endpoint = CreateEndpoint("/a/{b}/{c}", new { b = "17", c = "18", d = "19" }); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/a"); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint, new { b = "17", c = "18", d = "19" }); + } + + [Theory] + [InlineData("/a/{b=15}", "/54/b")] + [InlineData("/a/{b=15}", "/54/")] + [InlineData("/a/{b=15}", "/54")] + [InlineData("/a/{b=15}", "/a//")] + [InlineData("/a/{b=15}", "/54/43/23")] + [InlineData("/{a=19}/{b=15}", "/54/b/c")] + [InlineData("/a/{b=15}/c", "/a/b")] // Intermediate default values don't act like optional segments + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a")] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b")] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c")] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d")] + public virtual async Task NotMatch_DefaultValues(string template, string path) + { + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertNotMatch(httpContext); + } + + [Theory] + [InlineData("/{a?}/{b?}/{c?}", "/", null, null)] + [InlineData("/{a?}/{b?}/{c?}", "/a", new[] { "a", }, new[] { "a", })] + [InlineData("/{a?}/{b?}/{c?}", "/a/", new[] { "a", }, new[] { "a", })] + [InlineData("/{a?}/{b?}/{c?}", "/a/b", new[] { "a", "b", }, new[] { "a", "b", })] + [InlineData("/{a?}/{b?}/{c?}", "/a/b/", new[] { "a", "b", }, new[] { "a", "b", })] + [InlineData("/{a?}/{b?}/{c?}", "/a/b/c", new[] { "a", "b", "c", }, new[] { "a", "b", "c", })] + [InlineData("/{a?}/{b?}/{c?}", "/a/b/c/", new[] { "a", "b", "c", }, new[] { "a", "b", "c", })] + [InlineData("/{c}/{a?}", "/h/i", new[] { "c", "a", }, new[] { "h", "i", })] + [InlineData("/{c}/{a?}", "/h/", new[] { "c", }, new[] { "h", })] + [InlineData("/{c}/{a?}", "/h", new[] { "c", }, new[] { "h", })] + [InlineData("/{c?}/{a?}", "/", null, null)] + [InlineData("/{c}/{a?}/{id?}", "/h/i/18", new[] { "c", "a", "id", }, new[] { "h", "i", "18", })] + [InlineData("/{c}/{a?}/{id?}", "/h/i", new[] { "c", "a", }, new[] { "h", "i", })] + [InlineData("/{c}/{a?}/{id?}", "/h", new[] { "c", }, new[] { "h", })] + [InlineData("template/{p:int?}", "/template/5", new[] { "p", }, new[] { "5", })] + [InlineData("template/{p:int?}", "/template", null, null)] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e", new[] { "b", "d", "f" }, new[] { "b", "d", null, })] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e/f", new[] { "b", "d", "f", }, new[] { "b", "d", "f", })] + public virtual async Task Match_OptionalParameter(string template, string path, string[] keys, string[] values) + { + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint, keys, values); + } + + [Theory] + [InlineData("/{a?}/{b?}/{c?}", "///")] + [InlineData("/{a?}/{b?}/{c?}", "/a//")] + [InlineData("/{a?}/{b?}/{c?}", "/a/b//")] + [InlineData("/{a?}/{b?}/{c?}", "//b//")] + [InlineData("/{a?}/{b?}/{c?}", "///c")] + [InlineData("/{a?}/{b?}/{c?}", "///c/")] + [InlineData("/{a?}/{b?}/{c?}", "/a/b/c/d")] + [InlineData("/a/{b?}/{c?}", "/")] + [InlineData("template/{parameter:int?}", "/template/qwer")] + public virtual async Task NotMatch_OptionalParameter(string template, string path) + { + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertNotMatch(httpContext); + } + + [Theory] + [InlineData("/{a}/{*b}", "/a", new[] { "a", "b", }, new[] { "a", null, })] + [InlineData("/{a}/{*b}", "/a/", new[] { "a", "b", }, new[] { "a", null, })] + [InlineData("/{a}/{*b=b}", "/a", new[] { "a", "b", }, new[] { "a", "b", })] + [InlineData("/{a}/{*b=b}", "/a/", new[] { "a", "b", }, new[] { "a", "b", })] + [InlineData("/{a}/{*b=b}", "/a/hello", new[] { "a", "b", }, new[] { "a", "hello", })] + [InlineData("/{a}/{*b=b}", "/a/hello/goodbye", new[] { "a", "b", }, new[] { "a", "hello/goodbye", })] + [InlineData("/{a}/{*b=b}", "/a/b//", new[] { "a", "b", }, new[] { "a", "b//", })] + [InlineData("/{a}/{*b=b}", "/a/b/c/", new[] { "a", "b", }, new[] { "a", "b/c/", })] + [InlineData("/{a=1}/{b=2}/{c=3}/{d=4}", "/a/b/c", new[] { "a", "b", "c", "d", }, new[] { "a", "b", "c", "4", })] + [InlineData("a/{*path:regex(10/20/30)}", "/a/10/20/30", new[] { "path", }, new[] { "10/20/30" })] + public virtual async Task Match_CatchAllParameter(string template, string path, string[] keys, string[] values) + { + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint, keys, values); + } + + // Historically catchall segments don't match an empty segment, but only if it's + // the first one. So `/a/b//` would match, but `/a//` would not. This is pretty + // weird and inconsistent with the intent of using a catch all. The DfaMatcher + // fixes this issue. + [Theory] + [InlineData("/{a}/{*b=b}", "/a///", new[] { "a", "b", }, new[] { "a", "//" })] + [InlineData("/{a}/{*b=b}", "/a//c/", new[] { "a", "b", }, new[] { "a", "/c/" })] + public virtual async Task Quirks_CatchAllParameter(string template, string path, string[] keys, string[] values) + { + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertNotMatch(httpContext); + + // Need to access these to prevent a warning from the xUnit analyzer. + // Some of these tests will match (and process the values) and some will not. + GC.KeepAlive(keys); + GC.KeepAlive(values); + } + + [Theory] + [InlineData("{p}x{s}", "/xxxxxxxxxx", new[] { "p", "s" }, new[] { "xxxxxxxx", "x", })] + [InlineData("{p}xyz{s}", "/xxxxyzxyzxxxxxxyz", new[] { "p", "s" }, new[] { "xxxxyz", "xxxxxxyz", })] + [InlineData("{p}xyz{s}", "/abcxxxxyzxyzxxxxxxyzxx", new[] { "p", "s" }, new[] { "abcxxxxyzxyzxxxxx", "xx", })] + [InlineData("{p}xyz{s}", "/xyzxyzxyzxyzxyz", new[] { "p", "s" }, new[] { "xyzxyzxyz", "xyz", })] + [InlineData("{p}xyz{s}", "/xyzxyzxyzxyzxyz1", new[] { "p", "s" }, new[] { "xyzxyzxyzxyz", "1", })] + [InlineData("{p}xyz{s}", "/xyzxyzxyz", new[] { "p", "s" }, new[] { "xyz", "xyz", })] + [InlineData("{p}aa{s}", "/aaaaa", new[] { "p", "s" }, new[] { "aa", "a", })] + [InlineData("{p}aaa{s}", "/aaaaa", new[] { "p", "s" }, new[] { "a", "a", })] + [InlineData("language/{lang=en}-{region=US}", "/language/xx-yy", new[] { "lang", "region" }, new[] { "xx", "yy", })] + [InlineData("language/{lang}-{region}", "/language/en-US", new[] { "lang", "region" }, new[] { "en", "US", })] + [InlineData("language/{lang}-{region}a", "/language/en-USa", new[] { "lang", "region" }, new[] { "en", "US", })] + [InlineData("language/a{lang}-{region}", "/language/aen-US", new[] { "lang", "region" }, new[] { "en", "US", })] + [InlineData("language/a{lang}-{region}a", "/language/aen-USa", new[] { "lang", "region" }, new[] { "en", "US", })] + [InlineData("language/{lang}-", "/language/en-", new[] { "lang", }, new[] { "en", })] + [InlineData("language/a{lang}", "/language/aen", new[] { "lang", }, new[] { "en", })] + [InlineData("language/a{lang}a", "/language/aena", new[] { "lang", }, new[] { "en", })] + public virtual async Task Match_ComplexSegment(string template, string path, string[] keys, string[] values) + { + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint, keys, values); + } + + [Theory] + [InlineData("language/a{lang}-{region}a", "/language/a-USa")] + [InlineData("language/a{lang}-{region}a", "/language/aen-a")] + [InlineData("language/{lang=en}-{region=US}", "/language")] + [InlineData("language/{lang=en}-{region=US}", "/language/-")] + [InlineData("language/{lang=en}-{region=US}", "/language/xx-")] + [InlineData("language/{lang=en}-{region=US}", "/language/-xx")] + public virtual async Task NotMatch_ComplexSegment(string template, string path) + { + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertNotMatch(httpContext); + } + + [Theory] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", new[] { "p1", "p2", }, new[] { "foo", "bar" })] + [InlineData("moo/{p1}.{p2?}", "/moo/foo", new[] { "p1", }, new[] { "foo", })] + [InlineData("moo/{p1}.{p2?}", "/moo/.foo", new[] { "p1", }, new[] { ".foo", })] + [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", new[] { "p1", "p2", }, new[] { "foo.", "bar" })] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", new[] { "p1", "p2", }, new[] { "foo.moo", "bar" })] + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", new[] { "p1", }, new[] { "moo", })] + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.foo.bar", new[] { "p1", "p2", }, new[] { "foo", "bar" })] + [InlineData("moo/.{p2?}", "/moo/.foo", new[] { "p2", }, new[] { "foo", })] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", new[] { "p1", "p2", "p3" }, new[] { "foo", "moo", "bar" })] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", new[] { "p1", "p2", }, new[] { "foo", "moo" })] + [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", new[] { "p1", "p2", "p3" }, new[] { "foo", "moo", "bar" })] + [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", new[] { "p1", "p2", "p3" }, new[] { "foo", "moo", "bar" })] + [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", new[] { "p1", "p3" }, new[] { "foo", "bar" })] + [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", new[] { "p1", "p3" }, new[] { ".foo", "bar" })] + [InlineData("{p1}/{p2}/{p3?}", "/foo/bar/baz", new[] { "p1", "p2", "p3" }, new[] { "foo", "bar", "baz" })] + public virtual async Task Match_OptionalSeparator(string template, string path, string[] keys, string[] values) + { + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint, keys, values); + } + + [Theory] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.")] + [InlineData("moo/{p1}.{p2?}", "/moo/.")] + [InlineData("moo/{p1}.{p2}", "/foo.")] + [InlineData("moo/{p1}.{p2}", "/foo")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")] + [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")] + [InlineData("moo/.{p2?}", "/moo/.")] + [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")] + public virtual async Task NotMatch_OptionalSeparator(string template, string path) + { + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertNotMatch(httpContext); + } + + // Most of are copied from old routing tests that date back to the VS 2010 era. Enjoy! + [Theory] + [InlineData("{Controller}.mvc/../{action}", "/Home.mvc/../index", new string[] { "Controller", "action" }, new string[] { "Home", "index" })] + [InlineData("{Controller}.mvc/.../{action}", "/Home.mvc/.../index", new string[] { "Controller", "action" }, new string[] { "Home", "index" })] + [InlineData("{Controller}.mvc/../../../{action}", "/Home.mvc/../../../index", new string[] { "Controller", "action" }, new string[] { "Home", "index" })] + [InlineData("{Controller}.mvc!/{action}", "/Home.mvc!/index", new string[] { "Controller", "action" }, new string[] { "Home", "index" })] + [InlineData("../{Controller}.mvc", "/../Home.mvc", new string[] { "Controller", }, new string[] { "Home", })] + [InlineData(@"\{Controller}.mvc", @"/\Home.mvc", new string[] { "Controller", }, new string[] { "Home", })] + [InlineData(@"{Controller}.mvc\{id}\{Param1}", @"/Home.mvc\123\p1", new string[] { "Controller", "id", "Param1" }, new string[] { "Home", "123", "p1" })] + [InlineData("(Controller).mvc", "/(Controller).mvc", new string[] { }, new string[] { })] + [InlineData("Controller.mvc/ ", "/Controller.mvc/ ", new string[] { }, new string[] { })] + [InlineData("Controller.mvc ", "/Controller.mvc ", new string[] { }, new string[] { })] + public virtual async Task Match_WeirdCharacterCases(string template, string path, string[] keys, string[] values) + { + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint, keys, values); + } + + [Theory] + [InlineData("template/5", "template/{parameter:int}")] + [InlineData("template/5", "template/{parameter}")] + [InlineData("template/5", "template/{*parameter:int}")] + [InlineData("template/5", "template/{*parameter}")] + [InlineData("template/{parameter}", "template/{parameter:alpha}")] // constraint doesn't match + [InlineData("template/{parameter:int}", "template/{parameter}")] + [InlineData("template/{parameter:int}", "template/{*parameter:int}")] + [InlineData("template/{parameter:int}", "template/{*parameter}")] + [InlineData("template/{parameter}", "template/{*parameter:int}")] + [InlineData("template/{parameter}", "template/{*parameter}")] + [InlineData("template/{*parameter:int}", "template/{*parameter}")] + public virtual async Task Match_SelectEndpoint_BasedOnPrecedence(string template1, string template2) + { + // Arrange + var expected = CreateEndpoint(template1); + var other = CreateEndpoint(template2); + var path = "/template/5"; + + // Arrange + var matcher = CreateMatcher(other, expected); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true); + } + + [Theory] + [InlineData("template/5", "template/{parameter:int}")] + [InlineData("template/5", "template/{parameter}")] + [InlineData("template/5", "template/{*parameter:int}")] + [InlineData("template/5", "template/{*parameter}")] + [InlineData("template/{parameter:int}", "template/{parameter}")] + [InlineData("template/{parameter:int}", "template/{*parameter:int}")] + [InlineData("template/{parameter:int}", "template/{*parameter}")] + [InlineData("template/{parameter}", "template/{*parameter:int}")] + [InlineData("template/{parameter}", "template/{*parameter}")] + [InlineData("template/{*parameter:int}", "template/{*parameter}")] + [InlineData("template/5", "template/5")] + [InlineData("template/{parameter:int}", "template/{parameter:int}")] + [InlineData("template/{parameter}", "template/{parameter}")] + [InlineData("template/{*parameter:int}", "template/{*parameter:int}")] + [InlineData("template/{*parameter}", "template/{*parameter}")] + public virtual async Task Match_SelectEndpoint_BasedOnOrder(string template1, string template2) + { + // Arrange + var expected = CreateEndpoint(template1, order: 0); + var other = CreateEndpoint(template2, order: 1); + var path = "/template/5"; + + // Arrange + var matcher = CreateMatcher(other, expected); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true); + } + + [Theory] + [InlineData("/", "")] + [InlineData("/Literal1", "Literal1")] + [InlineData("/Literal1/Literal2", "Literal1/Literal2")] + [InlineData("/Literal1/Literal2/Literal3", "Literal1/Literal2/Literal3")] + [InlineData("/Literal1/Literal2/Literal3/4", "Literal1/Literal2/Literal3/{*constrainedCatchAll:int}")] + [InlineData("/Literal1/Literal2/Literal3/Literal4", "Literal1/Literal2/Literal3/{*catchAll}")] + [InlineData("/1", "{constrained1:int}")] + [InlineData("/1/2", "{constrained1:int}/{constrained2:int}")] + [InlineData("/1/2/3", "{constrained1:int}/{constrained2:int}/{constrained3:int}")] + [InlineData("/1/2/3/4", "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*constrainedCatchAll:int}")] + [InlineData("/1/2/3/CatchAll4", "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*catchAll}")] + [InlineData("/parameter1", "{parameter1}")] + [InlineData("/parameter1/parameter2", "{parameter1}/{parameter2}")] + [InlineData("/parameter1/parameter2/parameter3", "{parameter1}/{parameter2}/{parameter3}")] + [InlineData("/parameter1/parameter2/parameter3/4", "{parameter1}/{parameter2}/{parameter3}/{*constrainedCatchAll:int}")] + [InlineData("/parameter1/parameter2/parameter3/CatchAll4", "{parameter1}/{parameter2}/{parameter3}/{*catchAll}")] + public virtual async Task Match_IntegrationTest_MultipleEndpoints(string path, string expectedTemplate) + { + // Arrange + var templates = new[] { - // Arrange - var templates = new[] - { "", "Literal1", "Literal1/Literal2", @@ -431,25 +431,25 @@ namespace Microsoft.AspNetCore.Routing.Matching "{parameter1}/{parameter2}/{parameter3}/{*catchAll}", }; - var endpoints = templates.Select((t) => CreateEndpoint(t)).ToArray(); - var expected = endpoints[Array.IndexOf(templates, expectedTemplate)]; + var endpoints = templates.Select((t) => CreateEndpoint(t)).ToArray(); + var expected = endpoints[Array.IndexOf(templates, expectedTemplate)]; - var matcher = CreateMatcher(endpoints); - var httpContext = CreateContext(path); + var matcher = CreateMatcher(endpoints); + var httpContext = CreateContext(path); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true); - } + // Assert + MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true); + } - // https://github.com/dotnet/aspnetcore/issues/16579 - [Fact] - public virtual async Task Match_Regression_16579_Order1() + // https://github.com/dotnet/aspnetcore/issues/16579 + [Fact] + public virtual async Task Match_Regression_16579_Order1() + { + var endpoints = new RouteEndpoint[] { - var endpoints = new RouteEndpoint[] - { EndpointFactory.CreateRouteEndpoint( "{controller}/folder/{*path}", order: 0, @@ -460,26 +460,26 @@ namespace Microsoft.AspNetCore.Routing.Matching order: 1, defaults: new { controller = "File", action = "Index", }, requiredValues: new { controller = "File", action = "Index", }), - }; + }; - var expected = endpoints[0]; + var expected = endpoints[0]; - var matcher = CreateMatcher(endpoints); - var httpContext = CreateContext("/file/folder/abc/abc"); + var matcher = CreateMatcher(endpoints); + var httpContext = CreateContext("/file/folder/abc/abc"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true); - } + // Assert + MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true); + } - // https://github.com/dotnet/aspnetcore/issues/16579 - [Fact] - public virtual async Task Match_Regression_16579_Order2() + // https://github.com/dotnet/aspnetcore/issues/16579 + [Fact] + public virtual async Task Match_Regression_16579_Order2() + { + var endpoints = new RouteEndpoint[] { - var endpoints = new RouteEndpoint[] - { EndpointFactory.CreateRouteEndpoint( "{controller}/{action}/{filename}", order: 0, @@ -491,18 +491,17 @@ namespace Microsoft.AspNetCore.Routing.Matching order: 1, defaults: new { controller = "File", action = "Folder", }, requiredValues: new { controller = "File", }), - }; + }; - var expected = endpoints[1]; + var expected = endpoints[1]; - var matcher = CreateMatcher(endpoints); - var httpContext = CreateContext("/file/folder/abc/abc"); + var matcher = CreateMatcher(endpoints); + var httpContext = CreateContext("/file/folder/abc/abc"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true); - } + // Assert + MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIEndpointSelectorPolicyIntegrationTest.cs b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIEndpointSelectorPolicyIntegrationTest.cs index b42026ed96..c2313d1b28 100644 --- a/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIEndpointSelectorPolicyIntegrationTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIEndpointSelectorPolicyIntegrationTest.cs @@ -1,11 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// End-to-end tests for the host matching functionality +public class HostMatcherPolicyIEndpointSelectorPolicyIntegrationTest : HostMatcherPolicyIntegrationTestBase { - // End-to-end tests for the host matching functionality - public class HostMatcherPolicyIEndpointSelectorPolicyIntegrationTest : HostMatcherPolicyIntegrationTestBase - { - protected override bool HasDynamicMetadata => true; - } + protected override bool HasDynamicMetadata => true; } diff --git a/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyINodeBuilderPolicyIntegrationTest.cs b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyINodeBuilderPolicyIntegrationTest.cs index 3ef23079f9..10c89d8b15 100644 --- a/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyINodeBuilderPolicyIntegrationTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyINodeBuilderPolicyIntegrationTest.cs @@ -1,11 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// End-to-end tests for the host matching functionality +public class HostMatcherPolicyINodeBuilderPolicyIntegrationTest : HostMatcherPolicyIntegrationTestBase { - // End-to-end tests for the host matching functionality - public class HostMatcherPolicyINodeBuilderPolicyIntegrationTest : HostMatcherPolicyIntegrationTestBase - { - protected override bool HasDynamicMetadata => false; - } + protected override bool HasDynamicMetadata => false; } diff --git a/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIntegrationTestBase.cs b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIntegrationTestBase.cs index bb0647aab7..fa06c2ad18 100644 --- a/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIntegrationTestBase.cs +++ b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyIntegrationTestBase.cs @@ -10,434 +10,433 @@ using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// End-to-end tests for the host matching functionality +public abstract class HostMatcherPolicyIntegrationTestBase { - // End-to-end tests for the host matching functionality - public abstract class HostMatcherPolicyIntegrationTestBase - { - protected abstract bool HasDynamicMetadata { get; } + protected abstract bool HasDynamicMetadata { get; } - [Fact] - public async Task Match_Host() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com", }); + [Fact] + public async Task Match_Host() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "contoso.com"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "contoso.com"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_HostWithPort() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:8080", }); + [Fact] + public async Task Match_HostWithPort() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:8080", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "contoso.com:8080"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "contoso.com:8080"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_Host_Unicode() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "æon.contoso.com", }); + [Fact] + public async Task Match_Host_Unicode() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "æon.contoso.com", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "æon.contoso.com"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "æon.contoso.com"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_HostWithPort_IncorrectPort() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:8080", }); + [Fact] + public async Task Match_HostWithPort_IncorrectPort() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:8080", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "contoso.com:1111"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "contoso.com:1111"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertNotMatch(httpContext); - } + // Assert + MatcherAssert.AssertNotMatch(httpContext); + } - [Fact] - public async Task Match_HostWithPort_IncorrectHost() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:8080", }); + [Fact] + public async Task Match_HostWithPort_IncorrectHost() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:8080", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "www.contoso.com:8080"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "www.contoso.com:8080"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertNotMatch(httpContext); - } + // Assert + MatcherAssert.AssertNotMatch(httpContext); + } - [Fact] - public async Task Match_HostWithWildcard_Unicode() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*.contoso.com:8080", }); + [Fact] + public async Task Match_HostWithWildcard_Unicode() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*.contoso.com:8080", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "æon.contoso.com:8080"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "æon.contoso.com:8080"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_HostWithWildcard_NoSubdomain() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*.contoso.com:8080", }); + [Fact] + public async Task Match_HostWithWildcard_NoSubdomain() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*.contoso.com:8080", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "contoso.com:8080"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "contoso.com:8080"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertNotMatch(httpContext); - } + // Assert + MatcherAssert.AssertNotMatch(httpContext); + } - [Fact] - public async Task Match_HostWithWildcard_Subdomain() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*.contoso.com:8080", }); + [Fact] + public async Task Match_HostWithWildcard_Subdomain() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*.contoso.com:8080", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "www.contoso.com:8080"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "www.contoso.com:8080"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_HostWithWildcard_MultipleSubdomains() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*.contoso.com:8080", }); + [Fact] + public async Task Match_HostWithWildcard_MultipleSubdomains() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*.contoso.com:8080", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "www.blog.contoso.com:8080"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "www.blog.contoso.com:8080"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_HostWithWildcard_PrefixNotInSubdomain() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*.contoso.com:8080", }); + [Fact] + public async Task Match_HostWithWildcard_PrefixNotInSubdomain() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*.contoso.com:8080", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "mycontoso.com:8080"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "mycontoso.com:8080"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertNotMatch(httpContext); - } + // Assert + MatcherAssert.AssertNotMatch(httpContext); + } - [Fact] - public async Task Match_HostAndHostWithWildcard_NoSubdomain() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:8080", "*.contoso.com:8080", }); + [Fact] + public async Task Match_HostAndHostWithWildcard_NoSubdomain() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:8080", "*.contoso.com:8080", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "contoso.com:8080"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "contoso.com:8080"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_Host_CaseInsensitive() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "Contoso.COM", }); + [Fact] + public async Task Match_Host_CaseInsensitive() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "Contoso.COM", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "contoso.com"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "contoso.com"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_HostWithPort_InferHttpPort() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:80", }); + [Fact] + public async Task Match_HostWithPort_InferHttpPort() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:80", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "contoso.com", "http"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "contoso.com", "http"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_HostWithPort_InferHttpsPort() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:443", }); + [Fact] + public async Task Match_HostWithPort_InferHttpsPort() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:443", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "contoso.com", "https"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "contoso.com", "https"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_HostWithPort_NoHostHeader() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:443", }); + [Fact] + public async Task Match_HostWithPort_NoHostHeader() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "contoso.com:443", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", null, "https"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", null, "https"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertNotMatch(httpContext); - } + // Assert + MatcherAssert.AssertNotMatch(httpContext); + } - [Fact] - public async Task Match_Port_NoHostHeader_InferHttpsPort() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*:443", }); + [Fact] + public async Task Match_Port_NoHostHeader_InferHttpsPort() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*:443", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", null, "https"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", null, "https"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_NoMetadata_MatchesAnyHost() - { - // Arrange - var endpoint = CreateEndpoint("/hello"); + [Fact] + public async Task Match_NoMetadata_MatchesAnyHost() + { + // Arrange + var endpoint = CreateEndpoint("/hello"); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "contoso.com"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "contoso.com"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_EmptyHostList_MatchesAnyHost() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { }); + [Fact] + public async Task Match_EmptyHostList_MatchesAnyHost() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "contoso.com"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "contoso.com"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_WildcardHost_MatchesAnyHost() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*", }); + [Fact] + public async Task Match_WildcardHost_MatchesAnyHost() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "contoso.com"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "contoso.com"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_WildcardHostAndWildcardPort_MatchesAnyHost() - { - // Arrange - var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*:*", }); + [Fact] + public async Task Match_WildcardHostAndWildcardPort_MatchesAnyHost() + { + // Arrange + var endpoint = CreateEndpoint("/hello", hosts: new string[] { "*:*", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "contoso.com"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "contoso.com"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_CatchAllRouteWithMatchingHost_Success() - { - // Arrange - var endpoint = CreateEndpoint("/{**path}", hosts: new string[] { "contoso.com", }); + [Fact] + public async Task Match_CatchAllRouteWithMatchingHost_Success() + { + // Arrange + var endpoint = CreateEndpoint("/{**path}", hosts: new string[] { "contoso.com", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "contoso.com"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "contoso.com"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint, new { path = "hello" }); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint, new { path = "hello" }); + } - [Fact] - public async Task Match_CatchAllRouteFailureHost_NoMatch() - { - // Arrange - var endpoint = CreateEndpoint("/{**path}", hosts: new string[] { "contoso.com", }); + [Fact] + public async Task Match_CatchAllRouteFailureHost_NoMatch() + { + // Arrange + var endpoint = CreateEndpoint("/{**path}", hosts: new string[] { "contoso.com", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "nomatch.com"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "nomatch.com"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertNotMatch(httpContext); - } + // Assert + MatcherAssert.AssertNotMatch(httpContext); + } - private static Matcher CreateMatcher(params RouteEndpoint[] endpoints) + private static Matcher CreateMatcher(params RouteEndpoint[] endpoints) + { + var services = new ServiceCollection() + .AddOptions() + .AddLogging() + .AddRouting() + .BuildServiceProvider(); + + var builder = services.GetRequiredService(); + for (var i = 0; i < endpoints.Length; i++) { - var services = new ServiceCollection() - .AddOptions() - .AddLogging() - .AddRouting() - .BuildServiceProvider(); - - var builder = services.GetRequiredService(); - for (var i = 0; i < endpoints.Length; i++) - { - builder.AddEndpoint(endpoints[i]); - } - - return builder.Build(); + builder.AddEndpoint(endpoints[i]); } - internal static HttpContext CreateContext( - string path, - string host, - string scheme = null) - { - var httpContext = new DefaultHttpContext(); - if (host != null) - { - httpContext.Request.Host = new HostString(host); - } - httpContext.Request.Path = path; - httpContext.Request.Scheme = scheme; - - return httpContext; - } + return builder.Build(); + } - internal RouteEndpoint CreateEndpoint( - string template, - object defaults = null, - object constraints = null, - int order = 0, - string[] hosts = null) + internal static HttpContext CreateContext( + string path, + string host, + string scheme = null) + { + var httpContext = new DefaultHttpContext(); + if (host != null) { - var metadata = new List(); - if (hosts != null) - { - metadata.Add(new HostAttribute(hosts ?? Array.Empty())); - } - - if (HasDynamicMetadata) - { - metadata.Add(new DynamicEndpointMetadata()); - } - - var displayName = "endpoint: " + template + " " + string.Join(", ", hosts ?? new[] { "*:*" }); - return new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse(template, defaults, constraints), - order, - new EndpointMetadataCollection(metadata), - displayName); + httpContext.Request.Host = new HostString(host); } + httpContext.Request.Path = path; + httpContext.Request.Scheme = scheme; - internal (Matcher matcher, RouteEndpoint endpoint) CreateMatcher(string template) + return httpContext; + } + + internal RouteEndpoint CreateEndpoint( + string template, + object defaults = null, + object constraints = null, + int order = 0, + string[] hosts = null) + { + var metadata = new List(); + if (hosts != null) { - var endpoint = CreateEndpoint(template); - return (CreateMatcher(endpoint), endpoint); + metadata.Add(new HostAttribute(hosts ?? Array.Empty())); } - private class DynamicEndpointMetadata : IDynamicEndpointMetadata + if (HasDynamicMetadata) { - public bool IsDynamic => true; + metadata.Add(new DynamicEndpointMetadata()); } + + var displayName = "endpoint: " + template + " " + string.Join(", ", hosts ?? new[] { "*:*" }); + return new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse(template, defaults, constraints), + order, + new EndpointMetadataCollection(metadata), + displayName); + } + + internal (Matcher matcher, RouteEndpoint endpoint) CreateMatcher(string template) + { + var endpoint = CreateEndpoint(template); + return (CreateMatcher(endpoint), endpoint); + } + + private class DynamicEndpointMetadata : IDynamicEndpointMetadata + { + public bool IsDynamic => true; } } diff --git a/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyTest.cs b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyTest.cs index b20597aa91..922edc402a 100644 --- a/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/HostMatcherPolicyTest.cs @@ -9,200 +9,200 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Patterns; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class HostMatcherPolicyTest { - public class HostMatcherPolicyTest + [Fact] + public void INodeBuilderPolicy_AppliesToEndpoints_EndpointWithoutMetadata_ReturnsFalse() { - [Fact] - public void INodeBuilderPolicy_AppliesToEndpoints_EndpointWithoutMetadata_ReturnsFalse() - { - // Arrange - var endpoints = new[] { CreateEndpoint("/", null), }; + // Arrange + var endpoints = new[] { CreateEndpoint("/", null), }; - var policy = (INodeBuilderPolicy)CreatePolicy(); + var policy = (INodeBuilderPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Fact] - public void INodeBuilderPolicy_AppliesToEndpoints_EndpointWithoutHosts_ReturnsFalse() + [Fact] + public void INodeBuilderPolicy_AppliesToEndpoints_EndpointWithoutHosts_ReturnsFalse() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new HostAttribute(Array.Empty())), }; - var policy = (INodeBuilderPolicy)CreatePolicy(); + var policy = (INodeBuilderPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Fact] - public void INodeBuilderPolicy_AppliesToEndpoints_EndpointHasHosts_ReturnsTrue() + [Fact] + public void INodeBuilderPolicy_AppliesToEndpoints_EndpointHasHosts_ReturnsTrue() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new HostAttribute(Array.Empty())), CreateEndpoint("/", new HostAttribute(new[] { "localhost", })), }; - var policy = (INodeBuilderPolicy)CreatePolicy(); + var policy = (INodeBuilderPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void INodeBuilderPolicy_AppliesToEndpoints_EndpointHasDynamicMetadata_ReturnsFalse() + [Fact] + public void INodeBuilderPolicy_AppliesToEndpoints_EndpointHasDynamicMetadata_ReturnsFalse() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new HostAttribute(Array.Empty())), CreateEndpoint("/", new HostAttribute(new[] { "localhost", }), new DynamicEndpointMetadata()), }; - var policy = (INodeBuilderPolicy)CreatePolicy(); + var policy = (INodeBuilderPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.False(result); - } - - [Theory] - [InlineData(":")] - [InlineData(":80")] - [InlineData("80:")] - [InlineData("")] - [InlineData("::")] - [InlineData("*:test")] - public void INodeBuilderPolicy_AppliesToEndpoints_InvalidHosts(string host) - { - // Arrange - var endpoints = new[] { CreateEndpoint("/", new HostAttribute(new[] { host })), }; + // Assert + Assert.False(result); + } - var policy = (INodeBuilderPolicy)CreatePolicy(); + [Theory] + [InlineData(":")] + [InlineData(":80")] + [InlineData("80:")] + [InlineData("")] + [InlineData("::")] + [InlineData("*:test")] + public void INodeBuilderPolicy_AppliesToEndpoints_InvalidHosts(string host) + { + // Arrange + var endpoints = new[] { CreateEndpoint("/", new HostAttribute(new[] { host })), }; - // Act & Assert - Assert.Throws(() => - { - policy.AppliesToEndpoints(endpoints); - }); - } + var policy = (INodeBuilderPolicy)CreatePolicy(); - [Fact] - public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointWithoutMetadata_ReturnsTrue() + // Act & Assert + Assert.Throws(() => { - // Arrange - var endpoints = new[] { CreateEndpoint("/", null, new DynamicEndpointMetadata()), }; + policy.AppliesToEndpoints(endpoints); + }); + } - var policy = (IEndpointSelectorPolicy)CreatePolicy(); + [Fact] + public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointWithoutMetadata_ReturnsTrue() + { + // Arrange + var endpoints = new[] { CreateEndpoint("/", null, new DynamicEndpointMetadata()), }; - // Act - var result = policy.AppliesToEndpoints(endpoints); + var policy = (IEndpointSelectorPolicy)CreatePolicy(); - // Assert - Assert.True(result); - } + // Act + var result = policy.AppliesToEndpoints(endpoints); + + // Assert + Assert.True(result); + } - [Fact] - public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointWithoutHosts_ReturnsTrue() + [Fact] + public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointWithoutHosts_ReturnsTrue() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new HostAttribute(Array.Empty()), new DynamicEndpointMetadata()), }; - var policy = (IEndpointSelectorPolicy)CreatePolicy(); + var policy = (IEndpointSelectorPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointHasHosts_ReturnsTrue() + [Fact] + public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointHasHosts_ReturnsTrue() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new HostAttribute(Array.Empty())), CreateEndpoint("/", new HostAttribute(new[] { "localhost", }), new DynamicEndpointMetadata()), }; - var policy = (IEndpointSelectorPolicy)CreatePolicy(); + var policy = (IEndpointSelectorPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointHasNoDynamicMetadata_ReturnsFalse() + [Fact] + public void IEndpointSelectorPolicy_AppliesToEndpoints_EndpointHasNoDynamicMetadata_ReturnsFalse() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new HostAttribute(Array.Empty())), CreateEndpoint("/", new HostAttribute(new[] { "localhost", })), }; - var policy = (IEndpointSelectorPolicy)CreatePolicy(); + var policy = (IEndpointSelectorPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Theory] - [InlineData(":")] - [InlineData(":80")] - [InlineData("80:")] - [InlineData("")] - [InlineData("::")] - [InlineData("*:test")] - public void IEndpointSelectorPolicy_AppliesToEndpoints_InvalidHosts(string host) - { - // Arrange - var endpoints = new[] { CreateEndpoint("/", new HostAttribute(new[] { host }), new DynamicEndpointMetadata()), }; + [Theory] + [InlineData(":")] + [InlineData(":80")] + [InlineData("80:")] + [InlineData("")] + [InlineData("::")] + [InlineData("*:test")] + public void IEndpointSelectorPolicy_AppliesToEndpoints_InvalidHosts(string host) + { + // Arrange + var endpoints = new[] { CreateEndpoint("/", new HostAttribute(new[] { host }), new DynamicEndpointMetadata()), }; - var policy = (IEndpointSelectorPolicy)CreatePolicy(); + var policy = (IEndpointSelectorPolicy)CreatePolicy(); - // Act & Assert - Assert.Throws(() => - { - policy.AppliesToEndpoints(endpoints); - }); - } + // Act & Assert + Assert.Throws(() => + { + policy.AppliesToEndpoints(endpoints); + }); + } - [Fact] - public void GetEdges_GroupsByHost() + [Fact] + public void GetEdges_GroupsByHost() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new HostAttribute(new[] { "*:5000", "*:5001", })), CreateEndpoint("/", new HostAttribute(Array.Empty())), CreateEndpoint("/", hostMetadata: null), @@ -213,82 +213,81 @@ namespace Microsoft.AspNetCore.Routing.Matching CreateEndpoint("/", new HostAttribute("*:*")), }; - var policy = CreatePolicy(); - - // Act - var edges = policy.GetEdges(endpoints); - - var data = edges.OrderBy(e => e.State).ToList(); - - // Assert - Assert.Collection( - data, - e => - { - Assert.Equal("*:*", e.State.ToString()); - Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[7], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal("*:5000", e.State.ToString()); - Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal("*:5001", e.State.ToString()); - Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal("*.contoso.com:*", e.State.ToString()); - Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal("*.sub.contoso.com:*", e.State.ToString()); - Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal("www.contoso.com:*", e.State.ToString()); - Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[5], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal("www.contoso.com:5000", e.State.ToString()); - Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[6], }, e.Endpoints.ToArray()); - }); - } + var policy = CreatePolicy(); - private static RouteEndpoint CreateEndpoint(string template, IHostMetadata hostMetadata, params object[] more) - { - var metadata = new List(); - if (hostMetadata != null) - { - metadata.Add(hostMetadata); - } + // Act + var edges = policy.GetEdges(endpoints); + + var data = edges.OrderBy(e => e.State).ToList(); - if (more != null) + // Assert + Assert.Collection( + data, + e => { - metadata.AddRange(more); - } - - return new RouteEndpoint( - (context) => Task.CompletedTask, - RoutePatternFactory.Parse(template), - 0, - new EndpointMetadataCollection(metadata), - $"test: {template} - {string.Join(", ", hostMetadata?.Hosts ?? Array.Empty())}"); - } + Assert.Equal("*:*", e.State.ToString()); + Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[7], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("*:5000", e.State.ToString()); + Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("*:5001", e.State.ToString()); + Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("*.contoso.com:*", e.State.ToString()); + Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("*.sub.contoso.com:*", e.State.ToString()); + Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("www.contoso.com:*", e.State.ToString()); + Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[5], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal("www.contoso.com:5000", e.State.ToString()); + Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[6], }, e.Endpoints.ToArray()); + }); + } - private static HostMatcherPolicy CreatePolicy() + private static RouteEndpoint CreateEndpoint(string template, IHostMetadata hostMetadata, params object[] more) + { + var metadata = new List(); + if (hostMetadata != null) { - return new HostMatcherPolicy(); + metadata.Add(hostMetadata); } - private class DynamicEndpointMetadata : IDynamicEndpointMetadata + if (more != null) { - public bool IsDynamic => true; + metadata.AddRange(more); } + + return new RouteEndpoint( + (context) => Task.CompletedTask, + RoutePatternFactory.Parse(template), + 0, + new EndpointMetadataCollection(metadata), + $"test: {template} - {string.Join(", ", hostMetadata?.Hosts ?? Array.Empty())}"); + } + + private static HostMatcherPolicy CreatePolicy() + { + return new HostMatcherPolicy(); + } + + private class DynamicEndpointMetadata : IDynamicEndpointMetadata + { + public bool IsDynamic => true; } } diff --git a/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyIEndpointSelectorPolicyIntegrationTest.cs b/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyIEndpointSelectorPolicyIntegrationTest.cs index d26f4ddd11..5f41c1dc02 100644 --- a/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyIEndpointSelectorPolicyIntegrationTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyIEndpointSelectorPolicyIntegrationTest.cs @@ -1,11 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// End-to-end tests for the HTTP method matching functionality +public class HttpMethodMatcherPolicyIEndpointSelectorPolicyIntegrationTestBase : HttpMethodMatcherPolicyIntegrationTestBase { - // End-to-end tests for the HTTP method matching functionality - public class HttpMethodMatcherPolicyIEndpointSelectorPolicyIntegrationTestBase : HttpMethodMatcherPolicyIntegrationTestBase - { - protected override bool HasDynamicMetadata => true; - } + protected override bool HasDynamicMetadata => true; } diff --git a/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyINodeBuilderPolicyIntegrationTest.cs b/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyINodeBuilderPolicyIntegrationTest.cs index 49934d3ecf..a74c5eb7d2 100644 --- a/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyINodeBuilderPolicyIntegrationTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyINodeBuilderPolicyIntegrationTest.cs @@ -1,11 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// End-to-end tests for the HTTP method matching functionality +public class HttpMethodMatcherPolicyINodeBuilderPolicyIntegrationTestBase : HttpMethodMatcherPolicyIntegrationTestBase { - // End-to-end tests for the HTTP method matching functionality - public class HttpMethodMatcherPolicyINodeBuilderPolicyIntegrationTestBase : HttpMethodMatcherPolicyIntegrationTestBase - { - protected override bool HasDynamicMetadata => false; - } + protected override bool HasDynamicMetadata => false; } diff --git a/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyIntegrationTestBase.cs b/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyIntegrationTestBase.cs index bd38bed90b..fa1ba98493 100644 --- a/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyIntegrationTestBase.cs +++ b/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyIntegrationTestBase.cs @@ -12,391 +12,390 @@ using Microsoft.Net.Http.Headers; using Xunit; using static Microsoft.AspNetCore.Routing.Matching.HttpMethodMatcherPolicy; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// End-to-end tests for the HTTP method matching functionality +public abstract class HttpMethodMatcherPolicyIntegrationTestBase { - // End-to-end tests for the HTTP method matching functionality - public abstract class HttpMethodMatcherPolicyIntegrationTestBase - { - protected abstract bool HasDynamicMetadata { get; } + protected abstract bool HasDynamicMetadata { get; } - [Fact] - public async Task Match_HttpMethod() - { - // Arrange - var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }); + [Fact] + public async Task Match_HttpMethod() + { + // Arrange + var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "GET"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "GET"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_HttpMethod_CORS() - { - // Arrange - var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }, acceptCorsPreflight: true); + [Fact] + public async Task Match_HttpMethod_CORS() + { + // Arrange + var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }, acceptCorsPreflight: true); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "GET"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "GET"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_HttpMethod_CORS_Preflight() - { - // Arrange - var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }, acceptCorsPreflight: true); + [Fact] + public async Task Match_HttpMethod_CORS_Preflight() + { + // Arrange + var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }, acceptCorsPreflight: true); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "GET", corsPreflight: true); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "GET", corsPreflight: true); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] // Nothing here supports OPTIONS, so it goes to a 405. - public async Task NotMatch_HttpMethod_CORS_Preflight() - { - // Arrange - var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }, acceptCorsPreflight: false); + [Fact] // Nothing here supports OPTIONS, so it goes to a 405. + public async Task NotMatch_HttpMethod_CORS_Preflight() + { + // Arrange + var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }, acceptCorsPreflight: false); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "GET", corsPreflight: true); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "GET", corsPreflight: true); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - Assert.NotSame(endpoint, httpContext.GetEndpoint()); - Assert.Same(HttpMethodMatcherPolicy.Http405EndpointDisplayName, httpContext.GetEndpoint().DisplayName); - } + // Assert + Assert.NotSame(endpoint, httpContext.GetEndpoint()); + Assert.Same(HttpMethodMatcherPolicy.Http405EndpointDisplayName, httpContext.GetEndpoint().DisplayName); + } - [Theory] - [InlineData("GeT", "GET")] - [InlineData("unKNOWN", "UNKNOWN")] - public async Task Match_HttpMethod_CaseInsensitive(string endpointMethod, string requestMethod) - { - // Arrange - var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { endpointMethod, }); + [Theory] + [InlineData("GeT", "GET")] + [InlineData("unKNOWN", "UNKNOWN")] + public async Task Match_HttpMethod_CaseInsensitive(string endpointMethod, string requestMethod) + { + // Arrange + var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { endpointMethod, }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", requestMethod); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", requestMethod); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Theory] - [InlineData("GeT", "GET")] - [InlineData("unKNOWN", "UNKNOWN")] - public async Task Match_HttpMethod_CaseInsensitive_CORS_Preflight(string endpointMethod, string requestMethod) - { - // Arrange - var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { endpointMethod, }, acceptCorsPreflight: true); + [Theory] + [InlineData("GeT", "GET")] + [InlineData("unKNOWN", "UNKNOWN")] + public async Task Match_HttpMethod_CaseInsensitive_CORS_Preflight(string endpointMethod, string requestMethod) + { + // Arrange + var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { endpointMethod, }, acceptCorsPreflight: true); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", requestMethod, corsPreflight: true); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", requestMethod, corsPreflight: true); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_NoMetadata_MatchesAnyHttpMethod() - { - // Arrange - var endpoint = CreateEndpoint("/hello"); + [Fact] + public async Task Match_NoMetadata_MatchesAnyHttpMethod() + { + // Arrange + var endpoint = CreateEndpoint("/hello"); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "GET"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "GET"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_NoMetadata_MatchesAnyHttpMethod_CORS_Preflight() - { - // Arrange - var endpoint = CreateEndpoint("/hello", acceptCorsPreflight: true); + [Fact] + public async Task Match_NoMetadata_MatchesAnyHttpMethod_CORS_Preflight() + { + // Arrange + var endpoint = CreateEndpoint("/hello", acceptCorsPreflight: true); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "GET", corsPreflight: true); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "GET", corsPreflight: true); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] // This matches because the endpoint accepts OPTIONS - public async Task Match_NoMetadata_MatchesAnyHttpMethod_CORS_Preflight_DoesNotSupportPreflight() - { - // Arrange - var endpoint = CreateEndpoint("/hello", acceptCorsPreflight: false); + [Fact] // This matches because the endpoint accepts OPTIONS + public async Task Match_NoMetadata_MatchesAnyHttpMethod_CORS_Preflight_DoesNotSupportPreflight() + { + // Arrange + var endpoint = CreateEndpoint("/hello", acceptCorsPreflight: false); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "GET", corsPreflight: true); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "GET", corsPreflight: true); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] - public async Task Match_EmptyMethodList_MatchesAnyHttpMethod() - { - // Arrange - var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { }); + [Fact] + public async Task Match_EmptyMethodList_MatchesAnyHttpMethod() + { + // Arrange + var endpoint = CreateEndpoint("/hello", httpMethods: new string[] { }); - var matcher = CreateMatcher(endpoint); - var httpContext = CreateContext("/hello", "GET"); + var matcher = CreateMatcher(endpoint); + var httpContext = CreateContext("/hello", "GET"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } - [Fact] // When all of the candidates handles specific verbs, use a 405 endpoint - public async Task NotMatch_HttpMethod_Returns405Endpoint() - { - // Arrange - var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", "PUT" }); - var endpoint2 = CreateEndpoint("/hello", httpMethods: new string[] { "DELETE" }); + [Fact] // When all of the candidates handles specific verbs, use a 405 endpoint + public async Task NotMatch_HttpMethod_Returns405Endpoint() + { + // Arrange + var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", "PUT" }); + var endpoint2 = CreateEndpoint("/hello", httpMethods: new string[] { "DELETE" }); - var matcher = CreateMatcher(endpoint1, endpoint2); - var httpContext = CreateContext("/hello", "POST"); + var matcher = CreateMatcher(endpoint1, endpoint2); + var httpContext = CreateContext("/hello", "POST"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - Assert.NotSame(endpoint1, httpContext.GetEndpoint()); - Assert.NotSame(endpoint2, httpContext.GetEndpoint()); + // Assert + Assert.NotSame(endpoint1, httpContext.GetEndpoint()); + Assert.NotSame(endpoint2, httpContext.GetEndpoint()); - Assert.Same(HttpMethodMatcherPolicy.Http405EndpointDisplayName, httpContext.GetEndpoint().DisplayName); + Assert.Same(HttpMethodMatcherPolicy.Http405EndpointDisplayName, httpContext.GetEndpoint().DisplayName); - // Invoke the endpoint - await httpContext.GetEndpoint().RequestDelegate(httpContext); - Assert.Equal(405, httpContext.Response.StatusCode); - Assert.Equal("DELETE, GET, PUT", httpContext.Response.Headers["Allow"]); - } + // Invoke the endpoint + await httpContext.GetEndpoint().RequestDelegate(httpContext); + Assert.Equal(405, httpContext.Response.StatusCode); + Assert.Equal("DELETE, GET, PUT", httpContext.Response.Headers["Allow"]); + } - [Fact] - public async Task NotMatch_HttpMethod_CORS_DoesNotReturn405() - { - // Arrange - var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", "PUT" }, acceptCorsPreflight: true); - var endpoint2 = CreateEndpoint("/hello", httpMethods: new string[] { "DELETE" }); + [Fact] + public async Task NotMatch_HttpMethod_CORS_DoesNotReturn405() + { + // Arrange + var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", "PUT" }, acceptCorsPreflight: true); + var endpoint2 = CreateEndpoint("/hello", httpMethods: new string[] { "DELETE" }); - var matcher = CreateMatcher(endpoint1, endpoint2); - var httpContext = CreateContext("/hello", "POST", corsPreflight: true); + var matcher = CreateMatcher(endpoint1, endpoint2); + var httpContext = CreateContext("/hello", "POST", corsPreflight: true); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertNotMatch(httpContext); - } + // Assert + MatcherAssert.AssertNotMatch(httpContext); + } - [Fact] // When one of the candidates handles all verbs, dont use a 405 endpoint - public async Task NotMatch_HttpMethod_WithAllMethodEndpoint_DoesNotReturn405() - { - // Arrange - var endpoint1 = CreateEndpoint("/{x:int}", httpMethods: new string[] { }); - var endpoint2 = CreateEndpoint("/{hello:regex(hello)}", httpMethods: new string[] { "DELETE" }); + [Fact] // When one of the candidates handles all verbs, dont use a 405 endpoint + public async Task NotMatch_HttpMethod_WithAllMethodEndpoint_DoesNotReturn405() + { + // Arrange + var endpoint1 = CreateEndpoint("/{x:int}", httpMethods: new string[] { }); + var endpoint2 = CreateEndpoint("/{hello:regex(hello)}", httpMethods: new string[] { "DELETE" }); - var matcher = CreateMatcher(endpoint1, endpoint2); - var httpContext = CreateContext("/hello", "POST"); + var matcher = CreateMatcher(endpoint1, endpoint2); + var httpContext = CreateContext("/hello", "POST"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertNotMatch(httpContext); - } + // Assert + MatcherAssert.AssertNotMatch(httpContext); + } - [Fact] - public async Task Match_EndpointWithHttpMethodPreferred() - { - // Arrange - var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }); - var endpoint2 = CreateEndpoint("/bar"); + [Fact] + public async Task Match_EndpointWithHttpMethodPreferred() + { + // Arrange + var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }); + var endpoint2 = CreateEndpoint("/bar"); - var matcher = CreateMatcher(endpoint1, endpoint2); - var httpContext = CreateContext("/hello", "GET"); + var matcher = CreateMatcher(endpoint1, endpoint2); + var httpContext = CreateContext("/hello", "GET"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint1); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint1); + } - [Fact] - public async Task Match_EndpointWithHttpMethodPreferred_EmptyList() - { - // Arrange - var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }); - var endpoint2 = CreateEndpoint("/bar", httpMethods: new string[] { }); + [Fact] + public async Task Match_EndpointWithHttpMethodPreferred_EmptyList() + { + // Arrange + var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", }); + var endpoint2 = CreateEndpoint("/bar", httpMethods: new string[] { }); - var matcher = CreateMatcher(endpoint1, endpoint2); - var httpContext = CreateContext("/hello", "GET"); + var matcher = CreateMatcher(endpoint1, endpoint2); + var httpContext = CreateContext("/hello", "GET"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint1); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint1); + } - [Fact] // The non-http-method-specific endpoint is part of the same candidate set - public async Task Match_EndpointWithHttpMethodPreferred_FallsBackToNonSpecific() - { - // Arrange - var endpoint1 = CreateEndpoint("/{x}", httpMethods: new string[] { "GET", }); - var endpoint2 = CreateEndpoint("/{x}", httpMethods: new string[] { }); + [Fact] // The non-http-method-specific endpoint is part of the same candidate set + public async Task Match_EndpointWithHttpMethodPreferred_FallsBackToNonSpecific() + { + // Arrange + var endpoint1 = CreateEndpoint("/{x}", httpMethods: new string[] { "GET", }); + var endpoint2 = CreateEndpoint("/{x}", httpMethods: new string[] { }); - var matcher = CreateMatcher(endpoint1, endpoint2); - var httpContext = CreateContext("/hello", "POST"); + var matcher = CreateMatcher(endpoint1, endpoint2); + var httpContext = CreateContext("/hello", "POST"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint2, ignoreValues: true); - } + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint2, ignoreValues: true); + } - [Fact] // See https://github.com/dotnet/aspnetcore/issues/6415 - public async Task NotMatch_HttpMethod_Returns405Endpoint_ReExecute() - { - // Arrange - var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", "PUT" }); - var endpoint2 = CreateEndpoint("/hello", httpMethods: new string[] { "DELETE" }); + [Fact] // See https://github.com/dotnet/aspnetcore/issues/6415 + public async Task NotMatch_HttpMethod_Returns405Endpoint_ReExecute() + { + // Arrange + var endpoint1 = CreateEndpoint("/hello", httpMethods: new string[] { "GET", "PUT" }); + var endpoint2 = CreateEndpoint("/hello", httpMethods: new string[] { "DELETE" }); - var matcher = CreateMatcher(endpoint1, endpoint2); - var httpContext = CreateContext("/hello", "POST"); + var matcher = CreateMatcher(endpoint1, endpoint2); + var httpContext = CreateContext("/hello", "POST"); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - Assert.NotSame(endpoint1, httpContext.GetEndpoint()); - Assert.NotSame(endpoint2, httpContext.GetEndpoint()); + // Assert + Assert.NotSame(endpoint1, httpContext.GetEndpoint()); + Assert.NotSame(endpoint2, httpContext.GetEndpoint()); - Assert.Same(HttpMethodMatcherPolicy.Http405EndpointDisplayName, httpContext.GetEndpoint().DisplayName); + Assert.Same(HttpMethodMatcherPolicy.Http405EndpointDisplayName, httpContext.GetEndpoint().DisplayName); - // Invoke the endpoint - await httpContext.GetEndpoint().RequestDelegate(httpContext); - Assert.Equal(405, httpContext.Response.StatusCode); - Assert.Equal("DELETE, GET, PUT", httpContext.Response.Headers["Allow"]); + // Invoke the endpoint + await httpContext.GetEndpoint().RequestDelegate(httpContext); + Assert.Equal(405, httpContext.Response.StatusCode); + Assert.Equal("DELETE, GET, PUT", httpContext.Response.Headers["Allow"]); - // Invoke the endpoint again to verify headers not duplicated - await httpContext.GetEndpoint().RequestDelegate(httpContext); - Assert.Equal(405, httpContext.Response.StatusCode); - Assert.Equal("DELETE, GET, PUT", httpContext.Response.Headers["Allow"]); - } + // Invoke the endpoint again to verify headers not duplicated + await httpContext.GetEndpoint().RequestDelegate(httpContext); + Assert.Equal(405, httpContext.Response.StatusCode); + Assert.Equal("DELETE, GET, PUT", httpContext.Response.Headers["Allow"]); + } - private static Matcher CreateMatcher(params RouteEndpoint[] endpoints) + private static Matcher CreateMatcher(params RouteEndpoint[] endpoints) + { + var services = new ServiceCollection() + .AddOptions() + .AddLogging() + .AddRouting() + .BuildServiceProvider(); + + var builder = services.GetRequiredService(); + for (var i = 0; i < endpoints.Length; i++) { - var services = new ServiceCollection() - .AddOptions() - .AddLogging() - .AddRouting() - .BuildServiceProvider(); - - var builder = services.GetRequiredService(); - for (var i = 0; i < endpoints.Length; i++) - { - builder.AddEndpoint(endpoints[i]); - } - - return builder.Build(); + builder.AddEndpoint(endpoints[i]); } - internal static HttpContext CreateContext( - string path, - string httpMethod, - bool corsPreflight = false) - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = corsPreflight ? PreflightHttpMethod : httpMethod; - httpContext.Request.Path = path; - - if (corsPreflight) - { - httpContext.Request.Headers[HeaderNames.Origin] = "example.com"; - httpContext.Request.Headers[HeaderNames.AccessControlRequestMethod] = httpMethod; - } + return builder.Build(); + } - return httpContext; - } + internal static HttpContext CreateContext( + string path, + string httpMethod, + bool corsPreflight = false) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = corsPreflight ? PreflightHttpMethod : httpMethod; + httpContext.Request.Path = path; - internal RouteEndpoint CreateEndpoint( - string template, - object defaults = null, - object constraints = null, - int order = 0, - string[] httpMethods = null, - bool acceptCorsPreflight = false) + if (corsPreflight) { - var metadata = new List(); - if (httpMethods != null) - { - metadata.Add(new HttpMethodMetadata(httpMethods ?? Array.Empty(), acceptCorsPreflight)); - } - - if (HasDynamicMetadata) - { - metadata.Add(new DynamicEndpointMetadata()); - } - - var displayName = "endpoint: " + template + " " + string.Join(", ", httpMethods ?? new[] { "(any)" }); - return new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse(template, defaults, constraints), - order, - new EndpointMetadataCollection(metadata), - displayName); + httpContext.Request.Headers[HeaderNames.Origin] = "example.com"; + httpContext.Request.Headers[HeaderNames.AccessControlRequestMethod] = httpMethod; } - internal (Matcher matcher, RouteEndpoint endpoint) CreateMatcher(string template) + return httpContext; + } + + internal RouteEndpoint CreateEndpoint( + string template, + object defaults = null, + object constraints = null, + int order = 0, + string[] httpMethods = null, + bool acceptCorsPreflight = false) + { + var metadata = new List(); + if (httpMethods != null) { - var endpoint = CreateEndpoint(template); - return (CreateMatcher(endpoint), endpoint); + metadata.Add(new HttpMethodMetadata(httpMethods ?? Array.Empty(), acceptCorsPreflight)); } - private class DynamicEndpointMetadata : IDynamicEndpointMetadata + if (HasDynamicMetadata) { - public bool IsDynamic => true; + metadata.Add(new DynamicEndpointMetadata()); } + + var displayName = "endpoint: " + template + " " + string.Join(", ", httpMethods ?? new[] { "(any)" }); + return new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse(template, defaults, constraints), + order, + new EndpointMetadataCollection(metadata), + displayName); + } + + internal (Matcher matcher, RouteEndpoint endpoint) CreateMatcher(string template) + { + var endpoint = CreateEndpoint(template); + return (CreateMatcher(endpoint), endpoint); + } + + private class DynamicEndpointMetadata : IDynamicEndpointMetadata + { + public bool IsDynamic => true; } } diff --git a/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyTest.cs b/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyTest.cs index b6dbfcdad4..7bbfa4a93b 100644 --- a/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/HttpMethodMatcherPolicyTest.cs @@ -9,158 +9,158 @@ using Microsoft.AspNetCore.Routing.Patterns; using Xunit; using static Microsoft.AspNetCore.Routing.Matching.HttpMethodMatcherPolicy; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class HttpMethodMatcherPolicyTest { - public class HttpMethodMatcherPolicyTest + [Fact] + public void INodeBuilderPolicy_AppliesToNode_EndpointWithoutMetadata_ReturnsFalse() { - [Fact] - public void INodeBuilderPolicy_AppliesToNode_EndpointWithoutMetadata_ReturnsFalse() - { - // Arrange - var endpoints = new[] { CreateEndpoint("/", null), }; + // Arrange + var endpoints = new[] { CreateEndpoint("/", null), }; - var policy = (INodeBuilderPolicy)CreatePolicy(); + var policy = (INodeBuilderPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Fact] - public void INodeBuilderPolicy_AppliesToNode_EndpointWithoutHttpMethods_ReturnsFalse() + [Fact] + public void INodeBuilderPolicy_AppliesToNode_EndpointWithoutHttpMethods_ReturnsFalse() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new HttpMethodMetadata(Array.Empty())), }; - var policy = (INodeBuilderPolicy)CreatePolicy(); + var policy = (INodeBuilderPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Fact] - public void INodeBuilderPolicy_AppliesToNode_EndpointHasHttpMethods_ReturnsTrue() + [Fact] + public void INodeBuilderPolicy_AppliesToNode_EndpointHasHttpMethods_ReturnsTrue() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new HttpMethodMetadata(Array.Empty())), CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })), }; - var policy = (INodeBuilderPolicy)CreatePolicy(); + var policy = (INodeBuilderPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void INodeBuilderPolicy_AppliesToNode_EndpointIsDynamic_ReturnsFalse() + [Fact] + public void INodeBuilderPolicy_AppliesToNode_EndpointIsDynamic_ReturnsFalse() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new HttpMethodMetadata(Array.Empty())), CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", }), new DynamicEndpointMetadata()), }; - var policy = (INodeBuilderPolicy)CreatePolicy(); + var policy = (INodeBuilderPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Fact] - public void IEndpointSelectorPolicy_AppliesToNode_EndpointWithoutMetadata_ReturnsTrue() - { - // Arrange - var endpoints = new[] { CreateEndpoint("/", null, new DynamicEndpointMetadata()), }; + [Fact] + public void IEndpointSelectorPolicy_AppliesToNode_EndpointWithoutMetadata_ReturnsTrue() + { + // Arrange + var endpoints = new[] { CreateEndpoint("/", null, new DynamicEndpointMetadata()), }; - var policy = (IEndpointSelectorPolicy)CreatePolicy(); + var policy = (IEndpointSelectorPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void IEndpointSelectorPolicy_AppliesToNode_EndpointWithoutHttpMethods_ReturnsTrue() + [Fact] + public void IEndpointSelectorPolicy_AppliesToNode_EndpointWithoutHttpMethods_ReturnsTrue() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new HttpMethodMetadata(Array.Empty()), new DynamicEndpointMetadata()), }; - var policy = (IEndpointSelectorPolicy)CreatePolicy(); + var policy = (IEndpointSelectorPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void IEndpointSelectorPolicy_AppliesToNode_EndpointHasHttpMethods_ReturnsTrue() + [Fact] + public void IEndpointSelectorPolicy_AppliesToNode_EndpointHasHttpMethods_ReturnsTrue() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new HttpMethodMetadata(Array.Empty()), new DynamicEndpointMetadata()), CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })), }; - var policy = (IEndpointSelectorPolicy)CreatePolicy(); + var policy = (IEndpointSelectorPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.True(result); - } + // Assert + Assert.True(result); + } - [Fact] - public void IEndpointSelectorPolicy_AppliesToNode_EndpointIsNotDynamic_ReturnsFalse() + [Fact] + public void IEndpointSelectorPolicy_AppliesToNode_EndpointIsNotDynamic_ReturnsFalse() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { CreateEndpoint("/", new HttpMethodMetadata(Array.Empty())), CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })), }; - var policy = (IEndpointSelectorPolicy)CreatePolicy(); + var policy = (IEndpointSelectorPolicy)CreatePolicy(); - // Act - var result = policy.AppliesToEndpoints(endpoints); + // Act + var result = policy.AppliesToEndpoints(endpoints); - // Assert - Assert.False(result); - } + // Assert + Assert.False(result); + } - [Fact] - public void GetEdges_GroupsByHttpMethod() + [Fact] + public void GetEdges_GroupsByHttpMethod() + { + // Arrange + var endpoints = new[] { - // Arrange - var endpoints = new[] - { // These are arrange in an order that we won't actually see in a product scenario. It's done // this way so we can verify that ordering is preserved by GetEdges. CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })), @@ -170,42 +170,42 @@ namespace Microsoft.AspNetCore.Routing.Matching CreateEndpoint("/", new HttpMethodMetadata(Array.Empty())), }; - var policy = CreatePolicy(); - - // Act - var edges = policy.GetEdges(endpoints); - - // Assert - Assert.Collection( - edges.OrderBy(e => e.State), - e => - { - Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State); - Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State); - Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State); - Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State); - Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); - }); - } + var policy = CreatePolicy(); - [Fact] - public void GetEdges_GroupsByHttpMethod_Cors() - { - // Arrange - var endpoints = new[] + // Act + var edges = policy.GetEdges(endpoints); + + // Assert + Assert.Collection( + edges.OrderBy(e => e.State), + e => + { + Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State); + Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State); + Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => { + Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State); + Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State); + Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); + }); + } + + [Fact] + public void GetEdges_GroupsByHttpMethod_Cors() + { + // Arrange + var endpoints = new[] + { // These are arrange in an order that we won't actually see in a product scenario. It's done // this way so we can verify that ordering is preserved by GetEdges. CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })), @@ -215,62 +215,62 @@ namespace Microsoft.AspNetCore.Routing.Matching CreateEndpoint("/", new HttpMethodMetadata(Array.Empty(), acceptCorsPreflight: true)), }; - var policy = CreatePolicy(); - - // Act - var edges = policy.GetEdges(endpoints); - - // Assert - Assert.Collection( - edges.OrderBy(e => e.State), - e => - { - Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State); - Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: true), e.State); - Assert.Equal(new[] { endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State); - Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: true), e.State); - Assert.Equal(new[] { endpoints[2], endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State); - Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: true), e.State); - Assert.Equal(new[] { endpoints[2], endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State); - Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: true), e.State); - Assert.Equal(new[] { endpoints[2], endpoints[4], }, e.Endpoints.ToArray()); - }); - } + var policy = CreatePolicy(); - [Fact] // See explanation in GetEdges for how this case is different - public void GetEdges_GroupsByHttpMethod_CreatesHttp405Endpoint() - { - // Arrange - var endpoints = new[] + // Act + var edges = policy.GetEdges(endpoints); + + // Assert + Assert.Collection( + edges.OrderBy(e => e.State), + e => + { + Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State); + Assert.Equal(new[] { endpoints[1], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: true), e.State); + Assert.Equal(new[] { endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State); + Assert.Equal(new[] { endpoints[0], endpoints[1], endpoints[2], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => { + Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: true), e.State); + Assert.Equal(new[] { endpoints[2], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State); + Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: true), e.State); + Assert.Equal(new[] { endpoints[2], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State); + Assert.Equal(new[] { endpoints[1], endpoints[2], endpoints[3], endpoints[4], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: true), e.State); + Assert.Equal(new[] { endpoints[2], endpoints[4], }, e.Endpoints.ToArray()); + }); + } + + [Fact] // See explanation in GetEdges for how this case is different + public void GetEdges_GroupsByHttpMethod_CreatesHttp405Endpoint() + { + // Arrange + var endpoints = new[] + { // These are arrange in an order that we won't actually see in a product scenario. It's done // this way so we can verify that ordering is preserved by GetEdges. CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })), @@ -278,43 +278,43 @@ namespace Microsoft.AspNetCore.Routing.Matching CreateEndpoint("/", new HttpMethodMetadata(new[] { "PUT", "POST" })), }; - var policy = CreatePolicy(); - - // Act - var edges = policy.GetEdges(endpoints); - - // Assert - Assert.Collection( - edges.OrderBy(e => e.State), - e => - { - Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State); - Assert.Equal(Http405EndpointDisplayName, e.Endpoints.Single().DisplayName); - }, - e => - { - Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State); - Assert.Equal(new[] { endpoints[0], endpoints[1], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State); - Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State); - Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); - }); + var policy = CreatePolicy(); - } + // Act + var edges = policy.GetEdges(endpoints); - [Fact] // See explanation in GetEdges for how this case is different - public void GetEdges_GroupsByHttpMethod_CreatesHttp405Endpoint_CORS() - { - // Arrange - var endpoints = new[] + // Assert + Assert.Collection( + edges.OrderBy(e => e.State), + e => + { + Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State); + Assert.Equal(Http405EndpointDisplayName, e.Endpoints.Single().DisplayName); + }, + e => { + Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State); + Assert.Equal(new[] { endpoints[0], endpoints[1], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State); + Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State); + Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); + }); + + } + + [Fact] // See explanation in GetEdges for how this case is different + public void GetEdges_GroupsByHttpMethod_CreatesHttp405Endpoint_CORS() + { + // Arrange + var endpoints = new[] + { // These are arrange in an order that we won't actually see in a product scenario. It's done // this way so we can verify that ordering is preserved by GetEdges. CreateEndpoint("/", new HttpMethodMetadata(new[] { "GET", })), @@ -322,80 +322,79 @@ namespace Microsoft.AspNetCore.Routing.Matching CreateEndpoint("/", new HttpMethodMetadata(new[] { "PUT", "POST" })), }; - var policy = CreatePolicy(); - - // Act - var edges = policy.GetEdges(endpoints); - - // Assert - Assert.Collection( - edges.OrderBy(e => e.State), - e => - { - Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State); - Assert.Equal(Http405EndpointDisplayName, e.Endpoints.Single().DisplayName); - }, - e => - { - Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State); - Assert.Equal(new[] { endpoints[0], endpoints[1], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: true), e.State); - Assert.Equal(new[] { endpoints[1], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State); - Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: true), e.State); - Assert.Equal(new[] { endpoints[1], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State); - Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); - }, - e => - { - Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: true), e.State); - Assert.Equal(new[] { endpoints[1], }, e.Endpoints.ToArray()); - }); - } + var policy = CreatePolicy(); - private static RouteEndpoint CreateEndpoint(string template, HttpMethodMetadata httpMethodMetadata, params object[] more) - { - var metadata = new List(); - if (httpMethodMetadata != null) - { - metadata.Add(httpMethodMetadata); - } + // Act + var edges = policy.GetEdges(endpoints); - if (more != null) + // Assert + Assert.Collection( + edges.OrderBy(e => e.State), + e => { - metadata.AddRange(more); - } - - return new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse(template), - 0, - new EndpointMetadataCollection(metadata), - $"test: {template}"); - } + Assert.Equal(new EdgeKey(AnyMethod, isCorsPreflightRequest: false), e.State); + Assert.Equal(Http405EndpointDisplayName, e.Endpoints.Single().DisplayName); + }, + e => + { + Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: false), e.State); + Assert.Equal(new[] { endpoints[0], endpoints[1], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal(new EdgeKey("GET", isCorsPreflightRequest: true), e.State); + Assert.Equal(new[] { endpoints[1], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: false), e.State); + Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal(new EdgeKey("POST", isCorsPreflightRequest: true), e.State); + Assert.Equal(new[] { endpoints[1], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: false), e.State); + Assert.Equal(new[] { endpoints[1], endpoints[2], }, e.Endpoints.ToArray()); + }, + e => + { + Assert.Equal(new EdgeKey("PUT", isCorsPreflightRequest: true), e.State); + Assert.Equal(new[] { endpoints[1], }, e.Endpoints.ToArray()); + }); + } - private static HttpMethodMatcherPolicy CreatePolicy() + private static RouteEndpoint CreateEndpoint(string template, HttpMethodMetadata httpMethodMetadata, params object[] more) + { + var metadata = new List(); + if (httpMethodMetadata != null) { - return new HttpMethodMatcherPolicy(); + metadata.Add(httpMethodMetadata); } - private class DynamicEndpointMetadata : IDynamicEndpointMetadata + if (more != null) { - public bool IsDynamic => true; + metadata.AddRange(more); } + + return new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse(template), + 0, + new EndpointMetadataCollection(metadata), + $"test: {template}"); + } + + private static HttpMethodMatcherPolicy CreatePolicy() + { + return new HttpMethodMatcherPolicy(); + } + + private class DynamicEndpointMetadata : IDynamicEndpointMetadata + { + public bool IsDynamic => true; } } diff --git a/src/Http/Routing/test/UnitTests/Matching/ILEmitTrieFactoryTest.cs b/src/Http/Routing/test/UnitTests/Matching/ILEmitTrieFactoryTest.cs index 0b52ef77e9..e0b41d7a90 100644 --- a/src/Http/Routing/test/UnitTests/Matching/ILEmitTrieFactoryTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/ILEmitTrieFactoryTest.cs @@ -4,48 +4,47 @@ using System; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class ILEmitTrieFactoryTest { - public class ILEmitTrieFactoryTest + // We never vectorize on 32bit, so that's part of the test. + [Fact] + public void ShouldVectorize_ReturnsTrue_ForLargeEnoughStrings() { - // We never vectorize on 32bit, so that's part of the test. - [Fact] - public void ShouldVectorize_ReturnsTrue_ForLargeEnoughStrings() - { - // Arrange - var is64Bit = IntPtr.Size == 8; - var expected = is64Bit; + // Arrange + var is64Bit = IntPtr.Size == 8; + var expected = is64Bit; - var entries = new[] - { + var entries = new[] + { ("foo", 0), ("badr", 0), ("", 0), }; - // Act - var actual = ILEmitTrieFactory.ShouldVectorize(entries); + // Act + var actual = ILEmitTrieFactory.ShouldVectorize(entries); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); + } - [Fact] - public void ShouldVectorize_ReturnsFalseForSmallStrings() + [Fact] + public void ShouldVectorize_ReturnsFalseForSmallStrings() + { + // Arrange + var entries = new[] { - // Arrange - var entries = new[] - { ("foo", 0), ("sma", 0), ("", 0), }; - // Act - var actual = ILEmitTrieFactory.ShouldVectorize(entries); + // Act + var actual = ILEmitTrieFactory.ShouldVectorize(entries); - // Assert - Assert.False(actual); - } + // Assert + Assert.False(actual); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/ILEmitTrieJumpTableTest.cs b/src/Http/Routing/test/UnitTests/Matching/ILEmitTrieJumpTableTest.cs index b182730a23..7fff4c814b 100644 --- a/src/Http/Routing/test/UnitTests/Matching/ILEmitTrieJumpTableTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/ILEmitTrieJumpTableTest.cs @@ -1,226 +1,225 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Moq; using System.Threading.Tasks; +using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// We get a lot of good coverage of basics since this implementation is used +// as the default in many cases. The tests here are focused on details of the +// implementation (boundaries, casing, non-ASCII). +public abstract class ILEmitTreeJumpTableTestBase : MultipleEntryJumpTableTest { - // We get a lot of good coverage of basics since this implementation is used - // as the default in many cases. The tests here are focused on details of the - // implementation (boundaries, casing, non-ASCII). - public abstract class ILEmitTreeJumpTableTestBase : MultipleEntryJumpTableTest + public abstract bool Vectorize { get; } + + internal override JumpTable CreateTable( + int defaultDestination, + int exitDestination, + params (string text, int destination)[] entries) + { + var fallback = new DictionaryJumpTable(defaultDestination, exitDestination, entries); + var table = new ILEmitTrieJumpTable(defaultDestination, exitDestination, entries, Vectorize, fallback); + table.InitializeILDelegate(); + return table; + } + + [Fact] // Not calling CreateTable here because we want to test the initialization + public async Task InitializeILDelegateAsync_ReplacesDelegate() + { + // Arrange + var table = new ILEmitTrieJumpTable(0, -1, new[] { ("hi", 1), }, Vectorize, Mock.Of()); + var original = table._getDestination; + + // Act + await table.InitializeILDelegateAsync(); + + // Assert + Assert.NotSame(original, table._getDestination); + } + + // Tests that we can detect non-ASCII characters and use the fallback jump table. + // Testing different indices since that affects which part of the code is running. + // \u007F = lowest non-ASCII character + // \uFFFF = highest non-ASCII character + [Theory] + + // non-ASCII character in first section non-vectorized comparisons + [InlineData("he\u007F", "he\u007Flo-world", 0, 3)] + [InlineData("he\uFFFF", "he\uFFFFlo-world", 0, 3)] + [InlineData("e\u007F", "he\u007Flo-world", 1, 2)] + [InlineData("e\uFFFF", "he\uFFFFlo-world", 1, 2)] + [InlineData("\u007F", "he\u007Flo-world", 2, 1)] + [InlineData("\uFFFF", "he\uFFFFlo-world", 2, 1)] + + // non-ASCII character in first section vectorized comparions + [InlineData("hel\u007F", "hel\u007Fo-world", 0, 4)] + [InlineData("hel\uFFFF", "hel\uFFFFo-world", 0, 4)] + [InlineData("el\u007Fo", "hel\u007Fo-world", 1, 4)] + [InlineData("el\uFFFFo", "hel\uFFFFo-world", 1, 4)] + [InlineData("l\u007Fo-", "hel\u007Fo-world", 2, 4)] + [InlineData("l\uFFFFo-", "hel\uFFFFo-world", 2, 4)] + [InlineData("\u007Fo-w", "hel\u007Fo-world", 3, 4)] + [InlineData("\uFFFFo-w", "hel\uFFFFo-world", 3, 4)] + + // non-ASCII character in second section non-vectorized comparisons + [InlineData("hello-\u007F", "hello-\u007Forld", 0, 7)] + [InlineData("hello-\uFFFF", "hello-\uFFFForld", 0, 7)] + [InlineData("ello-\u007F", "hello-\u007Forld", 1, 6)] + [InlineData("ello-\uFFFF", "hello-\uFFFForld", 1, 6)] + [InlineData("llo-\u007F", "hello-\u007Forld", 2, 5)] + [InlineData("llo-\uFFFF", "hello-\uFFFFForld", 2, 5)] + + // non-ASCII character in first section vectorized comparions + [InlineData("hello-w\u007F", "hello-w\u007Forld", 0, 8)] + [InlineData("hello-w\uFFFF", "hello-w\uFFFForld", 0, 8)] + [InlineData("ello-w\u007Fo", "hello-w\u007Forld", 1, 8)] + [InlineData("ello-w\uFFFFo", "hello-w\uFFFForld", 1, 8)] + [InlineData("llo-w\u007For", "hello-w\u007Forld", 2, 8)] + [InlineData("llo-w\uFFFFor", "hello-w\uFFFForld", 2, 8)] + [InlineData("lo-w\u007Forl", "hello-w\u007Forld", 3, 8)] + [InlineData("lo-w\uFFFForl", "hello-w\uFFFForld", 3, 8)] + public void GetDestination_Found_IncludesNonAsciiCharacters(string entry, string path, int start, int length) + { + // Makes it easy to spot invalid tests + Assert.Equal(entry.Length, length); + Assert.Equal(entry, path.Substring(start, length), ignoreCase: true); + + // Arrange + var table = CreateTable(0, -1, new[] { (entry, 1), }); + + var segment = new PathSegment(start, length); + + // Act + var result = table.GetDestination(path, segment); + + // Assert + Assert.Equal(1, result); + } + + // Tests for difference in casing with ASCII casing rules. Verifies our case + // manipulation algorthm is correct. + // + // We convert from upper case to lower + // 'A' and 'a' are 32 bits apart at the low end + // 'Z' and 'z' are 32 bits apart at the high end + [Theory] + + // character in first section non-vectorized comparisons + [InlineData("heA", "healo-world", 0, 3)] + [InlineData("heZ", "hezlo-world", 0, 3)] + [InlineData("eA", "healo-world", 1, 2)] + [InlineData("eZ", "hezlo-world", 1, 2)] + [InlineData("A", "healo-world", 2, 1)] + [InlineData("Z", "hezlo-world", 2, 1)] + + // character in first section vectorized comparions + [InlineData("helA", "helao-world", 0, 4)] + [InlineData("helZ", "helzo-world", 0, 4)] + [InlineData("elAo", "helao-world", 1, 4)] + [InlineData("elZo", "helzo-world", 1, 4)] + [InlineData("lAo-", "helao-world", 2, 4)] + [InlineData("lZo-", "helzo-world", 2, 4)] + [InlineData("Ao-w", "helao-world", 3, 4)] + [InlineData("Zo-w", "helzo-world", 3, 4)] + + // character in second section non-vectorized comparisons + [InlineData("hello-A", "hello-aorld", 0, 7)] + [InlineData("hello-Z", "hello-zorld", 0, 7)] + [InlineData("ello-A", "hello-aorld", 1, 6)] + [InlineData("ello-Z", "hello-zorld", 1, 6)] + [InlineData("llo-A", "hello-aorld", 2, 5)] + [InlineData("llo-Z", "hello-zForld", 2, 5)] + + // character in first section vectorized comparions + [InlineData("hello-wA", "hello-waorld", 0, 8)] + [InlineData("hello-wZ", "hello-wzorld", 0, 8)] + [InlineData("ello-wAo", "hello-waorld", 1, 8)] + [InlineData("ello-wZo", "hello-wzorld", 1, 8)] + [InlineData("llo-wAor", "hello-waorld", 2, 8)] + [InlineData("llo-wZor", "hello-wzorld", 2, 8)] + [InlineData("lo-wAorl", "hello-waorld", 3, 8)] + [InlineData("lo-wZorl", "hello-wzorld", 3, 8)] + public void GetDestination_Found_IncludesCharactersWithCasingDifference(string entry, string path, int start, int length) { - public abstract bool Vectorize { get; } - - internal override JumpTable CreateTable( - int defaultDestination, - int exitDestination, - params (string text, int destination)[] entries) - { - var fallback = new DictionaryJumpTable(defaultDestination, exitDestination, entries); - var table = new ILEmitTrieJumpTable(defaultDestination, exitDestination, entries, Vectorize, fallback); - table.InitializeILDelegate(); - return table; - } - - [Fact] // Not calling CreateTable here because we want to test the initialization - public async Task InitializeILDelegateAsync_ReplacesDelegate() - { - // Arrange - var table = new ILEmitTrieJumpTable(0, -1, new[] { ("hi", 1), }, Vectorize, Mock.Of()); - var original = table._getDestination; - - // Act - await table.InitializeILDelegateAsync(); - - // Assert - Assert.NotSame(original, table._getDestination); - } - - // Tests that we can detect non-ASCII characters and use the fallback jump table. - // Testing different indices since that affects which part of the code is running. - // \u007F = lowest non-ASCII character - // \uFFFF = highest non-ASCII character - [Theory] - - // non-ASCII character in first section non-vectorized comparisons - [InlineData("he\u007F", "he\u007Flo-world", 0, 3)] - [InlineData("he\uFFFF", "he\uFFFFlo-world", 0, 3)] - [InlineData("e\u007F", "he\u007Flo-world", 1, 2)] - [InlineData("e\uFFFF", "he\uFFFFlo-world", 1, 2)] - [InlineData("\u007F", "he\u007Flo-world", 2, 1)] - [InlineData("\uFFFF", "he\uFFFFlo-world", 2, 1)] - - // non-ASCII character in first section vectorized comparions - [InlineData("hel\u007F", "hel\u007Fo-world", 0, 4)] - [InlineData("hel\uFFFF", "hel\uFFFFo-world", 0, 4)] - [InlineData("el\u007Fo", "hel\u007Fo-world", 1, 4)] - [InlineData("el\uFFFFo", "hel\uFFFFo-world", 1, 4)] - [InlineData("l\u007Fo-", "hel\u007Fo-world", 2, 4)] - [InlineData("l\uFFFFo-", "hel\uFFFFo-world", 2, 4)] - [InlineData("\u007Fo-w", "hel\u007Fo-world", 3, 4)] - [InlineData("\uFFFFo-w", "hel\uFFFFo-world", 3, 4)] - - // non-ASCII character in second section non-vectorized comparisons - [InlineData("hello-\u007F", "hello-\u007Forld", 0, 7)] - [InlineData("hello-\uFFFF", "hello-\uFFFForld", 0, 7)] - [InlineData("ello-\u007F", "hello-\u007Forld", 1, 6)] - [InlineData("ello-\uFFFF", "hello-\uFFFForld", 1, 6)] - [InlineData("llo-\u007F", "hello-\u007Forld", 2, 5)] - [InlineData("llo-\uFFFF", "hello-\uFFFFForld", 2, 5)] - - // non-ASCII character in first section vectorized comparions - [InlineData("hello-w\u007F", "hello-w\u007Forld", 0, 8)] - [InlineData("hello-w\uFFFF", "hello-w\uFFFForld", 0, 8)] - [InlineData("ello-w\u007Fo", "hello-w\u007Forld", 1, 8)] - [InlineData("ello-w\uFFFFo", "hello-w\uFFFForld", 1, 8)] - [InlineData("llo-w\u007For", "hello-w\u007Forld", 2, 8)] - [InlineData("llo-w\uFFFFor", "hello-w\uFFFForld", 2, 8)] - [InlineData("lo-w\u007Forl", "hello-w\u007Forld", 3, 8)] - [InlineData("lo-w\uFFFForl", "hello-w\uFFFForld", 3, 8)] - public void GetDestination_Found_IncludesNonAsciiCharacters(string entry, string path, int start, int length) - { - // Makes it easy to spot invalid tests - Assert.Equal(entry.Length, length); - Assert.Equal(entry, path.Substring(start, length), ignoreCase: true); - - // Arrange - var table = CreateTable(0, -1, new[] { (entry, 1), }); - - var segment = new PathSegment(start, length); - - // Act - var result = table.GetDestination(path, segment); - - // Assert - Assert.Equal(1, result); - } - - // Tests for difference in casing with ASCII casing rules. Verifies our case - // manipulation algorthm is correct. - // - // We convert from upper case to lower - // 'A' and 'a' are 32 bits apart at the low end - // 'Z' and 'z' are 32 bits apart at the high end - [Theory] - - // character in first section non-vectorized comparisons - [InlineData("heA", "healo-world", 0, 3)] - [InlineData("heZ", "hezlo-world", 0, 3)] - [InlineData("eA", "healo-world", 1, 2)] - [InlineData("eZ", "hezlo-world", 1, 2)] - [InlineData("A", "healo-world", 2, 1)] - [InlineData("Z", "hezlo-world", 2, 1)] - - // character in first section vectorized comparions - [InlineData("helA", "helao-world", 0, 4)] - [InlineData("helZ", "helzo-world", 0, 4)] - [InlineData("elAo", "helao-world", 1, 4)] - [InlineData("elZo", "helzo-world", 1, 4)] - [InlineData("lAo-", "helao-world", 2, 4)] - [InlineData("lZo-", "helzo-world", 2, 4)] - [InlineData("Ao-w", "helao-world", 3, 4)] - [InlineData("Zo-w", "helzo-world", 3, 4)] - - // character in second section non-vectorized comparisons - [InlineData("hello-A", "hello-aorld", 0, 7)] - [InlineData("hello-Z", "hello-zorld", 0, 7)] - [InlineData("ello-A", "hello-aorld", 1, 6)] - [InlineData("ello-Z", "hello-zorld", 1, 6)] - [InlineData("llo-A", "hello-aorld", 2, 5)] - [InlineData("llo-Z", "hello-zForld", 2, 5)] - - // character in first section vectorized comparions - [InlineData("hello-wA", "hello-waorld", 0, 8)] - [InlineData("hello-wZ", "hello-wzorld", 0, 8)] - [InlineData("ello-wAo", "hello-waorld", 1, 8)] - [InlineData("ello-wZo", "hello-wzorld", 1, 8)] - [InlineData("llo-wAor", "hello-waorld", 2, 8)] - [InlineData("llo-wZor", "hello-wzorld", 2, 8)] - [InlineData("lo-wAorl", "hello-waorld", 3, 8)] - [InlineData("lo-wZorl", "hello-wzorld", 3, 8)] - public void GetDestination_Found_IncludesCharactersWithCasingDifference(string entry, string path, int start, int length) - { - // Makes it easy to spot invalid tests - Assert.Equal(entry.Length, length); - Assert.Equal(entry, path.Substring(start, length), ignoreCase: true); - - // Arrange - var table = CreateTable(0, -1, new[] { (entry, 1), }); - - var segment = new PathSegment(start, length); - - // Act - var result = table.GetDestination(path, segment); - - // Assert - Assert.Equal(1, result); - } - - // Tests for difference in casing with ASCII casing rules. Verifies our case - // manipulation algorthm is correct. - // - // We convert from upper case to lower - // '@' and '`' are 32 bits apart at the low end - // '[' and '}' are 32 bits apart at the high end - // - // How to understand these tests: - // "an @ should not be converted to a ` since it is out of range" - [Theory] - - // character in first section non-vectorized comparisons - [InlineData("he@", "he`lo-world", 0, 3)] - [InlineData("he[", "he{lo-world", 0, 3)] - [InlineData("e@", "he`lo-world", 1, 2)] - [InlineData("e[", "he{lo-world", 1, 2)] - [InlineData("@", "he`lo-world", 2, 1)] - [InlineData("[", "he{lo-world", 2, 1)] - - // character in first section vectorized comparions - [InlineData("hel@", "hel`o-world", 0, 4)] - [InlineData("hel[", "hel{o-world", 0, 4)] - [InlineData("el@o", "hel`o-world", 1, 4)] - [InlineData("el[o", "hel{o-world", 1, 4)] - [InlineData("l@o-", "hel`o-world", 2, 4)] - [InlineData("l[o-", "hel{o-world", 2, 4)] - [InlineData("@o-w", "hel`o-world", 3, 4)] - [InlineData("[o-w", "hel{o-world", 3, 4)] - - // character in second section non-vectorized comparisons - [InlineData("hello-@", "hello-`orld", 0, 7)] - [InlineData("hello-[", "hello-{orld", 0, 7)] - [InlineData("ello-@", "hello-`orld", 1, 6)] - [InlineData("ello-[", "hello-{orld", 1, 6)] - [InlineData("llo-@", "hello-`orld", 2, 5)] - [InlineData("llo-[", "hello-{Forld", 2, 5)] - - // character in first section vectorized comparions - [InlineData("hello-w@", "hello-w`orld", 0, 8)] - [InlineData("hello-w[", "hello-w{orld", 0, 8)] - [InlineData("ello-w@o", "hello-w`orld", 1, 8)] - [InlineData("ello-w[o", "hello-w{orld", 1, 8)] - [InlineData("llo-w@or", "hello-w`orld", 2, 8)] - [InlineData("llo-w[or", "hello-w{orld", 2, 8)] - [InlineData("lo-w@orl", "hello-w`orld", 3, 8)] - [InlineData("lo-w[orl", "hello-w{orld", 3, 8)] - public void GetDestination_NotFound_IncludesCharactersWithCasingDifference(string entry, string path, int start, int length) - { - // Makes it easy to spot invalid tests - Assert.Equal(entry.Length, length); - Assert.NotEqual(entry, path.Substring(start, length)); - - // Arrange - var table = CreateTable(0, -1, new[] { (entry, 1), }); - - var segment = new PathSegment(start, length); - - // Act - var result = table.GetDestination(path, segment); - - // Assert - Assert.Equal(0, result); - } + // Makes it easy to spot invalid tests + Assert.Equal(entry.Length, length); + Assert.Equal(entry, path.Substring(start, length), ignoreCase: true); + + // Arrange + var table = CreateTable(0, -1, new[] { (entry, 1), }); + + var segment = new PathSegment(start, length); + + // Act + var result = table.GetDestination(path, segment); + + // Assert + Assert.Equal(1, result); + } + + // Tests for difference in casing with ASCII casing rules. Verifies our case + // manipulation algorthm is correct. + // + // We convert from upper case to lower + // '@' and '`' are 32 bits apart at the low end + // '[' and '}' are 32 bits apart at the high end + // + // How to understand these tests: + // "an @ should not be converted to a ` since it is out of range" + [Theory] + + // character in first section non-vectorized comparisons + [InlineData("he@", "he`lo-world", 0, 3)] + [InlineData("he[", "he{lo-world", 0, 3)] + [InlineData("e@", "he`lo-world", 1, 2)] + [InlineData("e[", "he{lo-world", 1, 2)] + [InlineData("@", "he`lo-world", 2, 1)] + [InlineData("[", "he{lo-world", 2, 1)] + + // character in first section vectorized comparions + [InlineData("hel@", "hel`o-world", 0, 4)] + [InlineData("hel[", "hel{o-world", 0, 4)] + [InlineData("el@o", "hel`o-world", 1, 4)] + [InlineData("el[o", "hel{o-world", 1, 4)] + [InlineData("l@o-", "hel`o-world", 2, 4)] + [InlineData("l[o-", "hel{o-world", 2, 4)] + [InlineData("@o-w", "hel`o-world", 3, 4)] + [InlineData("[o-w", "hel{o-world", 3, 4)] + + // character in second section non-vectorized comparisons + [InlineData("hello-@", "hello-`orld", 0, 7)] + [InlineData("hello-[", "hello-{orld", 0, 7)] + [InlineData("ello-@", "hello-`orld", 1, 6)] + [InlineData("ello-[", "hello-{orld", 1, 6)] + [InlineData("llo-@", "hello-`orld", 2, 5)] + [InlineData("llo-[", "hello-{Forld", 2, 5)] + + // character in first section vectorized comparions + [InlineData("hello-w@", "hello-w`orld", 0, 8)] + [InlineData("hello-w[", "hello-w{orld", 0, 8)] + [InlineData("ello-w@o", "hello-w`orld", 1, 8)] + [InlineData("ello-w[o", "hello-w{orld", 1, 8)] + [InlineData("llo-w@or", "hello-w`orld", 2, 8)] + [InlineData("llo-w[or", "hello-w{orld", 2, 8)] + [InlineData("lo-w@orl", "hello-w`orld", 3, 8)] + [InlineData("lo-w[orl", "hello-w{orld", 3, 8)] + public void GetDestination_NotFound_IncludesCharactersWithCasingDifference(string entry, string path, int start, int length) + { + // Makes it easy to spot invalid tests + Assert.Equal(entry.Length, length); + Assert.NotEqual(entry, path.Substring(start, length)); + + // Arrange + var table = CreateTable(0, -1, new[] { (entry, 1), }); + + var segment = new PathSegment(start, length); + + // Act + var result = table.GetDestination(path, segment); + + // Assert + Assert.Equal(0, result); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/LinearSearchJumpTableTest.cs b/src/Http/Routing/test/UnitTests/Matching/LinearSearchJumpTableTest.cs index c1ba75dbb8..ff177bf528 100644 --- a/src/Http/Routing/test/UnitTests/Matching/LinearSearchJumpTableTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/LinearSearchJumpTableTest.cs @@ -2,16 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class LinearSearchJumpTableTest : MultipleEntryJumpTableTest { - public class LinearSearchJumpTableTest : MultipleEntryJumpTableTest + internal override JumpTable CreateTable( + int defaultDestination, + int existDestination, + params (string text, int destination)[] entries) { - internal override JumpTable CreateTable( - int defaultDestination, - int existDestination, - params (string text, int destination)[] entries) - { - return new LinearSearchJumpTable(defaultDestination, existDestination, entries); - } + return new LinearSearchJumpTable(defaultDestination, existDestination, entries); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/MatcherAssert.cs b/src/Http/Routing/test/UnitTests/Matching/MatcherAssert.cs index 35c3ab87e4..670e30e31a 100644 --- a/src/Http/Routing/test/UnitTests/Matching/MatcherAssert.cs +++ b/src/Http/Routing/test/UnitTests/Matching/MatcherAssert.cs @@ -8,106 +8,105 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Xunit.Sdk; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal static class MatcherAssert { - internal static class MatcherAssert + public static void AssertRouteValuesEqual(object expectedValues, RouteValueDictionary actualValues) { - public static void AssertRouteValuesEqual(object expectedValues, RouteValueDictionary actualValues) - { - AssertRouteValuesEqual(new RouteValueDictionary(expectedValues), actualValues); - } + AssertRouteValuesEqual(new RouteValueDictionary(expectedValues), actualValues); + } - public static void AssertRouteValuesEqual(RouteValueDictionary expectedValues, RouteValueDictionary actualValues) + public static void AssertRouteValuesEqual(RouteValueDictionary expectedValues, RouteValueDictionary actualValues) + { + if (expectedValues.Count != actualValues.Count || + !expectedValues.OrderBy(kvp => kvp.Key).SequenceEqual(actualValues.OrderBy(kvp => kvp.Key))) { - if (expectedValues.Count != actualValues.Count || - !expectedValues.OrderBy(kvp => kvp.Key).SequenceEqual(actualValues.OrderBy(kvp => kvp.Key))) - { - throw new XunitException( - $"Expected values:{FormatRouteValues(expectedValues)} Actual values: {FormatRouteValues(actualValues)}."); - } + throw new XunitException( + $"Expected values:{FormatRouteValues(expectedValues)} Actual values: {FormatRouteValues(actualValues)}."); } + } - public static void AssertMatch(HttpContext httpContext, Endpoint expected) - { - AssertMatch(httpContext, expected, new RouteValueDictionary()); - } + public static void AssertMatch(HttpContext httpContext, Endpoint expected) + { + AssertMatch(httpContext, expected, new RouteValueDictionary()); + } - public static void AssertMatch(HttpContext httpContext, Endpoint expected, bool ignoreValues) - { - AssertMatch(httpContext, expected, new RouteValueDictionary(), ignoreValues); - } + public static void AssertMatch(HttpContext httpContext, Endpoint expected, bool ignoreValues) + { + AssertMatch(httpContext, expected, new RouteValueDictionary(), ignoreValues); + } + + public static void AssertMatch(HttpContext httpContext, Endpoint expected, object values) + { + AssertMatch(httpContext, expected, new RouteValueDictionary(values)); + } + + public static void AssertMatch(HttpContext httpContext, Endpoint expected, string[] keys, string[] values) + { + keys = keys ?? Array.Empty(); + values = values ?? Array.Empty(); - public static void AssertMatch(HttpContext httpContext, Endpoint expected, object values) + if (keys.Length != values.Length) { - AssertMatch(httpContext, expected, new RouteValueDictionary(values)); + throw new XunitException("Keys and Values must be the same length."); } - public static void AssertMatch(HttpContext httpContext, Endpoint expected, string[] keys, string[] values) + var zipped = keys.Zip(values, (k, v) => new KeyValuePair(k, v)); + AssertMatch(httpContext, expected, new RouteValueDictionary(zipped)); + } + + public static void AssertMatch( + HttpContext httpContext, + Endpoint expected, + RouteValueDictionary values, + bool ignoreValues = false) + { + if (httpContext.GetEndpoint() == null) { - keys = keys ?? Array.Empty(); - values = values ?? Array.Empty(); + throw new XunitException($"Was expected to match '{expected.DisplayName}' but did not match."); + } - if (keys.Length != values.Length) - { - throw new XunitException("Keys and Values must be the same length."); - } + var actualValues = httpContext.Request.RouteValues; - var zipped = keys.Zip(values, (k, v) => new KeyValuePair(k, v)); - AssertMatch(httpContext, expected, new RouteValueDictionary(zipped)); + if (actualValues == null) + { + throw new XunitException("RouteValues is null."); } - public static void AssertMatch( - HttpContext httpContext, - Endpoint expected, - RouteValueDictionary values, - bool ignoreValues = false) + if (!object.ReferenceEquals(expected, httpContext.GetEndpoint())) { - if (httpContext.GetEndpoint() == null) - { - throw new XunitException($"Was expected to match '{expected.DisplayName}' but did not match."); - } - - var actualValues = httpContext.Request.RouteValues; - - if (actualValues == null) - { - throw new XunitException("RouteValues is null."); - } - - if (!object.ReferenceEquals(expected, httpContext.GetEndpoint())) - { - throw new XunitException( - $"Was expected to match '{expected.DisplayName}' but matched " + - $"'{httpContext.GetEndpoint().DisplayName}' with values: {FormatRouteValues(actualValues)}."); - } - - if (!ignoreValues) - { - // Note: this comparison is intended for unit testing, and is stricter than necessary to make tests - // more precise. Route value comparisons in product code are more flexible than a simple .Equals. - if (values.Count != actualValues.Count || - !values.OrderBy(kvp => kvp.Key).SequenceEqual(actualValues.OrderBy(kvp => kvp.Key))) - { - throw new XunitException( - $"Was expected to match '{expected.DisplayName}' with values {FormatRouteValues(values)} but matched " + - $"values: {FormatRouteValues(actualValues)}."); - } - } + throw new XunitException( + $"Was expected to match '{expected.DisplayName}' but matched " + + $"'{httpContext.GetEndpoint().DisplayName}' with values: {FormatRouteValues(actualValues)}."); } - public static void AssertNotMatch(HttpContext httpContext) + if (!ignoreValues) { - if (httpContext.GetEndpoint() != null) + // Note: this comparison is intended for unit testing, and is stricter than necessary to make tests + // more precise. Route value comparisons in product code are more flexible than a simple .Equals. + if (values.Count != actualValues.Count || + !values.OrderBy(kvp => kvp.Key).SequenceEqual(actualValues.OrderBy(kvp => kvp.Key))) { throw new XunitException( - $"Was expected not to match '{httpContext.GetEndpoint().DisplayName}' " + - $"but matched with values: {FormatRouteValues(httpContext.Request.RouteValues)}."); + $"Was expected to match '{expected.DisplayName}' with values {FormatRouteValues(values)} but matched " + + $"values: {FormatRouteValues(actualValues)}."); } } + } - private static string FormatRouteValues(RouteValueDictionary values) + public static void AssertNotMatch(HttpContext httpContext) + { + if (httpContext.GetEndpoint() != null) { - return values == null ? "{}" : "{" + string.Join(", ", values.Select(kvp => $"{kvp.Key} = '{kvp.Value}'")) + "}"; + throw new XunitException( + $"Was expected not to match '{httpContext.GetEndpoint().DisplayName}' " + + $"but matched with values: {FormatRouteValues(httpContext.Request.RouteValues)}."); } } + + private static string FormatRouteValues(RouteValueDictionary values) + { + return values == null ? "{}" : "{" + string.Join(", ", values.Select(kvp => $"{kvp.Key} = '{kvp.Value}'")) + "}"; + } } diff --git a/src/Http/Routing/test/UnitTests/Matching/MatcherConformanceTest.MultipleEndpoint.cs b/src/Http/Routing/test/UnitTests/Matching/MatcherConformanceTest.MultipleEndpoint.cs index faf86cce41..c8c12fbf32 100644 --- a/src/Http/Routing/test/UnitTests/Matching/MatcherConformanceTest.MultipleEndpoint.cs +++ b/src/Http/Routing/test/UnitTests/Matching/MatcherConformanceTest.MultipleEndpoint.cs @@ -1,9 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public abstract partial class MatcherConformanceTest { - public abstract partial class MatcherConformanceTest - { - } } diff --git a/src/Http/Routing/test/UnitTests/Matching/MatcherConformanceTest.SingleEndpoint.cs b/src/Http/Routing/test/UnitTests/Matching/MatcherConformanceTest.SingleEndpoint.cs index 1813f9f3e6..5bf400dc41 100644 --- a/src/Http/Routing/test/UnitTests/Matching/MatcherConformanceTest.SingleEndpoint.cs +++ b/src/Http/Routing/test/UnitTests/Matching/MatcherConformanceTest.SingleEndpoint.cs @@ -4,326 +4,325 @@ using System.Threading.Tasks; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public abstract partial class MatcherConformanceTest { - public abstract partial class MatcherConformanceTest + [Fact] + public virtual async Task Match_EmptyRoute() + { + // Arrange + var (matcher, endpoint) = CreateMatcher("/"); + var httpContext = CreateContext("/"); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } + + [Fact] + public virtual async Task Match_SingleLiteralSegment() + { + // Arrange + var (matcher, endpoint) = CreateMatcher("/simple"); + var httpContext = CreateContext("/simple"); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } + + [Fact] + public virtual async Task Match_SingleLiteralSegment_TrailingSlash() + { + // Arrange + var (matcher, endpoint) = CreateMatcher("/simple"); + var httpContext = CreateContext("/simple/"); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } + + [Theory] + [InlineData("/simple")] + [InlineData("/sImpLe")] + [InlineData("/SIMPLE")] + public virtual async Task Match_SingleLiteralSegment_CaseInsensitive(string path) + { + // Arrange + var (matcher, endpoint) = CreateMatcher("/Simple"); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } + + // Some matchers will optimize for the ASCII case + [Theory] + [InlineData("/SÏmple", "/SÏmple")] + [InlineData("/ab\uD834\uDD1Ecd", "/ab\uD834\uDD1Ecd")] // surrogate pair + public virtual async Task Match_SingleLiteralSegment_Unicode(string template, string path) + { + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } + + // Matchers should operate on the decoded representation - a matcher that calls + // `httpContext.Request.Path.ToString()` will break this test. + [Theory] + [InlineData("/S%mple", "/S%mple")] + [InlineData("/S\\imple", "/S\\imple")] // surrogate pair + public virtual async Task Match_SingleLiteralSegment_PercentEncoded(string template, string path) + { + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } + + [Theory] + [InlineData("/")] + [InlineData("/imple")] + [InlineData("/siple")] + [InlineData("/simple1")] + [InlineData("/simple/not-simple")] + [InlineData("/simple/a/b/c")] + public virtual async Task NotMatch_SingleLiteralSegment(string path) + { + // Arrange + var (matcher, endpoint) = CreateMatcher("/simple"); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertNotMatch(httpContext); + } + + [Theory] + [InlineData("simple")] + [InlineData("/simple")] + [InlineData("~/simple")] + public virtual async Task Match_Sanitizies_Template(string template) + { + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext("/simple"); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } + + // Matchers do their own 'splitting' of the path into segments, so including + // some extra variation here + [Theory] + [InlineData("/a/b", "/a/b")] + [InlineData("/a/b", "/A/B")] + [InlineData("/a/b", "/a/b/")] + [InlineData("/a/b/c", "/a/b/c")] + [InlineData("/a/b/c", "/a/b/c/")] + [InlineData("/a/b/c/d", "/a/b/c/d")] + [InlineData("/a/b/c/d", "/a/b/c/d/")] + public virtual async Task Match_MultipleLiteralSegments(string template, string path) { - [Fact] - public virtual async Task Match_EmptyRoute() - { - // Arrange - var (matcher, endpoint) = CreateMatcher("/"); - var httpContext = CreateContext("/"); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } - - [Fact] - public virtual async Task Match_SingleLiteralSegment() - { - // Arrange - var (matcher, endpoint) = CreateMatcher("/simple"); - var httpContext = CreateContext("/simple"); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } - - [Fact] - public virtual async Task Match_SingleLiteralSegment_TrailingSlash() - { - // Arrange - var (matcher, endpoint) = CreateMatcher("/simple"); - var httpContext = CreateContext("/simple/"); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } - - [Theory] - [InlineData("/simple")] - [InlineData("/sImpLe")] - [InlineData("/SIMPLE")] - public virtual async Task Match_SingleLiteralSegment_CaseInsensitive(string path) - { - // Arrange - var (matcher, endpoint) = CreateMatcher("/Simple"); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } - - // Some matchers will optimize for the ASCII case - [Theory] - [InlineData("/SÏmple", "/SÏmple")] - [InlineData("/ab\uD834\uDD1Ecd", "/ab\uD834\uDD1Ecd")] // surrogate pair - public virtual async Task Match_SingleLiteralSegment_Unicode(string template, string path) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } - - // Matchers should operate on the decoded representation - a matcher that calls - // `httpContext.Request.Path.ToString()` will break this test. - [Theory] - [InlineData("/S%mple", "/S%mple")] - [InlineData("/S\\imple", "/S\\imple")] // surrogate pair - public virtual async Task Match_SingleLiteralSegment_PercentEncoded(string template, string path) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } - - [Theory] - [InlineData("/")] - [InlineData("/imple")] - [InlineData("/siple")] - [InlineData("/simple1")] - [InlineData("/simple/not-simple")] - [InlineData("/simple/a/b/c")] - public virtual async Task NotMatch_SingleLiteralSegment(string path) - { - // Arrange - var (matcher, endpoint) = CreateMatcher("/simple"); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertNotMatch(httpContext); - } - - [Theory] - [InlineData("simple")] - [InlineData("/simple")] - [InlineData("~/simple")] - public virtual async Task Match_Sanitizies_Template(string template) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext("/simple"); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } - - // Matchers do their own 'splitting' of the path into segments, so including - // some extra variation here - [Theory] - [InlineData("/a/b", "/a/b")] - [InlineData("/a/b", "/A/B")] - [InlineData("/a/b", "/a/b/")] - [InlineData("/a/b/c", "/a/b/c")] - [InlineData("/a/b/c", "/a/b/c/")] - [InlineData("/a/b/c/d", "/a/b/c/d")] - [InlineData("/a/b/c/d", "/a/b/c/d/")] - public virtual async Task Match_MultipleLiteralSegments(string template, string path) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint); - } - - // Matchers do their own 'splitting' of the path into segments, so including - // some extra variation here - [Theory] - [InlineData("/a/b", "/")] - [InlineData("/a/b", "/a")] - [InlineData("/a/b", "/a/")] - [InlineData("/a/b", "/a//")] - [InlineData("/a/b", "/aa/")] - [InlineData("/a/b", "/a/bb")] - [InlineData("/a/b", "/a/bb/")] - [InlineData("/a/b/c", "/aa/b/c")] - [InlineData("/a/b/c", "/a/bb/c/")] - [InlineData("/a/b/c", "/a/b/cab")] - [InlineData("/a/b/c", "/d/b/c/")] - [InlineData("/a/b/c", "//b/c")] - [InlineData("/a/b/c", "/a/b//")] - [InlineData("/a/b/c", "/a/b/c/d")] - [InlineData("/a/b/c", "/a/b/c/d/e")] - public virtual async Task NotMatch_MultipleLiteralSegments(string template, string path) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertNotMatch(httpContext); - } - - [Fact] - public virtual async Task Match_SingleParameter() - { - // Arrange - var (matcher, endpoint) = CreateMatcher("/{p}"); - var httpContext = CreateContext("/14"); - var values = new RouteValueDictionary(new { p = "14", }); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint, values); - } - - [Fact] - public virtual async Task Match_Constraint() - { - // Arrange - var (matcher, endpoint) = CreateMatcher("/{p:int}"); - var httpContext = CreateContext("/14"); - var values = new RouteValueDictionary(new { p = "14", }); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint, values); - } - - [Fact] - public virtual async Task Match_SingleParameter_TrailingSlash() - { - // Arrange - var (matcher, endpoint) = CreateMatcher("/{p}"); - var httpContext = CreateContext("/14/"); - var values = new RouteValueDictionary(new { p = "14", }); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint, values); - } - - [Fact] - public virtual async Task Match_SingleParameter_WeirdNames() - { - // Arrange - var (matcher, endpoint) = CreateMatcher("/foo/{ }/{.!$%}/{dynamic.data}"); - var httpContext = CreateContext("/foo/space/weirdmatch/matcherid"); - var values = new RouteValueDictionary() + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint); + } + + // Matchers do their own 'splitting' of the path into segments, so including + // some extra variation here + [Theory] + [InlineData("/a/b", "/")] + [InlineData("/a/b", "/a")] + [InlineData("/a/b", "/a/")] + [InlineData("/a/b", "/a//")] + [InlineData("/a/b", "/aa/")] + [InlineData("/a/b", "/a/bb")] + [InlineData("/a/b", "/a/bb/")] + [InlineData("/a/b/c", "/aa/b/c")] + [InlineData("/a/b/c", "/a/bb/c/")] + [InlineData("/a/b/c", "/a/b/cab")] + [InlineData("/a/b/c", "/d/b/c/")] + [InlineData("/a/b/c", "//b/c")] + [InlineData("/a/b/c", "/a/b//")] + [InlineData("/a/b/c", "/a/b/c/d")] + [InlineData("/a/b/c", "/a/b/c/d/e")] + public virtual async Task NotMatch_MultipleLiteralSegments(string template, string path) + { + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertNotMatch(httpContext); + } + + [Fact] + public virtual async Task Match_SingleParameter() + { + // Arrange + var (matcher, endpoint) = CreateMatcher("/{p}"); + var httpContext = CreateContext("/14"); + var values = new RouteValueDictionary(new { p = "14", }); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint, values); + } + + [Fact] + public virtual async Task Match_Constraint() + { + // Arrange + var (matcher, endpoint) = CreateMatcher("/{p:int}"); + var httpContext = CreateContext("/14"); + var values = new RouteValueDictionary(new { p = "14", }); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint, values); + } + + [Fact] + public virtual async Task Match_SingleParameter_TrailingSlash() + { + // Arrange + var (matcher, endpoint) = CreateMatcher("/{p}"); + var httpContext = CreateContext("/14/"); + var values = new RouteValueDictionary(new { p = "14", }); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint, values); + } + + [Fact] + public virtual async Task Match_SingleParameter_WeirdNames() + { + // Arrange + var (matcher, endpoint) = CreateMatcher("/foo/{ }/{.!$%}/{dynamic.data}"); + var httpContext = CreateContext("/foo/space/weirdmatch/matcherid"); + var values = new RouteValueDictionary() { { " ", "space" }, { ".!$%", "weirdmatch" }, { "dynamic.data", "matcherid" }, }; - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint, values); - } - - [Theory] - [InlineData("/")] - [InlineData("/a/b")] - [InlineData("/a/b/c")] - [InlineData("//")] - public virtual async Task NotMatch_SingleParameter(string path) - { - // Arrange - var (matcher, endpoint) = CreateMatcher("/{p}"); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertNotMatch(httpContext); - } - - [Theory] - [InlineData("/{a}/b", "/54/b", new string[] { "a", }, new string[] { "54", })] - [InlineData("/{a}/b", "/54/b/", new string[] { "a", }, new string[] { "54", })] - [InlineData("/{a}/{b}", "/54/73", new string[] { "a", "b" }, new string[] { "54", "73", })] - [InlineData("/a/{b}/c", "/a/b/c", new string[] { "b", }, new string[] { "b", })] - [InlineData("/a/{b}/c/", "/a/b/c", new string[] { "b", }, new string[] { "b", })] - [InlineData("/{a}/b/{c}", "/54/b/c", new string[] { "a", "c", }, new string[] { "54", "c", })] - [InlineData("/{a}/{b}/{c}", "/54/b/c", new string[] { "a", "b", "c", }, new string[] { "54", "b", "c", })] - public virtual async Task Match_MultipleParameters(string template, string path, string[] keys, string[] values) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertMatch(httpContext, endpoint, keys, values); - } - - [Theory] - [InlineData("/{a}/b", "/54/bb")] - [InlineData("/{a}/b", "/54/b/17")] - [InlineData("/{a}/b", "/54/b//")] - [InlineData("/{a}/{b}", "//73")] - [InlineData("/{a}/{b}", "/54//")] - [InlineData("/{a}/{b}", "/54/73/18")] - [InlineData("/a/{b}/c", "/aa/b/c")] - [InlineData("/a/{b}/c", "/a/b/cc")] - [InlineData("/a/{b}/c", "/a/b/c/d")] - [InlineData("/{a}/b/{c}", "/54/bb/c")] - [InlineData("/{a}/{b}/{c}", "/54/b/c/d")] - [InlineData("/{a}/{b}/{c}", "/54/b/c//")] - [InlineData("/{a}/{b}/{c}", "//b/c/")] - [InlineData("/{a}/{b}/{c}", "/54//c/")] - [InlineData("/{a}/{b}/{c}", "/54/b//")] - public virtual async Task NotMatch_MultipleParameters(string template, string path) - { - // Arrange - var (matcher, endpoint) = CreateMatcher(template); - var httpContext = CreateContext(path); - - // Act - await matcher.MatchAsync(httpContext); - - // Assert - MatcherAssert.AssertNotMatch(httpContext); - } + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint, values); + } + + [Theory] + [InlineData("/")] + [InlineData("/a/b")] + [InlineData("/a/b/c")] + [InlineData("//")] + public virtual async Task NotMatch_SingleParameter(string path) + { + // Arrange + var (matcher, endpoint) = CreateMatcher("/{p}"); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertNotMatch(httpContext); + } + + [Theory] + [InlineData("/{a}/b", "/54/b", new string[] { "a", }, new string[] { "54", })] + [InlineData("/{a}/b", "/54/b/", new string[] { "a", }, new string[] { "54", })] + [InlineData("/{a}/{b}", "/54/73", new string[] { "a", "b" }, new string[] { "54", "73", })] + [InlineData("/a/{b}/c", "/a/b/c", new string[] { "b", }, new string[] { "b", })] + [InlineData("/a/{b}/c/", "/a/b/c", new string[] { "b", }, new string[] { "b", })] + [InlineData("/{a}/b/{c}", "/54/b/c", new string[] { "a", "c", }, new string[] { "54", "c", })] + [InlineData("/{a}/{b}/{c}", "/54/b/c", new string[] { "a", "b", "c", }, new string[] { "54", "b", "c", })] + public virtual async Task Match_MultipleParameters(string template, string path, string[] keys, string[] values) + { + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertMatch(httpContext, endpoint, keys, values); + } + + [Theory] + [InlineData("/{a}/b", "/54/bb")] + [InlineData("/{a}/b", "/54/b/17")] + [InlineData("/{a}/b", "/54/b//")] + [InlineData("/{a}/{b}", "//73")] + [InlineData("/{a}/{b}", "/54//")] + [InlineData("/{a}/{b}", "/54/73/18")] + [InlineData("/a/{b}/c", "/aa/b/c")] + [InlineData("/a/{b}/c", "/a/b/cc")] + [InlineData("/a/{b}/c", "/a/b/c/d")] + [InlineData("/{a}/b/{c}", "/54/bb/c")] + [InlineData("/{a}/{b}/{c}", "/54/b/c/d")] + [InlineData("/{a}/{b}/{c}", "/54/b/c//")] + [InlineData("/{a}/{b}/{c}", "//b/c/")] + [InlineData("/{a}/{b}/{c}", "/54//c/")] + [InlineData("/{a}/{b}/{c}", "/54/b//")] + public virtual async Task NotMatch_MultipleParameters(string template, string path) + { + // Arrange + var (matcher, endpoint) = CreateMatcher(template); + var httpContext = CreateContext(path); + + // Act + await matcher.MatchAsync(httpContext); + + // Assert + MatcherAssert.AssertNotMatch(httpContext); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/MatcherConformanceTest.cs b/src/Http/Routing/test/UnitTests/Matching/MatcherConformanceTest.cs index 0eebfe66e6..293ef2fcb6 100644 --- a/src/Http/Routing/test/UnitTests/Matching/MatcherConformanceTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/MatcherConformanceTest.cs @@ -8,47 +8,46 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public abstract partial class MatcherConformanceTest { - public abstract partial class MatcherConformanceTest - { - internal abstract Matcher CreateMatcher(params RouteEndpoint[] endpoints); + internal abstract Matcher CreateMatcher(params RouteEndpoint[] endpoints); - internal static HttpContext CreateContext(string path) - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "TEST"; - httpContext.Request.Path = path; - httpContext.RequestServices = CreateServices(); - return httpContext; - } + internal static HttpContext CreateContext(string path) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "TEST"; + httpContext.Request.Path = path; + httpContext.RequestServices = CreateServices(); + return httpContext; + } - // The older routing implementations retrieve services when they first execute. - internal static IServiceProvider CreateServices() - { - var services = new ServiceCollection(); - services.AddLogging(); - return services.BuildServiceProvider(); - } + // The older routing implementations retrieve services when they first execute. + internal static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddLogging(); + return services.BuildServiceProvider(); + } - internal static RouteEndpoint CreateEndpoint( - string template, - object defaults = null, - object constraints = null, - int? order = null) - { - return new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse(template, defaults, constraints), - order ?? 0, - EndpointMetadataCollection.Empty, - "endpoint: " + template); - } + internal static RouteEndpoint CreateEndpoint( + string template, + object defaults = null, + object constraints = null, + int? order = null) + { + return new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse(template, defaults, constraints), + order ?? 0, + EndpointMetadataCollection.Empty, + "endpoint: " + template); + } - internal (Matcher matcher, RouteEndpoint endpoint) CreateMatcher(string template) - { - var endpoint = CreateEndpoint(template); - return (CreateMatcher(endpoint), endpoint); - } + internal (Matcher matcher, RouteEndpoint endpoint) CreateMatcher(string template) + { + var endpoint = CreateEndpoint(template); + return (CreateMatcher(endpoint), endpoint); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/MultipleEntryJumpTableTest.cs b/src/Http/Routing/test/UnitTests/Matching/MultipleEntryJumpTableTest.cs index bec41c3f05..92736dd515 100644 --- a/src/Http/Routing/test/UnitTests/Matching/MultipleEntryJumpTableTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/MultipleEntryJumpTableTest.cs @@ -3,78 +3,77 @@ using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public abstract class MultipleEntryJumpTableTest { - public abstract class MultipleEntryJumpTableTest + internal abstract JumpTable CreateTable( + int defaultDestination, + int exitDestination, + params (string text, int destination)[] entries); + + [Fact] + public void GetDestination_ZeroLengthSegment_JumpsToExit() + { + // Arrange + var table = CreateTable(0, 1, ("text", 2)); + + // Act + var result = table.GetDestination("ignored", new PathSegment(0, 0)); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void GetDestination_NonMatchingSegment_JumpsToDefault() + { + // Arrange + var table = CreateTable(0, 1, ("text", 2)); + + // Act + var result = table.GetDestination("text", new PathSegment(1, 2)); + + // Assert + Assert.Equal(0, result); + } + + [Fact] + public void GetDestination_SegmentMatchingText_JumpsToDestination() { - internal abstract JumpTable CreateTable( - int defaultDestination, - int exitDestination, - params (string text, int destination)[] entries); - - [Fact] - public void GetDestination_ZeroLengthSegment_JumpsToExit() - { - // Arrange - var table = CreateTable(0, 1, ("text", 2)); - - // Act - var result = table.GetDestination("ignored", new PathSegment(0, 0)); - - // Assert - Assert.Equal(1, result); - } - - [Fact] - public void GetDestination_NonMatchingSegment_JumpsToDefault() - { - // Arrange - var table = CreateTable(0, 1, ("text", 2)); - - // Act - var result = table.GetDestination("text", new PathSegment(1, 2)); - - // Assert - Assert.Equal(0, result); - } - - [Fact] - public void GetDestination_SegmentMatchingText_JumpsToDestination() - { - // Arrange - var table = CreateTable(0, 1, ("text", 2)); - - // Act - var result = table.GetDestination("some-text", new PathSegment(5, 4)); - - // Assert - Assert.Equal(2, result); - } - - [Fact] - public void GetDestination_SegmentMatchingTextIgnoreCase_JumpsToDestination() - { - // Arrange - var table = CreateTable(0, 1, ("text", 2)); - - // Act - var result = table.GetDestination("some-tExt", new PathSegment(5, 4)); - - // Assert - Assert.Equal(2, result); - } - - [Fact] - public void GetDestination_SegmentMatchingTextIgnoreCase_MultipleEntries() - { - // Arrange - var table = CreateTable(0, 1, ("tezt", 2), ("text", 3)); - - // Act - var result = table.GetDestination("some-tExt", new PathSegment(5, 4)); - - // Assert - Assert.Equal(3, result); - } + // Arrange + var table = CreateTable(0, 1, ("text", 2)); + + // Act + var result = table.GetDestination("some-text", new PathSegment(5, 4)); + + // Assert + Assert.Equal(2, result); + } + + [Fact] + public void GetDestination_SegmentMatchingTextIgnoreCase_JumpsToDestination() + { + // Arrange + var table = CreateTable(0, 1, ("text", 2)); + + // Act + var result = table.GetDestination("some-tExt", new PathSegment(5, 4)); + + // Assert + Assert.Equal(2, result); + } + + [Fact] + public void GetDestination_SegmentMatchingTextIgnoreCase_MultipleEntries() + { + // Arrange + var table = CreateTable(0, 1, ("tezt", 2), ("text", 3)); + + // Act + var result = table.GetDestination("some-tExt", new PathSegment(5, 4)); + + // Assert + Assert.Equal(3, result); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/NonVectorizedILEmitTrieJumpTableTest.cs b/src/Http/Routing/test/UnitTests/Matching/NonVectorizedILEmitTrieJumpTableTest.cs index c90b67dfc4..f0b991426a 100644 --- a/src/Http/Routing/test/UnitTests/Matching/NonVectorizedILEmitTrieJumpTableTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/NonVectorizedILEmitTrieJumpTableTest.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class NonVectorizedILEmitTrieJumpTableTest : ILEmitTreeJumpTableTestBase { - public class NonVectorizedILEmitTrieJumpTableTest : ILEmitTreeJumpTableTestBase - { - public override bool Vectorize => false; - } + public override bool Vectorize => false; } diff --git a/src/Http/Routing/test/UnitTests/Matching/RouteMatcher.cs b/src/Http/Routing/test/UnitTests/Matching/RouteMatcher.cs index 474551f4a8..67fb3451d9 100644 --- a/src/Http/Routing/test/UnitTests/Matching/RouteMatcher.cs +++ b/src/Http/Routing/test/UnitTests/Matching/RouteMatcher.cs @@ -5,33 +5,32 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// This is an adapter to use Route in the conformance tests +internal class RouteMatcher : Matcher { - // This is an adapter to use Route in the conformance tests - internal class RouteMatcher : Matcher + private readonly RouteCollection _inner; + + internal RouteMatcher(RouteCollection inner) { - private readonly RouteCollection _inner; + _inner = inner; + } - internal RouteMatcher(RouteCollection inner) + public override async Task MatchAsync(HttpContext httpContext) + { + if (httpContext == null) { - _inner = inner; + throw new ArgumentNullException(nameof(httpContext)); } - public override async Task MatchAsync(HttpContext httpContext) + var routeContext = new RouteContext(httpContext); + await _inner.RouteAsync(routeContext); + + if (routeContext.Handler != null) { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - var routeContext = new RouteContext(httpContext); - await _inner.RouteAsync(routeContext); - - if (routeContext.Handler != null) - { - httpContext.Request.RouteValues = routeContext.RouteData.Values; - await routeContext.Handler(httpContext); - } + httpContext.Request.RouteValues = routeContext.RouteData.Values; + await routeContext.Handler(httpContext); } } } diff --git a/src/Http/Routing/test/UnitTests/Matching/RouteMatcherBuilder.cs b/src/Http/Routing/test/UnitTests/Matching/RouteMatcherBuilder.cs index ac23e13bf4..93cd89a3cf 100644 --- a/src/Http/Routing/test/UnitTests/Matching/RouteMatcherBuilder.cs +++ b/src/Http/Routing/test/UnitTests/Matching/RouteMatcherBuilder.cs @@ -11,99 +11,98 @@ using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.TestObjects; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal class RouteMatcherBuilder : MatcherBuilder { - internal class RouteMatcherBuilder : MatcherBuilder - { - private readonly IInlineConstraintResolver _constraintResolver; - private readonly List _endpoints; + private readonly IInlineConstraintResolver _constraintResolver; + private readonly List _endpoints; - public RouteMatcherBuilder() - { - _constraintResolver = new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()), new TestServiceProvider()); - _endpoints = new List(); - } + public RouteMatcherBuilder() + { + _constraintResolver = new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()), new TestServiceProvider()); + _endpoints = new List(); + } - public override void AddEndpoint(RouteEndpoint endpoint) - { - _endpoints.Add(endpoint); - } + public override void AddEndpoint(RouteEndpoint endpoint) + { + _endpoints.Add(endpoint); + } - public override Matcher Build() - { - var selector = new DefaultEndpointSelector(); + public override Matcher Build() + { + var selector = new DefaultEndpointSelector(); - var groups = _endpoints - .GroupBy(e => (e.Order, e.RoutePattern.InboundPrecedence, e.RoutePattern.RawText)) - .OrderBy(g => g.Key.Order) - .ThenBy(g => g.Key.InboundPrecedence); + var groups = _endpoints + .GroupBy(e => (e.Order, e.RoutePattern.InboundPrecedence, e.RoutePattern.RawText)) + .OrderBy(g => g.Key.Order) + .ThenBy(g => g.Key.InboundPrecedence); - var routes = new RouteCollection(); + var routes = new RouteCollection(); - foreach (var group in groups) + foreach (var group in groups) + { + var candidates = group.ToArray(); + var endpoint = group.First(); + + // RoutePattern.Defaults contains the default values parsed from the template + // as well as those specified with a literal. We need to separate those + // for legacy cases. + // + // To do this we re-parse the original text and compare. + var withoutDefaults = RoutePatternFactory.Parse(endpoint.RoutePattern.RawText); + var defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults); + for (var i = 0; i < withoutDefaults.Parameters.Count; i++) { - var candidates = group.ToArray(); - var endpoint = group.First(); - - // RoutePattern.Defaults contains the default values parsed from the template - // as well as those specified with a literal. We need to separate those - // for legacy cases. - // - // To do this we re-parse the original text and compare. - var withoutDefaults = RoutePatternFactory.Parse(endpoint.RoutePattern.RawText); - var defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults); - for (var i = 0; i < withoutDefaults.Parameters.Count; i++) + var parameter = withoutDefaults.Parameters[i]; + if (parameter.Default != null) { - var parameter = withoutDefaults.Parameters[i]; - if (parameter.Default != null) - { - defaults.Remove(parameter.Name); - } + defaults.Remove(parameter.Name); } - - routes.Add(new Route( - new SelectorRouter(selector, candidates), - endpoint.RoutePattern.RawText, - defaults, - new Dictionary(), - new RouteValueDictionary(), - _constraintResolver)); } - return new RouteMatcher(routes); + routes.Add(new Route( + new SelectorRouter(selector, candidates), + endpoint.RoutePattern.RawText, + defaults, + new Dictionary(), + new RouteValueDictionary(), + _constraintResolver)); } - private class SelectorRouter : IRouter + return new RouteMatcher(routes); + } + + private class SelectorRouter : IRouter + { + private readonly EndpointSelector _selector; + private readonly RouteEndpoint[] _candidates; + private readonly RouteValueDictionary[] _values; + private readonly int[] _scores; + + public SelectorRouter(EndpointSelector selector, RouteEndpoint[] candidates) { - private readonly EndpointSelector _selector; - private readonly RouteEndpoint[] _candidates; - private readonly RouteValueDictionary[] _values; - private readonly int[] _scores; + _selector = selector; + _candidates = candidates; - public SelectorRouter(EndpointSelector selector, RouteEndpoint[] candidates) - { - _selector = selector; - _candidates = candidates; + _values = new RouteValueDictionary[_candidates.Length]; + _scores = new int[_candidates.Length]; + } - _values = new RouteValueDictionary[_candidates.Length]; - _scores = new int[_candidates.Length]; - } + public VirtualPathData GetVirtualPath(VirtualPathContext context) + { + throw new NotImplementedException(); + } - public VirtualPathData GetVirtualPath(VirtualPathContext context) - { - throw new NotImplementedException(); - } + public async Task RouteAsync(RouteContext routeContext) + { + // This is needed due to a quirk of our tests - they reuse the endpoint feature. + routeContext.HttpContext.SetEndpoint(null); - public async Task RouteAsync(RouteContext routeContext) + await _selector.SelectAsync(routeContext.HttpContext, new CandidateSet(_candidates, _values, _scores)); + if (routeContext.HttpContext.GetEndpoint() != null) { - // This is needed due to a quirk of our tests - they reuse the endpoint feature. - routeContext.HttpContext.SetEndpoint(null); - - await _selector.SelectAsync(routeContext.HttpContext, new CandidateSet(_candidates, _values, _scores)); - if (routeContext.HttpContext.GetEndpoint() != null) - { - routeContext.Handler = (_) => Task.CompletedTask; - } + routeContext.Handler = (_) => Task.CompletedTask; } } } diff --git a/src/Http/Routing/test/UnitTests/Matching/RouteMatcherConformanceTest.cs b/src/Http/Routing/test/UnitTests/Matching/RouteMatcherConformanceTest.cs index 4b7b3a7ac2..010080be46 100644 --- a/src/Http/Routing/test/UnitTests/Matching/RouteMatcherConformanceTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/RouteMatcherConformanceTest.cs @@ -4,21 +4,21 @@ using System.Threading.Tasks; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class RouteMatcherConformanceTest : FullFeaturedMatcherConformanceTest { - public class RouteMatcherConformanceTest : FullFeaturedMatcherConformanceTest + // https://github.com/dotnet/aspnetcore/issues/18677 + // + [Theory] + [InlineData("/middleware", 1)] + [InlineData("/middleware/test", 1)] + [InlineData("/middleware/test1/test2", 1)] + [InlineData("/bill/boga", 0)] + public async Task Match_Regression_1867(string path, int endpointIndex) { - // https://github.com/dotnet/aspnetcore/issues/18677 - // - [Theory] - [InlineData("/middleware", 1)] - [InlineData("/middleware/test", 1)] - [InlineData("/middleware/test1/test2", 1)] - [InlineData("/bill/boga", 0)] - public async Task Match_Regression_1867(string path, int endpointIndex) + var endpoints = new RouteEndpoint[] { - var endpoints = new RouteEndpoint[] - { EndpointFactory.CreateRouteEndpoint( "{firstName}/{lastName}", order: 0, @@ -27,28 +27,27 @@ namespace Microsoft.AspNetCore.Routing.Matching EndpointFactory.CreateRouteEndpoint( "middleware/{**_}", order: 0), - }; + }; - var expected = endpoints[endpointIndex]; + var expected = endpoints[endpointIndex]; - var matcher = CreateMatcher(endpoints); - var httpContext = CreateContext(path); + var matcher = CreateMatcher(endpoints); + var httpContext = CreateContext(path); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true); - } + // Assert + MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true); + } - internal override Matcher CreateMatcher(params RouteEndpoint[] endpoints) + internal override Matcher CreateMatcher(params RouteEndpoint[] endpoints) + { + var builder = new RouteMatcherBuilder(); + for (var i = 0; i < endpoints.Length; i++) { - var builder = new RouteMatcherBuilder(); - for (var i = 0; i < endpoints.Length; i++) - { - builder.AddEndpoint(endpoints[i]); - } - return builder.Build(); + builder.AddEndpoint(endpoints[i]); } + return builder.Build(); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/SingleEntryAsciiJumpTableTest.cs b/src/Http/Routing/test/UnitTests/Matching/SingleEntryAsciiJumpTableTest.cs index b8ad193fa6..2bfea66a51 100644 --- a/src/Http/Routing/test/UnitTests/Matching/SingleEntryAsciiJumpTableTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/SingleEntryAsciiJumpTableTest.cs @@ -1,13 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class SingleEntryAsciiJumpTableTest : SingleEntryJumpTableTestBase { - public class SingleEntryAsciiJumpTableTest : SingleEntryJumpTableTestBase + private protected override JumpTable CreateJumpTable(int defaultDestination, int exitDestination, string text, int destination) { - private protected override JumpTable CreateJumpTable(int defaultDestination, int exitDestination, string text, int destination) - { - return new SingleEntryAsciiJumpTable(defaultDestination, exitDestination, text, destination); - } + return new SingleEntryAsciiJumpTable(defaultDestination, exitDestination, text, destination); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/SingleEntryJumpTableTest.cs b/src/Http/Routing/test/UnitTests/Matching/SingleEntryJumpTableTest.cs index 78b50d120a..08da6fba03 100644 --- a/src/Http/Routing/test/UnitTests/Matching/SingleEntryJumpTableTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/SingleEntryJumpTableTest.cs @@ -1,13 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class SingleEntryJumpTableTest : SingleEntryJumpTableTestBase { - public class SingleEntryJumpTableTest : SingleEntryJumpTableTestBase + private protected override JumpTable CreateJumpTable(int defaultDestination, int exitDestination, string text, int destination) { - private protected override JumpTable CreateJumpTable(int defaultDestination, int exitDestination, string text, int destination) - { - return new SingleEntryJumpTable(defaultDestination, exitDestination, text, destination); - } + return new SingleEntryJumpTable(defaultDestination, exitDestination, text, destination); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/SingleEntryJumpTableTestBase.cs b/src/Http/Routing/test/UnitTests/Matching/SingleEntryJumpTableTestBase.cs index 4310c0a6df..ed9d040d6d 100644 --- a/src/Http/Routing/test/UnitTests/Matching/SingleEntryJumpTableTestBase.cs +++ b/src/Http/Routing/test/UnitTests/Matching/SingleEntryJumpTableTestBase.cs @@ -3,66 +3,65 @@ using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public abstract class SingleEntryJumpTableTestBase { - public abstract class SingleEntryJumpTableTestBase - { - private protected abstract JumpTable CreateJumpTable( - int defaultDestination, - int exitDestination, - string text, - int destination); + private protected abstract JumpTable CreateJumpTable( + int defaultDestination, + int exitDestination, + string text, + int destination); - [Fact] - public void GetDestination_ZeroLengthSegment_JumpsToExit() - { - // Arrange - var table = CreateJumpTable(0, 1, "text", 2); + [Fact] + public void GetDestination_ZeroLengthSegment_JumpsToExit() + { + // Arrange + var table = CreateJumpTable(0, 1, "text", 2); - // Act - var result = table.GetDestination("ignored", new PathSegment(0, 0)); + // Act + var result = table.GetDestination("ignored", new PathSegment(0, 0)); - // Assert - Assert.Equal(1, result); - } + // Assert + Assert.Equal(1, result); + } - [Fact] - public void GetDestination_NonMatchingSegment_JumpsToDefault() - { - // Arrange - var table = CreateJumpTable(0, 1, "text", 2); + [Fact] + public void GetDestination_NonMatchingSegment_JumpsToDefault() + { + // Arrange + var table = CreateJumpTable(0, 1, "text", 2); - // Act - var result = table.GetDestination("text", new PathSegment(1, 2)); + // Act + var result = table.GetDestination("text", new PathSegment(1, 2)); - // Assert - Assert.Equal(0, result); - } + // Assert + Assert.Equal(0, result); + } - [Fact] - public void GetDestination_SegmentMatchingText_JumpsToDestination() - { - // Arrange - var table = CreateJumpTable(0, 1, "text", 2); + [Fact] + public void GetDestination_SegmentMatchingText_JumpsToDestination() + { + // Arrange + var table = CreateJumpTable(0, 1, "text", 2); - // Act - var result = table.GetDestination("some-text", new PathSegment(5, 4)); + // Act + var result = table.GetDestination("some-text", new PathSegment(5, 4)); - // Assert - Assert.Equal(2, result); - } + // Assert + Assert.Equal(2, result); + } - [Fact] - public void GetDestination_SegmentMatchingTextIgnoreCase_JumpsToDestination() - { - // Arrange - var table = CreateJumpTable(0, 1, "text", 2); + [Fact] + public void GetDestination_SegmentMatchingTextIgnoreCase_JumpsToDestination() + { + // Arrange + var table = CreateJumpTable(0, 1, "text", 2); - // Act - var result = table.GetDestination("some-tExt", new PathSegment(5, 4)); + // Act + var result = table.GetDestination("some-tExt", new PathSegment(5, 4)); - // Assert - Assert.Equal(2, result); - } + // Assert + Assert.Equal(2, result); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcher.cs b/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcher.cs index eca1b96494..990b071afc 100644 --- a/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcher.cs +++ b/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcher.cs @@ -7,33 +7,32 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing.Tree; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +// This is an adapter to use TreeRouter in the conformance tests +internal class TreeRouterMatcher : Matcher { - // This is an adapter to use TreeRouter in the conformance tests - internal class TreeRouterMatcher : Matcher + private readonly TreeRouter _inner; + + internal TreeRouterMatcher(TreeRouter inner) { - private readonly TreeRouter _inner; + _inner = inner; + } - internal TreeRouterMatcher(TreeRouter inner) + public override async Task MatchAsync(HttpContext httpContext) + { + if (httpContext == null) { - _inner = inner; + throw new ArgumentNullException(nameof(httpContext)); } - public override async Task MatchAsync(HttpContext httpContext) + var routeContext = new RouteContext(httpContext); + await _inner.RouteAsync(routeContext); + + if (routeContext.Handler != null) { - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - var routeContext = new RouteContext(httpContext); - await _inner.RouteAsync(routeContext); - - if (routeContext.Handler != null) - { - httpContext.Request.RouteValues = routeContext.RouteData.Values; - await routeContext.Handler(httpContext); - } + httpContext.Request.RouteValues = routeContext.RouteData.Values; + await routeContext.Handler(httpContext); } } } diff --git a/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcherBuilder.cs b/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcherBuilder.cs index 00c239a634..c9eece1d44 100644 --- a/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcherBuilder.cs +++ b/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcherBuilder.cs @@ -14,97 +14,96 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +internal class TreeRouterMatcherBuilder : MatcherBuilder { - internal class TreeRouterMatcherBuilder : MatcherBuilder - { - private readonly List _endpoints; + private readonly List _endpoints; - public TreeRouterMatcherBuilder() - { - _endpoints = new List(); - } + public TreeRouterMatcherBuilder() + { + _endpoints = new List(); + } - public override void AddEndpoint(RouteEndpoint endpoint) - { - _endpoints.Add(endpoint); - } + public override void AddEndpoint(RouteEndpoint endpoint) + { + _endpoints.Add(endpoint); + } - public override Matcher Build() - { - var builder = new TreeRouteBuilder( - NullLoggerFactory.Instance, - new DefaultObjectPool(new UriBuilderContextPooledObjectPolicy()), - new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()), new TestServiceProvider())); + public override Matcher Build() + { + var builder = new TreeRouteBuilder( + NullLoggerFactory.Instance, + new DefaultObjectPool(new UriBuilderContextPooledObjectPolicy()), + new DefaultInlineConstraintResolver(Options.Create(new RouteOptions()), new TestServiceProvider())); - var selector = new DefaultEndpointSelector(); + var selector = new DefaultEndpointSelector(); - var groups = _endpoints - .GroupBy(e => (e.Order, e.RoutePattern.InboundPrecedence, e.RoutePattern.RawText)) - .OrderBy(g => g.Key.Order) - .ThenBy(g => g.Key.InboundPrecedence); + var groups = _endpoints + .GroupBy(e => (e.Order, e.RoutePattern.InboundPrecedence, e.RoutePattern.RawText)) + .OrderBy(g => g.Key.Order) + .ThenBy(g => g.Key.InboundPrecedence); - var routes = new RouteCollection(); + var routes = new RouteCollection(); - foreach (var group in groups) + foreach (var group in groups) + { + var candidates = group.ToArray(); + + // RouteEndpoint.Values contains the default values parsed from the template + // as well as those specified with a literal. We need to separate those + // for legacy cases. + var endpoint = group.First(); + var defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults); + for (var i = 0; i < endpoint.RoutePattern.Parameters.Count; i++) { - var candidates = group.ToArray(); - - // RouteEndpoint.Values contains the default values parsed from the template - // as well as those specified with a literal. We need to separate those - // for legacy cases. - var endpoint = group.First(); - var defaults = new RouteValueDictionary(endpoint.RoutePattern.Defaults); - for (var i = 0; i < endpoint.RoutePattern.Parameters.Count; i++) + var parameter = endpoint.RoutePattern.Parameters[i]; + if (parameter.Default != null) { - var parameter = endpoint.RoutePattern.Parameters[i]; - if (parameter.Default != null) - { - defaults.Remove(parameter.Name); - } + defaults.Remove(parameter.Name); } - - builder.MapInbound( - new SelectorRouter(selector, candidates), - new RouteTemplate(endpoint.RoutePattern), - routeName: null, - order: endpoint.Order); } - return new TreeRouterMatcher(builder.Build()); + builder.MapInbound( + new SelectorRouter(selector, candidates), + new RouteTemplate(endpoint.RoutePattern), + routeName: null, + order: endpoint.Order); } - private class SelectorRouter : IRouter + return new TreeRouterMatcher(builder.Build()); + } + + private class SelectorRouter : IRouter + { + private readonly EndpointSelector _selector; + private readonly RouteEndpoint[] _candidates; + private readonly RouteValueDictionary[] _values; + private readonly int[] _scores; + + public SelectorRouter(EndpointSelector selector, RouteEndpoint[] candidates) { - private readonly EndpointSelector _selector; - private readonly RouteEndpoint[] _candidates; - private readonly RouteValueDictionary[] _values; - private readonly int[] _scores; + _selector = selector; + _candidates = candidates; - public SelectorRouter(EndpointSelector selector, RouteEndpoint[] candidates) - { - _selector = selector; - _candidates = candidates; + _values = new RouteValueDictionary[_candidates.Length]; + _scores = new int[_candidates.Length]; + } - _values = new RouteValueDictionary[_candidates.Length]; - _scores = new int[_candidates.Length]; - } + public VirtualPathData GetVirtualPath(VirtualPathContext context) + { + throw new NotImplementedException(); + } - public VirtualPathData GetVirtualPath(VirtualPathContext context) - { - throw new NotImplementedException(); - } + public async Task RouteAsync(RouteContext routeContext) + { + // This is needed due to a quirk of our tests - they reuse the endpoint feature. + routeContext.HttpContext.SetEndpoint(null); - public async Task RouteAsync(RouteContext routeContext) + await _selector.SelectAsync(routeContext.HttpContext, new CandidateSet(_candidates, _values, _scores)); + if (routeContext.HttpContext.GetEndpoint() != null) { - // This is needed due to a quirk of our tests - they reuse the endpoint feature. - routeContext.HttpContext.SetEndpoint(null); - - await _selector.SelectAsync(routeContext.HttpContext, new CandidateSet(_candidates, _values, _scores)); - if (routeContext.HttpContext.GetEndpoint() != null) - { - routeContext.Handler = (_) => Task.CompletedTask; - } + routeContext.Handler = (_) => Task.CompletedTask; } } } diff --git a/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcherConformanceTest.cs b/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcherConformanceTest.cs index 1ae231a324..70a2837bf9 100644 --- a/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcherConformanceTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/TreeRouterMatcherConformanceTest.cs @@ -4,35 +4,35 @@ using System.Threading.Tasks; using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class TreeRouterMatcherConformanceTest : FullFeaturedMatcherConformanceTest { - public class TreeRouterMatcherConformanceTest : FullFeaturedMatcherConformanceTest + // TreeRouter doesn't support non-inline default values. + [Fact] + public override Task Match_NonInlineDefaultValues() { - // TreeRouter doesn't support non-inline default values. - [Fact] - public override Task Match_NonInlineDefaultValues() - { - return Task.CompletedTask; - } + return Task.CompletedTask; + } - // TreeRouter doesn't support non-inline default values. - [Fact] - public override Task Match_ExtraDefaultValues() - { - return Task.CompletedTask; - } + // TreeRouter doesn't support non-inline default values. + [Fact] + public override Task Match_ExtraDefaultValues() + { + return Task.CompletedTask; + } - // https://github.com/dotnet/aspnetcore/issues/18677 - // - [Theory] - [InlineData("/middleware", 1)] - [InlineData("/middleware/test", 1)] - [InlineData("/middleware/test1/test2", 1)] - [InlineData("/bill/boga", 0)] - public async Task Match_Regression_1867(string path, int endpointIndex) + // https://github.com/dotnet/aspnetcore/issues/18677 + // + [Theory] + [InlineData("/middleware", 1)] + [InlineData("/middleware/test", 1)] + [InlineData("/middleware/test1/test2", 1)] + [InlineData("/bill/boga", 0)] + public async Task Match_Regression_1867(string path, int endpointIndex) + { + var endpoints = new RouteEndpoint[] { - var endpoints = new RouteEndpoint[] - { EndpointFactory.CreateRouteEndpoint( "{firstName}/{lastName}", order: 0, @@ -41,28 +41,27 @@ namespace Microsoft.AspNetCore.Routing.Matching EndpointFactory.CreateRouteEndpoint( "middleware/{**_}", order: 0), - }; + }; - var expected = endpoints[endpointIndex]; + var expected = endpoints[endpointIndex]; - var matcher = CreateMatcher(endpoints); - var httpContext = CreateContext(path); + var matcher = CreateMatcher(endpoints); + var httpContext = CreateContext(path); - // Act - await matcher.MatchAsync(httpContext); + // Act + await matcher.MatchAsync(httpContext); - // Assert - MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true); - } + // Assert + MatcherAssert.AssertMatch(httpContext, expected, ignoreValues: true); + } - internal override Matcher CreateMatcher(params RouteEndpoint[] endpoints) + internal override Matcher CreateMatcher(params RouteEndpoint[] endpoints) + { + var builder = new TreeRouterMatcherBuilder(); + for (var i = 0; i < endpoints.Length; i++) { - var builder = new TreeRouterMatcherBuilder(); - for (var i = 0; i < endpoints.Length; i++) - { - builder.AddEndpoint(endpoints[i]); - } - return builder.Build(); + builder.AddEndpoint(endpoints[i]); } + return builder.Build(); } } diff --git a/src/Http/Routing/test/UnitTests/Matching/VectorizedILEmitTrieJumpTableTest.cs b/src/Http/Routing/test/UnitTests/Matching/VectorizedILEmitTrieJumpTableTest.cs index d0321db276..a9920124ba 100644 --- a/src/Http/Routing/test/UnitTests/Matching/VectorizedILEmitTrieJumpTableTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/VectorizedILEmitTrieJumpTableTest.cs @@ -1,12 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class VectorizedILEmitTrieJumpTableTest : ILEmitTreeJumpTableTestBase { - public class VectorizedILEmitTrieJumpTableTest : ILEmitTreeJumpTableTestBase - { - // We can still run the vectorized implementation on 32 bit, we just - // don't expect it to be performant - it will still be correct. - public override bool Vectorize => true; - } + // We can still run the vectorized implementation on 32 bit, we just + // don't expect it to be performant - it will still be correct. + public override bool Vectorize => true; } diff --git a/src/Http/Routing/test/UnitTests/Matching/ZeroEntryJumpTableTest.cs b/src/Http/Routing/test/UnitTests/Matching/ZeroEntryJumpTableTest.cs index 1db558483e..0fedd5218f 100644 --- a/src/Http/Routing/test/UnitTests/Matching/ZeroEntryJumpTableTest.cs +++ b/src/Http/Routing/test/UnitTests/Matching/ZeroEntryJumpTableTest.cs @@ -3,34 +3,33 @@ using Xunit; -namespace Microsoft.AspNetCore.Routing.Matching +namespace Microsoft.AspNetCore.Routing.Matching; + +public class ZeroEntryJumpTableTest { - public class ZeroEntryJumpTableTest + [Fact] + public void GetDestination_ZeroLengthSegment_JumpsToExit() + { + // Arrange + var table = new ZeroEntryJumpTable(0, 1); + + // Act + var result = table.GetDestination("ignored", new PathSegment(0, 0)); + + // Assert + Assert.Equal(1, result); + } + + [Fact] + public void GetDestination_SegmentWithLength_JumpsToDefault() { - [Fact] - public void GetDestination_ZeroLengthSegment_JumpsToExit() - { - // Arrange - var table = new ZeroEntryJumpTable(0, 1); - - // Act - var result = table.GetDestination("ignored", new PathSegment(0, 0)); - - // Assert - Assert.Equal(1, result); - } - - [Fact] - public void GetDestination_SegmentWithLength_JumpsToDefault() - { - // Arrange - var table = new ZeroEntryJumpTable(0, 1); - - // Act - var result = table.GetDestination("ignored", new PathSegment(0, 1)); - - // Assert - Assert.Equal(0, result); - } + // Arrange + var table = new ZeroEntryJumpTable(0, 1); + + // Act + var result = table.GetDestination("ignored", new PathSegment(0, 1)); + + // Assert + Assert.Equal(0, result); } } diff --git a/src/Http/Routing/test/UnitTests/PathTokenizerTest.cs b/src/Http/Routing/test/UnitTests/PathTokenizerTest.cs index 8d29d7142f..2feb5100c0 100644 --- a/src/Http/Routing/test/UnitTests/PathTokenizerTest.cs +++ b/src/Http/Routing/test/UnitTests/PathTokenizerTest.cs @@ -5,15 +5,15 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class PathTokenizerTest { - public class PathTokenizerTest + public static TheoryData TokenizationData { - public static TheoryData TokenizationData + get { - get - { - return new TheoryData + return new TheoryData { { string.Empty, new StringSegment[] { } }, { "/", new StringSegment[] { } }, @@ -72,46 +72,45 @@ namespace Microsoft.AspNetCore.Routing } }, }; - } } + } - [Theory] - [MemberData(nameof(TokenizationData))] - public void PathTokenizer_Count(string path, StringSegment[] expectedSegments) - { - // Arrange - var tokenizer = new PathTokenizer(new PathString(path)); + [Theory] + [MemberData(nameof(TokenizationData))] + public void PathTokenizer_Count(string path, StringSegment[] expectedSegments) + { + // Arrange + var tokenizer = new PathTokenizer(new PathString(path)); - // Act - var count = tokenizer.Count; + // Act + var count = tokenizer.Count; - // Assert - Assert.Equal(expectedSegments.Length, count); - } + // Assert + Assert.Equal(expectedSegments.Length, count); + } - [Theory] - [MemberData(nameof(TokenizationData))] - public void PathTokenizer_Indexer(string path, StringSegment[] expectedSegments) - { - // Arrange - var tokenizer = new PathTokenizer(new PathString(path)); + [Theory] + [MemberData(nameof(TokenizationData))] + public void PathTokenizer_Indexer(string path, StringSegment[] expectedSegments) + { + // Arrange + var tokenizer = new PathTokenizer(new PathString(path)); - // Act & Assert - for (var i = 0; i < expectedSegments.Length; i++) - { - Assert.Equal(expectedSegments[i], tokenizer[i]); - } + // Act & Assert + for (var i = 0; i < expectedSegments.Length; i++) + { + Assert.Equal(expectedSegments[i], tokenizer[i]); } + } - [Theory] - [MemberData(nameof(TokenizationData))] - public void PathTokenizer_Enumerator(string path, StringSegment[] expectedSegments) - { - // Arrange - var tokenizer = new PathTokenizer(new PathString(path)); + [Theory] + [MemberData(nameof(TokenizationData))] + public void PathTokenizer_Enumerator(string path, StringSegment[] expectedSegments) + { + // Arrange + var tokenizer = new PathTokenizer(new PathString(path)); - // Act & Assert - Assert.Equal(expectedSegments, tokenizer); - } + // Act & Assert + Assert.Equal(expectedSegments, tokenizer); } } diff --git a/src/Http/Routing/test/UnitTests/Patterns/DefaultRoutePatternTransformerTest.cs b/src/Http/Routing/test/UnitTests/Patterns/DefaultRoutePatternTransformerTest.cs index 9b17791d41..1f53fb71b3 100644 --- a/src/Http/Routing/test/UnitTests/Patterns/DefaultRoutePatternTransformerTest.cs +++ b/src/Http/Routing/test/UnitTests/Patterns/DefaultRoutePatternTransformerTest.cs @@ -1,403 +1,402 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.Routing.Constraints; -using Microsoft.Extensions.DependencyInjection; using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.Routing.Constraints; +using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +public class DefaultRoutePatternTransformerTest { - public class DefaultRoutePatternTransformerTest + public DefaultRoutePatternTransformerTest() + { + var services = new ServiceCollection(); + services.AddRouting(); + services.AddOptions(); + Transformer = services.BuildServiceProvider().GetRequiredService(); + } + + public RoutePatternTransformer Transformer { get; } + + [Fact] + public void SubstituteRequiredValues_CanAcceptNullForAnyKey() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { a = (string)null, b = "", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("a", null), kvp), + kvp => Assert.Equal(new KeyValuePair("b", string.Empty), kvp)); + } + + [Fact] + public void SubstituteRequiredValues_RejectsNullForParameter() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = string.Empty, }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Null(actual); + } + + [Fact] + public void SubstituteRequiredValues_AllowRequiredValueAnyForParameter() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = RoutePattern.RequiredValueAny, }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.Defaults.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); // default is preserved + + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("controller", RoutePattern.RequiredValueAny), kvp)); + } + + [Fact] + public void SubstituteRequiredValues_RejectsNullForOutOfLineDefault() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { area = "Admin" }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { area = string.Empty, }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Null(actual); + } + + [Fact] + public void SubstituteRequiredValues_RejectsRequiredValueAnyForOutOfLineDefault() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { area = RoutePattern.RequiredValueAny }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { area = string.Empty, }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Null(actual); + } + + [Fact] + public void SubstituteRequiredValues_CanAcceptValueForParameter() + { + // Arrange + var template = "{controller}/{action}/{id?}"; + var defaults = new { }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); + } + + [Fact] + public void SubstituteRequiredValues_CanAcceptValueForParameter_WithSameDefault() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); + + // We should not need to rewrite anything in this case. + Assert.Same(actual.Defaults, original.Defaults); + Assert.Same(actual.Parameters, original.Parameters); + Assert.Same(actual.PathSegments, original.PathSegments); + } + + [Fact] + public void SubstituteRequiredValues_CanAcceptValueForParameter_WithDifferentDefault() + { + // Arrange + var template = "{controller=Blog}/{action=ReadPost}/{id?}"; + var defaults = new { area = "Admin", }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { area = "Admin", controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("area", "Admin"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); + + // We should not need to rewrite anything in this case. + Assert.NotSame(actual.Defaults, original.Defaults); + Assert.NotSame(actual.Parameters, original.Parameters); + Assert.NotSame(actual.PathSegments, original.PathSegments); + + // other defaults were wiped out + Assert.Equal(new KeyValuePair("area", "Admin"), Assert.Single(actual.Defaults)); + Assert.Null(actual.GetParameter("controller").Default); + Assert.False(actual.Defaults.ContainsKey("controller")); + Assert.Null(actual.GetParameter("action").Default); + Assert.False(actual.Defaults.ContainsKey("action")); + } + + [Fact] + public void SubstituteRequiredValues_CanAcceptValueForParameter_WithMatchingConstraint() + { + // Arrange + var template = "{controller}/{action}/{id?}"; + var defaults = new { }; + var policies = new { controller = "Home", action = new RegexRouteConstraint("Index"), }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); + } + + [Fact] + public void SubstituteRequiredValues_CanRejectValueForParameter_WithNonMatchingConstraint() + { + // Arrange + var template = "{controller}/{action}/{id?}"; + var defaults = new { }; + var policies = new { controller = "Home", action = new RegexRouteConstraint("Index"), }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = "Blog", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Null(actual); + } + + [Fact] + public void SubstituteRequiredValues_CanAcceptValueForDefault_WithSameValue() { - public DefaultRoutePatternTransformerTest() - { - var services = new ServiceCollection(); - services.AddRouting(); - services.AddOptions(); - Transformer = services.BuildServiceProvider().GetRequiredService(); - } - - public RoutePatternTransformer Transformer { get; } - - [Fact] - public void SubstituteRequiredValues_CanAcceptNullForAnyKey() - { - // Arrange - var template = "{controller=Home}/{action=Index}/{id?}"; - var defaults = new { }; - var policies = new { }; - - var original = RoutePatternFactory.Parse(template, defaults, policies); - - var requiredValues = new { a = (string)null, b = "", }; - - // Act - var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - - // Assert - Assert.Collection( - actual.RequiredValues.OrderBy(kvp => kvp.Key), - kvp => Assert.Equal(new KeyValuePair("a", null), kvp), - kvp => Assert.Equal(new KeyValuePair("b", string.Empty), kvp)); - } - - [Fact] - public void SubstituteRequiredValues_RejectsNullForParameter() - { - // Arrange - var template = "{controller=Home}/{action=Index}/{id?}"; - var defaults = new { }; - var policies = new { }; - - var original = RoutePatternFactory.Parse(template, defaults, policies); - - var requiredValues = new { controller = string.Empty, }; - - // Act - var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - - // Assert - Assert.Null(actual); - } - - [Fact] - public void SubstituteRequiredValues_AllowRequiredValueAnyForParameter() - { - // Arrange - var template = "{controller=Home}/{action=Index}/{id?}"; - var defaults = new { }; - var policies = new { }; - - var original = RoutePatternFactory.Parse(template, defaults, policies); - - var requiredValues = new { controller = RoutePattern.RequiredValueAny, }; - - // Act - var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - - // Assert - Assert.Collection( - actual.Defaults.OrderBy(kvp => kvp.Key), - kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), - kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); // default is preserved - - Assert.Collection( - actual.RequiredValues.OrderBy(kvp => kvp.Key), - kvp => Assert.Equal(new KeyValuePair("controller", RoutePattern.RequiredValueAny), kvp)); - } - - [Fact] - public void SubstituteRequiredValues_RejectsNullForOutOfLineDefault() - { - // Arrange - var template = "{controller=Home}/{action=Index}/{id?}"; - var defaults = new { area = "Admin" }; - var policies = new { }; - - var original = RoutePatternFactory.Parse(template, defaults, policies); - - var requiredValues = new { area = string.Empty, }; - - // Act - var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - - // Assert - Assert.Null(actual); - } - - [Fact] - public void SubstituteRequiredValues_RejectsRequiredValueAnyForOutOfLineDefault() - { - // Arrange - var template = "{controller=Home}/{action=Index}/{id?}"; - var defaults = new { area = RoutePattern.RequiredValueAny }; - var policies = new { }; - - var original = RoutePatternFactory.Parse(template, defaults, policies); - - var requiredValues = new { area = string.Empty, }; - - // Act - var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - - // Assert - Assert.Null(actual); - } - - [Fact] - public void SubstituteRequiredValues_CanAcceptValueForParameter() - { - // Arrange - var template = "{controller}/{action}/{id?}"; - var defaults = new { }; - var policies = new { }; - - var original = RoutePatternFactory.Parse(template, defaults, policies); - - var requiredValues = new { controller = "Home", action = "Index", }; - - // Act - var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - - // Assert - Assert.Collection( - actual.RequiredValues.OrderBy(kvp => kvp.Key), - kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), - kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); - } - - [Fact] - public void SubstituteRequiredValues_CanAcceptValueForParameter_WithSameDefault() - { - // Arrange - var template = "{controller=Home}/{action=Index}/{id?}"; - var defaults = new { }; - var policies = new { }; - - var original = RoutePatternFactory.Parse(template, defaults, policies); - - var requiredValues = new { controller = "Home", action = "Index", }; - - // Act - var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - - // Assert - Assert.Collection( - actual.RequiredValues.OrderBy(kvp => kvp.Key), - kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), - kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); - - // We should not need to rewrite anything in this case. - Assert.Same(actual.Defaults, original.Defaults); - Assert.Same(actual.Parameters, original.Parameters); - Assert.Same(actual.PathSegments, original.PathSegments); - } - - [Fact] - public void SubstituteRequiredValues_CanAcceptValueForParameter_WithDifferentDefault() - { - // Arrange - var template = "{controller=Blog}/{action=ReadPost}/{id?}"; - var defaults = new { area = "Admin", }; - var policies = new { }; - - var original = RoutePatternFactory.Parse(template, defaults, policies); - - var requiredValues = new { area = "Admin", controller = "Home", action = "Index", }; - - // Act - var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - - // Assert - Assert.Collection( - actual.RequiredValues.OrderBy(kvp => kvp.Key), - kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), - kvp => Assert.Equal(new KeyValuePair("area", "Admin"), kvp), - kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); - - // We should not need to rewrite anything in this case. - Assert.NotSame(actual.Defaults, original.Defaults); - Assert.NotSame(actual.Parameters, original.Parameters); - Assert.NotSame(actual.PathSegments, original.PathSegments); - - // other defaults were wiped out - Assert.Equal(new KeyValuePair("area", "Admin"), Assert.Single(actual.Defaults)); - Assert.Null(actual.GetParameter("controller").Default); - Assert.False(actual.Defaults.ContainsKey("controller")); - Assert.Null(actual.GetParameter("action").Default); - Assert.False(actual.Defaults.ContainsKey("action")); - } - - [Fact] - public void SubstituteRequiredValues_CanAcceptValueForParameter_WithMatchingConstraint() - { - // Arrange - var template = "{controller}/{action}/{id?}"; - var defaults = new { }; - var policies = new { controller = "Home", action = new RegexRouteConstraint("Index"), }; - - var original = RoutePatternFactory.Parse(template, defaults, policies); - - var requiredValues = new { controller = "Home", action = "Index", }; - - // Act - var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - - // Assert - Assert.Collection( - actual.RequiredValues.OrderBy(kvp => kvp.Key), - kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), - kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); - } - - [Fact] - public void SubstituteRequiredValues_CanRejectValueForParameter_WithNonMatchingConstraint() - { - // Arrange - var template = "{controller}/{action}/{id?}"; - var defaults = new { }; - var policies = new { controller = "Home", action = new RegexRouteConstraint("Index"), }; - - var original = RoutePatternFactory.Parse(template, defaults, policies); - - var requiredValues = new { controller = "Blog", action = "Index", }; - - // Act - var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - - // Assert - Assert.Null(actual); - } - - [Fact] - public void SubstituteRequiredValues_CanAcceptValueForDefault_WithSameValue() - { - // Arrange - var template = "Home/Index/{id?}"; - var defaults = new { controller = "Home", action = "Index", }; - var policies = new { }; - - var original = RoutePatternFactory.Parse(template, defaults, policies); - - var requiredValues = new { controller = "Home", action = "Index", }; - - // Act - var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - - // Assert - Assert.Collection( - actual.RequiredValues.OrderBy(kvp => kvp.Key), - kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), - kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); - } - - [Fact] - public void SubstituteRequiredValues_CanRejectValueForDefault_WithDifferentValue() - { - // Arrange - var template = "Home/Index/{id?}"; - var defaults = new { controller = "Home", action = "Index", }; - var policies = new { }; - - var original = RoutePatternFactory.Parse(template, defaults, policies); - - var requiredValues = new { controller = "Blog", action = "Index", }; - - // Act - var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - - // Assert - Assert.Null(actual); - } - - [Fact] - public void SubstituteRequiredValues_CanAcceptValueForDefault_WithSameValue_Null() - { - // Arrange - var template = "Home/Index/{id?}"; - var defaults = new { controller = (string)null, action = "", }; - var policies = new { }; - - var original = RoutePatternFactory.Parse(template, defaults, policies); - - var requiredValues = new { controller = string.Empty, action = (string)null, }; - - // Act - var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - - // Assert - Assert.Collection( - actual.RequiredValues.OrderBy(kvp => kvp.Key), - kvp => Assert.Equal(new KeyValuePair("action", null), kvp), - kvp => Assert.Equal(new KeyValuePair("controller", ""), kvp)); - } - - [Fact] - public void SubstituteRequiredValues_CanAcceptValueForDefault_WithSameValue_WithMatchingConstraint() - { - // Arrange - var template = "Home/Index/{id?}"; - var defaults = new { controller = "Home", action = "Index", }; - var policies = new { controller = "Home", }; - - var original = RoutePatternFactory.Parse(template, defaults, policies); - - var requiredValues = new { controller = "Home", action = "Index", }; - - // Act - var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - - // Assert - Assert.Collection( - actual.RequiredValues.OrderBy(kvp => kvp.Key), - kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), - kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); - } - - [Fact] - public void SubstituteRequiredValues_CanRejectValueForDefault_WithSameValue_WithNonMatchingConstraint() - { - // Arrange - var template = "Home/Index/{id?}"; - var defaults = new { controller = "Home", action = "Index", }; - var policies = new { controller = "Home", }; - - var original = RoutePatternFactory.Parse(template, defaults, policies); - - var requiredValues = new { controller = "Home", action = "Index", }; - - // Act - var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - - // Assert - Assert.Collection( - actual.RequiredValues.OrderBy(kvp => kvp.Key), - kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), - kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); - } - - [Fact] - public void SubstituteRequiredValues_CanMergeExistingRequiredValues() - { - // Arrange - var template = "Home/Index/{id?}"; - var defaults = new { area = "Admin", controller = "Home", action = "Index", }; - var policies = new { }; - - var original = RoutePatternFactory.Parse(template, defaults, policies, new { area = "Admin", controller = "Home", }); - - var requiredValues = new { controller = "Home", action = "Index", }; - - // Act - var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - - // Assert - Assert.Collection( - actual.RequiredValues.OrderBy(kvp => kvp.Key), - kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), - kvp => Assert.Equal(new KeyValuePair("area", "Admin"), kvp), - kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); - } + // Arrange + var template = "Home/Index/{id?}"; + var defaults = new { controller = "Home", action = "Index", }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); + } + + [Fact] + public void SubstituteRequiredValues_CanRejectValueForDefault_WithDifferentValue() + { + // Arrange + var template = "Home/Index/{id?}"; + var defaults = new { controller = "Home", action = "Index", }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = "Blog", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Null(actual); + } + + [Fact] + public void SubstituteRequiredValues_CanAcceptValueForDefault_WithSameValue_Null() + { + // Arrange + var template = "Home/Index/{id?}"; + var defaults = new { controller = (string)null, action = "", }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = string.Empty, action = (string)null, }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", null), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", ""), kvp)); + } + + [Fact] + public void SubstituteRequiredValues_CanAcceptValueForDefault_WithSameValue_WithMatchingConstraint() + { + // Arrange + var template = "Home/Index/{id?}"; + var defaults = new { controller = "Home", action = "Index", }; + var policies = new { controller = "Home", }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); + } + + [Fact] + public void SubstituteRequiredValues_CanRejectValueForDefault_WithSameValue_WithNonMatchingConstraint() + { + // Arrange + var template = "Home/Index/{id?}"; + var defaults = new { controller = "Home", action = "Index", }; + var policies = new { controller = "Home", }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); + } + + [Fact] + public void SubstituteRequiredValues_CanMergeExistingRequiredValues() + { + // Arrange + var template = "Home/Index/{id?}"; + var defaults = new { area = "Admin", controller = "Home", action = "Index", }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies, new { area = "Admin", controller = "Home", }); + + var requiredValues = new { controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); + + // Assert + Assert.Collection( + actual.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => Assert.Equal(new KeyValuePair("action", "Index"), kvp), + kvp => Assert.Equal(new KeyValuePair("area", "Admin"), kvp), + kvp => Assert.Equal(new KeyValuePair("controller", "Home"), kvp)); + } + + [Fact] + public void SubstituteRequiredValues_NullRequiredValueParameter_Fail() + { + // Arrange + var template = "PageRoute/Attribute/{page}"; + var defaults = new { area = (string)null, page = (string)null, controller = "Home", action = "Index", }; + var policies = new { }; + + var original = RoutePatternFactory.Parse(template, defaults, policies); + + var requiredValues = new { area = (string)null, page = (string)null, controller = "Home", action = "Index", }; + + // Act + var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - [Fact] - public void SubstituteRequiredValues_NullRequiredValueParameter_Fail() - { - // Arrange - var template = "PageRoute/Attribute/{page}"; - var defaults = new { area = (string)null, page = (string)null, controller = "Home", action = "Index", }; - var policies = new { }; - - var original = RoutePatternFactory.Parse(template, defaults, policies); - - var requiredValues = new { area = (string)null, page = (string)null, controller = "Home", action = "Index", }; - - // Act - var actual = Transformer.SubstituteRequiredValues(original, requiredValues); - - // Assert - Assert.Null(actual); - } + // Assert + Assert.Null(actual); } } diff --git a/src/Http/Routing/test/UnitTests/Patterns/InlineRouteParameterParserTest.cs b/src/Http/Routing/test/UnitTests/Patterns/InlineRouteParameterParserTest.cs index a65c940c31..5fbee00c6d 100644 --- a/src/Http/Routing/test/UnitTests/Patterns/InlineRouteParameterParserTest.cs +++ b/src/Http/Routing/test/UnitTests/Patterns/InlineRouteParameterParserTest.cs @@ -5,1072 +5,1071 @@ using System.Linq; using Microsoft.AspNetCore.Routing.Constraints; using Xunit; -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +public class InlineRouteParameterParserTest { - public class InlineRouteParameterParserTest - { - [Theory] - [InlineData("=")] - [InlineData(":")] - public void ParseRouteParameter_WithoutADefaultValue(string parameterName) - { - // Arrange & Act - var templatePart = ParseParameter(parameterName); - - // Assert - Assert.Equal(parameterName, templatePart.Name); - Assert.Null(templatePart.Default); - Assert.Empty(templatePart.ParameterPolicies); - } - - [Fact] - public void ParseRouteParameter_WithEmptyDefaultValue() - { - // Arrange & Act - var templatePart = ParseParameter("param="); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("", templatePart.Default); - Assert.Empty(templatePart.ParameterPolicies); - } - - [Fact] - public void ParseRouteParameter_WithoutAConstraintName() - { - // Arrange & Act - var templatePart = ParseParameter("param:"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Null(templatePart.Default); - Assert.Empty(templatePart.ParameterPolicies); - } - - [Fact] - public void ParseRouteParameter_WithoutAConstraintNameOrParameterName() - { - // Arrange & Act - var templatePart = ParseParameter("param:="); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("", templatePart.Default); - Assert.Empty(templatePart.ParameterPolicies); - } - - [Fact] - public void ParseRouteParameter_WithADefaultValueContainingConstraintSeparator() - { - // Arrange & Act - var templatePart = ParseParameter("param=:"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal(":", templatePart.Default); - Assert.Empty(templatePart.ParameterPolicies); - } - - [Fact] - public void ParseRouteParameter_ConstraintAndDefault_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter("param:int=111111"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("111111", templatePart.Default); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal("int", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithArgumentsAndDefault_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\d+)=111111"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("111111", templatePart.Default); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\d+)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintAndOptional_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:int?"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.True(templatePart.IsOptional); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal("int", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintAndOptional_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:int=12?"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("12", templatePart.Default); - Assert.True(templatePart.IsOptional); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal("int", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintAndOptional_WithDefaultValueWithQuestionMark_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:int=12??"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("12?", templatePart.Default); - Assert.True(templatePart.IsOptional); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal("int", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithArgumentsAndOptional_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\d+)?"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.True(templatePart.IsOptional); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\d+)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithArgumentsAndOptional_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\d+)=abc?"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.True(templatePart.IsOptional); - - Assert.Equal("abc", templatePart.Default); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\d+)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ChainedConstraints_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(d+):test(w+)"); - - // Assert - Assert.Equal("param", templatePart.Name); - - Assert.Collection(templatePart.ParameterPolicies, - constraint => Assert.Equal(@"test(d+)", constraint.Content), - constraint => Assert.Equal(@"test(w+)", constraint.Content)); - } - - [Fact] - public void ParseRouteParameter_ChainedConstraints_DoubleDelimiters_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param::test(d+)::test(w+)"); - - // Assert - Assert.Equal("param", templatePart.Name); - - Assert.Collection( - templatePart.ParameterPolicies, - constraint => Assert.Equal(@"test(d+)", constraint.Content), - constraint => Assert.Equal(@"test(w+)", constraint.Content)); - } - - [Fact] - public void ParseRouteParameter_ChainedConstraints_ColonInPattern_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\d+):test(\w:+)"); - - // Assert - Assert.Equal("param", templatePart.Name); - - Assert.Collection(templatePart.ParameterPolicies, - constraint => Assert.Equal(@"test(\d+)", constraint.Content), - constraint => Assert.Equal(@"test(\w:+)", constraint.Content)); - } - - [Fact] - public void ParseRouteParameter_ChainedConstraints_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\d+):test(\w+)=qwer"); - - // Assert - Assert.Equal("param", templatePart.Name); - - Assert.Equal("qwer", templatePart.Default); - - Assert.Collection(templatePart.ParameterPolicies, - constraint => Assert.Equal(@"test(\d+)", constraint.Content), - constraint => Assert.Equal(@"test(\w+)", constraint.Content)); - } - - [Fact] - public void ParseRouteParameter_ChainedConstraints_WithDefaultValue_DoubleDelimiters_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\d+)::test(\w+)==qwer"); - - // Assert - Assert.Equal("param", templatePart.Name); - - Assert.Equal("=qwer", templatePart.Default); - - Assert.Collection( - templatePart.ParameterPolicies, - constraint => Assert.Equal(@"test(\d+)", constraint.Content), - constraint => Assert.Equal(@"test(\w+)", constraint.Content)); - } - - [Theory] - [InlineData("=")] - [InlineData("+=")] - [InlineData(">= || <= || ==")] - public void ParseRouteParameter_WithDefaultValue_ContainingDelimiter(string defaultValue) - { - // Arrange & Act - var templatePart = ParseParameter($"comparison-operator:length(6)={defaultValue}"); - - // Assert - Assert.Equal("comparison-operator", templatePart.Name); - Assert.Equal(defaultValue, templatePart.Default); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal("length(6)", constraint.Content); - } - - [Fact] - public void ParseRouteTemplate_ConstraintsDefaultsAndOptionalsInMultipleSections_ParsedCorrectly() - { - // Arrange & Act - var routePattern = RoutePatternFactory.Parse(@"some/url-{p1:int:test(3)=hello}/{p2=abc}/{p3?}"); - - // Assert - var parameters = routePattern.Parameters.ToArray(); - - var param1 = parameters[0]; - Assert.Equal("p1", param1.Name); - Assert.Equal("hello", param1.Default); - Assert.False(param1.IsOptional); - - Assert.Collection(param1.ParameterPolicies, - constraint => Assert.Equal("int", constraint.Content), - constraint => Assert.Equal("test(3)", constraint.Content) - ); - - var param2 = parameters[1]; - Assert.Equal("p2", param2.Name); - Assert.Equal("abc", param2.Default); - Assert.False(param2.IsOptional); - - var param3 = parameters[2]; - Assert.Equal("p3", param3.Name); - Assert.True(param3.IsOptional); - } - - [Fact] - public void ParseRouteParameter_NoTokens_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter("world"); - - // Assert - Assert.Equal("world", templatePart.Name); - } - - [Fact] - public void ParseRouteParameter_ParamDefault_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter("param=world"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("world", templatePart.Default); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_ClosingBraceIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\})"); - - // Assert - Assert.Equal("param", templatePart.Name); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\})", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\})=wer"); - - // Assert - Assert.Equal("param", templatePart.Name); - - Assert.Equal("wer", templatePart.Default); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\})", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithClosingParenInPattern_ClosingParenIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\))"); - - // Assert - Assert.Equal("param", templatePart.Name); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\))", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithClosingParenInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\))=fsd"); - - // Assert - Assert.Equal("param", templatePart.Name); - - Assert.Equal("fsd", templatePart.Default); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\))", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithColonInPattern_ColonIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(:)"); - - // Assert - Assert.Equal("param", templatePart.Name); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(:)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithColonInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(:)=mnf"); - - // Assert - Assert.Equal("param", templatePart.Name); - - Assert.Equal("mnf", templatePart.Default); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(:)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithColonsInPattern_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(a:b:c)"); - - // Assert - Assert.Equal("param", templatePart.Name); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(a:b:c)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithColonInParamName_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@":param:test=12"); - - // Assert - Assert.Equal(":param", templatePart.Name); + [Theory] + [InlineData("=")] + [InlineData(":")] + public void ParseRouteParameter_WithoutADefaultValue(string parameterName) + { + // Arrange & Act + var templatePart = ParseParameter(parameterName); + + // Assert + Assert.Equal(parameterName, templatePart.Name); + Assert.Null(templatePart.Default); + Assert.Empty(templatePart.ParameterPolicies); + } + + [Fact] + public void ParseRouteParameter_WithEmptyDefaultValue() + { + // Arrange & Act + var templatePart = ParseParameter("param="); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("", templatePart.Default); + Assert.Empty(templatePart.ParameterPolicies); + } + + [Fact] + public void ParseRouteParameter_WithoutAConstraintName() + { + // Arrange & Act + var templatePart = ParseParameter("param:"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.Default); + Assert.Empty(templatePart.ParameterPolicies); + } + + [Fact] + public void ParseRouteParameter_WithoutAConstraintNameOrParameterName() + { + // Arrange & Act + var templatePart = ParseParameter("param:="); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("", templatePart.Default); + Assert.Empty(templatePart.ParameterPolicies); + } + + [Fact] + public void ParseRouteParameter_WithADefaultValueContainingConstraintSeparator() + { + // Arrange & Act + var templatePart = ParseParameter("param=:"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal(":", templatePart.Default); + Assert.Empty(templatePart.ParameterPolicies); + } + + [Fact] + public void ParseRouteParameter_ConstraintAndDefault_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter("param:int=111111"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("111111", templatePart.Default); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal("int", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithArgumentsAndDefault_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+)=111111"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("111111", templatePart.Default); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\d+)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintAndOptional_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:int?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal("int", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintAndOptional_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:int=12?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("12", templatePart.Default); + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal("int", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintAndOptional_WithDefaultValueWithQuestionMark_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:int=12??"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("12?", templatePart.Default); + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal("int", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithArgumentsAndOptional_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+)?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\d+)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithArgumentsAndOptional_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+)=abc?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.True(templatePart.IsOptional); + + Assert.Equal("abc", templatePart.Default); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\d+)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ChainedConstraints_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(d+):test(w+)"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Collection(templatePart.ParameterPolicies, + constraint => Assert.Equal(@"test(d+)", constraint.Content), + constraint => Assert.Equal(@"test(w+)", constraint.Content)); + } + + [Fact] + public void ParseRouteParameter_ChainedConstraints_DoubleDelimiters_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param::test(d+)::test(w+)"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Collection( + templatePart.ParameterPolicies, + constraint => Assert.Equal(@"test(d+)", constraint.Content), + constraint => Assert.Equal(@"test(w+)", constraint.Content)); + } + + [Fact] + public void ParseRouteParameter_ChainedConstraints_ColonInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+):test(\w:+)"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Collection(templatePart.ParameterPolicies, + constraint => Assert.Equal(@"test(\d+)", constraint.Content), + constraint => Assert.Equal(@"test(\w:+)", constraint.Content)); + } + + [Fact] + public void ParseRouteParameter_ChainedConstraints_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+):test(\w+)=qwer"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Equal("qwer", templatePart.Default); + + Assert.Collection(templatePart.ParameterPolicies, + constraint => Assert.Equal(@"test(\d+)", constraint.Content), + constraint => Assert.Equal(@"test(\w+)", constraint.Content)); + } + + [Fact] + public void ParseRouteParameter_ChainedConstraints_WithDefaultValue_DoubleDelimiters_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\d+)::test(\w+)==qwer"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Equal("=qwer", templatePart.Default); + + Assert.Collection( + templatePart.ParameterPolicies, + constraint => Assert.Equal(@"test(\d+)", constraint.Content), + constraint => Assert.Equal(@"test(\w+)", constraint.Content)); + } + + [Theory] + [InlineData("=")] + [InlineData("+=")] + [InlineData(">= || <= || ==")] + public void ParseRouteParameter_WithDefaultValue_ContainingDelimiter(string defaultValue) + { + // Arrange & Act + var templatePart = ParseParameter($"comparison-operator:length(6)={defaultValue}"); + + // Assert + Assert.Equal("comparison-operator", templatePart.Name); + Assert.Equal(defaultValue, templatePart.Default); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal("length(6)", constraint.Content); + } + + [Fact] + public void ParseRouteTemplate_ConstraintsDefaultsAndOptionalsInMultipleSections_ParsedCorrectly() + { + // Arrange & Act + var routePattern = RoutePatternFactory.Parse(@"some/url-{p1:int:test(3)=hello}/{p2=abc}/{p3?}"); + + // Assert + var parameters = routePattern.Parameters.ToArray(); + + var param1 = parameters[0]; + Assert.Equal("p1", param1.Name); + Assert.Equal("hello", param1.Default); + Assert.False(param1.IsOptional); + + Assert.Collection(param1.ParameterPolicies, + constraint => Assert.Equal("int", constraint.Content), + constraint => Assert.Equal("test(3)", constraint.Content) + ); + + var param2 = parameters[1]; + Assert.Equal("p2", param2.Name); + Assert.Equal("abc", param2.Default); + Assert.False(param2.IsOptional); + + var param3 = parameters[2]; + Assert.Equal("p3", param3.Name); + Assert.True(param3.IsOptional); + } + + [Fact] + public void ParseRouteParameter_NoTokens_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter("world"); + + // Assert + Assert.Equal("world", templatePart.Name); + } + + [Fact] + public void ParseRouteParameter_ParamDefault_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter("param=world"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("world", templatePart.Default); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_ClosingBraceIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\})"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\})", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithClosingBraceInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\})=wer"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Equal("wer", templatePart.Default); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\})", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithClosingParenInPattern_ClosingParenIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\))"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\))", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithClosingParenInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\))=fsd"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Equal("fsd", templatePart.Default); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\))", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithColonInPattern_ColonIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(:)"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(:)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithColonInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(:)=mnf"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Equal("mnf", templatePart.Default); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(:)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithColonsInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(a:b:c)"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(a:b:c)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithColonInParamName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@":param:test=12"); + + // Assert + Assert.Equal(":param", templatePart.Name); + + Assert.Equal("12", templatePart.Default); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal("test", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithTwoColonInParamName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@":param::test=12"); + + // Assert + Assert.Equal(":param", templatePart.Name); + + Assert.Equal("12", templatePart.Default); + + Assert.Collection( + templatePart.ParameterPolicies, + constraint => Assert.Equal("test", constraint.Content)); + } + + [Fact] + public void ParseRouteParameter_EmptyConstraint_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@":param:test:"); + + // Assert + Assert.Equal(":param", templatePart.Name); + + Assert.Collection( + templatePart.ParameterPolicies, + constraint => Assert.Equal("test", constraint.Content)); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithCommaInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\w,\w)"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\w,\w)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithCommaInName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par,am:test(\w)"); + + // Assert + Assert.Equal("par,am", templatePart.Name); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\w)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithCommaInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\w,\w)=jsd"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Equal("jsd", templatePart.Default); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\w,\w)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithEqualsFollowedByQuestionMark_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:int=?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("", templatePart.Default); + + Assert.True(templatePart.IsOptional); - Assert.Equal("12", templatePart.Default); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal("test", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithTwoColonInParamName_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@":param::test=12"); - - // Assert - Assert.Equal(":param", templatePart.Name); - - Assert.Equal("12", templatePart.Default); - - Assert.Collection( - templatePart.ParameterPolicies, - constraint => Assert.Equal("test", constraint.Content)); - } - - [Fact] - public void ParseRouteParameter_EmptyConstraint_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@":param:test:"); - - // Assert - Assert.Equal(":param", templatePart.Name); - - Assert.Collection( - templatePart.ParameterPolicies, - constraint => Assert.Equal("test", constraint.Content)); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithCommaInPattern_PatternIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\w,\w)"); - - // Assert - Assert.Equal("param", templatePart.Name); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\w,\w)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithCommaInName_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"par,am:test(\w)"); - - // Assert - Assert.Equal("par,am", templatePart.Name); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\w)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithCommaInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\w,\w)=jsd"); - - // Assert - Assert.Equal("param", templatePart.Name); - - Assert.Equal("jsd", templatePart.Default); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\w,\w)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithEqualsFollowedByQuestionMark_PatternIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:int=?"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("", templatePart.Default); - - Assert.True(templatePart.IsOptional); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal("int", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_PatternIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(=)"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Null(templatePart.Default); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal("test(=)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_EqualsSignInDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param=test=bar"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("test=bar", templatePart.Default); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(a==b)"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Null(templatePart.Default); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal("test(a==b)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(a==b)=dvds"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("dvds", templatePart.Default); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal("test(a==b)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_EqualEqualSignInName_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"par==am:test=dvds"); - - // Assert - Assert.Equal("par", templatePart.Name); - Assert.Equal("=am:test=dvds", templatePart.Default); - } - - [Fact] - public void ParseRouteParameter_EqualEqualSignInDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test==dvds"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("=dvds", templatePart.Default); - } - - [Fact] - public void ParseRouteParameter_DefaultValueWithColonAndParens_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"par=am:test(asd)"); - - // Assert - Assert.Equal("par", templatePart.Name); - Assert.Equal("am:test(asd)", templatePart.Default); - } - - [Fact] - public void ParseRouteParameter_DefaultValueWithEqualsSignIn_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"par=test(am):est=asd"); - - // Assert - Assert.Equal("par", templatePart.Name); - Assert.Equal("test(am):est=asd", templatePart.Default); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(=)=sds"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("sds", templatePart.Default); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal("test(=)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_PatternIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\{)"); - - // Assert - Assert.Equal("param", templatePart.Name); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\{)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithOpenBraceInName_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"par{am:test(\sd)"); - - // Assert - Assert.Equal("par{am", templatePart.Name); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\sd)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\{)=xvc"); - - // Assert - Assert.Equal("param", templatePart.Name); - - Assert.Equal("xvc", templatePart.Default); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\{)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithOpenParenInName_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"par(am:test(\()"); - - // Assert - Assert.Equal("par(am", templatePart.Name); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\()", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithOpenParenInPattern_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\()"); - - // Assert - Assert.Equal("param", templatePart.Name); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\()", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithOpenParenNoCloseParen_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(#$%"); - - // Assert - Assert.Equal("param", templatePart.Name); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal("test(#$%", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithOpenParenAndColon_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(#:test1"); - - // Assert - Assert.Equal("param", templatePart.Name); - - Assert.Collection(templatePart.ParameterPolicies, - constraint => Assert.Equal(@"test(#", constraint.Content), - constraint => Assert.Equal(@"test1", constraint.Content)); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithOpenParenAndColonWithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(abc:somevalue):name(test1:differentname=default-value"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("default-value", templatePart.Default); - - Assert.Collection(templatePart.ParameterPolicies, - constraint => Assert.Equal(@"test(abc:somevalue)", constraint.Content), - constraint => Assert.Equal(@"name(test1", constraint.Content), - constraint => Assert.Equal(@"differentname", constraint.Content)); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithOpenParenAndDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(constraintvalue=test1"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("test1", templatePart.Default); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(constraintvalue", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithOpenParenInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\()=djk"); - - // Assert - Assert.Equal("param", templatePart.Name); - - Assert.Equal("djk", templatePart.Default); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\()", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_PatternIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\?)"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Null(templatePart.Default); - Assert.False(templatePart.IsOptional); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\?)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_PatternIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\?)?"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Null(templatePart.Default); - Assert.True(templatePart.IsOptional); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\?)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\?)=sdf"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("sdf", templatePart.Default); - Assert.False(templatePart.IsOptional); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\?)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_WithDefaultValue_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(\?)=sdf?"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Equal("sdf", templatePart.Default); - Assert.True(templatePart.IsOptional); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\?)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithQuestionMarkInName_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"par?am:test(\?)"); - - // Assert - Assert.Equal("par?am", templatePart.Name); - Assert.Null(templatePart.Default); - Assert.False(templatePart.IsOptional); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(\?)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithClosedParenAndColonInPattern_ParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(#):$)"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Null(templatePart.Default); - Assert.False(templatePart.IsOptional); - - Assert.Collection(templatePart.ParameterPolicies, - constraint => Assert.Equal(@"test(#)", constraint.Content), - constraint => Assert.Equal(@"$)", constraint.Content)); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithColonAndClosedParenInPattern_PatternIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"param:test(#:)$)"); - - // Assert - Assert.Equal("param", templatePart.Name); - Assert.Null(templatePart.Default); - Assert.False(templatePart.IsOptional); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"test(#:)$)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ContainingMultipleUnclosedParenthesisInConstraint() - { - // Arrange & Act - var templatePart = ParseParameter(@"foo:regex(\\(\\(\\(\\()"); - - // Assert - Assert.Equal("foo", templatePart.Name); - Assert.Null(templatePart.Default); - Assert.False(templatePart.IsOptional); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"regex(\\(\\(\\(\\()", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithBraces_PatternIsParsedCorrectly() - { - // Arrange & Act - var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)"); // ssn - - // Assert - Assert.Equal("p1", templatePart.Name); - Assert.Null(templatePart.Default); - Assert.False(templatePart.IsOptional); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Content); - } - - [Fact] - public void ParseRouteParameter_ConstraintWithBraces_WithDefaultValue() - { - // Arrange & Act - var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)=123-456-7890"); // ssn - - // Assert - Assert.Equal("p1", templatePart.Name); - Assert.Equal("123-456-7890", templatePart.Default); - Assert.False(templatePart.IsOptional); - - var constraint = Assert.Single(templatePart.ParameterPolicies); - Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Content); - } - - [Theory] - [InlineData("", "")] - [InlineData("?", "")] - [InlineData("*", "")] - [InlineData("**", "")] - [InlineData(" ", " ")] - [InlineData("\t", "\t")] - [InlineData("#!@#$%Q@#@%", "#!@#$%Q@#@%")] - [InlineData(",,,", ",,,")] - public void ParseRouteParameter_ParameterWithoutInlineConstraint_ReturnsTemplatePartWithEmptyInlineValues( - string parameter, - string expectedParameterName) - { - // Arrange & Act - var templatePart = ParseParameter(parameter); - - // Assert - Assert.Equal(expectedParameterName, templatePart.Name); - Assert.Empty(templatePart.ParameterPolicies); - Assert.Null(templatePart.Default); - } - - [Fact] - public void ParseRouteParameter_WithSingleAsteriskCatchAll_IsParsedCorrectly() - { - // Arrange & Act - var parameterPart = ParseParameter("*path"); - - // Assert - Assert.Equal("path", parameterPart.Name); - Assert.True(parameterPart.IsCatchAll); - Assert.Equal(RoutePatternParameterKind.CatchAll, parameterPart.ParameterKind); - Assert.True(parameterPart.EncodeSlashes); - } - - [Fact] - public void ParseRouteParameter_WithSingleAsteriskCatchAll_AndDefaultValue_IsParsedCorrectly() - { - // Arrange & Act - var parameterPart = ParseParameter("*path=a/b/c"); - - // Assert - Assert.Equal("path", parameterPart.Name); - Assert.True(parameterPart.IsCatchAll); - Assert.NotNull(parameterPart.Default); - Assert.Equal("a/b/c", parameterPart.Default.ToString()); - Assert.Equal(RoutePatternParameterKind.CatchAll, parameterPart.ParameterKind); - Assert.True(parameterPart.EncodeSlashes); - } - - [Fact] - public void ParseRouteParameter_WithSingleAsteriskCatchAll_AndConstraints_IsParsedCorrectly() - { - // Arrange - var constraintContent = "regex(^(/[^/ ]*)+/?$)"; - - // Act - var parameterPart = ParseParameter($"*path:{constraintContent}"); - - // Assert - Assert.Equal("path", parameterPart.Name); - Assert.True(parameterPart.IsCatchAll); - Assert.Equal(RoutePatternParameterKind.CatchAll, parameterPart.ParameterKind); - var constraintReference = Assert.Single(parameterPart.ParameterPolicies); - Assert.Equal(constraintContent, constraintReference.Content); - Assert.True(parameterPart.EncodeSlashes); - } - - [Fact] - public void ParseRouteParameter_WithSingleAsteriskCatchAll_AndConstraints_AndDefaultValue_IsParsedCorrectly() - { - // Arrange - var constraintContent = "regex(^(/[^/ ]*)+/?$)"; - - // Act - var parameterPart = ParseParameter($"*path:{constraintContent}=a/b/c"); - - // Assert - Assert.Equal("path", parameterPart.Name); - Assert.True(parameterPart.IsCatchAll); - Assert.Equal(RoutePatternParameterKind.CatchAll, parameterPart.ParameterKind); - var constraintReference = Assert.Single(parameterPart.ParameterPolicies); - Assert.Equal(constraintContent, constraintReference.Content); - Assert.NotNull(parameterPart.Default); - Assert.Equal("a/b/c", parameterPart.Default.ToString()); - Assert.True(parameterPart.EncodeSlashes); - } - - [Fact] - public void ParseRouteParameter_WithDoubleAsteriskCatchAll_IsParsedCorrectly() - { - // Arrange & Act - var parameterPart = ParseParameter("**path"); - - // Assert - Assert.Equal("path", parameterPart.Name); - Assert.True(parameterPart.IsCatchAll); - Assert.False(parameterPart.EncodeSlashes); - } - - [Fact] - public void ParseRouteParameter_WithDoubleAsteriskCatchAll_AndDefaultValue_IsParsedCorrectly() - { - // Arrange & Act - var parameterPart = ParseParameter("**path=a/b/c"); - - // Assert - Assert.Equal("path", parameterPart.Name); - Assert.True(parameterPart.IsCatchAll); - Assert.NotNull(parameterPart.Default); - Assert.Equal("a/b/c", parameterPart.Default.ToString()); - Assert.False(parameterPart.EncodeSlashes); - } - - [Fact] - public void ParseRouteParameter_WithDoubleAsteriskCatchAll_AndConstraints_IsParsedCorrectly() - { - // Arrange - var constraintContent = "regex(^(/[^/ ]*)+/?$)"; - - // Act - var parameterPart = ParseParameter($"**path:{constraintContent}"); - - // Assert - Assert.Equal("path", parameterPart.Name); - Assert.True(parameterPart.IsCatchAll); - Assert.False(parameterPart.EncodeSlashes); - var constraintReference = Assert.Single(parameterPart.ParameterPolicies); - Assert.Equal(constraintContent, constraintReference.Content); - } - - [Fact] - public void ParseRouteParameter_WithDoubleAsteriskCatchAll_AndConstraints_AndDefaultValue_IsParsedCorrectly() - { - // Arrange - var constraintContent = "regex(^(/[^/ ]*)+/?$)"; - - // Act - var parameterPart = ParseParameter($"**path:{constraintContent}=a/b/c"); - - // Assert - Assert.Equal("path", parameterPart.Name); - Assert.True(parameterPart.IsCatchAll); - Assert.False(parameterPart.EncodeSlashes); - var constraintReference = Assert.Single(parameterPart.ParameterPolicies); - Assert.Equal(constraintContent, constraintReference.Content); - Assert.NotNull(parameterPart.Default); - Assert.Equal("a/b/c", parameterPart.Default.ToString()); - } - - private RoutePatternParameterPart ParseParameter(string routeParameter) - { - // See: #475 - these tests don't pass the 'whole' text. - var templatePart = RouteParameterParser.ParseRouteParameter(routeParameter); - return templatePart; - } + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal("int", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(=)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.Default); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal("test(=)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_EqualsSignInDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param=test=bar"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("test=bar", templatePart.Default); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(a==b)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.Default); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal("test(a==b)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithEqualEqualSignInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(a==b)=dvds"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("dvds", templatePart.Default); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal("test(a==b)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_EqualEqualSignInName_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par==am:test=dvds"); + + // Assert + Assert.Equal("par", templatePart.Name); + Assert.Equal("=am:test=dvds", templatePart.Default); + } + + [Fact] + public void ParseRouteParameter_EqualEqualSignInDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test==dvds"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("=dvds", templatePart.Default); + } + + [Fact] + public void ParseRouteParameter_DefaultValueWithColonAndParens_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par=am:test(asd)"); + + // Assert + Assert.Equal("par", templatePart.Name); + Assert.Equal("am:test(asd)", templatePart.Default); + } + + [Fact] + public void ParseRouteParameter_DefaultValueWithEqualsSignIn_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par=test(am):est=asd"); + + // Assert + Assert.Equal("par", templatePart.Name); + Assert.Equal("test(am):est=asd", templatePart.Default); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithEqualsSignInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(=)=sds"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("sds", templatePart.Default); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal("test(=)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\{)"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\{)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenBraceInName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par{am:test(\sd)"); + + // Assert + Assert.Equal("par{am", templatePart.Name); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\sd)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenBraceInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\{)=xvc"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Equal("xvc", templatePart.Default); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\{)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenInName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par(am:test(\()"); + + // Assert + Assert.Equal("par(am", templatePart.Name); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\()", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\()"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\()", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenNoCloseParen_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(#$%"); + + // Assert + Assert.Equal("param", templatePart.Name); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal("test(#$%", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenAndColon_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(#:test1"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Collection(templatePart.ParameterPolicies, + constraint => Assert.Equal(@"test(#", constraint.Content), + constraint => Assert.Equal(@"test1", constraint.Content)); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenAndColonWithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(abc:somevalue):name(test1:differentname=default-value"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("default-value", templatePart.Default); + + Assert.Collection(templatePart.ParameterPolicies, + constraint => Assert.Equal(@"test(abc:somevalue)", constraint.Content), + constraint => Assert.Equal(@"name(test1", constraint.Content), + constraint => Assert.Equal(@"differentname", constraint.Content)); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenAndDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(constraintvalue=test1"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("test1", templatePart.Default); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(constraintvalue", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithOpenParenInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\()=djk"); + + // Assert + Assert.Equal("param", templatePart.Name); + + Assert.Equal("djk", templatePart.Default); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\()", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\?)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.Default); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\?)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\?)?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.Default); + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\?)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\?)=sdf"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("sdf", templatePart.Default); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\?)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInPattern_Optional_WithDefaultValue_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(\?)=sdf?"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Equal("sdf", templatePart.Default); + Assert.True(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\?)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithQuestionMarkInName_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"par?am:test(\?)"); + + // Assert + Assert.Equal("par?am", templatePart.Name); + Assert.Null(templatePart.Default); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(\?)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithClosedParenAndColonInPattern_ParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(#):$)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.Default); + Assert.False(templatePart.IsOptional); + + Assert.Collection(templatePart.ParameterPolicies, + constraint => Assert.Equal(@"test(#)", constraint.Content), + constraint => Assert.Equal(@"$)", constraint.Content)); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithColonAndClosedParenInPattern_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"param:test(#:)$)"); + + // Assert + Assert.Equal("param", templatePart.Name); + Assert.Null(templatePart.Default); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"test(#:)$)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ContainingMultipleUnclosedParenthesisInConstraint() + { + // Arrange & Act + var templatePart = ParseParameter(@"foo:regex(\\(\\(\\(\\()"); + + // Assert + Assert.Equal("foo", templatePart.Name); + Assert.Null(templatePart.Default); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"regex(\\(\\(\\(\\()", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithBraces_PatternIsParsedCorrectly() + { + // Arrange & Act + var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)"); // ssn + + // Assert + Assert.Equal("p1", templatePart.Name); + Assert.Null(templatePart.Default); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Content); + } + + [Fact] + public void ParseRouteParameter_ConstraintWithBraces_WithDefaultValue() + { + // Arrange & Act + var templatePart = ParseParameter(@"p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)=123-456-7890"); // ssn + + // Assert + Assert.Equal("p1", templatePart.Name); + Assert.Equal("123-456-7890", templatePart.Default); + Assert.False(templatePart.IsOptional); + + var constraint = Assert.Single(templatePart.ParameterPolicies); + Assert.Equal(@"regex(^\d{{3}}-\d{{3}}-\d{{4}}$)", constraint.Content); + } + + [Theory] + [InlineData("", "")] + [InlineData("?", "")] + [InlineData("*", "")] + [InlineData("**", "")] + [InlineData(" ", " ")] + [InlineData("\t", "\t")] + [InlineData("#!@#$%Q@#@%", "#!@#$%Q@#@%")] + [InlineData(",,,", ",,,")] + public void ParseRouteParameter_ParameterWithoutInlineConstraint_ReturnsTemplatePartWithEmptyInlineValues( + string parameter, + string expectedParameterName) + { + // Arrange & Act + var templatePart = ParseParameter(parameter); + + // Assert + Assert.Equal(expectedParameterName, templatePart.Name); + Assert.Empty(templatePart.ParameterPolicies); + Assert.Null(templatePart.Default); + } + + [Fact] + public void ParseRouteParameter_WithSingleAsteriskCatchAll_IsParsedCorrectly() + { + // Arrange & Act + var parameterPart = ParseParameter("*path"); + + // Assert + Assert.Equal("path", parameterPart.Name); + Assert.True(parameterPart.IsCatchAll); + Assert.Equal(RoutePatternParameterKind.CatchAll, parameterPart.ParameterKind); + Assert.True(parameterPart.EncodeSlashes); + } + + [Fact] + public void ParseRouteParameter_WithSingleAsteriskCatchAll_AndDefaultValue_IsParsedCorrectly() + { + // Arrange & Act + var parameterPart = ParseParameter("*path=a/b/c"); + + // Assert + Assert.Equal("path", parameterPart.Name); + Assert.True(parameterPart.IsCatchAll); + Assert.NotNull(parameterPart.Default); + Assert.Equal("a/b/c", parameterPart.Default.ToString()); + Assert.Equal(RoutePatternParameterKind.CatchAll, parameterPart.ParameterKind); + Assert.True(parameterPart.EncodeSlashes); + } + + [Fact] + public void ParseRouteParameter_WithSingleAsteriskCatchAll_AndConstraints_IsParsedCorrectly() + { + // Arrange + var constraintContent = "regex(^(/[^/ ]*)+/?$)"; + + // Act + var parameterPart = ParseParameter($"*path:{constraintContent}"); + + // Assert + Assert.Equal("path", parameterPart.Name); + Assert.True(parameterPart.IsCatchAll); + Assert.Equal(RoutePatternParameterKind.CatchAll, parameterPart.ParameterKind); + var constraintReference = Assert.Single(parameterPart.ParameterPolicies); + Assert.Equal(constraintContent, constraintReference.Content); + Assert.True(parameterPart.EncodeSlashes); + } + + [Fact] + public void ParseRouteParameter_WithSingleAsteriskCatchAll_AndConstraints_AndDefaultValue_IsParsedCorrectly() + { + // Arrange + var constraintContent = "regex(^(/[^/ ]*)+/?$)"; + + // Act + var parameterPart = ParseParameter($"*path:{constraintContent}=a/b/c"); + + // Assert + Assert.Equal("path", parameterPart.Name); + Assert.True(parameterPart.IsCatchAll); + Assert.Equal(RoutePatternParameterKind.CatchAll, parameterPart.ParameterKind); + var constraintReference = Assert.Single(parameterPart.ParameterPolicies); + Assert.Equal(constraintContent, constraintReference.Content); + Assert.NotNull(parameterPart.Default); + Assert.Equal("a/b/c", parameterPart.Default.ToString()); + Assert.True(parameterPart.EncodeSlashes); + } + + [Fact] + public void ParseRouteParameter_WithDoubleAsteriskCatchAll_IsParsedCorrectly() + { + // Arrange & Act + var parameterPart = ParseParameter("**path"); + + // Assert + Assert.Equal("path", parameterPart.Name); + Assert.True(parameterPart.IsCatchAll); + Assert.False(parameterPart.EncodeSlashes); + } + + [Fact] + public void ParseRouteParameter_WithDoubleAsteriskCatchAll_AndDefaultValue_IsParsedCorrectly() + { + // Arrange & Act + var parameterPart = ParseParameter("**path=a/b/c"); + + // Assert + Assert.Equal("path", parameterPart.Name); + Assert.True(parameterPart.IsCatchAll); + Assert.NotNull(parameterPart.Default); + Assert.Equal("a/b/c", parameterPart.Default.ToString()); + Assert.False(parameterPart.EncodeSlashes); + } + + [Fact] + public void ParseRouteParameter_WithDoubleAsteriskCatchAll_AndConstraints_IsParsedCorrectly() + { + // Arrange + var constraintContent = "regex(^(/[^/ ]*)+/?$)"; + + // Act + var parameterPart = ParseParameter($"**path:{constraintContent}"); + + // Assert + Assert.Equal("path", parameterPart.Name); + Assert.True(parameterPart.IsCatchAll); + Assert.False(parameterPart.EncodeSlashes); + var constraintReference = Assert.Single(parameterPart.ParameterPolicies); + Assert.Equal(constraintContent, constraintReference.Content); + } + + [Fact] + public void ParseRouteParameter_WithDoubleAsteriskCatchAll_AndConstraints_AndDefaultValue_IsParsedCorrectly() + { + // Arrange + var constraintContent = "regex(^(/[^/ ]*)+/?$)"; + + // Act + var parameterPart = ParseParameter($"**path:{constraintContent}=a/b/c"); + + // Assert + Assert.Equal("path", parameterPart.Name); + Assert.True(parameterPart.IsCatchAll); + Assert.False(parameterPart.EncodeSlashes); + var constraintReference = Assert.Single(parameterPart.ParameterPolicies); + Assert.Equal(constraintContent, constraintReference.Content); + Assert.NotNull(parameterPart.Default); + Assert.Equal("a/b/c", parameterPart.Default.ToString()); + } + + private RoutePatternParameterPart ParseParameter(string routeParameter) + { + // See: #475 - these tests don't pass the 'whole' text. + var templatePart = RouteParameterParser.ParseRouteParameter(routeParameter); + return templatePart; } } diff --git a/src/Http/Routing/test/UnitTests/Patterns/RoutePatternFactoryTest.cs b/src/Http/Routing/test/UnitTests/Patterns/RoutePatternFactoryTest.cs index b0bbc3b0cb..68878bd8f5 100644 --- a/src/Http/Routing/test/UnitTests/Patterns/RoutePatternFactoryTest.cs +++ b/src/Http/Routing/test/UnitTests/Patterns/RoutePatternFactoryTest.cs @@ -9,713 +9,712 @@ using Microsoft.AspNetCore.Routing.Template; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +public class RoutePatternFactoryTest { - public class RoutePatternFactoryTest + [Fact] + public void Pattern_MergesDefaultValues() { - [Fact] - public void Pattern_MergesDefaultValues() - { - // Arrange - var template = "{a}/{b}/{c=19}"; - var defaults = new { a = "15", b = 17 }; - var constraints = new { }; - - var original = RoutePatternFactory.Parse(template); - - // Act - var actual = RoutePatternFactory.Pattern( - original.RawText, - defaults, - constraints, - original.PathSegments); - - // Assert - Assert.Equal("15", actual.GetParameter("a").Default); - Assert.Equal(17, actual.GetParameter("b").Default); - Assert.Equal("19", actual.GetParameter("c").Default); - - Assert.Collection( - actual.Defaults.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("a", kvp.Key); Assert.Equal("15", kvp.Value); }, - kvp => { Assert.Equal("b", kvp.Key); Assert.Equal(17, kvp.Value); }, - kvp => { Assert.Equal("c", kvp.Key); Assert.Equal("19", kvp.Value); }); - } - - [Fact] - public void Pattern_ExtraDefaultValues() - { - // Arrange - var template = "{a}/{b}/{c}"; - var defaults = new { d = "15", e = 17 }; - var constraints = new { }; - - var original = RoutePatternFactory.Parse(template); - - // Act - var actual = RoutePatternFactory.Pattern( - original.RawText, - defaults, - constraints, - original.PathSegments); - - // Assert - Assert.Collection( - actual.Defaults.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("d", kvp.Key); Assert.Equal("15", kvp.Value); }, - kvp => { Assert.Equal("e", kvp.Key); Assert.Equal(17, kvp.Value); }); - } - - [Fact] - public void Pattern_DifferentDuplicateDefaultValue_Throws() - { - // Arrange - var template = "{a=13}/{b}/{c}"; - var defaults = new { a = "15", }; - var constraints = new { }; - - var original = RoutePatternFactory.Parse(template); - - // Act - var ex = Assert.Throws(() => RoutePatternFactory.Pattern( - original.RawText, - defaults, - constraints, - original.PathSegments)); - - // Assert - Assert.Equal( - "The route parameter 'a' has both an inline default value and an explicit default " + - "value specified. A route parameter cannot contain an inline default value when a " + - "default value is specified explicitly. Consider removing one of them.", - ex.Message); - } - - [Fact] - public void Pattern_SameDuplicateDefaultValue() - { - // Arrange - var template = "{a=13}/{b}/{c}"; - var defaults = new { a = "13", }; - var constraints = new { }; - - var original = RoutePatternFactory.Parse(template); - - // Act - var actual = RoutePatternFactory.Pattern( - original.RawText, - defaults, - constraints, - original.PathSegments); - - // Assert - Assert.Collection( - actual.Defaults, - kvp => { Assert.Equal("a", kvp.Key); Assert.Equal("13", kvp.Value); }); - } - - [Fact] - public void Pattern_OptionalParameterDefaultValue_Throws() - { - // Arrange - var template = "{a}/{b}/{c?}"; - var defaults = new { c = "15", }; - var constraints = new { }; - - var original = RoutePatternFactory.Parse(template); - - // Act - var ex = Assert.Throws(() => RoutePatternFactory.Pattern( - original.RawText, - defaults, - constraints, - original.PathSegments)); - - // Assert - Assert.Equal( - "An optional parameter cannot have default value.", - ex.Message); - } - - [Fact] - public void Pattern_MergesConstraints() - { - // Arrange - var template = "{a:int}/{b}/{c}"; - var defaults = new { }; - var constraints = new { a = new RegexRouteConstraint("foo"), b = new RegexRouteConstraint("bar") }; - - var original = RoutePatternFactory.Parse(template); - - // Act - var actual = RoutePatternFactory.Pattern( - original.RawText, - defaults, - constraints, - original.PathSegments); - - // Assert - Assert.Collection( - actual.GetParameter("a").ParameterPolicies, - c => Assert.IsType(c.ParameterPolicy), - c => Assert.Equal("int", c.Content)); - Assert.Collection( - actual.GetParameter("b").ParameterPolicies, - c => Assert.IsType(c.ParameterPolicy)); - - Assert.Collection( - actual.ParameterPolicies.OrderBy(kvp => kvp.Key), - kvp => - { - Assert.Equal("a", kvp.Key); - Assert.Collection( - kvp.Value, - c => Assert.IsType(c.ParameterPolicy), - c => Assert.Equal("int", c.Content)); - }, - kvp => - { - Assert.Equal("b", kvp.Key); - Assert.Collection( - kvp.Value, - c => Assert.IsType(c.ParameterPolicy)); - }); - } - - [Fact] - public void Pattern_ExtraConstraints() - { - // Arrange - var template = "{a}/{b}/{c}"; - var defaults = new { }; - var constraints = new { d = new RegexRouteConstraint("foo"), e = new RegexRouteConstraint("bar") }; - - var original = RoutePatternFactory.Parse(template); - - // Act - var actual = RoutePatternFactory.Pattern( - original.RawText, - defaults, - constraints, - original.PathSegments); - - // Assert - Assert.Collection( - actual.ParameterPolicies.OrderBy(kvp => kvp.Key), - kvp => - { - Assert.Equal("d", kvp.Key); - Assert.Collection( - kvp.Value, - c => Assert.IsType(c.ParameterPolicy)); - }, - kvp => - { - Assert.Equal("e", kvp.Key); - Assert.Collection( - kvp.Value, - c => Assert.IsType(c.ParameterPolicy)); - }); - } - - [Fact] - public void Pattern_ExtraConstraints_MultipleConstraintsForKey() - { - // Arrange - var template = "{a}/{b}/{c}"; - var defaults = new { }; - var constraints = new { d = new object[] { new RegexRouteConstraint("foo"), new RegexRouteConstraint("bar"), "baz" } }; - - var original = RoutePatternFactory.Parse(template); + // Arrange + var template = "{a}/{b}/{c=19}"; + var defaults = new { a = "15", b = 17 }; + var constraints = new { }; + + var original = RoutePatternFactory.Parse(template); + + // Act + var actual = RoutePatternFactory.Pattern( + original.RawText, + defaults, + constraints, + original.PathSegments); + + // Assert + Assert.Equal("15", actual.GetParameter("a").Default); + Assert.Equal(17, actual.GetParameter("b").Default); + Assert.Equal("19", actual.GetParameter("c").Default); + + Assert.Collection( + actual.Defaults.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("a", kvp.Key); Assert.Equal("15", kvp.Value); }, + kvp => { Assert.Equal("b", kvp.Key); Assert.Equal(17, kvp.Value); }, + kvp => { Assert.Equal("c", kvp.Key); Assert.Equal("19", kvp.Value); }); + } - // Act - var actual = RoutePatternFactory.Pattern( - original.RawText, - defaults, - constraints, - original.PathSegments); + [Fact] + public void Pattern_ExtraDefaultValues() + { + // Arrange + var template = "{a}/{b}/{c}"; + var defaults = new { d = "15", e = 17 }; + var constraints = new { }; + + var original = RoutePatternFactory.Parse(template); + + // Act + var actual = RoutePatternFactory.Pattern( + original.RawText, + defaults, + constraints, + original.PathSegments); + + // Assert + Assert.Collection( + actual.Defaults.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("d", kvp.Key); Assert.Equal("15", kvp.Value); }, + kvp => { Assert.Equal("e", kvp.Key); Assert.Equal(17, kvp.Value); }); + } - // Assert - Assert.Collection( - actual.ParameterPolicies.OrderBy(kvp => kvp.Key), - kvp => - { - Assert.Equal("d", kvp.Key); - Assert.Collection( - kvp.Value, - c => Assert.Equal("foo", Assert.IsType(c.ParameterPolicy).Constraint.ToString()), - c => Assert.Equal("bar", Assert.IsType(c.ParameterPolicy).Constraint.ToString()), - c => Assert.Equal("^(baz)$", Assert.IsType(c.ParameterPolicy).Constraint.ToString())); - }); - } - - [Fact] - public void Pattern_ExtraConstraints_MergeMultipleConstraintsForKey() - { - // Arrange - var template = "{a:int}/{b}/{c:int}"; - var defaults = new { }; - var constraints = new { b = "fizz", c = new object[] { new RegexRouteConstraint("foo"), new RegexRouteConstraint("bar"), "baz" } }; + [Fact] + public void Pattern_DifferentDuplicateDefaultValue_Throws() + { + // Arrange + var template = "{a=13}/{b}/{c}"; + var defaults = new { a = "15", }; + var constraints = new { }; + + var original = RoutePatternFactory.Parse(template); + + // Act + var ex = Assert.Throws(() => RoutePatternFactory.Pattern( + original.RawText, + defaults, + constraints, + original.PathSegments)); + + // Assert + Assert.Equal( + "The route parameter 'a' has both an inline default value and an explicit default " + + "value specified. A route parameter cannot contain an inline default value when a " + + "default value is specified explicitly. Consider removing one of them.", + ex.Message); + } - var original = RoutePatternFactory.Parse(template); + [Fact] + public void Pattern_SameDuplicateDefaultValue() + { + // Arrange + var template = "{a=13}/{b}/{c}"; + var defaults = new { a = "13", }; + var constraints = new { }; + + var original = RoutePatternFactory.Parse(template); + + // Act + var actual = RoutePatternFactory.Pattern( + original.RawText, + defaults, + constraints, + original.PathSegments); + + // Assert + Assert.Collection( + actual.Defaults, + kvp => { Assert.Equal("a", kvp.Key); Assert.Equal("13", kvp.Value); }); + } - // Act - var actual = RoutePatternFactory.Pattern( - original.RawText, - defaults, - constraints, - original.PathSegments); + [Fact] + public void Pattern_OptionalParameterDefaultValue_Throws() + { + // Arrange + var template = "{a}/{b}/{c?}"; + var defaults = new { c = "15", }; + var constraints = new { }; + + var original = RoutePatternFactory.Parse(template); + + // Act + var ex = Assert.Throws(() => RoutePatternFactory.Pattern( + original.RawText, + defaults, + constraints, + original.PathSegments)); + + // Assert + Assert.Equal( + "An optional parameter cannot have default value.", + ex.Message); + } - // Assert - Assert.Collection( - actual.ParameterPolicies.OrderBy(kvp => kvp.Key), - kvp => - { - Assert.Equal("a", kvp.Key); - Assert.Collection( - kvp.Value, - c => Assert.Equal("int", c.Content)); - }, - kvp => - { - Assert.Equal("b", kvp.Key); - Assert.Collection( - kvp.Value, - c => Assert.Equal("^(fizz)$", Assert.IsType(c.ParameterPolicy).Constraint.ToString())); - }, - kvp => - { - Assert.Equal("c", kvp.Key); - Assert.Collection( - kvp.Value, - c => Assert.Equal("foo", Assert.IsType(c.ParameterPolicy).Constraint.ToString()), - c => Assert.Equal("bar", Assert.IsType(c.ParameterPolicy).Constraint.ToString()), - c => Assert.Equal("^(baz)$", Assert.IsType(c.ParameterPolicy).Constraint.ToString()), - c => Assert.Equal("int", c.Content)); - }); - } - - [Fact] - public void Pattern_ExtraConstraints_NestedArray_Throws() - { - // Arrange - var template = "{a}/{b}/{c:int}"; - var defaults = new { }; - var constraints = new { c = new object[] { new object[0] } }; + [Fact] + public void Pattern_MergesConstraints() + { + // Arrange + var template = "{a:int}/{b}/{c}"; + var defaults = new { }; + var constraints = new { a = new RegexRouteConstraint("foo"), b = new RegexRouteConstraint("bar") }; + + var original = RoutePatternFactory.Parse(template); + + // Act + var actual = RoutePatternFactory.Pattern( + original.RawText, + defaults, + constraints, + original.PathSegments); + + // Assert + Assert.Collection( + actual.GetParameter("a").ParameterPolicies, + c => Assert.IsType(c.ParameterPolicy), + c => Assert.Equal("int", c.Content)); + Assert.Collection( + actual.GetParameter("b").ParameterPolicies, + c => Assert.IsType(c.ParameterPolicy)); + + Assert.Collection( + actual.ParameterPolicies.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("a", kvp.Key); + Assert.Collection( + kvp.Value, + c => Assert.IsType(c.ParameterPolicy), + c => Assert.Equal("int", c.Content)); + }, + kvp => + { + Assert.Equal("b", kvp.Key); + Assert.Collection( + kvp.Value, + c => Assert.IsType(c.ParameterPolicy)); + }); + } - var original = RoutePatternFactory.Parse(template); + [Fact] + public void Pattern_ExtraConstraints() + { + // Arrange + var template = "{a}/{b}/{c}"; + var defaults = new { }; + var constraints = new { d = new RegexRouteConstraint("foo"), e = new RegexRouteConstraint("bar") }; + + var original = RoutePatternFactory.Parse(template); + + // Act + var actual = RoutePatternFactory.Pattern( + original.RawText, + defaults, + constraints, + original.PathSegments); + + // Assert + Assert.Collection( + actual.ParameterPolicies.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("d", kvp.Key); + Assert.Collection( + kvp.Value, + c => Assert.IsType(c.ParameterPolicy)); + }, + kvp => + { + Assert.Equal("e", kvp.Key); + Assert.Collection( + kvp.Value, + c => Assert.IsType(c.ParameterPolicy)); + }); + } - // Act & Assert - Assert.Throws(() => + [Fact] + public void Pattern_ExtraConstraints_MultipleConstraintsForKey() + { + // Arrange + var template = "{a}/{b}/{c}"; + var defaults = new { }; + var constraints = new { d = new object[] { new RegexRouteConstraint("foo"), new RegexRouteConstraint("bar"), "baz" } }; + + var original = RoutePatternFactory.Parse(template); + + // Act + var actual = RoutePatternFactory.Pattern( + original.RawText, + defaults, + constraints, + original.PathSegments); + + // Assert + Assert.Collection( + actual.ParameterPolicies.OrderBy(kvp => kvp.Key), + kvp => { - RoutePatternFactory.Pattern( - original.RawText, - defaults, - constraints, - original.PathSegments); + Assert.Equal("d", kvp.Key); + Assert.Collection( + kvp.Value, + c => Assert.Equal("foo", Assert.IsType(c.ParameterPolicy).Constraint.ToString()), + c => Assert.Equal("bar", Assert.IsType(c.ParameterPolicy).Constraint.ToString()), + c => Assert.Equal("^(baz)$", Assert.IsType(c.ParameterPolicy).Constraint.ToString())); }); - } + } - [Fact] - public void Pattern_ExtraConstraints_RouteConstraint() - { - // Arrange - var template = "{a}/{b}/{c}"; - var defaults = new { }; - var constraints = new { d = Mock.Of(), e = Mock.Of(), }; + [Fact] + public void Pattern_ExtraConstraints_MergeMultipleConstraintsForKey() + { + // Arrange + var template = "{a:int}/{b}/{c:int}"; + var defaults = new { }; + var constraints = new { b = "fizz", c = new object[] { new RegexRouteConstraint("foo"), new RegexRouteConstraint("bar"), "baz" } }; + + var original = RoutePatternFactory.Parse(template); + + // Act + var actual = RoutePatternFactory.Pattern( + original.RawText, + defaults, + constraints, + original.PathSegments); + + // Assert + Assert.Collection( + actual.ParameterPolicies.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("a", kvp.Key); + Assert.Collection( + kvp.Value, + c => Assert.Equal("int", c.Content)); + }, + kvp => + { + Assert.Equal("b", kvp.Key); + Assert.Collection( + kvp.Value, + c => Assert.Equal("^(fizz)$", Assert.IsType(c.ParameterPolicy).Constraint.ToString())); + }, + kvp => + { + Assert.Equal("c", kvp.Key); + Assert.Collection( + kvp.Value, + c => Assert.Equal("foo", Assert.IsType(c.ParameterPolicy).Constraint.ToString()), + c => Assert.Equal("bar", Assert.IsType(c.ParameterPolicy).Constraint.ToString()), + c => Assert.Equal("^(baz)$", Assert.IsType(c.ParameterPolicy).Constraint.ToString()), + c => Assert.Equal("int", c.Content)); + }); + } - var original = RoutePatternFactory.Parse(template); + [Fact] + public void Pattern_ExtraConstraints_NestedArray_Throws() + { + // Arrange + var template = "{a}/{b}/{c:int}"; + var defaults = new { }; + var constraints = new { c = new object[] { new object[0] } }; - // Act - var actual = RoutePatternFactory.Pattern( - original.RawText, - defaults, - constraints, - original.PathSegments); + var original = RoutePatternFactory.Parse(template); - // Assert - Assert.Collection( - actual.ParameterPolicies.OrderBy(kvp => kvp.Key), - kvp => - { - Assert.Equal("d", kvp.Key); - Assert.Collection( - kvp.Value, - c => Assert.NotNull(c.ParameterPolicy)); - }, - kvp => - { - Assert.Equal("e", kvp.Key); - Assert.Collection( - kvp.Value, - c => Assert.NotNull(c.ParameterPolicy)); - }); - } - - [Fact] - public void Pattern_CreatesConstraintFromString() + // Act & Assert + Assert.Throws(() => { - // Arrange - var template = "{a}/{b}/{c}"; - var defaults = new { }; - var constraints = new { d = "foo", }; - - var original = RoutePatternFactory.Parse(template); - - // Act - var actual = RoutePatternFactory.Pattern( + RoutePatternFactory.Pattern( original.RawText, defaults, constraints, original.PathSegments); + }); + } - // Assert - Assert.Collection( - actual.ParameterPolicies.OrderBy(kvp => kvp.Key), - kvp => - { - Assert.Equal("d", kvp.Key); - var regex = Assert.IsType(Assert.Single(kvp.Value).ParameterPolicy); - Assert.Equal("^(foo)$", regex.Constraint.ToString()); - }); - } - - [Fact] - public void Pattern_InvalidConstraintTypeThrows() - { - // Arrange - var template = "{a}/{b}/{c}"; - var defaults = new { }; - var constraints = new { d = 17, }; - - var original = RoutePatternFactory.Parse(template); + [Fact] + public void Pattern_ExtraConstraints_RouteConstraint() + { + // Arrange + var template = "{a}/{b}/{c}"; + var defaults = new { }; + var constraints = new { d = Mock.Of(), e = Mock.Of(), }; + + var original = RoutePatternFactory.Parse(template); + + // Act + var actual = RoutePatternFactory.Pattern( + original.RawText, + defaults, + constraints, + original.PathSegments); + + // Assert + Assert.Collection( + actual.ParameterPolicies.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("d", kvp.Key); + Assert.Collection( + kvp.Value, + c => Assert.NotNull(c.ParameterPolicy)); + }, + kvp => + { + Assert.Equal("e", kvp.Key); + Assert.Collection( + kvp.Value, + c => Assert.NotNull(c.ParameterPolicy)); + }); + } - // Act - var ex = Assert.Throws(() => RoutePatternFactory.Pattern( - original.RawText, - defaults, - constraints, - original.PathSegments)); + [Fact] + public void Pattern_CreatesConstraintFromString() + { + // Arrange + var template = "{a}/{b}/{c}"; + var defaults = new { }; + var constraints = new { d = "foo", }; + + var original = RoutePatternFactory.Parse(template); + + // Act + var actual = RoutePatternFactory.Pattern( + original.RawText, + defaults, + constraints, + original.PathSegments); + + // Assert + Assert.Collection( + actual.ParameterPolicies.OrderBy(kvp => kvp.Key), + kvp => + { + Assert.Equal("d", kvp.Key); + var regex = Assert.IsType(Assert.Single(kvp.Value).ParameterPolicy); + Assert.Equal("^(foo)$", regex.Constraint.ToString()); + }); + } - // Assert - Assert.Equal( - $"Invalid constraint '17'. A constraint must be of type 'string' or '{typeof(IRouteConstraint)}'.", - ex.Message); - } + [Fact] + public void Pattern_InvalidConstraintTypeThrows() + { + // Arrange + var template = "{a}/{b}/{c}"; + var defaults = new { }; + var constraints = new { d = 17, }; + + var original = RoutePatternFactory.Parse(template); + + // Act + var ex = Assert.Throws(() => RoutePatternFactory.Pattern( + original.RawText, + defaults, + constraints, + original.PathSegments)); + + // Assert + Assert.Equal( + $"Invalid constraint '17'. A constraint must be of type 'string' or '{typeof(IRouteConstraint)}'.", + ex.Message); + } - [Fact] - public void Pattern_ArrayOfSegments_ShouldMakeCopyOfArrayOfSegments() - { - // Arrange - var literalPartA = RoutePatternFactory.LiteralPart("A"); - var paramPartB = RoutePatternFactory.ParameterPart("B"); - var paramPartC = RoutePatternFactory.ParameterPart("C"); - var paramPartD = RoutePatternFactory.ParameterPart("D"); - var segments = new[] - { + [Fact] + public void Pattern_ArrayOfSegments_ShouldMakeCopyOfArrayOfSegments() + { + // Arrange + var literalPartA = RoutePatternFactory.LiteralPart("A"); + var paramPartB = RoutePatternFactory.ParameterPart("B"); + var paramPartC = RoutePatternFactory.ParameterPart("C"); + var paramPartD = RoutePatternFactory.ParameterPart("D"); + var segments = new[] + { RoutePatternFactory.Segment(literalPartA, paramPartB), RoutePatternFactory.Segment(paramPartC, literalPartA), RoutePatternFactory.Segment(paramPartD), RoutePatternFactory.Segment(literalPartA) }; - // Act - var actual = RoutePatternFactory.Pattern(segments); - segments[1] = RoutePatternFactory.Segment(RoutePatternFactory.ParameterPart("E")); - Array.Resize(ref segments, 2); + // Act + var actual = RoutePatternFactory.Pattern(segments); + segments[1] = RoutePatternFactory.Segment(RoutePatternFactory.ParameterPart("E")); + Array.Resize(ref segments, 2); - // Assert - Assert.Equal(3, actual.Parameters.Count); - Assert.Same(paramPartB, actual.Parameters[0]); - Assert.Same(paramPartC, actual.Parameters[1]); - Assert.Same(paramPartD, actual.Parameters[2]); - } + // Assert + Assert.Equal(3, actual.Parameters.Count); + Assert.Same(paramPartB, actual.Parameters[0]); + Assert.Same(paramPartC, actual.Parameters[1]); + Assert.Same(paramPartD, actual.Parameters[2]); + } - [Fact] - public void Pattern_RawTextAndArrayOfSegments_ShouldMakeCopyOfArrayOfSegments() - { - // Arrange - var rawText = "raw"; - var literalPartA = RoutePatternFactory.LiteralPart("A"); - var paramPartB = RoutePatternFactory.ParameterPart("B"); - var paramPartC = RoutePatternFactory.ParameterPart("C"); - var paramPartD = RoutePatternFactory.ParameterPart("D"); - var segments = new[] - { + [Fact] + public void Pattern_RawTextAndArrayOfSegments_ShouldMakeCopyOfArrayOfSegments() + { + // Arrange + var rawText = "raw"; + var literalPartA = RoutePatternFactory.LiteralPart("A"); + var paramPartB = RoutePatternFactory.ParameterPart("B"); + var paramPartC = RoutePatternFactory.ParameterPart("C"); + var paramPartD = RoutePatternFactory.ParameterPart("D"); + var segments = new[] + { RoutePatternFactory.Segment(literalPartA, paramPartB), RoutePatternFactory.Segment(paramPartC, literalPartA), RoutePatternFactory.Segment(paramPartD), RoutePatternFactory.Segment(literalPartA) }; - // Act - var actual = RoutePatternFactory.Pattern(rawText, segments); - segments[1] = RoutePatternFactory.Segment(RoutePatternFactory.ParameterPart("E")); - Array.Resize(ref segments, 2); + // Act + var actual = RoutePatternFactory.Pattern(rawText, segments); + segments[1] = RoutePatternFactory.Segment(RoutePatternFactory.ParameterPart("E")); + Array.Resize(ref segments, 2); - // Assert - Assert.Equal(3, actual.Parameters.Count); - Assert.Same(paramPartB, actual.Parameters[0]); - Assert.Same(paramPartC, actual.Parameters[1]); - Assert.Same(paramPartD, actual.Parameters[2]); - } + // Assert + Assert.Equal(3, actual.Parameters.Count); + Assert.Same(paramPartB, actual.Parameters[0]); + Assert.Same(paramPartC, actual.Parameters[1]); + Assert.Same(paramPartD, actual.Parameters[2]); + } - [Fact] - public void Pattern_DefaultsAndParameterPoliciesAndArrayOfSegments_ShouldMakeCopyOfArrayOfSegments() - { - // Arrange - object defaults = new { B = 12, C = 4 }; - object parameterPolicies = null; - var literalPartA = RoutePatternFactory.LiteralPart("A"); - var paramPartB = RoutePatternFactory.ParameterPart("B"); - var paramPartC = RoutePatternFactory.ParameterPart("C"); - var paramPartD = RoutePatternFactory.ParameterPart("D"); - var segments = new[] - { + [Fact] + public void Pattern_DefaultsAndParameterPoliciesAndArrayOfSegments_ShouldMakeCopyOfArrayOfSegments() + { + // Arrange + object defaults = new { B = 12, C = 4 }; + object parameterPolicies = null; + var literalPartA = RoutePatternFactory.LiteralPart("A"); + var paramPartB = RoutePatternFactory.ParameterPart("B"); + var paramPartC = RoutePatternFactory.ParameterPart("C"); + var paramPartD = RoutePatternFactory.ParameterPart("D"); + var segments = new[] + { RoutePatternFactory.Segment(literalPartA, paramPartB), RoutePatternFactory.Segment(paramPartC, literalPartA), RoutePatternFactory.Segment(paramPartD), RoutePatternFactory.Segment(literalPartA) }; - // Act - var actual = RoutePatternFactory.Pattern(defaults, parameterPolicies, segments); - segments[1] = RoutePatternFactory.Segment(RoutePatternFactory.ParameterPart("E")); - Array.Resize(ref segments, 2); - - // Assert - Assert.Equal(3, actual.Parameters.Count); - Assert.Equal(paramPartB.Name, actual.Parameters[0].Name); - Assert.Equal(12, actual.Parameters[0].Default); - Assert.Null(paramPartB.Default); - Assert.NotSame(paramPartB, actual.Parameters[0]); - Assert.Equal(paramPartC.Name, actual.Parameters[1].Name); - Assert.Equal(4, actual.Parameters[1].Default); - Assert.NotSame(paramPartC, actual.Parameters[1]); - Assert.Null(paramPartC.Default); - Assert.Equal(paramPartD.Name, actual.Parameters[2].Name); - Assert.Null(actual.Parameters[2].Default); - Assert.Same(paramPartD, actual.Parameters[2]); - Assert.Null(paramPartD.Default); - } - - [Fact] - public void Pattern_RawTextAndDefaultsAndParameterPoliciesAndArrayOfSegments_ShouldMakeCopyOfArrayOfSegments() - { - // Arrange - var rawText = "raw"; - object defaults = new { B = 12, C = 4 }; - object parameterPolicies = null; - var literalPartA = RoutePatternFactory.LiteralPart("A"); - var paramPartB = RoutePatternFactory.ParameterPart("B"); - var paramPartC = RoutePatternFactory.ParameterPart("C"); - var paramPartD = RoutePatternFactory.ParameterPart("D"); - var segments = new[] - { + // Act + var actual = RoutePatternFactory.Pattern(defaults, parameterPolicies, segments); + segments[1] = RoutePatternFactory.Segment(RoutePatternFactory.ParameterPart("E")); + Array.Resize(ref segments, 2); + + // Assert + Assert.Equal(3, actual.Parameters.Count); + Assert.Equal(paramPartB.Name, actual.Parameters[0].Name); + Assert.Equal(12, actual.Parameters[0].Default); + Assert.Null(paramPartB.Default); + Assert.NotSame(paramPartB, actual.Parameters[0]); + Assert.Equal(paramPartC.Name, actual.Parameters[1].Name); + Assert.Equal(4, actual.Parameters[1].Default); + Assert.NotSame(paramPartC, actual.Parameters[1]); + Assert.Null(paramPartC.Default); + Assert.Equal(paramPartD.Name, actual.Parameters[2].Name); + Assert.Null(actual.Parameters[2].Default); + Assert.Same(paramPartD, actual.Parameters[2]); + Assert.Null(paramPartD.Default); + } + + [Fact] + public void Pattern_RawTextAndDefaultsAndParameterPoliciesAndArrayOfSegments_ShouldMakeCopyOfArrayOfSegments() + { + // Arrange + var rawText = "raw"; + object defaults = new { B = 12, C = 4 }; + object parameterPolicies = null; + var literalPartA = RoutePatternFactory.LiteralPart("A"); + var paramPartB = RoutePatternFactory.ParameterPart("B"); + var paramPartC = RoutePatternFactory.ParameterPart("C"); + var paramPartD = RoutePatternFactory.ParameterPart("D"); + var segments = new[] + { RoutePatternFactory.Segment(literalPartA, paramPartB), RoutePatternFactory.Segment(paramPartC, literalPartA), RoutePatternFactory.Segment(paramPartD), RoutePatternFactory.Segment(literalPartA) }; - // Act - var actual = RoutePatternFactory.Pattern(rawText, defaults, parameterPolicies, segments); - segments[1] = RoutePatternFactory.Segment(RoutePatternFactory.ParameterPart("E")); - Array.Resize(ref segments, 2); - - // Assert - Assert.Equal(3, actual.Parameters.Count); - Assert.Equal(paramPartB.Name, actual.Parameters[0].Name); - Assert.Equal(12, actual.Parameters[0].Default); - Assert.Null(paramPartB.Default); - Assert.NotSame(paramPartB, actual.Parameters[0]); - Assert.Equal(paramPartC.Name, actual.Parameters[1].Name); - Assert.Equal(4, actual.Parameters[1].Default); - Assert.NotSame(paramPartC, actual.Parameters[1]); - Assert.Null(paramPartC.Default); - Assert.Equal(paramPartD.Name, actual.Parameters[2].Name); - Assert.Null(actual.Parameters[2].Default); - Assert.Same(paramPartD, actual.Parameters[2]); - Assert.Null(paramPartD.Default); - } - - [Fact] - public void Parse_WithRequiredValues() - { - // Arrange - var template = "{controller=Home}/{action=Index}/{id?}"; - var defaults = new { area = "Admin", }; - var policies = new { }; - var requiredValues = new { area = "Admin", controller = "Store", action = "Index", }; - - // Act - var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues); - - // Assert - Assert.Collection( - action.RequiredValues.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); }, - kvp => { Assert.Equal("area", kvp.Key); Assert.Equal("Admin", kvp.Value); }, - kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); }); - } + // Act + var actual = RoutePatternFactory.Pattern(rawText, defaults, parameterPolicies, segments); + segments[1] = RoutePatternFactory.Segment(RoutePatternFactory.ParameterPart("E")); + Array.Resize(ref segments, 2); + + // Assert + Assert.Equal(3, actual.Parameters.Count); + Assert.Equal(paramPartB.Name, actual.Parameters[0].Name); + Assert.Equal(12, actual.Parameters[0].Default); + Assert.Null(paramPartB.Default); + Assert.NotSame(paramPartB, actual.Parameters[0]); + Assert.Equal(paramPartC.Name, actual.Parameters[1].Name); + Assert.Equal(4, actual.Parameters[1].Default); + Assert.NotSame(paramPartC, actual.Parameters[1]); + Assert.Null(paramPartC.Default); + Assert.Equal(paramPartD.Name, actual.Parameters[2].Name); + Assert.Null(actual.Parameters[2].Default); + Assert.Same(paramPartD, actual.Parameters[2]); + Assert.Null(paramPartD.Default); + } - [Fact] - public void Parse_WithRequiredValues_AllowsNullRequiredValue() - { - // Arrange - var template = "{controller=Home}/{action=Index}/{id?}"; - var defaults = new { }; - var policies = new { }; - var requiredValues = new { area = (string)null, controller = "Store", action = "Index", }; + [Fact] + public void Parse_WithRequiredValues() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { area = "Admin", }; + var policies = new { }; + var requiredValues = new { area = "Admin", controller = "Store", action = "Index", }; + + // Act + var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues); + + // Assert + Assert.Collection( + action.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); }, + kvp => { Assert.Equal("area", kvp.Key); Assert.Equal("Admin", kvp.Value); }, + kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); }); + } - // Act - var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues); + [Fact] + public void Parse_WithRequiredValues_AllowsNullRequiredValue() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { }; + var policies = new { }; + var requiredValues = new { area = (string)null, controller = "Store", action = "Index", }; + + // Act + var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues); + + // Assert + Assert.Collection( + action.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); }, + kvp => { Assert.Equal("area", kvp.Key); Assert.Null(kvp.Value); }, + kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); }); + } - // Assert - Assert.Collection( - action.RequiredValues.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); }, - kvp => { Assert.Equal("area", kvp.Key); Assert.Null(kvp.Value); }, - kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); }); - } + [Fact] + public void Parse_WithRequiredValues_AllowsEmptyRequiredValue() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { }; + var policies = new { }; + var requiredValues = new { area = "", controller = "Store", action = "Index", }; + + // Act + var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues); + + // Assert + Assert.Collection( + action.RequiredValues.OrderBy(kvp => kvp.Key), + kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); }, + kvp => { Assert.Equal("area", kvp.Key); Assert.Equal("", kvp.Value); }, + kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); }); + } - [Fact] - public void Parse_WithRequiredValues_AllowsEmptyRequiredValue() + [Fact] + public void Parse_WithRequiredValues_ThrowsForNonParameterNonDefault() + { + // Arrange + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new { }; + var policies = new { }; + var requiredValues = new { area = "Admin", controller = "Store", action = "Index", }; + + // Act + var exception = Assert.Throws(() => { - // Arrange - var template = "{controller=Home}/{action=Index}/{id?}"; - var defaults = new { }; - var policies = new { }; - var requiredValues = new { area = "", controller = "Store", action = "Index", }; - - // Act var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues); + }); + + // Assert + Assert.Equal( + "No corresponding parameter or default value could be found for the required value " + + "'area=Admin'. A non-null required value must correspond to a route parameter or the " + + "route pattern must have a matching default value.", + exception.Message); + } - // Assert - Assert.Collection( - action.RequiredValues.OrderBy(kvp => kvp.Key), - kvp => { Assert.Equal("action", kvp.Key); Assert.Equal("Index", kvp.Value); }, - kvp => { Assert.Equal("area", kvp.Key); Assert.Equal("", kvp.Value); }, - kvp => { Assert.Equal("controller", kvp.Key); Assert.Equal("Store", kvp.Value); }); - } + [Fact] + public void ParameterPart_ParameterNameAndDefaultAndParameterKindAndArrayOfParameterPolicies_ShouldMakeCopyOfParameterPolicies() + { + // Arrange (going through hoops to get an array of RoutePatternParameterPolicyReference) + const string name = "Id"; + var defaults = new { a = "13", }; + var x = new InlineConstraint("x"); + var y = new InlineConstraint("y"); + var z = new InlineConstraint("z"); + var constraints = new[] { x, y, z }; + var templatePart = TemplatePart.CreateParameter("t", false, false, null, constraints); + var routePatternParameterPart = (RoutePatternParameterPart)templatePart.ToRoutePatternPart(); + var policies = routePatternParameterPart.ParameterPolicies.ToArray(); + + // Act + var parameterPart = RoutePatternFactory.ParameterPart(name, defaults, RoutePatternParameterKind.Standard, policies); + policies[0] = null; + Array.Resize(ref policies, 2); + + // Assert + Assert.NotNull(parameterPart.ParameterPolicies); + Assert.Equal(3, parameterPart.ParameterPolicies.Count); + Assert.NotNull(parameterPart.ParameterPolicies[0]); + Assert.NotNull(parameterPart.ParameterPolicies[1]); + Assert.NotNull(parameterPart.ParameterPolicies[2]); + } - [Fact] - public void Parse_WithRequiredValues_ThrowsForNonParameterNonDefault() - { - // Arrange - var template = "{controller=Home}/{action=Index}/{id?}"; - var defaults = new { }; - var policies = new { }; - var requiredValues = new { area = "Admin", controller = "Store", action = "Index", }; - - // Act - var exception = Assert.Throws(() => - { - var action = RoutePatternFactory.Parse(template, defaults, policies, requiredValues); - }); + [Fact] + public void ParameterPart_ParameterNameAndDefaultAndParameterKindAndEnumerableOfParameterPolicies_ShouldMakeCopyOfParameterPolicies() + { + // Arrange (going through hoops to get an enumerable of RoutePatternParameterPolicyReference) + const string name = "Id"; + var defaults = new { a = "13", }; + var x = new InlineConstraint("x"); + var y = new InlineConstraint("y"); + var z = new InlineConstraint("z"); + var constraints = new[] { x, y, z }; + var templatePart = TemplatePart.CreateParameter("t", false, false, null, constraints); + var routePatternParameterPart = (RoutePatternParameterPart)templatePart.ToRoutePatternPart(); + var policies = routePatternParameterPart.ParameterPolicies.ToList(); + + // Act + var parameterPart = RoutePatternFactory.ParameterPart(name, defaults, RoutePatternParameterKind.Standard, policies); + policies[0] = null; + policies.RemoveAt(1); + + // Assert + Assert.NotNull(parameterPart.ParameterPolicies); + Assert.Equal(3, parameterPart.ParameterPolicies.Count); + Assert.NotNull(parameterPart.ParameterPolicies[0]); + Assert.NotNull(parameterPart.ParameterPolicies[1]); + Assert.NotNull(parameterPart.ParameterPolicies[2]); + } - // Assert - Assert.Equal( - "No corresponding parameter or default value could be found for the required value " + - "'area=Admin'. A non-null required value must correspond to a route parameter or the " + - "route pattern must have a matching default value.", - exception.Message); - } + [Fact] + public void Segment_EnumerableOfParts() + { + // Arrange + var paramPartB = RoutePatternFactory.ParameterPart("B"); + var paramPartC = RoutePatternFactory.ParameterPart("C"); + var paramPartD = RoutePatternFactory.ParameterPart("D"); + var parts = new[] { paramPartB, paramPartC, paramPartD }; + + // Act + var actual = RoutePatternFactory.Segment((IEnumerable)parts); + parts[1] = RoutePatternFactory.ParameterPart("E"); + Array.Resize(ref parts, 2); + + // Assert + Assert.Equal(3, actual.Parts.Count); + Assert.Same(paramPartB, actual.Parts[0]); + Assert.Same(paramPartC, actual.Parts[1]); + Assert.Same(paramPartD, actual.Parts[2]); + } - [Fact] - public void ParameterPart_ParameterNameAndDefaultAndParameterKindAndArrayOfParameterPolicies_ShouldMakeCopyOfParameterPolicies() - { - // Arrange (going through hoops to get an array of RoutePatternParameterPolicyReference) - const string name = "Id"; - var defaults = new { a = "13", }; - var x = new InlineConstraint("x"); - var y = new InlineConstraint("y"); - var z = new InlineConstraint("z"); - var constraints = new[] { x, y, z }; - var templatePart = TemplatePart.CreateParameter("t", false, false, null, constraints); - var routePatternParameterPart = (RoutePatternParameterPart) templatePart.ToRoutePatternPart(); - var policies = routePatternParameterPart.ParameterPolicies.ToArray(); - - // Act - var parameterPart = RoutePatternFactory.ParameterPart(name, defaults, RoutePatternParameterKind.Standard, policies); - policies[0] = null; - Array.Resize(ref policies, 2); - - // Assert - Assert.NotNull(parameterPart.ParameterPolicies); - Assert.Equal(3, parameterPart.ParameterPolicies.Count); - Assert.NotNull(parameterPart.ParameterPolicies[0]); - Assert.NotNull(parameterPart.ParameterPolicies[1]); - Assert.NotNull(parameterPart.ParameterPolicies[2]); - } - - [Fact] - public void ParameterPart_ParameterNameAndDefaultAndParameterKindAndEnumerableOfParameterPolicies_ShouldMakeCopyOfParameterPolicies() - { - // Arrange (going through hoops to get an enumerable of RoutePatternParameterPolicyReference) - const string name = "Id"; - var defaults = new { a = "13", }; - var x = new InlineConstraint("x"); - var y = new InlineConstraint("y"); - var z = new InlineConstraint("z"); - var constraints = new[] { x, y, z }; - var templatePart = TemplatePart.CreateParameter("t", false, false, null, constraints); - var routePatternParameterPart = (RoutePatternParameterPart)templatePart.ToRoutePatternPart(); - var policies = routePatternParameterPart.ParameterPolicies.ToList(); - - // Act - var parameterPart = RoutePatternFactory.ParameterPart(name, defaults, RoutePatternParameterKind.Standard, policies); - policies[0] = null; - policies.RemoveAt(1); - - // Assert - Assert.NotNull(parameterPart.ParameterPolicies); - Assert.Equal(3, parameterPart.ParameterPolicies.Count); - Assert.NotNull(parameterPart.ParameterPolicies[0]); - Assert.NotNull(parameterPart.ParameterPolicies[1]); - Assert.NotNull(parameterPart.ParameterPolicies[2]); - } - - [Fact] - public void Segment_EnumerableOfParts() - { - // Arrange - var paramPartB = RoutePatternFactory.ParameterPart("B"); - var paramPartC = RoutePatternFactory.ParameterPart("C"); - var paramPartD = RoutePatternFactory.ParameterPart("D"); - var parts = new[] { paramPartB, paramPartC, paramPartD }; - - // Act - var actual = RoutePatternFactory.Segment((IEnumerable) parts); - parts[1] = RoutePatternFactory.ParameterPart("E"); - Array.Resize(ref parts, 2); - - // Assert - Assert.Equal(3, actual.Parts.Count); - Assert.Same(paramPartB, actual.Parts[0]); - Assert.Same(paramPartC, actual.Parts[1]); - Assert.Same(paramPartD, actual.Parts[2]); - } - - [Fact] - public void Segment_ArrayOfParts() - { - // Arrange - var paramPartB = RoutePatternFactory.ParameterPart("B"); - var paramPartC = RoutePatternFactory.ParameterPart("C"); - var paramPartD = RoutePatternFactory.ParameterPart("D"); - var parts = new[] { paramPartB, paramPartC, paramPartD }; - - // Act - var actual = RoutePatternFactory.Segment(parts); - parts[1] = RoutePatternFactory.ParameterPart("E"); - Array.Resize(ref parts, 2); - - // Assert - Assert.Equal(3, actual.Parts.Count); - Assert.Same(paramPartB, actual.Parts[0]); - Assert.Same(paramPartC, actual.Parts[1]); - Assert.Same(paramPartD, actual.Parts[2]); - } + [Fact] + public void Segment_ArrayOfParts() + { + // Arrange + var paramPartB = RoutePatternFactory.ParameterPart("B"); + var paramPartC = RoutePatternFactory.ParameterPart("C"); + var paramPartD = RoutePatternFactory.ParameterPart("D"); + var parts = new[] { paramPartB, paramPartC, paramPartD }; + + // Act + var actual = RoutePatternFactory.Segment(parts); + parts[1] = RoutePatternFactory.ParameterPart("E"); + Array.Resize(ref parts, 2); + + // Assert + Assert.Equal(3, actual.Parts.Count); + Assert.Same(paramPartB, actual.Parts[0]); + Assert.Same(paramPartC, actual.Parts[1]); + Assert.Same(paramPartD, actual.Parts[2]); } } diff --git a/src/Http/Routing/test/UnitTests/Patterns/RoutePatternMatcherTest.cs b/src/Http/Routing/test/UnitTests/Patterns/RoutePatternMatcherTest.cs index f6e4940b1b..e035ea7e68 100644 --- a/src/Http/Routing/test/UnitTests/Patterns/RoutePatternMatcherTest.cs +++ b/src/Http/Routing/test/UnitTests/Patterns/RoutePatternMatcherTest.cs @@ -2,1129 +2,1128 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing.Patterns; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class RoutePatternMatcherTest { - public class RoutePatternMatcherTest + [Fact] + public void TryMatch_Success() { - [Fact] - public void TryMatch_Success() - { - // Arrange - var matcher = CreateMatcher("{controller}/{action}/{id}"); + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}"); - var values = new RouteValueDictionary(); + var values = new RouteValueDictionary(); - // Act - var match = matcher.TryMatch("/Bank/DoAction/123", values); + // Act + var match = matcher.TryMatch("/Bank/DoAction/123", values); - // Assert - Assert.True(match); - Assert.Equal("Bank", values["controller"]); - Assert.Equal("DoAction", values["action"]); - Assert.Equal("123", values["id"]); - } + // Assert + Assert.True(match); + Assert.Equal("Bank", values["controller"]); + Assert.Equal("DoAction", values["action"]); + Assert.Equal("123", values["id"]); + } - [Fact] - public void TryMatch_Fails() - { - // Arrange - var matcher = CreateMatcher("{controller}/{action}/{id}"); + [Fact] + public void TryMatch_Fails() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}"); - var values = new RouteValueDictionary(); + var values = new RouteValueDictionary(); - // Act - var match = matcher.TryMatch("/Bank/DoAction", values); + // Act + var match = matcher.TryMatch("/Bank/DoAction", values); - // Assert - Assert.False(match); - } + // Assert + Assert.False(match); + } - [Fact] - public void TryMatch_WithDefaults_Success() - { - // Arrange - var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" }); + [Fact] + public void TryMatch_WithDefaults_Success() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" }); - var values = new RouteValueDictionary(); + var values = new RouteValueDictionary(); - // Act - var match = matcher.TryMatch("/Bank/DoAction", values); + // Act + var match = matcher.TryMatch("/Bank/DoAction", values); - // Assert - Assert.True(match); - Assert.Equal("Bank", values["controller"]); - Assert.Equal("DoAction", values["action"]); - Assert.Equal("default id", values["id"]); - } + // Assert + Assert.True(match); + Assert.Equal("Bank", values["controller"]); + Assert.Equal("DoAction", values["action"]); + Assert.Equal("default id", values["id"]); + } - [Fact] - public void TryMatch_WithDefaults_Fails() - { - // Arrange - var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" }); + [Fact] + public void TryMatch_WithDefaults_Fails() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" }); - var values = new RouteValueDictionary(); + var values = new RouteValueDictionary(); - // Act - var match = matcher.TryMatch("/Bank", values); + // Act + var match = matcher.TryMatch("/Bank", values); - // Assert - Assert.False(match); - } + // Assert + Assert.False(match); + } - [Fact] - public void TryMatch_WithLiterals_Success() - { - // Arrange - var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" }); + [Fact] + public void TryMatch_WithLiterals_Success() + { + // Arrange + var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" }); - var values = new RouteValueDictionary(); + var values = new RouteValueDictionary(); - // Act - var match = matcher.TryMatch("/moo/111/bar/222", values); + // Act + var match = matcher.TryMatch("/moo/111/bar/222", values); - // Assert - Assert.True(match); - Assert.Equal("111", values["p1"]); - Assert.Equal("222", values["p2"]); - } + // Assert + Assert.True(match); + Assert.Equal("111", values["p1"]); + Assert.Equal("222", values["p2"]); + } - [Fact] - public void TryMatch_RouteWithLiteralsAndDefaults_Success() - { - // Arrange - var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" }); + [Fact] + public void TryMatch_RouteWithLiteralsAndDefaults_Success() + { + // Arrange + var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" }); - var values = new RouteValueDictionary(); + var values = new RouteValueDictionary(); - // Act - var match = matcher.TryMatch("/moo/111/bar/", values); + // Act + var match = matcher.TryMatch("/moo/111/bar/", values); - // Assert - Assert.True(match); - Assert.Equal("111", values["p1"]); - Assert.Equal("default p2", values["p2"]); - } + // Assert + Assert.True(match); + Assert.Equal("111", values["p1"]); + Assert.Equal("default p2", values["p2"]); + } - [Theory] - [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", "/123-456-7890")] // ssn - [InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", "/asd@assds.com")] // email - [InlineData(@"{p1:regex(([}}])\w+)}", "/}sda")] // Not balanced } - [InlineData(@"{p1:regex(([{{)])\w+)}", "/})sda")] // Not balanced { - public void TryMatch_RegularExpressionConstraint_Valid( - string template, - string path) - { - // Arrange - var matcher = CreateMatcher(template); + [Theory] + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", "/123-456-7890")] // ssn + [InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", "/asd@assds.com")] // email + [InlineData(@"{p1:regex(([}}])\w+)}", "/}sda")] // Not balanced } + [InlineData(@"{p1:regex(([{{)])\w+)}", "/})sda")] // Not balanced { + public void TryMatch_RegularExpressionConstraint_Valid( + string template, + string path) + { + // Arrange + var matcher = CreateMatcher(template); - var values = new RouteValueDictionary(); + var values = new RouteValueDictionary(); - // Act - var match = matcher.TryMatch(path, values); + // Act + var match = matcher.TryMatch(path, values); - // Assert - Assert.True(match); - } + // Assert + Assert.True(match); + } - [Theory] - [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", true, "foo", "bar")] - [InlineData("moo/{p1?}", "/moo/foo", true, "foo", null)] - [InlineData("moo/{p1?}", "/moo", true, null, null)] - [InlineData("moo/{p1}.{p2?}", "/moo/foo", true, "foo", null)] - [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", true, "foo.", "bar")] - [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", true, "foo.moo", "bar")] - [InlineData("moo/{p1}.{p2}", "/moo/foo.bar", true, "foo", "bar")] - [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo.bar", true, "moo", "bar")] - [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", true, "moo", null)] - [InlineData("moo/.{p2?}", "/moo/.foo", true, null, "foo")] - [InlineData("moo/.{p2?}", "/moo", false, null, null)] - [InlineData("moo/{p1}.{p2?}", "/moo/....", true, "..", ".")] - [InlineData("moo/{p1}.{p2?}", "/moo/.bar", true, ".bar", null)] - public void TryMatch_OptionalParameter_FollowedByPeriod_Valid( - string template, - string path, - bool expectedMatch, - string p1, - string p2) - { - // Arrange - var matcher = CreateMatcher(template); + [Theory] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", true, "foo", "bar")] + [InlineData("moo/{p1?}", "/moo/foo", true, "foo", null)] + [InlineData("moo/{p1?}", "/moo", true, null, null)] + [InlineData("moo/{p1}.{p2?}", "/moo/foo", true, "foo", null)] + [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", true, "foo.", "bar")] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", true, "foo.moo", "bar")] + [InlineData("moo/{p1}.{p2}", "/moo/foo.bar", true, "foo", "bar")] + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo.bar", true, "moo", "bar")] + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", true, "moo", null)] + [InlineData("moo/.{p2?}", "/moo/.foo", true, null, "foo")] + [InlineData("moo/.{p2?}", "/moo", false, null, null)] + [InlineData("moo/{p1}.{p2?}", "/moo/....", true, "..", ".")] + [InlineData("moo/{p1}.{p2?}", "/moo/.bar", true, ".bar", null)] + public void TryMatch_OptionalParameter_FollowedByPeriod_Valid( + string template, + string path, + bool expectedMatch, + string p1, + string p2) + { + // Arrange + var matcher = CreateMatcher(template); - var values = new RouteValueDictionary(); + var values = new RouteValueDictionary(); - // Act - var match = matcher.TryMatch(path, values); + // Act + var match = matcher.TryMatch(path, values); - // Assert - Assert.Equal(expectedMatch, match); - if (p1 != null) - { - Assert.Equal(p1, values["p1"]); - } - if (p2 != null) - { - Assert.Equal(p2, values["p2"]); - } + // Assert + Assert.Equal(expectedMatch, match); + if (p1 != null) + { + Assert.Equal(p1, values["p1"]); } - - [Theory] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", "foo", "moo", null)] - [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] - [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", "foo", "moo", "bar")] - [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", "foo", null, "bar")] - [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", ".foo", null, "bar")] - [InlineData("{p1}/{p2}/{p3?}", "/foo/bar/baz", "foo", "bar", "baz")] - public void TryMatch_OptionalParameter_FollowedByPeriod_3Parameters_Valid( - string template, - string path, - string p1, - string p2, - string p3) + if (p2 != null) { - // Arrange - var matcher = CreateMatcher(template); + Assert.Equal(p2, values["p2"]); + } + } - var values = new RouteValueDictionary(); + [Theory] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", "foo", "moo", null)] + [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", "foo", "moo", "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", "foo", null, "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", ".foo", null, "bar")] + [InlineData("{p1}/{p2}/{p3?}", "/foo/bar/baz", "foo", "bar", "baz")] + public void TryMatch_OptionalParameter_FollowedByPeriod_3Parameters_Valid( + string template, + string path, + string p1, + string p2, + string p3) + { + // Arrange + var matcher = CreateMatcher(template); - // Act - var match = matcher.TryMatch(path, values); + var values = new RouteValueDictionary(); - // Assert - Assert.True(match); - Assert.Equal(p1, values["p1"]); + // Act + var match = matcher.TryMatch(path, values); - if (p2 != null) - { - Assert.Equal(p2, values["p2"]); - } + // Assert + Assert.True(match); + Assert.Equal(p1, values["p1"]); - if (p3 != null) - { - Assert.Equal(p3, values["p3"]); - } + if (p2 != null) + { + Assert.Equal(p2, values["p2"]); } - [Theory] - [InlineData("moo/{p1}.{p2?}", "/moo/foo.")] - [InlineData("moo/{p1}.{p2?}", "/moo/.")] - [InlineData("moo/{p1}.{p2}", "/foo.")] - [InlineData("moo/{p1}.{p2}", "/foo")] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")] - [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")] - [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")] - [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")] - [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")] - [InlineData("moo/.{p2?}", "/moo/.")] - [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")] - public void TryMatch_OptionalParameter_FollowedByPeriod_Invalid(string template, string path) + if (p3 != null) { - // Arrange - var matcher = CreateMatcher(template); + Assert.Equal(p3, values["p3"]); + } + } - var values = new RouteValueDictionary(); + [Theory] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.")] + [InlineData("moo/{p1}.{p2?}", "/moo/.")] + [InlineData("moo/{p1}.{p2}", "/foo.")] + [InlineData("moo/{p1}.{p2}", "/foo")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")] + [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")] + [InlineData("moo/.{p2?}", "/moo/.")] + [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")] + public void TryMatch_OptionalParameter_FollowedByPeriod_Invalid(string template, string path) + { + // Arrange + var matcher = CreateMatcher(template); - // Act - var match = matcher.TryMatch(path, values); + var values = new RouteValueDictionary(); - // Assert - Assert.False(match); - } + // Act + var match = matcher.TryMatch(path, values); - [Fact] - public void TryMatch_RouteWithOnlyLiterals_Success() - { - // Arrange - var matcher = CreateMatcher("moo/bar"); + // Assert + Assert.False(match); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_RouteWithOnlyLiterals_Success() + { + // Arrange + var matcher = CreateMatcher("moo/bar"); - // Act - var match = matcher.TryMatch("/moo/bar", values); + var values = new RouteValueDictionary(); - // Assert - Assert.True(match); - Assert.Empty(values); - } + // Act + var match = matcher.TryMatch("/moo/bar", values); - [Fact] - public void TryMatch_RouteWithOnlyLiterals_Fails() - { - // Arrange - var matcher = CreateMatcher("moo/bars"); + // Assert + Assert.True(match); + Assert.Empty(values); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_RouteWithOnlyLiterals_Fails() + { + // Arrange + var matcher = CreateMatcher("moo/bars"); - // Act - var match = matcher.TryMatch("/moo/bar", values); + var values = new RouteValueDictionary(); - // Assert - Assert.False(match); - } + // Act + var match = matcher.TryMatch("/moo/bar", values); - [Fact] - public void TryMatch_RouteWithExtraSeparators_Success() - { - // Arrange - var matcher = CreateMatcher("moo/bar"); + // Assert + Assert.False(match); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_RouteWithExtraSeparators_Success() + { + // Arrange + var matcher = CreateMatcher("moo/bar"); - // Act - var match = matcher.TryMatch("/moo/bar/", values); + var values = new RouteValueDictionary(); - // Assert - Assert.True(match); - Assert.Empty(values); - } + // Act + var match = matcher.TryMatch("/moo/bar/", values); - [Fact] - public void TryMatch_UrlWithExtraSeparators_Success() - { - // Arrange - var matcher = CreateMatcher("moo/bar/"); + // Assert + Assert.True(match); + Assert.Empty(values); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_UrlWithExtraSeparators_Success() + { + // Arrange + var matcher = CreateMatcher("moo/bar/"); - // Act - var match = matcher.TryMatch("/moo/bar", values); + var values = new RouteValueDictionary(); - // Assert - Assert.True(match); - Assert.Empty(values); - } + // Act + var match = matcher.TryMatch("/moo/bar", values); - [Fact] - public void TryMatch_RouteWithParametersAndExtraSeparators_Success() - { - // Arrange - var matcher = CreateMatcher("{p1}/{p2}/"); + // Assert + Assert.True(match); + Assert.Empty(values); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_RouteWithParametersAndExtraSeparators_Success() + { + // Arrange + var matcher = CreateMatcher("{p1}/{p2}/"); - // Act - var match = matcher.TryMatch("/moo/bar", values); + var values = new RouteValueDictionary(); - // Assert - Assert.True(match); - Assert.Equal("moo", values["p1"]); - Assert.Equal("bar", values["p2"]); - } + // Act + var match = matcher.TryMatch("/moo/bar", values); - [Fact] - public void TryMatch_RouteWithDifferentLiterals_Fails() - { - // Arrange - var matcher = CreateMatcher("{p1}/{p2}/baz"); + // Assert + Assert.True(match); + Assert.Equal("moo", values["p1"]); + Assert.Equal("bar", values["p2"]); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_RouteWithDifferentLiterals_Fails() + { + // Arrange + var matcher = CreateMatcher("{p1}/{p2}/baz"); - // Act - var match = matcher.TryMatch("/moo/bar/boo", values); + var values = new RouteValueDictionary(); - // Assert - Assert.False(match); - } + // Act + var match = matcher.TryMatch("/moo/bar/boo", values); - [Fact] - public void TryMatch_LongerUrl_Fails() - { - // Arrange - var matcher = CreateMatcher("{p1}"); + // Assert + Assert.False(match); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_LongerUrl_Fails() + { + // Arrange + var matcher = CreateMatcher("{p1}"); - // Act - var match = matcher.TryMatch("/moo/bar", values); + var values = new RouteValueDictionary(); - // Assert - Assert.False(match); - } + // Act + var match = matcher.TryMatch("/moo/bar", values); - [Fact] - public void TryMatch_SimpleFilename_Success() - { - // Arrange - var matcher = CreateMatcher("DEFAULT.ASPX"); + // Assert + Assert.False(match); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_SimpleFilename_Success() + { + // Arrange + var matcher = CreateMatcher("DEFAULT.ASPX"); - // Act - var match = matcher.TryMatch("/default.aspx", values); + var values = new RouteValueDictionary(); - // Assert - Assert.True(match); - } + // Act + var match = matcher.TryMatch("/default.aspx", values); - [Theory] - [InlineData("{prefix}x{suffix}", "/xxxxxxxxxx")] - [InlineData("{prefix}xyz{suffix}", "/xxxxyzxyzxxxxxxyz")] - [InlineData("{prefix}xyz{suffix}", "/abcxxxxyzxyzxxxxxxyzxx")] - [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyzxyzxyz")] - [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyzxyzxyz1")] - [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyz")] - [InlineData("{prefix}aa{suffix}", "/aaaaa")] - [InlineData("{prefix}aaa{suffix}", "/aaaaa")] - public void TryMatch_RouteWithComplexSegment_Success(string template, string path) - { - var matcher = CreateMatcher(template); + // Assert + Assert.True(match); + } - var values = new RouteValueDictionary(); + [Theory] + [InlineData("{prefix}x{suffix}", "/xxxxxxxxxx")] + [InlineData("{prefix}xyz{suffix}", "/xxxxyzxyzxxxxxxyz")] + [InlineData("{prefix}xyz{suffix}", "/abcxxxxyzxyzxxxxxxyzxx")] + [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyzxyzxyz")] + [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyzxyzxyz1")] + [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyz")] + [InlineData("{prefix}aa{suffix}", "/aaaaa")] + [InlineData("{prefix}aaa{suffix}", "/aaaaa")] + public void TryMatch_RouteWithComplexSegment_Success(string template, string path) + { + var matcher = CreateMatcher(template); - // Act - var match = matcher.TryMatch(path, values); + var values = new RouteValueDictionary(); - // Assert - Assert.True(match); - } + // Act + var match = matcher.TryMatch(path, values); - [Fact] - public void TryMatch_RouteWithExtraDefaultValues_Success() - { - // Arrange - var matcher = CreateMatcher("{p1}/{p2}", new { p2 = (string)null, foo = "bar" }); + // Assert + Assert.True(match); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_RouteWithExtraDefaultValues_Success() + { + // Arrange + var matcher = CreateMatcher("{p1}/{p2}", new { p2 = (string)null, foo = "bar" }); - // Act - var match = matcher.TryMatch("/v1", values); + var values = new RouteValueDictionary(); - // Assert - Assert.True(match); - Assert.Equal(3, values.Count); - Assert.Equal("v1", values["p1"]); - Assert.Null(values["p2"]); - Assert.Equal("bar", values["foo"]); - } + // Act + var match = matcher.TryMatch("/v1", values); - [Fact] - public void TryMatch_PrettyRouteWithExtraDefaultValues_Success() - { - // Arrange - var matcher = CreateMatcher( - "date/{y}/{m}/{d}", - new { controller = "blog", action = "showpost", m = (string)null, d = (string)null }); + // Assert + Assert.True(match); + Assert.Equal(3, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Null(values["p2"]); + Assert.Equal("bar", values["foo"]); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_PrettyRouteWithExtraDefaultValues_Success() + { + // Arrange + var matcher = CreateMatcher( + "date/{y}/{m}/{d}", + new { controller = "blog", action = "showpost", m = (string)null, d = (string)null }); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/date/2007/08", values); + + // Assert + Assert.True(match); + Assert.Equal(5, values.Count); + Assert.Equal("blog", values["controller"]); + Assert.Equal("showpost", values["action"]); + Assert.Equal("2007", values["y"]); + Assert.Equal("08", values["m"]); + Assert.Null(values["d"]); + } - // Act - var match = matcher.TryMatch("/date/2007/08", values); + [Fact] + public void TryMatch_WithMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}-{region}", + "/language/en-US", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } - // Assert - Assert.True(match); - Assert.Equal(5, values.Count); - Assert.Equal("blog", values["controller"]); - Assert.Equal("showpost", values["action"]); - Assert.Equal("2007", values["y"]); - Assert.Equal("08", values["m"]); - Assert.Null(values["d"]); - } + [Fact] + public void TryMatch_WithMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-{region}a", + "/language/en-USa", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } - [Fact] - public void TryMatch_WithMultiSegmentParamsOnBothEndsMatches() - { - RunTest( - "language/{lang}-{region}", - "/language/en-US", - null, - new RouteValueDictionary(new { lang = "en", region = "US" })); - } + [Fact] + public void TryMatch_WithMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}-{region}", + "/language/aen-US", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } - [Fact] - public void TryMatch_WithMultiSegmentParamsOnLeftEndMatches() - { - RunTest( - "language/{lang}-{region}a", - "/language/en-USa", - null, - new RouteValueDictionary(new { lang = "en", region = "US" })); - } + [Fact] + public void TryMatch_WithMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}-{region}a", + "/language/aen-USa", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } - [Fact] - public void TryMatch_WithMultiSegmentParamsOnRightEndMatches() - { - RunTest( - "language/a{lang}-{region}", - "/language/aen-US", - null, - new RouteValueDictionary(new { lang = "en", region = "US" })); - } + [Fact] + public void TryMatch_WithMultiSegmentParamsOnNeitherEndDoesNotMatch() + { + RunTest( + "language/a{lang}-{region}a", + "/language/a-USa", + null, + null); + } - [Fact] - public void TryMatch_WithMultiSegmentParamsOnNeitherEndMatches() - { - RunTest( - "language/a{lang}-{region}a", - "/language/aen-USa", - null, - new RouteValueDictionary(new { lang = "en", region = "US" })); - } + [Fact] + public void TryMatch_WithMultiSegmentParamsOnNeitherEndDoesNotMatch2() + { + RunTest( + "language/a{lang}-{region}a", + "/language/aen-a", + null, + null); + } - [Fact] - public void TryMatch_WithMultiSegmentParamsOnNeitherEndDoesNotMatch() - { - RunTest( - "language/a{lang}-{region}a", - "/language/a-USa", - null, - null); - } + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}", + "/language/en", + null, + new RouteValueDictionary(new { lang = "en" })); + } - [Fact] - public void TryMatch_WithMultiSegmentParamsOnNeitherEndDoesNotMatch2() - { - RunTest( - "language/a{lang}-{region}a", - "/language/aen-a", - null, - null); - } + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsTrailingSlashDoesNotMatch() + { + RunTest( + "language/{lang}", + "/language/", + null, + null); + } - [Fact] - public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsMatches() - { - RunTest( - "language/{lang}", - "/language/en", - null, - new RouteValueDictionary(new { lang = "en" })); - } + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsDoesNotMatch() + { + RunTest( + "language/{lang}", + "/language", + null, + null); + } - [Fact] - public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsTrailingSlashDoesNotMatch() - { - RunTest( - "language/{lang}", - "/language/", - null, - null); - } + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-", + "/language/en-", + null, + new RouteValueDictionary(new { lang = "en" })); + } - [Fact] - public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsDoesNotMatch() - { - RunTest( - "language/{lang}", - "/language", - null, - null); - } + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}", + "/language/aen", + null, + new RouteValueDictionary(new { lang = "en" })); + } - [Fact] - public void TryMatch_WithSimpleMultiSegmentParamsOnLeftEndMatches() - { - RunTest( - "language/{lang}-", - "/language/en-", - null, - new RouteValueDictionary(new { lang = "en" })); - } + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}a", + "/language/aena", + null, + new RouteValueDictionary(new { lang = "en" })); + } - [Fact] - public void TryMatch_WithSimpleMultiSegmentParamsOnRightEndMatches() - { - RunTest( - "language/a{lang}", - "/language/aen", - null, - new RouteValueDictionary(new { lang = "en" })); - } + [Fact] + public void TryMatch_WithMultiSegmentStandamatchMvcRouteMatches() + { + RunTest( + "{controller}.mvc/{action}/{id}", + "/home.mvc/index", + new RouteValueDictionary(new { action = "Index", id = (string)null }), + new RouteValueDictionary(new { controller = "home", action = "index", id = (string)null })); + } - [Fact] - public void TryMatch_WithSimpleMultiSegmentParamsOnNeitherEndMatches() - { - RunTest( - "language/a{lang}a", - "/language/aena", - null, - new RouteValueDictionary(new { lang = "en" })); - } + [Fact] + public void TryMatch_WithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() + { + RunTest( + "language/{lang}-{region}", + "/language/-", + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + null); + } - [Fact] - public void TryMatch_WithMultiSegmentStandamatchMvcRouteMatches() - { - RunTest( - "{controller}.mvc/{action}/{id}", - "/home.mvc/index", - new RouteValueDictionary(new { action = "Index", id = (string)null }), - new RouteValueDictionary(new { controller = "home", action = "index", id = (string)null })); - } + [Fact] + public void TryMatch_WithUrlWithMultiSegmentWithRepeatedDots() + { + RunTest( + "{Controller}..mvc/{id}/{Param1}", + "/Home..mvc/123/p1", + null, + new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); + } - [Fact] - public void TryMatch_WithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() - { - RunTest( - "language/{lang}-{region}", - "/language/-", - new RouteValueDictionary(new { lang = "xx", region = "yy" }), - null); - } + [Fact] + public void TryMatch_WithUrlWithTwoRepeatedDots() + { + RunTest( + "{Controller}.mvc/../{action}", + "/Home.mvc/../index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } - [Fact] - public void TryMatch_WithUrlWithMultiSegmentWithRepeatedDots() - { - RunTest( - "{Controller}..mvc/{id}/{Param1}", - "/Home..mvc/123/p1", - null, - new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); - } + [Fact] + public void TryMatch_WithUrlWithThreeRepeatedDots() + { + RunTest( + "{Controller}.mvc/.../{action}", + "/Home.mvc/.../index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } - [Fact] - public void TryMatch_WithUrlWithTwoRepeatedDots() - { - RunTest( - "{Controller}.mvc/../{action}", - "/Home.mvc/../index", - null, - new RouteValueDictionary(new { Controller = "Home", action = "index" })); - } + [Fact] + public void TryMatch_WithUrlWithManyRepeatedDots() + { + RunTest( + "{Controller}.mvc/../../../{action}", + "/Home.mvc/../../../index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } - [Fact] - public void TryMatch_WithUrlWithThreeRepeatedDots() - { - RunTest( - "{Controller}.mvc/.../{action}", - "/Home.mvc/.../index", - null, - new RouteValueDictionary(new { Controller = "Home", action = "index" })); - } + [Fact] + public void TryMatch_WithUrlWithExclamationPoint() + { + RunTest( + "{Controller}.mvc!/{action}", + "/Home.mvc!/index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } - [Fact] - public void TryMatch_WithUrlWithManyRepeatedDots() - { - RunTest( - "{Controller}.mvc/../../../{action}", - "/Home.mvc/../../../index", - null, - new RouteValueDictionary(new { Controller = "Home", action = "index" })); - } + [Fact] + public void TryMatch_WithUrlWithStartingDotDotSlash() + { + RunTest( + "../{Controller}.mvc", + "/../Home.mvc", + null, + new RouteValueDictionary(new { Controller = "Home" })); + } - [Fact] - public void TryMatch_WithUrlWithExclamationPoint() - { - RunTest( - "{Controller}.mvc!/{action}", - "/Home.mvc!/index", - null, - new RouteValueDictionary(new { Controller = "Home", action = "index" })); - } + [Fact] + public void TryMatch_WithUrlWithStartingBackslash() + { + RunTest( + @"\{Controller}.mvc", + @"/\Home.mvc", + null, + new RouteValueDictionary(new { Controller = "Home" })); + } - [Fact] - public void TryMatch_WithUrlWithStartingDotDotSlash() - { - RunTest( - "../{Controller}.mvc", - "/../Home.mvc", - null, - new RouteValueDictionary(new { Controller = "Home" })); - } + [Fact] + public void TryMatch_WithUrlWithBackslashSeparators() + { + RunTest( + @"{Controller}.mvc\{id}\{Param1}", + @"/Home.mvc\123\p1", + null, + new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); + } - [Fact] - public void TryMatch_WithUrlWithStartingBackslash() - { - RunTest( - @"\{Controller}.mvc", - @"/\Home.mvc", - null, - new RouteValueDictionary(new { Controller = "Home" })); - } + [Fact] + public void TryMatch_WithUrlWithParenthesesLiterals() + { + RunTest( + @"(Controller).mvc", + @"/(Controller).mvc", + null, + new RouteValueDictionary()); + } - [Fact] - public void TryMatch_WithUrlWithBackslashSeparators() - { - RunTest( - @"{Controller}.mvc\{id}\{Param1}", - @"/Home.mvc\123\p1", - null, - new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); - } + [Fact] + public void TryMatch_WithUrlWithTrailingSlashSpace() + { + RunTest( + @"Controller.mvc/ ", + @"/Controller.mvc/ ", + null, + new RouteValueDictionary()); + } - [Fact] - public void TryMatch_WithUrlWithParenthesesLiterals() - { - RunTest( - @"(Controller).mvc", - @"/(Controller).mvc", - null, - new RouteValueDictionary()); - } + [Fact] + public void TryMatch_WithUrlWithTrailingSpace() + { + RunTest( + @"Controller.mvc ", + @"/Controller.mvc ", + null, + new RouteValueDictionary()); + } - [Fact] - public void TryMatch_WithUrlWithTrailingSlashSpace() - { - RunTest( - @"Controller.mvc/ ", - @"/Controller.mvc/ ", - null, - new RouteValueDictionary()); - } + [Fact] + public void TryMatch_WithCatchAllCapturesDots() + { + // DevDiv Bugs 189892: UrlRouting: Catch all parameter cannot capture url segments that contain the "." + RunTest( + "Home/ShowPilot/{missionId}/{*name}", + "/Home/ShowPilot/777/12345./foobar", + new RouteValueDictionary(new + { + controller = "Home", + action = "ShowPilot", + missionId = (string)null, + name = (string)null + }), + new RouteValueDictionary(new { controller = "Home", action = "ShowPilot", missionId = "777", name = "12345./foobar" })); + } - [Fact] - public void TryMatch_WithUrlWithTrailingSpace() - { - RunTest( - @"Controller.mvc ", - @"/Controller.mvc ", - null, - new RouteValueDictionary()); - } + [Fact] + public void TryMatch_RouteWithCatchAll_MatchesMultiplePathSegments() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); - [Fact] - public void TryMatch_WithCatchAllCapturesDots() - { - // DevDiv Bugs 189892: UrlRouting: Catch all parameter cannot capture url segments that contain the "." - RunTest( - "Home/ShowPilot/{missionId}/{*name}", - "/Home/ShowPilot/777/12345./foobar", - new RouteValueDictionary(new - { - controller = "Home", - action = "ShowPilot", - missionId = (string)null, - name = (string)null - }), - new RouteValueDictionary(new { controller = "Home", action = "ShowPilot", missionId = "777", name = "12345./foobar" })); - } + var values = new RouteValueDictionary(); - [Fact] - public void TryMatch_RouteWithCatchAll_MatchesMultiplePathSegments() - { - // Arrange - var matcher = CreateMatcher("{p1}/{*p2}"); + // Act + var match = matcher.TryMatch("/v1/v2/v3", values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Equal("v2/v3", values["p2"]); + } - // Act - var match = matcher.TryMatch("/v1/v2/v3", values); + [Fact] + public void TryMatch_RouteWithCatchAll_MatchesTrailingSlash() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); - // Assert - Assert.True(match); - Assert.Equal(2, values.Count); - Assert.Equal("v1", values["p1"]); - Assert.Equal("v2/v3", values["p2"]); - } + var values = new RouteValueDictionary(); - [Fact] - public void TryMatch_RouteWithCatchAll_MatchesTrailingSlash() - { - // Arrange - var matcher = CreateMatcher("{p1}/{*p2}"); + // Act + var match = matcher.TryMatch("/v1/", values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Null(values["p2"]); + } - // Act - var match = matcher.TryMatch("/v1/", values); + [Fact] + public void TryMatch_RouteWithCatchAll_MatchesEmptyContent() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); - // Assert - Assert.True(match); - Assert.Equal(2, values.Count); - Assert.Equal("v1", values["p1"]); - Assert.Null(values["p2"]); - } + var values = new RouteValueDictionary(); - [Fact] - public void TryMatch_RouteWithCatchAll_MatchesEmptyContent() - { - // Arrange - var matcher = CreateMatcher("{p1}/{*p2}"); + // Act + var match = matcher.TryMatch("/v1", values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Null(values["p2"]); + } - // Act - var match = matcher.TryMatch("/v1", values); + [Fact] + public void TryMatch_RouteWithCatchAll_MatchesEmptyContent_DoesNotReplaceExistingRouteValue() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); - // Assert - Assert.True(match); - Assert.Equal(2, values.Count); - Assert.Equal("v1", values["p1"]); - Assert.Null(values["p2"]); - } + var values = new RouteValueDictionary(new { p2 = "hello" }); - [Fact] - public void TryMatch_RouteWithCatchAll_MatchesEmptyContent_DoesNotReplaceExistingRouteValue() - { - // Arrange - var matcher = CreateMatcher("{p1}/{*p2}"); + // Act + var match = matcher.TryMatch("/v1", values); - var values = new RouteValueDictionary(new { p2 = "hello" }); + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Equal("hello", values["p2"]); + } - // Act - var match = matcher.TryMatch("/v1", values); + [Fact] + public void TryMatch_RouteWithCatchAll_UsesDefaultValueForEmptyContent() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" }); - // Assert - Assert.True(match); - Assert.Equal(2, values.Count); - Assert.Equal("v1", values["p1"]); - Assert.Equal("hello", values["p2"]); - } + var values = new RouteValueDictionary(new { p2 = "overridden" }); - [Fact] - public void TryMatch_RouteWithCatchAll_UsesDefaultValueForEmptyContent() - { - // Arrange - var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" }); + // Act + var match = matcher.TryMatch("/v1", values); - var values = new RouteValueDictionary(new { p2 = "overridden" }); + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Equal("catchall", values["p2"]); + } - // Act - var match = matcher.TryMatch("/v1", values); + [Fact] + public void TryMatch_RouteWithCatchAll_IgnoresDefaultValueForNonEmptyContent() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" }); - // Assert - Assert.True(match); - Assert.Equal(2, values.Count); - Assert.Equal("v1", values["p1"]); - Assert.Equal("catchall", values["p2"]); - } + var values = new RouteValueDictionary(new { p2 = "overridden" }); - [Fact] - public void TryMatch_RouteWithCatchAll_IgnoresDefaultValueForNonEmptyContent() - { - // Arrange - var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" }); + // Act + var match = matcher.TryMatch("/v1/hello/whatever", values); - var values = new RouteValueDictionary(new { p2 = "overridden" }); + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Equal("hello/whatever", values["p2"]); + } - // Act - var match = matcher.TryMatch("/v1/hello/whatever", values); + [Fact] + public void TryMatch_DoesNotMatchOnlyLeftLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "/fooBAR", + null, + null); + } - // Assert - Assert.True(match); - Assert.Equal(2, values.Count); - Assert.Equal("v1", values["p1"]); - Assert.Equal("hello/whatever", values["p2"]); - } + [Fact] + public void TryMatch_DoesNotMatchOnlyRightLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "/BARfoo", + null, + null); + } - [Fact] - public void TryMatch_DoesNotMatchOnlyLeftLiteralMatch() - { - // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url - RunTest( - "foo", - "/fooBAR", - null, - null); - } + [Fact] + public void TryMatch_DoesNotMatchMiddleLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "/BARfooBAR", + null, + null); + } - [Fact] - public void TryMatch_DoesNotMatchOnlyRightLiteralMatch() - { - // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url - RunTest( - "foo", - "/BARfoo", - null, - null); - } + [Fact] + public void TryMatch_DoesMatchesExactLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "/foo", + null, + new RouteValueDictionary()); + } - [Fact] - public void TryMatch_DoesNotMatchMiddleLiteralMatch() - { - // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url - RunTest( - "foo", - "/BARfooBAR", - null, - null); - } + [Fact] + public void TryMatch_WithWeimatchParameterNames() + { + RunTest( + "foo/{ }/{.!$%}/{dynamic.data}/{op.tional}", + "/foo/space/weimatch/omatcherid", + new RouteValueDictionary() { { " ", "not a space" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }, + new RouteValueDictionary() { { " ", "space" }, { ".!$%", "weimatch" }, { "dynamic.data", "omatcherid" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }); + } - [Fact] - public void TryMatch_DoesMatchesExactLiteralMatch() - { - // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url - RunTest( - "foo", - "/foo", - null, - new RouteValueDictionary()); - } + [Fact] + public void TryMatch_DoesNotMatchRouteWithLiteralSeparatomatchefaultsButNoValue() + { + RunTest( + "{controller}/{language}-{locale}", + "/foo", + new RouteValueDictionary(new { language = "en", locale = "US" }), + null); + } - [Fact] - public void TryMatch_WithWeimatchParameterNames() - { - RunTest( - "foo/{ }/{.!$%}/{dynamic.data}/{op.tional}", - "/foo/space/weimatch/omatcherid", - new RouteValueDictionary() { { " ", "not a space" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }, - new RouteValueDictionary() { { " ", "space" }, { ".!$%", "weimatch" }, { "dynamic.data", "omatcherid" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }); - } + [Fact] + public void TryMatch_DoesNotMatchesRouteWithLiteralSeparatomatchefaultsAndLeftValue() + { + RunTest( + "{controller}/{language}-{locale}", + "/foo/xx-", + new RouteValueDictionary(new { language = "en", locale = "US" }), + null); + } - [Fact] - public void TryMatch_DoesNotMatchRouteWithLiteralSeparatomatchefaultsButNoValue() - { - RunTest( - "{controller}/{language}-{locale}", - "/foo", - new RouteValueDictionary(new { language = "en", locale = "US" }), - null); - } + [Fact] + public void TryMatch_DoesNotMatchesRouteWithLiteralSeparatomatchefaultsAndRightValue() + { + RunTest( + "{controller}/{language}-{locale}", + "/foo/-yy", + new RouteValueDictionary(new { language = "en", locale = "US" }), + null); + } - [Fact] - public void TryMatch_DoesNotMatchesRouteWithLiteralSeparatomatchefaultsAndLeftValue() - { - RunTest( - "{controller}/{language}-{locale}", - "/foo/xx-", - new RouteValueDictionary(new { language = "en", locale = "US" }), - null); - } + [Fact] + public void TryMatch_MatchesRouteWithLiteralSeparatomatchefaultsAndValue() + { + RunTest( + "{controller}/{language}-{locale}", + "/foo/xx-yy", + new RouteValueDictionary(new { language = "en", locale = "US" }), + new RouteValueDictionary { { "language", "xx" }, { "locale", "yy" }, { "controller", "foo" } }); + } - [Fact] - public void TryMatch_DoesNotMatchesRouteWithLiteralSeparatomatchefaultsAndRightValue() - { - RunTest( - "{controller}/{language}-{locale}", - "/foo/-yy", - new RouteValueDictionary(new { language = "en", locale = "US" }), - null); - } + [Fact] + public void TryMatch_SetsOptionalParameter() + { + // Arrange + var route = CreateMatcher("{controller}/{action?}"); + var url = "/Home/Index"; - [Fact] - public void TryMatch_MatchesRouteWithLiteralSeparatomatchefaultsAndValue() - { - RunTest( - "{controller}/{language}-{locale}", - "/foo/xx-yy", - new RouteValueDictionary(new { language = "en", locale = "US" }), - new RouteValueDictionary { { "language", "xx" }, { "locale", "yy" }, { "controller", "foo" } }); - } + var values = new RouteValueDictionary(); - [Fact] - public void TryMatch_SetsOptionalParameter() - { - // Arrange - var route = CreateMatcher("{controller}/{action?}"); - var url = "/Home/Index"; + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("Home", values["controller"]); + Assert.Equal("Index", values["action"]); + } - // Act - var match = route.TryMatch(url, values); + [Fact] + public void TryMatch_DoesNotSetOptionalParameter() + { + // Arrange + var route = CreateMatcher("{controller}/{action?}"); + var url = "/Home"; - // Assert - Assert.True(match); - Assert.Equal(2, values.Count); - Assert.Equal("Home", values["controller"]); - Assert.Equal("Index", values["action"]); - } + var values = new RouteValueDictionary(); - [Fact] - public void TryMatch_DoesNotSetOptionalParameter() - { - // Arrange - var route = CreateMatcher("{controller}/{action?}"); - var url = "/Home"; + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + Assert.Single(values); + Assert.Equal("Home", values["controller"]); + Assert.False(values.ContainsKey("action")); + } - // Act - var match = route.TryMatch(url, values); + [Fact] + public void TryMatch_DoesNotSetOptionalParameter_EmptyString() + { + // Arrange + var route = CreateMatcher("{controller?}"); + var url = ""; - // Assert - Assert.True(match); - Assert.Single(values); - Assert.Equal("Home", values["controller"]); - Assert.False(values.ContainsKey("action")); - } + var values = new RouteValueDictionary(); - [Fact] - public void TryMatch_DoesNotSetOptionalParameter_EmptyString() - { - // Arrange - var route = CreateMatcher("{controller?}"); - var url = ""; + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + Assert.Empty(values); + Assert.False(values.ContainsKey("controller")); + } - // Act - var match = route.TryMatch(url, values); + [Fact] + public void TryMatch__EmptyRouteWith_EmptyString() + { + // Arrange + var route = CreateMatcher(""); + var url = ""; - // Assert - Assert.True(match); - Assert.Empty(values); - Assert.False(values.ContainsKey("controller")); - } + var values = new RouteValueDictionary(); - [Fact] - public void TryMatch__EmptyRouteWith_EmptyString() - { - // Arrange - var route = CreateMatcher(""); - var url = ""; + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + Assert.Empty(values); + } - // Act - var match = route.TryMatch(url, values); + [Fact] + public void TryMatch_MultipleOptionalParameters() + { + // Arrange + var route = CreateMatcher("{controller}/{action?}/{id?}"); + var url = "/Home/Index"; - // Assert - Assert.True(match); - Assert.Empty(values); - } + var values = new RouteValueDictionary(); - [Fact] - public void TryMatch_MultipleOptionalParameters() - { - // Arrange - var route = CreateMatcher("{controller}/{action?}/{id?}"); - var url = "/Home/Index"; + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("Home", values["controller"]); + Assert.Equal("Index", values["action"]); + Assert.False(values.ContainsKey("id")); + } - // Act - var match = route.TryMatch(url, values); + [Theory] + [InlineData("///")] + [InlineData("/a//")] + [InlineData("/a/b//")] + [InlineData("//b//")] + [InlineData("///c")] + [InlineData("///c/")] + public void TryMatch_MultipleOptionalParameters_WithEmptyIntermediateSegmentsDoesNotMatch(string url) + { + // Arrange + var route = CreateMatcher("{controller?}/{action?}/{id?}"); - // Assert - Assert.True(match); - Assert.Equal(2, values.Count); - Assert.Equal("Home", values["controller"]); - Assert.Equal("Index", values["action"]); - Assert.False(values.ContainsKey("id")); - } + var values = new RouteValueDictionary(); - [Theory] - [InlineData("///")] - [InlineData("/a//")] - [InlineData("/a/b//")] - [InlineData("//b//")] - [InlineData("///c")] - [InlineData("///c/")] - public void TryMatch_MultipleOptionalParameters_WithEmptyIntermediateSegmentsDoesNotMatch(string url) - { - // Arrange - var route = CreateMatcher("{controller?}/{action?}/{id?}"); + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.False(match); + } - // Act - var match = route.TryMatch(url, values); + [Theory] + [InlineData("")] + [InlineData("/")] + [InlineData("/a")] + [InlineData("/a/")] + [InlineData("/a/b")] + [InlineData("/a/b/")] + [InlineData("/a/b/c")] + [InlineData("/a/b/c/")] + public void TryMatch_MultipleOptionalParameters_WithIncrementalOptionalValues(string url) + { + // Arrange + var route = CreateMatcher("{controller?}/{action?}/{id?}"); - // Assert - Assert.False(match); - } + var values = new RouteValueDictionary(); - [Theory] - [InlineData("")] - [InlineData("/")] - [InlineData("/a")] - [InlineData("/a/")] - [InlineData("/a/b")] - [InlineData("/a/b/")] - [InlineData("/a/b/c")] - [InlineData("/a/b/c/")] - public void TryMatch_MultipleOptionalParameters_WithIncrementalOptionalValues(string url) - { - // Arrange - var route = CreateMatcher("{controller?}/{action?}/{id?}"); + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + } - // Act - var match = route.TryMatch(url, values); + [Theory] + [InlineData("///")] + [InlineData("////")] + [InlineData("/a//")] + [InlineData("/a///")] + [InlineData("//b/")] + [InlineData("//b//")] + [InlineData("///c")] + [InlineData("///c/")] + public void TryMatch_MultipleParameters_WithEmptyValues(string url) + { + // Arrange + var route = CreateMatcher("{controller}/{action}/{id}"); - // Assert - Assert.True(match); - } + var values = new RouteValueDictionary(); - [Theory] - [InlineData("///")] - [InlineData("////")] - [InlineData("/a//")] - [InlineData("/a///")] - [InlineData("//b/")] - [InlineData("//b//")] - [InlineData("///c")] - [InlineData("///c/")] - public void TryMatch_MultipleParameters_WithEmptyValues(string url) - { - // Arrange - var route = CreateMatcher("{controller}/{action}/{id}"); + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.False(match); + } - // Act - var match = route.TryMatch(url, values); + [Theory] + [InlineData("/a/b/c//")] + [InlineData("/a/b/c/////")] + public void TryMatch_CatchAllParameters_WithEmptyValuesAtTheEnd(string url) + { + // Arrange + var route = CreateMatcher("{controller}/{action}/{*id}"); - // Assert - Assert.False(match); - } + var values = new RouteValueDictionary(); - [Theory] - [InlineData("/a/b/c//")] - [InlineData("/a/b/c/////")] - public void TryMatch_CatchAllParameters_WithEmptyValuesAtTheEnd(string url) - { - // Arrange - var route = CreateMatcher("{controller}/{action}/{*id}"); + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + } - // Act - var match = route.TryMatch(url, values); + [Theory] + [InlineData("/a/b//")] + [InlineData("/a/b///c")] + public void TryMatch_CatchAllParameters_WithEmptyValues(string url) + { + // Arrange + var route = CreateMatcher("{controller}/{action}/{*id}"); - // Assert - Assert.True(match); - } + var values = new RouteValueDictionary(); - [Theory] - [InlineData("/a/b//")] - [InlineData("/a/b///c")] - public void TryMatch_CatchAllParameters_WithEmptyValues(string url) - { - // Arrange - var route = CreateMatcher("{controller}/{action}/{*id}"); + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.False(match); + } - // Act - var match = route.TryMatch(url, values); + private RoutePatternMatcher CreateMatcher(string template, object defaults = null) + { + return new RoutePatternMatcher( + RoutePatternParser.Parse(template), + new RouteValueDictionary(defaults)); + } - // Assert - Assert.False(match); - } + private static void RunTest( + string template, + string path, + RouteValueDictionary defaults, + IDictionary expected) + { + // Arrange + var matcher = new RoutePatternMatcher( + RoutePatternParser.Parse(template), + defaults ?? new RouteValueDictionary()); - private RoutePatternMatcher CreateMatcher(string template, object defaults = null) + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch(new PathString(path), values); + + // Assert + if (expected == null) { - return new RoutePatternMatcher( - RoutePatternParser.Parse(template), - new RouteValueDictionary(defaults)); + Assert.False(match); } - - private static void RunTest( - string template, - string path, - RouteValueDictionary defaults, - IDictionary expected) + else { - // Arrange - var matcher = new RoutePatternMatcher( - RoutePatternParser.Parse(template), - defaults ?? new RouteValueDictionary()); - - var values = new RouteValueDictionary(); - - // Act - var match = matcher.TryMatch(new PathString(path), values); - - // Assert - if (expected == null) - { - Assert.False(match); - } - else + Assert.True(match); + Assert.Equal(expected.Count, values.Count); + foreach (string key in values.Keys) { - Assert.True(match); - Assert.Equal(expected.Count, values.Count); - foreach (string key in values.Keys) - { - Assert.Equal(expected[key], values[key]); - } + Assert.Equal(expected[key], values[key]); } } } diff --git a/src/Http/Routing/test/UnitTests/Patterns/RoutePatternParserTest.cs b/src/Http/Routing/test/UnitTests/Patterns/RoutePatternParserTest.cs index 42b0310eda..900492fa76 100644 --- a/src/Http/Routing/test/UnitTests/Patterns/RoutePatternParserTest.cs +++ b/src/Http/Routing/test/UnitTests/Patterns/RoutePatternParserTest.cs @@ -9,755 +9,754 @@ using Microsoft.AspNetCore.Testing; using Xunit; using static Microsoft.AspNetCore.Routing.Patterns.RoutePatternFactory; -namespace Microsoft.AspNetCore.Routing.Patterns +namespace Microsoft.AspNetCore.Routing.Patterns; + +public class RoutePatternParameterParserTest { - public class RoutePatternParameterParserTest + [Fact] + public void Parse_SingleLiteral() { - [Fact] - public void Parse_SingleLiteral() - { - // Arrange - var template = "cool"; + // Arrange + var template = "cool"; - var expected = Pattern( - template, - Segment(LiteralPart("cool"))); + var expected = Pattern( + template, + Segment(LiteralPart("cool"))); - // Act - var actual = RoutePatternParser.Parse(template); + // Act + var actual = RoutePatternParser.Parse(template); - // Assert - Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); - } + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } - [Fact] - public void Parse_SingleParameter() - { - // Arrange - var template = "{p}"; + [Fact] + public void Parse_SingleParameter() + { + // Arrange + var template = "{p}"; - var expected = Pattern(template, Segment(ParameterPart("p"))); + var expected = Pattern(template, Segment(ParameterPart("p"))); - // Act - var actual = RoutePatternParser.Parse(template); + // Act + var actual = RoutePatternParser.Parse(template); - // Assert - Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); - } + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } - [Fact] - public void Parse_OptionalParameter() - { - // Arrange - var template = "{p?}"; + [Fact] + public void Parse_OptionalParameter() + { + // Arrange + var template = "{p?}"; - var expected = Pattern(template, Segment(ParameterPart("p", null, RoutePatternParameterKind.Optional))); + var expected = Pattern(template, Segment(ParameterPart("p", null, RoutePatternParameterKind.Optional))); - // Act - var actual = RoutePatternParser.Parse(template); + // Act + var actual = RoutePatternParser.Parse(template); - // Assert - Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); - } + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } - [Fact] - public void Parse_MultipleLiterals() - { - // Arrange - var template = "cool/awesome/super"; + [Fact] + public void Parse_MultipleLiterals() + { + // Arrange + var template = "cool/awesome/super"; - var expected = Pattern( - template, - Segment(LiteralPart("cool")), - Segment(LiteralPart("awesome")), - Segment(LiteralPart("super"))); + var expected = Pattern( + template, + Segment(LiteralPart("cool")), + Segment(LiteralPart("awesome")), + Segment(LiteralPart("super"))); - // Act - var actual = RoutePatternParser.Parse(template); + // Act + var actual = RoutePatternParser.Parse(template); - // Assert - Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); - } + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } - [Fact] - public void Parse_MultipleParameters() - { - // Arrange - var template = "{p1}/{p2}/{*p3}"; + [Fact] + public void Parse_MultipleParameters() + { + // Arrange + var template = "{p1}/{p2}/{*p3}"; - var expected = Pattern( - template, - Segment(ParameterPart("p1")), - Segment(ParameterPart("p2")), - Segment(ParameterPart("p3", null, RoutePatternParameterKind.CatchAll))); + var expected = Pattern( + template, + Segment(ParameterPart("p1")), + Segment(ParameterPart("p2")), + Segment(ParameterPart("p3", null, RoutePatternParameterKind.CatchAll))); - // Act - var actual = RoutePatternParser.Parse(template); + // Act + var actual = RoutePatternParser.Parse(template); - // Assert - Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); - } + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_LP() - { - // Arrange - var template = "cool-{p1}"; + [Fact] + public void Parse_ComplexSegment_LP() + { + // Arrange + var template = "cool-{p1}"; - var expected = Pattern( - template, - Segment( - LiteralPart("cool-"), - ParameterPart("p1"))); + var expected = Pattern( + template, + Segment( + LiteralPart("cool-"), + ParameterPart("p1"))); - // Act - var actual = RoutePatternParser.Parse(template); + // Act + var actual = RoutePatternParser.Parse(template); - // Assert - Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); - } + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_PL() - { - // Arrange - var template = "{p1}-cool"; + [Fact] + public void Parse_ComplexSegment_PL() + { + // Arrange + var template = "{p1}-cool"; - var expected = Pattern( - template, - Segment( - ParameterPart("p1"), - LiteralPart("-cool"))); + var expected = Pattern( + template, + Segment( + ParameterPart("p1"), + LiteralPart("-cool"))); - // Act - var actual = RoutePatternParser.Parse(template); + // Act + var actual = RoutePatternParser.Parse(template); - // Assert - Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); - } + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_PLP() - { - // Arrange - var template = "{p1}-cool-{p2}"; + [Fact] + public void Parse_ComplexSegment_PLP() + { + // Arrange + var template = "{p1}-cool-{p2}"; - var expected = Pattern( - template, - Segment( - ParameterPart("p1"), - LiteralPart("-cool-"), - ParameterPart("p2"))); + var expected = Pattern( + template, + Segment( + ParameterPart("p1"), + LiteralPart("-cool-"), + ParameterPart("p2"))); - // Act - var actual = RoutePatternParser.Parse(template); + // Act + var actual = RoutePatternParser.Parse(template); - // Assert - Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); - } + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_LPL() - { - // Arrange - var template = "cool-{p1}-awesome"; + [Fact] + public void Parse_ComplexSegment_LPL() + { + // Arrange + var template = "cool-{p1}-awesome"; - var expected = Pattern( - template, - Segment( - LiteralPart("cool-"), - ParameterPart("p1"), - LiteralPart("-awesome"))); + var expected = Pattern( + template, + Segment( + LiteralPart("cool-"), + ParameterPart("p1"), + LiteralPart("-awesome"))); - // Act - var actual = RoutePatternParser.Parse(template); + // Act + var actual = RoutePatternParser.Parse(template); - // Assert - Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); - } + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_OptionalParameterFollowingPeriod() - { - // Arrange - var template = "{p1}.{p2?}"; + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod() + { + // Arrange + var template = "{p1}.{p2?}"; - var expected = Pattern( - template, - Segment( - ParameterPart("p1"), - SeparatorPart("."), - ParameterPart("p2", null, RoutePatternParameterKind.Optional))); + var expected = Pattern( + template, + Segment( + ParameterPart("p1"), + SeparatorPart("."), + ParameterPart("p2", null, RoutePatternParameterKind.Optional))); - // Act - var actual = RoutePatternParser.Parse(template); + // Act + var actual = RoutePatternParser.Parse(template); - // Assert - Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); - } + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_ParametersFollowingPeriod() - { - // Arrange - var template = "{p1}.{p2}"; + [Fact] + public void Parse_ComplexSegment_ParametersFollowingPeriod() + { + // Arrange + var template = "{p1}.{p2}"; - var expected = Pattern( - template, - Segment( - ParameterPart("p1"), - LiteralPart("."), - ParameterPart("p2"))); + var expected = Pattern( + template, + Segment( + ParameterPart("p1"), + LiteralPart("."), + ParameterPart("p2"))); - // Act - var actual = RoutePatternParser.Parse(template); + // Act + var actual = RoutePatternParser.Parse(template); - // Assert - Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); - } + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_ThreeParameters() - { - // Arrange - var template = "{p1}.{p2}.{p3?}"; - - var expected = Pattern( - template, - Segment( - ParameterPart("p1"), - LiteralPart("."), - ParameterPart("p2"), - SeparatorPart("."), - ParameterPart("p3", null, RoutePatternParameterKind.Optional))); - - // Act - var actual = RoutePatternParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); - } + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_ThreeParameters() + { + // Arrange + var template = "{p1}.{p2}.{p3?}"; + + var expected = Pattern( + template, + Segment( + ParameterPart("p1"), + LiteralPart("."), + ParameterPart("p2"), + SeparatorPart("."), + ParameterPart("p3", null, RoutePatternParameterKind.Optional))); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_ThreeParametersSeparatedByPeriod() - { - // Arrange - var template = "{p1}.{p2}.{p3}"; - - var expected = Pattern( - template, - Segment( - ParameterPart("p1"), - LiteralPart("."), - ParameterPart("p2"), - LiteralPart("."), - ParameterPart("p3"))); - - // Act - var actual = RoutePatternParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); - } + [Fact] + public void Parse_ComplexSegment_ThreeParametersSeparatedByPeriod() + { + // Arrange + var template = "{p1}.{p2}.{p3}"; + + var expected = Pattern( + template, + Segment( + ParameterPart("p1"), + LiteralPart("."), + ParameterPart("p2"), + LiteralPart("."), + ParameterPart("p3"))); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_MiddleSegment() - { - // Arrange - var template = "{p1}.{p2?}/{p3}"; - - var expected = Pattern( - template, - Segment( - ParameterPart("p1"), - SeparatorPart("."), - ParameterPart("p2", null, RoutePatternParameterKind.Optional)), - Segment( - ParameterPart("p3"))); - - // Act - var actual = RoutePatternParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); - } + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_MiddleSegment() + { + // Arrange + var template = "{p1}.{p2?}/{p3}"; + + var expected = Pattern( + template, + Segment( + ParameterPart("p1"), + SeparatorPart("."), + ParameterPart("p2", null, RoutePatternParameterKind.Optional)), + Segment( + ParameterPart("p3"))); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_LastSegment() - { - // Arrange - var template = "{p1}/{p2}.{p3?}"; - - var expected = Pattern( - template, - Segment( - ParameterPart("p1")), - Segment( - ParameterPart("p2"), - SeparatorPart("."), - ParameterPart("p3", null, RoutePatternParameterKind.Optional))); - - // Act - var actual = RoutePatternParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); - } + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_LastSegment() + { + // Arrange + var template = "{p1}/{p2}.{p3?}"; + + var expected = Pattern( + template, + Segment( + ParameterPart("p1")), + Segment( + ParameterPart("p2"), + SeparatorPart("."), + ParameterPart("p3", null, RoutePatternParameterKind.Optional))); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_PeriodAfterSlash() - { - // Arrange - var template = "{p2}/.{p3?}"; + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_PeriodAfterSlash() + { + // Arrange + var template = "{p2}/.{p3?}"; - var expected = Pattern( - template, - Segment(ParameterPart("p2")), - Segment( - SeparatorPart("."), - ParameterPart("p3", null, RoutePatternParameterKind.Optional))); + var expected = Pattern( + template, + Segment(ParameterPart("p2")), + Segment( + SeparatorPart("."), + ParameterPart("p3", null, RoutePatternParameterKind.Optional))); - // Act - var actual = RoutePatternParser.Parse(template); + // Act + var actual = RoutePatternParser.Parse(template); - // Assert - Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); - } + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } - [Theory] - [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", @"regex(^\d{3}-\d{3}-\d{4}$)")] // ssn - [InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)}", @"regex(^\d{1,2}\/\d{1,2}\/\d{4}$)")] // date - [InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", @"regex(^\w+\@\w+\.\w+)")] // email - [InlineData(@"{p1:regex(([}}])\w+)}", @"regex(([}])\w+)")] // Not balanced } - [InlineData(@"{p1:regex(([{{(])\w+)}", @"regex(([{(])\w+)")] // Not balanced { - public void Parse_RegularExpressions(string template, string constraint) - { - // Arrange - var expected = Pattern( - template, - Segment( - ParameterPart( - "p1", - null, - RoutePatternParameterKind.Standard, - Constraint(constraint)))); - - // Act - var actual = RoutePatternParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); - } + [Theory] + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", @"regex(^\d{3}-\d{3}-\d{4}$)")] // ssn + [InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)}", @"regex(^\d{1,2}\/\d{1,2}\/\d{4}$)")] // date + [InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", @"regex(^\w+\@\w+\.\w+)")] // email + [InlineData(@"{p1:regex(([}}])\w+)}", @"regex(([}])\w+)")] // Not balanced } + [InlineData(@"{p1:regex(([{{(])\w+)}", @"regex(([{(])\w+)")] // Not balanced { + public void Parse_RegularExpressions(string template, string constraint) + { + // Arrange + var expected = Pattern( + template, + Segment( + ParameterPart( + "p1", + null, + RoutePatternParameterKind.Standard, + Constraint(constraint)))); + + // Act + var actual = RoutePatternParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new RoutePatternEqualityComparer()); + } - [Theory] - [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}}$)}")] // extra } - [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}}")] // extra } at the end - [InlineData(@"{{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}")] // extra { at the beginning - [InlineData(@"{p1:regex(([}])\w+}")] // Not escaped } - [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}$)}")] // Not escaped } - [InlineData(@"{p1:regex(abc)")] - public void Parse_RegularExpressions_Invalid(string template) - { - // Act and Assert - ExceptionAssert.Throws( - () => RoutePatternParser.Parse(template), - "There is an incomplete parameter in the route template. Check that each '{' character has a matching " + - "'}' character."); - } + [Theory] + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}}$)}")] // extra } + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}}")] // extra } at the end + [InlineData(@"{{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}")] // extra { at the beginning + [InlineData(@"{p1:regex(([}])\w+}")] // Not escaped } + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}$)}")] // Not escaped } + [InlineData(@"{p1:regex(abc)")] + public void Parse_RegularExpressions_Invalid(string template) + { + // Act and Assert + ExceptionAssert.Throws( + () => RoutePatternParser.Parse(template), + "There is an incomplete parameter in the route template. Check that each '{' character has a matching " + + "'}' character."); + } - [Theory] - [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{{4}}$)}")] // extra { - [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{4}}$)}")] // Not escaped { - public void Parse_RegularExpressions_Unescaped(string template) - { - // Act and Assert - ExceptionAssert.Throws( - () => RoutePatternParser.Parse(template), - "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'."); - } + [Theory] + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{{4}}$)}")] // extra { + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{4}}$)}")] // Not escaped { + public void Parse_RegularExpressions_Unescaped(string template) + { + // Act and Assert + ExceptionAssert.Throws( + () => RoutePatternParser.Parse(template), + "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'."); + } - [Theory] - [InlineData("{p1}.{p2?}.{p3}", "p2", ".")] - [InlineData("{p1?}{p2}", "p1", "{p2}")] - [InlineData("{p1?}{p2?}", "p1", "{p2?}")] - [InlineData("{p1}.{p2?})", "p2", ")")] - [InlineData("{foorb?}-bar-{z}", "foorb", "-bar-")] - public void Parse_ComplexSegment_OptionalParameter_NotTheLastPart( - string template, - string parameter, - string invalid) - { - // Act and Assert - ExceptionAssert.Throws( - () => RoutePatternParser.Parse(template), - "An optional parameter must be at the end of the segment. In the segment '" + template + - "', optional parameter '" + parameter + "' is followed by '" + invalid + "'."); - } + [Theory] + [InlineData("{p1}.{p2?}.{p3}", "p2", ".")] + [InlineData("{p1?}{p2}", "p1", "{p2}")] + [InlineData("{p1?}{p2?}", "p1", "{p2?}")] + [InlineData("{p1}.{p2?})", "p2", ")")] + [InlineData("{foorb?}-bar-{z}", "foorb", "-bar-")] + public void Parse_ComplexSegment_OptionalParameter_NotTheLastPart( + string template, + string parameter, + string invalid) + { + // Act and Assert + ExceptionAssert.Throws( + () => RoutePatternParser.Parse(template), + "An optional parameter must be at the end of the segment. In the segment '" + template + + "', optional parameter '" + parameter + "' is followed by '" + invalid + "'."); + } - [Theory] - [InlineData("{p1}-{p2?}", "-")] - [InlineData("{p1}..{p2?}", "..")] - [InlineData("..{p2?}", "..")] - [InlineData("{p1}.abc.{p2?}", ".abc.")] - [InlineData("{p1}{p2?}", "{p1}")] - public void Parse_ComplexSegment_OptionalParametersSeparatedByPeriod_Invalid(string template, string parameter) - { - // Act and Assert - ExceptionAssert.Throws( - () => RoutePatternParser.Parse(template), - "In the segment '" + template + "', the optional parameter 'p2' is preceded by an invalid " + - "segment '" + parameter + "'. Only a period (.) can precede an optional parameter."); - } + [Theory] + [InlineData("{p1}-{p2?}", "-")] + [InlineData("{p1}..{p2?}", "..")] + [InlineData("..{p2?}", "..")] + [InlineData("{p1}.abc.{p2?}", ".abc.")] + [InlineData("{p1}{p2?}", "{p1}")] + public void Parse_ComplexSegment_OptionalParametersSeparatedByPeriod_Invalid(string template, string parameter) + { + // Act and Assert + ExceptionAssert.Throws( + () => RoutePatternParser.Parse(template), + "In the segment '" + template + "', the optional parameter 'p2' is preceded by an invalid " + + "segment '" + parameter + "'. Only a period (.) can precede an optional parameter."); + } - [Fact] - public void InvalidTemplate_WithRepeatedParameter() - { - var ex = ExceptionAssert.Throws( - () => RoutePatternParser.Parse("{Controller}.mvc/{id}/{controller}"), - "The route parameter name 'controller' appears more than one time in the route template."); - } + [Fact] + public void InvalidTemplate_WithRepeatedParameter() + { + var ex = ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{Controller}.mvc/{id}/{controller}"), + "The route parameter name 'controller' appears more than one time in the route template."); + } - [Theory] - [InlineData("123{a}abc{")] - [InlineData("123{a}abc}")] - [InlineData("xyz}123{a}abc}")] - [InlineData("{{p1}")] - [InlineData("{p1}}")] - [InlineData("p1}}p2{")] - public void InvalidTemplate_WithMismatchedBraces(string template) - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse(template), - @"There is an incomplete parameter in the route template. Check that each '{' character has a " + - "matching '}' character."); - } + [Theory] + [InlineData("123{a}abc{")] + [InlineData("123{a}abc}")] + [InlineData("xyz}123{a}abc}")] + [InlineData("{{p1}")] + [InlineData("{p1}}")] + [InlineData("p1}}p2{")] + public void InvalidTemplate_WithMismatchedBraces(string template) + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse(template), + @"There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character."); + } - [Fact] - public void InvalidTemplate_CannotHaveCatchAllInMultiSegment() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("123{a}abc{*moo}"), - "A path segment that contains more than one section, such as a literal section or a parameter, " + - "cannot contain a catch-all parameter."); - } + [Fact] + public void InvalidTemplate_CannotHaveCatchAllInMultiSegment() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("123{a}abc{*moo}"), + "A path segment that contains more than one section, such as a literal section or a parameter, " + + "cannot contain a catch-all parameter."); + } - [Fact] - public void InvalidTemplate_CannotHaveMoreThanOneCatchAll() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("{*p1}/{*p2}"), - "A catch-all parameter can only appear as the last segment of the route template."); - } + [Fact] + public void InvalidTemplate_CannotHaveMoreThanOneCatchAll() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{*p1}/{*p2}"), + "A catch-all parameter can only appear as the last segment of the route template."); + } - [Fact] - public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("{*p1}abc{*p2}"), - "A path segment that contains more than one section, such as a literal section or a parameter, " + - "cannot contain a catch-all parameter."); - } + [Fact] + public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{*p1}abc{*p2}"), + "A path segment that contains more than one section, such as a literal section or a parameter, " + + "cannot contain a catch-all parameter."); + } - [Fact] - public void InvalidTemplate_CannotHaveCatchAllWithNoName() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("foo/{*}"), - "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + - " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional," + - " and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + - " and can occur only at the start of the parameter."); - } + [Fact] + public void InvalidTemplate_CannotHaveCatchAllWithNoName() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("foo/{*}"), + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional," + + " and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + + " and can occur only at the start of the parameter."); + } - [Theory] - [InlineData("{a*}", "a*")] - [InlineData("{*a*}", "a*")] - [InlineData("{*a*:int}", "a*")] - [InlineData("{*a*=5}", "a*")] - [InlineData("{*a*b=5}", "a*b")] - [InlineData("{p1?}.{p2/}/{p3}", "p2/")] - [InlineData("{p{{}", "p{")] - [InlineData("{p}}}", "p}")] - [InlineData("{p/}", "p/")] - public void ParseRouteParameter_ThrowsIf_ParameterContainsSpecialCharacters( - string template, - string parameterName) - { - // Arrange - var expectedMessage = "The route parameter name '" + parameterName + "' is invalid. Route parameter " + - "names must be non-empty and cannot contain these characters: '{', '}', '/'. The '?' character " + - "marks a parameter as optional, and can occur only at the end of the parameter. The '*' character " + - "marks a parameter as catch-all, and can occur only at the start of the parameter."; - - // Act & Assert - ExceptionAssert.Throws(() => RoutePatternParser.Parse(template), expectedMessage); - } + [Theory] + [InlineData("{a*}", "a*")] + [InlineData("{*a*}", "a*")] + [InlineData("{*a*:int}", "a*")] + [InlineData("{*a*=5}", "a*")] + [InlineData("{*a*b=5}", "a*b")] + [InlineData("{p1?}.{p2/}/{p3}", "p2/")] + [InlineData("{p{{}", "p{")] + [InlineData("{p}}}", "p}")] + [InlineData("{p/}", "p/")] + public void ParseRouteParameter_ThrowsIf_ParameterContainsSpecialCharacters( + string template, + string parameterName) + { + // Arrange + var expectedMessage = "The route parameter name '" + parameterName + "' is invalid. Route parameter " + + "names must be non-empty and cannot contain these characters: '{', '}', '/'. The '?' character " + + "marks a parameter as optional, and can occur only at the end of the parameter. The '*' character " + + "marks a parameter as catch-all, and can occur only at the start of the parameter."; + + // Act & Assert + ExceptionAssert.Throws(() => RoutePatternParser.Parse(template), expectedMessage); + } - [Fact] - public void InvalidTemplate_CannotHaveConsecutiveOpenBrace() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("foo/{{p1}"), - "There is an incomplete parameter in the route template. Check that each '{' character has a " + - "matching '}' character."); - } + [Fact] + public void InvalidTemplate_CannotHaveConsecutiveOpenBrace() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("foo/{{p1}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character."); + } - [Fact] - public void InvalidTemplate_CannotHaveConsecutiveCloseBrace() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("foo/{p1}}"), - "There is an incomplete parameter in the route template. Check that each '{' character has a " + - "matching '}' character."); - } + [Fact] + public void InvalidTemplate_CannotHaveConsecutiveCloseBrace() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("foo/{p1}}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character."); + } - [Fact] - public void InvalidTemplate_SameParameterTwiceThrows() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("{aaa}/{AAA}"), - "The route parameter name 'AAA' appears more than one time in the route template."); - } + [Fact] + public void InvalidTemplate_SameParameterTwiceThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{aaa}/{AAA}"), + "The route parameter name 'AAA' appears more than one time in the route template."); + } - [Fact] - public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("{aaa}/{*AAA}"), - "The route parameter name 'AAA' appears more than one time in the route template."); - } + [Fact] + public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{aaa}/{*AAA}"), + "The route parameter name 'AAA' appears more than one time in the route template."); + } - [Fact] - public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("{a}/{aa}a}/{z}"), - "There is an incomplete parameter in the route template. Check that each '{' character has a " + - "matching '}' character."); - } + [Fact] + public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{a}/{aa}a}/{z}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character."); + } - [Fact] - public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("{a}/{a{aa}/{z}"), - "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'."); - } + [Fact] + public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{a}/{a{aa}/{z}"), + "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'."); + } - [Fact] - public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("{a}/{}/{z}"), - "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + - " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + - " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + - " and can occur only at the start of the parameter."); - } + [Fact] + public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{a}/{}/{z}"), + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + + " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + + " and can occur only at the start of the parameter."); + } - [Fact] - public void InvalidTemplate_InvalidParameterNameWithQuestionThrows() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("{Controller}.mvc/{?}"), - "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + - " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + - " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + - " and can occur only at the start of the parameter."); - } + [Fact] + public void InvalidTemplate_InvalidParameterNameWithQuestionThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{Controller}.mvc/{?}"), + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + + " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + + " and can occur only at the start of the parameter."); + } - [Fact] - public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("{a}//{z}"), - "The route template separator character '/' cannot appear consecutively. It must be separated by " + - "either a parameter or a literal value."); - } + [Fact] + public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{a}//{z}"), + "The route template separator character '/' cannot appear consecutively. It must be separated by " + + "either a parameter or a literal value."); + } - [Fact] - public void InvalidTemplate_WithCatchAllNotAtTheEndThrows() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("foo/{p1}/{*p2}/{p3}"), - "A catch-all parameter can only appear as the last segment of the route template."); - } + [Fact] + public void InvalidTemplate_WithCatchAllNotAtTheEndThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("foo/{p1}/{*p2}/{p3}"), + "A catch-all parameter can only appear as the last segment of the route template."); + } - [Fact] - public void InvalidTemplate_RepeatedParametersThrows() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("foo/aa{p1}{p2}"), - "A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by " + - "a literal string."); - } + [Fact] + public void InvalidTemplate_RepeatedParametersThrows() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("foo/aa{p1}{p2}"), + "A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by " + + "a literal string."); + } - [Theory] - [InlineData("/foo")] - [InlineData("~/foo")] - public void ValidTemplate_CanStartWithSlashOrTildeSlash(string routePattern) - { - // Arrange & Act - var pattern = RoutePatternParser.Parse(routePattern); + [Theory] + [InlineData("/foo")] + [InlineData("~/foo")] + public void ValidTemplate_CanStartWithSlashOrTildeSlash(string routePattern) + { + // Arrange & Act + var pattern = RoutePatternParser.Parse(routePattern); - // Assert - Assert.Equal(routePattern, pattern.RawText); - } + // Assert + Assert.Equal(routePattern, pattern.RawText); + } - [Fact] - public void InvalidTemplate_CannotStartWithTilde() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("~foo"), - "The route template cannot start with a '~' character unless followed by a '/'."); - } + [Fact] + public void InvalidTemplate_CannotStartWithTilde() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("~foo"), + "The route template cannot start with a '~' character unless followed by a '/'."); + } - [Fact] - public void InvalidTemplate_CannotContainQuestionMark() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("foor?bar"), - "The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character."); - } + [Fact] + public void InvalidTemplate_CannotContainQuestionMark() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("foor?bar"), + "The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character."); + } - [Fact] - public void InvalidTemplate_ParameterCannotContainQuestionMark_UnlessAtEnd() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("{foor?b}"), - "The route parameter name 'foor?b' is invalid. Route parameter names must be non-empty and cannot" + - " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + - " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + - " and can occur only at the start of the parameter."); - } + [Fact] + public void InvalidTemplate_ParameterCannotContainQuestionMark_UnlessAtEnd() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{foor?b}"), + "The route parameter name 'foor?b' is invalid. Route parameter names must be non-empty and cannot" + + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + + " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + + " and can occur only at the start of the parameter."); + } - [Fact] - public void InvalidTemplate_CatchAllMarkedOptional() - { - ExceptionAssert.Throws( - () => RoutePatternParser.Parse("{a}/{*b?}"), - "A catch-all parameter cannot be marked optional."); - } + [Fact] + public void InvalidTemplate_CatchAllMarkedOptional() + { + ExceptionAssert.Throws( + () => RoutePatternParser.Parse("{a}/{*b?}"), + "A catch-all parameter cannot be marked optional."); + } - private class RoutePatternEqualityComparer : - IEqualityComparer, - IEqualityComparer + private class RoutePatternEqualityComparer : + IEqualityComparer, + IEqualityComparer + { + public bool Equals(RoutePattern x, RoutePattern y) { - public bool Equals(RoutePattern x, RoutePattern y) + if (x == null && y == null) + { + return true; + } + else if (x == null || y == null) + { + return false; + } + else { - if (x == null && y == null) + if (!string.Equals(x.RawText, y.RawText, StringComparison.Ordinal)) { - return true; + return false; } - else if (x == null || y == null) + + if (x.PathSegments.Count != y.PathSegments.Count) { return false; } - else - { - if (!string.Equals(x.RawText, y.RawText, StringComparison.Ordinal)) - { - return false; - } - - if (x.PathSegments.Count != y.PathSegments.Count) - { - return false; - } - - for (var i = 0; i < x.PathSegments.Count; i++) - { - if (x.PathSegments[i].Parts.Count != y.PathSegments[i].Parts.Count) - { - return false; - } - - for (int j = 0; j < x.PathSegments[i].Parts.Count; j++) - { - if (!Equals(x.PathSegments[i].Parts[j], y.PathSegments[i].Parts[j])) - { - return false; - } - } - } - if (x.Parameters.Count != y.Parameters.Count) + for (var i = 0; i < x.PathSegments.Count; i++) + { + if (x.PathSegments[i].Parts.Count != y.PathSegments[i].Parts.Count) { return false; } - for (var i = 0; i < x.Parameters.Count; i++) + for (int j = 0; j < x.PathSegments[i].Parts.Count; j++) { - if (!Equals(x.Parameters[i], y.Parameters[i])) + if (!Equals(x.PathSegments[i].Parts[j], y.PathSegments[i].Parts[j])) { return false; } } - - return true; } - } - private bool Equals(RoutePatternPart x, RoutePatternPart y) - { - if (x.GetType() != y.GetType()) + if (x.Parameters.Count != y.Parameters.Count) { return false; } - if (x.IsLiteral && y.IsLiteral) - { - return Equals((RoutePatternLiteralPart)x, (RoutePatternLiteralPart)y); - } - else if (x.IsParameter && y.IsParameter) + for (var i = 0; i < x.Parameters.Count; i++) { - return Equals((RoutePatternParameterPart)x, (RoutePatternParameterPart)y); - } - else if (x.IsSeparator && y.IsSeparator) - { - return Equals((RoutePatternSeparatorPart)x, (RoutePatternSeparatorPart)y); + if (!Equals(x.Parameters[i], y.Parameters[i])) + { + return false; + } } - Debug.Fail("This should not be reachable. Do you need to update the comparison logic?"); - return false; + return true; } + } - private bool Equals(RoutePatternLiteralPart x, RoutePatternLiteralPart y) + private bool Equals(RoutePatternPart x, RoutePatternPart y) + { + if (x.GetType() != y.GetType()) { - return x.Content == y.Content; + return false; } - private bool Equals(RoutePatternParameterPart x, RoutePatternParameterPart y) + if (x.IsLiteral && y.IsLiteral) { - return - x.Name == y.Name && - x.Default == y.Default && - x.ParameterKind == y.ParameterKind && - Enumerable.SequenceEqual(x.ParameterPolicies, y.ParameterPolicies, this); - + return Equals((RoutePatternLiteralPart)x, (RoutePatternLiteralPart)y); } - - public bool Equals(RoutePatternParameterPolicyReference x, RoutePatternParameterPolicyReference y) + else if (x.IsParameter && y.IsParameter) { - return - x.Content == y.Content && - x.ParameterPolicy == y.ParameterPolicy; + return Equals((RoutePatternParameterPart)x, (RoutePatternParameterPart)y); } - - private bool Equals(RoutePatternSeparatorPart x, RoutePatternSeparatorPart y) + else if (x.IsSeparator && y.IsSeparator) { - return x.Content == y.Content; + return Equals((RoutePatternSeparatorPart)x, (RoutePatternSeparatorPart)y); } - public int GetHashCode(RoutePattern obj) - { - throw new NotImplementedException(); - } + Debug.Fail("This should not be reachable. Do you need to update the comparison logic?"); + return false; + } - public int GetHashCode(RoutePatternParameterPolicyReference obj) - { - throw new NotImplementedException(); - } + private bool Equals(RoutePatternLiteralPart x, RoutePatternLiteralPart y) + { + return x.Content == y.Content; + } + + private bool Equals(RoutePatternParameterPart x, RoutePatternParameterPart y) + { + return + x.Name == y.Name && + x.Default == y.Default && + x.ParameterKind == y.ParameterKind && + Enumerable.SequenceEqual(x.ParameterPolicies, y.ParameterPolicies, this); + + } + + public bool Equals(RoutePatternParameterPolicyReference x, RoutePatternParameterPolicyReference y) + { + return + x.Content == y.Content && + x.ParameterPolicy == y.ParameterPolicy; + } + + private bool Equals(RoutePatternSeparatorPart x, RoutePatternSeparatorPart y) + { + return x.Content == y.Content; + } + + public int GetHashCode(RoutePattern obj) + { + throw new NotImplementedException(); + } + + public int GetHashCode(RoutePatternParameterPolicyReference obj) + { + throw new NotImplementedException(); } } } diff --git a/src/Http/Routing/test/UnitTests/RequestDelegateRouteBuilderExtensionsTest.cs b/src/Http/Routing/test/UnitTests/RequestDelegateRouteBuilderExtensionsTest.cs index ee1034d0cc..5470d1490e 100644 --- a/src/Http/Routing/test/UnitTests/RequestDelegateRouteBuilderExtensionsTest.cs +++ b/src/Http/Routing/test/UnitTests/RequestDelegateRouteBuilderExtensionsTest.cs @@ -10,19 +10,19 @@ using Microsoft.Extensions.ObjectPool; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +// These are really more like integration tests. They verify that these extensions +// add routes that behave as advertised. +public class RequestDelegateRouteBuilderExtensionsTest { - // These are really more like integration tests. They verify that these extensions - // add routes that behave as advertised. - public class RequestDelegateRouteBuilderExtensionsTest - { - private static readonly RequestDelegate NullHandler = (c) => Task.CompletedTask; + private static readonly RequestDelegate NullHandler = (c) => Task.CompletedTask; - public static TheoryData, Action> MatchingActions + public static TheoryData, Action> MatchingActions + { + get { - get - { - return new TheoryData, Action>() + return new TheoryData, Action>() { { b => { b.MapRoute("api/{id}", NullHandler); }, null }, { b => { b.MapMiddlewareRoute("api/{id}", app => { }); }, null }, @@ -39,38 +39,38 @@ namespace Microsoft.AspNetCore.Routing { b => { b.MapVerb("PUT", "api/{id}", NullHandler); }, c => { c.Request.Method = "PUT"; } }, { b => { b.MapMiddlewareVerb("PUT", "api/{id}", app => { }); }, c => { c.Request.Method = "PUT"; } }, }; - } } + } - [Theory] - [MemberData(nameof(MatchingActions))] - public async Task Map_MatchesRequest( - Action routeSetup, - Action requestSetup) - { - // Arrange - var services = CreateServices(); + [Theory] + [MemberData(nameof(MatchingActions))] + public async Task Map_MatchesRequest( + Action routeSetup, + Action requestSetup) + { + // Arrange + var services = CreateServices(); - var context = CreateRouteContext(services); - context.HttpContext.Request.Path = new PathString("/api/5"); - requestSetup?.Invoke(context.HttpContext); + var context = CreateRouteContext(services); + context.HttpContext.Request.Path = new PathString("/api/5"); + requestSetup?.Invoke(context.HttpContext); - var builder = CreateRouteBuilder(services); - routeSetup(builder); - var route = builder.Build(); + var builder = CreateRouteBuilder(services); + routeSetup(builder); + var route = builder.Build(); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Same(NullHandler, context.Handler); - } + // Assert + Assert.Same(NullHandler, context.Handler); + } - public static TheoryData, Action> NonmatchingActions + public static TheoryData, Action> NonmatchingActions + { + get { - get - { - return new TheoryData, Action>() + return new TheoryData, Action>() { { b => { b.MapRoute("api/{id}/extra", NullHandler); }, null }, { b => { b.MapMiddlewareRoute("api/{id}/extra", app => { }); }, null }, @@ -97,62 +97,61 @@ namespace Microsoft.AspNetCore.Routing { b => { b.MapVerb("PUT", "api/{id}/extra", NullHandler); }, c => { c.Request.Method = "PUT"; } }, { b => { b.MapMiddlewareVerb("PUT", "api/{id}/extra", app => { }); }, c => { c.Request.Method = "PUT"; } }, }; - } } + } - [Theory] - [MemberData(nameof(NonmatchingActions))] - public async Task Map_DoesNotMatchRequest( - Action routeSetup, - Action requestSetup) - { - // Arrange - var services = CreateServices(); + [Theory] + [MemberData(nameof(NonmatchingActions))] + public async Task Map_DoesNotMatchRequest( + Action routeSetup, + Action requestSetup) + { + // Arrange + var services = CreateServices(); - var context = CreateRouteContext(services); - context.HttpContext.Request.Path = new PathString("/api/5"); - requestSetup?.Invoke(context.HttpContext); + var context = CreateRouteContext(services); + context.HttpContext.Request.Path = new PathString("/api/5"); + requestSetup?.Invoke(context.HttpContext); - var builder = CreateRouteBuilder(services); - routeSetup(builder); - var route = builder.Build(); + var builder = CreateRouteBuilder(services); + routeSetup(builder); + var route = builder.Build(); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Null(context.Handler); - } + // Assert + Assert.Null(context.Handler); + } - private static IServiceProvider CreateServices() - { - var services = new ServiceCollection(); - services.AddOptions(); - services.AddRouting(); - services.AddLogging(); - return services.BuildServiceProvider(); - } + private static IServiceProvider CreateServices() + { + var services = new ServiceCollection(); + services.AddOptions(); + services.AddRouting(); + services.AddLogging(); + return services.BuildServiceProvider(); + } - private static RouteContext CreateRouteContext(IServiceProvider services) - { - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = services; - return new RouteContext(httpContext); - } + private static RouteContext CreateRouteContext(IServiceProvider services) + { + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = services; + return new RouteContext(httpContext); + } - private static IRouteBuilder CreateRouteBuilder(IServiceProvider services) - { - var applicationBuilder = new Mock(); - applicationBuilder.SetupAllProperties(); + private static IRouteBuilder CreateRouteBuilder(IServiceProvider services) + { + var applicationBuilder = new Mock(); + applicationBuilder.SetupAllProperties(); - applicationBuilder - .Setup(b => b.New().Build()) - .Returns(NullHandler); + applicationBuilder + .Setup(b => b.New().Build()) + .Returns(NullHandler); - applicationBuilder.Object.ApplicationServices = services; + applicationBuilder.Object.ApplicationServices = services; - var routeBuilder = new RouteBuilder(applicationBuilder.Object); - return routeBuilder; - } + var routeBuilder = new RouteBuilder(applicationBuilder.Object); + return routeBuilder; } } diff --git a/src/Http/Routing/test/UnitTests/RouteBuilderTest.cs b/src/Http/Routing/test/UnitTests/RouteBuilderTest.cs index 381aa1d865..c671247a31 100644 --- a/src/Http/Routing/test/UnitTests/RouteBuilderTest.cs +++ b/src/Http/Routing/test/UnitTests/RouteBuilderTest.cs @@ -7,48 +7,47 @@ using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class RouteBuilderTest { - public class RouteBuilderTest + [Fact] + public void Ctor_SetsPropertyValues() + { + // Arrange + var services = new ServiceCollection(); + services.AddSingleton(typeof(RoutingMarkerService)); + var applicationServices = services.BuildServiceProvider(); + var applicationBuilderMock = new Mock(); + applicationBuilderMock.Setup(a => a.ApplicationServices).Returns(applicationServices); + var applicationBuilder = applicationBuilderMock.Object; + var defaultHandler = Mock.Of(); + + // Act + var builder = new RouteBuilder(applicationBuilder, defaultHandler); + + // Assert + Assert.Same(applicationBuilder, builder.ApplicationBuilder); + Assert.Same(defaultHandler, builder.DefaultHandler); + Assert.Same(applicationServices, builder.ServiceProvider); + } + + [Fact] + public void Ctor_ThrowsInvalidOperationException_IfRoutingMarkerServiceIsNotRegistered() { - [Fact] - public void Ctor_SetsPropertyValues() - { - // Arrange - var services = new ServiceCollection(); - services.AddSingleton(typeof(RoutingMarkerService)); - var applicationServices = services.BuildServiceProvider(); - var applicationBuilderMock = new Mock(); - applicationBuilderMock.Setup(a => a.ApplicationServices).Returns(applicationServices); - var applicationBuilder = applicationBuilderMock.Object; - var defaultHandler = Mock.Of(); - - // Act - var builder = new RouteBuilder(applicationBuilder, defaultHandler); - - // Assert - Assert.Same(applicationBuilder, builder.ApplicationBuilder); - Assert.Same(defaultHandler, builder.DefaultHandler); - Assert.Same(applicationServices, builder.ServiceProvider); - } - - [Fact] - public void Ctor_ThrowsInvalidOperationException_IfRoutingMarkerServiceIsNotRegistered() - { - // Arrange - var applicationBuilderMock = new Mock(); - applicationBuilderMock - .Setup(s => s.ApplicationServices) - .Returns(Mock.Of()); - - // Act & Assert - var exception = Assert.Throws(() => new RouteBuilder(applicationBuilderMock.Object)); - - Assert.Equal( - "Unable to find the required services. Please add all the required services by calling " + - "'IServiceCollection.AddRouting' inside the call to 'ConfigureServices(...)'" + - " in the application startup code.", - exception.Message); - } + // Arrange + var applicationBuilderMock = new Mock(); + applicationBuilderMock + .Setup(s => s.ApplicationServices) + .Returns(Mock.Of()); + + // Act & Assert + var exception = Assert.Throws(() => new RouteBuilder(applicationBuilderMock.Object)); + + Assert.Equal( + "Unable to find the required services. Please add all the required services by calling " + + "'IServiceCollection.AddRouting' inside the call to 'ConfigureServices(...)'" + + " in the application startup code.", + exception.Message); } } diff --git a/src/Http/Routing/test/UnitTests/RouteCollectionTest.cs b/src/Http/Routing/test/UnitTests/RouteCollectionTest.cs index 8082af30f0..62346d6af9 100644 --- a/src/Http/Routing/test/UnitTests/RouteCollectionTest.cs +++ b/src/Http/Routing/test/UnitTests/RouteCollectionTest.cs @@ -15,349 +15,349 @@ using Microsoft.Extensions.Options; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class RouteCollectionTest { - public class RouteCollectionTest + private static readonly RequestDelegate NullHandler = (c) => Task.CompletedTask; + + [Theory] + [InlineData(@"Home/Index/23", "/home/index/23", true, false)] + [InlineData(@"Home/Index/23", "/Home/Index/23", false, false)] + [InlineData(@"Home/Index/23", "/home/index/23/", true, true)] + [InlineData(@"Home/Index/23", "/Home/Index/23/", false, true)] + [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/Home/Index/23/?Param1=ABC&Param2=Xyz", false, true)] + [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/Home/Index/23?Param1=ABC&Param2=Xyz", false, false)] + [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/home/index/23/?Param1=ABC&Param2=Xyz", true, true)] + [InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "/Home/Index/23/#Param1=ABC&Param2=Xyz", false, true)] + [InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "/home/index/23#Param1=ABC&Param2=Xyz", true, false)] + [InlineData(@"Home/Index/23/?Param1=ABC&Param2=Xyz", "/home/index/23/?Param1=ABC&Param2=Xyz", true, true)] + [InlineData(@"Home/Index/23/#Param1=ABC&Param2=Xyz", "/home/index/23/#Param1=ABC&Param2=Xyz", true, false)] + public void GetVirtualPath_CanLowerCaseUrls_And_AppendTrailingSlash_BasedOnOptions( + string returnUrl, + string expectedUrl, + bool lowercaseUrls, + bool appendTrailingSlash) { - private static readonly RequestDelegate NullHandler = (c) => Task.CompletedTask; - - [Theory] - [InlineData(@"Home/Index/23", "/home/index/23", true, false)] - [InlineData(@"Home/Index/23", "/Home/Index/23", false, false)] - [InlineData(@"Home/Index/23", "/home/index/23/", true, true)] - [InlineData(@"Home/Index/23", "/Home/Index/23/", false, true)] - [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/Home/Index/23/?Param1=ABC&Param2=Xyz", false, true)] - [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/Home/Index/23?Param1=ABC&Param2=Xyz", false, false)] - [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/home/index/23/?Param1=ABC&Param2=Xyz", true, true)] - [InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "/Home/Index/23/#Param1=ABC&Param2=Xyz", false, true)] - [InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "/home/index/23#Param1=ABC&Param2=Xyz", true, false)] - [InlineData(@"Home/Index/23/?Param1=ABC&Param2=Xyz", "/home/index/23/?Param1=ABC&Param2=Xyz", true, true)] - [InlineData(@"Home/Index/23/#Param1=ABC&Param2=Xyz", "/home/index/23/#Param1=ABC&Param2=Xyz", true, false)] - public void GetVirtualPath_CanLowerCaseUrls_And_AppendTrailingSlash_BasedOnOptions( - string returnUrl, - string expectedUrl, - bool lowercaseUrls, - bool appendTrailingSlash) - { - // Arrange - var target = new Mock(MockBehavior.Strict); - target - .Setup(e => e.GetVirtualPath(It.IsAny())) - .Returns(new VirtualPathData(target.Object, returnUrl)); - - var routeCollection = new RouteCollection(); - routeCollection.Add(target.Object); - var virtualPathContext = CreateVirtualPathContext( - options: GetRouteOptions( - lowerCaseUrls: lowercaseUrls, - appendTrailingSlash: appendTrailingSlash)); - - // Act - var pathData = routeCollection.GetVirtualPath(virtualPathContext); - - // Assert - Assert.Equal(expectedUrl, pathData.VirtualPath); - Assert.Same(target.Object, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Arrange + var target = new Mock(MockBehavior.Strict); + target + .Setup(e => e.GetVirtualPath(It.IsAny())) + .Returns(new VirtualPathData(target.Object, returnUrl)); + + var routeCollection = new RouteCollection(); + routeCollection.Add(target.Object); + var virtualPathContext = CreateVirtualPathContext( + options: GetRouteOptions( + lowerCaseUrls: lowercaseUrls, + appendTrailingSlash: appendTrailingSlash)); + + // Act + var pathData = routeCollection.GetVirtualPath(virtualPathContext); + + // Assert + Assert.Equal(expectedUrl, pathData.VirtualPath); + Assert.Same(target.Object, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - [Theory] - [InlineData(@"\u0130", @"/\u0130", true)] - [InlineData(@"\u0049", @"/\u0049", true)] - [InlineData(@"�ino", @"/�ino", true)] - public void GetVirtualPath_DoesntLowerCaseUrls_Invariant( - string returnUrl, - string lowercaseUrl, - bool lowercaseUrls) - { - // Arrange - var target = new Mock(MockBehavior.Strict); - target - .Setup(e => e.GetVirtualPath(It.IsAny())) - .Returns(new VirtualPathData(target.Object, returnUrl)); - - var routeCollection = new RouteCollection(); - routeCollection.Add(target.Object); - var virtualPathContext = CreateVirtualPathContext(options: GetRouteOptions(lowercaseUrls)); - - // Act - var pathData = routeCollection.GetVirtualPath(virtualPathContext); - - // Assert - Assert.Equal(lowercaseUrl, pathData.VirtualPath); - Assert.Same(target.Object, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + [Theory] + [InlineData(@"\u0130", @"/\u0130", true)] + [InlineData(@"\u0049", @"/\u0049", true)] + [InlineData(@"�ino", @"/�ino", true)] + public void GetVirtualPath_DoesntLowerCaseUrls_Invariant( + string returnUrl, + string lowercaseUrl, + bool lowercaseUrls) + { + // Arrange + var target = new Mock(MockBehavior.Strict); + target + .Setup(e => e.GetVirtualPath(It.IsAny())) + .Returns(new VirtualPathData(target.Object, returnUrl)); + + var routeCollection = new RouteCollection(); + routeCollection.Add(target.Object); + var virtualPathContext = CreateVirtualPathContext(options: GetRouteOptions(lowercaseUrls)); + + // Act + var pathData = routeCollection.GetVirtualPath(virtualPathContext); + + // Assert + Assert.Equal(lowercaseUrl, pathData.VirtualPath); + Assert.Same(target.Object, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - [Theory] - [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/Home/Index/23?Param1=ABC&Param2=Xyz", false, true, false)] - [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/Home/Index/23?Param1=ABC&Param2=Xyz", false, false, false)] - [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/home/index/23/?param1=abc¶m2=xyz", true, true, true)] - [InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "/Home/Index/23/#Param1=ABC&Param2=Xyz", false, true, true)] - [InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "/home/index/23#Param1=ABC&Param2=Xyz", true, false, false)] - [InlineData(@"Home/Index/23/?Param1=ABC&Param2=Xyz", "/home/index/23/?param1=abc¶m2=xyz", true, true, true)] - [InlineData(@"Home/Index/23/#Param1=ABC&Param2=Xyz", "/home/index/23/#Param1=ABC&Param2=Xyz", true, false, true)] - [InlineData(@"Home/Index/23/#Param1=ABC&Param2=Xyz", "/home/index/23/#param1=abc¶m2=xyz", true, true, true)] - public void GetVirtualPath_CanLowerCaseUrls_QueryStrings_BasedOnOptions( - string returnUrl, - string expectedUrl, - bool lowercaseUrls, - bool lowercaseQueryStrings, bool appendTrailingSlash) - { - // Arrange - var target = new Mock(MockBehavior.Strict); - target - .Setup(e => e.GetVirtualPath(It.IsAny())) - .Returns(new VirtualPathData(target.Object, returnUrl)); - - var routeCollection = new RouteCollection(); - routeCollection.Add(target.Object); - var virtualPathContext = CreateVirtualPathContext( - options: GetRouteOptions( - lowerCaseUrls: lowercaseUrls, - lowercaseQueryStrings: lowercaseQueryStrings, - appendTrailingSlash: appendTrailingSlash)); - - // Act - var pathData = routeCollection.GetVirtualPath(virtualPathContext); - - // Assert - Assert.Equal(expectedUrl, pathData.VirtualPath); - Assert.Same(target.Object, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + [Theory] + [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/Home/Index/23?Param1=ABC&Param2=Xyz", false, true, false)] + [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/Home/Index/23?Param1=ABC&Param2=Xyz", false, false, false)] + [InlineData(@"Home/Index/23?Param1=ABC&Param2=Xyz", "/home/index/23/?param1=abc¶m2=xyz", true, true, true)] + [InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "/Home/Index/23/#Param1=ABC&Param2=Xyz", false, true, true)] + [InlineData(@"Home/Index/23#Param1=ABC&Param2=Xyz", "/home/index/23#Param1=ABC&Param2=Xyz", true, false, false)] + [InlineData(@"Home/Index/23/?Param1=ABC&Param2=Xyz", "/home/index/23/?param1=abc¶m2=xyz", true, true, true)] + [InlineData(@"Home/Index/23/#Param1=ABC&Param2=Xyz", "/home/index/23/#Param1=ABC&Param2=Xyz", true, false, true)] + [InlineData(@"Home/Index/23/#Param1=ABC&Param2=Xyz", "/home/index/23/#param1=abc¶m2=xyz", true, true, true)] + public void GetVirtualPath_CanLowerCaseUrls_QueryStrings_BasedOnOptions( + string returnUrl, + string expectedUrl, + bool lowercaseUrls, + bool lowercaseQueryStrings, bool appendTrailingSlash) + { + // Arrange + var target = new Mock(MockBehavior.Strict); + target + .Setup(e => e.GetVirtualPath(It.IsAny())) + .Returns(new VirtualPathData(target.Object, returnUrl)); + + var routeCollection = new RouteCollection(); + routeCollection.Add(target.Object); + var virtualPathContext = CreateVirtualPathContext( + options: GetRouteOptions( + lowerCaseUrls: lowercaseUrls, + lowercaseQueryStrings: lowercaseQueryStrings, + appendTrailingSlash: appendTrailingSlash)); + + // Act + var pathData = routeCollection.GetVirtualPath(virtualPathContext); + + // Assert + Assert.Equal(expectedUrl, pathData.VirtualPath); + Assert.Same(target.Object, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - [Theory] - [MemberData(nameof(DataTokensTestData))] - public void GetVirtualPath_ReturnsDataTokens(RouteValueDictionary dataTokens, string routerName) - { - // Arrange - var virtualPath = "/TestVirtualPath"; + [Theory] + [MemberData(nameof(DataTokensTestData))] + public void GetVirtualPath_ReturnsDataTokens(RouteValueDictionary dataTokens, string routerName) + { + // Arrange + var virtualPath = "/TestVirtualPath"; - var pathContextValues = new RouteValueDictionary { { "controller", virtualPath } }; + var pathContextValues = new RouteValueDictionary { { "controller", virtualPath } }; - var pathContext = CreateVirtualPathContext( - pathContextValues, - GetRouteOptions(), - routerName); + var pathContext = CreateVirtualPathContext( + pathContextValues, + GetRouteOptions(), + routerName); - var route = CreateTemplateRoute("{controller}", routerName, dataTokens); - var routeCollection = new RouteCollection(); - routeCollection.Add(route); + var route = CreateTemplateRoute("{controller}", routerName, dataTokens); + var routeCollection = new RouteCollection(); + routeCollection.Add(route); - var expectedDataTokens = dataTokens ?? new RouteValueDictionary(); + var expectedDataTokens = dataTokens ?? new RouteValueDictionary(); - // Act - var pathData = routeCollection.GetVirtualPath(pathContext); + // Act + var pathData = routeCollection.GetVirtualPath(pathContext); - // Assert - Assert.NotNull(pathData); - Assert.Same(route, pathData.Router); + // Assert + Assert.NotNull(pathData); + Assert.Same(route, pathData.Router); - Assert.Equal(virtualPath, pathData.VirtualPath); + Assert.Equal(virtualPath, pathData.VirtualPath); - Assert.Equal(expectedDataTokens.Count, pathData.DataTokens.Count); - foreach (var dataToken in expectedDataTokens) - { - Assert.True(pathData.DataTokens.ContainsKey(dataToken.Key)); - Assert.Equal(dataToken.Value, pathData.DataTokens[dataToken.Key]); - } + Assert.Equal(expectedDataTokens.Count, pathData.DataTokens.Count); + foreach (var dataToken in expectedDataTokens) + { + Assert.True(pathData.DataTokens.ContainsKey(dataToken.Key)); + Assert.Equal(dataToken.Value, pathData.DataTokens[dataToken.Key]); } + } - [Fact] - public async Task RouteAsync_FirstMatches() - { - // Arrange - var routes = new RouteCollection(); + [Fact] + public async Task RouteAsync_FirstMatches() + { + // Arrange + var routes = new RouteCollection(); - var route1 = CreateRoute(accept: true); - routes.Add(route1.Object); + var route1 = CreateRoute(accept: true); + routes.Add(route1.Object); - var route2 = CreateRoute(accept: false); - routes.Add(route2.Object); + var route2 = CreateRoute(accept: false); + routes.Add(route2.Object); - var context = CreateRouteContext("/Cool"); + var context = CreateRouteContext("/Cool"); - // Act - await routes.RouteAsync(context); + // Act + await routes.RouteAsync(context); - // Assert - route1.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); - route2.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(0)); - Assert.NotNull(context.Handler); + // Assert + route1.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); + route2.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(0)); + Assert.NotNull(context.Handler); - Assert.Equal(1, context.RouteData.Routers.Count); - Assert.Same(route1.Object, context.RouteData.Routers[0]); - } + Assert.Equal(1, context.RouteData.Routers.Count); + Assert.Same(route1.Object, context.RouteData.Routers[0]); + } - [Fact] - public async Task RouteAsync_SecondMatches() - { - // Arrange + [Fact] + public async Task RouteAsync_SecondMatches() + { + // Arrange - var routes = new RouteCollection(); - var route1 = CreateRoute(accept: false); - routes.Add(route1.Object); + var routes = new RouteCollection(); + var route1 = CreateRoute(accept: false); + routes.Add(route1.Object); - var route2 = CreateRoute(accept: true); - routes.Add(route2.Object); + var route2 = CreateRoute(accept: true); + routes.Add(route2.Object); - var context = CreateRouteContext("/Cool"); + var context = CreateRouteContext("/Cool"); - // Act - await routes.RouteAsync(context); + // Act + await routes.RouteAsync(context); - // Assert - route1.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); - route2.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); - Assert.NotNull(context.Handler); + // Assert + route1.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); + route2.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); + Assert.NotNull(context.Handler); - Assert.Equal(1, context.RouteData.Routers.Count); - Assert.Same(route2.Object, context.RouteData.Routers[0]); - } + Assert.Equal(1, context.RouteData.Routers.Count); + Assert.Same(route2.Object, context.RouteData.Routers[0]); + } - [Fact] - public async Task RouteAsync_NoMatch() - { - // Arrange - var routes = new RouteCollection(); - var route1 = CreateRoute(accept: false); - routes.Add(route1.Object); + [Fact] + public async Task RouteAsync_NoMatch() + { + // Arrange + var routes = new RouteCollection(); + var route1 = CreateRoute(accept: false); + routes.Add(route1.Object); - var route2 = CreateRoute(accept: false); - routes.Add(route2.Object); + var route2 = CreateRoute(accept: false); + routes.Add(route2.Object); - var context = CreateRouteContext("/Cool"); + var context = CreateRouteContext("/Cool"); - // Act - await routes.RouteAsync(context); + // Act + await routes.RouteAsync(context); - // Assert - route1.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); - route2.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); - Assert.Null(context.Handler); + // Assert + route1.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); + route2.Verify(e => e.RouteAsync(It.IsAny()), Times.Exactly(1)); + Assert.Null(context.Handler); - Assert.Empty(context.RouteData.Routers); - } + Assert.Empty(context.RouteData.Routers); + } - [Theory] - [InlineData(false, "/RouteName")] - [InlineData(true, "/routename")] - public void NamedRouteTests_GetNamedRoute_ReturnsValue(bool lowercaseUrls, string expectedUrl) - { - // Arrange - var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "RouteName", "Route3" }); - var virtualPathContext = CreateVirtualPathContext( - routeName: "RouteName", - options: GetRouteOptions(lowercaseUrls)); - - // Act - var pathData = routeCollection.GetVirtualPath(virtualPathContext); - - // Assert - Assert.Equal(expectedUrl, pathData.VirtualPath); - var namedRouter = Assert.IsAssignableFrom(pathData.Router); - Assert.Equal(virtualPathContext.RouteName, namedRouter.Name); - Assert.Empty(pathData.DataTokens); - } + [Theory] + [InlineData(false, "/RouteName")] + [InlineData(true, "/routename")] + public void NamedRouteTests_GetNamedRoute_ReturnsValue(bool lowercaseUrls, string expectedUrl) + { + // Arrange + var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "RouteName", "Route3" }); + var virtualPathContext = CreateVirtualPathContext( + routeName: "RouteName", + options: GetRouteOptions(lowercaseUrls)); + + // Act + var pathData = routeCollection.GetVirtualPath(virtualPathContext); + + // Assert + Assert.Equal(expectedUrl, pathData.VirtualPath); + var namedRouter = Assert.IsAssignableFrom(pathData.Router); + Assert.Equal(virtualPathContext.RouteName, namedRouter.Name); + Assert.Empty(pathData.DataTokens); + } - [Fact] - public void NamedRouteTests_GetNamedRoute_RouteNotFound() - { - // Arrange - var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "Route3" }); - var virtualPathContext = CreateVirtualPathContext("NonExistantRoute"); + [Fact] + public void NamedRouteTests_GetNamedRoute_RouteNotFound() + { + // Arrange + var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "Route3" }); + var virtualPathContext = CreateVirtualPathContext("NonExistantRoute"); - // Act - var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext); + // Act + var stringVirtualPath = routeCollection.GetVirtualPath(virtualPathContext); - // Assert - Assert.Null(stringVirtualPath); - } + // Assert + Assert.Null(stringVirtualPath); + } - [Fact] - public void NamedRouteTests_GetNamedRoute_AmbiguousRoutesInCollection_DoesNotThrowForUnambiguousRoute() - { - // Arrange - var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "Route3", "Route4" }); + [Fact] + public void NamedRouteTests_GetNamedRoute_AmbiguousRoutesInCollection_DoesNotThrowForUnambiguousRoute() + { + // Arrange + var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", "Route3", "Route4" }); - // Add Duplicate route. - routeCollection.Add(CreateNamedRoute("Route3")); - var virtualPathContext = CreateVirtualPathContext(routeName: "Route1", options: GetRouteOptions(true)); + // Add Duplicate route. + routeCollection.Add(CreateNamedRoute("Route3")); + var virtualPathContext = CreateVirtualPathContext(routeName: "Route1", options: GetRouteOptions(true)); - // Act - var pathData = routeCollection.GetVirtualPath(virtualPathContext); + // Act + var pathData = routeCollection.GetVirtualPath(virtualPathContext); - // Assert - Assert.Equal("/route1", pathData.VirtualPath); - var namedRouter = Assert.IsAssignableFrom(pathData.Router); - Assert.Equal("Route1", namedRouter.Name); - Assert.Empty(pathData.DataTokens); - } + // Assert + Assert.Equal("/route1", pathData.VirtualPath); + var namedRouter = Assert.IsAssignableFrom(pathData.Router); + Assert.Equal("Route1", namedRouter.Name); + Assert.Empty(pathData.DataTokens); + } - [Fact] - public void NamedRouteTests_GetNamedRoute_AmbiguousRoutesInCollection_ThrowsForAmbiguousRoute() - { - // Arrange - var ambiguousRoute = "ambiguousRoute"; - var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", ambiguousRoute, "Route4" }); - - // Add Duplicate route. - routeCollection.Add(CreateNamedRoute(ambiguousRoute)); - var virtualPathContext = CreateVirtualPathContext(routeName: ambiguousRoute, options: GetRouteOptions()); - - // Act & Assert - var ex = Assert.Throws(() => routeCollection.GetVirtualPath(virtualPathContext)); - Assert.Equal( - "The supplied route name 'ambiguousRoute' is ambiguous and matched more than one route.", - ex.Message); - } + [Fact] + public void NamedRouteTests_GetNamedRoute_AmbiguousRoutesInCollection_ThrowsForAmbiguousRoute() + { + // Arrange + var ambiguousRoute = "ambiguousRoute"; + var routeCollection = GetNestedRouteCollection(new string[] { "Route1", "Route2", ambiguousRoute, "Route4" }); + + // Add Duplicate route. + routeCollection.Add(CreateNamedRoute(ambiguousRoute)); + var virtualPathContext = CreateVirtualPathContext(routeName: ambiguousRoute, options: GetRouteOptions()); + + // Act & Assert + var ex = Assert.Throws(() => routeCollection.GetVirtualPath(virtualPathContext)); + Assert.Equal( + "The supplied route name 'ambiguousRoute' is ambiguous and matched more than one route.", + ex.Message); + } - [Fact] - public void GetVirtualPath_AmbiguousRoutes_RequiresRouteValueValidation_Error() - { - // Arrange - var namedRoute = CreateNamedRoute("Ambiguous", accept: false); + [Fact] + public void GetVirtualPath_AmbiguousRoutes_RequiresRouteValueValidation_Error() + { + // Arrange + var namedRoute = CreateNamedRoute("Ambiguous", accept: false); - var routeCollection = new RouteCollection(); - routeCollection.Add(namedRoute); + var routeCollection = new RouteCollection(); + routeCollection.Add(namedRoute); - var innerRouteCollection = new RouteCollection(); - innerRouteCollection.Add(namedRoute); - routeCollection.Add(innerRouteCollection); + var innerRouteCollection = new RouteCollection(); + innerRouteCollection.Add(namedRoute); + routeCollection.Add(innerRouteCollection); - var virtualPathContext = CreateVirtualPathContext("Ambiguous"); + var virtualPathContext = CreateVirtualPathContext("Ambiguous"); - // Act & Assert - var ex = Assert.Throws(() => routeCollection.GetVirtualPath(virtualPathContext)); - Assert.Equal("The supplied route name 'Ambiguous' is ambiguous and matched more than one route.", ex.Message); - } + // Act & Assert + var ex = Assert.Throws(() => routeCollection.GetVirtualPath(virtualPathContext)); + Assert.Equal("The supplied route name 'Ambiguous' is ambiguous and matched more than one route.", ex.Message); + } - // "Integration" tests for RouteCollection + // "Integration" tests for RouteCollection - public static IEnumerable IntegrationTestData + public static IEnumerable IntegrationTestData + { + get { - get - { - yield return new object[] { + yield return new object[] { "{controller}/{action}", new RouteValueDictionary { { "controller", "Home" }, { "action", "Index" } }, "/home/index", true }; - yield return new object[] { + yield return new object[] { "{controller}/{action}/", new RouteValueDictionary { { "controller", "Home" }, { "action", "Index" } }, "/Home/Index", false }; - yield return new object[] { + yield return new object[] { "api/{action}/", new RouteValueDictionary { { "action", "Create" } }, "/api/create", true }; - yield return new object[] { + yield return new object[] { "api/{action}/{id}", new RouteValueDictionary { { "action", "Create" }, @@ -367,7 +367,7 @@ namespace Microsoft.AspNetCore.Routing "/api/create/23?Param1=Value1&Param2=Value2", true }; - yield return new object[] { + yield return new object[] { "api/{action}/{id}", new RouteValueDictionary { { "action", "Create" }, @@ -376,39 +376,39 @@ namespace Microsoft.AspNetCore.Routing { "Param2", "Value2" } }, "/api/Create/23?Param1=Value1&Param2=Value2", false }; - } } + } - [Theory] - [MemberData(nameof(IntegrationTestData))] - public void GetVirtualPath_Success( - string template, - RouteValueDictionary values, - string expectedUrl, - bool lowercaseUrls) - { - // Arrange - var routeCollection = new RouteCollection(); - var route = CreateTemplateRoute(template); - routeCollection.Add(route); - var context = CreateVirtualPathContext(values, options: GetRouteOptions(lowercaseUrls)); - - // Act - var pathData = routeCollection.GetVirtualPath(context); - - // Assert - Assert.Equal(expectedUrl, pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + [Theory] + [MemberData(nameof(IntegrationTestData))] + public void GetVirtualPath_Success( + string template, + RouteValueDictionary values, + string expectedUrl, + bool lowercaseUrls) + { + // Arrange + var routeCollection = new RouteCollection(); + var route = CreateTemplateRoute(template); + routeCollection.Add(route); + var context = CreateVirtualPathContext(values, options: GetRouteOptions(lowercaseUrls)); + + // Act + var pathData = routeCollection.GetVirtualPath(context); + + // Assert + Assert.Equal(expectedUrl, pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - public static IEnumerable RestoresRouteDataForEachRouterData + public static IEnumerable RestoresRouteDataForEachRouterData + { + get { - get - { - // Here 'area' segment doesn't have a value but the later segments have values. This is an invalid - // route match and the url generation should look into the next available route in the collection. - yield return new object[] { + // Here 'area' segment doesn't have a value but the later segments have values. This is an invalid + // route match and the url generation should look into the next available route in the collection. + yield return new object[] { new Route[] { CreateTemplateRoute("{area?}/{controller=Home}/{action=Index}/{id?}", "1"), @@ -418,9 +418,9 @@ namespace Microsoft.AspNetCore.Routing "/Test", "2" }; - // Here the segment 'a' is valid but 'b' is not as it would be empty. This would be an invalid route match, but - // the route value of 'a' should still be present to be evaluated for the next available route. - yield return new object[] { + // Here the segment 'a' is valid but 'b' is not as it would be empty. This would be an invalid route match, but + // the route value of 'a' should still be present to be evaluated for the next available route. + yield return new object[] { new[] { CreateTemplateRoute("{a}/{b?}/{c}", "1"), @@ -429,284 +429,283 @@ namespace Microsoft.AspNetCore.Routing new RouteValueDictionary(new { a = "Test", c = "Foo" }), "/Test?c=Foo", "2" }; - } } + } - [Theory] - [MemberData(nameof(RestoresRouteDataForEachRouterData))] - public void GetVirtualPath_RestoresRouteData_ForEachRouter( - Route[] routes, - RouteValueDictionary routeValues, - string expectedUrl, - string expectedRouteToMatch) + [Theory] + [MemberData(nameof(RestoresRouteDataForEachRouterData))] + public void GetVirtualPath_RestoresRouteData_ForEachRouter( + Route[] routes, + RouteValueDictionary routeValues, + string expectedUrl, + string expectedRouteToMatch) + { + // Arrange + var routeCollection = new RouteCollection(); + foreach (var route in routes) { - // Arrange - var routeCollection = new RouteCollection(); - foreach (var route in routes) - { - routeCollection.Add(route); - } - var context = CreateVirtualPathContext(routeValues); - - // Act - var pathData = routeCollection.GetVirtualPath(context); - - // Assert - Assert.Equal(expectedUrl, pathData.VirtualPath); - Assert.Same(expectedRouteToMatch, ((INamedRouter)pathData.Router).Name); - Assert.Empty(pathData.DataTokens); + routeCollection.Add(route); } + var context = CreateVirtualPathContext(routeValues); - [Fact] - public void GetVirtualPath_NoBestEffort_NoMatch() - { - // Arrange - var route1 = CreateRoute(accept: false, match: false, matchValue: "bad"); - var route2 = CreateRoute(accept: false, match: false, matchValue: "bad"); - var route3 = CreateRoute(accept: false, match: false, matchValue: "bad"); + // Act + var pathData = routeCollection.GetVirtualPath(context); - var routeCollection = new RouteCollection(); - routeCollection.Add(route1.Object); - routeCollection.Add(route2.Object); - routeCollection.Add(route3.Object); + // Assert + Assert.Equal(expectedUrl, pathData.VirtualPath); + Assert.Same(expectedRouteToMatch, ((INamedRouter)pathData.Router).Name); + Assert.Empty(pathData.DataTokens); + } - var virtualPathContext = CreateVirtualPathContext(); + [Fact] + public void GetVirtualPath_NoBestEffort_NoMatch() + { + // Arrange + var route1 = CreateRoute(accept: false, match: false, matchValue: "bad"); + var route2 = CreateRoute(accept: false, match: false, matchValue: "bad"); + var route3 = CreateRoute(accept: false, match: false, matchValue: "bad"); - // Act - var path = routeCollection.GetVirtualPath(virtualPathContext); + var routeCollection = new RouteCollection(); + routeCollection.Add(route1.Object); + routeCollection.Add(route2.Object); + routeCollection.Add(route3.Object); - Assert.Null(path); + var virtualPathContext = CreateVirtualPathContext(); - // All of these should be called - route1.Verify(r => r.GetVirtualPath(It.IsAny()), Times.Once()); - route2.Verify(r => r.GetVirtualPath(It.IsAny()), Times.Once()); - route3.Verify(r => r.GetVirtualPath(It.IsAny()), Times.Once()); - } + // Act + var path = routeCollection.GetVirtualPath(virtualPathContext); + + Assert.Null(path); - // DataTokens test data for RouterCollection.GetVirtualPath - public static IEnumerable DataTokensTestData + // All of these should be called + route1.Verify(r => r.GetVirtualPath(It.IsAny()), Times.Once()); + route2.Verify(r => r.GetVirtualPath(It.IsAny()), Times.Once()); + route3.Verify(r => r.GetVirtualPath(It.IsAny()), Times.Once()); + } + + // DataTokens test data for RouterCollection.GetVirtualPath + public static IEnumerable DataTokensTestData + { + get { - get - { - yield return new object[] { null, null }; - yield return new object[] { new RouteValueDictionary(), null }; - yield return new object[] { new RouteValueDictionary() { { "tokenKey", "tokenValue" } }, null }; - - yield return new object[] { null, "routerA" }; - yield return new object[] { new RouteValueDictionary(), "routerA" }; - yield return new object[] { new RouteValueDictionary() { { "tokenKey", "tokenValue" } }, "routerA" }; - } + yield return new object[] { null, null }; + yield return new object[] { new RouteValueDictionary(), null }; + yield return new object[] { new RouteValueDictionary() { { "tokenKey", "tokenValue" } }, null }; + + yield return new object[] { null, "routerA" }; + yield return new object[] { new RouteValueDictionary(), "routerA" }; + yield return new object[] { new RouteValueDictionary() { { "tokenKey", "tokenValue" } }, "routerA" }; } + } - private static RouteCollection GetRouteCollectionWithNamedRoutes(IEnumerable routeNames) + private static RouteCollection GetRouteCollectionWithNamedRoutes(IEnumerable routeNames) + { + var routes = new RouteCollection(); + foreach (var routeName in routeNames) { - var routes = new RouteCollection(); - foreach (var routeName in routeNames) - { - var route1 = CreateNamedRoute(routeName, accept: true); - routes.Add(route1); - } - - return routes; + var route1 = CreateNamedRoute(routeName, accept: true); + routes.Add(route1); } - private static RouteCollection GetNestedRouteCollection(string[] routeNames) + return routes; + } + + private static RouteCollection GetNestedRouteCollection(string[] routeNames) + { + int index = Random.Shared.Next(0, routeNames.Length - 1); + var first = routeNames.Take(index).ToArray(); + var second = routeNames.Skip(index).ToArray(); + + var rc1 = GetRouteCollectionWithNamedRoutes(first); + var rc2 = GetRouteCollectionWithNamedRoutes(second); + var rc3 = new RouteCollection(); + var rc4 = new RouteCollection(); + + rc1.Add(rc3); + rc4.Add(rc2); + + // Add a few unnamedRoutes. + rc1.Add(CreateRoute(accept: false).Object); + rc2.Add(CreateRoute(accept: false).Object); + rc3.Add(CreateRoute(accept: false).Object); + rc3.Add(CreateRoute(accept: false).Object); + rc4.Add(CreateRoute(accept: false).Object); + rc4.Add(CreateRoute(accept: false).Object); + + var routeCollection = new RouteCollection(); + routeCollection.Add(rc1); + routeCollection.Add(rc4); + + return routeCollection; + } + + private static INamedRouter CreateNamedRoute(string name, bool accept = false, string matchValue = null) + { + if (matchValue == null) { - int index = Random.Shared.Next(0, routeNames.Length - 1); - var first = routeNames.Take(index).ToArray(); - var second = routeNames.Skip(index).ToArray(); - - var rc1 = GetRouteCollectionWithNamedRoutes(first); - var rc2 = GetRouteCollectionWithNamedRoutes(second); - var rc3 = new RouteCollection(); - var rc4 = new RouteCollection(); - - rc1.Add(rc3); - rc4.Add(rc2); - - // Add a few unnamedRoutes. - rc1.Add(CreateRoute(accept: false).Object); - rc2.Add(CreateRoute(accept: false).Object); - rc3.Add(CreateRoute(accept: false).Object); - rc3.Add(CreateRoute(accept: false).Object); - rc4.Add(CreateRoute(accept: false).Object); - rc4.Add(CreateRoute(accept: false).Object); - - var routeCollection = new RouteCollection(); - routeCollection.Add(rc1); - routeCollection.Add(rc4); - - return routeCollection; + matchValue = name; } - private static INamedRouter CreateNamedRoute(string name, bool accept = false, string matchValue = null) + var target = new Mock(MockBehavior.Strict); + target + .Setup(e => e.GetVirtualPath(It.IsAny())) + .Returns(c => + c.RouteName == name ? new VirtualPathData(target.Object, matchValue) : null) + .Verifiable(); + + target + .SetupGet(e => e.Name) + .Returns(name); + + target + .Setup(e => e.RouteAsync(It.IsAny())) + .Callback((c) => c.Handler = accept ? NullHandler : null) + .Returns(Task.FromResult(null)) + .Verifiable(); + + return target.Object; + } + + private static Route CreateTemplateRoute( + string template, + string routerName = null, + RouteValueDictionary dataTokens = null, + IInlineConstraintResolver constraintResolver = null) + { + var target = new Mock(MockBehavior.Strict); + target + .Setup(e => e.GetVirtualPath(It.IsAny())) + .Returns(rc => null); + + if (constraintResolver == null) { - if (matchValue == null) - { - matchValue = name; - } - - var target = new Mock(MockBehavior.Strict); - target - .Setup(e => e.GetVirtualPath(It.IsAny())) - .Returns(c => - c.RouteName == name ? new VirtualPathData(target.Object, matchValue) : null) - .Verifiable(); - - target - .SetupGet(e => e.Name) - .Returns(name); - - target - .Setup(e => e.RouteAsync(It.IsAny())) - .Callback((c) => c.Handler = accept ? NullHandler : null) - .Returns(Task.FromResult(null)) - .Verifiable(); - - return target.Object; + constraintResolver = new Mock().Object; } - private static Route CreateTemplateRoute( - string template, - string routerName = null, - RouteValueDictionary dataTokens = null, - IInlineConstraintResolver constraintResolver = null) + return new Route( + target.Object, + routerName, + template, + defaults: null, + constraints: null, + dataTokens: dataTokens, + inlineConstraintResolver: constraintResolver); + } + + private static VirtualPathContext CreateVirtualPathContext( + string routeName = null, + ILoggerFactory loggerFactory = null, + Action options = null) + { + if (loggerFactory == null) { - var target = new Mock(MockBehavior.Strict); - target - .Setup(e => e.GetVirtualPath(It.IsAny())) - .Returns(rc => null); - - if (constraintResolver == null) - { - constraintResolver = new Mock().Object; - } - - return new Route( - target.Object, - routerName, - template, - defaults: null, - constraints: null, - dataTokens: dataTokens, - inlineConstraintResolver: constraintResolver); + loggerFactory = NullLoggerFactory.Instance; } - private static VirtualPathContext CreateVirtualPathContext( - string routeName = null, - ILoggerFactory loggerFactory = null, - Action options = null) + var request = new Mock(MockBehavior.Strict); + + var services = new ServiceCollection(); + services.AddOptions(); + services.AddRouting(); + if (options != null) { - if (loggerFactory == null) - { - loggerFactory = NullLoggerFactory.Instance; - } - - var request = new Mock(MockBehavior.Strict); - - var services = new ServiceCollection(); - services.AddOptions(); - services.AddRouting(); - if (options != null) - { - services.Configure(options); - } - - var context = new Mock(MockBehavior.Strict); - context.SetupGet(m => m.RequestServices).Returns(services.BuildServiceProvider()); - context.SetupGet(c => c.Request).Returns(request.Object); - - return new VirtualPathContext(context.Object, null, null, routeName); + services.Configure(options); } - private static VirtualPathContext CreateVirtualPathContext( - RouteValueDictionary values, - Action options = null, - string routeName = null) + var context = new Mock(MockBehavior.Strict); + context.SetupGet(m => m.RequestServices).Returns(services.BuildServiceProvider()); + context.SetupGet(c => c.Request).Returns(request.Object); + + return new VirtualPathContext(context.Object, null, null, routeName); + } + + private static VirtualPathContext CreateVirtualPathContext( + RouteValueDictionary values, + Action options = null, + string routeName = null) + { + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddOptions(); + services.AddRouting(); + if (options != null) { - var services = new ServiceCollection(); - services.AddSingleton(NullLoggerFactory.Instance); - services.AddOptions(); - services.AddRouting(); - if (options != null) - { - services.Configure(options); - } - - var context = new DefaultHttpContext - { - RequestServices = services.BuildServiceProvider(), - }; - - return new VirtualPathContext( - context, - ambientValues: null, - values: values, - routeName: routeName); + services.Configure(options); } - private static RouteContext CreateRouteContext( - string requestPath, - ILoggerFactory loggerFactory = null, - RouteOptions options = null) + var context = new DefaultHttpContext { - if (loggerFactory == null) - { - loggerFactory = NullLoggerFactory.Instance; - } - - if (options == null) - { - options = new RouteOptions(); - } - - var request = new Mock(MockBehavior.Strict); - request.SetupGet(r => r.Path).Returns(requestPath); - - var optionsAccessor = new Mock>(MockBehavior.Strict); - optionsAccessor.SetupGet(o => o.Value).Returns(options); - - var context = new Mock(MockBehavior.Strict); - context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) - .Returns(loggerFactory); - context.Setup(m => m.RequestServices.GetService(typeof(IOptions))) - .Returns(optionsAccessor.Object); - context.SetupGet(c => c.Request).Returns(request.Object); - - return new RouteContext(context.Object); - } + RequestServices = services.BuildServiceProvider(), + }; + + return new VirtualPathContext( + context, + ambientValues: null, + values: values, + routeName: routeName); + } - private static Mock CreateRoute( - bool accept = true, - bool match = false, - string matchValue = "value") + private static RouteContext CreateRouteContext( + string requestPath, + ILoggerFactory loggerFactory = null, + RouteOptions options = null) + { + if (loggerFactory == null) { - var target = new Mock(MockBehavior.Strict); - target - .Setup(e => e.GetVirtualPath(It.IsAny())) - .Returns(accept || match ? new VirtualPathData(target.Object, matchValue) : null) - .Verifiable(); - - target - .Setup(e => e.RouteAsync(It.IsAny())) - .Callback((c) => c.Handler = accept ? NullHandler : null) - .Returns(Task.FromResult(null)) - .Verifiable(); - - return target; + loggerFactory = NullLoggerFactory.Instance; } - private static Action GetRouteOptions( - bool lowerCaseUrls = false, - bool appendTrailingSlash = false, - bool lowercaseQueryStrings = false) + if (options == null) { - return (options) => - { - options.LowercaseUrls = lowerCaseUrls; - options.AppendTrailingSlash = appendTrailingSlash; - options.LowercaseQueryStrings = lowercaseQueryStrings; - }; + options = new RouteOptions(); } + + var request = new Mock(MockBehavior.Strict); + request.SetupGet(r => r.Path).Returns(requestPath); + + var optionsAccessor = new Mock>(MockBehavior.Strict); + optionsAccessor.SetupGet(o => o.Value).Returns(options); + + var context = new Mock(MockBehavior.Strict); + context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) + .Returns(loggerFactory); + context.Setup(m => m.RequestServices.GetService(typeof(IOptions))) + .Returns(optionsAccessor.Object); + context.SetupGet(c => c.Request).Returns(request.Object); + + return new RouteContext(context.Object); + } + + private static Mock CreateRoute( + bool accept = true, + bool match = false, + string matchValue = "value") + { + var target = new Mock(MockBehavior.Strict); + target + .Setup(e => e.GetVirtualPath(It.IsAny())) + .Returns(accept || match ? new VirtualPathData(target.Object, matchValue) : null) + .Verifiable(); + + target + .Setup(e => e.RouteAsync(It.IsAny())) + .Callback((c) => c.Handler = accept ? NullHandler : null) + .Returns(Task.FromResult(null)) + .Verifiable(); + + return target; + } + + private static Action GetRouteOptions( + bool lowerCaseUrls = false, + bool appendTrailingSlash = false, + bool lowercaseQueryStrings = false) + { + return (options) => + { + options.LowercaseUrls = lowerCaseUrls; + options.AppendTrailingSlash = appendTrailingSlash; + options.LowercaseQueryStrings = lowercaseQueryStrings; + }; } } diff --git a/src/Http/Routing/test/UnitTests/RouteConstraintBuilderTest.cs b/src/Http/Routing/test/UnitTests/RouteConstraintBuilderTest.cs index 66eb7ada18..6188ce2694 100644 --- a/src/Http/Routing/test/UnitTests/RouteConstraintBuilderTest.cs +++ b/src/Http/Routing/test/UnitTests/RouteConstraintBuilderTest.cs @@ -11,181 +11,180 @@ using Microsoft.Extensions.Options; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class RouteConstraintBuilderTest { - public class RouteConstraintBuilderTest + [Fact] + public void AddConstraint_String_CreatesARegex() + { + // Arrange + var builder = CreateBuilder("{controller}/{action}"); + builder.AddConstraint("controller", "abc"); + + // Act + var result = builder.Build(); + + // Assert + Assert.Equal(1, result.Count); + Assert.Equal("controller", result.First().Key); + + Assert.IsType(Assert.Single(result).Value); + } + + [Fact] + public void AddConstraint_IRouteConstraint() + { + // Arrange + var originalConstraint = Mock.Of(); + + var builder = CreateBuilder("{controller}/{action}"); + builder.AddConstraint("controller", originalConstraint); + + // Act + var result = builder.Build(); + + // Assert + Assert.Equal(1, result.Count); + + var kvp = Assert.Single(result); + Assert.Equal("controller", kvp.Key); + + Assert.Same(originalConstraint, kvp.Value); + } + + [Fact] + public void AddResolvedConstraint_IRouteConstraint() + { + // Arrange + var builder = CreateBuilder("{controller}/{action}"); + builder.AddResolvedConstraint("controller", "int"); + + // Act + var result = builder.Build(); + + // Assert + Assert.Equal(1, result.Count); + + var kvp = Assert.Single(result); + Assert.Equal("controller", kvp.Key); + + Assert.IsType(kvp.Value); + } + + [Fact] + public void AddConstraint_InvalidType_Throws() + { + // Arrange + var builder = CreateBuilder("{controller}/{action}"); + + // Act & Assert + ExceptionAssert.Throws( + () => builder.AddConstraint("controller", 5), + "The constraint entry 'controller' - '5' on the route " + + "'{controller}/{action}' must have a string value or be of a type which implements '" + + typeof(IRouteConstraint) + "'."); + } + + [Fact] + public void AddResolvedConstraint_NotFound_Throws() + { + // Arrange + var unresolvedConstraint = @"test"; + + var builder = CreateBuilder("{controller}/{action}"); + + // Act & Assert + ExceptionAssert.Throws( + () => builder.AddResolvedConstraint("controller", unresolvedConstraint), + @"The constraint entry 'controller' - '" + unresolvedConstraint + "' on the route " + + "'{controller}/{action}' could not be resolved by the constraint resolver " + + "of type 'DefaultInlineConstraintResolver'."); + } + + [Fact] + public void AddResolvedConstraint_ForOptionalParameter() + { + var builder = CreateBuilder("{controller}/{action}/{id}"); + builder.SetOptional("id"); + builder.AddResolvedConstraint("id", "int"); + + var result = builder.Build(); + Assert.Equal(1, result.Count); + Assert.Equal("id", result.First().Key); + Assert.IsType(Assert.Single(result).Value); + } + + [Fact] + public void AddResolvedConstraint_SetOptionalParameter_AfterAddingTheParameter() { - [Fact] - public void AddConstraint_String_CreatesARegex() - { - // Arrange - var builder = CreateBuilder("{controller}/{action}"); - builder.AddConstraint("controller", "abc"); - - // Act - var result = builder.Build(); - - // Assert - Assert.Equal(1, result.Count); - Assert.Equal("controller", result.First().Key); - - Assert.IsType(Assert.Single(result).Value); - } - - [Fact] - public void AddConstraint_IRouteConstraint() - { - // Arrange - var originalConstraint = Mock.Of(); - - var builder = CreateBuilder("{controller}/{action}"); - builder.AddConstraint("controller", originalConstraint); - - // Act - var result = builder.Build(); - - // Assert - Assert.Equal(1, result.Count); - - var kvp = Assert.Single(result); - Assert.Equal("controller", kvp.Key); - - Assert.Same(originalConstraint, kvp.Value); - } - - [Fact] - public void AddResolvedConstraint_IRouteConstraint() - { - // Arrange - var builder = CreateBuilder("{controller}/{action}"); - builder.AddResolvedConstraint("controller", "int"); - - // Act - var result = builder.Build(); - - // Assert - Assert.Equal(1, result.Count); - - var kvp = Assert.Single(result); - Assert.Equal("controller", kvp.Key); - - Assert.IsType(kvp.Value); - } - - [Fact] - public void AddConstraint_InvalidType_Throws() - { - // Arrange - var builder = CreateBuilder("{controller}/{action}"); - - // Act & Assert - ExceptionAssert.Throws( - () => builder.AddConstraint("controller", 5), - "The constraint entry 'controller' - '5' on the route " + - "'{controller}/{action}' must have a string value or be of a type which implements '" + - typeof(IRouteConstraint) + "'."); - } - - [Fact] - public void AddResolvedConstraint_NotFound_Throws() - { - // Arrange - var unresolvedConstraint = @"test"; - - var builder = CreateBuilder("{controller}/{action}"); - - // Act & Assert - ExceptionAssert.Throws( - () => builder.AddResolvedConstraint("controller", unresolvedConstraint), - @"The constraint entry 'controller' - '" + unresolvedConstraint + "' on the route " + - "'{controller}/{action}' could not be resolved by the constraint resolver " + - "of type 'DefaultInlineConstraintResolver'."); - } - - [Fact] - public void AddResolvedConstraint_ForOptionalParameter() - { - var builder = CreateBuilder("{controller}/{action}/{id}"); - builder.SetOptional("id"); - builder.AddResolvedConstraint("id", "int"); - - var result = builder.Build(); - Assert.Equal(1, result.Count); - Assert.Equal("id", result.First().Key); - Assert.IsType(Assert.Single(result).Value); - } - - [Fact] - public void AddResolvedConstraint_SetOptionalParameter_AfterAddingTheParameter() - { - var builder = CreateBuilder("{controller}/{action}/{id}"); - builder.AddResolvedConstraint("id", "int"); - builder.SetOptional("id"); - - var result = builder.Build(); - Assert.Equal(1, result.Count); - Assert.Equal("id", result.First().Key); - Assert.IsType(Assert.Single(result).Value); - } - - [Fact] - public void AddResolvedConstraint_And_AddConstraint_ForOptionalParameter() - { - var builder = CreateBuilder("{controller}/{action}/{name}"); - builder.SetOptional("name"); - builder.AddResolvedConstraint("name", "alpha"); - var minLenConstraint = new MinLengthRouteConstraint(10); - builder.AddConstraint("name", minLenConstraint); - - var result = builder.Build(); - Assert.Equal(1, result.Count); - Assert.Equal("name", result.First().Key); - Assert.IsType(Assert.Single(result).Value); - var optionalConstraint = (OptionalRouteConstraint)result.First().Value; - var compositeConstraint = Assert.IsType(optionalConstraint.InnerConstraint); ; - Assert.Equal(2, compositeConstraint.Constraints.Count()); - - Assert.Single(compositeConstraint.Constraints, c => c is MinLengthRouteConstraint); - Assert.Single(compositeConstraint.Constraints, c => c is AlphaRouteConstraint); - } - - [Theory] - [InlineData("abc", "abc", true)] // simple case - [InlineData("abc", "bbb|abc", true)] // Regex or - [InlineData("Abc", "abc", true)] // Case insensitive - [InlineData("Abc ", "abc", false)] // Matches whole (but no trimming) - [InlineData("Abcd", "abc", false)] // Matches whole (additional non whitespace char) - [InlineData("Abc", " abc", false)] // Matches whole (less one char) - public void StringConstraintsMatchingScenarios(string routeValue, - string constraintValue, - bool shouldMatch) - { - // Arrange - var routeValues = new RouteValueDictionary(new { controller = routeValue }); - - var builder = CreateBuilder("{controller}/{action}"); - builder.AddConstraint("controller", constraintValue); - - var constraint = Assert.Single(builder.Build()).Value; - - Assert.Equal(shouldMatch, - constraint.Match( - httpContext: new Mock().Object, - route: new Mock().Object, - routeKey: "controller", - values: routeValues, - routeDirection: RouteDirection.IncomingRequest)); - } - - private static RouteConstraintBuilder CreateBuilder(string template) - { - var options = new Mock>(MockBehavior.Strict); - options - .SetupGet(o => o.Value) - .Returns(new RouteOptions()); - - var inlineConstraintResolver = new DefaultInlineConstraintResolver(options.Object, new TestServiceProvider()); - return new RouteConstraintBuilder(inlineConstraintResolver, template); - } + var builder = CreateBuilder("{controller}/{action}/{id}"); + builder.AddResolvedConstraint("id", "int"); + builder.SetOptional("id"); + + var result = builder.Build(); + Assert.Equal(1, result.Count); + Assert.Equal("id", result.First().Key); + Assert.IsType(Assert.Single(result).Value); + } + + [Fact] + public void AddResolvedConstraint_And_AddConstraint_ForOptionalParameter() + { + var builder = CreateBuilder("{controller}/{action}/{name}"); + builder.SetOptional("name"); + builder.AddResolvedConstraint("name", "alpha"); + var minLenConstraint = new MinLengthRouteConstraint(10); + builder.AddConstraint("name", minLenConstraint); + + var result = builder.Build(); + Assert.Equal(1, result.Count); + Assert.Equal("name", result.First().Key); + Assert.IsType(Assert.Single(result).Value); + var optionalConstraint = (OptionalRouteConstraint)result.First().Value; + var compositeConstraint = Assert.IsType(optionalConstraint.InnerConstraint); ; + Assert.Equal(2, compositeConstraint.Constraints.Count()); + + Assert.Single(compositeConstraint.Constraints, c => c is MinLengthRouteConstraint); + Assert.Single(compositeConstraint.Constraints, c => c is AlphaRouteConstraint); + } + + [Theory] + [InlineData("abc", "abc", true)] // simple case + [InlineData("abc", "bbb|abc", true)] // Regex or + [InlineData("Abc", "abc", true)] // Case insensitive + [InlineData("Abc ", "abc", false)] // Matches whole (but no trimming) + [InlineData("Abcd", "abc", false)] // Matches whole (additional non whitespace char) + [InlineData("Abc", " abc", false)] // Matches whole (less one char) + public void StringConstraintsMatchingScenarios(string routeValue, + string constraintValue, + bool shouldMatch) + { + // Arrange + var routeValues = new RouteValueDictionary(new { controller = routeValue }); + + var builder = CreateBuilder("{controller}/{action}"); + builder.AddConstraint("controller", constraintValue); + + var constraint = Assert.Single(builder.Build()).Value; + + Assert.Equal(shouldMatch, + constraint.Match( + httpContext: new Mock().Object, + route: new Mock().Object, + routeKey: "controller", + values: routeValues, + routeDirection: RouteDirection.IncomingRequest)); + } + + private static RouteConstraintBuilder CreateBuilder(string template) + { + var options = new Mock>(MockBehavior.Strict); + options + .SetupGet(o => o.Value) + .Returns(new RouteOptions()); + + var inlineConstraintResolver = new DefaultInlineConstraintResolver(options.Object, new TestServiceProvider()); + return new RouteConstraintBuilder(inlineConstraintResolver, template); } } diff --git a/src/Http/Routing/test/UnitTests/RouteEndpointBuilderTest.cs b/src/Http/Routing/test/UnitTests/RouteEndpointBuilderTest.cs index df43e7d4b1..131b72f0d2 100644 --- a/src/Http/Routing/test/UnitTests/RouteEndpointBuilderTest.cs +++ b/src/Http/Routing/test/UnitTests/RouteEndpointBuilderTest.cs @@ -8,29 +8,28 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Patterns; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class RouteEndpointBuilderTest { - public class RouteEndpointBuilderTest + [Fact] + public void Build_AllValuesSet_EndpointCreated() { - [Fact] - public void Build_AllValuesSet_EndpointCreated() - { - const int defaultOrder = 0; - var metadata = new object(); - RequestDelegate requestDelegate = (d) => null; + const int defaultOrder = 0; + var metadata = new object(); + RequestDelegate requestDelegate = (d) => null; - var builder = new RouteEndpointBuilder(requestDelegate, RoutePatternFactory.Parse("/"), defaultOrder) - { - DisplayName = "Display name!", - Metadata = { metadata } - }; + var builder = new RouteEndpointBuilder(requestDelegate, RoutePatternFactory.Parse("/"), defaultOrder) + { + DisplayName = "Display name!", + Metadata = { metadata } + }; - var endpoint = Assert.IsType(builder.Build()); - Assert.Equal("Display name!", endpoint.DisplayName); - Assert.Equal(defaultOrder, endpoint.Order); - Assert.Equal(requestDelegate, endpoint.RequestDelegate); - Assert.Equal("/", endpoint.RoutePattern.RawText); - Assert.Equal(metadata, Assert.Single(endpoint.Metadata)); - } + var endpoint = Assert.IsType(builder.Build()); + Assert.Equal("Display name!", endpoint.DisplayName); + Assert.Equal(defaultOrder, endpoint.Order); + Assert.Equal(requestDelegate, endpoint.RequestDelegate); + Assert.Equal("/", endpoint.RoutePattern.RawText); + Assert.Equal(metadata, Assert.Single(endpoint.Metadata)); } } diff --git a/src/Http/Routing/test/UnitTests/RouteHandlerOptionsTests.cs b/src/Http/Routing/test/UnitTests/RouteHandlerOptionsTests.cs index 3bbad84808..63d72cb6de 100644 --- a/src/Http/Routing/test/UnitTests/RouteHandlerOptionsTests.cs +++ b/src/Http/Routing/test/UnitTests/RouteHandlerOptionsTests.cs @@ -11,71 +11,70 @@ using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class RouteHandlerOptionsTests { - public class RouteHandlerOptionsTests + [Theory] + [InlineData("Development", true)] + [InlineData("DEVELOPMENT", true)] + [InlineData("Production", false)] + [InlineData("Custom", false)] + public void ThrowOnBadRequestIsTrueIfInDevelopmentEnvironmentFalseOtherwise(string environmentName, bool expectedThrowOnBadRequest) { - [Theory] - [InlineData("Development", true)] - [InlineData("DEVELOPMENT", true)] - [InlineData("Production", false)] - [InlineData("Custom", false)] - public void ThrowOnBadRequestIsTrueIfInDevelopmentEnvironmentFalseOtherwise(string environmentName, bool expectedThrowOnBadRequest) + var services = new ServiceCollection(); + services.AddOptions(); + services.AddRouting(); + services.AddSingleton(new HostEnvironment { - var services = new ServiceCollection(); - services.AddOptions(); - services.AddRouting(); - services.AddSingleton(new HostEnvironment - { - EnvironmentName = environmentName, - }); - var serviceProvider = services.BuildServiceProvider(); + EnvironmentName = environmentName, + }); + var serviceProvider = services.BuildServiceProvider(); - var options = serviceProvider.GetRequiredService>().Value; - Assert.Equal(expectedThrowOnBadRequest, options.ThrowOnBadRequest); - } + var options = serviceProvider.GetRequiredService>().Value; + Assert.Equal(expectedThrowOnBadRequest, options.ThrowOnBadRequest); + } - [Fact] - public void ThrowOnBadRequestIsNotOverwrittenIfNotInDevelopmentEnvironment() - { - var services = new ServiceCollection(); + [Fact] + public void ThrowOnBadRequestIsNotOverwrittenIfNotInDevelopmentEnvironment() + { + var services = new ServiceCollection(); - services.Configure(options => - { - options.ThrowOnBadRequest = true; - }); + services.Configure(options => + { + options.ThrowOnBadRequest = true; + }); - services.AddSingleton(new HostEnvironment - { - EnvironmentName = "Production", - }); + services.AddSingleton(new HostEnvironment + { + EnvironmentName = "Production", + }); - services.AddOptions(); - services.AddRouting(); + services.AddOptions(); + services.AddRouting(); - var serviceProvider = services.BuildServiceProvider(); + var serviceProvider = services.BuildServiceProvider(); - var options = serviceProvider.GetRequiredService>().Value; - Assert.True(options.ThrowOnBadRequest); - } + var options = serviceProvider.GetRequiredService>().Value; + Assert.True(options.ThrowOnBadRequest); + } - [Fact] - public void RouteHandlerOptionsFailsToResolveWithoutHostEnvironment() - { - var services = new ServiceCollection(); - services.AddOptions(); - services.AddRouting(); - var serviceProvider = services.BuildServiceProvider(); + [Fact] + public void RouteHandlerOptionsFailsToResolveWithoutHostEnvironment() + { + var services = new ServiceCollection(); + services.AddOptions(); + services.AddRouting(); + var serviceProvider = services.BuildServiceProvider(); - Assert.Throws(() => serviceProvider.GetRequiredService>()); - } + Assert.Throws(() => serviceProvider.GetRequiredService>()); + } - private class HostEnvironment : IHostEnvironment - { - public string ApplicationName { get; set; } - public IFileProvider ContentRootFileProvider { get; set; } - public string ContentRootPath { get; set; } - public string EnvironmentName { get; set; } - } + private class HostEnvironment : IHostEnvironment + { + public string ApplicationName { get; set; } + public IFileProvider ContentRootFileProvider { get; set; } + public string ContentRootPath { get; set; } + public string EnvironmentName { get; set; } } } diff --git a/src/Http/Routing/test/UnitTests/RouteOptionsTests.cs b/src/Http/Routing/test/UnitTests/RouteOptionsTests.cs index 0169de5da2..da342ba11c 100644 --- a/src/Http/Routing/test/UnitTests/RouteOptionsTests.cs +++ b/src/Http/Routing/test/UnitTests/RouteOptionsTests.cs @@ -8,74 +8,73 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class RouteOptionsTests { - public class RouteOptionsTests + [Fact] + public void ConfigureRouting_ConfiguresOptionsProperly() { - [Fact] - public void ConfigureRouting_ConfiguresOptionsProperly() - { - // Arrange - var services = new ServiceCollection(); - services.AddOptions(); + // Arrange + var services = new ServiceCollection(); + services.AddOptions(); - // Act - services.AddRouting(options => options.ConstraintMap.Add("foo", typeof(TestRouteConstraint))); - var serviceProvider = services.BuildServiceProvider(); + // Act + services.AddRouting(options => options.ConstraintMap.Add("foo", typeof(TestRouteConstraint))); + var serviceProvider = services.BuildServiceProvider(); - // Assert - var accessor = serviceProvider.GetRequiredService>(); - Assert.Equal("TestRouteConstraint", accessor.Value.ConstraintMap["foo"].Name); - } + // Assert + var accessor = serviceProvider.GetRequiredService>(); + Assert.Equal("TestRouteConstraint", accessor.Value.ConstraintMap["foo"].Name); + } - [Fact] - public void EndpointDataSources_WithDependencyInjection_AddedDataSourcesAddedToEndpointDataSource() - { - // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - services.AddRouting(); - var serviceProvider = services.BuildServiceProvider(); + [Fact] + public void EndpointDataSources_WithDependencyInjection_AddedDataSourcesAddedToEndpointDataSource() + { + // Arrange + var services = new ServiceCollection(); + services.AddOptions(); + services.AddRouting(); + var serviceProvider = services.BuildServiceProvider(); - var endpoint1 = new Endpoint((c) => Task.CompletedTask, EndpointMetadataCollection.Empty, string.Empty); - var endpoint2 = new Endpoint((c) => Task.CompletedTask, EndpointMetadataCollection.Empty, string.Empty); + var endpoint1 = new Endpoint((c) => Task.CompletedTask, EndpointMetadataCollection.Empty, string.Empty); + var endpoint2 = new Endpoint((c) => Task.CompletedTask, EndpointMetadataCollection.Empty, string.Empty); - var options = serviceProvider.GetRequiredService>().Value; - var endpointDataSource = serviceProvider.GetRequiredService(); + var options = serviceProvider.GetRequiredService>().Value; + var endpointDataSource = serviceProvider.GetRequiredService(); - // Act 1 - options.EndpointDataSources.Add(new DefaultEndpointDataSource(endpoint1)); + // Act 1 + options.EndpointDataSources.Add(new DefaultEndpointDataSource(endpoint1)); - // Assert 1 - var result = Assert.Single(endpointDataSource.Endpoints); - Assert.Same(endpoint1, result); + // Assert 1 + var result = Assert.Single(endpointDataSource.Endpoints); + Assert.Same(endpoint1, result); - // Act 2 - options.EndpointDataSources.Add(new DefaultEndpointDataSource(endpoint2)); + // Act 2 + options.EndpointDataSources.Add(new DefaultEndpointDataSource(endpoint2)); - // Assert 2 - Assert.Collection(endpointDataSource.Endpoints, - ep => Assert.Same(endpoint1, ep), - ep => Assert.Same(endpoint2, ep)); - } + // Assert 2 + Assert.Collection(endpointDataSource.Endpoints, + ep => Assert.Same(endpoint1, ep), + ep => Assert.Same(endpoint2, ep)); + } - private class TestRouteConstraint : IRouteConstraint + private class TestRouteConstraint : IRouteConstraint + { + public TestRouteConstraint(string pattern) { - public TestRouteConstraint(string pattern) - { - Pattern = pattern; - } + Pattern = pattern; + } - public string Pattern { get; private set; } - public bool Match( - HttpContext httpContext, - IRouter route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) - { - throw new NotImplementedException(); - } + public string Pattern { get; private set; } + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + throw new NotImplementedException(); } } } diff --git a/src/Http/Routing/test/UnitTests/RouteTest.cs b/src/Http/Routing/test/UnitTests/RouteTest.cs index 97af84ed73..26030f262a 100644 --- a/src/Http/Routing/test/UnitTests/RouteTest.cs +++ b/src/Http/Routing/test/UnitTests/RouteTest.cs @@ -18,1858 +18,1857 @@ using Microsoft.Extensions.WebEncoders.Testing; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing -{ - public class RouteTest - { - private static readonly RequestDelegate NullHandler = (c) => Task.CompletedTask; - private static readonly IInlineConstraintResolver _inlineConstraintResolver = GetInlineConstraintResolver(); - - [Fact] - public void CreateTemplate_InlineConstraint_Regex_Malformed() - { - // Arrange - var template = @"{controller}/{action}/ {p1:regex(abc} "; - var mockTarget = new Mock(MockBehavior.Strict); - - var exception = Assert.Throws( - () => new Route( - mockTarget.Object, - template, - defaults: null, - constraints: null, - dataTokens: null, - inlineConstraintResolver: _inlineConstraintResolver)); - - var expected = "An error occurred while creating the route with name '' and template" + - $" '{template}'."; - Assert.Equal(expected, exception.Message); - - Assert.NotNull(exception.InnerException); - expected = "The constraint entry 'p1' - 'regex(abc' on the route " + - "'{controller}/{action}/ {p1:regex(abc} ' could not be resolved by the constraint resolver of type " + - $"'{nameof(DefaultInlineConstraintResolver)}'."; - Assert.Equal(expected, exception.InnerException.Message); - } - - [Fact] - public async Task RouteAsync_MergesExistingRouteData_IfRouteMatches() - { - // Arrange - var template = "{controller}/{action}/{id:int}"; - - var context = CreateRouteContext("/Home/Index/5"); - - var originalRouteDataValues = context.RouteData.Values; - originalRouteDataValues.Add("country", "USA"); - - var originalDataTokens = context.RouteData.DataTokens; - originalDataTokens.Add("company", "Contoso"); - - IDictionary routeValues = null; - var mockTarget = new Mock(MockBehavior.Strict); - mockTarget - .Setup(s => s.RouteAsync(It.IsAny())) - .Callback(ctx => - { - routeValues = ctx.RouteData.Values; - ctx.Handler = NullHandler; - }) - .Returns(Task.FromResult(true)); - - var route = new Route( - mockTarget.Object, - template, - defaults: null, - constraints: null, - dataTokens: new RouteValueDictionary(new { today = "Friday" }), - inlineConstraintResolver: _inlineConstraintResolver); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.NotNull(routeValues); - - Assert.True(routeValues.ContainsKey("country")); - Assert.Equal("USA", routeValues["country"]); - Assert.True(routeValues.ContainsKey("id")); - Assert.Equal("5", routeValues["id"]); - - Assert.True(context.RouteData.Values.ContainsKey("country")); - Assert.Equal("USA", context.RouteData.Values["country"]); - Assert.True(context.RouteData.Values.ContainsKey("id")); - Assert.Equal("5", context.RouteData.Values["id"]); - Assert.Same(originalRouteDataValues, context.RouteData.Values); - - Assert.Equal("Contoso", context.RouteData.DataTokens["company"]); - Assert.Equal("Friday", context.RouteData.DataTokens["today"]); - Assert.Same(originalDataTokens, context.RouteData.DataTokens); - } - - [Fact] - public async Task RouteAsync_MergesExistingRouteData_PassedToConstraint() - { - // Arrange - var template = "{controller}/{action}/{id:int}"; - - var context = CreateRouteContext("/Home/Index/5"); - var originalRouteDataValues = context.RouteData.Values; - originalRouteDataValues.Add("country", "USA"); - - var originalDataTokens = context.RouteData.DataTokens; - originalDataTokens.Add("company", "Contoso"); - - IDictionary routeValues = null; - var mockTarget = new Mock(MockBehavior.Strict); - mockTarget - .Setup(s => s.RouteAsync(It.IsAny())) - .Callback(ctx => - { - routeValues = ctx.RouteData.Values; - ctx.Handler = NullHandler; - }) - .Returns(Task.FromResult(true)); - - var constraint = new CapturingConstraint(); - - var route = new Route( - mockTarget.Object, - template, - defaults: null, - constraints: new RouteValueDictionary(new { action = constraint }), - dataTokens: new RouteValueDictionary(new { today = "Friday" }), - inlineConstraintResolver: _inlineConstraintResolver); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.NotNull(routeValues); - - Assert.True(routeValues.ContainsKey("country")); - Assert.Equal("USA", routeValues["country"]); - Assert.True(routeValues.ContainsKey("id")); - Assert.Equal("5", routeValues["id"]); - - Assert.True(constraint.Values.ContainsKey("country")); - Assert.Equal("USA", constraint.Values["country"]); - Assert.True(constraint.Values.ContainsKey("id")); - Assert.Equal("5", constraint.Values["id"]); - - Assert.True(context.RouteData.Values.ContainsKey("country")); - Assert.Equal("USA", context.RouteData.Values["country"]); - Assert.True(context.RouteData.Values.ContainsKey("id")); - Assert.Equal("5", context.RouteData.Values["id"]); - - Assert.Equal("Contoso", context.RouteData.DataTokens["company"]); - Assert.Equal("Friday", context.RouteData.DataTokens["today"]); - } - - [Fact] - public async Task RouteAsync_InlineConstraint_OptionalParameter() - { - // Arrange - var template = "{controller}/{action}/{id:int?}"; - - var context = CreateRouteContext("/Home/Index/5"); - - IDictionary routeValues = null; - var mockTarget = new Mock(MockBehavior.Strict); - mockTarget - .Setup(s => s.RouteAsync(It.IsAny())) - .Callback(ctx => - { - routeValues = ctx.RouteData.Values; - ctx.Handler = NullHandler; - }) - .Returns(Task.FromResult(true)); - - var route = new Route( - mockTarget.Object, - template, - defaults: null, - constraints: null, - dataTokens: null, - inlineConstraintResolver: _inlineConstraintResolver); - - Assert.NotEmpty(route.Constraints); - Assert.IsType(route.Constraints["id"]); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.NotNull(context.Handler); - Assert.True(routeValues.ContainsKey("id")); - Assert.Equal("5", routeValues["id"]); - - Assert.True(context.RouteData.Values.ContainsKey("id")); - Assert.Equal("5", context.RouteData.Values["id"]); - } - - [Fact] - public async Task RouteAsync_InlineConstraint_Regex() - { - // Arrange - var template = @"{controller}/{action}/{ssn:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}"; - - var context = CreateRouteContext("/Home/Index/123-456-7890"); - - IDictionary routeValues = null; - var mockTarget = new Mock(MockBehavior.Strict); - mockTarget - .Setup(s => s.RouteAsync(It.IsAny())) - .Callback(ctx => - { - routeValues = ctx.RouteData.Values; - ctx.Handler = NullHandler; - }) - .Returns(Task.FromResult(true)); - - var route = new Route( - mockTarget.Object, - template, - defaults: null, - constraints: null, - dataTokens: null, - inlineConstraintResolver: _inlineConstraintResolver); - - Assert.NotEmpty(route.Constraints); - Assert.IsType(route.Constraints["ssn"]); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.NotNull(context.Handler); - Assert.True(routeValues.ContainsKey("ssn")); - Assert.Equal("123-456-7890", routeValues["ssn"]); - - Assert.True(context.RouteData.Values.ContainsKey("ssn")); - Assert.Equal("123-456-7890", context.RouteData.Values["ssn"]); - } - - [Fact] - public async Task RouteAsync_InlineConstraint_OptionalParameter_NotPresent() - { - // Arrange - var template = "{controller}/{action}/{id:int?}"; - - var context = CreateRouteContext("/Home/Index"); - - IDictionary routeValues = null; - var mockTarget = new Mock(MockBehavior.Strict); - mockTarget - .Setup(s => s.RouteAsync(It.IsAny())) - .Callback(ctx => - { - routeValues = ctx.RouteData.Values; - ctx.Handler = NullHandler; - }) - .Returns(Task.FromResult(true)); - - var route = new Route( - mockTarget.Object, - template, - defaults: null, - constraints: null, - dataTokens: null, - inlineConstraintResolver: _inlineConstraintResolver); - - Assert.NotEmpty(route.Constraints); - Assert.IsType(route.Constraints["id"]); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.NotNull(context.Handler); - Assert.NotNull(routeValues); - Assert.False(routeValues.ContainsKey("id")); - Assert.False(context.RouteData.Values.ContainsKey("id")); - } - - [Fact] - public async Task RouteAsync_InlineConstraint_OptionalParameter_WithInConstructorConstraint() - { - // Arrange - var template = "{controller}/{action}/{id:int?}"; - - var context = CreateRouteContext("/Home/Index/5"); - - IDictionary routeValues = null; - var mockTarget = new Mock(MockBehavior.Strict); - mockTarget - .Setup(s => s.RouteAsync(It.IsAny())) - .Callback(ctx => - { - routeValues = ctx.RouteData.Values; - ctx.Handler = NullHandler; - }) - .Returns(Task.FromResult(true)); - - var constraints = new Dictionary(); - constraints.Add("id", new RangeRouteConstraint(1, 20)); - - var route = new Route( - mockTarget.Object, - template, - defaults: null, - constraints: constraints, - dataTokens: null, - inlineConstraintResolver: _inlineConstraintResolver); +namespace Microsoft.AspNetCore.Routing; - Assert.NotEmpty(route.Constraints); - Assert.IsType(route.Constraints["id"]); - var innerConstraint = ((OptionalRouteConstraint)route.Constraints["id"]).InnerConstraint; - Assert.IsType(innerConstraint); - var compositeConstraint = (CompositeRouteConstraint)innerConstraint; - Assert.Equal(2, compositeConstraint.Constraints.Count()); - - Assert.Single(compositeConstraint.Constraints, c => c is IntRouteConstraint); - Assert.Single(compositeConstraint.Constraints, c => c is RangeRouteConstraint); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.NotNull(context.Handler); - Assert.True(routeValues.ContainsKey("id")); - Assert.Equal("5", routeValues["id"]); +public class RouteTest +{ + private static readonly RequestDelegate NullHandler = (c) => Task.CompletedTask; + private static readonly IInlineConstraintResolver _inlineConstraintResolver = GetInlineConstraintResolver(); - Assert.True(context.RouteData.Values.ContainsKey("id")); - Assert.Equal("5", context.RouteData.Values["id"]); - } + [Fact] + public void CreateTemplate_InlineConstraint_Regex_Malformed() + { + // Arrange + var template = @"{controller}/{action}/ {p1:regex(abc} "; + var mockTarget = new Mock(MockBehavior.Strict); - [Fact] - public async Task RouteAsync_InlineConstraint_OptionalParameter_ConstraintFails() - { - // Arrange - var template = "{controller}/{action}/{id:range(1,20)?}"; - - var context = CreateRouteContext("/Home/Index/100"); - - IDictionary routeValues = null; - var mockTarget = new Mock(MockBehavior.Strict); - mockTarget - .Setup(s => s.RouteAsync(It.IsAny())) - .Callback(ctx => - { - routeValues = ctx.RouteData.Values; - ctx.Handler = NullHandler; - }) - .Returns(Task.FromResult(true)); - - var route = new Route( + var exception = Assert.Throws( + () => new Route( mockTarget.Object, template, defaults: null, constraints: null, dataTokens: null, - inlineConstraintResolver: _inlineConstraintResolver); - - Assert.NotEmpty(route.Constraints); - Assert.IsType(route.Constraints["id"]); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.Null(context.Handler); - } - - // PathString in HttpAbstractions guarantees a leading slash - so no value in testing other cases. - [Fact] - public async Task Match_Success_LeadingSlash() - { - // Arrange - var route = CreateRoute("{controller}/{action}"); - var context = CreateRouteContext("/Home/Index"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.NotNull(context.Handler); - Assert.Equal(2, context.RouteData.Values.Count); - Assert.Equal("Home", context.RouteData.Values["controller"]); - Assert.Equal("Index", context.RouteData.Values["action"]); - } - - [Fact] - public async Task Match_Success_RootUrl() - { - // Arrange - var route = CreateRoute(""); - var context = CreateRouteContext("/"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.NotNull(context.Handler); - Assert.Empty(context.RouteData.Values); - } - - [Fact] - public async Task Match_Success_Defaults() - { - // Arrange - var route = CreateRoute("{controller}/{action}", new { action = "Index" }); - var context = CreateRouteContext("/Home"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.NotNull(context.Handler); - Assert.Equal(2, context.RouteData.Values.Count); - Assert.Equal("Home", context.RouteData.Values["controller"]); - Assert.Equal("Index", context.RouteData.Values["action"]); - } - - [Fact] - public async Task Match_Success_CopiesDataTokens() - { - // Arrange - var route = CreateRoute( - "{controller}/{action}", - defaults: new { action = "Index" }, - dataTokens: new { culture = "en-CA" }); - - var context = CreateRouteContext("/Home"); - - // Act - await route.RouteAsync(context); - Assert.NotNull(context.Handler); - - // This should not affect the route - RouteData.DataTokens is a copy - context.RouteData.DataTokens.Add("company", "contoso"); - - // Assert - Assert.Single(route.DataTokens); - Assert.Single(route.DataTokens, kvp => kvp.Key == "culture" && ((string)kvp.Value) == "en-CA"); - } + inlineConstraintResolver: _inlineConstraintResolver)); - [Fact] - public async Task Match_Fails() - { - // Arrange - var route = CreateRoute("{controller}/{action}"); - var context = CreateRouteContext("/Home"); - - // Act - await route.RouteAsync(context); + var expected = "An error occurred while creating the route with name '' and template" + + $" '{template}'."; + Assert.Equal(expected, exception.Message); - // Assert - Assert.Null(context.Handler); - } + Assert.NotNull(exception.InnerException); + expected = "The constraint entry 'p1' - 'regex(abc' on the route " + + "'{controller}/{action}/ {p1:regex(abc} ' could not be resolved by the constraint resolver of type " + + $"'{nameof(DefaultInlineConstraintResolver)}'."; + Assert.Equal(expected, exception.InnerException.Message); + } - [Fact] - public async Task Match_RejectedByHandler() - { - // Arrange - var route = CreateRoute("{controller}", handleRequest: false); - var context = CreateRouteContext("/Home"); + [Fact] + public async Task RouteAsync_MergesExistingRouteData_IfRouteMatches() + { + // Arrange + var template = "{controller}/{action}/{id:int}"; - // Act - await route.RouteAsync(context); + var context = CreateRouteContext("/Home/Index/5"); - // Assert - Assert.Null(context.Handler); + var originalRouteDataValues = context.RouteData.Values; + originalRouteDataValues.Add("country", "USA"); - var value = Assert.Single(context.RouteData.Values); - Assert.Equal("controller", value.Key); - Assert.Equal("Home", Assert.IsType(value.Value)); - } + var originalDataTokens = context.RouteData.DataTokens; + originalDataTokens.Add("company", "Contoso"); - [Fact] - public async Task Match_SetsRouters() - { - // Arrange - var target = CreateTarget(handleRequest: true); - var route = CreateRoute(target, "{controller}"); - var context = CreateRouteContext("/Home"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.NotNull(context.Handler); - Assert.Equal(1, context.RouteData.Routers.Count); - Assert.Same(target, context.RouteData.Routers[0]); - } + IDictionary routeValues = null; + var mockTarget = new Mock(MockBehavior.Strict); + mockTarget + .Setup(s => s.RouteAsync(It.IsAny())) + .Callback(ctx => + { + routeValues = ctx.RouteData.Values; + ctx.Handler = NullHandler; + }) + .Returns(Task.FromResult(true)); + + var route = new Route( + mockTarget.Object, + template, + defaults: null, + constraints: null, + dataTokens: new RouteValueDictionary(new { today = "Friday" }), + inlineConstraintResolver: _inlineConstraintResolver); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(routeValues); + + Assert.True(routeValues.ContainsKey("country")); + Assert.Equal("USA", routeValues["country"]); + Assert.True(routeValues.ContainsKey("id")); + Assert.Equal("5", routeValues["id"]); + + Assert.True(context.RouteData.Values.ContainsKey("country")); + Assert.Equal("USA", context.RouteData.Values["country"]); + Assert.True(context.RouteData.Values.ContainsKey("id")); + Assert.Equal("5", context.RouteData.Values["id"]); + Assert.Same(originalRouteDataValues, context.RouteData.Values); + + Assert.Equal("Contoso", context.RouteData.DataTokens["company"]); + Assert.Equal("Friday", context.RouteData.DataTokens["today"]); + Assert.Same(originalDataTokens, context.RouteData.DataTokens); + } - [Fact] - public async Task Match_RouteValuesDoesntThrowOnKeyNotFound() - { - // Arrange - var route = CreateRoute("{controller}/{action}"); - var context = CreateRouteContext("/Home/Index"); + [Fact] + public async Task RouteAsync_MergesExistingRouteData_PassedToConstraint() + { + // Arrange + var template = "{controller}/{action}/{id:int}"; - // Act - await route.RouteAsync(context); + var context = CreateRouteContext("/Home/Index/5"); + var originalRouteDataValues = context.RouteData.Values; + originalRouteDataValues.Add("country", "USA"); - // Assert - Assert.Null(context.RouteData.Values["1controller"]); - } + var originalDataTokens = context.RouteData.DataTokens; + originalDataTokens.Add("company", "Contoso"); - [Fact] - public async Task Match_Success_OptionalParameter_ValueProvided() - { - // Arrange - var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index" }); - var context = CreateRouteContext("/Home/Create.xml"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.NotNull(context.Handler); - Assert.Equal(3, context.RouteData.Values.Count); - Assert.Equal("Home", context.RouteData.Values["controller"]); - Assert.Equal("Create", context.RouteData.Values["action"]); - Assert.Equal("xml", context.RouteData.Values["format"]); - } + IDictionary routeValues = null; + var mockTarget = new Mock(MockBehavior.Strict); + mockTarget + .Setup(s => s.RouteAsync(It.IsAny())) + .Callback(ctx => + { + routeValues = ctx.RouteData.Values; + ctx.Handler = NullHandler; + }) + .Returns(Task.FromResult(true)); + + var constraint = new CapturingConstraint(); + + var route = new Route( + mockTarget.Object, + template, + defaults: null, + constraints: new RouteValueDictionary(new { action = constraint }), + dataTokens: new RouteValueDictionary(new { today = "Friday" }), + inlineConstraintResolver: _inlineConstraintResolver); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(routeValues); + + Assert.True(routeValues.ContainsKey("country")); + Assert.Equal("USA", routeValues["country"]); + Assert.True(routeValues.ContainsKey("id")); + Assert.Equal("5", routeValues["id"]); + + Assert.True(constraint.Values.ContainsKey("country")); + Assert.Equal("USA", constraint.Values["country"]); + Assert.True(constraint.Values.ContainsKey("id")); + Assert.Equal("5", constraint.Values["id"]); + + Assert.True(context.RouteData.Values.ContainsKey("country")); + Assert.Equal("USA", context.RouteData.Values["country"]); + Assert.True(context.RouteData.Values.ContainsKey("id")); + Assert.Equal("5", context.RouteData.Values["id"]); + + Assert.Equal("Contoso", context.RouteData.DataTokens["company"]); + Assert.Equal("Friday", context.RouteData.DataTokens["today"]); + } - [Fact] - public async Task Match_Success_OptionalParameter_ValueNotProvided() - { - // Arrange - var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index" }); - var context = CreateRouteContext("/Home/Create"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.NotNull(context.Handler); - Assert.Equal(2, context.RouteData.Values.Count); - Assert.Equal("Home", context.RouteData.Values["controller"]); - Assert.Equal("Create", context.RouteData.Values["action"]); - } + [Fact] + public async Task RouteAsync_InlineConstraint_OptionalParameter() + { + // Arrange + var template = "{controller}/{action}/{id:int?}"; - [Fact] - public async Task Match_Success_OptionalParameter_DefaultValue() - { - // Arrange - var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index", format = "xml" }); - var context = CreateRouteContext("/Home/Create"); - - // Act - await route.RouteAsync(context); - - // Assert - Assert.NotNull(context.Handler); - Assert.Equal(3, context.RouteData.Values.Count); - Assert.Equal("Home", context.RouteData.Values["controller"]); - Assert.Equal("Create", context.RouteData.Values["action"]); - Assert.Equal("xml", context.RouteData.Values["format"]); - } + var context = CreateRouteContext("/Home/Index/5"); - [Fact] - public async Task Match_Success_OptionalParameter_EndsWithDot() - { - // Arrange - var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index" }); - var context = CreateRouteContext("/Home/Create."); + IDictionary routeValues = null; + var mockTarget = new Mock(MockBehavior.Strict); + mockTarget + .Setup(s => s.RouteAsync(It.IsAny())) + .Callback(ctx => + { + routeValues = ctx.RouteData.Values; + ctx.Handler = NullHandler; + }) + .Returns(Task.FromResult(true)); + + var route = new Route( + mockTarget.Object, + template, + defaults: null, + constraints: null, + dataTokens: null, + inlineConstraintResolver: _inlineConstraintResolver); + + Assert.NotEmpty(route.Constraints); + Assert.IsType(route.Constraints["id"]); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.True(routeValues.ContainsKey("id")); + Assert.Equal("5", routeValues["id"]); + + Assert.True(context.RouteData.Values.ContainsKey("id")); + Assert.Equal("5", context.RouteData.Values["id"]); + } - // Act - await route.RouteAsync(context); + [Fact] + public async Task RouteAsync_InlineConstraint_Regex() + { + // Arrange + var template = @"{controller}/{action}/{ssn:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}"; - // Assert - Assert.Null(context.Handler); - } + var context = CreateRouteContext("/Home/Index/123-456-7890"); - private static RouteContext CreateRouteContext(string requestPath, ILoggerFactory factory = null) - { - if (factory == null) + IDictionary routeValues = null; + var mockTarget = new Mock(MockBehavior.Strict); + mockTarget + .Setup(s => s.RouteAsync(It.IsAny())) + .Callback(ctx => { - factory = NullLoggerFactory.Instance; - } - - var request = new Mock(MockBehavior.Strict); - request.SetupGet(r => r.Path).Returns(requestPath); - - var context = new Mock(MockBehavior.Strict); - context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) - .Returns(factory); - context.SetupGet(c => c.Request).Returns(request.Object); + routeValues = ctx.RouteData.Values; + ctx.Handler = NullHandler; + }) + .Returns(Task.FromResult(true)); + + var route = new Route( + mockTarget.Object, + template, + defaults: null, + constraints: null, + dataTokens: null, + inlineConstraintResolver: _inlineConstraintResolver); + + Assert.NotEmpty(route.Constraints); + Assert.IsType(route.Constraints["ssn"]); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.True(routeValues.ContainsKey("ssn")); + Assert.Equal("123-456-7890", routeValues["ssn"]); + + Assert.True(context.RouteData.Values.ContainsKey("ssn")); + Assert.Equal("123-456-7890", context.RouteData.Values["ssn"]); + } - return new RouteContext(context.Object); - } + [Fact] + public async Task RouteAsync_InlineConstraint_OptionalParameter_NotPresent() + { + // Arrange + var template = "{controller}/{action}/{id:int?}"; - [Fact] - public void GetVirtualPath_Success() - { - // Arrange - var route = CreateRoute("{controller}"); - var context = CreateVirtualPathContext(new { controller = "Home" }); + var context = CreateRouteContext("/Home/Index"); - // Act - var pathData = route.GetVirtualPath(context); + IDictionary routeValues = null; + var mockTarget = new Mock(MockBehavior.Strict); + mockTarget + .Setup(s => s.RouteAsync(It.IsAny())) + .Callback(ctx => + { + routeValues = ctx.RouteData.Values; + ctx.Handler = NullHandler; + }) + .Returns(Task.FromResult(true)); + + var route = new Route( + mockTarget.Object, + template, + defaults: null, + constraints: null, + dataTokens: null, + inlineConstraintResolver: _inlineConstraintResolver); + + Assert.NotEmpty(route.Constraints); + Assert.IsType(route.Constraints["id"]); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.NotNull(routeValues); + Assert.False(routeValues.ContainsKey("id")); + Assert.False(context.RouteData.Values.ContainsKey("id")); + } - // Assert - Assert.Equal("/Home", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + [Fact] + public async Task RouteAsync_InlineConstraint_OptionalParameter_WithInConstructorConstraint() + { + // Arrange + var template = "{controller}/{action}/{id:int?}"; - [Fact] - public void GetVirtualPath_Fail() - { - // Arrange - var route = CreateRoute("{controller}/{action}"); - var context = CreateVirtualPathContext(new { controller = "Home" }); + var context = CreateRouteContext("/Home/Index/5"); - // Act - var path = route.GetVirtualPath(context); + IDictionary routeValues = null; + var mockTarget = new Mock(MockBehavior.Strict); + mockTarget + .Setup(s => s.RouteAsync(It.IsAny())) + .Callback(ctx => + { + routeValues = ctx.RouteData.Values; + ctx.Handler = NullHandler; + }) + .Returns(Task.FromResult(true)); + + var constraints = new Dictionary(); + constraints.Add("id", new RangeRouteConstraint(1, 20)); + + var route = new Route( + mockTarget.Object, + template, + defaults: null, + constraints: constraints, + dataTokens: null, + inlineConstraintResolver: _inlineConstraintResolver); + + Assert.NotEmpty(route.Constraints); + Assert.IsType(route.Constraints["id"]); + var innerConstraint = ((OptionalRouteConstraint)route.Constraints["id"]).InnerConstraint; + Assert.IsType(innerConstraint); + var compositeConstraint = (CompositeRouteConstraint)innerConstraint; + Assert.Equal(2, compositeConstraint.Constraints.Count()); + + Assert.Single(compositeConstraint.Constraints, c => c is IntRouteConstraint); + Assert.Single(compositeConstraint.Constraints, c => c is RangeRouteConstraint); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.True(routeValues.ContainsKey("id")); + Assert.Equal("5", routeValues["id"]); + + Assert.True(context.RouteData.Values.ContainsKey("id")); + Assert.Equal("5", context.RouteData.Values["id"]); + } - // Assert - Assert.Null(path); - } + [Fact] + public async Task RouteAsync_InlineConstraint_OptionalParameter_ConstraintFails() + { + // Arrange + var template = "{controller}/{action}/{id:range(1,20)?}"; - [Fact] - public void GetVirtualPath_EncodesValues() - { - // Arrange - var route = CreateRoute("{controller}/{action}"); - var context = CreateVirtualPathContext( - new { name = "name with %special #characters" }, - new { controller = "Home", action = "Index" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.Equal("/Home/Index?name=name%20with%20%25special%20%23characters", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + var context = CreateRouteContext("/Home/Index/100"); - [Fact] - public void GetVirtualPath_AlwaysUsesDefaultUrlEncoder() - { - // Arrange - var nameRouteValue = "name with %special #characters Jörn"; - var expected = "/Home/Index?name=" + UrlEncoder.Default.Encode(nameRouteValue); - var services = new ServiceCollection(); - services.AddSingleton(NullLoggerFactory.Instance); - services.AddOptions(); - services.AddRouting(); - // This test encoder should not be used by Routing and should always use the default one. - services.AddSingleton(new UrlTestEncoder()); - var httpContext = new DefaultHttpContext + IDictionary routeValues = null; + var mockTarget = new Mock(MockBehavior.Strict); + mockTarget + .Setup(s => s.RouteAsync(It.IsAny())) + .Callback(ctx => { - RequestServices = services.BuildServiceProvider(), - }; + routeValues = ctx.RouteData.Values; + ctx.Handler = NullHandler; + }) + .Returns(Task.FromResult(true)); + + var route = new Route( + mockTarget.Object, + template, + defaults: null, + constraints: null, + dataTokens: null, + inlineConstraintResolver: _inlineConstraintResolver); + + Assert.NotEmpty(route.Constraints); + Assert.IsType(route.Constraints["id"]); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.Null(context.Handler); + } - var context = new VirtualPathContext( - httpContext, - values: new RouteValueDictionary(new { name = nameRouteValue }), - ambientValues: new RouteValueDictionary(new { controller = "Home", action = "Index" })); + // PathString in HttpAbstractions guarantees a leading slash - so no value in testing other cases. + [Fact] + public async Task Match_Success_LeadingSlash() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateRouteContext("/Home/Index"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.Equal(2, context.RouteData.Values.Count); + Assert.Equal("Home", context.RouteData.Values["controller"]); + Assert.Equal("Index", context.RouteData.Values["action"]); + } - var route = CreateRoute("{controller}/{action}"); + [Fact] + public async Task Match_Success_RootUrl() + { + // Arrange + var route = CreateRoute(""); + var context = CreateRouteContext("/"); - // Act - var pathData = route.GetVirtualPath(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Equal(expected, pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Assert + Assert.NotNull(context.Handler); + Assert.Empty(context.RouteData.Values); + } - [Fact] - public void GetVirtualPath_ForListOfStrings() - { - // Arrange - var route = CreateRoute("{controller}/{action}"); - var context = CreateVirtualPathContext( - new { color = new List { "red", "green", "blue" } }, - new { controller = "Home", action = "Index" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.Equal("/Home/Index?color=red&color=green&color=blue", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + [Fact] + public async Task Match_Success_Defaults() + { + // Arrange + var route = CreateRoute("{controller}/{action}", new { action = "Index" }); + var context = CreateRouteContext("/Home"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.Equal(2, context.RouteData.Values.Count); + Assert.Equal("Home", context.RouteData.Values["controller"]); + Assert.Equal("Index", context.RouteData.Values["action"]); + } - [Fact] - public void GetVirtualPath_ForListOfInts() - { - // Arrange - var route = CreateRoute("{controller}/{action}"); - var context = CreateVirtualPathContext( - new { items = new List { 10, 20, 30 } }, - new { controller = "Home", action = "Index" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.Equal("/Home/Index?items=10&items=20&items=30", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + [Fact] + public async Task Match_Success_CopiesDataTokens() + { + // Arrange + var route = CreateRoute( + "{controller}/{action}", + defaults: new { action = "Index" }, + dataTokens: new { culture = "en-CA" }); - [Fact] - public void GetVirtualPath_ForList_Empty() - { - // Arrange - var route = CreateRoute("{controller}/{action}"); - var context = CreateVirtualPathContext( - new { color = new List { } }, - new { controller = "Home", action = "Index" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.Equal("/Home/Index", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + var context = CreateRouteContext("/Home"); - [Fact] - public void GetVirtualPath_ForList_StringWorkaround() - { - // Arrange - var route = CreateRoute("{controller}/{action}"); - var context = CreateVirtualPathContext( - new { page = 1, color = new List { "red", "green", "blue" }, message = "textfortest" }, - new { controller = "Home", action = "Index" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.Equal("/Home/Index?page=1&color=red&color=green&color=blue&message=textfortest", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + await route.RouteAsync(context); + Assert.NotNull(context.Handler); - [Theory] - [MemberData(nameof(DataTokensTestData))] - public void GetVirtualPath_ReturnsDataTokens_WhenTargetReturnsVirtualPathData( - RouteValueDictionary dataTokens) - { - // Arrange - var path = "/TestPath"; + // This should not affect the route - RouteData.DataTokens is a copy + context.RouteData.DataTokens.Add("company", "contoso"); - var target = new Mock(MockBehavior.Strict); - target - .Setup(r => r.GetVirtualPath(It.IsAny())) - .Returns(() => new VirtualPathData(target.Object, path, dataTokens)); + // Assert + Assert.Single(route.DataTokens); + Assert.Single(route.DataTokens, kvp => kvp.Key == "culture" && ((string)kvp.Value) == "en-CA"); + } - var routeDataTokens = - new RouteValueDictionary() { { "ThisShouldBeIgnored", "" } }; + [Fact] + public async Task Match_Fails() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateRouteContext("/Home"); - var route = CreateRoute( - target.Object, - "{controller}", - defaults: null, - dataTokens: routeDataTokens); - var context = CreateVirtualPathContext(new { controller = path }); + // Act + await route.RouteAsync(context); - var expectedDataTokens = dataTokens ?? new RouteValueDictionary(); + // Assert + Assert.Null(context.Handler); + } - // Act - var pathData = route.GetVirtualPath(context); + [Fact] + public async Task Match_RejectedByHandler() + { + // Arrange + var route = CreateRoute("{controller}", handleRequest: false); + var context = CreateRouteContext("/Home"); - // Assert - Assert.NotNull(pathData); - Assert.Same(target.Object, pathData.Router); - Assert.Equal(path, pathData.VirtualPath); - Assert.NotNull(pathData.DataTokens); + // Act + await route.RouteAsync(context); - Assert.DoesNotContain(routeDataTokens.First().Key, pathData.DataTokens.Keys); + // Assert + Assert.Null(context.Handler); - Assert.Equal(expectedDataTokens.Count, pathData.DataTokens.Count); - foreach (var dataToken in expectedDataTokens) - { - Assert.True(pathData.DataTokens.ContainsKey(dataToken.Key)); - Assert.Equal(dataToken.Value, pathData.DataTokens[dataToken.Key]); - } - } + var value = Assert.Single(context.RouteData.Values); + Assert.Equal("controller", value.Key); + Assert.Equal("Home", Assert.IsType(value.Value)); + } - [Theory] - [MemberData(nameof(DataTokensTestData))] - public void GetVirtualPath_ReturnsDataTokens_WhenTargetReturnsNullVirtualPathData( - RouteValueDictionary dataTokens) - { - // Arrange - var path = "/TestPath"; + [Fact] + public async Task Match_SetsRouters() + { + // Arrange + var target = CreateTarget(handleRequest: true); + var route = CreateRoute(target, "{controller}"); + var context = CreateRouteContext("/Home"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.Equal(1, context.RouteData.Routers.Count); + Assert.Same(target, context.RouteData.Routers[0]); + } - var target = new Mock(MockBehavior.Strict); - target - .Setup(r => r.GetVirtualPath(It.IsAny())) - .Returns(() => null); + [Fact] + public async Task Match_RouteValuesDoesntThrowOnKeyNotFound() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateRouteContext("/Home/Index"); - var route = CreateRoute( - target.Object, - "{controller}", - defaults: null, - dataTokens: dataTokens); - var context = CreateVirtualPathContext(new { controller = path }); + // Act + await route.RouteAsync(context); - var expectedDataTokens = dataTokens ?? new RouteValueDictionary(); + // Assert + Assert.Null(context.RouteData.Values["1controller"]); + } - // Act - var pathData = route.GetVirtualPath(context); + [Fact] + public async Task Match_Success_OptionalParameter_ValueProvided() + { + // Arrange + var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index" }); + var context = CreateRouteContext("/Home/Create.xml"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.Equal(3, context.RouteData.Values.Count); + Assert.Equal("Home", context.RouteData.Values["controller"]); + Assert.Equal("Create", context.RouteData.Values["action"]); + Assert.Equal("xml", context.RouteData.Values["format"]); + } - // Assert - Assert.NotNull(pathData); - Assert.Same(route, pathData.Router); - Assert.Equal(path, pathData.VirtualPath); - Assert.NotNull(pathData.DataTokens); + [Fact] + public async Task Match_Success_OptionalParameter_ValueNotProvided() + { + // Arrange + var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index" }); + var context = CreateRouteContext("/Home/Create"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.Equal(2, context.RouteData.Values.Count); + Assert.Equal("Home", context.RouteData.Values["controller"]); + Assert.Equal("Create", context.RouteData.Values["action"]); + } - Assert.Equal(expectedDataTokens.Count, pathData.DataTokens.Count); - foreach (var dataToken in expectedDataTokens) - { - Assert.True(pathData.DataTokens.ContainsKey(dataToken.Key)); - Assert.Equal(dataToken.Value, pathData.DataTokens[dataToken.Key]); - } - } + [Fact] + public async Task Match_Success_OptionalParameter_DefaultValue() + { + // Arrange + var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index", format = "xml" }); + var context = CreateRouteContext("/Home/Create"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(context.Handler); + Assert.Equal(3, context.RouteData.Values.Count); + Assert.Equal("Home", context.RouteData.Values["controller"]); + Assert.Equal("Create", context.RouteData.Values["action"]); + Assert.Equal("xml", context.RouteData.Values["format"]); + } - [Fact] - public void GetVirtualPath_ValuesRejectedByHandler_StillGeneratesPath() - { - // Arrange - var route = CreateRoute("{controller}", handleRequest: false); - var context = CreateVirtualPathContext(new { controller = "Home" }); + [Fact] + public async Task Match_Success_OptionalParameter_EndsWithDot() + { + // Arrange + var route = CreateRoute("{controller}/{action}.{format?}", new { action = "Index" }); + var context = CreateRouteContext("/Home/Create."); - // Act - var pathData = route.GetVirtualPath(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Equal("/Home", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Assert + Assert.Null(context.Handler); + } - [Fact] - public void GetVirtualPath_Success_AmbientValues() + private static RouteContext CreateRouteContext(string requestPath, ILoggerFactory factory = null) + { + if (factory == null) { - // Arrange - var route = CreateRoute("{controller}/{action}"); - var context = CreateVirtualPathContext(new { action = "Index" }, new { controller = "Home" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.Equal("/Home/Index", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); + factory = NullLoggerFactory.Instance; } - [Fact] - public void RouteGenerationRejectsConstraints() - { - // Arrange - var context = CreateVirtualPathContext(new { p1 = "abcd" }); + var request = new Mock(MockBehavior.Strict); + request.SetupGet(r => r.Path).Returns(requestPath); - var route = CreateRoute( - "{p1}/{p2}", - new { p2 = "catchall" }, - true, - new RouteValueDictionary(new { p2 = "\\d{4}" })); + var context = new Mock(MockBehavior.Strict); + context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) + .Returns(factory); + context.SetupGet(c => c.Request).Returns(request.Object); - // Act - var virtualPath = route.GetVirtualPath(context); + return new RouteContext(context.Object); + } - // Assert - Assert.Null(virtualPath); - } + [Fact] + public void GetVirtualPath_Success() + { + // Arrange + var route = CreateRoute("{controller}"); + var context = CreateVirtualPathContext(new { controller = "Home" }); - [Fact] - public void RouteGenerationAcceptsConstraints() - { - // Arrange - var context = CreateVirtualPathContext(new { p1 = "hello", p2 = "1234" }); - - var route = CreateRoute( - "{p1}/{p2}", - new { p2 = "catchall" }, - true, - new RouteValueDictionary(new { p2 = "\\d{4}" })); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal("/hello/1234", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + var pathData = route.GetVirtualPath(context); - [Fact] - public void RouteWithCatchAllRejectsConstraints() - { - // Arrange - var context = CreateVirtualPathContext(new { p1 = "abcd" }); + // Assert + Assert.Equal("/Home", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var route = CreateRoute( - "{p1}/{*p2}", - new { p2 = "catchall" }, - true, - new RouteValueDictionary(new { p2 = "\\d{4}" })); + [Fact] + public void GetVirtualPath_Fail() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateVirtualPathContext(new { controller = "Home" }); - // Act - var virtualPath = route.GetVirtualPath(context); + // Act + var path = route.GetVirtualPath(context); - // Assert - Assert.Null(virtualPath); - } + // Assert + Assert.Null(path); + } - [Fact] - public void RouteWithCatchAllAcceptsConstraints() - { - // Arrange - var context = CreateVirtualPathContext(new { p1 = "hello", p2 = "1234" }); - - var route = CreateRoute( - "{p1}/{*p2}", - new { p2 = "catchall" }, - true, - new RouteValueDictionary(new { p2 = "\\d{4}" })); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal("/hello/1234", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + [Fact] + public void GetVirtualPath_EncodesValues() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateVirtualPathContext( + new { name = "name with %special #characters" }, + new { controller = "Home", action = "Index" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index?name=name%20with%20%25special%20%23characters", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - [Fact] - public void GetVirtualPathWithNonParameterConstraintReturnsUrlWithoutQueryString() - { - // Arrange - var context = CreateVirtualPathContext(new { p1 = "hello", p2 = "1234" }); - - var target = new Mock(); - target - .Setup( - e => e.Match( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(true) - .Verifiable(); - - var route = CreateRoute( - "{p1}/{p2}", - new { p2 = "catchall" }, - true, - new RouteValueDictionary(new { p2 = target.Object })); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal("/hello/1234", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - - target.VerifyAll(); - } + [Fact] + public void GetVirtualPath_AlwaysUsesDefaultUrlEncoder() + { + // Arrange + var nameRouteValue = "name with %special #characters Jörn"; + var expected = "/Home/Index?name=" + UrlEncoder.Default.Encode(nameRouteValue); + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddOptions(); + services.AddRouting(); + // This test encoder should not be used by Routing and should always use the default one. + services.AddSingleton(new UrlTestEncoder()); + var httpContext = new DefaultHttpContext + { + RequestServices = services.BuildServiceProvider(), + }; + + var context = new VirtualPathContext( + httpContext, + values: new RouteValueDictionary(new { name = nameRouteValue }), + ambientValues: new RouteValueDictionary(new { controller = "Home", action = "Index" })); + + var route = CreateRoute("{controller}/{action}"); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal(expected, pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Any ambient values from the current request should be visible to constraint, even - // if they have nothing to do with the route generating a link - [Fact] - public void GetVirtualPath_ConstraintsSeeAmbientValues() - { - // Arrange - var constraint = new CapturingConstraint(); - var route = CreateRoute( - template: "slug/{controller}/{action}", - defaults: null, - handleRequest: true, - constraints: new { c = constraint }); + [Fact] + public void GetVirtualPath_ForListOfStrings() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateVirtualPathContext( + new { color = new List { "red", "green", "blue" } }, + new { controller = "Home", action = "Index" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index?color=red&color=green&color=blue", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var context = CreateVirtualPathContext( - values: new { action = "Store" }, - ambientValues: new { Controller = "Home", action = "Blog", extra = "42" }); + [Fact] + public void GetVirtualPath_ForListOfInts() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateVirtualPathContext( + new { items = new List { 10, 20, 30 } }, + new { controller = "Home", action = "Index" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index?items=10&items=20&items=30", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var expectedValues = new RouteValueDictionary( - new { controller = "Home", action = "Store", extra = "42" }); + [Fact] + public void GetVirtualPath_ForList_Empty() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateVirtualPathContext( + new { color = new List { } }, + new { controller = "Home", action = "Index" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Act - var pathData = route.GetVirtualPath(context); + [Fact] + public void GetVirtualPath_ForList_StringWorkaround() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateVirtualPathContext( + new { page = 1, color = new List { "red", "green", "blue" }, message = "textfortest" }, + new { controller = "Home", action = "Index" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index?page=1&color=red&color=green&color=blue&message=textfortest", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Assert - Assert.Equal("/slug/Home/Store", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); + [Theory] + [MemberData(nameof(DataTokensTestData))] + public void GetVirtualPath_ReturnsDataTokens_WhenTargetReturnsVirtualPathData( + RouteValueDictionary dataTokens) + { + // Arrange + var path = "/TestPath"; - Assert.Equal(expectedValues, constraint.Values); - } + var target = new Mock(MockBehavior.Strict); + target + .Setup(r => r.GetVirtualPath(It.IsAny())) + .Returns(() => new VirtualPathData(target.Object, path, dataTokens)); - // Non-parameter default values from the routing generating a link are not in the 'values' - // collection when constraints are processed. - [Fact] - public void GetVirtualPath_ConstraintsDontSeeDefaults_WhenTheyArentParameters() - { - // Arrange - var constraint = new CapturingConstraint(); - var route = CreateRoute( - template: "slug/{controller}/{action}", - defaults: new { otherthing = "17" }, - handleRequest: true, - constraints: new { c = constraint }); - - var context = CreateVirtualPathContext( - values: new { action = "Store" }, - ambientValues: new { Controller = "Home", action = "Blog" }); - - var expectedValues = new RouteValueDictionary( - new { controller = "Home", action = "Store" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.Equal("/slug/Home/Store", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - - Assert.Equal(expectedValues, constraint.Values); - } + var routeDataTokens = + new RouteValueDictionary() { { "ThisShouldBeIgnored", "" } }; - // Default values are visible to the constraint when they are used to fill a parameter. - [Fact] - public void GetVirtualPath_ConstraintsSeesDefault_WhenThereItsAParameter() - { - // Arrange - var constraint = new CapturingConstraint(); - var route = CreateRoute( - template: "slug/{controller}/{action}", - defaults: new { action = "Index" }, - handleRequest: true, - constraints: new { c = constraint }); - - var context = CreateVirtualPathContext( - values: new { controller = "Shopping" }, - ambientValues: new { Controller = "Home", action = "Blog" }); - - var expectedValues = new RouteValueDictionary( - new { controller = "Shopping", action = "Index" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.Equal("/slug/Shopping", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - - Assert.Equal(expectedValues, constraint.Values); - } + var route = CreateRoute( + target.Object, + "{controller}", + defaults: null, + dataTokens: routeDataTokens); + var context = CreateVirtualPathContext(new { controller = path }); - // Default values from the routing generating a link are in the 'values' collection when - // constraints are processed - IFF they are specified as values or ambient values. - [Fact] - public void GetVirtualPath_ConstraintsSeeDefaults_IfTheyAreSpecifiedOrAmbient() - { - // Arrange - var constraint = new CapturingConstraint(); - var route = CreateRoute( - template: "slug/{controller}/{action}", - defaults: new { otherthing = "17", thirdthing = "13" }, - handleRequest: true, - constraints: new { c = constraint }); - - var context = CreateVirtualPathContext( - values: new { action = "Store", thirdthing = "13" }, - ambientValues: new { Controller = "Home", action = "Blog", otherthing = "17" }); - - var expectedValues = new RouteValueDictionary( - new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.Equal("/slug/Home/Store", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - - Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key)); - } + var expectedDataTokens = dataTokens ?? new RouteValueDictionary(); - [Fact] - public void GetVirtualPath_InlineConstraints_Success() - { - // Arrange - var route = CreateRoute("{controller}/{action}/{id:int}"); - var context = CreateVirtualPathContext( - values: new { action = "Index", controller = "Home", id = 4 }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.Equal("/Home/Index/4", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + var pathData = route.GetVirtualPath(context); - [Fact] - public void GetVirtualPath_InlineConstraints_NonMatchingvalue() - { - // Arrange - var route = CreateRoute("{controller}/{action}/{id:int}"); - var context = CreateVirtualPathContext( - values: new { action = "Index", controller = "Home", id = "asf" }); + // Assert + Assert.NotNull(pathData); + Assert.Same(target.Object, pathData.Router); + Assert.Equal(path, pathData.VirtualPath); + Assert.NotNull(pathData.DataTokens); - // Act - var path = route.GetVirtualPath(context); + Assert.DoesNotContain(routeDataTokens.First().Key, pathData.DataTokens.Keys); - // Assert - Assert.Null(path); - } - - [Fact] - public void GetVirtualPath_InlineConstraints_OptionalParameter_ValuePresent() - { - // Arrange - var route = CreateRoute("{controller}/{action}/{id:int?}"); - var context = CreateVirtualPathContext( - values: new { action = "Index", controller = "Home", id = 98 }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.Equal("/Home/Index/98", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } - - [Fact] - public void GetVirtualPath_InlineConstraints_OptionalParameter_ValueNotPresent() + Assert.Equal(expectedDataTokens.Count, pathData.DataTokens.Count); + foreach (var dataToken in expectedDataTokens) { - // Arrange - var route = CreateRoute("{controller}/{action}/{id:int?}"); - var context = CreateVirtualPathContext( - values: new { action = "Index", controller = "Home" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.Equal("/Home/Index", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); + Assert.True(pathData.DataTokens.ContainsKey(dataToken.Key)); + Assert.Equal(dataToken.Value, pathData.DataTokens[dataToken.Key]); } + } - [Fact] - public void GetVirtualPath_InlineConstraints_OptionalParameter_ValuePresent_ConstraintFails() - { - // Arrange - var route = CreateRoute("{controller}/{action}/{id:int?}"); - var context = CreateVirtualPathContext( - values: new { action = "Index", controller = "Home", id = "sdfd" }); - - // Act - var path = route.GetVirtualPath(context); - - // Assert - Assert.Null(path); - } + [Theory] + [MemberData(nameof(DataTokensTestData))] + public void GetVirtualPath_ReturnsDataTokens_WhenTargetReturnsNullVirtualPathData( + RouteValueDictionary dataTokens) + { + // Arrange + var path = "/TestPath"; - [Fact] - public void GetVirtualPath_InlineConstraints_CompositeInlineConstraint() - { - // Arrange - var route = CreateRoute("{controller}/{action}/{id:int:range(1,20)}"); - var context = CreateVirtualPathContext( - values: new { action = "Index", controller = "Home", id = 14 }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.Equal("/Home/Index/14", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + var target = new Mock(MockBehavior.Strict); + target + .Setup(r => r.GetVirtualPath(It.IsAny())) + .Returns(() => null); - [Fact] - public void GetVirtualPath_InlineConstraints_CompositeConstraint_FromConstructor() - { - // Arrange - var constraint = new MaxLengthRouteConstraint(20); - var route = CreateRoute( - template: "{controller}/{action}/{name:alpha}", - defaults: null, - handleRequest: true, - constraints: new { name = constraint }); + var route = CreateRoute( + target.Object, + "{controller}", + defaults: null, + dataTokens: dataTokens); + var context = CreateVirtualPathContext(new { controller = path }); - var context = CreateVirtualPathContext( - values: new { action = "Index", controller = "Home", name = "products" }); + var expectedDataTokens = dataTokens ?? new RouteValueDictionary(); - // Act - var pathData = route.GetVirtualPath(context); + // Act + var pathData = route.GetVirtualPath(context); - // Assert - Assert.Equal("/Home/Index/products", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Assert + Assert.NotNull(pathData); + Assert.Same(route, pathData.Router); + Assert.Equal(path, pathData.VirtualPath); + Assert.NotNull(pathData.DataTokens); - [Fact] - public void GetVirtualPath_OptionalParameter_ParameterPresentInValues() + Assert.Equal(expectedDataTokens.Count, pathData.DataTokens.Count); + foreach (var dataToken in expectedDataTokens) { - // Arrange - var route = CreateRoute( - template: "{controller}/{action}/{name}.{format?}", - defaults: null, - handleRequest: true, - constraints: null); - - var context = CreateVirtualPathContext( - values: new { action = "Index", controller = "Home", name = "products", format = "xml" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.Equal("/Home/Index/products.xml", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); + Assert.True(pathData.DataTokens.ContainsKey(dataToken.Key)); + Assert.Equal(dataToken.Value, pathData.DataTokens[dataToken.Key]); } + } - [Fact] - public void GetVirtualPath_OptionalParameter_ParameterNotPresentInValues() - { - // Arrange - var route = CreateRoute( - template: "{controller}/{action}/{name}.{format?}", - defaults: null, - handleRequest: true, - constraints: null); + [Fact] + public void GetVirtualPath_ValuesRejectedByHandler_StillGeneratesPath() + { + // Arrange + var route = CreateRoute("{controller}", handleRequest: false); + var context = CreateVirtualPathContext(new { controller = "Home" }); - var context = CreateVirtualPathContext( - values: new { action = "Index", controller = "Home", name = "products" }); + // Act + var pathData = route.GetVirtualPath(context); - // Act - var pathData = route.GetVirtualPath(context); + // Assert + Assert.Equal("/Home", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Assert - Assert.Equal("/Home/Index/products", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + [Fact] + public void GetVirtualPath_Success_AmbientValues() + { + // Arrange + var route = CreateRoute("{controller}/{action}"); + var context = CreateVirtualPathContext(new { action = "Index" }, new { controller = "Home" }); - [Fact] - public void GetVirtualPath_OptionalParameter_ParameterPresentInValuesAndDefaults() - { - // Arrange - var route = CreateRoute( - template: "{controller}/{action}/{name}.{format?}", - defaults: new { format = "json" }, - handleRequest: true, - constraints: null); + // Act + var pathData = route.GetVirtualPath(context); - var context = CreateVirtualPathContext( - values: new { action = "Index", controller = "Home", name = "products", format = "xml" }); + // Assert + Assert.Equal("/Home/Index", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Act - var pathData = route.GetVirtualPath(context); + [Fact] + public void RouteGenerationRejectsConstraints() + { + // Arrange + var context = CreateVirtualPathContext(new { p1 = "abcd" }); - // Assert - Assert.Equal("/Home/Index/products.xml", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + var route = CreateRoute( + "{p1}/{p2}", + new { p2 = "catchall" }, + true, + new RouteValueDictionary(new { p2 = "\\d{4}" })); - [Fact] - public void GetVirtualPath_OptionalParameter_ParameterNotPresentInValues_PresentInDefaults() - { - // Arrange - var route = CreateRoute( - template: "{controller}/{action}/{name}.{format?}", - defaults: new { format = "json" }, - handleRequest: true, - constraints: null); + // Act + var virtualPath = route.GetVirtualPath(context); - var context = CreateVirtualPathContext( - values: new { action = "Index", controller = "Home", name = "products" }); + // Assert + Assert.Null(virtualPath); + } - // Act - var pathData = route.GetVirtualPath(context); + [Fact] + public void RouteGenerationAcceptsConstraints() + { + // Arrange + var context = CreateVirtualPathContext(new { p1 = "hello", p2 = "1234" }); + + var route = CreateRoute( + "{p1}/{p2}", + new { p2 = "catchall" }, + true, + new RouteValueDictionary(new { p2 = "\\d{4}" })); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/hello/1234", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Assert - Assert.Equal("/Home/Index/products", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + [Fact] + public void RouteWithCatchAllRejectsConstraints() + { + // Arrange + var context = CreateVirtualPathContext(new { p1 = "abcd" }); - [Fact] - public void GetVirtualPath_OptionalParameter_ParameterNotPresentInTemplate_PresentInValues() - { - // Arrange - var route = CreateRoute( - template: "{controller}/{action}/{name}", - defaults: null, - handleRequest: true, - constraints: null); + var route = CreateRoute( + "{p1}/{*p2}", + new { p2 = "catchall" }, + true, + new RouteValueDictionary(new { p2 = "\\d{4}" })); - var context = CreateVirtualPathContext( - values: new { action = "Index", controller = "Home", name = "products", format = "json" }); + // Act + var virtualPath = route.GetVirtualPath(context); - // Act - var pathData = route.GetVirtualPath(context); + // Assert + Assert.Null(virtualPath); + } - // Assert - Assert.Equal("/Home/Index/products?format=json", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + [Fact] + public void RouteWithCatchAllAcceptsConstraints() + { + // Arrange + var context = CreateVirtualPathContext(new { p1 = "hello", p2 = "1234" }); + + var route = CreateRoute( + "{p1}/{*p2}", + new { p2 = "catchall" }, + true, + new RouteValueDictionary(new { p2 = "\\d{4}" })); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/hello/1234", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - [Fact] - public void GetVirtualPath_OptionalParameter_FollowedByDotAfterSlash_ParameterPresent() - { - // Arrange - var route = CreateRoute( - template: "{controller}/{action}/.{name?}", - defaults: null, - handleRequest: true, - constraints: null); + [Fact] + public void GetVirtualPathWithNonParameterConstraintReturnsUrlWithoutQueryString() + { + // Arrange + var context = CreateVirtualPathContext(new { p1 = "hello", p2 = "1234" }); + + var target = new Mock(); + target + .Setup( + e => e.Match( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(true) + .Verifiable(); + + var route = CreateRoute( + "{p1}/{p2}", + new { p2 = "catchall" }, + true, + new RouteValueDictionary(new { p2 = target.Object })); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/hello/1234", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + + target.VerifyAll(); + } - var context = CreateVirtualPathContext( - values: new { action = "Index", controller = "Home", name = "products" }); + // Any ambient values from the current request should be visible to constraint, even + // if they have nothing to do with the route generating a link + [Fact] + public void GetVirtualPath_ConstraintsSeeAmbientValues() + { + // Arrange + var constraint = new CapturingConstraint(); + var route = CreateRoute( + template: "slug/{controller}/{action}", + defaults: null, + handleRequest: true, + constraints: new { c = constraint }); + + var context = CreateVirtualPathContext( + values: new { action = "Store" }, + ambientValues: new { Controller = "Home", action = "Blog", extra = "42" }); + + var expectedValues = new RouteValueDictionary( + new { controller = "Home", action = "Store", extra = "42" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/slug/Home/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + + Assert.Equal(expectedValues, constraint.Values); + } - // Act - var pathData = route.GetVirtualPath(context); + // Non-parameter default values from the routing generating a link are not in the 'values' + // collection when constraints are processed. + [Fact] + public void GetVirtualPath_ConstraintsDontSeeDefaults_WhenTheyArentParameters() + { + // Arrange + var constraint = new CapturingConstraint(); + var route = CreateRoute( + template: "slug/{controller}/{action}", + defaults: new { otherthing = "17" }, + handleRequest: true, + constraints: new { c = constraint }); + + var context = CreateVirtualPathContext( + values: new { action = "Store" }, + ambientValues: new { Controller = "Home", action = "Blog" }); + + var expectedValues = new RouteValueDictionary( + new { controller = "Home", action = "Store" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/slug/Home/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + + Assert.Equal(expectedValues, constraint.Values); + } - // Assert - Assert.Equal("/Home/Index/.products", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Default values are visible to the constraint when they are used to fill a parameter. + [Fact] + public void GetVirtualPath_ConstraintsSeesDefault_WhenThereItsAParameter() + { + // Arrange + var constraint = new CapturingConstraint(); + var route = CreateRoute( + template: "slug/{controller}/{action}", + defaults: new { action = "Index" }, + handleRequest: true, + constraints: new { c = constraint }); + + var context = CreateVirtualPathContext( + values: new { controller = "Shopping" }, + ambientValues: new { Controller = "Home", action = "Blog" }); + + var expectedValues = new RouteValueDictionary( + new { controller = "Shopping", action = "Index" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/slug/Shopping", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + + Assert.Equal(expectedValues, constraint.Values); + } - [Fact] - public void GetVirtualPath_OptionalParameter_FollowedByDotAfterSlash_ParameterNotPresent() - { - // Arrange - var route = CreateRoute( - template: "{controller}/{action}/.{name?}", - defaults: null, - handleRequest: true, - constraints: null); + // Default values from the routing generating a link are in the 'values' collection when + // constraints are processed - IFF they are specified as values or ambient values. + [Fact] + public void GetVirtualPath_ConstraintsSeeDefaults_IfTheyAreSpecifiedOrAmbient() + { + // Arrange + var constraint = new CapturingConstraint(); + var route = CreateRoute( + template: "slug/{controller}/{action}", + defaults: new { otherthing = "17", thirdthing = "13" }, + handleRequest: true, + constraints: new { c = constraint }); + + var context = CreateVirtualPathContext( + values: new { action = "Store", thirdthing = "13" }, + ambientValues: new { Controller = "Home", action = "Blog", otherthing = "17" }); + + var expectedValues = new RouteValueDictionary( + new { controller = "Home", action = "Store", otherthing = "17", thirdthing = "13" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/slug/Home/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + + Assert.Equal(expectedValues.OrderBy(kvp => kvp.Key), constraint.Values.OrderBy(kvp => kvp.Key)); + } - var context = CreateVirtualPathContext( - values: new { action = "Index", controller = "Home" }); + [Fact] + public void GetVirtualPath_InlineConstraints_Success() + { + // Arrange + var route = CreateRoute("{controller}/{action}/{id:int}"); + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", id = 4 }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/4", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Act - var pathData = route.GetVirtualPath(context); + [Fact] + public void GetVirtualPath_InlineConstraints_NonMatchingvalue() + { + // Arrange + var route = CreateRoute("{controller}/{action}/{id:int}"); + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", id = "asf" }); - // Assert - Assert.Equal("/Home/Index/", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + var path = route.GetVirtualPath(context); - [Fact] - public void GetVirtualPath_OptionalParameter_InSimpleSegment() - { - // Arrange - var route = CreateRoute( - template: "{controller}/{action}/{name?}", - defaults: null, - handleRequest: true, - constraints: null); + // Assert + Assert.Null(path); + } - var context = CreateVirtualPathContext( - values: new { action = "Index", controller = "Home" }); + [Fact] + public void GetVirtualPath_InlineConstraints_OptionalParameter_ValuePresent() + { + // Arrange + var route = CreateRoute("{controller}/{action}/{id:int?}"); + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", id = 98 }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/98", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Act - var pathData = route.GetVirtualPath(context); + [Fact] + public void GetVirtualPath_InlineConstraints_OptionalParameter_ValueNotPresent() + { + // Arrange + var route = CreateRoute("{controller}/{action}/{id:int?}"); + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Assert - Assert.Equal("/Home/Index", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + [Fact] + public void GetVirtualPath_InlineConstraints_OptionalParameter_ValuePresent_ConstraintFails() + { + // Arrange + var route = CreateRoute("{controller}/{action}/{id:int?}"); + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", id = "sdfd" }); - [Fact] - public void GetVirtualPath_TwoOptionalParameters_OneValueFromAmbientValues() - { - // Arrange - var route = CreateRoute( - template: "a/{b=15}/{c?}/{d?}", - defaults: null, - handleRequest: true, - constraints: null); + // Act + var path = route.GetVirtualPath(context); - var context = CreateVirtualPathContext( - values: new { }, - ambientValues: new { c = "17" }); + // Assert + Assert.Null(path); + } - // Act - var pathData = route.GetVirtualPath(context); + [Fact] + public void GetVirtualPath_InlineConstraints_CompositeInlineConstraint() + { + // Arrange + var route = CreateRoute("{controller}/{action}/{id:int:range(1,20)}"); + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", id = 14 }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/14", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Assert - Assert.NotNull(pathData); - Assert.Equal("/a/15/17", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + [Fact] + public void GetVirtualPath_InlineConstraints_CompositeConstraint_FromConstructor() + { + // Arrange + var constraint = new MaxLengthRouteConstraint(20); + var route = CreateRoute( + template: "{controller}/{action}/{name:alpha}", + defaults: null, + handleRequest: true, + constraints: new { name = constraint }); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", name = "products" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/products", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + [Fact] + public void GetVirtualPath_OptionalParameter_ParameterPresentInValues() + { + // Arrange + var route = CreateRoute( + template: "{controller}/{action}/{name}.{format?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", name = "products", format = "xml" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/products.xml", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - [Fact] - public void GetVirtualPath_OptionalParameterAfterDefault_OneValueFromAmbientValues() - { - // Arrange - var route = CreateRoute( - template: "a/{b=15}/{c?}", - defaults: null, - handleRequest: true, - constraints: null); + [Fact] + public void GetVirtualPath_OptionalParameter_ParameterNotPresentInValues() + { + // Arrange + var route = CreateRoute( + template: "{controller}/{action}/{name}.{format?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", name = "products" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/products", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var context = CreateVirtualPathContext( - values: new { }, - ambientValues: new { c = "17" }); + [Fact] + public void GetVirtualPath_OptionalParameter_ParameterPresentInValuesAndDefaults() + { + // Arrange + var route = CreateRoute( + template: "{controller}/{action}/{name}.{format?}", + defaults: new { format = "json" }, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", name = "products", format = "xml" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/products.xml", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Act - var pathData = route.GetVirtualPath(context); + [Fact] + public void GetVirtualPath_OptionalParameter_ParameterNotPresentInValues_PresentInDefaults() + { + // Arrange + var route = CreateRoute( + template: "{controller}/{action}/{name}.{format?}", + defaults: new { format = "json" }, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", name = "products" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/products", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Assert - Assert.NotNull(pathData); - Assert.Equal("/a/15/17", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + [Fact] + public void GetVirtualPath_OptionalParameter_ParameterNotPresentInTemplate_PresentInValues() + { + // Arrange + var route = CreateRoute( + template: "{controller}/{action}/{name}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", name = "products", format = "json" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/products?format=json", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - [Fact] - public void GetVirtualPath_TwoOptionalParametersAfterDefault_OneValueFromAmbientValues() - { - // Arrange - var route = CreateRoute( - template: "a/{b=15}/{c?}/{d?}", - defaults: null, - handleRequest: true, - constraints: null); + [Fact] + public void GetVirtualPath_OptionalParameter_FollowedByDotAfterSlash_ParameterPresent() + { + // Arrange + var route = CreateRoute( + template: "{controller}/{action}/.{name?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home", name = "products" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/.products", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var context = CreateVirtualPathContext( - values: new { }, - ambientValues: new { c = "17" }); + [Fact] + public void GetVirtualPath_OptionalParameter_FollowedByDotAfterSlash_ParameterNotPresent() + { + // Arrange + var route = CreateRoute( + template: "{controller}/{action}/.{name?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index/", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Act - var pathData = route.GetVirtualPath(context); + [Fact] + public void GetVirtualPath_OptionalParameter_InSimpleSegment() + { + // Arrange + var route = CreateRoute( + template: "{controller}/{action}/{name?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { action = "Index", controller = "Home" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.Equal("/Home/Index", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Assert - Assert.NotNull(pathData); - Assert.Equal("/a/15/17", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + [Fact] + public void GetVirtualPath_TwoOptionalParameters_OneValueFromAmbientValues() + { + // Arrange + var route = CreateRoute( + template: "a/{b=15}/{c?}/{d?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { }, + ambientValues: new { c = "17" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/a/15/17", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - [Fact] - public void GetVirtualPath_TwoOptionalParametersAfterDefault_LastValueFromAmbientValues() - { - // Arrange - var route = CreateRoute( - template: "a/{b=15}/{c?}/{d?}", - defaults: null, - handleRequest: true, - constraints: null); - var context = CreateVirtualPathContext( - values: new { }, - ambientValues: new { d = "17" }); + [Fact] + public void GetVirtualPath_OptionalParameterAfterDefault_OneValueFromAmbientValues() + { + // Arrange + var route = CreateRoute( + template: "a/{b=15}/{c?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { }, + ambientValues: new { c = "17" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/a/15/17", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Act - var pathData = route.GetVirtualPath(context); + [Fact] + public void GetVirtualPath_TwoOptionalParametersAfterDefault_OneValueFromAmbientValues() + { + // Arrange + var route = CreateRoute( + template: "a/{b=15}/{c?}/{d?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { }, + ambientValues: new { c = "17" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/a/15/17", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Assert - Assert.NotNull(pathData); - Assert.Equal("/a", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + [Fact] + public void GetVirtualPath_TwoOptionalParametersAfterDefault_LastValueFromAmbientValues() + { + // Arrange + var route = CreateRoute( + template: "a/{b=15}/{c?}/{d?}", + defaults: null, + handleRequest: true, + constraints: null); + + var context = CreateVirtualPathContext( + values: new { }, + ambientValues: new { d = "17" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/a", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - private static VirtualPathContext CreateVirtualPathContext(object values) - { - return CreateVirtualPathContext(new RouteValueDictionary(values), null); - } + private static VirtualPathContext CreateVirtualPathContext(object values) + { + return CreateVirtualPathContext(new RouteValueDictionary(values), null); + } - private static VirtualPathContext CreateVirtualPathContext(object values, object ambientValues) - { - return CreateVirtualPathContext(new RouteValueDictionary(values), new RouteValueDictionary(ambientValues)); - } + private static VirtualPathContext CreateVirtualPathContext(object values, object ambientValues) + { + return CreateVirtualPathContext(new RouteValueDictionary(values), new RouteValueDictionary(ambientValues)); + } - private static VirtualPathContext CreateVirtualPathContext( - RouteValueDictionary values, - RouteValueDictionary ambientValues) - { - var services = new ServiceCollection(); - services.AddSingleton(NullLoggerFactory.Instance); - services.AddOptions(); - services.AddRouting(); + private static VirtualPathContext CreateVirtualPathContext( + RouteValueDictionary values, + RouteValueDictionary ambientValues) + { + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddOptions(); + services.AddRouting(); - var context = new DefaultHttpContext - { - RequestServices = services.BuildServiceProvider(), - }; + var context = new DefaultHttpContext + { + RequestServices = services.BuildServiceProvider(), + }; - return new VirtualPathContext(context, ambientValues, values); - } + return new VirtualPathContext(context, ambientValues, values); + } - private static VirtualPathContext CreateVirtualPathContext(string routeName) - { - return new VirtualPathContext(null, null, null, routeName); - } + private static VirtualPathContext CreateVirtualPathContext(string routeName) + { + return new VirtualPathContext(null, null, null, routeName); + } - public static IEnumerable DataTokens + public static IEnumerable DataTokens + { + get { - get - { - yield return new object[] { + yield return new object[] { new Dictionary { { "key1", "data1" }, { "key2", 13 } }, new Dictionary { { "key1", "data1" }, { "key2", 13 } }, }; - yield return new object[] { + yield return new object[] { new RouteValueDictionary { { "key1", "data1" }, { "key2", 13 } }, new Dictionary { { "key1", "data1" }, { "key2", 13 } }, }; - yield return new object[] { + yield return new object[] { new object(), new Dictionary(), }; - yield return new object[] { + yield return new object[] { null, new Dictionary() }; - yield return new object[] { + yield return new object[] { new { key1 = "data1", key2 = 13 }, new Dictionary { { "key1", "data1" }, { "key2", 13 } }, }; - } } + } - [Theory] - [MemberData(nameof(DataTokens))] - public void RegisteringRoute_WithDataTokens_AbleToAddTheRoute(object dataToken, - IDictionary expectedDictionary) - { - // Arrange - var routeBuilder = CreateRouteBuilder(); + [Theory] + [MemberData(nameof(DataTokens))] + public void RegisteringRoute_WithDataTokens_AbleToAddTheRoute(object dataToken, + IDictionary expectedDictionary) + { + // Arrange + var routeBuilder = CreateRouteBuilder(); - // Act - routeBuilder.MapRoute("mockName", - "{controller}/{action}", - defaults: null, - constraints: null, - dataTokens: dataToken); + // Act + routeBuilder.MapRoute("mockName", + "{controller}/{action}", + defaults: null, + constraints: null, + dataTokens: dataToken); - // Assert - var templateRoute = (Route)routeBuilder.Routes[0]; + // Assert + var templateRoute = (Route)routeBuilder.Routes[0]; - Assert.Equal(expectedDictionary.Count, templateRoute.DataTokens.Count); - foreach (var expectedKey in expectedDictionary.Keys) - { - Assert.True(templateRoute.DataTokens.ContainsKey(expectedKey)); - Assert.Equal(expectedDictionary[expectedKey], templateRoute.DataTokens[expectedKey]); - } - } - - [Fact] - public void RegisteringRoute_WithParameterPolicy_AbleToAddTheRoute() + Assert.Equal(expectedDictionary.Count, templateRoute.DataTokens.Count); + foreach (var expectedKey in expectedDictionary.Keys) { - // Arrange - var routeBuilder = CreateRouteBuilder(); + Assert.True(templateRoute.DataTokens.ContainsKey(expectedKey)); + Assert.Equal(expectedDictionary[expectedKey], templateRoute.DataTokens[expectedKey]); + } + } - // Act - routeBuilder.MapRoute("mockName", - "{controller:test-policy}/{action}"); + [Fact] + public void RegisteringRoute_WithParameterPolicy_AbleToAddTheRoute() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); - // Assert - var templateRoute = (Route)routeBuilder.Routes[0]; + // Act + routeBuilder.MapRoute("mockName", + "{controller:test-policy}/{action}"); - Assert.Empty(templateRoute.Constraints); - } + // Assert + var templateRoute = (Route)routeBuilder.Routes[0]; - [Fact] - public void RegisteringRouteWithInvalidConstraints_Throws() - { - // Arrange - var routeBuilder = CreateRouteBuilder(); - - // Assert - var expectedMessage = "An error occurred while creating the route with name 'mockName' and template" + - " '{controller}/{action}'."; - - var exception = ExceptionAssert.Throws( - () => routeBuilder.MapRoute("mockName", - "{controller}/{action}", - defaults: null, - constraints: new { controller = "a.*", action = 17 }), - expectedMessage); - - expectedMessage = "The constraint entry 'action' - '17' on the route '{controller}/{action}' " + - "must have a string value or be of a type which implements '" + - typeof(IRouteConstraint) + "'."; - Assert.NotNull(exception.InnerException); - Assert.Equal(expectedMessage, exception.InnerException.Message); - } + Assert.Empty(templateRoute.Constraints); + } - [Fact] - public void RegisteringRouteWithTwoConstraints() - { - // Arrange - var routeBuilder = CreateRouteBuilder(); + [Fact] + public void RegisteringRouteWithInvalidConstraints_Throws() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); - var mockConstraint = new Mock().Object; + // Assert + var expectedMessage = "An error occurred while creating the route with name 'mockName' and template" + + " '{controller}/{action}'."; - routeBuilder.MapRoute("mockName", + var exception = ExceptionAssert.Throws( + () => routeBuilder.MapRoute("mockName", "{controller}/{action}", defaults: null, - constraints: new { controller = "a.*", action = mockConstraint }); + constraints: new { controller = "a.*", action = 17 }), + expectedMessage); + + expectedMessage = "The constraint entry 'action' - '17' on the route '{controller}/{action}' " + + "must have a string value or be of a type which implements '" + + typeof(IRouteConstraint) + "'."; + Assert.NotNull(exception.InnerException); + Assert.Equal(expectedMessage, exception.InnerException.Message); + } - var constraints = ((Route)routeBuilder.Routes[0]).Constraints; + [Fact] + public void RegisteringRouteWithTwoConstraints() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); - // Assert - Assert.Equal(2, constraints.Count); - Assert.IsType(constraints["controller"]); - Assert.Equal(mockConstraint, constraints["action"]); - } + var mockConstraint = new Mock().Object; - [Fact] - public void RegisteringRouteWithOneInlineConstraintAndOneUsingConstraintArgument() - { - // Arrange - var routeBuilder = CreateRouteBuilder(); + routeBuilder.MapRoute("mockName", + "{controller}/{action}", + defaults: null, + constraints: new { controller = "a.*", action = mockConstraint }); - // Act - routeBuilder.MapRoute("mockName", - "{controller}/{action}/{id:int}", - defaults: null, - constraints: new { id = "1*" }); - - // Assert - var constraints = ((Route)routeBuilder.Routes[0]).Constraints; - Assert.Equal(1, constraints.Count); - var constraint = (CompositeRouteConstraint)constraints["id"]; - Assert.IsType(constraint); - Assert.IsType(constraint.Constraints.ElementAt(0)); - Assert.IsType(constraint.Constraints.ElementAt(1)); - } + var constraints = ((Route)routeBuilder.Routes[0]).Constraints; - [Fact] - public void RegisteringRoute_WithOneInlineConstraint_AddsItToConstraintCollection() - { - // Arrange - var routeBuilder = CreateRouteBuilder(); + // Assert + Assert.Equal(2, constraints.Count); + Assert.IsType(constraints["controller"]); + Assert.Equal(mockConstraint, constraints["action"]); + } - // Act - routeBuilder.MapRoute("mockName", - "{controller}/{action}/{id:int}", - defaults: null, - constraints: null); + [Fact] + public void RegisteringRouteWithOneInlineConstraintAndOneUsingConstraintArgument() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); + + // Act + routeBuilder.MapRoute("mockName", + "{controller}/{action}/{id:int}", + defaults: null, + constraints: new { id = "1*" }); + + // Assert + var constraints = ((Route)routeBuilder.Routes[0]).Constraints; + Assert.Equal(1, constraints.Count); + var constraint = (CompositeRouteConstraint)constraints["id"]; + Assert.IsType(constraint); + Assert.IsType(constraint.Constraints.ElementAt(0)); + Assert.IsType(constraint.Constraints.ElementAt(1)); + } - // Assert - var constraints = ((Route)routeBuilder.Routes[0]).Constraints; - Assert.Equal(1, constraints.Count); - Assert.IsType(constraints["id"]); - } + [Fact] + public void RegisteringRoute_WithOneInlineConstraint_AddsItToConstraintCollection() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); + + // Act + routeBuilder.MapRoute("mockName", + "{controller}/{action}/{id:int}", + defaults: null, + constraints: null); + + // Assert + var constraints = ((Route)routeBuilder.Routes[0]).Constraints; + Assert.Equal(1, constraints.Count); + Assert.IsType(constraints["id"]); + } - [Fact] - public void RegisteringRouteWithRouteName_WithNullDefaults_AddsTheRoute() - { - // Arrange - var routeBuilder = CreateRouteBuilder(); + [Fact] + public void RegisteringRouteWithRouteName_WithNullDefaults_AddsTheRoute() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); - routeBuilder.MapRoute(name: "RouteName", template: "{controller}/{action}", defaults: null); + routeBuilder.MapRoute(name: "RouteName", template: "{controller}/{action}", defaults: null); - // Act - var name = ((Route)routeBuilder.Routes[0]).Name; + // Act + var name = ((Route)routeBuilder.Routes[0]).Name; - // Assert - Assert.Equal("RouteName", name); - } + // Assert + Assert.Equal("RouteName", name); + } - [Fact] - public void RegisteringRouteWithRouteName_WithNullDefaultsAndConstraints_AddsTheRoute() - { - // Arrange - var routeBuilder = CreateRouteBuilder(); + [Fact] + public void RegisteringRouteWithRouteName_WithNullDefaultsAndConstraints_AddsTheRoute() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); - routeBuilder.MapRoute(name: "RouteName", - template: "{controller}/{action}", - defaults: null, - constraints: null); + routeBuilder.MapRoute(name: "RouteName", + template: "{controller}/{action}", + defaults: null, + constraints: null); - // Act - var name = ((Route)routeBuilder.Routes[0]).Name; + // Act + var name = ((Route)routeBuilder.Routes[0]).Name; - // Assert - Assert.Equal("RouteName", name); - } + // Assert + Assert.Equal("RouteName", name); + } - [Theory] - [InlineData("///")] - [InlineData("/a//")] - [InlineData("/a/b//")] - [InlineData("//b//")] - [InlineData("///c")] - [InlineData("///c/")] - public async Task RouteAsync_MultipleOptionalParameters_WithEmptyIntermediateSegmentsDoesNotMatch(string url) - { - // Arrange - var builder = CreateRouteBuilder(); + [Theory] + [InlineData("///")] + [InlineData("/a//")] + [InlineData("/a/b//")] + [InlineData("//b//")] + [InlineData("///c")] + [InlineData("///c/")] + public async Task RouteAsync_MultipleOptionalParameters_WithEmptyIntermediateSegmentsDoesNotMatch(string url) + { + // Arrange + var builder = CreateRouteBuilder(); - builder.MapRoute(name: null, - template: "{controller?}/{action?}/{id?}", - defaults: null, - constraints: null); + builder.MapRoute(name: null, + template: "{controller?}/{action?}/{id?}", + defaults: null, + constraints: null); - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext(url); + var context = CreateRouteContext(url); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Null(context.Handler); - } + // Assert + Assert.Null(context.Handler); + } - // DataTokens test data for TemplateRoute.GetVirtualPath - public static IEnumerable DataTokensTestData + // DataTokens test data for TemplateRoute.GetVirtualPath + public static IEnumerable DataTokensTestData + { + get { - get - { - yield return new object[] { null }; - yield return new object[] { new RouteValueDictionary() }; - yield return new object[] { new RouteValueDictionary() { { "tokenKeyA", "tokenValueA" } } }; - } + yield return new object[] { null }; + yield return new object[] { new RouteValueDictionary() }; + yield return new object[] { new RouteValueDictionary() { { "tokenKeyA", "tokenValueA" } } }; } + } - private static IRouteBuilder CreateRouteBuilder() - { - var services = new ServiceCollection(); - services.AddSingleton(_inlineConstraintResolver); - services.AddSingleton(); - services.AddSingleton(); - services.Configure(ConfigureRouteOptions); - - var applicationBuilder = Mock.Of(); - applicationBuilder.ApplicationServices = services.BuildServiceProvider(); - - var routeBuilder = new RouteBuilder(applicationBuilder); - routeBuilder.DefaultHandler = new RouteHandler(NullHandler); - return routeBuilder; - } + private static IRouteBuilder CreateRouteBuilder() + { + var services = new ServiceCollection(); + services.AddSingleton(_inlineConstraintResolver); + services.AddSingleton(); + services.AddSingleton(); + services.Configure(ConfigureRouteOptions); + + var applicationBuilder = Mock.Of(); + applicationBuilder.ApplicationServices = services.BuildServiceProvider(); + + var routeBuilder = new RouteBuilder(applicationBuilder); + routeBuilder.DefaultHandler = new RouteHandler(NullHandler); + return routeBuilder; + } - private static Route CreateRoute(string routeName, string template, bool handleRequest = true) - { - return new Route( - CreateTarget(handleRequest), - routeName, - template, - defaults: null, - constraints: null, - dataTokens: null, - inlineConstraintResolver: _inlineConstraintResolver); - } + private static Route CreateRoute(string routeName, string template, bool handleRequest = true) + { + return new Route( + CreateTarget(handleRequest), + routeName, + template, + defaults: null, + constraints: null, + dataTokens: null, + inlineConstraintResolver: _inlineConstraintResolver); + } - private static Route CreateRoute(string template, bool handleRequest = true) - { - return new Route(CreateTarget(handleRequest), template, _inlineConstraintResolver); - } + private static Route CreateRoute(string template, bool handleRequest = true) + { + return new Route(CreateTarget(handleRequest), template, _inlineConstraintResolver); + } - private static Route CreateRoute( - string template, - object defaults, - bool handleRequest = true, - object constraints = null, - object dataTokens = null) - { - return new Route( - CreateTarget(handleRequest), - template, - new RouteValueDictionary(defaults), - new RouteValueDictionary(constraints), - new RouteValueDictionary(dataTokens), - _inlineConstraintResolver); - } + private static Route CreateRoute( + string template, + object defaults, + bool handleRequest = true, + object constraints = null, + object dataTokens = null) + { + return new Route( + CreateTarget(handleRequest), + template, + new RouteValueDictionary(defaults), + new RouteValueDictionary(constraints), + new RouteValueDictionary(dataTokens), + _inlineConstraintResolver); + } - private static Route CreateRoute(IRouter target, string template) - { - return new Route( - target, - template, - new RouteValueDictionary(), - constraints: null, - dataTokens: null, - inlineConstraintResolver: _inlineConstraintResolver); - } + private static Route CreateRoute(IRouter target, string template) + { + return new Route( + target, + template, + new RouteValueDictionary(), + constraints: null, + dataTokens: null, + inlineConstraintResolver: _inlineConstraintResolver); + } - private static Route CreateRoute( - IRouter target, - string template, - object defaults, - RouteValueDictionary dataTokens = null) - { - return new Route( - target, - template, - new RouteValueDictionary(defaults), - constraints: null, - dataTokens: dataTokens, - inlineConstraintResolver: _inlineConstraintResolver); - } + private static Route CreateRoute( + IRouter target, + string template, + object defaults, + RouteValueDictionary dataTokens = null) + { + return new Route( + target, + template, + new RouteValueDictionary(defaults), + constraints: null, + dataTokens: dataTokens, + inlineConstraintResolver: _inlineConstraintResolver); + } - private static IRouter CreateTarget(bool handleRequest = true) - { - var target = new Mock(MockBehavior.Strict); - target - .Setup(e => e.GetVirtualPath(It.IsAny())) - .Returns(rc => null); + private static IRouter CreateTarget(bool handleRequest = true) + { + var target = new Mock(MockBehavior.Strict); + target + .Setup(e => e.GetVirtualPath(It.IsAny())) + .Returns(rc => null); - target - .Setup(e => e.RouteAsync(It.IsAny())) - .Callback((c) => c.Handler = handleRequest ? NullHandler : null) - .Returns(Task.FromResult(null)); + target + .Setup(e => e.RouteAsync(It.IsAny())) + .Callback((c) => c.Handler = handleRequest ? NullHandler : null) + .Returns(Task.FromResult(null)); - return target.Object; - } + return target.Object; + } - private static IInlineConstraintResolver GetInlineConstraintResolver() - { - var routeOptions = new RouteOptions(); - ConfigureRouteOptions(routeOptions); + private static IInlineConstraintResolver GetInlineConstraintResolver() + { + var routeOptions = new RouteOptions(); + ConfigureRouteOptions(routeOptions); - var routeOptionsMock = new Mock>(); - routeOptionsMock - .SetupGet(o => o.Value) - .Returns(routeOptions); + var routeOptionsMock = new Mock>(); + routeOptionsMock + .SetupGet(o => o.Value) + .Returns(routeOptions); - return new DefaultInlineConstraintResolver(routeOptionsMock.Object, new TestServiceProvider()); - } + return new DefaultInlineConstraintResolver(routeOptionsMock.Object, new TestServiceProvider()); + } - private static void ConfigureRouteOptions(RouteOptions options) - { - options.ConstraintMap["test-policy"] = typeof(TestPolicy); - } + private static void ConfigureRouteOptions(RouteOptions options) + { + options.ConstraintMap["test-policy"] = typeof(TestPolicy); + } - private class TestPolicy : IParameterPolicy - { - } + private class TestPolicy : IParameterPolicy + { } } diff --git a/src/Http/Routing/test/UnitTests/RouteValueEqualityComparerTest.cs b/src/Http/Routing/test/UnitTests/RouteValueEqualityComparerTest.cs index b00e812340..91573ed5f0 100644 --- a/src/Http/Routing/test/UnitTests/RouteValueEqualityComparerTest.cs +++ b/src/Http/Routing/test/UnitTests/RouteValueEqualityComparerTest.cs @@ -3,39 +3,38 @@ using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class RouteValueEqualityComparerTest { - public class RouteValueEqualityComparerTest - { - private readonly RouteValueEqualityComparer _comparer; + private readonly RouteValueEqualityComparer _comparer; - public RouteValueEqualityComparerTest() - { - _comparer = new RouteValueEqualityComparer(); - } + public RouteValueEqualityComparerTest() + { + _comparer = new RouteValueEqualityComparer(); + } - [Theory] - [InlineData(5, 7, false)] - [InlineData("foo", "foo", true)] - [InlineData("foo", "FoO", true)] - [InlineData("foo", "boo", false)] - [InlineData("7", 7, true)] - [InlineData(7, "7", true)] - [InlineData(5.7d, 5.7d, true)] - [InlineData(null, null, true)] - [InlineData(null, "foo", false)] - [InlineData("foo", null, false)] - [InlineData(null, "", true)] - [InlineData("", null, true)] - [InlineData("", "", true)] - [InlineData("", "foo", false)] - [InlineData("foo", "", false)] - [InlineData(true, true, true)] - [InlineData(true, false, false)] - public void EqualsTest(object x, object y, bool expected) - { - var actual = _comparer.Equals(x, y); - Assert.Equal(expected, actual); - } + [Theory] + [InlineData(5, 7, false)] + [InlineData("foo", "foo", true)] + [InlineData("foo", "FoO", true)] + [InlineData("foo", "boo", false)] + [InlineData("7", 7, true)] + [InlineData(7, "7", true)] + [InlineData(5.7d, 5.7d, true)] + [InlineData(null, null, true)] + [InlineData(null, "foo", false)] + [InlineData("foo", null, false)] + [InlineData(null, "", true)] + [InlineData("", null, true)] + [InlineData("", "", true)] + [InlineData("", "foo", false)] + [InlineData("foo", "", false)] + [InlineData(true, true, true)] + [InlineData(true, false, false)] + public void EqualsTest(object x, object y, bool expected) + { + var actual = _comparer.Equals(x, y); + Assert.Equal(expected, actual); } } diff --git a/src/Http/Routing/test/UnitTests/RouteValuesAddressSchemeTest.cs b/src/Http/Routing/test/UnitTests/RouteValuesAddressSchemeTest.cs index cf1f1ad259..1584eff8ef 100644 --- a/src/Http/Routing/test/UnitTests/RouteValuesAddressSchemeTest.cs +++ b/src/Http/Routing/test/UnitTests/RouteValuesAddressSchemeTest.cs @@ -5,464 +5,463 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.TestObjects; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class RouteValuesAddressSchemeTest { - public class RouteValuesAddressSchemeTest + [Fact] + public void GetOutboundMatches_GetsNamedMatchesFor_EndpointsHaving_IRouteNameMetadata() { - [Fact] - public void GetOutboundMatches_GetsNamedMatchesFor_EndpointsHaving_IRouteNameMetadata() - { - // Arrange - var endpoint1 = CreateEndpoint("/a", routeName: "other"); - var endpoint2 = CreateEndpoint("/a", routeName: "named"); - - // Act - var addressScheme = CreateAddressScheme(endpoint1, endpoint2); - - // Assert - var allMatches = GetMatchesWithRequiredValuesPlusNamedMatches(addressScheme); - Assert.Equal(2, allMatches.Count); - Assert.True(addressScheme.State.NamedMatches.TryGetValue("named", out var namedMatches)); - var namedMatch = Assert.Single(namedMatches); - var actual = Assert.IsType(namedMatch.Match.Entry.Data); - Assert.Same(endpoint2, actual); - } + // Arrange + var endpoint1 = CreateEndpoint("/a", routeName: "other"); + var endpoint2 = CreateEndpoint("/a", routeName: "named"); + + // Act + var addressScheme = CreateAddressScheme(endpoint1, endpoint2); + + // Assert + var allMatches = GetMatchesWithRequiredValuesPlusNamedMatches(addressScheme); + Assert.Equal(2, allMatches.Count); + Assert.True(addressScheme.State.NamedMatches.TryGetValue("named", out var namedMatches)); + var namedMatch = Assert.Single(namedMatches); + var actual = Assert.IsType(namedMatch.Match.Entry.Data); + Assert.Same(endpoint2, actual); + } - [Fact] - public void GetOutboundMatches_GroupsMultipleEndpoints_WithSameName() - { - // Arrange - var endpoint1 = CreateEndpoint("/a", routeName: "other"); - var endpoint2 = CreateEndpoint("/a", routeName: "named"); - var endpoint3 = CreateEndpoint("/b", routeName: "named"); - - // Act - var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3); - - // Assert - var allMatches = GetMatchesWithRequiredValuesPlusNamedMatches(addressScheme); - Assert.Equal(3, allMatches.Count); - Assert.True(addressScheme.State.NamedMatches.TryGetValue("named", out var namedMatches)); - Assert.Equal(2, namedMatches.Count); - Assert.Same(endpoint2, Assert.IsType(namedMatches[0].Match.Entry.Data)); - Assert.Same(endpoint3, Assert.IsType(namedMatches[1].Match.Entry.Data)); - } + [Fact] + public void GetOutboundMatches_GroupsMultipleEndpoints_WithSameName() + { + // Arrange + var endpoint1 = CreateEndpoint("/a", routeName: "other"); + var endpoint2 = CreateEndpoint("/a", routeName: "named"); + var endpoint3 = CreateEndpoint("/b", routeName: "named"); + + // Act + var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3); + + // Assert + var allMatches = GetMatchesWithRequiredValuesPlusNamedMatches(addressScheme); + Assert.Equal(3, allMatches.Count); + Assert.True(addressScheme.State.NamedMatches.TryGetValue("named", out var namedMatches)); + Assert.Equal(2, namedMatches.Count); + Assert.Same(endpoint2, Assert.IsType(namedMatches[0].Match.Entry.Data)); + Assert.Same(endpoint3, Assert.IsType(namedMatches[1].Match.Entry.Data)); + } - [Fact] - public void GetOutboundMatches_GroupsMultipleEndpoints_WithSameName_IgnoringCase() - { - // Arrange - var endpoint1 = CreateEndpoint("/a", routeName: "other"); - var endpoint2 = CreateEndpoint("/a", routeName: "named"); - var endpoint3 = CreateEndpoint("/b", routeName: "NaMed"); - - // Act - var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3); - - // Assert - var allMatches = GetMatchesWithRequiredValuesPlusNamedMatches(addressScheme); - Assert.Equal(3, allMatches.Count); - Assert.True(addressScheme.State.NamedMatches.TryGetValue("named", out var namedMatches)); - Assert.Equal(2, namedMatches.Count); - Assert.Same(endpoint2, Assert.IsType(namedMatches[0].Match.Entry.Data)); - Assert.Same(endpoint3, Assert.IsType(namedMatches[1].Match.Entry.Data)); - } + [Fact] + public void GetOutboundMatches_GroupsMultipleEndpoints_WithSameName_IgnoringCase() + { + // Arrange + var endpoint1 = CreateEndpoint("/a", routeName: "other"); + var endpoint2 = CreateEndpoint("/a", routeName: "named"); + var endpoint3 = CreateEndpoint("/b", routeName: "NaMed"); + + // Act + var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3); + + // Assert + var allMatches = GetMatchesWithRequiredValuesPlusNamedMatches(addressScheme); + Assert.Equal(3, allMatches.Count); + Assert.True(addressScheme.State.NamedMatches.TryGetValue("named", out var namedMatches)); + Assert.Equal(2, namedMatches.Count); + Assert.Same(endpoint2, Assert.IsType(namedMatches[0].Match.Entry.Data)); + Assert.Same(endpoint3, Assert.IsType(namedMatches[1].Match.Entry.Data)); + } - [Fact] - public void EndpointDataSource_ChangeCallback_Refreshes_OutboundMatches() - { - // Arrange 1 - var endpoint1 = CreateEndpoint("/a", routeName: "a"); - var dynamicDataSource = new DynamicEndpointDataSource(new[] { endpoint1 }); - - // Act 1 - var addressScheme = new RouteValuesAddressScheme(new CompositeEndpointDataSource(new[] { dynamicDataSource })); - - // Assert 1 - var state = addressScheme.State; - var allMatches = GetMatchesWithRequiredValuesPlusNamedMatches(addressScheme); - - Assert.NotEmpty(allMatches); - - var match = Assert.Single(allMatches); - var actual = Assert.IsType(match.Entry.Data); - Assert.Same(endpoint1, actual); - - // Arrange 2 - var endpoint2 = CreateEndpoint("/b", routeName: "b"); - - // Act 2 - // Trigger change - dynamicDataSource.AddEndpoint(endpoint2); - - // Assert 2 - Assert.NotSame(state, addressScheme.State); - state = addressScheme.State; - - // Arrange 3 - var endpoint3 = CreateEndpoint("/c", routeName: "c"); - - // Act 3 - // Trigger change - dynamicDataSource.AddEndpoint(endpoint3); - - // Assert 3 - Assert.NotSame(state, addressScheme.State); - state = addressScheme.State; - - // Arrange 4 - var endpoint4 = CreateEndpoint("/d", routeName: "d"); - - // Act 4 - // Trigger change - dynamicDataSource.AddEndpoint(endpoint4); - - // Assert 4 - Assert.NotSame(state, addressScheme.State); - state = addressScheme.State; - - allMatches = GetMatchesWithRequiredValuesPlusNamedMatches(addressScheme); - - Assert.NotEmpty(allMatches); - Assert.Collection( - allMatches, - (m) => - { - actual = Assert.IsType(m.Entry.Data); - Assert.Same(endpoint1, actual); - }, - (m) => - { - actual = Assert.IsType(m.Entry.Data); - Assert.Same(endpoint2, actual); - }, - (m) => - { - actual = Assert.IsType(m.Entry.Data); - Assert.Same(endpoint3, actual); - }, - (m) => - { - actual = Assert.IsType(m.Entry.Data); - Assert.Same(endpoint4, actual); - }); - } + [Fact] + public void EndpointDataSource_ChangeCallback_Refreshes_OutboundMatches() + { + // Arrange 1 + var endpoint1 = CreateEndpoint("/a", routeName: "a"); + var dynamicDataSource = new DynamicEndpointDataSource(new[] { endpoint1 }); - [Fact] - public void FindEndpoints_LookedUpByCriteria_NoMatch() - { - // Arrange - var endpoint1 = CreateEndpoint( - "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", - defaults: new { zipCode = 3510 }, - metadataRequiredValues: new { id = 7 }); - var endpoint2 = CreateEndpoint( - "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", - defaults: new { id = 12 }, - metadataRequiredValues: new { zipCode = 3510 }); - var addressScheme = CreateAddressScheme(endpoint1, endpoint2); - - // Act - var foundEndpoints = addressScheme.FindEndpoints( - new RouteValuesAddress - { - ExplicitValues = new RouteValueDictionary(new { id = 8 }), - AmbientValues = new RouteValueDictionary(new { urgent = false }), - }); - - // Assert - Assert.Empty(foundEndpoints); - } + // Act 1 + var addressScheme = new RouteValuesAddressScheme(new CompositeEndpointDataSource(new[] { dynamicDataSource })); - [Fact] - public void FindEndpoints_LookedUpByCriteria_OneMatch() - { - // Arrange - var endpoint1 = CreateEndpoint( - "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", - defaults: new { zipCode = 3510 }, - metadataRequiredValues: new { id = 7 }); - var endpoint2 = CreateEndpoint( - "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", - defaults: new { id = 12 }); - var addressScheme = CreateAddressScheme(endpoint1, endpoint2); - - // Act - var foundEndpoints = addressScheme.FindEndpoints( - new RouteValuesAddress - { - ExplicitValues = new RouteValueDictionary(new { id = 7 }), - AmbientValues = new RouteValueDictionary(new { zipCode = 3500 }), - }); - - // Assert - var actual = Assert.Single(foundEndpoints); - Assert.Same(endpoint1, actual); - } + // Assert 1 + var state = addressScheme.State; + var allMatches = GetMatchesWithRequiredValuesPlusNamedMatches(addressScheme); - [Fact] - public void FindEndpoints_LookedUpByCriteria_MultipleMatches() - { - // Arrange - var endpoint1 = CreateEndpoint( - "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", - defaults: new { zipCode = 3510 }, - metadataRequiredValues: new { id = 7 }); - var endpoint2 = CreateEndpoint( - "api/orders/{id}/{name?}/{urgent}/{zipCode}", - defaults: new { id = 12 }, - metadataRequiredValues: new { id = 12 }); - var endpoint3 = CreateEndpoint( - "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", - defaults: new { id = 12 }, - metadataRequiredValues: new { id = 12 }); - var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3); - - // Act - var foundEndpoints = addressScheme.FindEndpoints( - new RouteValuesAddress - { - ExplicitValues = new RouteValueDictionary(new { id = 12 }), - AmbientValues = new RouteValueDictionary(new { zipCode = 3500 }), - }); - - // Assert - Assert.Collection(foundEndpoints, - e => Assert.Equal(endpoint3, e), - e => Assert.Equal(endpoint2, e)); - } + Assert.NotEmpty(allMatches); - [Fact] - public void FindEndpoints_LookedUpByCriteria_ExcludeEndpointWithoutRouteValuesAddressMetadata() - { - // Arrange - var endpoint1 = CreateEndpoint( - "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", - defaults: new { zipCode = 3510 }, - metadataRequiredValues: new { id = 7 }); - var endpoint2 = CreateEndpoint("test"); - - var addressScheme = CreateAddressScheme(endpoint1, endpoint2); - - // Act - var foundEndpoints = addressScheme.FindEndpoints( - new RouteValuesAddress - { - ExplicitValues = new RouteValueDictionary(new { id = 7 }), - AmbientValues = new RouteValueDictionary(new { zipCode = 3500 }), - }).ToList(); - - // Assert - Assert.DoesNotContain(endpoint2, foundEndpoints); - Assert.Contains(endpoint1, foundEndpoints); - } + var match = Assert.Single(allMatches); + var actual = Assert.IsType(match.Entry.Data); + Assert.Same(endpoint1, actual); - [Fact] - public void FindEndpoints_ReturnsEndpoint_WhenLookedUpByRouteName() - { - // Arrange - var expected = CreateEndpoint( - "api/orders/{id}", - defaults: new { controller = "Orders", action = "GetById" }, - metadataRequiredValues: new { controller = "Orders", action = "GetById" }, - routeName: "OrdersApi"); - var addressScheme = CreateAddressScheme(expected); - - // Act - var foundEndpoints = addressScheme.FindEndpoints( - new RouteValuesAddress - { - ExplicitValues = new RouteValueDictionary(new { id = 10 }), - AmbientValues = new RouteValueDictionary(new { controller = "Home", action = "Index" }), - RouteName = "OrdersApi" - }); - - // Assert - var actual = Assert.Single(foundEndpoints); - Assert.Same(expected, actual); - } + // Arrange 2 + var endpoint2 = CreateEndpoint("/b", routeName: "b"); - [Fact] - public void FindEndpoints_ReturnsEndpoint_UsingRoutePatternRequiredValues() - { - // Arrange - var expected = CreateEndpoint( - "api/orders/{id}", - defaults: new { controller = "Orders", action = "GetById" }, - metadataRequiredValues: new { controller = "Orders", action = "GetById" }); - var addressScheme = CreateAddressScheme(expected); - - // Act - var foundEndpoints = addressScheme.FindEndpoints( - new RouteValuesAddress - { - ExplicitValues = new RouteValueDictionary(new { id = 10 }), - AmbientValues = new RouteValueDictionary(new { controller = "Orders", action = "GetById" }), - }); - - // Assert - var actual = Assert.Single(foundEndpoints); - Assert.Same(expected, actual); - } + // Act 2 + // Trigger change + dynamicDataSource.AddEndpoint(endpoint2); - [Fact] - public void FindEndpoints_AlwaysReturnsEndpointsByRouteName_IgnoringMissingRequiredParameterValues() - { - // Here 'id' is the required value. The endpoint addressScheme would always return an endpoint by looking up - // name only. Its the link generator which uses these endpoints finally to generate a link or not - // based on the required parameter values being present or not. - - // Arrange - var expected = CreateEndpoint( - "api/orders/{id}", - defaults: new { controller = "Orders", action = "GetById" }, - metadataRequiredValues: new { controller = "Orders", action = "GetById" }, - routeName: "OrdersApi"); - var addressScheme = CreateAddressScheme(expected); - - // Act - var foundEndpoints = addressScheme.FindEndpoints( - new RouteValuesAddress - { - ExplicitValues = new RouteValueDictionary(), - AmbientValues = new RouteValueDictionary(), - RouteName = "OrdersApi" - }); - - // Assert - var actual = Assert.Single(foundEndpoints); - Assert.Same(expected, actual); - } + // Assert 2 + Assert.NotSame(state, addressScheme.State); + state = addressScheme.State; - [Fact] - public void GetOutboundMatches_Includes_SameEndpointInNamedMatchesAndMatchesWithRequiredValues() - { - // Arrange - var endpoint = CreateEndpoint( - "api/orders/{id}", - defaults: new { controller = "Orders", action = "GetById" }, - metadataRequiredValues: new { controller = "Orders", action = "GetById" }, - routeName: "a"); - - // Act - var addressScheme = CreateAddressScheme(endpoint); - - // Assert - var matchWithRequiredValue = Assert.Single(addressScheme.State.MatchesWithRequiredValues); - var namedMatches = Assert.Single(addressScheme.State.NamedMatches).Value; - var namedMatch = Assert.Single(namedMatches).Match; - - Assert.Same(endpoint, matchWithRequiredValue.Entry.Data); - Assert.Same(endpoint, namedMatch.Entry.Data); - } + // Arrange 3 + var endpoint3 = CreateEndpoint("/c", routeName: "c"); - // Regression test for https://github.com/dotnet/aspnetcore/issues/35592 - [Fact] - public void GetOutboundMatches_DoesNotInclude_EndpointsWithoutRequiredValuesInMatchesWithRequiredValues() - { - // Arrange - var endpoint = CreateEndpoint( - "api/orders/{id}", - defaults: new { controller = "Orders", action = "GetById" }, - routeName: "a"); + // Act 3 + // Trigger change + dynamicDataSource.AddEndpoint(endpoint3); - // Act - var addressScheme = CreateAddressScheme(endpoint); + // Assert 3 + Assert.NotSame(state, addressScheme.State); + state = addressScheme.State; - // Assert - Assert.Empty(addressScheme.State.MatchesWithRequiredValues); + // Arrange 4 + var endpoint4 = CreateEndpoint("/d", routeName: "d"); - var namedMatches = Assert.Single(addressScheme.State.NamedMatches).Value; - var namedMatch = Assert.Single(namedMatches).Match; - Assert.Same(endpoint, namedMatch.Entry.Data); - } + // Act 4 + // Trigger change + dynamicDataSource.AddEndpoint(endpoint4); - [Fact] - public void GetOutboundMatches_DoesNotInclude_EndpointsWithSuppressLinkGenerationMetadata() - { - // Arrange - var endpoint = CreateEndpoint( - "api/orders/{id}", - defaults: new { controller = "Orders", action = "GetById" }, - metadataRequiredValues: new { controller = "Orders", action = "GetById" }, - routeName: "a", - metadataCollection: new EndpointMetadataCollection(new[] { new SuppressLinkGenerationMetadata() })); - - // Act - var addressScheme = CreateAddressScheme(endpoint); - - // Assert - var allMatches = GetMatchesWithRequiredValuesPlusNamedMatches(addressScheme); - Assert.Empty(allMatches); - } + // Assert 4 + Assert.NotSame(state, addressScheme.State); + state = addressScheme.State; - [Fact] - public void AddressScheme_UnsuppressedEndpoint_IsUsed() - { - // Arrange - var endpoint = EndpointFactory.CreateRouteEndpoint( - "/a", - metadata: new object[] { new SuppressLinkGenerationMetadata(), new EncourageLinkGenerationMetadata(), new RouteNameMetadata("a"), }); + allMatches = GetMatchesWithRequiredValuesPlusNamedMatches(addressScheme); - // Act - var addressScheme = CreateAddressScheme(endpoint); + Assert.NotEmpty(allMatches); + Assert.Collection( + allMatches, + (m) => + { + actual = Assert.IsType(m.Entry.Data); + Assert.Same(endpoint1, actual); + }, + (m) => + { + actual = Assert.IsType(m.Entry.Data); + Assert.Same(endpoint2, actual); + }, + (m) => + { + actual = Assert.IsType(m.Entry.Data); + Assert.Same(endpoint3, actual); + }, + (m) => + { + actual = Assert.IsType(m.Entry.Data); + Assert.Same(endpoint4, actual); + }); + } - // Assert - var allMatches = GetMatchesWithRequiredValuesPlusNamedMatches(addressScheme); - Assert.Same(endpoint, Assert.Single(allMatches).Entry.Data); - } + [Fact] + public void FindEndpoints_LookedUpByCriteria_NoMatch() + { + // Arrange + var endpoint1 = CreateEndpoint( + "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", + defaults: new { zipCode = 3510 }, + metadataRequiredValues: new { id = 7 }); + var endpoint2 = CreateEndpoint( + "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", + defaults: new { id = 12 }, + metadataRequiredValues: new { zipCode = 3510 }); + var addressScheme = CreateAddressScheme(endpoint1, endpoint2); + + // Act + var foundEndpoints = addressScheme.FindEndpoints( + new RouteValuesAddress + { + ExplicitValues = new RouteValueDictionary(new { id = 8 }), + AmbientValues = new RouteValueDictionary(new { urgent = false }), + }); - private RouteValuesAddressScheme CreateAddressScheme(params Endpoint[] endpoints) - { - return CreateAddressScheme(new DefaultEndpointDataSource(endpoints)); - } + // Assert + Assert.Empty(foundEndpoints); + } - private RouteValuesAddressScheme CreateAddressScheme(params EndpointDataSource[] dataSources) - { - return new RouteValuesAddressScheme(new CompositeEndpointDataSource(dataSources)); - } + [Fact] + public void FindEndpoints_LookedUpByCriteria_OneMatch() + { + // Arrange + var endpoint1 = CreateEndpoint( + "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", + defaults: new { zipCode = 3510 }, + metadataRequiredValues: new { id = 7 }); + var endpoint2 = CreateEndpoint( + "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", + defaults: new { id = 12 }); + var addressScheme = CreateAddressScheme(endpoint1, endpoint2); + + // Act + var foundEndpoints = addressScheme.FindEndpoints( + new RouteValuesAddress + { + ExplicitValues = new RouteValueDictionary(new { id = 7 }), + AmbientValues = new RouteValueDictionary(new { zipCode = 3500 }), + }); - private RouteEndpoint CreateEndpoint( - string template, - object defaults = null, - object metadataRequiredValues = null, - int order = 0, - string routeName = null, - EndpointMetadataCollection metadataCollection = null) - { - if (metadataCollection == null) + // Assert + var actual = Assert.Single(foundEndpoints); + Assert.Same(endpoint1, actual); + } + + [Fact] + public void FindEndpoints_LookedUpByCriteria_MultipleMatches() + { + // Arrange + var endpoint1 = CreateEndpoint( + "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", + defaults: new { zipCode = 3510 }, + metadataRequiredValues: new { id = 7 }); + var endpoint2 = CreateEndpoint( + "api/orders/{id}/{name?}/{urgent}/{zipCode}", + defaults: new { id = 12 }, + metadataRequiredValues: new { id = 12 }); + var endpoint3 = CreateEndpoint( + "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", + defaults: new { id = 12 }, + metadataRequiredValues: new { id = 12 }); + var addressScheme = CreateAddressScheme(endpoint1, endpoint2, endpoint3); + + // Act + var foundEndpoints = addressScheme.FindEndpoints( + new RouteValuesAddress { - var metadata = new List(); - if (!string.IsNullOrEmpty(routeName)) - { - metadata.Add(new RouteNameMetadata(routeName)); - } - metadataCollection = new EndpointMetadataCollection(metadata); - } + ExplicitValues = new RouteValueDictionary(new { id = 12 }), + AmbientValues = new RouteValueDictionary(new { zipCode = 3500 }), + }); + + // Assert + Assert.Collection(foundEndpoints, + e => Assert.Equal(endpoint3, e), + e => Assert.Equal(endpoint2, e)); + } - return new RouteEndpoint( - TestConstants.EmptyRequestDelegate, - RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues: metadataRequiredValues), - order, - metadataCollection, - null); - } + [Fact] + public void FindEndpoints_LookedUpByCriteria_ExcludeEndpointWithoutRouteValuesAddressMetadata() + { + // Arrange + var endpoint1 = CreateEndpoint( + "api/orders/{id}/{name?}/{urgent=true}/{zipCode}", + defaults: new { zipCode = 3510 }, + metadataRequiredValues: new { id = 7 }); + var endpoint2 = CreateEndpoint("test"); + + var addressScheme = CreateAddressScheme(endpoint1, endpoint2); + + // Act + var foundEndpoints = addressScheme.FindEndpoints( + new RouteValuesAddress + { + ExplicitValues = new RouteValueDictionary(new { id = 7 }), + AmbientValues = new RouteValueDictionary(new { zipCode = 3500 }), + }).ToList(); - private static List GetMatchesWithRequiredValuesPlusNamedMatches(RouteValuesAddressScheme routeValuesAddressScheme) - { - var state = routeValuesAddressScheme.State; + // Assert + Assert.DoesNotContain(endpoint2, foundEndpoints); + Assert.Contains(endpoint1, foundEndpoints); + } - Assert.NotNull(state.MatchesWithRequiredValues); - Assert.NotNull(state.NamedMatches); + [Fact] + public void FindEndpoints_ReturnsEndpoint_WhenLookedUpByRouteName() + { + // Arrange + var expected = CreateEndpoint( + "api/orders/{id}", + defaults: new { controller = "Orders", action = "GetById" }, + metadataRequiredValues: new { controller = "Orders", action = "GetById" }, + routeName: "OrdersApi"); + var addressScheme = CreateAddressScheme(expected); + + // Act + var foundEndpoints = addressScheme.FindEndpoints( + new RouteValuesAddress + { + ExplicitValues = new RouteValueDictionary(new { id = 10 }), + AmbientValues = new RouteValueDictionary(new { controller = "Home", action = "Index" }), + RouteName = "OrdersApi" + }); + + // Assert + var actual = Assert.Single(foundEndpoints); + Assert.Same(expected, actual); + } - var namedMatches = state.NamedMatches.Aggregate(Enumerable.Empty(), - (acc, kvp) => acc.Concat(kvp.Value.Select(matchResult => matchResult.Match))); - return state.MatchesWithRequiredValues.Concat(namedMatches).ToList(); - } + [Fact] + public void FindEndpoints_ReturnsEndpoint_UsingRoutePatternRequiredValues() + { + // Arrange + var expected = CreateEndpoint( + "api/orders/{id}", + defaults: new { controller = "Orders", action = "GetById" }, + metadataRequiredValues: new { controller = "Orders", action = "GetById" }); + var addressScheme = CreateAddressScheme(expected); + + // Act + var foundEndpoints = addressScheme.FindEndpoints( + new RouteValuesAddress + { + ExplicitValues = new RouteValueDictionary(new { id = 10 }), + AmbientValues = new RouteValueDictionary(new { controller = "Orders", action = "GetById" }), + }); + + // Assert + var actual = Assert.Single(foundEndpoints); + Assert.Same(expected, actual); + } + + [Fact] + public void FindEndpoints_AlwaysReturnsEndpointsByRouteName_IgnoringMissingRequiredParameterValues() + { + // Here 'id' is the required value. The endpoint addressScheme would always return an endpoint by looking up + // name only. Its the link generator which uses these endpoints finally to generate a link or not + // based on the required parameter values being present or not. + + // Arrange + var expected = CreateEndpoint( + "api/orders/{id}", + defaults: new { controller = "Orders", action = "GetById" }, + metadataRequiredValues: new { controller = "Orders", action = "GetById" }, + routeName: "OrdersApi"); + var addressScheme = CreateAddressScheme(expected); + + // Act + var foundEndpoints = addressScheme.FindEndpoints( + new RouteValuesAddress + { + ExplicitValues = new RouteValueDictionary(), + AmbientValues = new RouteValueDictionary(), + RouteName = "OrdersApi" + }); + + // Assert + var actual = Assert.Single(foundEndpoints); + Assert.Same(expected, actual); + } + + [Fact] + public void GetOutboundMatches_Includes_SameEndpointInNamedMatchesAndMatchesWithRequiredValues() + { + // Arrange + var endpoint = CreateEndpoint( + "api/orders/{id}", + defaults: new { controller = "Orders", action = "GetById" }, + metadataRequiredValues: new { controller = "Orders", action = "GetById" }, + routeName: "a"); + + // Act + var addressScheme = CreateAddressScheme(endpoint); + + // Assert + var matchWithRequiredValue = Assert.Single(addressScheme.State.MatchesWithRequiredValues); + var namedMatches = Assert.Single(addressScheme.State.NamedMatches).Value; + var namedMatch = Assert.Single(namedMatches).Match; + + Assert.Same(endpoint, matchWithRequiredValue.Entry.Data); + Assert.Same(endpoint, namedMatch.Entry.Data); + } + + // Regression test for https://github.com/dotnet/aspnetcore/issues/35592 + [Fact] + public void GetOutboundMatches_DoesNotInclude_EndpointsWithoutRequiredValuesInMatchesWithRequiredValues() + { + // Arrange + var endpoint = CreateEndpoint( + "api/orders/{id}", + defaults: new { controller = "Orders", action = "GetById" }, + routeName: "a"); + + // Act + var addressScheme = CreateAddressScheme(endpoint); + + // Assert + Assert.Empty(addressScheme.State.MatchesWithRequiredValues); + + var namedMatches = Assert.Single(addressScheme.State.NamedMatches).Value; + var namedMatch = Assert.Single(namedMatches).Match; + Assert.Same(endpoint, namedMatch.Entry.Data); + } + + [Fact] + public void GetOutboundMatches_DoesNotInclude_EndpointsWithSuppressLinkGenerationMetadata() + { + // Arrange + var endpoint = CreateEndpoint( + "api/orders/{id}", + defaults: new { controller = "Orders", action = "GetById" }, + metadataRequiredValues: new { controller = "Orders", action = "GetById" }, + routeName: "a", + metadataCollection: new EndpointMetadataCollection(new[] { new SuppressLinkGenerationMetadata() })); + + // Act + var addressScheme = CreateAddressScheme(endpoint); + + // Assert + var allMatches = GetMatchesWithRequiredValuesPlusNamedMatches(addressScheme); + Assert.Empty(allMatches); + } + + [Fact] + public void AddressScheme_UnsuppressedEndpoint_IsUsed() + { + // Arrange + var endpoint = EndpointFactory.CreateRouteEndpoint( + "/a", + metadata: new object[] { new SuppressLinkGenerationMetadata(), new EncourageLinkGenerationMetadata(), new RouteNameMetadata("a"), }); + + // Act + var addressScheme = CreateAddressScheme(endpoint); - private class EncourageLinkGenerationMetadata : ISuppressLinkGenerationMetadata + // Assert + var allMatches = GetMatchesWithRequiredValuesPlusNamedMatches(addressScheme); + Assert.Same(endpoint, Assert.Single(allMatches).Entry.Data); + } + + private RouteValuesAddressScheme CreateAddressScheme(params Endpoint[] endpoints) + { + return CreateAddressScheme(new DefaultEndpointDataSource(endpoints)); + } + + private RouteValuesAddressScheme CreateAddressScheme(params EndpointDataSource[] dataSources) + { + return new RouteValuesAddressScheme(new CompositeEndpointDataSource(dataSources)); + } + + private RouteEndpoint CreateEndpoint( + string template, + object defaults = null, + object metadataRequiredValues = null, + int order = 0, + string routeName = null, + EndpointMetadataCollection metadataCollection = null) + { + if (metadataCollection == null) { - public bool SuppressLinkGeneration => false; + var metadata = new List(); + if (!string.IsNullOrEmpty(routeName)) + { + metadata.Add(new RouteNameMetadata(routeName)); + } + metadataCollection = new EndpointMetadataCollection(metadata); } + + return new RouteEndpoint( + TestConstants.EmptyRequestDelegate, + RoutePatternFactory.Parse(template, defaults, parameterPolicies: null, requiredValues: metadataRequiredValues), + order, + metadataCollection, + null); + } + + private static List GetMatchesWithRequiredValuesPlusNamedMatches(RouteValuesAddressScheme routeValuesAddressScheme) + { + var state = routeValuesAddressScheme.State; + + Assert.NotNull(state.MatchesWithRequiredValues); + Assert.NotNull(state.NamedMatches); + + var namedMatches = state.NamedMatches.Aggregate(Enumerable.Empty(), + (acc, kvp) => acc.Concat(kvp.Value.Select(matchResult => matchResult.Match))); + return state.MatchesWithRequiredValues.Concat(namedMatches).ToList(); + } + + private class EncourageLinkGenerationMetadata : ISuppressLinkGenerationMetadata + { + public bool SuppressLinkGeneration => false; } } diff --git a/src/Http/Routing/test/UnitTests/RouterMiddlewareTest.cs b/src/Http/Routing/test/UnitTests/RouterMiddlewareTest.cs index d72963238a..eed862bc10 100644 --- a/src/Http/Routing/test/UnitTests/RouterMiddlewareTest.cs +++ b/src/Http/Routing/test/UnitTests/RouterMiddlewareTest.cs @@ -12,147 +12,146 @@ using Microsoft.Extensions.Logging.Testing; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class RouterMiddlewareTest { - public class RouterMiddlewareTest + [Fact] + public async Task RoutingFeatureSetInIRouter() { - [Fact] - public async Task RoutingFeatureSetInIRouter() + // Arrange + var services = new ServiceCollection(); + services.AddLogging(); + var httpContext = new DefaultHttpContext { - // Arrange - var services = new ServiceCollection(); - services.AddLogging(); - var httpContext = new DefaultHttpContext - { - RequestServices = services.BuildServiceProvider() - }; + RequestServices = services.BuildServiceProvider() + }; - httpContext.Request.Path = "/foo/10"; + httpContext.Request.Path = "/foo/10"; - var routeHandlerExecuted = false; + var routeHandlerExecuted = false; - var handler = new RouteHandler(context => - { - routeHandlerExecuted = true; + var handler = new RouteHandler(context => + { + routeHandlerExecuted = true; - var routingFeature = context.Features.Get(); + var routingFeature = context.Features.Get(); - Assert.NotNull(routingFeature); - Assert.NotNull(context.Features.Get()); + Assert.NotNull(routingFeature); + Assert.NotNull(context.Features.Get()); - Assert.Single(routingFeature.RouteData.Values); - Assert.Single(context.Request.RouteValues); - Assert.True(routingFeature.RouteData.Values.ContainsKey("id")); - Assert.True(context.Request.RouteValues.ContainsKey("id")); - Assert.Equal("10", routingFeature.RouteData.Values["id"]); - Assert.Equal("10", context.Request.RouteValues["id"]); - Assert.Equal("10", context.GetRouteValue("id")); - Assert.Same(routingFeature.RouteData, context.GetRouteData()); + Assert.Single(routingFeature.RouteData.Values); + Assert.Single(context.Request.RouteValues); + Assert.True(routingFeature.RouteData.Values.ContainsKey("id")); + Assert.True(context.Request.RouteValues.ContainsKey("id")); + Assert.Equal("10", routingFeature.RouteData.Values["id"]); + Assert.Equal("10", context.Request.RouteValues["id"]); + Assert.Equal("10", context.GetRouteValue("id")); + Assert.Same(routingFeature.RouteData, context.GetRouteData()); - return Task.CompletedTask; - }); + return Task.CompletedTask; + }); - var route = new Route(handler, "/foo/{id}", Mock.Of()); + var route = new Route(handler, "/foo/{id}", Mock.Of()); - var middleware = new RouterMiddleware(context => Task.CompletedTask, NullLoggerFactory.Instance, route); + var middleware = new RouterMiddleware(context => Task.CompletedTask, NullLoggerFactory.Instance, route); - // Act - await middleware.Invoke(httpContext); + // Act + await middleware.Invoke(httpContext); - // Assert - Assert.True(routeHandlerExecuted); + // Assert + Assert.True(routeHandlerExecuted); - } + } - [Fact] - public async Task Invoke_LogsCorrectValues_WhenNotHandled() + [Fact] + public async Task Invoke_LogsCorrectValues_WhenNotHandled() + { + // Arrange + var expectedMessage = "Request did not match any routes"; + var isHandled = false; + + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = new ServiceProvider(); + + RequestDelegate next = (c) => { - // Arrange - var expectedMessage = "Request did not match any routes"; - var isHandled = false; + return Task.FromResult(null); + }; - var sink = new TestSink( - TestSink.EnableWithTypeName, - TestSink.EnableWithTypeName); - var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var router = new TestRouter(isHandled); + var middleware = new RouterMiddleware(next, loggerFactory, router); - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = new ServiceProvider(); + // Act + await middleware.Invoke(httpContext); - RequestDelegate next = (c) => - { - return Task.FromResult(null); - }; + // Assert + Assert.Empty(sink.Scopes); + var write = Assert.Single(sink.Writes); + Assert.Equal(expectedMessage, write.State?.ToString()); + } - var router = new TestRouter(isHandled); - var middleware = new RouterMiddleware(next, loggerFactory, router); + [Fact] + public async Task Invoke_DoesNotLog_WhenHandled() + { + // Arrange + var isHandled = true; - // Act - await middleware.Invoke(httpContext); + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); - // Assert - Assert.Empty(sink.Scopes); - var write = Assert.Single(sink.Writes); - Assert.Equal(expectedMessage, write.State?.ToString()); - } + var httpContext = new DefaultHttpContext(); + httpContext.RequestServices = new ServiceProvider(); - [Fact] - public async Task Invoke_DoesNotLog_WhenHandled() + RequestDelegate next = (c) => { - // Arrange - var isHandled = true; + return Task.FromResult(null); + }; - var sink = new TestSink( - TestSink.EnableWithTypeName, - TestSink.EnableWithTypeName); - var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var router = new TestRouter(isHandled); + var middleware = new RouterMiddleware(next, loggerFactory, router); - var httpContext = new DefaultHttpContext(); - httpContext.RequestServices = new ServiceProvider(); + // Act + await middleware.Invoke(httpContext); - RequestDelegate next = (c) => - { - return Task.FromResult(null); - }; + // Assert + Assert.Empty(sink.Scopes); + Assert.Empty(sink.Writes); + } - var router = new TestRouter(isHandled); - var middleware = new RouterMiddleware(next, loggerFactory, router); + private class TestRouter : IRouter + { + private readonly bool _isHandled; - // Act - await middleware.Invoke(httpContext); + public TestRouter(bool isHandled) + { + _isHandled = isHandled; + } - // Assert - Assert.Empty(sink.Scopes); - Assert.Empty(sink.Writes); + public VirtualPathData GetVirtualPath(VirtualPathContext context) + { + return new VirtualPathData(this, ""); } - private class TestRouter : IRouter + public Task RouteAsync(RouteContext context) { - private readonly bool _isHandled; - - public TestRouter(bool isHandled) - { - _isHandled = isHandled; - } - - public VirtualPathData GetVirtualPath(VirtualPathContext context) - { - return new VirtualPathData(this, ""); - } - - public Task RouteAsync(RouteContext context) - { - context.Handler = _isHandled ? (RequestDelegate)((c) => Task.CompletedTask) : null; - return Task.FromResult(null); - } + context.Handler = _isHandled ? (RequestDelegate)((c) => Task.CompletedTask) : null; + return Task.FromResult(null); } + } - private class ServiceProvider : IServiceProvider + private class ServiceProvider : IServiceProvider + { + public object GetService(Type serviceType) { - public object GetService(Type serviceType) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } } } diff --git a/src/Http/Routing/test/UnitTests/RoutingEndpointConventionBuilderExtensionsTests.cs b/src/Http/Routing/test/UnitTests/RoutingEndpointConventionBuilderExtensionsTests.cs index 58c806302e..fd38d17587 100644 --- a/src/Http/Routing/test/UnitTests/RoutingEndpointConventionBuilderExtensionsTests.cs +++ b/src/Http/Routing/test/UnitTests/RoutingEndpointConventionBuilderExtensionsTests.cs @@ -10,38 +10,37 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing.Patterns; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class RoutingEndpointConventionBuilderExtensionsTests { - public class RoutingEndpointConventionBuilderExtensionsTests + [Fact] + public void RequireHost_HostNames() { - [Fact] - public void RequireHost_HostNames() - { - // Arrange - var builder = new TestEndpointConventionBuilder(); + // Arrange + var builder = new TestEndpointConventionBuilder(); - // Act - builder.RequireHost("contoso.com:8080"); + // Act + builder.RequireHost("contoso.com:8080"); - // Assert - var convention = Assert.Single(builder.Conventions); + // Assert + var convention = Assert.Single(builder.Conventions); - var endpointModel = new RouteEndpointBuilder((context) => Task.CompletedTask, RoutePatternFactory.Parse("/"), 0); - convention(endpointModel); + var endpointModel = new RouteEndpointBuilder((context) => Task.CompletedTask, RoutePatternFactory.Parse("/"), 0); + convention(endpointModel); - var hostMetadata = Assert.IsType(Assert.Single(endpointModel.Metadata)); + var hostMetadata = Assert.IsType(Assert.Single(endpointModel.Metadata)); - Assert.Equal("contoso.com:8080", hostMetadata.Hosts.Single()); - } + Assert.Equal("contoso.com:8080", hostMetadata.Hosts.Single()); + } - private class TestEndpointConventionBuilder : IEndpointConventionBuilder - { - public IList> Conventions { get; } = new List>(); + private class TestEndpointConventionBuilder : IEndpointConventionBuilder + { + public IList> Conventions { get; } = new List>(); - public void Add(Action convention) - { - Conventions.Add(convention); - } + public void Add(Action convention) + { + Conventions.Add(convention); } } } diff --git a/src/Http/Routing/test/UnitTests/Template/RoutePatternPrecedenceTests.cs b/src/Http/Routing/test/UnitTests/Template/RoutePatternPrecedenceTests.cs index 023b9ab2ab..e5c4db0d2b 100644 --- a/src/Http/Routing/test/UnitTests/Template/RoutePatternPrecedenceTests.cs +++ b/src/Http/Routing/test/UnitTests/Template/RoutePatternPrecedenceTests.cs @@ -5,39 +5,38 @@ using System; using Microsoft.AspNetCore.Routing.Patterns; using Xunit; -namespace Microsoft.AspNetCore.Routing.Template +namespace Microsoft.AspNetCore.Routing.Template; + +public class RoutePatternPrecedenceTests : RoutePrecedenceTestsBase { - public class RoutePatternPrecedenceTests : RoutePrecedenceTestsBase + protected override decimal ComputeMatched(string template) + { + return ComputeRoutePattern(template, RoutePrecedence.ComputeInbound); + } + + protected override decimal ComputeGenerated(string template) + { + return ComputeRoutePattern(template, RoutePrecedence.ComputeOutbound); + } + + private static decimal ComputeRoutePattern(string template, Func func) + { + var parsed = RoutePatternFactory.Parse(template); + return func(parsed); + } + + [Fact] + public void InboundPrecedence_ParameterWithRequiredValue_HasPrecedence() { - protected override decimal ComputeMatched(string template) - { - return ComputeRoutePattern(template, RoutePrecedence.ComputeInbound); - } - - protected override decimal ComputeGenerated(string template) - { - return ComputeRoutePattern(template, RoutePrecedence.ComputeOutbound); - } - - private static decimal ComputeRoutePattern(string template, Func func) - { - var parsed = RoutePatternFactory.Parse(template); - return func(parsed); - } - - [Fact] - public void InboundPrecedence_ParameterWithRequiredValue_HasPrecedence() - { - var parameterPrecedence = RoutePatternFactory.Parse( - "{controller}").InboundPrecedence; - - var requiredValueParameterPrecedence = RoutePatternFactory.Parse( - "{controller}", - defaults: null, - parameterPolicies: null, - requiredValues: new { controller = "Home" }).InboundPrecedence; - - Assert.True(requiredValueParameterPrecedence < parameterPrecedence); - } + var parameterPrecedence = RoutePatternFactory.Parse( + "{controller}").InboundPrecedence; + + var requiredValueParameterPrecedence = RoutePatternFactory.Parse( + "{controller}", + defaults: null, + parameterPolicies: null, + requiredValues: new { controller = "Home" }).InboundPrecedence; + + Assert.True(requiredValueParameterPrecedence < parameterPrecedence); } } diff --git a/src/Http/Routing/test/UnitTests/Template/RoutePrecedenceTestsBase.cs b/src/Http/Routing/test/UnitTests/Template/RoutePrecedenceTestsBase.cs index 545564daa3..0fd8ba8902 100644 --- a/src/Http/Routing/test/UnitTests/Template/RoutePrecedenceTestsBase.cs +++ b/src/Http/Routing/test/UnitTests/Template/RoutePrecedenceTestsBase.cs @@ -4,128 +4,127 @@ using System; using Xunit; -namespace Microsoft.AspNetCore.Routing.Template +namespace Microsoft.AspNetCore.Routing.Template; + +public abstract class RoutePrecedenceTestsBase { - public abstract class RoutePrecedenceTestsBase + [Theory] + [InlineData("Employees/{id}", "Employees/{employeeId}")] + [InlineData("abc", "def")] + [InlineData("{x:alpha}", "{x:int}")] + public void ComputeMatched_IsEqual(string xTemplate, string yTemplate) { - [Theory] - [InlineData("Employees/{id}", "Employees/{employeeId}")] - [InlineData("abc", "def")] - [InlineData("{x:alpha}", "{x:int}")] - public void ComputeMatched_IsEqual(string xTemplate, string yTemplate) - { - // Arrange & Act - var xPrededence = ComputeMatched(xTemplate); - var yPrededence = ComputeMatched(yTemplate); - - // Assert - Assert.Equal(xPrededence, yPrededence); - } - - [Theory] - [InlineData("Employees/{id}", "Employees/{employeeId}")] - [InlineData("abc", "def")] - [InlineData("{x:alpha}", "{x:int}")] - public void ComputeGenerated_IsEqual(string xTemplate, string yTemplate) - { - // Arrange & Act - var xPrededence = ComputeGenerated(xTemplate); - var yPrededence = ComputeGenerated(yTemplate); - - // Assert - Assert.Equal(xPrededence, yPrededence); - } - - [Theory] - [InlineData("abc", "a{x}")] - [InlineData("abc", "{x}c")] - [InlineData("abc", "{x:int}")] - [InlineData("abc", "{x}")] - [InlineData("abc", "{*x}")] - [InlineData("{x:int}", "{x}")] - [InlineData("{x:int}", "{*x}")] - [InlineData("a{x}", "{x}")] - [InlineData("{x}c", "{x}")] - [InlineData("a{x}", "{*x}")] - [InlineData("{x}c", "{*x}")] - [InlineData("{x}", "{*x}")] - [InlineData("{*x:maxlength(10)}", "{*x}")] - [InlineData("abc/def", "abc/{x:int}")] - [InlineData("abc/def", "abc/{x}")] - [InlineData("abc/def", "abc/{*x}")] - [InlineData("abc/{x:int}", "abc/{x}")] - [InlineData("abc/{x:int}", "abc/{*x}")] - [InlineData("abc/{x}", "abc/{*x}")] - [InlineData("{x}/{y:int}", "{x}/{y}")] - public void ComputeMatched_IsLessThan(string xTemplate, string yTemplate) - { - // Arrange & Act - var xPrededence = ComputeMatched(xTemplate); - var yPrededence = ComputeMatched(yTemplate); - - // Assert - Assert.True(xPrededence < yPrededence); - } - - [Theory] - [InlineData("abc", "a{x}")] - [InlineData("abc", "{x}c")] - [InlineData("abc", "{x:int}")] - [InlineData("abc", "{x}")] - [InlineData("abc", "{*x}")] - [InlineData("{x:int}", "{x}")] - [InlineData("{x:int}", "{*x}")] - [InlineData("a{x}", "{x}")] - [InlineData("{x}c", "{x}")] - [InlineData("a{x}", "{*x}")] - [InlineData("{x}c", "{*x}")] - [InlineData("{x}", "{*x}")] - [InlineData("{*x:maxlength(10)}", "{*x}")] - [InlineData("abc/def", "abc/{x:int}")] - [InlineData("abc/def", "abc/{x}")] - [InlineData("abc/def", "abc/{*x}")] - [InlineData("abc/{x:int}", "abc/{x}")] - [InlineData("abc/{x:int}", "abc/{*x}")] - [InlineData("abc/{x}", "abc/{*x}")] - [InlineData("{x}/{y:int}", "{x}/{y}")] - public void ComputeGenerated_IsGreaterThan(string xTemplate, string yTemplate) - { - // Arrange & Act - var xPrecedence = ComputeGenerated(xTemplate); - var yPrecedence = ComputeGenerated(yTemplate); + // Arrange & Act + var xPrededence = ComputeMatched(xTemplate); + var yPrededence = ComputeMatched(yTemplate); + + // Assert + Assert.Equal(xPrededence, yPrededence); + } + + [Theory] + [InlineData("Employees/{id}", "Employees/{employeeId}")] + [InlineData("abc", "def")] + [InlineData("{x:alpha}", "{x:int}")] + public void ComputeGenerated_IsEqual(string xTemplate, string yTemplate) + { + // Arrange & Act + var xPrededence = ComputeGenerated(xTemplate); + var yPrededence = ComputeGenerated(yTemplate); - // Assert - Assert.True(xPrecedence > yPrecedence); - } + // Assert + Assert.Equal(xPrededence, yPrededence); + } + + [Theory] + [InlineData("abc", "a{x}")] + [InlineData("abc", "{x}c")] + [InlineData("abc", "{x:int}")] + [InlineData("abc", "{x}")] + [InlineData("abc", "{*x}")] + [InlineData("{x:int}", "{x}")] + [InlineData("{x:int}", "{*x}")] + [InlineData("a{x}", "{x}")] + [InlineData("{x}c", "{x}")] + [InlineData("a{x}", "{*x}")] + [InlineData("{x}c", "{*x}")] + [InlineData("{x}", "{*x}")] + [InlineData("{*x:maxlength(10)}", "{*x}")] + [InlineData("abc/def", "abc/{x:int}")] + [InlineData("abc/def", "abc/{x}")] + [InlineData("abc/def", "abc/{*x}")] + [InlineData("abc/{x:int}", "abc/{x}")] + [InlineData("abc/{x:int}", "abc/{*x}")] + [InlineData("abc/{x}", "abc/{*x}")] + [InlineData("{x}/{y:int}", "{x}/{y}")] + public void ComputeMatched_IsLessThan(string xTemplate, string yTemplate) + { + // Arrange & Act + var xPrededence = ComputeMatched(xTemplate); + var yPrededence = ComputeMatched(yTemplate); - [Fact] - public void ComputeGenerated_TooManySegments_ThrowHumaneError() + // Assert + Assert.True(xPrededence < yPrededence); + } + + [Theory] + [InlineData("abc", "a{x}")] + [InlineData("abc", "{x}c")] + [InlineData("abc", "{x:int}")] + [InlineData("abc", "{x}")] + [InlineData("abc", "{*x}")] + [InlineData("{x:int}", "{x}")] + [InlineData("{x:int}", "{*x}")] + [InlineData("a{x}", "{x}")] + [InlineData("{x}c", "{x}")] + [InlineData("a{x}", "{*x}")] + [InlineData("{x}c", "{*x}")] + [InlineData("{x}", "{*x}")] + [InlineData("{*x:maxlength(10)}", "{*x}")] + [InlineData("abc/def", "abc/{x:int}")] + [InlineData("abc/def", "abc/{x}")] + [InlineData("abc/def", "abc/{*x}")] + [InlineData("abc/{x:int}", "abc/{x}")] + [InlineData("abc/{x:int}", "abc/{*x}")] + [InlineData("abc/{x}", "abc/{*x}")] + [InlineData("{x}/{y:int}", "{x}/{y}")] + public void ComputeGenerated_IsGreaterThan(string xTemplate, string yTemplate) + { + // Arrange & Act + var xPrecedence = ComputeGenerated(xTemplate); + var yPrecedence = ComputeGenerated(yTemplate); + + // Assert + Assert.True(xPrecedence > yPrecedence); + } + + [Fact] + public void ComputeGenerated_TooManySegments_ThrowHumaneError() + { + var ex = Assert.Throws(() => { - var ex = Assert.Throws(() => - { // Arrange & Act ComputeGenerated("{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}/{r}/{s}/{t}/{u}/{v}/{w}/{x}/{y}/{z}/{a2}/{b2}/{b3}"); - }); + }); - // Assert - Assert.Equal("Route exceeds the maximum number of allowed segments of 28 and is unable to be processed.", ex.Message); - } + // Assert + Assert.Equal("Route exceeds the maximum number of allowed segments of 28 and is unable to be processed.", ex.Message); + } - [Fact] - public void ComputeMatched_TooManySegments_ThrowHumaneError() + [Fact] + public void ComputeMatched_TooManySegments_ThrowHumaneError() + { + var ex = Assert.Throws(() => { - var ex = Assert.Throws(() => - { // Arrange & Act ComputeMatched("{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}/{r}/{s}/{t}/{u}/{v}/{w}/{x}/{y}/{z}/{a2}/{b2}/{b3}"); - }); + }); - // Assert - Assert.Equal("Route exceeds the maximum number of allowed segments of 28 and is unable to be processed.", ex.Message); - } + // Assert + Assert.Equal("Route exceeds the maximum number of allowed segments of 28 and is unable to be processed.", ex.Message); + } - protected abstract decimal ComputeMatched(string template); + protected abstract decimal ComputeMatched(string template); - protected abstract decimal ComputeGenerated(string template); - } + protected abstract decimal ComputeGenerated(string template); } diff --git a/src/Http/Routing/test/UnitTests/Template/RouteTemplatePrecedenceTests.cs b/src/Http/Routing/test/UnitTests/Template/RouteTemplatePrecedenceTests.cs index 2dab105abf..184f4f4a72 100644 --- a/src/Http/Routing/test/UnitTests/Template/RouteTemplatePrecedenceTests.cs +++ b/src/Http/Routing/test/UnitTests/Template/RouteTemplatePrecedenceTests.cs @@ -3,24 +3,23 @@ using System; -namespace Microsoft.AspNetCore.Routing.Template +namespace Microsoft.AspNetCore.Routing.Template; + +public class RouteTemplatePrecedenceTests : RoutePrecedenceTestsBase { - public class RouteTemplatePrecedenceTests : RoutePrecedenceTestsBase + protected override decimal ComputeMatched(string template) { - protected override decimal ComputeMatched(string template) - { - return ComputeRouteTemplate(template, RoutePrecedence.ComputeInbound); - } + return ComputeRouteTemplate(template, RoutePrecedence.ComputeInbound); + } - protected override decimal ComputeGenerated(string template) - { - return ComputeRouteTemplate(template, RoutePrecedence.ComputeOutbound); - } + protected override decimal ComputeGenerated(string template) + { + return ComputeRouteTemplate(template, RoutePrecedence.ComputeOutbound); + } - private static decimal ComputeRouteTemplate(string template, Func func) - { - var parsed = TemplateParser.Parse(template); - return func(parsed); - } + private static decimal ComputeRouteTemplate(string template, Func func) + { + var parsed = TemplateParser.Parse(template); + return func(parsed); } } diff --git a/src/Http/Routing/test/UnitTests/Template/TemplateBinderTests.cs b/src/Http/Routing/test/UnitTests/Template/TemplateBinderTests.cs index ffb585da50..ebc8fd4fcf 100644 --- a/src/Http/Routing/test/UnitTests/Template/TemplateBinderTests.cs +++ b/src/Http/Routing/test/UnitTests/Template/TemplateBinderTests.cs @@ -13,13 +13,13 @@ using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Xunit; -namespace Microsoft.AspNetCore.Routing.Template.Tests +namespace Microsoft.AspNetCore.Routing.Template.Tests; + +public class TemplateBinderTests { - public class TemplateBinderTests - { - public static TheoryData EmptyAndNullDefaultValues => - new TheoryData - { + public static TheoryData EmptyAndNullDefaultValues => + new TheoryData + { { "Test/{val1}/{val2}", new RouteValueDictionary(new {val1 = "", val2 = ""}), @@ -104,85 +104,85 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests new RouteValueDictionary(new {val1 = "42", val2 = "11"}), "/Test/42/11" }, - }; - - [Theory] - [MemberData(nameof(EmptyAndNullDefaultValues))] - public void Binding_WithEmptyAndNull_DefaultValues( - string template, - RouteValueDictionary defaults, - RouteValueDictionary values, - string expected) - { - // Arrange - var binder = new TemplateBinder( - UrlEncoder.Default, - new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), - TemplateParser.Parse(template), - defaults); - - // Act & Assert - var result = binder.GetValues(ambientValues: null, values: values); - if (result == null) - { - if (expected == null) - { - return; - } - else - { - Assert.NotNull(result); - } - } + }; + + [Theory] + [MemberData(nameof(EmptyAndNullDefaultValues))] + public void Binding_WithEmptyAndNull_DefaultValues( + string template, + RouteValueDictionary defaults, + RouteValueDictionary values, + string expected) + { + // Arrange + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + TemplateParser.Parse(template), + defaults); - var boundTemplate = binder.BindValues(result.AcceptedValues); + // Act & Assert + var result = binder.GetValues(ambientValues: null, values: values); + if (result == null) + { if (expected == null) { - Assert.Null(boundTemplate); + return; } else { - Assert.NotNull(boundTemplate); - Assert.Equal(expected, boundTemplate); + Assert.NotNull(result); } } - [Fact] - public void GetVirtualPathWithMultiSegmentParamsOnBothEndsMatches() + var boundTemplate = binder.BindValues(result.AcceptedValues); + if (expected == null) { - RunTest( - "language/{lang}-{region}", - null, - new RouteValueDictionary(new { lang = "en", region = "US" }), - new RouteValueDictionary(new { lang = "xx", region = "yy" }), - "/language/xx-yy"); + Assert.Null(boundTemplate); } - - [Fact] - public void GetVirtualPathWithMultiSegmentParamsOnLeftEndMatches() + else { - RunTest( - "language/{lang}-{region}a", - null, - new RouteValueDictionary(new { lang = "en", region = "US" }), - new RouteValueDictionary(new { lang = "xx", region = "yy" }), - "/language/xx-yya"); + Assert.NotNull(boundTemplate); + Assert.Equal(expected, boundTemplate); } + } - [Fact] - public void GetVirtualPathWithMultiSegmentParamsOnRightEndMatches() - { - RunTest( - "language/a{lang}-{region}", - null, - new RouteValueDictionary(new { lang = "en", region = "US" }), - new RouteValueDictionary(new { lang = "xx", region = "yy" }), - "/language/axx-yy"); - } + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}-{region}", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "/language/xx-yy"); + } - public static TheoryData OptionalParamValues => - new TheoryData - { + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-{region}a", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "/language/xx-yya"); + } + + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}-{region}", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "/language/axx-yy"); + } + + public static TheoryData OptionalParamValues => + new TheoryData + { // defaults // ambient values // values @@ -252,487 +252,487 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests "/Test/someval1.someval2?" + "val3=someval3" }, - }; - - [Theory] - [MemberData(nameof(OptionalParamValues))] - public void GetVirtualPathWithMultiSegmentWithOptionalParam( - string template, - RouteValueDictionary defaults, - RouteValueDictionary ambientValues, - RouteValueDictionary values, - string expected) - { - // Arrange - var binder = new TemplateBinder( - UrlEncoder.Default, - new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), - TemplateParser.Parse(template), - defaults); - - // Act & Assert - var result = binder.GetValues(ambientValues: ambientValues, values: values); - if (result == null) - { - if (expected == null) - { - return; - } - else - { - Assert.NotNull(result); - } - } + }; + + [Theory] + [MemberData(nameof(OptionalParamValues))] + public void GetVirtualPathWithMultiSegmentWithOptionalParam( + string template, + RouteValueDictionary defaults, + RouteValueDictionary ambientValues, + RouteValueDictionary values, + string expected) + { + // Arrange + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + TemplateParser.Parse(template), + defaults); - var boundTemplate = binder.BindValues(result.AcceptedValues); + // Act & Assert + var result = binder.GetValues(ambientValues: ambientValues, values: values); + if (result == null) + { if (expected == null) { - Assert.Null(boundTemplate); + return; } else { - Assert.NotNull(boundTemplate); - Assert.Equal(expected, boundTemplate); + Assert.NotNull(result); } } - [Fact] - public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndMatches() + var boundTemplate = binder.BindValues(result.AcceptedValues); + if (expected == null) { - RunTest( - "language/a{lang}-{region}a", - null, - new RouteValueDictionary(new { lang = "en", region = "US" }), - new RouteValueDictionary(new { lang = "xx", region = "yy" }), - "/language/axx-yya"); - } - - [Fact] - public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch() - { - RunTest( - "language/a{lang}-{region}a", - null, - new RouteValueDictionary(new { lang = "en", region = "US" }), - new RouteValueDictionary(new { lang = "", region = "yy" }), - null); - } - - [Fact] - public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch2() - { - RunTest( - "language/a{lang}-{region}a", - null, - new RouteValueDictionary(new { lang = "en", region = "US" }), - new RouteValueDictionary(new { lang = "xx", region = "" }), - null); - } - - [Fact] - public void GetVirtualPathWithSimpleMultiSegmentParamsOnBothEndsMatches() - { - RunTest( - "language/{lang}", - null, - new RouteValueDictionary(new { lang = "en" }), - new RouteValueDictionary(new { lang = "xx" }), - "/language/xx"); - } - - [Fact] - public void GetVirtualPathWithSimpleMultiSegmentParamsOnLeftEndMatches() - { - RunTest( - "language/{lang}-", - null, - new RouteValueDictionary(new { lang = "en" }), - new RouteValueDictionary(new { lang = "xx" }), - "/language/xx-"); - } - - [Fact] - public void GetVirtualPathWithSimpleMultiSegmentParamsOnRightEndMatches() - { - RunTest( - "language/a{lang}", - null, - new RouteValueDictionary(new { lang = "en" }), - new RouteValueDictionary(new { lang = "xx" }), - "/language/axx"); + Assert.Null(boundTemplate); } - - [Fact] - public void GetVirtualPathWithSimpleMultiSegmentParamsOnNeitherEndMatches() + else { - RunTest( - "language/a{lang}a", - null, - new RouteValueDictionary(new { lang = "en" }), - new RouteValueDictionary(new { lang = "xx" }), - "/language/axxa"); + Assert.NotNull(boundTemplate); + Assert.Equal(expected, boundTemplate); } + } - [Fact] - public void GetVirtualPathWithMultiSegmentStandardMvcRouteMatches() - { - RunTest( - "{controller}.mvc/{action}/{id}", - new RouteValueDictionary(new { action = "Index", id = (string)null }), - new RouteValueDictionary(new { controller = "home", action = "list", id = (string)null }), - new RouteValueDictionary(new { controller = "products" }), - "/products.mvc"); - } + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}-{region}a", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + "/language/axx-yya"); + } - [Fact] - public void GetVirtualPathWithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() - { - RunTest( - "language/{lang}-{region}", - new RouteValueDictionary(new { lang = "xx", region = "yy" }), - new RouteValueDictionary(new { lang = "en", region = "US" }), - new RouteValueDictionary(new { lang = "zz" }), - "/language/zz-yy"); - } + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch() + { + RunTest( + "language/a{lang}-{region}a", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "", region = "yy" }), + null); + } - [Fact] - public void GetUrlWithDefaultValue() - { - // URL should be found but excluding the 'id' parameter, which has only a default value. - RunTest( - "{controller}/{action}/{id}", - new RouteValueDictionary(new { id = "defaultid" }), - new RouteValueDictionary(new { controller = "home", action = "oldaction" }), - new RouteValueDictionary(new { action = "newaction" }), - "/home/newaction"); - } + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnNeitherEndDoesNotMatch2() + { + RunTest( + "language/a{lang}-{region}a", + null, + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "xx", region = "" }), + null); + } - [Fact] - public void GetVirtualPathWithEmptyStringRequiredValueReturnsNull() - { - RunTest( - "foo/{controller}", - null, - new RouteValueDictionary(new { }), - new RouteValueDictionary(new { controller = "" }), - null); - } + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}", + null, + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "/language/xx"); + } - [Fact] - public void GetVirtualPathWithNullRequiredValueReturnsNull() - { - RunTest( - "foo/{controller}", - null, - new RouteValueDictionary(new { }), - new RouteValueDictionary(new { controller = (string)null }), - null); - } + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-", + null, + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "/language/xx-"); + } - [Fact] - public void GetVirtualPathWithRequiredValueReturnsPath() - { - RunTest( - "foo/{controller}", - null, - new RouteValueDictionary(new { }), - new RouteValueDictionary(new { controller = "home" }), - "/foo/home"); - } + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}", + null, + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "/language/axx"); + } - [Fact] - public void GetUrlWithNullDefaultValue() - { - // URL should be found but excluding the 'id' parameter, which has only a default value. - RunTest( - "{controller}/{action}/{id}", - new RouteValueDictionary(new { id = (string)null }), - new RouteValueDictionary(new { controller = "home", action = "oldaction", id = (string)null }), - new RouteValueDictionary(new { action = "newaction" }), - "/home/newaction"); - } + [Fact] + public void GetVirtualPathWithSimpleMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}a", + null, + new RouteValueDictionary(new { lang = "en" }), + new RouteValueDictionary(new { lang = "xx" }), + "/language/axxa"); + } - [Fact] - public void GetVirtualPathCanFillInSeparatedParametersWithDefaultValues() - { - RunTest( - "{controller}/{language}-{locale}", - new RouteValueDictionary(new { language = "en", locale = "US" }), - new RouteValueDictionary(), - new RouteValueDictionary(new { controller = "Orders" }), - "/Orders/en-US"); - } + [Fact] + public void GetVirtualPathWithMultiSegmentStandardMvcRouteMatches() + { + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { action = "Index", id = (string)null }), + new RouteValueDictionary(new { controller = "home", action = "list", id = (string)null }), + new RouteValueDictionary(new { controller = "products" }), + "/products.mvc"); + } - [Fact] - public void GetVirtualPathWithUnusedNullValueShouldGenerateUrlAndIgnoreNullValue() - { - RunTest( - "{controller}.mvc/{action}/{id}", - new RouteValueDictionary(new { action = "Index", id = "" }), - new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }), - new RouteValueDictionary(new { controller = "Home", action = "TestAction", id = "1", format = (string)null }), - "/Home.mvc/TestAction/1"); - } + [Fact] + public void GetVirtualPathWithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() + { + RunTest( + "language/{lang}-{region}", + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + new RouteValueDictionary(new { lang = "en", region = "US" }), + new RouteValueDictionary(new { lang = "zz" }), + "/language/zz-yy"); + } - [Fact] - public void GetUrlWithMissingValuesDoesntMatch() - { - RunTest( - "{controller}/{action}/{id}", - null, - new { controller = "home", action = "oldaction" }, - new { action = "newaction" }, - null); - } + [Fact] + public void GetUrlWithDefaultValue() + { + // URL should be found but excluding the 'id' parameter, which has only a default value. + RunTest( + "{controller}/{action}/{id}", + new RouteValueDictionary(new { id = "defaultid" }), + new RouteValueDictionary(new { controller = "home", action = "oldaction" }), + new RouteValueDictionary(new { action = "newaction" }), + "/home/newaction"); + } - [Fact] - public void GetUrlWithEmptyRequiredValuesReturnsNull() - { - RunTest( - "{p1}/{p2}/{p3}", - null, - new { p1 = "v1", }, - new { p2 = "", p3 = "" }, - null); - } + [Fact] + public void GetVirtualPathWithEmptyStringRequiredValueReturnsNull() + { + RunTest( + "foo/{controller}", + null, + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = "" }), + null); + } - [Fact] - public void GetUrlWithEmptyOptionalValuesReturnsShortUrl() - { - RunTest( - "{p1}/{p2}/{p3}", - new { p2 = "d2", p3 = "d3" }, - new { p1 = "v1", }, - new { p2 = "", p3 = "" }, - "/v1"); - } + [Fact] + public void GetVirtualPathWithNullRequiredValueReturnsNull() + { + RunTest( + "foo/{controller}", + null, + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = (string)null }), + null); + } - [Fact] - public void GetUrlShouldIgnoreValuesAfterChangedParameter() - { - RunTest( - "{controller}/{action}/{id}", - new { action = "Index", id = (string)null }, - new { controller = "orig", action = "init", id = "123" }, - new { action = "new", }, - "/orig/new"); - } + [Fact] + public void GetVirtualPathWithRequiredValueReturnsPath() + { + RunTest( + "foo/{controller}", + null, + new RouteValueDictionary(new { }), + new RouteValueDictionary(new { controller = "home" }), + "/foo/home"); + } - [Fact] - public void GetUrlWithNullForMiddleParameterIgnoresRemainingParameters() - { - RunTest( - "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}", - new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" }, - new { controller = "UrlRouting", action = "Play", category = "Photos", year = "2008", occasion = "Easter", SafeParam = "SafeParamValue" }, - new { year = (string)null, occasion = "Hola" }, - "/UrlGeneration1/UrlRouting.mvc/Play/" - + "Photos/1995/Hola"); - } + [Fact] + public void GetUrlWithNullDefaultValue() + { + // URL should be found but excluding the 'id' parameter, which has only a default value. + RunTest( + "{controller}/{action}/{id}", + new RouteValueDictionary(new { id = (string)null }), + new RouteValueDictionary(new { controller = "home", action = "oldaction", id = (string)null }), + new RouteValueDictionary(new { action = "newaction" }), + "/home/newaction"); + } - [Fact] - public void GetUrlWithEmptyStringForMiddleParameterIgnoresRemainingParameters() - { - var ambientValues = new RouteValueDictionary(); - ambientValues.Add("controller", "UrlRouting"); - ambientValues.Add("action", "Play"); - ambientValues.Add("category", "Photos"); - ambientValues.Add("year", "2008"); - ambientValues.Add("occasion", "Easter"); - ambientValues.Add("SafeParam", "SafeParamValue"); - - var values = new RouteValueDictionary(); - values.Add("year", String.Empty); - values.Add("occasion", "Hola"); + [Fact] + public void GetVirtualPathCanFillInSeparatedParametersWithDefaultValues() + { + RunTest( + "{controller}/{language}-{locale}", + new RouteValueDictionary(new { language = "en", locale = "US" }), + new RouteValueDictionary(), + new RouteValueDictionary(new { controller = "Orders" }), + "/Orders/en-US"); + } - RunTest( - "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}", - new RouteValueDictionary(new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" }), - ambientValues, - values, - "/UrlGeneration1/UrlRouting.mvc/" - + "Play/Photos/1995/Hola"); - } + [Fact] + public void GetVirtualPathWithUnusedNullValueShouldGenerateUrlAndIgnoreNullValue() + { + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { action = "Index", id = "" }), + new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }), + new RouteValueDictionary(new { controller = "Home", action = "TestAction", id = "1", format = (string)null }), + "/Home.mvc/TestAction/1"); + } - [Fact] - public void GetUrlWithEmptyStringForMiddleParameterShouldUseDefaultValue() - { - var ambientValues = new RouteValueDictionary(); - ambientValues.Add("Controller", "Test"); - ambientValues.Add("Action", "Fallback"); - ambientValues.Add("param1", "fallback1"); - ambientValues.Add("param2", "fallback2"); - ambientValues.Add("param3", "fallback3"); + [Fact] + public void GetUrlWithMissingValuesDoesntMatch() + { + RunTest( + "{controller}/{action}/{id}", + null, + new { controller = "home", action = "oldaction" }, + new { action = "newaction" }, + null); + } - var values = new RouteValueDictionary(); - values.Add("controller", "subtest"); - values.Add("param1", "b"); + [Fact] + public void GetUrlWithEmptyRequiredValuesReturnsNull() + { + RunTest( + "{p1}/{p2}/{p3}", + null, + new { p1 = "v1", }, + new { p2 = "", p3 = "" }, + null); + } - RunTest( - "{controller}.mvc/{action}/{param1}", - new RouteValueDictionary(new { action = "Default" }), - ambientValues, - values, - "/subtest.mvc/Default/b"); - } + [Fact] + public void GetUrlWithEmptyOptionalValuesReturnsShortUrl() + { + RunTest( + "{p1}/{p2}/{p3}", + new { p2 = "d2", p3 = "d3" }, + new { p1 = "v1", }, + new { p2 = "", p3 = "" }, + "/v1"); + } - [Fact] - public void GetUrlVerifyEncoding() - { - var values = new RouteValueDictionary(); - values.Add("controller", "#;?:@&=+$,"); - values.Add("action", "showcategory"); - values.Add("id", 123); - values.Add("so?rt", "de?sc"); - values.Add("maxPrice", 100); + [Fact] + public void GetUrlShouldIgnoreValuesAfterChangedParameter() + { + RunTest( + "{controller}/{action}/{id}", + new { action = "Index", id = (string)null }, + new { controller = "orig", action = "init", id = "123" }, + new { action = "new", }, + "/orig/new"); + } - RunTest( - "{controller}.mvc/{action}/{id}", - new RouteValueDictionary(new { controller = "Home" }), - new RouteValueDictionary(new { controller = "home", action = "Index", id = (string)null }), - values, - "/%23;%3F%3A@%26%3D%2B$,.mvc/showcategory/123?so%3Frt=de%3Fsc&maxPrice=100"); - } + [Fact] + public void GetUrlWithNullForMiddleParameterIgnoresRemainingParameters() + { + RunTest( + "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}", + new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" }, + new { controller = "UrlRouting", action = "Play", category = "Photos", year = "2008", occasion = "Easter", SafeParam = "SafeParamValue" }, + new { year = (string)null, occasion = "Hola" }, + "/UrlGeneration1/UrlRouting.mvc/Play/" + + "Photos/1995/Hola"); + } - [Fact] - public void GetUrlGeneratesQueryStringForNewValuesAndEscapesQueryString() - { - var values = new RouteValueDictionary(new { controller = "products", action = "showcategory", id = 123, maxPrice = 100 }); - values.Add("so?rt", "de?sc"); + [Fact] + public void GetUrlWithEmptyStringForMiddleParameterIgnoresRemainingParameters() + { + var ambientValues = new RouteValueDictionary(); + ambientValues.Add("controller", "UrlRouting"); + ambientValues.Add("action", "Play"); + ambientValues.Add("category", "Photos"); + ambientValues.Add("year", "2008"); + ambientValues.Add("occasion", "Easter"); + ambientValues.Add("SafeParam", "SafeParamValue"); + + var values = new RouteValueDictionary(); + values.Add("year", String.Empty); + values.Add("occasion", "Hola"); + + RunTest( + "UrlGeneration1/{controller}.mvc/{action}/{category}/{year}/{occasion}/{SafeParam}", + new RouteValueDictionary(new { year = 1995, occasion = "Christmas", action = "Play", SafeParam = "SafeParamValue" }), + ambientValues, + values, + "/UrlGeneration1/UrlRouting.mvc/" + + "Play/Photos/1995/Hola"); + } - RunTest( - "{controller}.mvc/{action}/{id}", - new RouteValueDictionary(new { controller = "Home" }), - new RouteValueDictionary(new { controller = "home", action = "Index", id = (string)null }), - values, - "/products.mvc/showcategory/123" + - "?so%3Frt=de%3Fsc&maxPrice=100"); - } + [Fact] + public void GetUrlWithEmptyStringForMiddleParameterShouldUseDefaultValue() + { + var ambientValues = new RouteValueDictionary(); + ambientValues.Add("Controller", "Test"); + ambientValues.Add("Action", "Fallback"); + ambientValues.Add("param1", "fallback1"); + ambientValues.Add("param2", "fallback2"); + ambientValues.Add("param3", "fallback3"); + + var values = new RouteValueDictionary(); + values.Add("controller", "subtest"); + values.Add("param1", "b"); + + RunTest( + "{controller}.mvc/{action}/{param1}", + new RouteValueDictionary(new { action = "Default" }), + ambientValues, + values, + "/subtest.mvc/Default/b"); + } - [Fact] - public void GetUrlGeneratesQueryStringForNewValuesButIgnoresNewValuesThatMatchDefaults() - { - RunTest( - "{controller}.mvc/{action}/{id}", - new RouteValueDictionary(new { controller = "Home", Custom = "customValue" }), - new RouteValueDictionary(new { controller = "Home", action = "Index", id = (string)null }), - new RouteValueDictionary( - new - { - controller = "products", - action = "showcategory", - id = 123, - sort = "desc", - maxPrice = 100, - custom = "customValue" - }), - "/products.mvc/showcategory/123" + - "?sort=desc&maxPrice=100"); - } + [Fact] + public void GetUrlVerifyEncoding() + { + var values = new RouteValueDictionary(); + values.Add("controller", "#;?:@&=+$,"); + values.Add("action", "showcategory"); + values.Add("id", 123); + values.Add("so?rt", "de?sc"); + values.Add("maxPrice", 100); + + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { controller = "Home" }), + new RouteValueDictionary(new { controller = "home", action = "Index", id = (string)null }), + values, + "/%23;%3F%3A@%26%3D%2B$,.mvc/showcategory/123?so%3Frt=de%3Fsc&maxPrice=100"); + } - [Fact] - public void GetVirtualPathEncodesParametersAndLiterals() - { - RunTest( - "bl%og/{controller}/he llo/{action}", - null, - new RouteValueDictionary(new { controller = "ho%me", action = "li st" }), - new RouteValueDictionary(), - "/bl%25og/ho%25me/he%20llo/li%20st"); - } + [Fact] + public void GetUrlGeneratesQueryStringForNewValuesAndEscapesQueryString() + { + var values = new RouteValueDictionary(new { controller = "products", action = "showcategory", id = 123, maxPrice = 100 }); + values.Add("so?rt", "de?sc"); + + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { controller = "Home" }), + new RouteValueDictionary(new { controller = "home", action = "Index", id = (string)null }), + values, + "/products.mvc/showcategory/123" + + "?so%3Frt=de%3Fsc&maxPrice=100"); + } - [Fact] - public void GetVirtualDoesNotEncodeLeadingSlashes() - { - RunTest( - "{controller}/{action}", - null, - new RouteValueDictionary(new { controller = "/home", action = "/my/index" }), - new RouteValueDictionary(), - "/home/%2Fmy%2Findex"); - } + [Fact] + public void GetUrlGeneratesQueryStringForNewValuesButIgnoresNewValuesThatMatchDefaults() + { + RunTest( + "{controller}.mvc/{action}/{id}", + new RouteValueDictionary(new { controller = "Home", Custom = "customValue" }), + new RouteValueDictionary(new { controller = "Home", action = "Index", id = (string)null }), + new RouteValueDictionary( + new + { + controller = "products", + action = "showcategory", + id = 123, + sort = "desc", + maxPrice = 100, + custom = "customValue" + }), + "/products.mvc/showcategory/123" + + "?sort=desc&maxPrice=100"); + } - [Fact] - public void GetUrlWithCatchAllWithValue() - { - RunTest( - "{p1}/{*p2}", - new RouteValueDictionary(new { id = "defaultid" }), - new RouteValueDictionary(new { p1 = "v1" }), - new RouteValueDictionary(new { p2 = "v2a/v2b" }), - "/v1/v2a%2Fv2b"); - } + [Fact] + public void GetVirtualPathEncodesParametersAndLiterals() + { + RunTest( + "bl%og/{controller}/he llo/{action}", + null, + new RouteValueDictionary(new { controller = "ho%me", action = "li st" }), + new RouteValueDictionary(), + "/bl%25og/ho%25me/he%20llo/li%20st"); + } - [Fact] - public void GetUrlWithCatchAllWithEmptyValue() - { - RunTest( - "{p1}/{*p2}", - new RouteValueDictionary(new { id = "defaultid" }), - new RouteValueDictionary(new { p1 = "v1" }), - new RouteValueDictionary(new { p2 = "" }), - "/v1"); - } + [Fact] + public void GetVirtualDoesNotEncodeLeadingSlashes() + { + RunTest( + "{controller}/{action}", + null, + new RouteValueDictionary(new { controller = "/home", action = "/my/index" }), + new RouteValueDictionary(), + "/home/%2Fmy%2Findex"); + } - [Fact] - public void GetUrlWithCatchAllWithNullValue() - { - RunTest( - "{p1}/{*p2}", - new RouteValueDictionary(new { id = "defaultid" }), - new RouteValueDictionary(new { p1 = "v1" }), - new RouteValueDictionary(new { p2 = (string)null }), - "/v1"); - } + [Fact] + public void GetUrlWithCatchAllWithValue() + { + RunTest( + "{p1}/{*p2}", + new RouteValueDictionary(new { id = "defaultid" }), + new RouteValueDictionary(new { p1 = "v1" }), + new RouteValueDictionary(new { p2 = "v2a/v2b" }), + "/v1/v2a%2Fv2b"); + } - [Fact] - public void GetUrlWithLeadingTildeSlash() - { - RunTest( - "~/foo", - null, - null, - new RouteValueDictionary(new { }), - "/foo"); - } + [Fact] + public void GetUrlWithCatchAllWithEmptyValue() + { + RunTest( + "{p1}/{*p2}", + new RouteValueDictionary(new { id = "defaultid" }), + new RouteValueDictionary(new { p1 = "v1" }), + new RouteValueDictionary(new { p2 = "" }), + "/v1"); + } - [Fact] - public void GetUrlWithLeadingSlash() - { - RunTest( - "/foo", - null, - null, - new RouteValueDictionary(new { }), - "/foo"); - } + [Fact] + public void GetUrlWithCatchAllWithNullValue() + { + RunTest( + "{p1}/{*p2}", + new RouteValueDictionary(new { id = "defaultid" }), + new RouteValueDictionary(new { p1 = "v1" }), + new RouteValueDictionary(new { p2 = (string)null }), + "/v1"); + } - [Fact] - public void TemplateBinder_KeepsExplicitlySuppliedRouteValues_OnFailedRouteMatch() - { - // Arrange - var template = "{area?}/{controller=Home}/{action=Index}/{id?}"; - var binder = new TemplateBinder( - UrlEncoder.Default, - new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), - TemplateParser.Parse(template), - defaults: null); - var ambientValues = new RouteValueDictionary(); - var routeValues = new RouteValueDictionary(new { controller = "Test", action = "Index" }); + [Fact] + public void GetUrlWithLeadingTildeSlash() + { + RunTest( + "~/foo", + null, + null, + new RouteValueDictionary(new { }), + "/foo"); + } - // Act - var templateValuesResult = binder.GetValues(ambientValues, routeValues); - var boundTemplate = binder.BindValues(templateValuesResult.AcceptedValues); + [Fact] + public void GetUrlWithLeadingSlash() + { + RunTest( + "/foo", + null, + null, + new RouteValueDictionary(new { }), + "/foo"); + } - // Assert - Assert.Null(boundTemplate); - Assert.Equal(2, templateValuesResult.CombinedValues.Count); - object routeValue; - Assert.True(templateValuesResult.CombinedValues.TryGetValue("controller", out routeValue)); - Assert.Equal("Test", routeValue?.ToString()); - Assert.True(templateValuesResult.CombinedValues.TryGetValue("action", out routeValue)); - Assert.Equal("Index", routeValue?.ToString()); - } + [Fact] + public void TemplateBinder_KeepsExplicitlySuppliedRouteValues_OnFailedRouteMatch() + { + // Arrange + var template = "{area?}/{controller=Home}/{action=Index}/{id?}"; + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + TemplateParser.Parse(template), + defaults: null); + var ambientValues = new RouteValueDictionary(); + var routeValues = new RouteValueDictionary(new { controller = "Test", action = "Index" }); + + // Act + var templateValuesResult = binder.GetValues(ambientValues, routeValues); + var boundTemplate = binder.BindValues(templateValuesResult.AcceptedValues); + + // Assert + Assert.Null(boundTemplate); + Assert.Equal(2, templateValuesResult.CombinedValues.Count); + object routeValue; + Assert.True(templateValuesResult.CombinedValues.TryGetValue("controller", out routeValue)); + Assert.Equal("Test", routeValue?.ToString()); + Assert.True(templateValuesResult.CombinedValues.TryGetValue("action", out routeValue)); + Assert.Equal("Index", routeValue?.ToString()); + } #if ROUTE_COLLECTION @@ -1133,340 +1133,339 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests #endif - private static void RunTest( - string template, - RouteValueDictionary defaults, - RouteValueDictionary ambientValues, - RouteValueDictionary values, - string expected) - { - // Arrange - var binder = new TemplateBinder( - UrlEncoder.Default, - new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), - TemplateParser.Parse(template), - defaults); - - // Act & Assert - var result = binder.GetValues(ambientValues, values); - if (result == null) - { - if (expected == null) - { - return; - } - else - { - Assert.NotNull(result); - } - } + private static void RunTest( + string template, + RouteValueDictionary defaults, + RouteValueDictionary ambientValues, + RouteValueDictionary values, + string expected) + { + // Arrange + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + TemplateParser.Parse(template), + defaults); - var boundTemplate = binder.BindValues(result.AcceptedValues); + // Act & Assert + var result = binder.GetValues(ambientValues, values); + if (result == null) + { if (expected == null) { - Assert.Null(boundTemplate); + return; } else { - Assert.NotNull(boundTemplate); - - // We want to chop off the query string and compare that using an unordered comparison - var expectedParts = new PathAndQuery(expected); - var actualParts = new PathAndQuery(boundTemplate); - - Assert.Equal(expectedParts.Path, actualParts.Path); - - if (expectedParts.Parameters == null) - { - Assert.Null(actualParts.Parameters); - } - else - { - Assert.Equal(expectedParts.Parameters.Count, actualParts.Parameters.Count); - - foreach (var kvp in expectedParts.Parameters) - { - string value; - Assert.True(actualParts.Parameters.TryGetValue(kvp.Key, out value)); - Assert.Equal(kvp.Value, value); - } - } + Assert.NotNull(result); } } - private static void RunTest( - string template, - object defaults, - object ambientValues, - object values, - string expected) + var boundTemplate = binder.BindValues(result.AcceptedValues); + if (expected == null) { - RunTest( - template, - new RouteValueDictionary(defaults), - new RouteValueDictionary(ambientValues), - new RouteValueDictionary(values), - expected); + Assert.Null(boundTemplate); } - - [Theory] - [InlineData(null, null, true)] - [InlineData("", null, true)] - [InlineData(null, "", true)] - [InlineData("blog", null, false)] - [InlineData(null, "store", false)] - [InlineData("Cool", "cool", true)] - [InlineData("Co0l", "cool", false)] - public void RoutePartsEqualTest(object left, object right, bool expected) + else { - // Arrange & Act & Assert - if (expected) + Assert.NotNull(boundTemplate); + + // We want to chop off the query string and compare that using an unordered comparison + var expectedParts = new PathAndQuery(expected); + var actualParts = new PathAndQuery(boundTemplate); + + Assert.Equal(expectedParts.Path, actualParts.Path); + + if (expectedParts.Parameters == null) { - Assert.True(TemplateBinder.RoutePartsEqual(left, right)); + Assert.Null(actualParts.Parameters); } else { - Assert.False(TemplateBinder.RoutePartsEqual(left, right)); + Assert.Equal(expectedParts.Parameters.Count, actualParts.Parameters.Count); + + foreach (var kvp in expectedParts.Parameters) + { + string value; + Assert.True(actualParts.Parameters.TryGetValue(kvp.Key, out value)); + Assert.Equal(kvp.Value, value); + } } } + } - [Fact] - public void GetValues_SuccessfullyMatchesRouteValues_ForExplicitEmptyStringValue_AndNullDefault() - { - // Arrange - var expected = "/Home/Index"; - var template = "Home/Index"; - var defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", area = (string)null }); - var ambientValues = new RouteValueDictionary(new { controller = "Rail", action = "Schedule", area = "Travel" }); - var explicitValues = new RouteValueDictionary(new { controller = "Home", action = "Index", area = "" }); - var binder = new TemplateBinder( - UrlEncoder.Default, - new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), - TemplateParser.Parse(template), - defaults); - - // Act1 - var result = binder.GetValues(ambientValues, explicitValues); - - // Assert1 - Assert.NotNull(result); - - // Act2 - var boundTemplate = binder.BindValues(result.AcceptedValues); - - // Assert2 - Assert.NotNull(boundTemplate); - Assert.Equal(expected, boundTemplate); - } + private static void RunTest( + string template, + object defaults, + object ambientValues, + object values, + string expected) + { + RunTest( + template, + new RouteValueDictionary(defaults), + new RouteValueDictionary(ambientValues), + new RouteValueDictionary(values), + expected); + } - [Fact] - public void GetValues_SuccessfullyMatchesRouteValues_ForExplicitNullValue_AndEmptyStringDefault() + [Theory] + [InlineData(null, null, true)] + [InlineData("", null, true)] + [InlineData(null, "", true)] + [InlineData("blog", null, false)] + [InlineData(null, "store", false)] + [InlineData("Cool", "cool", true)] + [InlineData("Co0l", "cool", false)] + public void RoutePartsEqualTest(object left, object right, bool expected) + { + // Arrange & Act & Assert + if (expected) { - // Arrange - var expected = "/Home/Index"; - var template = "Home/Index"; - var defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", area = "" }); - var ambientValues = new RouteValueDictionary(new { controller = "Rail", action = "Schedule", area = "Travel" }); - var explicitValues = new RouteValueDictionary(new { controller = "Home", action = "Index", area = (string)null }); - var binder = new TemplateBinder( - UrlEncoder.Default, - new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), - TemplateParser.Parse(template), - defaults); - - // Act1 - var result = binder.GetValues(ambientValues, explicitValues); - - // Assert1 - Assert.NotNull(result); - - // Act2 - var boundTemplate = binder.BindValues(result.AcceptedValues); - - // Assert2 - Assert.NotNull(boundTemplate); - Assert.Equal(expected, boundTemplate); + Assert.True(TemplateBinder.RoutePartsEqual(left, right)); } - - [Fact] - public void BindValues_ParameterTransformer() + else { - // Arrange - var expected = "/ConventionalTransformerRoute/conventional-transformer/Param/my-value"; - - var template = "ConventionalTransformerRoute/conventional-transformer/Param/{param:length(500):slugify?}"; - var defaults = new RouteValueDictionary(new { controller = "ConventionalTransformer", action = "Param" }); - var ambientValues = new RouteValueDictionary(new { controller = "ConventionalTransformer", action = "Param" }); - var explicitValues = new RouteValueDictionary(new { controller = "ConventionalTransformer", action = "Param", param = "MyValue" }); - var binder = new TemplateBinder( - UrlEncoder.Default, - new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), - RoutePatternFactory.Parse( - template, - defaults, - parameterPolicies: null, - requiredValues: new { area = (string)null, action = "Param", controller = "ConventionalTransformer", page = (string)null }), - defaults, - requiredKeys: defaults.Keys, - parameterPolicies: new (string, IParameterPolicy)[] { ("param", new LengthRouteConstraint(500)), ("param", new SlugifyParameterTransformer()), }); - - // Act - var result = binder.GetValues(ambientValues, explicitValues); - var boundTemplate = binder.BindValues(result.AcceptedValues); - - // Assert - Assert.Equal(expected, boundTemplate); + Assert.False(TemplateBinder.RoutePartsEqual(left, right)); } + } - [Fact] - public void BindValues_AmbientAndExplicitValuesDoNotMatch_Success() - { - // Arrange - var expected = "/Travel/Flight"; - - var template = "{area}/{controller}/{action}"; - var defaults = new RouteValueDictionary(new { action = "Index" }); - var ambientValues = new RouteValueDictionary(new { area = "Travel", controller = "Rail", action = "Index" }); - var explicitValues = new RouteValueDictionary(new { controller = "Flight", action = "Index" }); - var binder = new TemplateBinder( - UrlEncoder.Default, - new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), - RoutePatternFactory.Parse( - template, - defaults, - parameterPolicies: null, - requiredValues: new { area = "Travel", action = "SomeAction", controller = "Flight", page = (string)null }), - defaults, - requiredKeys: new string[] { "area", "action", "controller", "page" }, - parameterPolicies: null); - - // Act - var result = binder.GetValues(ambientValues, explicitValues); - var boundTemplate = binder.BindValues(result.AcceptedValues); + [Fact] + public void GetValues_SuccessfullyMatchesRouteValues_ForExplicitEmptyStringValue_AndNullDefault() + { + // Arrange + var expected = "/Home/Index"; + var template = "Home/Index"; + var defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", area = (string)null }); + var ambientValues = new RouteValueDictionary(new { controller = "Rail", action = "Schedule", area = "Travel" }); + var explicitValues = new RouteValueDictionary(new { controller = "Home", action = "Index", area = "" }); + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + TemplateParser.Parse(template), + defaults); + + // Act1 + var result = binder.GetValues(ambientValues, explicitValues); + + // Assert1 + Assert.NotNull(result); + + // Act2 + var boundTemplate = binder.BindValues(result.AcceptedValues); + + // Assert2 + Assert.NotNull(boundTemplate); + Assert.Equal(expected, boundTemplate); + } - // Assert - Assert.Equal(expected, boundTemplate); - } + [Fact] + public void GetValues_SuccessfullyMatchesRouteValues_ForExplicitNullValue_AndEmptyStringDefault() + { + // Arrange + var expected = "/Home/Index"; + var template = "Home/Index"; + var defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", area = "" }); + var ambientValues = new RouteValueDictionary(new { controller = "Rail", action = "Schedule", area = "Travel" }); + var explicitValues = new RouteValueDictionary(new { controller = "Home", action = "Index", area = (string)null }); + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + TemplateParser.Parse(template), + defaults); + + // Act1 + var result = binder.GetValues(ambientValues, explicitValues); + + // Assert1 + Assert.NotNull(result); + + // Act2 + var boundTemplate = binder.BindValues(result.AcceptedValues); + + // Assert2 + Assert.NotNull(boundTemplate); + Assert.Equal(expected, boundTemplate); + } - [Fact] - public void BindValues_LinkingFromPageToAController_Success() - { - // Arrange - var expected = "/LG2/SomeAction"; - - var template = "{controller=Home}/{action=Index}/{id?}"; - var defaults = new RouteValueDictionary(); - var ambientValues = new RouteValueDictionary(new { page = "/LGAnotherPage", id = "17" }); - var explicitValues = new RouteValueDictionary(new { controller = "LG2", action = "SomeAction" }); - var binder = new TemplateBinder( - UrlEncoder.Default, - new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), - RoutePatternFactory.Parse( - template, - defaults, - parameterPolicies: null, - requiredValues: new { area = (string)null, action = "SomeAction", controller = "LG2", page = (string)null }), + [Fact] + public void BindValues_ParameterTransformer() + { + // Arrange + var expected = "/ConventionalTransformerRoute/conventional-transformer/Param/my-value"; + + var template = "ConventionalTransformerRoute/conventional-transformer/Param/{param:length(500):slugify?}"; + var defaults = new RouteValueDictionary(new { controller = "ConventionalTransformer", action = "Param" }); + var ambientValues = new RouteValueDictionary(new { controller = "ConventionalTransformer", action = "Param" }); + var explicitValues = new RouteValueDictionary(new { controller = "ConventionalTransformer", action = "Param", param = "MyValue" }); + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + RoutePatternFactory.Parse( + template, defaults, - requiredKeys: new string[] { "area", "action", "controller", "page" }, - parameterPolicies: null); - - // Act - var result = binder.GetValues(ambientValues, explicitValues); - var boundTemplate = binder.BindValues(result.AcceptedValues); - - // Assert - Assert.Equal(expected, boundTemplate); - } + parameterPolicies: null, + requiredValues: new { area = (string)null, action = "Param", controller = "ConventionalTransformer", page = (string)null }), + defaults, + requiredKeys: defaults.Keys, + parameterPolicies: new (string, IParameterPolicy)[] { ("param", new LengthRouteConstraint(500)), ("param", new SlugifyParameterTransformer()), }); + + // Act + var result = binder.GetValues(ambientValues, explicitValues); + var boundTemplate = binder.BindValues(result.AcceptedValues); + + // Assert + Assert.Equal(expected, boundTemplate); + } - // Regression test for dotnet/aspnetcore#4212 - // - // An ambient value should be used to satisfy a required value even if if we're discarding - // ambient values. - [Fact] - public void BindValues_LinkingFromPageToAControllerInAreaWithAmbientArea_Success() - { - // Arrange - var expected = "/Admin/LG2/SomeAction"; - - var template = "{area}/{controller=Home}/{action=Index}/{id?}"; - var defaults = new RouteValueDictionary(); - var ambientValues = new RouteValueDictionary(new { area = "Admin", page = "/LGAnotherPage", id = "17" }); - var explicitValues = new RouteValueDictionary(new { controller = "LG2", action = "SomeAction" }); - var binder = new TemplateBinder( - UrlEncoder.Default, - new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), - RoutePatternFactory.Parse( - template, - defaults, - parameterPolicies: null, - requiredValues: new { area = "Admin", action = "SomeAction", controller = "LG2", page = (string)null }), + [Fact] + public void BindValues_AmbientAndExplicitValuesDoNotMatch_Success() + { + // Arrange + var expected = "/Travel/Flight"; + + var template = "{area}/{controller}/{action}"; + var defaults = new RouteValueDictionary(new { action = "Index" }); + var ambientValues = new RouteValueDictionary(new { area = "Travel", controller = "Rail", action = "Index" }); + var explicitValues = new RouteValueDictionary(new { controller = "Flight", action = "Index" }); + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + RoutePatternFactory.Parse( + template, defaults, - requiredKeys: new string[] { "area", "action", "controller", "page" }, - parameterPolicies: null); - - // Act - var result = binder.GetValues(ambientValues, explicitValues); - var boundTemplate = binder.BindValues(result.AcceptedValues); - - // Assert - Assert.Equal(expected, boundTemplate); - } + parameterPolicies: null, + requiredValues: new { area = "Travel", action = "SomeAction", controller = "Flight", page = (string)null }), + defaults, + requiredKeys: new string[] { "area", "action", "controller", "page" }, + parameterPolicies: null); + + // Act + var result = binder.GetValues(ambientValues, explicitValues); + var boundTemplate = binder.BindValues(result.AcceptedValues); + + // Assert + Assert.Equal(expected, boundTemplate); + } - [Fact] - public void BindValues_HasUnmatchingAmbientValues_Discard() - { - // Arrange - var expected = "/Admin/LG3/SomeAction?anothervalue=5"; - - var template = "Admin/LG3/SomeAction/{id?}"; - var defaults = new RouteValueDictionary(new { controller = "LG3", action = "SomeAction", area = "Admin" }); - var ambientValues = new RouteValueDictionary(new { controller = "LG1", action = "LinkToAnArea", id = "17" }); - var explicitValues = new RouteValueDictionary(new { controller = "LG3", area = "Admin", action = "SomeAction", anothervalue = "5" }); - var binder = new TemplateBinder( - UrlEncoder.Default, - new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), - RoutePatternFactory.Parse( - template, - defaults, - parameterPolicies: null, - requiredValues: new { area = "Admin", action = "SomeAction", controller = "LG3", page = (string)null }), + [Fact] + public void BindValues_LinkingFromPageToAController_Success() + { + // Arrange + var expected = "/LG2/SomeAction"; + + var template = "{controller=Home}/{action=Index}/{id?}"; + var defaults = new RouteValueDictionary(); + var ambientValues = new RouteValueDictionary(new { page = "/LGAnotherPage", id = "17" }); + var explicitValues = new RouteValueDictionary(new { controller = "LG2", action = "SomeAction" }); + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + RoutePatternFactory.Parse( + template, defaults, - requiredKeys: new string[] { "area", "action", "controller", "page" }, - parameterPolicies: null); + parameterPolicies: null, + requiredValues: new { area = (string)null, action = "SomeAction", controller = "LG2", page = (string)null }), + defaults, + requiredKeys: new string[] { "area", "action", "controller", "page" }, + parameterPolicies: null); + + // Act + var result = binder.GetValues(ambientValues, explicitValues); + var boundTemplate = binder.BindValues(result.AcceptedValues); + + // Assert + Assert.Equal(expected, boundTemplate); + } - // Act - var result = binder.GetValues(ambientValues, explicitValues); - var boundTemplate = binder.BindValues(result.AcceptedValues); + // Regression test for dotnet/aspnetcore#4212 + // + // An ambient value should be used to satisfy a required value even if if we're discarding + // ambient values. + [Fact] + public void BindValues_LinkingFromPageToAControllerInAreaWithAmbientArea_Success() + { + // Arrange + var expected = "/Admin/LG2/SomeAction"; + + var template = "{area}/{controller=Home}/{action=Index}/{id?}"; + var defaults = new RouteValueDictionary(); + var ambientValues = new RouteValueDictionary(new { area = "Admin", page = "/LGAnotherPage", id = "17" }); + var explicitValues = new RouteValueDictionary(new { controller = "LG2", action = "SomeAction" }); + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + RoutePatternFactory.Parse( + template, + defaults, + parameterPolicies: null, + requiredValues: new { area = "Admin", action = "SomeAction", controller = "LG2", page = (string)null }), + defaults, + requiredKeys: new string[] { "area", "action", "controller", "page" }, + parameterPolicies: null); + + // Act + var result = binder.GetValues(ambientValues, explicitValues); + var boundTemplate = binder.BindValues(result.AcceptedValues); + + // Assert + Assert.Equal(expected, boundTemplate); + } - // Assert - Assert.Equal(expected, boundTemplate); - } + [Fact] + public void BindValues_HasUnmatchingAmbientValues_Discard() + { + // Arrange + var expected = "/Admin/LG3/SomeAction?anothervalue=5"; + + var template = "Admin/LG3/SomeAction/{id?}"; + var defaults = new RouteValueDictionary(new { controller = "LG3", action = "SomeAction", area = "Admin" }); + var ambientValues = new RouteValueDictionary(new { controller = "LG1", action = "LinkToAnArea", id = "17" }); + var explicitValues = new RouteValueDictionary(new { controller = "LG3", area = "Admin", action = "SomeAction", anothervalue = "5" }); + var binder = new TemplateBinder( + UrlEncoder.Default, + new DefaultObjectPoolProvider().Create(new UriBuilderContextPooledObjectPolicy()), + RoutePatternFactory.Parse( + template, + defaults, + parameterPolicies: null, + requiredValues: new { area = "Admin", action = "SomeAction", controller = "LG3", page = (string)null }), + defaults, + requiredKeys: new string[] { "area", "action", "controller", "page" }, + parameterPolicies: null); + + // Act + var result = binder.GetValues(ambientValues, explicitValues); + var boundTemplate = binder.BindValues(result.AcceptedValues); + + // Assert + Assert.Equal(expected, boundTemplate); + } - private class PathAndQuery + private class PathAndQuery + { + public PathAndQuery(string uri) { - public PathAndQuery(string uri) + var queryIndex = uri.IndexOf("?", StringComparison.Ordinal); + if (queryIndex == -1) { - var queryIndex = uri.IndexOf("?", StringComparison.Ordinal); - if (queryIndex == -1) - { - Path = uri; - } - else - { - Path = uri.Substring(0, queryIndex); - - var query = uri.Substring(queryIndex + 1); - Parameters = - query - .Split(new char[] { '&' }, StringSplitOptions.None) - .Select(s => s.Split(new char[] { '=' }, StringSplitOptions.None)) - .ToDictionary(pair => pair[0], pair => pair[1]); - } + Path = uri; + } + else + { + Path = uri.Substring(0, queryIndex); + + var query = uri.Substring(queryIndex + 1); + Parameters = + query + .Split(new char[] { '&' }, StringSplitOptions.None) + .Select(s => s.Split(new char[] { '=' }, StringSplitOptions.None)) + .ToDictionary(pair => pair[0], pair => pair[1]); } + } - public string Path { get; private set; } + public string Path { get; private set; } - public Dictionary Parameters { get; private set; } - } + public Dictionary Parameters { get; private set; } } } diff --git a/src/Http/Routing/test/UnitTests/Template/TemplateMatcherTests.cs b/src/Http/Routing/test/UnitTests/Template/TemplateMatcherTests.cs index 5b6235375e..14ab1dcc7c 100644 --- a/src/Http/Routing/test/UnitTests/Template/TemplateMatcherTests.cs +++ b/src/Http/Routing/test/UnitTests/Template/TemplateMatcherTests.cs @@ -7,1125 +7,1124 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Xunit; -namespace Microsoft.AspNetCore.Routing.Template.Tests +namespace Microsoft.AspNetCore.Routing.Template.Tests; + +public class TemplateMatcherTests { - public class TemplateMatcherTests + [Fact] + public void TryMatch_Success() { - [Fact] - public void TryMatch_Success() - { - // Arrange - var matcher = CreateMatcher("{controller}/{action}/{id}"); + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}"); - var values = new RouteValueDictionary(); + var values = new RouteValueDictionary(); - // Act - var match = matcher.TryMatch("/Bank/DoAction/123", values); + // Act + var match = matcher.TryMatch("/Bank/DoAction/123", values); - // Assert - Assert.True(match); - Assert.Equal("Bank", values["controller"]); - Assert.Equal("DoAction", values["action"]); - Assert.Equal("123", values["id"]); - } + // Assert + Assert.True(match); + Assert.Equal("Bank", values["controller"]); + Assert.Equal("DoAction", values["action"]); + Assert.Equal("123", values["id"]); + } - [Fact] - public void TryMatch_Fails() - { - // Arrange - var matcher = CreateMatcher("{controller}/{action}/{id}"); + [Fact] + public void TryMatch_Fails() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}"); - var values = new RouteValueDictionary(); + var values = new RouteValueDictionary(); - // Act - var match = matcher.TryMatch("/Bank/DoAction", values); + // Act + var match = matcher.TryMatch("/Bank/DoAction", values); - // Assert - Assert.False(match); - } + // Assert + Assert.False(match); + } - [Fact] - public void TryMatch_WithDefaults_Success() - { - // Arrange - var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" }); + [Fact] + public void TryMatch_WithDefaults_Success() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" }); - var values = new RouteValueDictionary(); + var values = new RouteValueDictionary(); - // Act - var match = matcher.TryMatch("/Bank/DoAction", values); + // Act + var match = matcher.TryMatch("/Bank/DoAction", values); - // Assert - Assert.True(match); - Assert.Equal("Bank", values["controller"]); - Assert.Equal("DoAction", values["action"]); - Assert.Equal("default id", values["id"]); - } + // Assert + Assert.True(match); + Assert.Equal("Bank", values["controller"]); + Assert.Equal("DoAction", values["action"]); + Assert.Equal("default id", values["id"]); + } - [Fact] - public void TryMatch_WithDefaults_Fails() - { - // Arrange - var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" }); + [Fact] + public void TryMatch_WithDefaults_Fails() + { + // Arrange + var matcher = CreateMatcher("{controller}/{action}/{id}", new { id = "default id" }); - var values = new RouteValueDictionary(); + var values = new RouteValueDictionary(); - // Act - var match = matcher.TryMatch("/Bank", values); + // Act + var match = matcher.TryMatch("/Bank", values); - // Assert - Assert.False(match); - } + // Assert + Assert.False(match); + } - [Fact] - public void TryMatch_WithLiterals_Success() - { - // Arrange - var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" }); + [Fact] + public void TryMatch_WithLiterals_Success() + { + // Arrange + var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" }); - var values = new RouteValueDictionary(); + var values = new RouteValueDictionary(); - // Act - var match = matcher.TryMatch("/moo/111/bar/222", values); + // Act + var match = matcher.TryMatch("/moo/111/bar/222", values); - // Assert - Assert.True(match); - Assert.Equal("111", values["p1"]); - Assert.Equal("222", values["p2"]); - } + // Assert + Assert.True(match); + Assert.Equal("111", values["p1"]); + Assert.Equal("222", values["p2"]); + } - [Fact] - public void TryMatch_RouteWithLiteralsAndDefaults_Success() - { - // Arrange - var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" }); + [Fact] + public void TryMatch_RouteWithLiteralsAndDefaults_Success() + { + // Arrange + var matcher = CreateMatcher("moo/{p1}/bar/{p2}", new { p2 = "default p2" }); - var values = new RouteValueDictionary(); + var values = new RouteValueDictionary(); - // Act - var match = matcher.TryMatch("/moo/111/bar/", values); + // Act + var match = matcher.TryMatch("/moo/111/bar/", values); - // Assert - Assert.True(match); - Assert.Equal("111", values["p1"]); - Assert.Equal("default p2", values["p2"]); - } + // Assert + Assert.True(match); + Assert.Equal("111", values["p1"]); + Assert.Equal("default p2", values["p2"]); + } - [Theory] - [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", "/123-456-7890")] // ssn - [InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", "/asd@assds.com")] // email - [InlineData(@"{p1:regex(([}}])\w+)}", "/}sda")] // Not balanced } - [InlineData(@"{p1:regex(([{{)])\w+)}", "/})sda")] // Not balanced { - public void TryMatch_RegularExpressionConstraint_Valid( - string template, - string path) - { - // Arrange - var matcher = CreateMatcher(template); + [Theory] + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", "/123-456-7890")] // ssn + [InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", "/asd@assds.com")] // email + [InlineData(@"{p1:regex(([}}])\w+)}", "/}sda")] // Not balanced } + [InlineData(@"{p1:regex(([{{)])\w+)}", "/})sda")] // Not balanced { + public void TryMatch_RegularExpressionConstraint_Valid( + string template, + string path) + { + // Arrange + var matcher = CreateMatcher(template); - var values = new RouteValueDictionary(); + var values = new RouteValueDictionary(); - // Act - var match = matcher.TryMatch(path, values); + // Act + var match = matcher.TryMatch(path, values); - // Assert - Assert.True(match); - } + // Assert + Assert.True(match); + } - [Theory] - [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", true, "foo", "bar")] - [InlineData("moo/{p1?}", "/moo/foo", true, "foo", null)] - [InlineData("moo/{p1?}", "/moo", true, null, null)] - [InlineData("moo/{p1}.{p2?}", "/moo/foo", true, "foo", null)] - [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", true, "foo.", "bar")] - [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", true, "foo.moo", "bar")] - [InlineData("moo/{p1}.{p2}", "/moo/foo.bar", true, "foo", "bar")] - [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo.bar", true, "moo", "bar")] - [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", true, "moo", null)] - [InlineData("moo/.{p2?}", "/moo/.foo", true, null, "foo")] - [InlineData("moo/.{p2?}", "/moo", false, null, null)] - [InlineData("moo/{p1}.{p2?}", "/moo/....", true, "..", ".")] - [InlineData("moo/{p1}.{p2?}", "/moo/.bar", true, ".bar", null)] - public void TryMatch_OptionalParameter_FollowedByPeriod_Valid( - string template, - string path, - bool expectedMatch, - string p1, - string p2) - { - // Arrange - var matcher = CreateMatcher(template); + [Theory] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", true, "foo", "bar")] + [InlineData("moo/{p1?}", "/moo/foo", true, "foo", null)] + [InlineData("moo/{p1?}", "/moo", true, null, null)] + [InlineData("moo/{p1}.{p2?}", "/moo/foo", true, "foo", null)] + [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", true, "foo.", "bar")] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", true, "foo.moo", "bar")] + [InlineData("moo/{p1}.{p2}", "/moo/foo.bar", true, "foo", "bar")] + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo.bar", true, "moo", "bar")] + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", true, "moo", null)] + [InlineData("moo/.{p2?}", "/moo/.foo", true, null, "foo")] + [InlineData("moo/.{p2?}", "/moo", false, null, null)] + [InlineData("moo/{p1}.{p2?}", "/moo/....", true, "..", ".")] + [InlineData("moo/{p1}.{p2?}", "/moo/.bar", true, ".bar", null)] + public void TryMatch_OptionalParameter_FollowedByPeriod_Valid( + string template, + string path, + bool expectedMatch, + string p1, + string p2) + { + // Arrange + var matcher = CreateMatcher(template); - var values = new RouteValueDictionary(); + var values = new RouteValueDictionary(); - // Act - var match = matcher.TryMatch(path, values); + // Act + var match = matcher.TryMatch(path, values); - // Assert - Assert.Equal(expectedMatch, match); - if (p1 != null) - { - Assert.Equal(p1, values["p1"]); - } - if (p2 != null) - { - Assert.Equal(p2, values["p2"]); - } + // Assert + Assert.Equal(expectedMatch, match); + if (p1 != null) + { + Assert.Equal(p1, values["p1"]); } - - [Theory] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", "foo", "moo", null)] - [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] - [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", "foo", "moo", "bar")] - [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", "foo", null, "bar")] - [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", ".foo", null, "bar")] - [InlineData("{p1}/{p2}/{p3?}", "/foo/bar/baz", "foo", "bar", "baz")] - public void TryMatch_OptionalParameter_FollowedByPeriod_3Parameters_Valid( - string template, - string path, - string p1, - string p2, - string p3) + if (p2 != null) { - // Arrange - var matcher = CreateMatcher(template); + Assert.Equal(p2, values["p2"]); + } + } - var values = new RouteValueDictionary(); + [Theory] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", "foo", "moo", null)] + [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", "foo", "moo", "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", "foo", null, "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", ".foo", null, "bar")] + [InlineData("{p1}/{p2}/{p3?}", "/foo/bar/baz", "foo", "bar", "baz")] + public void TryMatch_OptionalParameter_FollowedByPeriod_3Parameters_Valid( + string template, + string path, + string p1, + string p2, + string p3) + { + // Arrange + var matcher = CreateMatcher(template); - // Act - var match = matcher.TryMatch(path, values); + var values = new RouteValueDictionary(); - // Assert - Assert.True(match); - Assert.Equal(p1, values["p1"]); + // Act + var match = matcher.TryMatch(path, values); - if (p2 != null) - { - Assert.Equal(p2, values["p2"]); - } + // Assert + Assert.True(match); + Assert.Equal(p1, values["p1"]); - if (p3 != null) - { - Assert.Equal(p3, values["p3"]); - } + if (p2 != null) + { + Assert.Equal(p2, values["p2"]); } - [Theory] - [InlineData("moo/{p1}.{p2?}", "/moo/foo.")] - [InlineData("moo/{p1}.{p2?}", "/moo/.")] - [InlineData("moo/{p1}.{p2}", "/foo.")] - [InlineData("moo/{p1}.{p2}", "/foo")] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")] - [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")] - [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")] - [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")] - [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")] - [InlineData("moo/.{p2?}", "/moo/.")] - [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")] - public void TryMatch_OptionalParameter_FollowedByPeriod_Invalid(string template, string path) + if (p3 != null) { - // Arrange - var matcher = CreateMatcher(template); + Assert.Equal(p3, values["p3"]); + } + } - var values = new RouteValueDictionary(); + [Theory] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.")] + [InlineData("moo/{p1}.{p2?}", "/moo/.")] + [InlineData("moo/{p1}.{p2}", "/foo.")] + [InlineData("moo/{p1}.{p2}", "/foo")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")] + [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")] + [InlineData("moo/.{p2?}", "/moo/.")] + [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")] + public void TryMatch_OptionalParameter_FollowedByPeriod_Invalid(string template, string path) + { + // Arrange + var matcher = CreateMatcher(template); - // Act - var match = matcher.TryMatch(path, values); + var values = new RouteValueDictionary(); - // Assert - Assert.False(match); - } + // Act + var match = matcher.TryMatch(path, values); - [Fact] - public void TryMatch_RouteWithOnlyLiterals_Success() - { - // Arrange - var matcher = CreateMatcher("moo/bar"); + // Assert + Assert.False(match); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_RouteWithOnlyLiterals_Success() + { + // Arrange + var matcher = CreateMatcher("moo/bar"); - // Act - var match = matcher.TryMatch("/moo/bar", values); + var values = new RouteValueDictionary(); - // Assert - Assert.True(match); - Assert.Empty(values); - } + // Act + var match = matcher.TryMatch("/moo/bar", values); - [Fact] - public void TryMatch_RouteWithOnlyLiterals_Fails() - { - // Arrange - var matcher = CreateMatcher("moo/bars"); + // Assert + Assert.True(match); + Assert.Empty(values); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_RouteWithOnlyLiterals_Fails() + { + // Arrange + var matcher = CreateMatcher("moo/bars"); - // Act - var match = matcher.TryMatch("/moo/bar", values); + var values = new RouteValueDictionary(); - // Assert - Assert.False(match); - } + // Act + var match = matcher.TryMatch("/moo/bar", values); - [Fact] - public void TryMatch_RouteWithExtraSeparators_Success() - { - // Arrange - var matcher = CreateMatcher("moo/bar"); + // Assert + Assert.False(match); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_RouteWithExtraSeparators_Success() + { + // Arrange + var matcher = CreateMatcher("moo/bar"); - // Act - var match = matcher.TryMatch("/moo/bar/", values); + var values = new RouteValueDictionary(); - // Assert - Assert.True(match); - Assert.Empty(values); - } + // Act + var match = matcher.TryMatch("/moo/bar/", values); - [Fact] - public void TryMatch_UrlWithExtraSeparators_Success() - { - // Arrange - var matcher = CreateMatcher("moo/bar/"); + // Assert + Assert.True(match); + Assert.Empty(values); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_UrlWithExtraSeparators_Success() + { + // Arrange + var matcher = CreateMatcher("moo/bar/"); - // Act - var match = matcher.TryMatch("/moo/bar", values); + var values = new RouteValueDictionary(); - // Assert - Assert.True(match); - Assert.Empty(values); - } + // Act + var match = matcher.TryMatch("/moo/bar", values); - [Fact] - public void TryMatch_RouteWithParametersAndExtraSeparators_Success() - { - // Arrange - var matcher = CreateMatcher("{p1}/{p2}/"); + // Assert + Assert.True(match); + Assert.Empty(values); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_RouteWithParametersAndExtraSeparators_Success() + { + // Arrange + var matcher = CreateMatcher("{p1}/{p2}/"); - // Act - var match = matcher.TryMatch("/moo/bar", values); + var values = new RouteValueDictionary(); - // Assert - Assert.True(match); - Assert.Equal("moo", values["p1"]); - Assert.Equal("bar", values["p2"]); - } + // Act + var match = matcher.TryMatch("/moo/bar", values); - [Fact] - public void TryMatch_RouteWithDifferentLiterals_Fails() - { - // Arrange - var matcher = CreateMatcher("{p1}/{p2}/baz"); + // Assert + Assert.True(match); + Assert.Equal("moo", values["p1"]); + Assert.Equal("bar", values["p2"]); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_RouteWithDifferentLiterals_Fails() + { + // Arrange + var matcher = CreateMatcher("{p1}/{p2}/baz"); - // Act - var match = matcher.TryMatch("/moo/bar/boo", values); + var values = new RouteValueDictionary(); - // Assert - Assert.False(match); - } + // Act + var match = matcher.TryMatch("/moo/bar/boo", values); - [Fact] - public void TryMatch_LongerUrl_Fails() - { - // Arrange - var matcher = CreateMatcher("{p1}"); + // Assert + Assert.False(match); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_LongerUrl_Fails() + { + // Arrange + var matcher = CreateMatcher("{p1}"); - // Act - var match = matcher.TryMatch("/moo/bar", values); + var values = new RouteValueDictionary(); - // Assert - Assert.False(match); - } + // Act + var match = matcher.TryMatch("/moo/bar", values); - [Fact] - public void TryMatch_SimpleFilename_Success() - { - // Arrange - var matcher = CreateMatcher("DEFAULT.ASPX"); + // Assert + Assert.False(match); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_SimpleFilename_Success() + { + // Arrange + var matcher = CreateMatcher("DEFAULT.ASPX"); - // Act - var match = matcher.TryMatch("/default.aspx", values); + var values = new RouteValueDictionary(); - // Assert - Assert.True(match); - } + // Act + var match = matcher.TryMatch("/default.aspx", values); - [Theory] - [InlineData("{prefix}x{suffix}", "/xxxxxxxxxx")] - [InlineData("{prefix}xyz{suffix}", "/xxxxyzxyzxxxxxxyz")] - [InlineData("{prefix}xyz{suffix}", "/abcxxxxyzxyzxxxxxxyzxx")] - [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyzxyzxyz")] - [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyzxyzxyz1")] - [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyz")] - [InlineData("{prefix}aa{suffix}", "/aaaaa")] - [InlineData("{prefix}aaa{suffix}", "/aaaaa")] - public void TryMatch_RouteWithComplexSegment_Success(string template, string path) - { - var matcher = CreateMatcher(template); + // Assert + Assert.True(match); + } - var values = new RouteValueDictionary(); + [Theory] + [InlineData("{prefix}x{suffix}", "/xxxxxxxxxx")] + [InlineData("{prefix}xyz{suffix}", "/xxxxyzxyzxxxxxxyz")] + [InlineData("{prefix}xyz{suffix}", "/abcxxxxyzxyzxxxxxxyzxx")] + [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyzxyzxyz")] + [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyzxyzxyz1")] + [InlineData("{prefix}xyz{suffix}", "/xyzxyzxyz")] + [InlineData("{prefix}aa{suffix}", "/aaaaa")] + [InlineData("{prefix}aaa{suffix}", "/aaaaa")] + public void TryMatch_RouteWithComplexSegment_Success(string template, string path) + { + var matcher = CreateMatcher(template); - // Act - var match = matcher.TryMatch(path, values); + var values = new RouteValueDictionary(); - // Assert - Assert.True(match); - } + // Act + var match = matcher.TryMatch(path, values); - [Fact] - public void TryMatch_RouteWithExtraDefaultValues_Success() - { - // Arrange - var matcher = CreateMatcher("{p1}/{p2}", new { p2 = (string)null, foo = "bar" }); + // Assert + Assert.True(match); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_RouteWithExtraDefaultValues_Success() + { + // Arrange + var matcher = CreateMatcher("{p1}/{p2}", new { p2 = (string)null, foo = "bar" }); - // Act - var match = matcher.TryMatch("/v1", values); + var values = new RouteValueDictionary(); - // Assert - Assert.True(match); - Assert.Equal(3, values.Count); - Assert.Equal("v1", values["p1"]); - Assert.Null(values["p2"]); - Assert.Equal("bar", values["foo"]); - } + // Act + var match = matcher.TryMatch("/v1", values); - [Fact] - public void TryMatch_PrettyRouteWithExtraDefaultValues_Success() - { - // Arrange - var matcher = CreateMatcher( - "date/{y}/{m}/{d}", - new { controller = "blog", action = "showpost", m = (string)null, d = (string)null }); + // Assert + Assert.True(match); + Assert.Equal(3, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Null(values["p2"]); + Assert.Equal("bar", values["foo"]); + } - var values = new RouteValueDictionary(); + [Fact] + public void TryMatch_PrettyRouteWithExtraDefaultValues_Success() + { + // Arrange + var matcher = CreateMatcher( + "date/{y}/{m}/{d}", + new { controller = "blog", action = "showpost", m = (string)null, d = (string)null }); + + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch("/date/2007/08", values); + + // Assert + Assert.True(match); + Assert.Equal(5, values.Count); + Assert.Equal("blog", values["controller"]); + Assert.Equal("showpost", values["action"]); + Assert.Equal("2007", values["y"]); + Assert.Equal("08", values["m"]); + Assert.Null(values["d"]); + } - // Act - var match = matcher.TryMatch("/date/2007/08", values); + [Fact] + public void TryMatch_WithMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}-{region}", + "/language/en-US", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } - // Assert - Assert.True(match); - Assert.Equal(5, values.Count); - Assert.Equal("blog", values["controller"]); - Assert.Equal("showpost", values["action"]); - Assert.Equal("2007", values["y"]); - Assert.Equal("08", values["m"]); - Assert.Null(values["d"]); - } + [Fact] + public void TryMatch_WithMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-{region}a", + "/language/en-USa", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } - [Fact] - public void TryMatch_WithMultiSegmentParamsOnBothEndsMatches() - { - RunTest( - "language/{lang}-{region}", - "/language/en-US", - null, - new RouteValueDictionary(new { lang = "en", region = "US" })); - } + [Fact] + public void TryMatch_WithMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}-{region}", + "/language/aen-US", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } - [Fact] - public void TryMatch_WithMultiSegmentParamsOnLeftEndMatches() - { - RunTest( - "language/{lang}-{region}a", - "/language/en-USa", - null, - new RouteValueDictionary(new { lang = "en", region = "US" })); - } + [Fact] + public void TryMatch_WithMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}-{region}a", + "/language/aen-USa", + null, + new RouteValueDictionary(new { lang = "en", region = "US" })); + } - [Fact] - public void TryMatch_WithMultiSegmentParamsOnRightEndMatches() - { - RunTest( - "language/a{lang}-{region}", - "/language/aen-US", - null, - new RouteValueDictionary(new { lang = "en", region = "US" })); - } + [Fact] + public void TryMatch_WithMultiSegmentParamsOnNeitherEndDoesNotMatch() + { + RunTest( + "language/a{lang}-{region}a", + "/language/a-USa", + null, + null); + } - [Fact] - public void TryMatch_WithMultiSegmentParamsOnNeitherEndMatches() - { - RunTest( - "language/a{lang}-{region}a", - "/language/aen-USa", - null, - new RouteValueDictionary(new { lang = "en", region = "US" })); - } + [Fact] + public void TryMatch_WithMultiSegmentParamsOnNeitherEndDoesNotMatch2() + { + RunTest( + "language/a{lang}-{region}a", + "/language/aen-a", + null, + null); + } - [Fact] - public void TryMatch_WithMultiSegmentParamsOnNeitherEndDoesNotMatch() - { - RunTest( - "language/a{lang}-{region}a", - "/language/a-USa", - null, - null); - } + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsMatches() + { + RunTest( + "language/{lang}", + "/language/en", + null, + new RouteValueDictionary(new { lang = "en" })); + } - [Fact] - public void TryMatch_WithMultiSegmentParamsOnNeitherEndDoesNotMatch2() - { - RunTest( - "language/a{lang}-{region}a", - "/language/aen-a", - null, - null); - } + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsTrailingSlashDoesNotMatch() + { + RunTest( + "language/{lang}", + "/language/", + null, + null); + } - [Fact] - public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsMatches() - { - RunTest( - "language/{lang}", - "/language/en", - null, - new RouteValueDictionary(new { lang = "en" })); - } + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsDoesNotMatch() + { + RunTest( + "language/{lang}", + "/language", + null, + null); + } - [Fact] - public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsTrailingSlashDoesNotMatch() - { - RunTest( - "language/{lang}", - "/language/", - null, - null); - } + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnLeftEndMatches() + { + RunTest( + "language/{lang}-", + "/language/en-", + null, + new RouteValueDictionary(new { lang = "en" })); + } - [Fact] - public void TryMatch_WithSimpleMultiSegmentParamsOnBothEndsDoesNotMatch() - { - RunTest( - "language/{lang}", - "/language", - null, - null); - } + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnRightEndMatches() + { + RunTest( + "language/a{lang}", + "/language/aen", + null, + new RouteValueDictionary(new { lang = "en" })); + } - [Fact] - public void TryMatch_WithSimpleMultiSegmentParamsOnLeftEndMatches() - { - RunTest( - "language/{lang}-", - "/language/en-", - null, - new RouteValueDictionary(new { lang = "en" })); - } + [Fact] + public void TryMatch_WithSimpleMultiSegmentParamsOnNeitherEndMatches() + { + RunTest( + "language/a{lang}a", + "/language/aena", + null, + new RouteValueDictionary(new { lang = "en" })); + } - [Fact] - public void TryMatch_WithSimpleMultiSegmentParamsOnRightEndMatches() - { - RunTest( - "language/a{lang}", - "/language/aen", - null, - new RouteValueDictionary(new { lang = "en" })); - } + [Fact] + public void TryMatch_WithMultiSegmentStandamatchMvcRouteMatches() + { + RunTest( + "{controller}.mvc/{action}/{id}", + "/home.mvc/index", + new RouteValueDictionary(new { action = "Index", id = (string)null }), + new RouteValueDictionary(new { controller = "home", action = "index", id = (string)null })); + } - [Fact] - public void TryMatch_WithSimpleMultiSegmentParamsOnNeitherEndMatches() - { - RunTest( - "language/a{lang}a", - "/language/aena", - null, - new RouteValueDictionary(new { lang = "en" })); - } + [Fact] + public void TryMatch_WithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() + { + RunTest( + "language/{lang}-{region}", + "/language/-", + new RouteValueDictionary(new { lang = "xx", region = "yy" }), + null); + } - [Fact] - public void TryMatch_WithMultiSegmentStandamatchMvcRouteMatches() - { - RunTest( - "{controller}.mvc/{action}/{id}", - "/home.mvc/index", - new RouteValueDictionary(new { action = "Index", id = (string)null }), - new RouteValueDictionary(new { controller = "home", action = "index", id = (string)null })); - } + [Fact] + public void TryMatch_WithUrlWithMultiSegmentWithRepeatedDots() + { + RunTest( + "{Controller}..mvc/{id}/{Param1}", + "/Home..mvc/123/p1", + null, + new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); + } - [Fact] - public void TryMatch_WithMultiSegmentParamsOnBothEndsWithDefaultValuesMatches() - { - RunTest( - "language/{lang}-{region}", - "/language/-", - new RouteValueDictionary(new { lang = "xx", region = "yy" }), - null); - } + [Fact] + public void TryMatch_WithUrlWithTwoRepeatedDots() + { + RunTest( + "{Controller}.mvc/../{action}", + "/Home.mvc/../index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } - [Fact] - public void TryMatch_WithUrlWithMultiSegmentWithRepeatedDots() - { - RunTest( - "{Controller}..mvc/{id}/{Param1}", - "/Home..mvc/123/p1", - null, - new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); - } + [Fact] + public void TryMatch_WithUrlWithThreeRepeatedDots() + { + RunTest( + "{Controller}.mvc/.../{action}", + "/Home.mvc/.../index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } - [Fact] - public void TryMatch_WithUrlWithTwoRepeatedDots() - { - RunTest( - "{Controller}.mvc/../{action}", - "/Home.mvc/../index", - null, - new RouteValueDictionary(new { Controller = "Home", action = "index" })); - } + [Fact] + public void TryMatch_WithUrlWithManyRepeatedDots() + { + RunTest( + "{Controller}.mvc/../../../{action}", + "/Home.mvc/../../../index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } - [Fact] - public void TryMatch_WithUrlWithThreeRepeatedDots() - { - RunTest( - "{Controller}.mvc/.../{action}", - "/Home.mvc/.../index", - null, - new RouteValueDictionary(new { Controller = "Home", action = "index" })); - } + [Fact] + public void TryMatch_WithUrlWithExclamationPoint() + { + RunTest( + "{Controller}.mvc!/{action}", + "/Home.mvc!/index", + null, + new RouteValueDictionary(new { Controller = "Home", action = "index" })); + } - [Fact] - public void TryMatch_WithUrlWithManyRepeatedDots() - { - RunTest( - "{Controller}.mvc/../../../{action}", - "/Home.mvc/../../../index", - null, - new RouteValueDictionary(new { Controller = "Home", action = "index" })); - } + [Fact] + public void TryMatch_WithUrlWithStartingDotDotSlash() + { + RunTest( + "../{Controller}.mvc", + "/../Home.mvc", + null, + new RouteValueDictionary(new { Controller = "Home" })); + } - [Fact] - public void TryMatch_WithUrlWithExclamationPoint() - { - RunTest( - "{Controller}.mvc!/{action}", - "/Home.mvc!/index", - null, - new RouteValueDictionary(new { Controller = "Home", action = "index" })); - } + [Fact] + public void TryMatch_WithUrlWithStartingBackslash() + { + RunTest( + @"\{Controller}.mvc", + @"/\Home.mvc", + null, + new RouteValueDictionary(new { Controller = "Home" })); + } - [Fact] - public void TryMatch_WithUrlWithStartingDotDotSlash() - { - RunTest( - "../{Controller}.mvc", - "/../Home.mvc", - null, - new RouteValueDictionary(new { Controller = "Home" })); - } + [Fact] + public void TryMatch_WithUrlWithBackslashSeparators() + { + RunTest( + @"{Controller}.mvc\{id}\{Param1}", + @"/Home.mvc\123\p1", + null, + new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); + } - [Fact] - public void TryMatch_WithUrlWithStartingBackslash() - { - RunTest( - @"\{Controller}.mvc", - @"/\Home.mvc", - null, - new RouteValueDictionary(new { Controller = "Home" })); - } + [Fact] + public void TryMatch_WithUrlWithParenthesesLiterals() + { + RunTest( + @"(Controller).mvc", + @"/(Controller).mvc", + null, + new RouteValueDictionary()); + } - [Fact] - public void TryMatch_WithUrlWithBackslashSeparators() - { - RunTest( - @"{Controller}.mvc\{id}\{Param1}", - @"/Home.mvc\123\p1", - null, - new RouteValueDictionary(new { Controller = "Home", id = "123", Param1 = "p1" })); - } + [Fact] + public void TryMatch_WithUrlWithTrailingSlashSpace() + { + RunTest( + @"Controller.mvc/ ", + @"/Controller.mvc/ ", + null, + new RouteValueDictionary()); + } - [Fact] - public void TryMatch_WithUrlWithParenthesesLiterals() - { - RunTest( - @"(Controller).mvc", - @"/(Controller).mvc", - null, - new RouteValueDictionary()); - } + [Fact] + public void TryMatch_WithUrlWithTrailingSpace() + { + RunTest( + @"Controller.mvc ", + @"/Controller.mvc ", + null, + new RouteValueDictionary()); + } - [Fact] - public void TryMatch_WithUrlWithTrailingSlashSpace() - { - RunTest( - @"Controller.mvc/ ", - @"/Controller.mvc/ ", - null, - new RouteValueDictionary()); - } + [Fact] + public void TryMatch_WithCatchAllCapturesDots() + { + // DevDiv Bugs 189892: UrlRouting: Catch all parameter cannot capture url segments that contain the "." + RunTest( + "Home/ShowPilot/{missionId}/{*name}", + "/Home/ShowPilot/777/12345./foobar", + new RouteValueDictionary(new + { + controller = "Home", + action = "ShowPilot", + missionId = (string)null, + name = (string)null + }), + new RouteValueDictionary(new { controller = "Home", action = "ShowPilot", missionId = "777", name = "12345./foobar" })); + } - [Fact] - public void TryMatch_WithUrlWithTrailingSpace() - { - RunTest( - @"Controller.mvc ", - @"/Controller.mvc ", - null, - new RouteValueDictionary()); - } + [Fact] + public void TryMatch_RouteWithCatchAll_MatchesMultiplePathSegments() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); - [Fact] - public void TryMatch_WithCatchAllCapturesDots() - { - // DevDiv Bugs 189892: UrlRouting: Catch all parameter cannot capture url segments that contain the "." - RunTest( - "Home/ShowPilot/{missionId}/{*name}", - "/Home/ShowPilot/777/12345./foobar", - new RouteValueDictionary(new - { - controller = "Home", - action = "ShowPilot", - missionId = (string)null, - name = (string)null - }), - new RouteValueDictionary(new { controller = "Home", action = "ShowPilot", missionId = "777", name = "12345./foobar" })); - } + var values = new RouteValueDictionary(); - [Fact] - public void TryMatch_RouteWithCatchAll_MatchesMultiplePathSegments() - { - // Arrange - var matcher = CreateMatcher("{p1}/{*p2}"); + // Act + var match = matcher.TryMatch("/v1/v2/v3", values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Equal("v2/v3", values["p2"]); + } - // Act - var match = matcher.TryMatch("/v1/v2/v3", values); + [Fact] + public void TryMatch_RouteWithCatchAll_MatchesTrailingSlash() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); - // Assert - Assert.True(match); - Assert.Equal(2, values.Count); - Assert.Equal("v1", values["p1"]); - Assert.Equal("v2/v3", values["p2"]); - } + var values = new RouteValueDictionary(); - [Fact] - public void TryMatch_RouteWithCatchAll_MatchesTrailingSlash() - { - // Arrange - var matcher = CreateMatcher("{p1}/{*p2}"); + // Act + var match = matcher.TryMatch("/v1/", values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Null(values["p2"]); + } - // Act - var match = matcher.TryMatch("/v1/", values); + [Fact] + public void TryMatch_RouteWithCatchAll_MatchesEmptyContent() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); - // Assert - Assert.True(match); - Assert.Equal(2, values.Count); - Assert.Equal("v1", values["p1"]); - Assert.Null(values["p2"]); - } + var values = new RouteValueDictionary(); - [Fact] - public void TryMatch_RouteWithCatchAll_MatchesEmptyContent() - { - // Arrange - var matcher = CreateMatcher("{p1}/{*p2}"); + // Act + var match = matcher.TryMatch("/v1", values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Null(values["p2"]); + } - // Act - var match = matcher.TryMatch("/v1", values); + [Fact] + public void TryMatch_RouteWithCatchAll_MatchesEmptyContent_DoesNotReplaceExistingRouteValue() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}"); - // Assert - Assert.True(match); - Assert.Equal(2, values.Count); - Assert.Equal("v1", values["p1"]); - Assert.Null(values["p2"]); - } + var values = new RouteValueDictionary(new { p2 = "hello" }); - [Fact] - public void TryMatch_RouteWithCatchAll_MatchesEmptyContent_DoesNotReplaceExistingRouteValue() - { - // Arrange - var matcher = CreateMatcher("{p1}/{*p2}"); + // Act + var match = matcher.TryMatch("/v1", values); - var values = new RouteValueDictionary(new { p2 = "hello" }); + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Equal("hello", values["p2"]); + } - // Act - var match = matcher.TryMatch("/v1", values); + [Fact] + public void TryMatch_RouteWithCatchAll_UsesDefaultValueForEmptyContent() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" }); - // Assert - Assert.True(match); - Assert.Equal(2, values.Count); - Assert.Equal("v1", values["p1"]); - Assert.Equal("hello", values["p2"]); - } + var values = new RouteValueDictionary(new { p2 = "overridden" }); - [Fact] - public void TryMatch_RouteWithCatchAll_UsesDefaultValueForEmptyContent() - { - // Arrange - var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" }); + // Act + var match = matcher.TryMatch("/v1", values); - var values = new RouteValueDictionary(new { p2 = "overridden" }); + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Equal("catchall", values["p2"]); + } - // Act - var match = matcher.TryMatch("/v1", values); + [Fact] + public void TryMatch_RouteWithCatchAll_IgnoresDefaultValueForNonEmptyContent() + { + // Arrange + var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" }); - // Assert - Assert.True(match); - Assert.Equal(2, values.Count); - Assert.Equal("v1", values["p1"]); - Assert.Equal("catchall", values["p2"]); - } + var values = new RouteValueDictionary(new { p2 = "overridden" }); - [Fact] - public void TryMatch_RouteWithCatchAll_IgnoresDefaultValueForNonEmptyContent() - { - // Arrange - var matcher = CreateMatcher("{p1}/{*p2}", new { p2 = "catchall" }); + // Act + var match = matcher.TryMatch("/v1/hello/whatever", values); - var values = new RouteValueDictionary(new { p2 = "overridden" }); + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("v1", values["p1"]); + Assert.Equal("hello/whatever", values["p2"]); + } - // Act - var match = matcher.TryMatch("/v1/hello/whatever", values); + [Fact] + public void TryMatch_DoesNotMatchOnlyLeftLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "/fooBAR", + null, + null); + } - // Assert - Assert.True(match); - Assert.Equal(2, values.Count); - Assert.Equal("v1", values["p1"]); - Assert.Equal("hello/whatever", values["p2"]); - } + [Fact] + public void TryMatch_DoesNotMatchOnlyRightLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "/BARfoo", + null, + null); + } - [Fact] - public void TryMatch_DoesNotMatchOnlyLeftLiteralMatch() - { - // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url - RunTest( - "foo", - "/fooBAR", - null, - null); - } + [Fact] + public void TryMatch_DoesNotMatchMiddleLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "/BARfooBAR", + null, + null); + } - [Fact] - public void TryMatch_DoesNotMatchOnlyRightLiteralMatch() - { - // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url - RunTest( - "foo", - "/BARfoo", - null, - null); - } + [Fact] + public void TryMatch_DoesMatchesExactLiteralMatch() + { + // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url + RunTest( + "foo", + "/foo", + null, + new RouteValueDictionary()); + } - [Fact] - public void TryMatch_DoesNotMatchMiddleLiteralMatch() - { - // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url - RunTest( - "foo", - "/BARfooBAR", - null, - null); - } + [Fact] + public void TryMatch_WithWeimatchParameterNames() + { + RunTest( + "foo/{ }/{.!$%}/{dynamic.data}/{op.tional}", + "/foo/space/weimatch/omatcherid", + new RouteValueDictionary() { { " ", "not a space" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }, + new RouteValueDictionary() { { " ", "space" }, { ".!$%", "weimatch" }, { "dynamic.data", "omatcherid" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }); + } - [Fact] - public void TryMatch_DoesMatchesExactLiteralMatch() - { - // DevDiv Bugs 191180: UrlRouting: Wrong template getting matched if a url segment is a substring of the requested url - RunTest( - "foo", - "/foo", - null, - new RouteValueDictionary()); - } + [Fact] + public void TryMatch_DoesNotMatchRouteWithLiteralSeparatomatchefaultsButNoValue() + { + RunTest( + "{controller}/{language}-{locale}", + "/foo", + new RouteValueDictionary(new { language = "en", locale = "US" }), + null); + } - [Fact] - public void TryMatch_WithWeimatchParameterNames() - { - RunTest( - "foo/{ }/{.!$%}/{dynamic.data}/{op.tional}", - "/foo/space/weimatch/omatcherid", - new RouteValueDictionary() { { " ", "not a space" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }, - new RouteValueDictionary() { { " ", "space" }, { ".!$%", "weimatch" }, { "dynamic.data", "omatcherid" }, { "op.tional", "default value" }, { "ran!dom", "va@lue" } }); - } + [Fact] + public void TryMatch_DoesNotMatchesRouteWithLiteralSeparatomatchefaultsAndLeftValue() + { + RunTest( + "{controller}/{language}-{locale}", + "/foo/xx-", + new RouteValueDictionary(new { language = "en", locale = "US" }), + null); + } - [Fact] - public void TryMatch_DoesNotMatchRouteWithLiteralSeparatomatchefaultsButNoValue() - { - RunTest( - "{controller}/{language}-{locale}", - "/foo", - new RouteValueDictionary(new { language = "en", locale = "US" }), - null); - } + [Fact] + public void TryMatch_DoesNotMatchesRouteWithLiteralSeparatomatchefaultsAndRightValue() + { + RunTest( + "{controller}/{language}-{locale}", + "/foo/-yy", + new RouteValueDictionary(new { language = "en", locale = "US" }), + null); + } - [Fact] - public void TryMatch_DoesNotMatchesRouteWithLiteralSeparatomatchefaultsAndLeftValue() - { - RunTest( - "{controller}/{language}-{locale}", - "/foo/xx-", - new RouteValueDictionary(new { language = "en", locale = "US" }), - null); - } + [Fact] + public void TryMatch_MatchesRouteWithLiteralSeparatomatchefaultsAndValue() + { + RunTest( + "{controller}/{language}-{locale}", + "/foo/xx-yy", + new RouteValueDictionary(new { language = "en", locale = "US" }), + new RouteValueDictionary { { "language", "xx" }, { "locale", "yy" }, { "controller", "foo" } }); + } - [Fact] - public void TryMatch_DoesNotMatchesRouteWithLiteralSeparatomatchefaultsAndRightValue() - { - RunTest( - "{controller}/{language}-{locale}", - "/foo/-yy", - new RouteValueDictionary(new { language = "en", locale = "US" }), - null); - } + [Fact] + public void TryMatch_SetsOptionalParameter() + { + // Arrange + var route = CreateMatcher("{controller}/{action?}"); + var url = "/Home/Index"; - [Fact] - public void TryMatch_MatchesRouteWithLiteralSeparatomatchefaultsAndValue() - { - RunTest( - "{controller}/{language}-{locale}", - "/foo/xx-yy", - new RouteValueDictionary(new { language = "en", locale = "US" }), - new RouteValueDictionary { { "language", "xx" }, { "locale", "yy" }, { "controller", "foo" } }); - } + var values = new RouteValueDictionary(); - [Fact] - public void TryMatch_SetsOptionalParameter() - { - // Arrange - var route = CreateMatcher("{controller}/{action?}"); - var url = "/Home/Index"; + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("Home", values["controller"]); + Assert.Equal("Index", values["action"]); + } - // Act - var match = route.TryMatch(url, values); + [Fact] + public void TryMatch_DoesNotSetOptionalParameter() + { + // Arrange + var route = CreateMatcher("{controller}/{action?}"); + var url = "/Home"; - // Assert - Assert.True(match); - Assert.Equal(2, values.Count); - Assert.Equal("Home", values["controller"]); - Assert.Equal("Index", values["action"]); - } + var values = new RouteValueDictionary(); - [Fact] - public void TryMatch_DoesNotSetOptionalParameter() - { - // Arrange - var route = CreateMatcher("{controller}/{action?}"); - var url = "/Home"; + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + Assert.Single(values); + Assert.Equal("Home", values["controller"]); + Assert.False(values.ContainsKey("action")); + } - // Act - var match = route.TryMatch(url, values); + [Fact] + public void TryMatch_DoesNotSetOptionalParameter_EmptyString() + { + // Arrange + var route = CreateMatcher("{controller?}"); + var url = ""; - // Assert - Assert.True(match); - Assert.Single(values); - Assert.Equal("Home", values["controller"]); - Assert.False(values.ContainsKey("action")); - } + var values = new RouteValueDictionary(); - [Fact] - public void TryMatch_DoesNotSetOptionalParameter_EmptyString() - { - // Arrange - var route = CreateMatcher("{controller?}"); - var url = ""; + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + Assert.Empty(values); + Assert.False(values.ContainsKey("controller")); + } - // Act - var match = route.TryMatch(url, values); + [Fact] + public void TryMatch__EmptyRouteWith_EmptyString() + { + // Arrange + var route = CreateMatcher(""); + var url = ""; - // Assert - Assert.True(match); - Assert.Empty(values); - Assert.False(values.ContainsKey("controller")); - } + var values = new RouteValueDictionary(); - [Fact] - public void TryMatch__EmptyRouteWith_EmptyString() - { - // Arrange - var route = CreateMatcher(""); - var url = ""; + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + Assert.Empty(values); + } - // Act - var match = route.TryMatch(url, values); + [Fact] + public void TryMatch_MultipleOptionalParameters() + { + // Arrange + var route = CreateMatcher("{controller}/{action?}/{id?}"); + var url = "/Home/Index"; - // Assert - Assert.True(match); - Assert.Empty(values); - } + var values = new RouteValueDictionary(); - [Fact] - public void TryMatch_MultipleOptionalParameters() - { - // Arrange - var route = CreateMatcher("{controller}/{action?}/{id?}"); - var url = "/Home/Index"; + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + Assert.Equal(2, values.Count); + Assert.Equal("Home", values["controller"]); + Assert.Equal("Index", values["action"]); + Assert.False(values.ContainsKey("id")); + } - // Act - var match = route.TryMatch(url, values); + [Theory] + [InlineData("///")] + [InlineData("/a//")] + [InlineData("/a/b//")] + [InlineData("//b//")] + [InlineData("///c")] + [InlineData("///c/")] + public void TryMatch_MultipleOptionalParameters_WithEmptyIntermediateSegmentsDoesNotMatch(string url) + { + // Arrange + var route = CreateMatcher("{controller?}/{action?}/{id?}"); - // Assert - Assert.True(match); - Assert.Equal(2, values.Count); - Assert.Equal("Home", values["controller"]); - Assert.Equal("Index", values["action"]); - Assert.False(values.ContainsKey("id")); - } + var values = new RouteValueDictionary(); - [Theory] - [InlineData("///")] - [InlineData("/a//")] - [InlineData("/a/b//")] - [InlineData("//b//")] - [InlineData("///c")] - [InlineData("///c/")] - public void TryMatch_MultipleOptionalParameters_WithEmptyIntermediateSegmentsDoesNotMatch(string url) - { - // Arrange - var route = CreateMatcher("{controller?}/{action?}/{id?}"); + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.False(match); + } - // Act - var match = route.TryMatch(url, values); + [Theory] + [InlineData("")] + [InlineData("/")] + [InlineData("/a")] + [InlineData("/a/")] + [InlineData("/a/b")] + [InlineData("/a/b/")] + [InlineData("/a/b/c")] + [InlineData("/a/b/c/")] + public void TryMatch_MultipleOptionalParameters_WithIncrementalOptionalValues(string url) + { + // Arrange + var route = CreateMatcher("{controller?}/{action?}/{id?}"); - // Assert - Assert.False(match); - } + var values = new RouteValueDictionary(); - [Theory] - [InlineData("")] - [InlineData("/")] - [InlineData("/a")] - [InlineData("/a/")] - [InlineData("/a/b")] - [InlineData("/a/b/")] - [InlineData("/a/b/c")] - [InlineData("/a/b/c/")] - public void TryMatch_MultipleOptionalParameters_WithIncrementalOptionalValues(string url) - { - // Arrange - var route = CreateMatcher("{controller?}/{action?}/{id?}"); + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + } - // Act - var match = route.TryMatch(url, values); + [Theory] + [InlineData("///")] + [InlineData("////")] + [InlineData("/a//")] + [InlineData("/a///")] + [InlineData("//b/")] + [InlineData("//b//")] + [InlineData("///c")] + [InlineData("///c/")] + public void TryMatch_MultipleParameters_WithEmptyValues(string url) + { + // Arrange + var route = CreateMatcher("{controller}/{action}/{id}"); - // Assert - Assert.True(match); - } + var values = new RouteValueDictionary(); - [Theory] - [InlineData("///")] - [InlineData("////")] - [InlineData("/a//")] - [InlineData("/a///")] - [InlineData("//b/")] - [InlineData("//b//")] - [InlineData("///c")] - [InlineData("///c/")] - public void TryMatch_MultipleParameters_WithEmptyValues(string url) - { - // Arrange - var route = CreateMatcher("{controller}/{action}/{id}"); + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.False(match); + } - // Act - var match = route.TryMatch(url, values); + [Theory] + [InlineData("/a/b/c//")] + [InlineData("/a/b/c/////")] + public void TryMatch_CatchAllParameters_WithEmptyValuesAtTheEnd(string url) + { + // Arrange + var route = CreateMatcher("{controller}/{action}/{*id}"); - // Assert - Assert.False(match); - } + var values = new RouteValueDictionary(); - [Theory] - [InlineData("/a/b/c//")] - [InlineData("/a/b/c/////")] - public void TryMatch_CatchAllParameters_WithEmptyValuesAtTheEnd(string url) - { - // Arrange - var route = CreateMatcher("{controller}/{action}/{*id}"); + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.True(match); + } - // Act - var match = route.TryMatch(url, values); + [Theory] + [InlineData("/a/b//")] + [InlineData("/a/b///c")] + public void TryMatch_CatchAllParameters_WithEmptyValues(string url) + { + // Arrange + var route = CreateMatcher("{controller}/{action}/{*id}"); - // Assert - Assert.True(match); - } + var values = new RouteValueDictionary(); - [Theory] - [InlineData("/a/b//")] - [InlineData("/a/b///c")] - public void TryMatch_CatchAllParameters_WithEmptyValues(string url) - { - // Arrange - var route = CreateMatcher("{controller}/{action}/{*id}"); + // Act + var match = route.TryMatch(url, values); - var values = new RouteValueDictionary(); + // Assert + Assert.False(match); + } - // Act - var match = route.TryMatch(url, values); + private TemplateMatcher CreateMatcher(string template, object defaults = null) + { + return new TemplateMatcher( + TemplateParser.Parse(template), + new RouteValueDictionary(defaults)); + } - // Assert - Assert.False(match); - } + private static void RunTest( + string template, + string path, + RouteValueDictionary defaults, + IDictionary expected) + { + // Arrange + var matcher = new TemplateMatcher( + TemplateParser.Parse(template), + defaults ?? new RouteValueDictionary()); - private TemplateMatcher CreateMatcher(string template, object defaults = null) + var values = new RouteValueDictionary(); + + // Act + var match = matcher.TryMatch(new PathString(path), values); + + // Assert + if (expected == null) { - return new TemplateMatcher( - TemplateParser.Parse(template), - new RouteValueDictionary(defaults)); + Assert.False(match); } - - private static void RunTest( - string template, - string path, - RouteValueDictionary defaults, - IDictionary expected) + else { - // Arrange - var matcher = new TemplateMatcher( - TemplateParser.Parse(template), - defaults ?? new RouteValueDictionary()); - - var values = new RouteValueDictionary(); - - // Act - var match = matcher.TryMatch(new PathString(path), values); - - // Assert - if (expected == null) - { - Assert.False(match); - } - else + Assert.True(match); + Assert.Equal(expected.Count, values.Count); + foreach (string key in values.Keys) { - Assert.True(match); - Assert.Equal(expected.Count, values.Count); - foreach (string key in values.Keys) - { - Assert.Equal(expected[key], values[key]); - } + Assert.Equal(expected[key], values[key]); } } } diff --git a/src/Http/Routing/test/UnitTests/Template/TemplateParserTests.cs b/src/Http/Routing/test/UnitTests/Template/TemplateParserTests.cs index 1fa3c16501..b10f084e0a 100644 --- a/src/Http/Routing/test/UnitTests/Template/TemplateParserTests.cs +++ b/src/Http/Routing/test/UnitTests/Template/TemplateParserTests.cs @@ -7,896 +7,859 @@ using System.Linq; using Microsoft.AspNetCore.Testing; using Xunit; -namespace Microsoft.AspNetCore.Routing.Template.Tests +namespace Microsoft.AspNetCore.Routing.Template.Tests; + +public class TemplateRouteParserTests { - public class TemplateRouteParserTests + [Fact] + public void Parse_SingleLiteral() { - [Fact] - public void Parse_SingleLiteral() - { - // Arrange - var template = "cool"; + // Arrange + var template = "cool"; - var expected = new RouteTemplate(template, new List()); - expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool")); + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool")); - // Act - var actual = TemplateParser.Parse(template); + // Act + var actual = TemplateParser.Parse(template); - // Assert - Assert.Equal(expected, actual, new TemplateEqualityComparer()); - } + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } - [Fact] - public void Parse_SingleParameter() - { - // Arrange - var template = "{p}"; + [Fact] + public void Parse_SingleParameter() + { + // Arrange + var template = "{p}"; - var expected = new RouteTemplate(template, new List()); - expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add( - TemplatePart.CreateParameter("p", false, false, defaultValue: null, inlineConstraints: null)); - expected.Parameters.Add(expected.Segments[0].Parts[0]); + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add( + TemplatePart.CreateParameter("p", false, false, defaultValue: null, inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); - // Act - var actual = TemplateParser.Parse(template); + // Act + var actual = TemplateParser.Parse(template); - // Assert - Assert.Equal(expected, actual, new TemplateEqualityComparer()); - } + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } - [Fact] - public void Parse_OptionalParameter() - { - // Arrange - var template = "{p?}"; + [Fact] + public void Parse_OptionalParameter() + { + // Arrange + var template = "{p?}"; - var expected = new RouteTemplate(template, new List()); - expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add( - TemplatePart.CreateParameter("p", false, true, defaultValue: null, inlineConstraints: null)); - expected.Parameters.Add(expected.Segments[0].Parts[0]); + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add( + TemplatePart.CreateParameter("p", false, true, defaultValue: null, inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); - // Act - var actual = TemplateParser.Parse(template); + // Act + var actual = TemplateParser.Parse(template); - // Assert - Assert.Equal(expected, actual, new TemplateEqualityComparer()); - } + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } - [Fact] - public void Parse_MultipleLiterals() - { - // Arrange - var template = "cool/awesome/super"; - - var expected = new RouteTemplate(template, new List()); - expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool")); - expected.Segments.Add(new TemplateSegment()); - expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral("awesome")); - expected.Segments.Add(new TemplateSegment()); - expected.Segments[2].Parts.Add(TemplatePart.CreateLiteral("super")); - - // Act - var actual = TemplateParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new TemplateEqualityComparer()); - } + [Fact] + public void Parse_MultipleLiterals() + { + // Arrange + var template = "cool/awesome/super"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool")); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral("awesome")); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[2].Parts.Add(TemplatePart.CreateLiteral("super")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } - [Fact] - public void Parse_MultipleParameters() - { - // Arrange - var template = "{p1}/{p2}/{*p3}"; - - var expected = new RouteTemplate(template, new List()); - - expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", - false, - false, - defaultValue: null, - inlineConstraints: null)); - expected.Parameters.Add(expected.Segments[0].Parts[0]); - - expected.Segments.Add(new TemplateSegment()); - expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2", - false, - false, - defaultValue: null, - inlineConstraints: null)); - expected.Parameters.Add(expected.Segments[1].Parts[0]); - - expected.Segments.Add(new TemplateSegment()); - expected.Segments[2].Parts.Add(TemplatePart.CreateParameter("p3", - true, - false, - defaultValue: null, - inlineConstraints: null)); - expected.Parameters.Add(expected.Segments[2].Parts[0]); - - // Act - var actual = TemplateParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new TemplateEqualityComparer()); - } + [Fact] + public void Parse_MultipleParameters() + { + // Arrange + var template = "{p1}/{p2}/{*p3}"; + + var expected = new RouteTemplate(template, new List()); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[1].Parts[0]); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[2].Parts.Add(TemplatePart.CreateParameter("p3", + true, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[2].Parts[0]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_LP() - { - // Arrange - var template = "cool-{p1}"; - - var expected = new RouteTemplate(template, new List()); - expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", - false, - false, - defaultValue: null, - inlineConstraints: null)); - expected.Parameters.Add(expected.Segments[0].Parts[1]); - - // Act - var actual = TemplateParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new TemplateEqualityComparer()); - } + [Fact] + public void Parse_ComplexSegment_LP() + { + // Arrange + var template = "cool-{p1}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[1]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_PL() - { - // Arrange - var template = "{p1}-cool"; - - var expected = new RouteTemplate(template, new List()); - expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", - false, - false, - defaultValue: null, - inlineConstraints: null)); - expected.Parameters.Add(expected.Segments[0].Parts[0]); - expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); - - // Act - var actual = TemplateParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new TemplateEqualityComparer()); - } + [Fact] + public void Parse_ComplexSegment_PL() + { + // Arrange + var template = "{p1}-cool"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_PLP() - { - // Arrange - var template = "{p1}-cool-{p2}"; - - var expected = new RouteTemplate(template, new List()); - expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", - false, - false, - defaultValue: null, - inlineConstraints: null)); - expected.Parameters.Add(expected.Segments[0].Parts[0]); - expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", - false, - false, - defaultValue: null, - inlineConstraints: null)); - expected.Parameters.Add(expected.Segments[0].Parts[2]); - - // Act - var actual = TemplateParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new TemplateEqualityComparer()); - } + [Fact] + public void Parse_ComplexSegment_PLP() + { + // Arrange + var template = "{p1}-cool-{p2}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_LPL() - { - // Arrange - var template = "cool-{p1}-awesome"; - - var expected = new RouteTemplate(template, new List()); - expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", - false, - false, - defaultValue: null, - inlineConstraints: null)); - expected.Parameters.Add(expected.Segments[0].Parts[1]); - expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("-awesome")); - - // Act - var actual = TemplateParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new TemplateEqualityComparer()); - } + [Fact] + public void Parse_ComplexSegment_LPL() + { + // Arrange + var template = "cool-{p1}-awesome"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("cool-")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Parameters.Add(expected.Segments[0].Parts[1]); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral("-awesome")); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_OptionalParameterFollowingPeriod() - { - // Arrange - var template = "{p1}.{p2?}"; - - var expected = new RouteTemplate(template, new List()); - expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", - false, - false, - defaultValue: null, - inlineConstraints: null)); - expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", - false, - true, - defaultValue: null, - inlineConstraints: null)); - - expected.Parameters.Add(expected.Segments[0].Parts[0]); - expected.Parameters.Add(expected.Segments[0].Parts[2]); - - // Act - var actual = TemplateParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new TemplateEqualityComparer()); - } + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod() + { + // Arrange + var template = "{p1}.{p2?}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + true, + defaultValue: null, + inlineConstraints: null)); + + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_ParametersFollowingPeriod() - { - // Arrange - var template = "{p1}.{p2}"; - - var expected = new RouteTemplate(template, new List()); - expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", - false, - false, - defaultValue: null, - inlineConstraints: null)); - expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", - false, - false, - defaultValue: null, - inlineConstraints: null)); - - expected.Parameters.Add(expected.Segments[0].Parts[0]); - expected.Parameters.Add(expected.Segments[0].Parts[2]); - - // Act - var actual = TemplateParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new TemplateEqualityComparer()); - } + [Fact] + public void Parse_ComplexSegment_ParametersFollowingPeriod() + { + // Arrange + var template = "{p1}.{p2}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_ThreeParameters() - { - // Arrange - var template = "{p1}.{p2}.{p3?}"; - - var expected = new RouteTemplate(template, new List()); - expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", - false, - false, - defaultValue: null, - inlineConstraints: null)); - expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", - false, - false, - defaultValue: null, - inlineConstraints: null)); - - expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p3", - false, - true, - defaultValue: null, - inlineConstraints: null)); - - - expected.Parameters.Add(expected.Segments[0].Parts[0]); - expected.Parameters.Add(expected.Segments[0].Parts[2]); - expected.Parameters.Add(expected.Segments[0].Parts[4]); - - // Act - var actual = TemplateParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new TemplateEqualityComparer()); - } + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_ThreeParameters() + { + // Arrange + var template = "{p1}.{p2}.{p3?}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p3", + false, + true, + defaultValue: null, + inlineConstraints: null)); + + + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + expected.Parameters.Add(expected.Segments[0].Parts[4]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_ThreeParametersSeparatedByPeriod() - { - // Arrange - var template = "{p1}.{p2}.{p3}"; - - var expected = new RouteTemplate(template, new List()); - expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", - false, - false, - defaultValue: null, - inlineConstraints: null)); - expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", - false, - false, - defaultValue: null, - inlineConstraints: null)); - - expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p3", - false, - false, - defaultValue: null, - inlineConstraints: null)); - - - expected.Parameters.Add(expected.Segments[0].Parts[0]); - expected.Parameters.Add(expected.Segments[0].Parts[2]); - expected.Parameters.Add(expected.Segments[0].Parts[4]); - - // Act - var actual = TemplateParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new TemplateEqualityComparer()); - } + [Fact] + public void Parse_ComplexSegment_ThreeParametersSeparatedByPeriod() + { + // Arrange + var template = "{p1}.{p2}.{p3}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p3", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + expected.Parameters.Add(expected.Segments[0].Parts[4]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_MiddleSegment() - { - // Arrange - var template = "{p1}.{p2?}/{p3}"; - - var expected = new RouteTemplate(template, new List()); - expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", - false, - false, - defaultValue: null, - inlineConstraints: null)); - expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", - false, - true, - defaultValue: null, - inlineConstraints: null)); - - expected.Parameters.Add(expected.Segments[0].Parts[0]); - expected.Parameters.Add(expected.Segments[0].Parts[2]); - - expected.Segments.Add(new TemplateSegment()); - expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3", - false, - false, - null, - null)); - expected.Parameters.Add(expected.Segments[1].Parts[0]); - // Act - var actual = TemplateParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new TemplateEqualityComparer()); - } + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_MiddleSegment() + { + // Arrange + var template = "{p1}.{p2?}/{p3}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[0].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + true, + defaultValue: null, + inlineConstraints: null)); + + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[0].Parts[2]); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3", + false, + false, + null, + null)); + expected.Parameters.Add(expected.Segments[1].Parts[0]); + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_LastSegment() - { - // Arrange - var template = "{p1}/{p2}.{p3?}"; - - var expected = new RouteTemplate(template, new List()); - expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", - false, - false, - defaultValue: null, - inlineConstraints: null)); - - expected.Segments.Add(new TemplateSegment()); - expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2", - false, - false, - defaultValue: null, - inlineConstraints: null)); - expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral(".")); - expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3", - false, - true, - null, - null)); - expected.Parameters.Add(expected.Segments[0].Parts[0]); - expected.Parameters.Add(expected.Segments[1].Parts[0]); - expected.Parameters.Add(expected.Segments[1].Parts[2]); - - // Act - var actual = TemplateParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new TemplateEqualityComparer()); - } + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_LastSegment() + { + // Arrange + var template = "{p1}/{p2}.{p3?}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3", + false, + true, + null, + null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[1].Parts[0]); + expected.Parameters.Add(expected.Segments[1].Parts[2]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } - [Fact] - public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_PeriodAfterSlash() - { - // Arrange - var template = "{p2}/.{p3?}"; - - var expected = new RouteTemplate(template, new List()); - expected.Segments.Add(new TemplateSegment()); - expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", - false, - false, - defaultValue: null, - inlineConstraints: null)); - - expected.Segments.Add(new TemplateSegment()); - expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral(".")); - expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3", - false, - true, - null, - null)); - expected.Parameters.Add(expected.Segments[0].Parts[0]); - expected.Parameters.Add(expected.Segments[1].Parts[1]); - - // Act - var actual = TemplateParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new TemplateEqualityComparer()); - } + [Fact] + public void Parse_ComplexSegment_OptionalParameterFollowingPeriod_PeriodAfterSlash() + { + // Arrange + var template = "{p2}/.{p3?}"; + + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + expected.Segments[0].Parts.Add(TemplatePart.CreateParameter("p2", + false, + false, + defaultValue: null, + inlineConstraints: null)); + + expected.Segments.Add(new TemplateSegment()); + expected.Segments[1].Parts.Add(TemplatePart.CreateLiteral(".")); + expected.Segments[1].Parts.Add(TemplatePart.CreateParameter("p3", + false, + true, + null, + null)); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + expected.Parameters.Add(expected.Segments[1].Parts[1]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } - [Theory] - [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", @"regex(^\d{3}-\d{3}-\d{4}$)")] // ssn - [InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)}", @"regex(^\d{1,2}\/\d{1,2}\/\d{4}$)")] // date - [InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", @"regex(^\w+\@\w+\.\w+)")] // email - [InlineData(@"{p1:regex(([}}])\w+)}", @"regex(([}])\w+)")] // Not balanced } - [InlineData(@"{p1:regex(([{{(])\w+)}", @"regex(([{(])\w+)")] // Not balanced { - public void Parse_RegularExpressions(string template, string constraint) - { - // Arrange - var expected = new RouteTemplate(template, new List()); - expected.Segments.Add(new TemplateSegment()); - var c = new InlineConstraint(constraint); - expected.Segments[0].Parts.Add( - TemplatePart.CreateParameter("p1", - false, - false, - defaultValue: null, - inlineConstraints: new List { c })); - expected.Parameters.Add(expected.Segments[0].Parts[0]); - - // Act - var actual = TemplateParser.Parse(template); - - // Assert - Assert.Equal(expected, actual, new TemplateEqualityComparer()); - } + [Theory] + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}", @"regex(^\d{3}-\d{3}-\d{4}$)")] // ssn + [InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)}", @"regex(^\d{1,2}\/\d{1,2}\/\d{4}$)")] // date + [InlineData(@"{p1:regex(^\w+\@\w+\.\w+)}", @"regex(^\w+\@\w+\.\w+)")] // email + [InlineData(@"{p1:regex(([}}])\w+)}", @"regex(([}])\w+)")] // Not balanced } + [InlineData(@"{p1:regex(([{{(])\w+)}", @"regex(([{(])\w+)")] // Not balanced { + public void Parse_RegularExpressions(string template, string constraint) + { + // Arrange + var expected = new RouteTemplate(template, new List()); + expected.Segments.Add(new TemplateSegment()); + var c = new InlineConstraint(constraint); + expected.Segments[0].Parts.Add( + TemplatePart.CreateParameter("p1", + false, + false, + defaultValue: null, + inlineConstraints: new List { c })); + expected.Parameters.Add(expected.Segments[0].Parts[0]); + + // Act + var actual = TemplateParser.Parse(template); + + // Assert + Assert.Equal(expected, actual, new TemplateEqualityComparer()); + } - [Theory] - [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}}$)}")] // extra } - [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}}")] // extra } at the end - [InlineData(@"{{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}")] // extra { at the beginning - [InlineData(@"{p1:regex(([}])\w+}")] // Not escaped } - [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}$)}")] // Not escaped } - [InlineData(@"{p1:regex(abc)")] - [ReplaceCulture] - public void Parse_RegularExpressions_Invalid(string template) - { - // Act and Assert - ExceptionAssert.Throws( - () => TemplateParser.Parse(template), - "There is an incomplete parameter in the route template. Check that each '{' character has a matching " + - "'}' character. (Parameter 'routeTemplate')"); - } + [Theory] + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}}$)}")] // extra } + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}}")] // extra } at the end + [InlineData(@"{{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}}$)}")] // extra { at the beginning + [InlineData(@"{p1:regex(([}])\w+}")] // Not escaped } + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{4}$)}")] // Not escaped } + [InlineData(@"{p1:regex(abc)")] + [ReplaceCulture] + public void Parse_RegularExpressions_Invalid(string template) + { + // Act and Assert + ExceptionAssert.Throws( + () => TemplateParser.Parse(template), + "There is an incomplete parameter in the route template. Check that each '{' character has a matching " + + "'}' character. (Parameter 'routeTemplate')"); + } - [Theory] - [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{{4}}$)}")] // extra { - [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{4}}$)}")] // Not escaped { - [ReplaceCulture] - public void Parse_RegularExpressions_Unescaped(string template) - { - // Act and Assert - ExceptionAssert.Throws( - () => TemplateParser.Parse(template), - "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'. (Parameter 'routeTemplate')"); - } + [Theory] + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{{{4}}$)}")] // extra { + [InlineData(@"{p1:regex(^\d{{3}}-\d{{3}}-\d{4}}$)}")] // Not escaped { + [ReplaceCulture] + public void Parse_RegularExpressions_Unescaped(string template) + { + // Act and Assert + ExceptionAssert.Throws( + () => TemplateParser.Parse(template), + "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'. (Parameter 'routeTemplate')"); + } - [Theory] - [InlineData("{p1}.{p2?}.{p3}", "p2", ".")] - [InlineData("{p1?}{p2}", "p1", "{p2}")] - [InlineData("{p1?}{p2?}", "p1", "{p2?}")] - [InlineData("{p1}.{p2?})", "p2", ")")] - [InlineData("{foorb?}-bar-{z}", "foorb", "-bar-")] - [ReplaceCulture] - public void Parse_ComplexSegment_OptionalParameter_NotTheLastPart( - string template, - string parameter, - string invalid) - { - // Act and Assert - ExceptionAssert.Throws( - () => TemplateParser.Parse(template), - "An optional parameter must be at the end of the segment. In the segment '" + template + - "', optional parameter '" + parameter + "' is followed by '" + invalid + "'. (Parameter 'routeTemplate')"); - } + [Theory] + [InlineData("{p1}.{p2?}.{p3}", "p2", ".")] + [InlineData("{p1?}{p2}", "p1", "{p2}")] + [InlineData("{p1?}{p2?}", "p1", "{p2?}")] + [InlineData("{p1}.{p2?})", "p2", ")")] + [InlineData("{foorb?}-bar-{z}", "foorb", "-bar-")] + [ReplaceCulture] + public void Parse_ComplexSegment_OptionalParameter_NotTheLastPart( + string template, + string parameter, + string invalid) + { + // Act and Assert + ExceptionAssert.Throws( + () => TemplateParser.Parse(template), + "An optional parameter must be at the end of the segment. In the segment '" + template + + "', optional parameter '" + parameter + "' is followed by '" + invalid + "'. (Parameter 'routeTemplate')"); + } - [Theory] - [InlineData("{p1}-{p2?}", "-")] - [InlineData("{p1}..{p2?}", "..")] - [InlineData("..{p2?}", "..")] - [InlineData("{p1}.abc.{p2?}", ".abc.")] - [InlineData("{p1}{p2?}", "{p1}")] - [ReplaceCulture] - public void Parse_ComplexSegment_OptionalParametersSeparatedByPeriod_Invalid(string template, string parameter) - { - // Act and Assert - ExceptionAssert.Throws( - () => TemplateParser.Parse(template), - "In the segment '" + template + "', the optional parameter 'p2' is preceded by an invalid " + - "segment '" + parameter + "'. Only a period (.) can precede an optional parameter. (Parameter 'routeTemplate')"); - } + [Theory] + [InlineData("{p1}-{p2?}", "-")] + [InlineData("{p1}..{p2?}", "..")] + [InlineData("..{p2?}", "..")] + [InlineData("{p1}.abc.{p2?}", ".abc.")] + [InlineData("{p1}{p2?}", "{p1}")] + [ReplaceCulture] + public void Parse_ComplexSegment_OptionalParametersSeparatedByPeriod_Invalid(string template, string parameter) + { + // Act and Assert + ExceptionAssert.Throws( + () => TemplateParser.Parse(template), + "In the segment '" + template + "', the optional parameter 'p2' is preceded by an invalid " + + "segment '" + parameter + "'. Only a period (.) can precede an optional parameter. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_WithRepeatedParameter() - { - var ex = ExceptionAssert.Throws( - () => TemplateParser.Parse("{Controller}.mvc/{id}/{controller}"), - "The route parameter name 'controller' appears more than one time in the route template. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_WithRepeatedParameter() + { + var ex = ExceptionAssert.Throws( + () => TemplateParser.Parse("{Controller}.mvc/{id}/{controller}"), + "The route parameter name 'controller' appears more than one time in the route template. (Parameter 'routeTemplate')"); + } - [Theory] - [InlineData("123{a}abc{")] - [InlineData("123{a}abc}")] - [InlineData("xyz}123{a}abc}")] - [InlineData("{{p1}")] - [InlineData("{p1}}")] - [InlineData("p1}}p2{")] - [ReplaceCulture] - public void InvalidTemplate_WithMismatchedBraces(string template) - { - ExceptionAssert.Throws( - () => TemplateParser.Parse(template), - @"There is an incomplete parameter in the route template. Check that each '{' character has a " + - "matching '}' character. (Parameter 'routeTemplate')"); - } + [Theory] + [InlineData("123{a}abc{")] + [InlineData("123{a}abc}")] + [InlineData("xyz}123{a}abc}")] + [InlineData("{{p1}")] + [InlineData("{p1}}")] + [InlineData("p1}}p2{")] + [ReplaceCulture] + public void InvalidTemplate_WithMismatchedBraces(string template) + { + ExceptionAssert.Throws( + () => TemplateParser.Parse(template), + @"There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_CannotHaveCatchAllInMultiSegment() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("123{a}abc{*moo}"), - "A path segment that contains more than one section, such as a literal section or a parameter, " + - "cannot contain a catch-all parameter. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_CannotHaveCatchAllInMultiSegment() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("123{a}abc{*moo}"), + "A path segment that contains more than one section, such as a literal section or a parameter, " + + "cannot contain a catch-all parameter. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_CannotHaveMoreThanOneCatchAll() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("{*p1}/{*p2}"), - "A catch-all parameter can only appear as the last segment of the route template. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_CannotHaveMoreThanOneCatchAll() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{*p1}/{*p2}"), + "A catch-all parameter can only appear as the last segment of the route template. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("{*p1}abc{*p2}"), - "A path segment that contains more than one section, such as a literal section or a parameter, " + - "cannot contain a catch-all parameter. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_CannotHaveMoreThanOneCatchAllInMultiSegment() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{*p1}abc{*p2}"), + "A path segment that contains more than one section, such as a literal section or a parameter, " + + "cannot contain a catch-all parameter. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_CannotHaveCatchAllWithNoName() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("foo/{*}"), - "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + - " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional," + - " and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + - " and can occur only at the start of the parameter. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_CannotHaveCatchAllWithNoName() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("foo/{*}"), + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional," + + " and can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + + " and can occur only at the start of the parameter. (Parameter 'routeTemplate')"); + } - [Theory] - [InlineData("{a*}", "a*")] - [InlineData("{*a*}", "a*")] - [InlineData("{*a*:int}", "a*")] - [InlineData("{*a*=5}", "a*")] - [InlineData("{*a*b=5}", "a*b")] - [InlineData("{p1?}.{p2/}/{p3}", "p2/")] - [InlineData("{p{{}", "p{")] - [InlineData("{p}}}", "p}")] - [InlineData("{p/}", "p/")] - [ReplaceCulture] - public void ParseRouteParameter_ThrowsIf_ParameterContainsSpecialCharacters( - string template, - string parameterName) - { - // Arrange - var expectedMessage = "The route parameter name '" + parameterName + "' is invalid. Route parameter " + - "names must be non-empty and cannot contain these characters: '{', '}', '/'. The '?' character " + - "marks a parameter as optional, and can occur only at the end of the parameter. The '*' character " + - "marks a parameter as catch-all, and can occur only at the start of the parameter."; - - // Act & Assert - ExceptionAssert.Throws( - () => TemplateParser.Parse(template), expectedMessage + " (Parameter 'routeTemplate')"); - } + [Theory] + [InlineData("{a*}", "a*")] + [InlineData("{*a*}", "a*")] + [InlineData("{*a*:int}", "a*")] + [InlineData("{*a*=5}", "a*")] + [InlineData("{*a*b=5}", "a*b")] + [InlineData("{p1?}.{p2/}/{p3}", "p2/")] + [InlineData("{p{{}", "p{")] + [InlineData("{p}}}", "p}")] + [InlineData("{p/}", "p/")] + [ReplaceCulture] + public void ParseRouteParameter_ThrowsIf_ParameterContainsSpecialCharacters( + string template, + string parameterName) + { + // Arrange + var expectedMessage = "The route parameter name '" + parameterName + "' is invalid. Route parameter " + + "names must be non-empty and cannot contain these characters: '{', '}', '/'. The '?' character " + + "marks a parameter as optional, and can occur only at the end of the parameter. The '*' character " + + "marks a parameter as catch-all, and can occur only at the start of the parameter."; + + // Act & Assert + ExceptionAssert.Throws( + () => TemplateParser.Parse(template), expectedMessage + " (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_CannotHaveConsecutiveOpenBrace() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("foo/{{p1}"), - "There is an incomplete parameter in the route template. Check that each '{' character has a " + - "matching '}' character. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_CannotHaveConsecutiveOpenBrace() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("foo/{{p1}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_CannotHaveConsecutiveCloseBrace() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("foo/{p1}}"), - "There is an incomplete parameter in the route template. Check that each '{' character has a " + - "matching '}' character. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_CannotHaveConsecutiveCloseBrace() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("foo/{p1}}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_SameParameterTwiceThrows() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("{aaa}/{AAA}"), - "The route parameter name 'AAA' appears more than one time in the route template. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_SameParameterTwiceThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{aaa}/{AAA}"), + "The route parameter name 'AAA' appears more than one time in the route template. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("{aaa}/{*AAA}"), - "The route parameter name 'AAA' appears more than one time in the route template. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_SameParameterTwiceAndOneCatchAllThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{aaa}/{*AAA}"), + "The route parameter name 'AAA' appears more than one time in the route template. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("{a}/{aa}a}/{z}"), - "There is an incomplete parameter in the route template. Check that each '{' character has a " + - "matching '}' character. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_InvalidParameterNameWithCloseBracketThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{a}/{aa}a}/{z}"), + "There is an incomplete parameter in the route template. Check that each '{' character has a " + + "matching '}' character. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("{a}/{a{aa}/{z}"), - "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_InvalidParameterNameWithOpenBracketThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{a}/{a{aa}/{z}"), + "In a route parameter, '{' and '}' must be escaped with '{{' and '}}'. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("{a}/{}/{z}"), - "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + - " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + - " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + - " and can occur only at the start of the parameter. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_InvalidParameterNameWithEmptyNameThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{a}/{}/{z}"), + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + + " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + + " and can occur only at the start of the parameter. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_InvalidParameterNameWithQuestionThrows() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("{Controller}.mvc/{?}"), - "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + - " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + - " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + - " and can occur only at the start of the parameter. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_InvalidParameterNameWithQuestionThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{Controller}.mvc/{?}"), + "The route parameter name '' is invalid. Route parameter names must be non-empty and cannot" + + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + + " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + + " and can occur only at the start of the parameter. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("{a}//{z}"), - "The route template separator character '/' cannot appear consecutively. It must be separated by " + - "either a parameter or a literal value. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_ConsecutiveSeparatorsSlashSlashThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{a}//{z}"), + "The route template separator character '/' cannot appear consecutively. It must be separated by " + + "either a parameter or a literal value. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_WithCatchAllNotAtTheEndThrows() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("foo/{p1}/{*p2}/{p3}"), - "A catch-all parameter can only appear as the last segment of the route template. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_WithCatchAllNotAtTheEndThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("foo/{p1}/{*p2}/{p3}"), + "A catch-all parameter can only appear as the last segment of the route template. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_RepeatedParametersThrows() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("foo/aa{p1}{p2}"), - "A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by " + - "a literal string. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_RepeatedParametersThrows() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("foo/aa{p1}{p2}"), + "A path segment cannot contain two consecutive parameters. They must be separated by a '/' or by " + + "a literal string. (Parameter 'routeTemplate')"); + } - [Theory] - [InlineData("/foo")] - [InlineData("~/foo")] - public void ValidTemplate_CanStartWithSlashOrTildeSlash(string routeTemplate) - { - // Arrange & Act - var pattern = TemplateParser.Parse(routeTemplate); + [Theory] + [InlineData("/foo")] + [InlineData("~/foo")] + public void ValidTemplate_CanStartWithSlashOrTildeSlash(string routeTemplate) + { + // Arrange & Act + var pattern = TemplateParser.Parse(routeTemplate); - // Assert - Assert.Equal(routeTemplate, pattern.TemplateText); - } + // Assert + Assert.Equal(routeTemplate, pattern.TemplateText); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_CannotStartWithTilde() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("~foo"), - "The route template cannot start with a '~' character unless followed by a '/'. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_CannotStartWithTilde() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("~foo"), + "The route template cannot start with a '~' character unless followed by a '/'. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_CannotContainQuestionMark() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("foor?bar"), - "The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_CannotContainQuestionMark() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("foor?bar"), + "The literal section 'foor?bar' is invalid. Literal sections cannot contain the '?' character. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_ParameterCannotContainQuestionMark_UnlessAtEnd() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("{foor?b}"), - "The route parameter name 'foor?b' is invalid. Route parameter names must be non-empty and cannot" + - " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + - " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + - " and can occur only at the start of the parameter. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_ParameterCannotContainQuestionMark_UnlessAtEnd() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{foor?b}"), + "The route parameter name 'foor?b' is invalid. Route parameter names must be non-empty and cannot" + + " contain these characters: '{', '}', '/'. The '?' character marks a parameter as optional, and" + + " can occur only at the end of the parameter. The '*' character marks a parameter as catch-all," + + " and can occur only at the start of the parameter. (Parameter 'routeTemplate')"); + } - [Fact] - [ReplaceCulture] - public void InvalidTemplate_CatchAllMarkedOptional() - { - ExceptionAssert.Throws( - () => TemplateParser.Parse("{a}/{*b?}"), - "A catch-all parameter cannot be marked optional. (Parameter 'routeTemplate')"); - } + [Fact] + [ReplaceCulture] + public void InvalidTemplate_CatchAllMarkedOptional() + { + ExceptionAssert.Throws( + () => TemplateParser.Parse("{a}/{*b?}"), + "A catch-all parameter cannot be marked optional. (Parameter 'routeTemplate')"); + } - private class TemplateEqualityComparer : IEqualityComparer + private class TemplateEqualityComparer : IEqualityComparer + { + public bool Equals(RouteTemplate x, RouteTemplate y) { - public bool Equals(RouteTemplate x, RouteTemplate y) + if (x == null && y == null) + { + return true; + } + else if (x == null || y == null) + { + return false; + } + else { - if (x == null && y == null) + if (!string.Equals(x.TemplateText, y.TemplateText, StringComparison.Ordinal)) { - return true; + return false; } - else if (x == null || y == null) + + if (x.Segments.Count != y.Segments.Count) { return false; } - else - { - if (!string.Equals(x.TemplateText, y.TemplateText, StringComparison.Ordinal)) - { - return false; - } - if (x.Segments.Count != y.Segments.Count) - { - return false; - } - - for (var i = 0; i < x.Segments.Count; i++) - { - if (x.Segments[i].Parts.Count != y.Segments[i].Parts.Count) - { - return false; - } - - for (int j = 0; j < x.Segments[i].Parts.Count; j++) - { - if (!Equals(x.Segments[i].Parts[j], y.Segments[i].Parts[j])) - { - return false; - } - } - } - - if (x.Parameters.Count != y.Parameters.Count) + for (var i = 0; i < x.Segments.Count; i++) + { + if (x.Segments[i].Parts.Count != y.Segments[i].Parts.Count) { return false; } - for (var i = 0; i < x.Parameters.Count; i++) + for (int j = 0; j < x.Segments[i].Parts.Count; j++) { - if (!Equals(x.Parameters[i], y.Parameters[i])) + if (!Equals(x.Segments[i].Parts[j], y.Segments[i].Parts[j])) { return false; } } - - return true; - } - } - - private bool Equals(TemplatePart x, TemplatePart y) - { - if (x.IsLiteral != y.IsLiteral || - x.IsParameter != y.IsParameter || - x.IsCatchAll != y.IsCatchAll || - x.IsOptional != y.IsOptional || - !String.Equals(x.Name, y.Name, StringComparison.Ordinal) || - !String.Equals(x.Name, y.Name, StringComparison.Ordinal) || - (x.InlineConstraints == null && y.InlineConstraints != null) || - (x.InlineConstraints != null && y.InlineConstraints == null)) - { - return false; - } - - if (x.InlineConstraints == null && y.InlineConstraints == null) - { - return true; } - if (x.InlineConstraints.Count() != y.InlineConstraints.Count()) + if (x.Parameters.Count != y.Parameters.Count) { return false; } - foreach (var xconstraint in x.InlineConstraints) + for (var i = 0; i < x.Parameters.Count; i++) { - if (!y.InlineConstraints.Any( - c => string.Equals(c.Constraint, xconstraint.Constraint, StringComparison.Ordinal))) + if (!Equals(x.Parameters[i], y.Parameters[i])) { return false; } @@ -904,11 +867,47 @@ namespace Microsoft.AspNetCore.Routing.Template.Tests return true; } + } + + private bool Equals(TemplatePart x, TemplatePart y) + { + if (x.IsLiteral != y.IsLiteral || + x.IsParameter != y.IsParameter || + x.IsCatchAll != y.IsCatchAll || + x.IsOptional != y.IsOptional || + !String.Equals(x.Name, y.Name, StringComparison.Ordinal) || + !String.Equals(x.Name, y.Name, StringComparison.Ordinal) || + (x.InlineConstraints == null && y.InlineConstraints != null) || + (x.InlineConstraints != null && y.InlineConstraints == null)) + { + return false; + } + + if (x.InlineConstraints == null && y.InlineConstraints == null) + { + return true; + } + + if (x.InlineConstraints.Count() != y.InlineConstraints.Count()) + { + return false; + } - public int GetHashCode(RouteTemplate obj) + foreach (var xconstraint in x.InlineConstraints) { - throw new NotImplementedException(); + if (!y.InlineConstraints.Any( + c => string.Equals(c.Constraint, xconstraint.Constraint, StringComparison.Ordinal))) + { + return false; + } } + + return true; + } + + public int GetHashCode(RouteTemplate obj) + { + throw new NotImplementedException(); } } } diff --git a/src/Http/Routing/test/UnitTests/Template/TemplateSegmentTest.cs b/src/Http/Routing/test/UnitTests/Template/TemplateSegmentTest.cs index b5a5e8b3f1..32ccf2d46d 100644 --- a/src/Http/Routing/test/UnitTests/Template/TemplateSegmentTest.cs +++ b/src/Http/Routing/test/UnitTests/Template/TemplateSegmentTest.cs @@ -5,45 +5,44 @@ using System; using Microsoft.AspNetCore.Routing.Patterns; using Xunit; -namespace Microsoft.AspNetCore.Routing.Template +namespace Microsoft.AspNetCore.Routing.Template; + +public class TemplateSegmentTest { - public class TemplateSegmentTest + [Fact] + public void Ctor_RoutePatternPathSegment_ShouldThrowArgumentNullExceptionWhenOtherIsNull() { - [Fact] - public void Ctor_RoutePatternPathSegment_ShouldThrowArgumentNullExceptionWhenOtherIsNull() - { - const RoutePatternPathSegment other = null; + const RoutePatternPathSegment other = null; - var actual = Assert.ThrowsAny(() => new TemplateSegment(other)); - Assert.Equal(nameof(other), actual.ParamName); - } + var actual = Assert.ThrowsAny(() => new TemplateSegment(other)); + Assert.Equal(nameof(other), actual.ParamName); + } - [Fact] - public void ToRoutePatternPathSegment() - { - // Arrange - var literalPartA = RoutePatternFactory.LiteralPart("A"); - var paramPartB = RoutePatternFactory.ParameterPart("B"); - var paramPartC = RoutePatternFactory.ParameterPart("C"); - var paramPartD = RoutePatternFactory.ParameterPart("D"); - var separatorPartE = RoutePatternFactory.SeparatorPart("E"); - var templateSegment = new TemplateSegment(RoutePatternFactory.Segment(paramPartC, literalPartA, separatorPartE, paramPartB)); + [Fact] + public void ToRoutePatternPathSegment() + { + // Arrange + var literalPartA = RoutePatternFactory.LiteralPart("A"); + var paramPartB = RoutePatternFactory.ParameterPart("B"); + var paramPartC = RoutePatternFactory.ParameterPart("C"); + var paramPartD = RoutePatternFactory.ParameterPart("D"); + var separatorPartE = RoutePatternFactory.SeparatorPart("E"); + var templateSegment = new TemplateSegment(RoutePatternFactory.Segment(paramPartC, literalPartA, separatorPartE, paramPartB)); - // Act - var routePatternPathSegment = templateSegment.ToRoutePatternPathSegment(); - templateSegment.Parts[1] = new TemplatePart(RoutePatternFactory.ParameterPart("D")); - templateSegment.Parts.RemoveAt(0); + // Act + var routePatternPathSegment = templateSegment.ToRoutePatternPathSegment(); + templateSegment.Parts[1] = new TemplatePart(RoutePatternFactory.ParameterPart("D")); + templateSegment.Parts.RemoveAt(0); - // Assert - Assert.Equal(4, routePatternPathSegment.Parts.Count); - Assert.IsType(routePatternPathSegment.Parts[0]); - Assert.Equal(paramPartC.Name, ((RoutePatternParameterPart) routePatternPathSegment.Parts[0]).Name); - Assert.IsType(routePatternPathSegment.Parts[1]); - Assert.Equal(literalPartA.Content, ((RoutePatternLiteralPart) routePatternPathSegment.Parts[1]).Content); - Assert.IsType(routePatternPathSegment.Parts[2]); - Assert.Equal(separatorPartE.Content, ((RoutePatternSeparatorPart) routePatternPathSegment.Parts[2]).Content); - Assert.IsType(routePatternPathSegment.Parts[3]); - Assert.Equal(paramPartB.Name, ((RoutePatternParameterPart) routePatternPathSegment.Parts[3]).Name); - } + // Assert + Assert.Equal(4, routePatternPathSegment.Parts.Count); + Assert.IsType(routePatternPathSegment.Parts[0]); + Assert.Equal(paramPartC.Name, ((RoutePatternParameterPart)routePatternPathSegment.Parts[0]).Name); + Assert.IsType(routePatternPathSegment.Parts[1]); + Assert.Equal(literalPartA.Content, ((RoutePatternLiteralPart)routePatternPathSegment.Parts[1]).Content); + Assert.IsType(routePatternPathSegment.Parts[2]); + Assert.Equal(separatorPartE.Content, ((RoutePatternSeparatorPart)routePatternPathSegment.Parts[2]).Content); + Assert.IsType(routePatternPathSegment.Parts[3]); + Assert.Equal(paramPartB.Name, ((RoutePatternParameterPart)routePatternPathSegment.Parts[3]).Name); } } diff --git a/src/Http/Routing/test/UnitTests/TemplateParserDefaultValuesTests.cs b/src/Http/Routing/test/UnitTests/TemplateParserDefaultValuesTests.cs index 760c46aa77..bd12008e45 100644 --- a/src/Http/Routing/test/UnitTests/TemplateParserDefaultValuesTests.cs +++ b/src/Http/Routing/test/UnitTests/TemplateParserDefaultValuesTests.cs @@ -8,143 +8,142 @@ using Microsoft.Extensions.Options; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tests +namespace Microsoft.AspNetCore.Routing.Tests; + +public class TemplateParserDefaultValuesTests { - public class TemplateParserDefaultValuesTests + private static readonly IInlineConstraintResolver _inlineConstraintResolver = GetInlineConstraintResolver(); + + [Fact] + public void InlineDefaultValueSpecified_InlineValueIsUsed() { - private static readonly IInlineConstraintResolver _inlineConstraintResolver = GetInlineConstraintResolver(); + // Arrange & Act + var routeBuilder = CreateRouteBuilder(); + + // Act + routeBuilder.MapRoute("mockName", + "{controller}/{action}/{id:int=12}", + defaults: null, + constraints: null); + + // Assert + var defaults = ((Route)routeBuilder.Routes[0]).Defaults; + Assert.Equal("12", defaults["id"]); + } - [Fact] - public void InlineDefaultValueSpecified_InlineValueIsUsed() - { - // Arrange & Act - var routeBuilder = CreateRouteBuilder(); + [Theory] + [InlineData(@"{controller}/{action}/{p1:regex(([}}])\w+)=}}asd}", "}asd")] + [InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)=12/12/1234}", @"12/12/1234")] + public void InlineDefaultValueSpecified_WithSpecialCharacters(string template, string value) + { + // Arrange & Act + var routeBuilder = CreateRouteBuilder(); + + // Act + routeBuilder.MapRoute("mockName", + template, + defaults: null, + constraints: null); + + // Assert + var defaults = ((Route)routeBuilder.Routes[0]).Defaults; + Assert.Equal(value, defaults["p1"]); + } - // Act - routeBuilder.MapRoute("mockName", - "{controller}/{action}/{id:int=12}", - defaults: null, - constraints: null); + [Fact] + public void ExplicitDefaultValueSpecified_WithInlineDefaultValue_Throws() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); + + // Act & Assert + var ex = Assert.Throws( + () => routeBuilder.MapRoute("mockName", + "{controller}/{action}/{id:int=12}", + defaults: new { id = 13 }, + constraints: null)); + + var message = "An error occurred while creating the route with name 'mockName' and template" + + " '{controller}/{action}/{id:int=12}'."; + Assert.Equal(message, ex.Message); + + Assert.NotNull(ex.InnerException); + message = "The route parameter 'id' has both an inline default value and an explicit default" + + " value specified. A route parameter cannot contain an inline default value when" + + " a default value is specified explicitly. Consider removing one of them."; + Assert.Equal(message, ex.InnerException.Message); + } - // Assert - var defaults = ((Route)routeBuilder.Routes[0]).Defaults; - Assert.Equal("12", defaults["id"]); - } + [Fact] + [ReplaceCulture] + public void EmptyDefaultValue_WithOptionalParameter_Throws() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); + + // Act & Assert + var ex = Assert.Throws( + () => routeBuilder.MapRoute("mockName", + "{controller}/{action}/{id:int=?}", + defaults: new { id = 13 }, + constraints: null)); + + var message = "An error occurred while creating the route with name 'mockName' and template" + + " '{controller}/{action}/{id:int=?}'."; + Assert.Equal(message, ex.Message); + + Assert.NotNull(ex.InnerException); + message = "An optional parameter cannot have default value. (Parameter 'routeTemplate')"; + Assert.Equal(message, ex.InnerException.Message); + } - [Theory] - [InlineData(@"{controller}/{action}/{p1:regex(([}}])\w+)=}}asd}", "}asd")] - [InlineData(@"{p1:regex(^\d{{1,2}}\/\d{{1,2}}\/\d{{4}}$)=12/12/1234}", @"12/12/1234")] - public void InlineDefaultValueSpecified_WithSpecialCharacters(string template, string value) - { - // Arrange & Act - var routeBuilder = CreateRouteBuilder(); + [Fact] + [ReplaceCulture] + public void NonEmptyDefaultValue_WithOptionalParameter_Throws() + { + // Arrange + var routeBuilder = CreateRouteBuilder(); - // Act - routeBuilder.MapRoute("mockName", - template, - defaults: null, + // Act & Assert + var ex = Assert.Throws(() => + { + routeBuilder.MapRoute( + "mockName", + "{controller}/{action}/{id:int=12?}", + defaults: new { id = 13 }, constraints: null); + }); - // Assert - var defaults = ((Route)routeBuilder.Routes[0]).Defaults; - Assert.Equal(value, defaults["p1"]); - } - - [Fact] - public void ExplicitDefaultValueSpecified_WithInlineDefaultValue_Throws() - { - // Arrange - var routeBuilder = CreateRouteBuilder(); - - // Act & Assert - var ex = Assert.Throws( - () => routeBuilder.MapRoute("mockName", - "{controller}/{action}/{id:int=12}", - defaults: new { id = 13 }, - constraints: null)); - - var message = "An error occurred while creating the route with name 'mockName' and template" + - " '{controller}/{action}/{id:int=12}'."; - Assert.Equal(message, ex.Message); - - Assert.NotNull(ex.InnerException); - message = "The route parameter 'id' has both an inline default value and an explicit default" + - " value specified. A route parameter cannot contain an inline default value when" + - " a default value is specified explicitly. Consider removing one of them."; - Assert.Equal(message, ex.InnerException.Message); - } - - [Fact] - [ReplaceCulture] - public void EmptyDefaultValue_WithOptionalParameter_Throws() - { - // Arrange - var routeBuilder = CreateRouteBuilder(); - - // Act & Assert - var ex = Assert.Throws( - () => routeBuilder.MapRoute("mockName", - "{controller}/{action}/{id:int=?}", - defaults: new { id = 13 }, - constraints: null)); - - var message = "An error occurred while creating the route with name 'mockName' and template" + - " '{controller}/{action}/{id:int=?}'."; - Assert.Equal(message, ex.Message); - - Assert.NotNull(ex.InnerException); - message = "An optional parameter cannot have default value. (Parameter 'routeTemplate')"; - Assert.Equal(message, ex.InnerException.Message); - } - - [Fact] - [ReplaceCulture] - public void NonEmptyDefaultValue_WithOptionalParameter_Throws() - { - // Arrange - var routeBuilder = CreateRouteBuilder(); - - // Act & Assert - var ex = Assert.Throws(() => - { - routeBuilder.MapRoute( - "mockName", - "{controller}/{action}/{id:int=12?}", - defaults: new { id = 13 }, - constraints: null); - }); - - var message = "An error occurred while creating the route with name 'mockName' and template" + - " '{controller}/{action}/{id:int=12?}'."; - Assert.Equal(message, ex.Message); - - Assert.NotNull(ex.InnerException); - message = "An optional parameter cannot have default value. (Parameter 'routeTemplate')"; - Assert.Equal(message, ex.InnerException.Message); - } - - private static IRouteBuilder CreateRouteBuilder() - { - var services = new ServiceCollection(); - services.AddSingleton(_inlineConstraintResolver); - services.AddSingleton(); - services.AddSingleton(); - services.Configure(options => { }); + var message = "An error occurred while creating the route with name 'mockName' and template" + + " '{controller}/{action}/{id:int=12?}'."; + Assert.Equal(message, ex.Message); - var applicationBuilder = Mock.Of(); - applicationBuilder.ApplicationServices = services.BuildServiceProvider(); + Assert.NotNull(ex.InnerException); + message = "An optional parameter cannot have default value. (Parameter 'routeTemplate')"; + Assert.Equal(message, ex.InnerException.Message); + } - var routeBuilder = new RouteBuilder(applicationBuilder); - routeBuilder.DefaultHandler = Mock.Of(); - return routeBuilder; - } + private static IRouteBuilder CreateRouteBuilder() + { + var services = new ServiceCollection(); + services.AddSingleton(_inlineConstraintResolver); + services.AddSingleton(); + services.AddSingleton(); + services.Configure(options => { }); + + var applicationBuilder = Mock.Of(); + applicationBuilder.ApplicationServices = services.BuildServiceProvider(); + + var routeBuilder = new RouteBuilder(applicationBuilder); + routeBuilder.DefaultHandler = Mock.Of(); + return routeBuilder; + } - private static IInlineConstraintResolver GetInlineConstraintResolver() - { - var services = new ServiceCollection().AddOptions(); - var serviceProvider = services.BuildServiceProvider(); - var accessor = serviceProvider.GetRequiredService>(); - return new DefaultInlineConstraintResolver(accessor, serviceProvider); - } + private static IInlineConstraintResolver GetInlineConstraintResolver() + { + var services = new ServiceCollection().AddOptions(); + var serviceProvider = services.BuildServiceProvider(); + var accessor = serviceProvider.GetRequiredService>(); + return new DefaultInlineConstraintResolver(accessor, serviceProvider); } } diff --git a/src/Http/Routing/test/UnitTests/TestConstants.cs b/src/Http/Routing/test/UnitTests/TestConstants.cs index 6a09494aa5..73e3f5ba47 100644 --- a/src/Http/Routing/test/UnitTests/TestConstants.cs +++ b/src/Http/Routing/test/UnitTests/TestConstants.cs @@ -4,10 +4,9 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public static class TestConstants { - public static class TestConstants - { - internal static readonly RequestDelegate EmptyRequestDelegate = (context) => Task.CompletedTask; - } + internal static readonly RequestDelegate EmptyRequestDelegate = (context) => Task.CompletedTask; } diff --git a/src/Http/Routing/test/UnitTests/TestObjects/CapturingConstraint.cs b/src/Http/Routing/test/UnitTests/TestObjects/CapturingConstraint.cs index 501a27ed23..cb14f1ed4b 100644 --- a/src/Http/Routing/test/UnitTests/TestObjects/CapturingConstraint.cs +++ b/src/Http/Routing/test/UnitTests/TestObjects/CapturingConstraint.cs @@ -4,21 +4,20 @@ using System.Collections.Generic; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.Routing.TestObjects +namespace Microsoft.AspNetCore.Routing.TestObjects; + +internal class CapturingConstraint : IRouteConstraint { - internal class CapturingConstraint : IRouteConstraint - { - public IDictionary Values { get; private set; } + public IDictionary Values { get; private set; } - public bool Match( - HttpContext httpContext, - IRouter route, - string routeKey, - RouteValueDictionary values, - RouteDirection routeDirection) - { - Values = new RouteValueDictionary(values); - return true; - } + public bool Match( + HttpContext httpContext, + IRouter route, + string routeKey, + RouteValueDictionary values, + RouteDirection routeDirection) + { + Values = new RouteValueDictionary(values); + return true; } } diff --git a/src/Http/Routing/test/UnitTests/TestObjects/DynamicEndpointDataSource.cs b/src/Http/Routing/test/UnitTests/TestObjects/DynamicEndpointDataSource.cs index c4d2175413..67d81494f7 100644 --- a/src/Http/Routing/test/UnitTests/TestObjects/DynamicEndpointDataSource.cs +++ b/src/Http/Routing/test/UnitTests/TestObjects/DynamicEndpointDataSource.cs @@ -6,49 +6,48 @@ using System.Threading; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.Routing.TestObjects +namespace Microsoft.AspNetCore.Routing.TestObjects; + +public class DynamicEndpointDataSource : EndpointDataSource { - public class DynamicEndpointDataSource : EndpointDataSource + private readonly List _endpoints; + private CancellationTokenSource _cts; + private CancellationChangeToken _changeToken; + private readonly object _lock; + + public DynamicEndpointDataSource(params Endpoint[] endpoints) + { + _endpoints = new List(); + _endpoints.AddRange(endpoints); + _lock = new object(); + + CreateChangeToken(); + } + + public override IChangeToken GetChangeToken() => _changeToken; + + public override IReadOnlyList Endpoints => _endpoints; + + // Trigger change + public void AddEndpoint(Endpoint endpoint) + { + _endpoints.Add(endpoint); + + // Capture the old tokens so that we can raise the callbacks on them. This is important so that + // consumers do not register callbacks on an inflight event causing a stackoverflow. + var oldTokenSource = _cts; + var oldToken = _changeToken; + + CreateChangeToken(); + + // Raise consumer callbacks. Any new callback registration would happen on the new token + // created in earlier step. + oldTokenSource.Cancel(); + } + + private void CreateChangeToken() { - private readonly List _endpoints; - private CancellationTokenSource _cts; - private CancellationChangeToken _changeToken; - private readonly object _lock; - - public DynamicEndpointDataSource(params Endpoint[] endpoints) - { - _endpoints = new List(); - _endpoints.AddRange(endpoints); - _lock = new object(); - - CreateChangeToken(); - } - - public override IChangeToken GetChangeToken() => _changeToken; - - public override IReadOnlyList Endpoints => _endpoints; - - // Trigger change - public void AddEndpoint(Endpoint endpoint) - { - _endpoints.Add(endpoint); - - // Capture the old tokens so that we can raise the callbacks on them. This is important so that - // consumers do not register callbacks on an inflight event causing a stackoverflow. - var oldTokenSource = _cts; - var oldToken = _changeToken; - - CreateChangeToken(); - - // Raise consumer callbacks. Any new callback registration would happen on the new token - // created in earlier step. - oldTokenSource.Cancel(); - } - - private void CreateChangeToken() - { - _cts = new CancellationTokenSource(); - _changeToken = new CancellationChangeToken(_cts.Token); - } + _cts = new CancellationTokenSource(); + _changeToken = new CancellationChangeToken(_cts.Token); } -} \ No newline at end of file +} diff --git a/src/Http/Routing/test/UnitTests/TestObjects/SlugifyParameterTransformer.cs b/src/Http/Routing/test/UnitTests/TestObjects/SlugifyParameterTransformer.cs index 209b4a106b..cfbb3c6f6f 100644 --- a/src/Http/Routing/test/UnitTests/TestObjects/SlugifyParameterTransformer.cs +++ b/src/Http/Routing/test/UnitTests/TestObjects/SlugifyParameterTransformer.cs @@ -3,14 +3,13 @@ using System.Text.RegularExpressions; -namespace Microsoft.AspNetCore.Routing.TestObjects +namespace Microsoft.AspNetCore.Routing.TestObjects; + +public class SlugifyParameterTransformer : IOutboundParameterTransformer { - public class SlugifyParameterTransformer : IOutboundParameterTransformer + public string TransformOutbound(object value) { - public string TransformOutbound(object value) - { - // Slugify value - return value == null ? null : Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLowerInvariant(); - } + // Slugify value + return value == null ? null : Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLowerInvariant(); } } diff --git a/src/Http/Routing/test/UnitTests/TestObjects/TestMatcher.cs b/src/Http/Routing/test/UnitTests/TestObjects/TestMatcher.cs index 4982b8d8b6..8c234537a3 100644 --- a/src/Http/Routing/test/UnitTests/TestObjects/TestMatcher.cs +++ b/src/Http/Routing/test/UnitTests/TestObjects/TestMatcher.cs @@ -6,26 +6,25 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.TestObjects +namespace Microsoft.AspNetCore.Routing.TestObjects; + +internal class TestMatcher : Matcher { - internal class TestMatcher : Matcher + private readonly bool _isHandled; + + public TestMatcher(bool isHandled) { - private readonly bool _isHandled; + _isHandled = isHandled; + } - public TestMatcher(bool isHandled) + public override Task MatchAsync(HttpContext httpContext) + { + if (_isHandled) { - _isHandled = isHandled; + httpContext.Request.RouteValues = new RouteValueDictionary(new { controller = "Home", action = "Index" }); + httpContext.SetEndpoint(new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "Test endpoint")); } - public override Task MatchAsync(HttpContext httpContext) - { - if (_isHandled) - { - httpContext.Request.RouteValues = new RouteValueDictionary(new { controller = "Home", action = "Index" }); - httpContext.SetEndpoint(new Endpoint(TestConstants.EmptyRequestDelegate, EndpointMetadataCollection.Empty, "Test endpoint")); - } - - return Task.CompletedTask; - } + return Task.CompletedTask; } } diff --git a/src/Http/Routing/test/UnitTests/TestObjects/TestMatcherFactory.cs b/src/Http/Routing/test/UnitTests/TestObjects/TestMatcherFactory.cs index 9894c590f0..ece8696eba 100644 --- a/src/Http/Routing/test/UnitTests/TestObjects/TestMatcherFactory.cs +++ b/src/Http/Routing/test/UnitTests/TestObjects/TestMatcherFactory.cs @@ -3,20 +3,19 @@ using Microsoft.AspNetCore.Routing.Matching; -namespace Microsoft.AspNetCore.Routing.TestObjects +namespace Microsoft.AspNetCore.Routing.TestObjects; + +internal class TestMatcherFactory : MatcherFactory { - internal class TestMatcherFactory : MatcherFactory - { - private readonly bool _isHandled; + private readonly bool _isHandled; - public TestMatcherFactory(bool isHandled) - { - _isHandled = isHandled; - } + public TestMatcherFactory(bool isHandled) + { + _isHandled = isHandled; + } - public override Matcher CreateMatcher(EndpointDataSource dataSource) - { - return new TestMatcher(_isHandled); - } + public override Matcher CreateMatcher(EndpointDataSource dataSource) + { + return new TestMatcher(_isHandled); } -} \ No newline at end of file +} diff --git a/src/Http/Routing/test/UnitTests/TestObjects/TestServiceProvider.cs b/src/Http/Routing/test/UnitTests/TestObjects/TestServiceProvider.cs index 95a82088d9..279bf80b66 100644 --- a/src/Http/Routing/test/UnitTests/TestObjects/TestServiceProvider.cs +++ b/src/Http/Routing/test/UnitTests/TestObjects/TestServiceProvider.cs @@ -3,13 +3,12 @@ using System; -namespace Microsoft.AspNetCore.Routing.TestObjects +namespace Microsoft.AspNetCore.Routing.TestObjects; + +internal class TestServiceProvider : IServiceProvider { - internal class TestServiceProvider : IServiceProvider + public object GetService(Type serviceType) { - public object GetService(Type serviceType) - { - throw new NotImplementedException(); - } + throw new NotImplementedException(); } -} \ No newline at end of file +} diff --git a/src/Http/Routing/test/UnitTests/TestObjects/UpperCaseParameterTransform.cs b/src/Http/Routing/test/UnitTests/TestObjects/UpperCaseParameterTransform.cs index b5bb1e8064..072e823180 100644 --- a/src/Http/Routing/test/UnitTests/TestObjects/UpperCaseParameterTransform.cs +++ b/src/Http/Routing/test/UnitTests/TestObjects/UpperCaseParameterTransform.cs @@ -1,13 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.Routing.TestObjects +namespace Microsoft.AspNetCore.Routing.TestObjects; + +public class UpperCaseParameterTransform : IOutboundParameterTransformer { - public class UpperCaseParameterTransform : IOutboundParameterTransformer + public string TransformOutbound(object value) { - public string TransformOutbound(object value) - { - return value?.ToString()?.ToUpperInvariant(); - } + return value?.ToString()?.ToUpperInvariant(); } } diff --git a/src/Http/Routing/test/UnitTests/Tree/LinkGenerationDecisionTreeTest.cs b/src/Http/Routing/test/UnitTests/Tree/LinkGenerationDecisionTreeTest.cs index e01c33908c..9e44787811 100644 --- a/src/Http/Routing/test/UnitTests/Tree/LinkGenerationDecisionTreeTest.cs +++ b/src/Http/Routing/test/UnitTests/Tree/LinkGenerationDecisionTreeTest.cs @@ -9,761 +9,760 @@ using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.AspNetCore.Routing.Template; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tree +namespace Microsoft.AspNetCore.Routing.Tree; + +public class LinkGenerationDecisionTreeTest { - public class LinkGenerationDecisionTreeTest + [Fact] + public void GetMatches_AllowsNullAmbientValues() { - [Fact] - public void GetMatches_AllowsNullAmbientValues() - { - // Arrange - var entries = new List(); + // Arrange + var entries = new List(); - var entry = CreateMatch(new { }); - entries.Add(entry); + var entry = CreateMatch(new { }); + entries.Add(entry); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(new { }); + var context = CreateContext(new { }); - // Act - var matches = tree.GetMatches(context.Values, ambientValues: null); + // Act + var matches = tree.GetMatches(context.Values, ambientValues: null); - // Assert - Assert.Same(entry, Assert.Single(matches).Match); - } + // Assert + Assert.Same(entry, Assert.Single(matches).Match); + } - [Fact] - public void SelectSingleEntry_NoCriteria() - { - // Arrange - var entries = new List(); + [Fact] + public void SelectSingleEntry_NoCriteria() + { + // Arrange + var entries = new List(); - var entry = CreateMatch(new { }); - entries.Add(entry); + var entry = CreateMatch(new { }); + entries.Add(entry); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(new { }); + var context = CreateContext(new { }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues); - // Assert - Assert.Same(entry, Assert.Single(matches).Match); - } + // Assert + Assert.Same(entry, Assert.Single(matches).Match); + } - [Fact] - public void SelectSingleEntry_MultipleCriteria() - { - // Arrange - var entries = new List(); + [Fact] + public void SelectSingleEntry_MultipleCriteria() + { + // Arrange + var entries = new List(); - var entry = CreateMatch(new { controller = "Store", action = "Buy" }); - entries.Add(entry); + var entry = CreateMatch(new { controller = "Store", action = "Buy" }); + entries.Add(entry); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(new { controller = "Store", action = "Buy" }); + var context = CreateContext(new { controller = "Store", action = "Buy" }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues); - // Assert - Assert.Same(entry, Assert.Single(matches).Match); - } + // Assert + Assert.Same(entry, Assert.Single(matches).Match); + } - [Fact] - public void SelectSingleEntry_MultipleCriteria_AmbientValues() - { - // Arrange - var entries = new List(); + [Fact] + public void SelectSingleEntry_MultipleCriteria_AmbientValues() + { + // Arrange + var entries = new List(); - var entry = CreateMatch(new { controller = "Store", action = "Buy" }); - entries.Add(entry); + var entry = CreateMatch(new { controller = "Store", action = "Buy" }); + entries.Add(entry); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(values: null, ambientValues: new { controller = "Store", action = "Buy" }); + var context = CreateContext(values: null, ambientValues: new { controller = "Store", action = "Buy" }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues); - // Assert - var match = Assert.Single(matches); - Assert.Same(entry, match.Match); - Assert.False(match.IsFallbackMatch); - } + // Assert + var match = Assert.Single(matches); + Assert.Same(entry, match.Match); + Assert.False(match.IsFallbackMatch); + } - [Fact] - public void SelectSingleEntry_MultipleCriteria_Replaced() - { - // Arrange - var entries = new List(); + [Fact] + public void SelectSingleEntry_MultipleCriteria_Replaced() + { + // Arrange + var entries = new List(); - var entry = CreateMatch(new { controller = "Store", action = "Buy" }); - entries.Add(entry); + var entry = CreateMatch(new { controller = "Store", action = "Buy" }); + entries.Add(entry); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext( - values: new { action = "Buy" }, - ambientValues: new { controller = "Store", action = "Cart" }); + var context = CreateContext( + values: new { action = "Buy" }, + ambientValues: new { controller = "Store", action = "Cart" }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues); - // Assert - var match = Assert.Single(matches); - Assert.Same(entry, match.Match); - Assert.False(match.IsFallbackMatch); - } + // Assert + var match = Assert.Single(matches); + Assert.Same(entry, match.Match); + Assert.False(match.IsFallbackMatch); + } - [Fact] - public void SelectSingleEntry_MultipleCriteria_AmbientValue_Ignored() - { - // Arrange - var entries = new List(); + [Fact] + public void SelectSingleEntry_MultipleCriteria_AmbientValue_Ignored() + { + // Arrange + var entries = new List(); - var entry = CreateMatch(new { controller = "Store", action = (string)null }); - entries.Add(entry); + var entry = CreateMatch(new { controller = "Store", action = (string)null }); + entries.Add(entry); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext( - values: new { controller = "Store" }, - ambientValues: new { controller = "Store", action = "Buy" }); + var context = CreateContext( + values: new { controller = "Store" }, + ambientValues: new { controller = "Store", action = "Buy" }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues); - // Assert - var match = Assert.Single(matches); - Assert.Same(entry, match.Match); - Assert.True(match.IsFallbackMatch); - } + // Assert + var match = Assert.Single(matches); + Assert.Same(entry, match.Match); + Assert.True(match.IsFallbackMatch); + } - [Fact] - public void SelectSingleEntry_MultipleCriteria_NoMatch() - { - // Arrange - var entries = new List(); + [Fact] + public void SelectSingleEntry_MultipleCriteria_NoMatch() + { + // Arrange + var entries = new List(); - var entry = CreateMatch(new { controller = "Store", action = "Buy" }); - entries.Add(entry); + var entry = CreateMatch(new { controller = "Store", action = "Buy" }); + entries.Add(entry); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(new { controller = "Store", action = "AddToCart" }); + var context = CreateContext(new { controller = "Store", action = "AddToCart" }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues); - // Assert - Assert.Empty(matches); - } + // Assert + Assert.Empty(matches); + } - [Fact] - public void SelectSingleEntry_MultipleCriteria_AmbientValue_NoMatch() - { - // Arrange - var entries = new List(); + [Fact] + public void SelectSingleEntry_MultipleCriteria_AmbientValue_NoMatch() + { + // Arrange + var entries = new List(); - var entry = CreateMatch(new { controller = "Store", action = "Buy" }); - entries.Add(entry); + var entry = CreateMatch(new { controller = "Store", action = "Buy" }); + entries.Add(entry); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext( - values: new { controller = "Store" }, - ambientValues: new { controller = "Store", action = "Cart" }); + var context = CreateContext( + values: new { controller = "Store" }, + ambientValues: new { controller = "Store", action = "Cart" }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues); - // Assert - Assert.Empty(matches); - } + // Assert + Assert.Empty(matches); + } - [Fact] - public void SelectMultipleEntries_OneDoesntMatch() - { - // Arrange - var entries = new List(); + [Fact] + public void SelectMultipleEntries_OneDoesntMatch() + { + // Arrange + var entries = new List(); - var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); - entries.Add(entry1); + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); + entries.Add(entry1); - var entry2 = CreateMatch(new { controller = "Store", action = "Cart" }); - entries.Add(entry2); + var entry2 = CreateMatch(new { controller = "Store", action = "Cart" }); + entries.Add(entry2); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext( - values: new { controller = "Store" }, - ambientValues: new { controller = "Store", action = "Buy" }); + var context = CreateContext( + values: new { controller = "Store" }, + ambientValues: new { controller = "Store", action = "Buy" }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues); - // Assert - Assert.Same(entry1, Assert.Single(matches).Match); - } + // Assert + Assert.Same(entry1, Assert.Single(matches).Match); + } - [Fact] - public void SelectMultipleEntries_BothMatch_CriteriaSubset() - { - // Arrange - var entries = new List(); + [Fact] + public void SelectMultipleEntries_BothMatch_CriteriaSubset() + { + // Arrange + var entries = new List(); - var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); - entries.Add(entry1); + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); + entries.Add(entry1); - var entry2 = CreateMatch(new { controller = "Store" }); - entry2.Entry.Order = 1; - entries.Add(entry2); + var entry2 = CreateMatch(new { controller = "Store" }); + entry2.Entry.Order = 1; + entries.Add(entry2); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext( - values: new { controller = "Store" }, - ambientValues: new { controller = "Store", action = "Buy" }); + var context = CreateContext( + values: new { controller = "Store" }, + ambientValues: new { controller = "Store", action = "Buy" }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - // Assert - Assert.Equal(entries, matches); - } + // Assert + Assert.Equal(entries, matches); + } - [Fact] - public void SelectMultipleEntries_BothMatch_NonOverlappingCriteria() - { - // Arrange - var entries = new List(); + [Fact] + public void SelectMultipleEntries_BothMatch_NonOverlappingCriteria() + { + // Arrange + var entries = new List(); - var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); - entries.Add(entry1); + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); + entries.Add(entry1); - var entry2 = CreateMatch(new { slug = "1234" }); - entry2.Entry.Order = 1; - entries.Add(entry2); + var entry2 = CreateMatch(new { slug = "1234" }); + entry2.Entry.Order = 1; + entries.Add(entry2); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(new { controller = "Store", action = "Buy", slug = "1234" }); + var context = CreateContext(new { controller = "Store", action = "Buy", slug = "1234" }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - // Assert - Assert.Equal(entries, matches); - } + // Assert + Assert.Equal(entries, matches); + } - // Precedence is ignored for sorting because they have different order - [Fact] - public void SelectMultipleEntries_BothMatch_OrderedByOrder() - { - // Arrange - var entries = new List(); + // Precedence is ignored for sorting because they have different order + [Fact] + public void SelectMultipleEntries_BothMatch_OrderedByOrder() + { + // Arrange + var entries = new List(); - var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); - entry1.Entry.Precedence = 0; - entries.Add(entry1); + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry1.Entry.Precedence = 0; + entries.Add(entry1); - var entry2 = CreateMatch(new { controller = "Store", action = "Buy" }); - entry2.Entry.Order = 1; - entry2.Entry.Precedence = 1; - entries.Add(entry2); + var entry2 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry2.Entry.Order = 1; + entry2.Entry.Precedence = 1; + entries.Add(entry2); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(new { controller = "Store", action = "Buy" }); + var context = CreateContext(new { controller = "Store", action = "Buy" }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - // Assert - Assert.Equal(entries, matches); - } + // Assert + Assert.Equal(entries, matches); + } - // Precedence is used for sorting because they have the same order - [Fact] - public void SelectMultipleEntries_BothMatch_OrderedByPrecedence() - { - // Arrange - var entries = new List(); + // Precedence is used for sorting because they have the same order + [Fact] + public void SelectMultipleEntries_BothMatch_OrderedByPrecedence() + { + // Arrange + var entries = new List(); - var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); - entry1.Entry.Precedence = 1; - entries.Add(entry1); + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry1.Entry.Precedence = 1; + entries.Add(entry1); - var entry2 = CreateMatch(new { controller = "Store", action = "Buy" }); - entry2.Entry.Precedence = 0; - entries.Add(entry2); + var entry2 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry2.Entry.Precedence = 0; + entries.Add(entry2); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(new { controller = "Store", action = "Buy" }); + var context = CreateContext(new { controller = "Store", action = "Buy" }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - // Assert - Assert.Equal(entries, matches); - } + // Assert + Assert.Equal(entries, matches); + } - // Template is used for sorting because they have the same order - [Fact] - public void SelectMultipleEntries_BothMatch_OrderedByTemplate() - { - // Arrange - var entries = new List(); + // Template is used for sorting because they have the same order + [Fact] + public void SelectMultipleEntries_BothMatch_OrderedByTemplate() + { + // Arrange + var entries = new List(); - var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); - entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); - entries.Add(entry1); + var entry1 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); + entries.Add(entry1); - var entry2 = CreateMatch(new { controller = "Store", action = "Buy" }); - entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); - entries.Add(entry2); + var entry2 = CreateMatch(new { controller = "Store", action = "Buy" }); + entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); + entries.Add(entry2); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(new { controller = "Store", action = "Buy" }); + var context = CreateContext(new { controller = "Store", action = "Buy" }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - // Assert - Assert.Equal(entries, matches); - } + // Assert + Assert.Equal(entries, matches); + } - [Fact] - public void GetMatches_ControllersWithArea_AllValuesExplicit() - { - // Arrange - var entries = new List(); + [Fact] + public void GetMatches_ControllersWithArea_AllValuesExplicit() + { + // Arrange + var entries = new List(); - var entry1 = CreateMatch(new { controller = "Store", action = "Buy", area = (string)null, }); - entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); - entries.Add(entry1); + var entry1 = CreateMatch(new { controller = "Store", action = "Buy", area = (string)null, }); + entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); + entries.Add(entry1); - var entry2 = CreateMatch(new { controller = "Store", action = "Buy", area = "Admin" }); - entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); - entries.Add(entry2); + var entry2 = CreateMatch(new { controller = "Store", action = "Buy", area = "Admin" }); + entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); + entries.Add(entry2); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(new { controller = "Store", action = "Buy", area = "Admin" }); + var context = CreateContext(new { controller = "Store", action = "Buy", area = "Admin" }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - // Assert - // Assert - Assert.Collection( - matches, - m => { Assert.Same(entry2, m); }); - } + // Assert + // Assert + Assert.Collection( + matches, + m => { Assert.Same(entry2, m); }); + } - [Fact] - public void GetMatches_ControllersWithArea_SomeValuesAmbient() - { - // Arrange - var entries = new List(); + [Fact] + public void GetMatches_ControllersWithArea_SomeValuesAmbient() + { + // Arrange + var entries = new List(); - var entry1 = CreateMatch(new { controller = "Store", action = "Buy", area = (string)null, }); - entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); - entries.Add(entry1); + var entry1 = CreateMatch(new { controller = "Store", action = "Buy", area = (string)null, }); + entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); + entries.Add(entry1); - var entry2 = CreateMatch(new { controller = "Store", action = "Buy", area = "Admin" }); - entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); - entries.Add(entry2); + var entry2 = CreateMatch(new { controller = "Store", action = "Buy", area = "Admin" }); + entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); + entries.Add(entry2); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(new { controller = "Store", }, new { action = "Buy", area = "Admin", }); + var context = CreateContext(new { controller = "Store", }, new { action = "Buy", area = "Admin", }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - // Assert - Assert.Collection( - matches, - m => { Assert.Same(entry2, m); }, - m => { Assert.Same(entry1, m); }); - } + // Assert + Assert.Collection( + matches, + m => { Assert.Same(entry2, m); }, + m => { Assert.Same(entry1, m); }); + } - [Fact] - public void GetMatches_ControllersWithArea_AllValuesAmbient() - { - // Arrange - var entries = new List(); + [Fact] + public void GetMatches_ControllersWithArea_AllValuesAmbient() + { + // Arrange + var entries = new List(); - var entry1 = CreateMatch(new { controller = "Store", action = "Buy", area = (string)null, }); - entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); - entries.Add(entry1); + var entry1 = CreateMatch(new { controller = "Store", action = "Buy", area = (string)null, }); + entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); + entries.Add(entry1); - var entry2 = CreateMatch(new { controller = "Store", action = "Buy", area = "Admin" }); - entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); - entries.Add(entry2); + var entry2 = CreateMatch(new { controller = "Store", action = "Buy", area = "Admin" }); + entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); + entries.Add(entry2); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(new { }, new { controller = "Store", action = "Buy", area = "Admin", }); + var context = CreateContext(new { }, new { controller = "Store", action = "Buy", area = "Admin", }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - // Assert - Assert.Collection( - matches, - m => { Assert.Same(entry2, m); }, - m => { Assert.Same(entry1, m); }); - } + // Assert + Assert.Collection( + matches, + m => { Assert.Same(entry2, m); }, + m => { Assert.Same(entry1, m); }); + } - [Fact] - public void GetMatches_PagesWithArea_AllValuesExplicit() - { - // Arrange - var entries = new List(); + [Fact] + public void GetMatches_PagesWithArea_AllValuesExplicit() + { + // Arrange + var entries = new List(); - var entry1 = CreateMatch(new { page = "/Store/Buy", area = (string)null, }); - entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); - entries.Add(entry1); + var entry1 = CreateMatch(new { page = "/Store/Buy", area = (string)null, }); + entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); + entries.Add(entry1); - var entry2 = CreateMatch(new { page = "/Store/Buy", area = "Admin" }); - entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); - entries.Add(entry2); + var entry2 = CreateMatch(new { page = "/Store/Buy", area = "Admin" }); + entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); + entries.Add(entry2); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(new { page = "/Store/Buy", area = "Admin" }); + var context = CreateContext(new { page = "/Store/Buy", area = "Admin" }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - // Assert - Assert.Collection( - matches, - m => { Assert.Same(entry2, m); }); - } + // Assert + Assert.Collection( + matches, + m => { Assert.Same(entry2, m); }); + } - [Fact] - public void GetMatches_PagesWithArea_SomeValuesAmbient() - { - // Arrange - var entries = new List(); + [Fact] + public void GetMatches_PagesWithArea_SomeValuesAmbient() + { + // Arrange + var entries = new List(); - var entry1 = CreateMatch(new { page = "/Store/Buy", area = (string)null, }); - entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); - entries.Add(entry1); + var entry1 = CreateMatch(new { page = "/Store/Buy", area = (string)null, }); + entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); + entries.Add(entry1); - var entry2 = CreateMatch(new { page = "/Store/Buy", area = "Admin" }); - entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); - entries.Add(entry2); + var entry2 = CreateMatch(new { page = "/Store/Buy", area = "Admin" }); + entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); + entries.Add(entry2); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(new { page = "/Store/Buy", }, new { area = "Admin", }); + var context = CreateContext(new { page = "/Store/Buy", }, new { area = "Admin", }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - // Assert - Assert.Collection( - matches, - m => { Assert.Same(entry2, m); }, - m => { Assert.Same(entry1, m); }); - } + // Assert + Assert.Collection( + matches, + m => { Assert.Same(entry2, m); }, + m => { Assert.Same(entry1, m); }); + } - [Fact] - public void GetMatches_PagesWithArea_AllValuesAmbient() - { - // Arrange - var entries = new List(); + [Fact] + public void GetMatches_PagesWithArea_AllValuesAmbient() + { + // Arrange + var entries = new List(); - var entry1 = CreateMatch(new { page = "/Store/Buy", area = (string)null, }); - entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); - entries.Add(entry1); + var entry1 = CreateMatch(new { page = "/Store/Buy", area = (string)null, }); + entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); + entries.Add(entry1); - var entry2 = CreateMatch(new { page = "/Store/Buy", area = "Admin" }); - entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); - entries.Add(entry2); + var entry2 = CreateMatch(new { page = "/Store/Buy", area = "Admin" }); + entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); + entries.Add(entry2); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(new { }, new { page = "/Store/Buy", area = "Admin", }); + var context = CreateContext(new { }, new { page = "/Store/Buy", area = "Admin", }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - // Assert - Assert.Collection( - matches, - m => { Assert.Same(entry2, m); }, - m => { Assert.Same(entry1, m); }); - } + // Assert + Assert.Collection( + matches, + m => { Assert.Same(entry2, m); }, + m => { Assert.Same(entry1, m); }); + } - [Fact] - public void GetMatches_LinkToControllerFromPage() - { - // Arrange - var entries = new List(); + [Fact] + public void GetMatches_LinkToControllerFromPage() + { + // Arrange + var entries = new List(); - var entry1 = CreateMatch(new { controller = "Home", action = "Index", area = (string)null, page = (string)null, }); - entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); - entries.Add(entry1); + var entry1 = CreateMatch(new { controller = "Home", action = "Index", area = (string)null, page = (string)null, }); + entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); + entries.Add(entry1); - var entry2 = CreateMatch(new { page = "/Store/Buy", area = (string)null, controller = (string)null, action = (string)null, }); - entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); - entries.Add(entry2); + var entry2 = CreateMatch(new { page = "/Store/Buy", area = (string)null, controller = (string)null, action = (string)null, }); + entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); + entries.Add(entry2); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(new { controller = "Home", action = "Index", }, new { page = "/Store/Buy", }); + var context = CreateContext(new { controller = "Home", action = "Index", }, new { page = "/Store/Buy", }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - // Assert - Assert.Collection( - matches, - m => { Assert.Same(entry1, m); }); - } + // Assert + Assert.Collection( + matches, + m => { Assert.Same(entry1, m); }); + } - [Fact] - public void GetMatches_LinkToControllerFromPage_WithArea() - { - // Arrange - var entries = new List(); + [Fact] + public void GetMatches_LinkToControllerFromPage_WithArea() + { + // Arrange + var entries = new List(); - var entry1 = CreateMatch(new { controller = "Home", action = "Index", area = "Admin", page = (string)null, }); - entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); - entries.Add(entry1); + var entry1 = CreateMatch(new { controller = "Home", action = "Index", area = "Admin", page = (string)null, }); + entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); + entries.Add(entry1); - var entry2 = CreateMatch(new { page = "/Store/Buy", area = "Admin", controller = (string)null, action = (string)null, }); - entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); - entries.Add(entry2); + var entry2 = CreateMatch(new { page = "/Store/Buy", area = "Admin", controller = (string)null, action = (string)null, }); + entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); + entries.Add(entry2); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(new { controller = "Home", action = "Index", }, new { page = "/Store/Buy", area = "Admin", }); + var context = CreateContext(new { controller = "Home", action = "Index", }, new { page = "/Store/Buy", area = "Admin", }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - // Assert - Assert.Collection( - matches, - m => { Assert.Same(entry1, m); }); - } + // Assert + Assert.Collection( + matches, + m => { Assert.Same(entry1, m); }); + } - [Fact] - public void GetMatches_LinkToControllerFromPage_WithPageValue() - { - // Arrange - var entries = new List(); + [Fact] + public void GetMatches_LinkToControllerFromPage_WithPageValue() + { + // Arrange + var entries = new List(); - var entry1 = CreateMatch(new { controller = "Home", action = "Index", area = (string)null, page = (string)null, }); - entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); - entries.Add(entry1); + var entry1 = CreateMatch(new { controller = "Home", action = "Index", area = (string)null, page = (string)null, }); + entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); + entries.Add(entry1); - var entry2 = CreateMatch(new { page = "/Store/Buy", area = (string)null, controller = (string)null, action = (string)null, }); - entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); - entries.Add(entry2); + var entry2 = CreateMatch(new { page = "/Store/Buy", area = (string)null, controller = (string)null, action = (string)null, }); + entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); + entries.Add(entry2); - var tree = new LinkGenerationDecisionTree(entries); + var tree = new LinkGenerationDecisionTree(entries); - var context = CreateContext(new { controller = "Home", action = "Index", page = "16", }, new { page = "/Store/Buy", }); + var context = CreateContext(new { controller = "Home", action = "Index", page = "16", }, new { page = "/Store/Buy", }); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - // Assert - Assert.Empty(matches); - } - - [Fact] - public void GetMatches_LinkToControllerFromPage_WithPageValueAmbiguous() - { - // Arrange - var entries = new List(); + // Assert + Assert.Empty(matches); + } - var entry1 = CreateMatch(new { controller = "Home", action = "Index", area = (string)null, page = (string)null, }); - entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); - entries.Add(entry1); + [Fact] + public void GetMatches_LinkToControllerFromPage_WithPageValueAmbiguous() + { + // Arrange + var entries = new List(); - var entry2 = CreateMatch(new { page = "/Store/Buy", area = (string)null, controller = (string)null, action = (string)null, }); - entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); - entries.Add(entry2); + var entry1 = CreateMatch(new { controller = "Home", action = "Index", area = (string)null, page = (string)null, }); + entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); + entries.Add(entry1); - var tree = new LinkGenerationDecisionTree(entries); + var entry2 = CreateMatch(new { page = "/Store/Buy", area = (string)null, controller = (string)null, action = (string)null, }); + entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); + entries.Add(entry2); - var context = CreateContext(new { controller = "Home", action = "Index", page = "/Store/Buy", }, new { page = "/Store/Buy", }); + var tree = new LinkGenerationDecisionTree(entries); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + var context = CreateContext(new { controller = "Home", action = "Index", page = "/Store/Buy", }, new { page = "/Store/Buy", }); - // Assert - Assert.Empty(matches); - } + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - [Fact] - public void GetMatches_LinkToPageFromController() - { - // Arrange - var entries = new List(); + // Assert + Assert.Empty(matches); + } - var entry1 = CreateMatch(new { controller = "Home", action = "Index", area = (string)null, page = (string)null, }); - entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); - entries.Add(entry1); + [Fact] + public void GetMatches_LinkToPageFromController() + { + // Arrange + var entries = new List(); - var entry2 = CreateMatch(new { page = "/Store/Buy", area = (string)null, controller = (string)null, action = (string)null, }); - entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); - entries.Add(entry2); + var entry1 = CreateMatch(new { controller = "Home", action = "Index", area = (string)null, page = (string)null, }); + entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); + entries.Add(entry1); - var tree = new LinkGenerationDecisionTree(entries); + var entry2 = CreateMatch(new { page = "/Store/Buy", area = (string)null, controller = (string)null, action = (string)null, }); + entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); + entries.Add(entry2); - var context = CreateContext(new { page = "/Store/Buy", }, new { controller = "Home", action = "Index", }); + var tree = new LinkGenerationDecisionTree(entries); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + var context = CreateContext(new { page = "/Store/Buy", }, new { controller = "Home", action = "Index", }); - // Assert - Assert.Collection( - matches, - m => { Assert.Same(entry2, m); }); - } + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - [Fact] - public void GetMatches_LinkToPageFromController_WithArea() - { - // Arrange - var entries = new List(); + // Assert + Assert.Collection( + matches, + m => { Assert.Same(entry2, m); }); + } - var entry1 = CreateMatch(new { controller = "Home", action = "Index", area = "Admin", page = (string)null, }); - entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); - entries.Add(entry1); + [Fact] + public void GetMatches_LinkToPageFromController_WithArea() + { + // Arrange + var entries = new List(); - var entry2 = CreateMatch(new { page = "/Store/Buy", area = "Admin", controller = (string)null, action = (string)null, }); - entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); - entries.Add(entry2); + var entry1 = CreateMatch(new { controller = "Home", action = "Index", area = "Admin", page = (string)null, }); + entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); + entries.Add(entry1); - var tree = new LinkGenerationDecisionTree(entries); + var entry2 = CreateMatch(new { page = "/Store/Buy", area = "Admin", controller = (string)null, action = (string)null, }); + entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); + entries.Add(entry2); - var context = CreateContext(new { page = "/Store/Buy", }, new { controller = "Home", action = "Index", area = "Admin", }); + var tree = new LinkGenerationDecisionTree(entries); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + var context = CreateContext(new { page = "/Store/Buy", }, new { controller = "Home", action = "Index", area = "Admin", }); - // Assert - Assert.Collection( - matches, - m => { Assert.Same(entry2, m); }); - } + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - [Fact] - public void GetMatches_LinkToPageFromController_WithActionValue() - { - // Arrange - var entries = new List(); + // Assert + Assert.Collection( + matches, + m => { Assert.Same(entry2, m); }); + } - var entry1 = CreateMatch(new { controller = "Home", action = "Index", area = (string)null, page = (string)null, }); - entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); - entries.Add(entry1); + [Fact] + public void GetMatches_LinkToPageFromController_WithActionValue() + { + // Arrange + var entries = new List(); - var entry2 = CreateMatch(new { page = "/Store/Buy", area = (string)null, controller = (string)null, action = (string)null, }); - entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); - entries.Add(entry2); + var entry1 = CreateMatch(new { controller = "Home", action = "Index", area = (string)null, page = (string)null, }); + entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); + entries.Add(entry1); - var tree = new LinkGenerationDecisionTree(entries); + var entry2 = CreateMatch(new { page = "/Store/Buy", area = (string)null, controller = (string)null, action = (string)null, }); + entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); + entries.Add(entry2); - var context = CreateContext(new { page = "/Store/Buy", action = "buy", }, new { controller = "Home", action = "Index", page = "16", }); + var tree = new LinkGenerationDecisionTree(entries); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + var context = CreateContext(new { page = "/Store/Buy", action = "buy", }, new { controller = "Home", action = "Index", page = "16", }); - // Assert - Assert.Empty(matches); - } + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - [Fact] - public void GetMatches_LinkToPageFromController_WithActionValueAmbiguous() - { - // Arrange - var entries = new List(); + // Assert + Assert.Empty(matches); + } - var entry1 = CreateMatch(new { controller = "Home", action = "Index", area = (string)null, page = (string)null, }); - entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); - entries.Add(entry1); + [Fact] + public void GetMatches_LinkToPageFromController_WithActionValueAmbiguous() + { + // Arrange + var entries = new List(); - var entry2 = CreateMatch(new { page = "/Store/Buy", area = (string)null, controller = (string)null, action = (string)null, }); - entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); - entries.Add(entry2); + var entry1 = CreateMatch(new { controller = "Home", action = "Index", area = (string)null, page = (string)null, }); + entry1.Entry.RouteTemplate = TemplateParser.Parse("a"); + entries.Add(entry1); - var tree = new LinkGenerationDecisionTree(entries); + var entry2 = CreateMatch(new { page = "/Store/Buy", area = (string)null, controller = (string)null, action = (string)null, }); + entry2.Entry.RouteTemplate = TemplateParser.Parse("b"); + entries.Add(entry2); - var context = CreateContext(new { page = "/Store/Buy", action = "Index", }, new { controller = "Home", action = "Index", page = "16", }); + var tree = new LinkGenerationDecisionTree(entries); - // Act - var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); + var context = CreateContext(new { page = "/Store/Buy", action = "Index", }, new { controller = "Home", action = "Index", page = "16", }); - // Assert - Assert.Empty(matches); - } + // Act + var matches = tree.GetMatches(context.Values, context.AmbientValues).Select(m => m.Match).ToList(); - [Fact] - public void ToDebuggerDisplayString_GivesAFlattenedTree() - { - // Arrange - var entries = new List(); - entries.Add(CreateMatch(new { action = "Buy", controller = "Store", version = "V1" }, "Store/Buy/V1")); - entries.Add(CreateMatch(new { action = "Buy", controller = "Store", area = "Admin" }, "Admin/Store/Buy")); - entries.Add(CreateMatch(new { action = "Buy", controller = "Products" }, "Products/Buy")); - entries.Add(CreateMatch(new { action = "Buy", controller = "Store", version = "V2" }, "Store/Buy/V2")); - entries.Add(CreateMatch(new { action = "Cart", controller = "Store" }, "Store/Cart")); - entries.Add(CreateMatch(new { action = "Index", controller = "Home" }, "Home/Index/{id?}")); - var tree = new LinkGenerationDecisionTree(entries); - var newLine = Environment.NewLine; - var expected = - " => action: Buy => controller: Store => version: V1 (Matches: Store/Buy/V1)" + newLine + - " => action: Buy => controller: Store => version: V2 (Matches: Store/Buy/V2)" + newLine + - " => action: Buy => controller: Store => area: Admin (Matches: Admin/Store/Buy)" + newLine + - " => action: Buy => controller: Products (Matches: Products/Buy)" + newLine + - " => action: Cart => controller: Store (Matches: Store/Cart)" + newLine + - " => action: Index => controller: Home (Matches: Home/Index/{id?})" + newLine; - - // Act - var flattenedTree = tree.DebuggerDisplayString; - - // Assert - Assert.Equal(expected, flattenedTree); - } + // Assert + Assert.Empty(matches); + } - private OutboundMatch CreateMatch(object requiredValues, string routeTemplate = null) - { - var match = new OutboundMatch(); - match.Entry = new OutboundRouteEntry(); - match.Entry.RequiredLinkValues = new RouteValueDictionary(requiredValues); + [Fact] + public void ToDebuggerDisplayString_GivesAFlattenedTree() + { + // Arrange + var entries = new List(); + entries.Add(CreateMatch(new { action = "Buy", controller = "Store", version = "V1" }, "Store/Buy/V1")); + entries.Add(CreateMatch(new { action = "Buy", controller = "Store", area = "Admin" }, "Admin/Store/Buy")); + entries.Add(CreateMatch(new { action = "Buy", controller = "Products" }, "Products/Buy")); + entries.Add(CreateMatch(new { action = "Buy", controller = "Store", version = "V2" }, "Store/Buy/V2")); + entries.Add(CreateMatch(new { action = "Cart", controller = "Store" }, "Store/Cart")); + entries.Add(CreateMatch(new { action = "Index", controller = "Home" }, "Home/Index/{id?}")); + var tree = new LinkGenerationDecisionTree(entries); + var newLine = Environment.NewLine; + var expected = + " => action: Buy => controller: Store => version: V1 (Matches: Store/Buy/V1)" + newLine + + " => action: Buy => controller: Store => version: V2 (Matches: Store/Buy/V2)" + newLine + + " => action: Buy => controller: Store => area: Admin (Matches: Admin/Store/Buy)" + newLine + + " => action: Buy => controller: Products (Matches: Products/Buy)" + newLine + + " => action: Cart => controller: Store (Matches: Store/Cart)" + newLine + + " => action: Index => controller: Home (Matches: Home/Index/{id?})" + newLine; + + // Act + var flattenedTree = tree.DebuggerDisplayString; + + // Assert + Assert.Equal(expected, flattenedTree); + } - if (!string.IsNullOrEmpty(routeTemplate)) - { - match.Entry.RouteTemplate = new RouteTemplate(RoutePatternFactory.Parse(routeTemplate)); - } + private OutboundMatch CreateMatch(object requiredValues, string routeTemplate = null) + { + var match = new OutboundMatch(); + match.Entry = new OutboundRouteEntry(); + match.Entry.RequiredLinkValues = new RouteValueDictionary(requiredValues); - return match; + if (!string.IsNullOrEmpty(routeTemplate)) + { + match.Entry.RouteTemplate = new RouteTemplate(RoutePatternFactory.Parse(routeTemplate)); } - private VirtualPathContext CreateContext(object values, object ambientValues = null) - { - var context = new VirtualPathContext( - new DefaultHttpContext(), - new RouteValueDictionary(ambientValues), - new RouteValueDictionary(values)); + return match; + } - return context; - } + private VirtualPathContext CreateContext(object values, object ambientValues = null) + { + var context = new VirtualPathContext( + new DefaultHttpContext(), + new RouteValueDictionary(ambientValues), + new RouteValueDictionary(values)); + + return context; } } diff --git a/src/Http/Routing/test/UnitTests/Tree/TreeRouteBuilderTest.cs b/src/Http/Routing/test/UnitTests/Tree/TreeRouteBuilderTest.cs index 6c462613bf..7a2ec9d5de 100644 --- a/src/Http/Routing/test/UnitTests/Tree/TreeRouteBuilderTest.cs +++ b/src/Http/Routing/test/UnitTests/Tree/TreeRouteBuilderTest.cs @@ -10,255 +10,254 @@ using Microsoft.Extensions.Options; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tree +namespace Microsoft.AspNetCore.Routing.Tree; + +public class TreeRouteBuilderTest { - public class TreeRouteBuilderTest + [Fact] + public void TreeRouter_BuildThrows_RoutesWithTheSameNameAndDifferentTemplates() { - [Fact] - public void TreeRouter_BuildThrows_RoutesWithTheSameNameAndDifferentTemplates() - { - // Arrange - var builder = CreateBuilder(); - - var message = "Two or more routes named 'Get_Products' have different templates."; - - builder.MapOutbound( - Mock.Of(), - TemplateParser.Parse("api/Products"), - new RouteValueDictionary(), - "Get_Products", - order: 0); - - builder.MapOutbound( - Mock.Of(), - TemplateParser.Parse("Products/Index"), - new RouteValueDictionary(), - "Get_Products", - order: 0); - - // Act & Assert - ExceptionAssert.ThrowsArgument(() => - { - builder.Build(); - }, "linkGenerationEntries", message); - } - - [Fact] - public void TreeRouter_BuildDoesNotThrow_RoutesWithTheSameNameAndSameTemplates() + // Arrange + var builder = CreateBuilder(); + + var message = "Two or more routes named 'Get_Products' have different templates."; + + builder.MapOutbound( + Mock.Of(), + TemplateParser.Parse("api/Products"), + new RouteValueDictionary(), + "Get_Products", + order: 0); + + builder.MapOutbound( + Mock.Of(), + TemplateParser.Parse("Products/Index"), + new RouteValueDictionary(), + "Get_Products", + order: 0); + + // Act & Assert + ExceptionAssert.ThrowsArgument(() => { - // Arrange - var builder = CreateBuilder(); - - builder.MapOutbound( - Mock.Of(), - TemplateParser.Parse("api/Products"), - new RouteValueDictionary(), - "Get_Products", - order: 0); - - builder.MapOutbound( - Mock.Of(), - TemplateParser.Parse("api/products"), - new RouteValueDictionary(), - "Get_Products", - order: 0); - - // Act & Assert (does not throw) builder.Build(); - } + }, "linkGenerationEntries", message); + } - [Fact] - public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithIntermediateParametersWithDefaultValues() - { - // Arrange - var builder = CreateBuilder(); + [Fact] + public void TreeRouter_BuildDoesNotThrow_RoutesWithTheSameNameAndSameTemplates() + { + // Arrange + var builder = CreateBuilder(); + + builder.MapOutbound( + Mock.Of(), + TemplateParser.Parse("api/Products"), + new RouteValueDictionary(), + "Get_Products", + order: 0); + + builder.MapOutbound( + Mock.Of(), + TemplateParser.Parse("api/products"), + new RouteValueDictionary(), + "Get_Products", + order: 0); + + // Act & Assert (does not throw) + builder.Build(); + } - builder.MapInbound( - Mock.Of(), - TemplateParser.Parse("a/{b=3}/c"), - "Intermediate", - order: 0); + [Fact] + public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithIntermediateParametersWithDefaultValues() + { + // Arrange + var builder = CreateBuilder(); - // Act - var tree = builder.Build(); + builder.MapInbound( + Mock.Of(), + TemplateParser.Parse("a/{b=3}/c"), + "Intermediate", + order: 0); - // Assert - Assert.NotNull(tree); - Assert.NotNull(tree.MatchingTrees); - var matchingTree = Assert.Single(tree.MatchingTrees); + // Act + var tree = builder.Build(); - var firstSegment = Assert.Single(matchingTree.Root.Literals); - Assert.Equal("a", firstSegment.Key); - Assert.NotNull(firstSegment.Value.Parameters); + // Assert + Assert.NotNull(tree); + Assert.NotNull(tree.MatchingTrees); + var matchingTree = Assert.Single(tree.MatchingTrees); - var secondSegment = firstSegment.Value.Parameters; - Assert.Empty(secondSegment.Matches); + var firstSegment = Assert.Single(matchingTree.Root.Literals); + Assert.Equal("a", firstSegment.Key); + Assert.NotNull(firstSegment.Value.Parameters); - var thirdSegment = Assert.Single(secondSegment.Literals); - Assert.Equal("c", thirdSegment.Key); - Assert.Single(thirdSegment.Value.Matches); - } + var secondSegment = firstSegment.Value.Parameters; + Assert.Empty(secondSegment.Matches); - [Fact] - public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithMultipleIntermediateParametersWithDefaultOrOptionalValues() - { - // Arrange - var builder = CreateBuilder(); + var thirdSegment = Assert.Single(secondSegment.Literals); + Assert.Equal("c", thirdSegment.Key); + Assert.Single(thirdSegment.Value.Matches); + } - builder.MapInbound( - Mock.Of(), - TemplateParser.Parse("a/{b=3}/c/{d?}/e/{*f}"), - "Intermediate", - order: 0); + [Fact] + public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithMultipleIntermediateParametersWithDefaultOrOptionalValues() + { + // Arrange + var builder = CreateBuilder(); - // Act - var tree = builder.Build(); + builder.MapInbound( + Mock.Of(), + TemplateParser.Parse("a/{b=3}/c/{d?}/e/{*f}"), + "Intermediate", + order: 0); - // Assert - Assert.NotNull(tree); - Assert.NotNull(tree.MatchingTrees); - var matchingTree = Assert.Single(tree.MatchingTrees); + // Act + var tree = builder.Build(); - var firstSegment = Assert.Single(matchingTree.Root.Literals); - Assert.Equal("a", firstSegment.Key); - Assert.NotNull(firstSegment.Value.Parameters); + // Assert + Assert.NotNull(tree); + Assert.NotNull(tree.MatchingTrees); + var matchingTree = Assert.Single(tree.MatchingTrees); - var secondSegment = firstSegment.Value.Parameters; - Assert.Empty(secondSegment.Matches); + var firstSegment = Assert.Single(matchingTree.Root.Literals); + Assert.Equal("a", firstSegment.Key); + Assert.NotNull(firstSegment.Value.Parameters); - var thirdSegment = Assert.Single(secondSegment.Literals); - Assert.Equal("c", thirdSegment.Key); - Assert.Empty(thirdSegment.Value.Matches); + var secondSegment = firstSegment.Value.Parameters; + Assert.Empty(secondSegment.Matches); - var fourthSegment = thirdSegment.Value.Parameters; - Assert.NotNull(fourthSegment); - Assert.Empty(fourthSegment.Matches); + var thirdSegment = Assert.Single(secondSegment.Literals); + Assert.Equal("c", thirdSegment.Key); + Assert.Empty(thirdSegment.Value.Matches); - var fifthSegment = Assert.Single(fourthSegment.Literals); - Assert.Equal("e", fifthSegment.Key); - Assert.Single(fifthSegment.Value.Matches); + var fourthSegment = thirdSegment.Value.Parameters; + Assert.NotNull(fourthSegment); + Assert.Empty(fourthSegment.Matches); - var sixthSegment = fifthSegment.Value.CatchAlls; - Assert.NotNull(sixthSegment); - Assert.Single(sixthSegment.Matches); - } + var fifthSegment = Assert.Single(fourthSegment.Literals); + Assert.Equal("e", fifthSegment.Key); + Assert.Single(fifthSegment.Value.Matches); - [Fact] - public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithIntermediateParametersWithOptionalValues() - { - // Arrange - var builder = CreateBuilder(); + var sixthSegment = fifthSegment.Value.CatchAlls; + Assert.NotNull(sixthSegment); + Assert.Single(sixthSegment.Matches); + } - builder.MapInbound( - Mock.Of(), - TemplateParser.Parse("a/{b?}/c"), - "Intermediate", - order: 0); + [Fact] + public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithIntermediateParametersWithOptionalValues() + { + // Arrange + var builder = CreateBuilder(); - // Act - var tree = builder.Build(); + builder.MapInbound( + Mock.Of(), + TemplateParser.Parse("a/{b?}/c"), + "Intermediate", + order: 0); - // Assert - Assert.NotNull(tree); - Assert.NotNull(tree.MatchingTrees); - var matchingTree = Assert.Single(tree.MatchingTrees); + // Act + var tree = builder.Build(); - var firstSegment = Assert.Single(matchingTree.Root.Literals); - Assert.Equal("a", firstSegment.Key); - Assert.NotNull(firstSegment.Value.Parameters); + // Assert + Assert.NotNull(tree); + Assert.NotNull(tree.MatchingTrees); + var matchingTree = Assert.Single(tree.MatchingTrees); - var secondSegment = firstSegment.Value.Parameters; - Assert.Empty(secondSegment.Matches); + var firstSegment = Assert.Single(matchingTree.Root.Literals); + Assert.Equal("a", firstSegment.Key); + Assert.NotNull(firstSegment.Value.Parameters); - var thirdSegment = Assert.Single(secondSegment.Literals); - Assert.Equal("c", thirdSegment.Key); - Assert.Single(thirdSegment.Value.Matches); - } + var secondSegment = firstSegment.Value.Parameters; + Assert.Empty(secondSegment.Matches); - [Fact] - public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithIntermediateParametersWithConstrainedDefaultValues() - { - // Arrange - var builder = CreateBuilder(); + var thirdSegment = Assert.Single(secondSegment.Literals); + Assert.Equal("c", thirdSegment.Key); + Assert.Single(thirdSegment.Value.Matches); + } - builder.MapInbound( - Mock.Of(), - TemplateParser.Parse("a/{b:int=3}/c"), - "Intermediate", - order: 0); + [Fact] + public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithIntermediateParametersWithConstrainedDefaultValues() + { + // Arrange + var builder = CreateBuilder(); - // Act - var tree = builder.Build(); + builder.MapInbound( + Mock.Of(), + TemplateParser.Parse("a/{b:int=3}/c"), + "Intermediate", + order: 0); - // Assert - Assert.NotNull(tree); - Assert.NotNull(tree.MatchingTrees); - var matchingTree = Assert.Single(tree.MatchingTrees); + // Act + var tree = builder.Build(); - var firstSegment = Assert.Single(matchingTree.Root.Literals); - Assert.Equal("a", firstSegment.Key); - Assert.NotNull(firstSegment.Value.ConstrainedParameters); + // Assert + Assert.NotNull(tree); + Assert.NotNull(tree.MatchingTrees); + var matchingTree = Assert.Single(tree.MatchingTrees); - var secondSegment = firstSegment.Value.ConstrainedParameters; - Assert.Empty(secondSegment.Matches); + var firstSegment = Assert.Single(matchingTree.Root.Literals); + Assert.Equal("a", firstSegment.Key); + Assert.NotNull(firstSegment.Value.ConstrainedParameters); - var thirdSegment = Assert.Single(secondSegment.Literals); - Assert.Equal("c", thirdSegment.Key); - Assert.Single(thirdSegment.Value.Matches); - } + var secondSegment = firstSegment.Value.ConstrainedParameters; + Assert.Empty(secondSegment.Matches); - [Fact] - public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithIntermediateParametersWithConstrainedOptionalValues() - { - // Arrange - var builder = CreateBuilder(); + var thirdSegment = Assert.Single(secondSegment.Literals); + Assert.Equal("c", thirdSegment.Key); + Assert.Single(thirdSegment.Value.Matches); + } - builder.MapInbound( - Mock.Of(), - TemplateParser.Parse("a/{b:int?}/c"), - "Intermediate", - order: 0); + [Fact] + public void TreeRouter_BuildDoesNotAddIntermediateMatchingNodes_ForRoutesWithIntermediateParametersWithConstrainedOptionalValues() + { + // Arrange + var builder = CreateBuilder(); - // Act - var tree = builder.Build(); + builder.MapInbound( + Mock.Of(), + TemplateParser.Parse("a/{b:int?}/c"), + "Intermediate", + order: 0); - // Assert - Assert.NotNull(tree); - Assert.NotNull(tree.MatchingTrees); - var matchingTree = Assert.Single(tree.MatchingTrees); + // Act + var tree = builder.Build(); - var firstSegment = Assert.Single(matchingTree.Root.Literals); - Assert.Equal("a", firstSegment.Key); - Assert.NotNull(firstSegment.Value.ConstrainedParameters); + // Assert + Assert.NotNull(tree); + Assert.NotNull(tree.MatchingTrees); + var matchingTree = Assert.Single(tree.MatchingTrees); - var secondSegment = firstSegment.Value.ConstrainedParameters; - Assert.Empty(secondSegment.Matches); + var firstSegment = Assert.Single(matchingTree.Root.Literals); + Assert.Equal("a", firstSegment.Key); + Assert.NotNull(firstSegment.Value.ConstrainedParameters); - var thirdSegment = Assert.Single(secondSegment.Literals); - Assert.Equal("c", thirdSegment.Key); - Assert.Single(thirdSegment.Value.Matches); - } + var secondSegment = firstSegment.Value.ConstrainedParameters; + Assert.Empty(secondSegment.Matches); - private static TreeRouteBuilder CreateBuilder() - { - var objectPoolProvider = new DefaultObjectPoolProvider(); - var objectPolicy = new UriBuilderContextPooledObjectPolicy(); - var objectPool = objectPoolProvider.Create(objectPolicy); - - var constraintResolver = GetInlineConstraintResolver(); - var builder = new TreeRouteBuilder( - NullLoggerFactory.Instance, - objectPool, - constraintResolver); - return builder; - } - - private static IInlineConstraintResolver GetInlineConstraintResolver() - { - var services = new ServiceCollection().AddOptions(); - var serviceProvider = services.BuildServiceProvider(); - var accessor = serviceProvider.GetRequiredService>(); - return new DefaultInlineConstraintResolver(accessor, serviceProvider); - } + var thirdSegment = Assert.Single(secondSegment.Literals); + Assert.Equal("c", thirdSegment.Key); + Assert.Single(thirdSegment.Value.Matches); + } + + private static TreeRouteBuilder CreateBuilder() + { + var objectPoolProvider = new DefaultObjectPoolProvider(); + var objectPolicy = new UriBuilderContextPooledObjectPolicy(); + var objectPool = objectPoolProvider.Create(objectPolicy); + + var constraintResolver = GetInlineConstraintResolver(); + var builder = new TreeRouteBuilder( + NullLoggerFactory.Instance, + objectPool, + constraintResolver); + return builder; + } + + private static IInlineConstraintResolver GetInlineConstraintResolver() + { + var services = new ServiceCollection().AddOptions(); + var serviceProvider = services.BuildServiceProvider(); + var accessor = serviceProvider.GetRequiredService>(); + return new DefaultInlineConstraintResolver(accessor, serviceProvider); } } diff --git a/src/Http/Routing/test/UnitTests/Tree/TreeRouterTest.cs b/src/Http/Routing/test/UnitTests/Tree/TreeRouterTest.cs index 3a20de866f..a94da3a18c 100644 --- a/src/Http/Routing/test/UnitTests/Tree/TreeRouterTest.cs +++ b/src/Http/Routing/test/UnitTests/Tree/TreeRouterTest.cs @@ -16,70 +16,70 @@ using Microsoft.Extensions.Options; using Moq; using Xunit; -namespace Microsoft.AspNetCore.Routing.Tree +namespace Microsoft.AspNetCore.Routing.Tree; + +public class TreeRouterTest { - public class TreeRouterTest - { - private static readonly RequestDelegate NullHandler = (c) => Task.CompletedTask; - - [Theory] - [InlineData("template/5", "template/{parameter:int}")] - [InlineData("template/5", "template/{parameter}")] - [InlineData("template/5", "template/{*parameter:int}")] - [InlineData("template/5", "template/{*parameter}")] - [InlineData("template/{parameter}", "template/{parameter:alpha}")] // constraint doesn't match - [InlineData("template/{parameter:int}", "template/{parameter}")] - [InlineData("template/{parameter:int}", "template/{*parameter:int}")] - [InlineData("template/{parameter:int}", "template/{*parameter}")] - [InlineData("template/{parameter}", "template/{*parameter:int}")] - [InlineData("template/{parameter}", "template/{*parameter}")] - [InlineData("template/{*parameter:int}", "template/{*parameter}")] - public async Task TreeRouter_RouteAsync_RespectsPrecedence( - string firstTemplate, - string secondTemplate) - { - // Arrange - var expectedRouteGroup = CreateRouteGroup(0, firstTemplate); + private static readonly RequestDelegate NullHandler = (c) => Task.CompletedTask; + + [Theory] + [InlineData("template/5", "template/{parameter:int}")] + [InlineData("template/5", "template/{parameter}")] + [InlineData("template/5", "template/{*parameter:int}")] + [InlineData("template/5", "template/{*parameter}")] + [InlineData("template/{parameter}", "template/{parameter:alpha}")] // constraint doesn't match + [InlineData("template/{parameter:int}", "template/{parameter}")] + [InlineData("template/{parameter:int}", "template/{*parameter:int}")] + [InlineData("template/{parameter:int}", "template/{*parameter}")] + [InlineData("template/{parameter}", "template/{*parameter:int}")] + [InlineData("template/{parameter}", "template/{*parameter}")] + [InlineData("template/{*parameter:int}", "template/{*parameter}")] + public async Task TreeRouter_RouteAsync_RespectsPrecedence( + string firstTemplate, + string secondTemplate) + { + // Arrange + var expectedRouteGroup = CreateRouteGroup(0, firstTemplate); - var builder = CreateBuilder(); + var builder = CreateBuilder(); - // We setup the route entries in reverse order of precedence to ensure that when we - // try to route the request, the route with a higher precedence gets tried first. - MapInboundEntry(builder, secondTemplate); - MapInboundEntry(builder, firstTemplate); + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + MapInboundEntry(builder, secondTemplate); + MapInboundEntry(builder, firstTemplate); - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext("/template/5"); + var context = CreateRouteContext("/template/5"); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); - } + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + } - [Theory] - [InlineData("/", "")] - [InlineData("/Literal1", "Literal1")] - [InlineData("/Literal1/Literal2", "Literal1/Literal2")] - [InlineData("/Literal1/Literal2/Literal3", "Literal1/Literal2/Literal3")] - [InlineData("/Literal1/Literal2/Literal3/4", "Literal1/Literal2/Literal3/{*constrainedCatchAll:int}")] - [InlineData("/Literal1/Literal2/Literal3/Literal4", "Literal1/Literal2/Literal3/{*catchAll}")] - [InlineData("/1", "{constrained1:int}")] - [InlineData("/1/2", "{constrained1:int}/{constrained2:int}")] - [InlineData("/1/2/3", "{constrained1:int}/{constrained2:int}/{constrained3:int}")] - [InlineData("/1/2/3/4", "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*constrainedCatchAll:int}")] - [InlineData("/1/2/3/CatchAll4", "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*catchAll}")] - [InlineData("/parameter1", "{parameter1}")] - [InlineData("/parameter1/parameter2", "{parameter1}/{parameter2}")] - [InlineData("/parameter1/parameter2/parameter3", "{parameter1}/{parameter2}/{parameter3}")] - [InlineData("/parameter1/parameter2/parameter3/4", "{parameter1}/{parameter2}/{parameter3}/{*constrainedCatchAll:int}")] - [InlineData("/parameter1/parameter2/parameter3/CatchAll4", "{parameter1}/{parameter2}/{parameter3}/{*catchAll}")] - public async Task TreeRouter_RouteAsync_MatchesRouteWithTheRightLength(string url, string expected) - { - // Arrange - var routes = new[] { + [Theory] + [InlineData("/", "")] + [InlineData("/Literal1", "Literal1")] + [InlineData("/Literal1/Literal2", "Literal1/Literal2")] + [InlineData("/Literal1/Literal2/Literal3", "Literal1/Literal2/Literal3")] + [InlineData("/Literal1/Literal2/Literal3/4", "Literal1/Literal2/Literal3/{*constrainedCatchAll:int}")] + [InlineData("/Literal1/Literal2/Literal3/Literal4", "Literal1/Literal2/Literal3/{*catchAll}")] + [InlineData("/1", "{constrained1:int}")] + [InlineData("/1/2", "{constrained1:int}/{constrained2:int}")] + [InlineData("/1/2/3", "{constrained1:int}/{constrained2:int}/{constrained3:int}")] + [InlineData("/1/2/3/4", "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*constrainedCatchAll:int}")] + [InlineData("/1/2/3/CatchAll4", "{constrained1:int}/{constrained2:int}/{constrained3:int}/{*catchAll}")] + [InlineData("/parameter1", "{parameter1}")] + [InlineData("/parameter1/parameter2", "{parameter1}/{parameter2}")] + [InlineData("/parameter1/parameter2/parameter3", "{parameter1}/{parameter2}/{parameter3}")] + [InlineData("/parameter1/parameter2/parameter3/4", "{parameter1}/{parameter2}/{parameter3}/{*constrainedCatchAll:int}")] + [InlineData("/parameter1/parameter2/parameter3/CatchAll4", "{parameter1}/{parameter2}/{parameter3}/{*catchAll}")] + public async Task TreeRouter_RouteAsync_MatchesRouteWithTheRightLength(string url, string expected) + { + // Arrange + var routes = new[] { "", "Literal1", "Literal1/Literal2", @@ -98,1516 +98,1516 @@ namespace Microsoft.AspNetCore.Routing.Tree "{parameter1}/{parameter2}/{parameter3}/{*catchAll}", }; - var expectedRouteGroup = CreateRouteGroup(0, expected); + var expectedRouteGroup = CreateRouteGroup(0, expected); - var builder = CreateBuilder(); + var builder = CreateBuilder(); - // We setup the route entries in reverse order of precedence to ensure that when we - // try to route the request, the route with a higher precedence gets tried first. - foreach (var template in routes.Reverse()) - { - MapInboundEntry(builder, template); - } + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + foreach (var template in routes.Reverse()) + { + MapInboundEntry(builder, template); + } - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext(url); + var context = CreateRouteContext(url); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); - } + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + } - public static TheoryData MatchesRoutesWithDefaultsData => - new TheoryData - { + public static TheoryData MatchesRoutesWithDefaultsData => + new TheoryData + { { "/", new object[] { "1", "2", "3", "4" } }, { "/a", new object[] { "a", "2", "3", "4" } }, { "/a/b", new object[] { "a", "b", "3", "4" } }, { "/a/b/c", new object[] { "a", "b", "c", "4" } }, { "/a/b/c/d", new object[] { "a", "b", "c", "d" } } - }; + }; - [Theory] - [MemberData(nameof(MatchesRoutesWithDefaultsData))] - public async Task TreeRouter_RouteAsync_MatchesRoutesWithDefaults(string url, object[] routeValues) - { - // Arrange - var routes = new[] { + [Theory] + [MemberData(nameof(MatchesRoutesWithDefaultsData))] + public async Task TreeRouter_RouteAsync_MatchesRoutesWithDefaults(string url, object[] routeValues) + { + // Arrange + var routes = new[] { "{parameter1=1}/{parameter2=2}/{parameter3=3}/{parameter4=4}", }; - var expectedRouteGroup = CreateRouteGroup(0, "{parameter1=1}/{parameter2=2}/{parameter3=3}/{parameter4=4}"); - var routeValueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" }; - var expectedRouteValues = new RouteValueDictionary(); - for (var i = 0; i < routeValueKeys.Length; i++) - { - expectedRouteValues.Add(routeValueKeys[i], routeValues[i]); - } + var expectedRouteGroup = CreateRouteGroup(0, "{parameter1=1}/{parameter2=2}/{parameter3=3}/{parameter4=4}"); + var routeValueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" }; + var expectedRouteValues = new RouteValueDictionary(); + for (var i = 0; i < routeValueKeys.Length; i++) + { + expectedRouteValues.Add(routeValueKeys[i], routeValues[i]); + } - var builder = CreateBuilder(); + var builder = CreateBuilder(); - // We setup the route entries in reverse order of precedence to ensure that when we - // try to route the request, the route with a higher precedence gets tried first. - foreach (var template in routes.Reverse()) - { - MapInboundEntry(builder, template); - } + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + foreach (var template in routes.Reverse()) + { + MapInboundEntry(builder, template); + } - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext(url); + var context = CreateRouteContext(url); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); - foreach (var entry in expectedRouteValues) - { - var data = Assert.Single(context.RouteData.Values, v => v.Key == entry.Key); - Assert.Equal(entry.Value, data.Value); - } + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + foreach (var entry in expectedRouteValues) + { + var data = Assert.Single(context.RouteData.Values, v => v.Key == entry.Key); + Assert.Equal(entry.Value, data.Value); } + } - public static TheoryData MatchesConstrainedRoutesWithDefaultsData => - new TheoryData - { + public static TheoryData MatchesConstrainedRoutesWithDefaultsData => + new TheoryData + { { "/", new object[] { "1", "2", "3", "4" } }, { "/10", new object[] { "10", "2", "3", "4" } }, { "/10/11", new object[] { "10", "11", "3", "4" } }, { "/10/11/12", new object[] { "10", "11", "12", "4" } }, { "/10/11/12/13", new object[] { "10", "11", "12", "13" } } - }; + }; - [Theory] - [MemberData(nameof(MatchesConstrainedRoutesWithDefaultsData))] - public async Task TreeRouter_RouteAsync_MatchesConstrainedRoutesWithDefaults(string url, object[] routeValues) - { - // Arrange - var routes = new[] { + [Theory] + [MemberData(nameof(MatchesConstrainedRoutesWithDefaultsData))] + public async Task TreeRouter_RouteAsync_MatchesConstrainedRoutesWithDefaults(string url, object[] routeValues) + { + // Arrange + var routes = new[] { "{parameter1:int=1}/{parameter2:int=2}/{parameter3:int=3}/{parameter4:int=4}", }; - var expectedRouteGroup = CreateRouteGroup(0, "{parameter1:int=1}/{parameter2:int=2}/{parameter3:int=3}/{parameter4:int=4}"); - var routeValueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" }; - var expectedRouteValues = new RouteValueDictionary(); - for (var i = 0; i < routeValueKeys.Length; i++) - { - expectedRouteValues.Add(routeValueKeys[i], routeValues[i]); - } + var expectedRouteGroup = CreateRouteGroup(0, "{parameter1:int=1}/{parameter2:int=2}/{parameter3:int=3}/{parameter4:int=4}"); + var routeValueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" }; + var expectedRouteValues = new RouteValueDictionary(); + for (var i = 0; i < routeValueKeys.Length; i++) + { + expectedRouteValues.Add(routeValueKeys[i], routeValues[i]); + } - var builder = CreateBuilder(); + var builder = CreateBuilder(); - // We setup the route entries in reverse order of precedence to ensure that when we - // try to route the request, the route with a higher precedence gets tried first. - foreach (var template in routes.Reverse()) - { - MapInboundEntry(builder, template); - } + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + foreach (var template in routes.Reverse()) + { + MapInboundEntry(builder, template); + } - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext(url); + var context = CreateRouteContext(url); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); - foreach (var entry in expectedRouteValues) - { - var data = Assert.Single(context.RouteData.Values, v => v.Key == entry.Key); - Assert.Equal(entry.Value, data.Value); - } + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + foreach (var entry in expectedRouteValues) + { + var data = Assert.Single(context.RouteData.Values, v => v.Key == entry.Key); + Assert.Equal(entry.Value, data.Value); } + } - [Fact] - public async Task TreeRouter_RouteAsync_MatchesCatchAllRoutesWithDefaults() - { - // Arrange - var routes = new[] { + [Fact] + public async Task TreeRouter_RouteAsync_MatchesCatchAllRoutesWithDefaults() + { + // Arrange + var routes = new[] { "{parameter1=1}/{parameter2=2}/{parameter3=3}/{*parameter4=4}", }; - var url = "/a/b/c"; - var routeValues = new[] { "a", "b", "c", "4" }; + var url = "/a/b/c"; + var routeValues = new[] { "a", "b", "c", "4" }; - var expectedRouteGroup = CreateRouteGroup(0, "{parameter1=1}/{parameter2=2}/{parameter3=3}/{*parameter4=4}"); - var routeValueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" }; - var expectedRouteValues = new RouteValueDictionary(); - for (var i = 0; i < routeValueKeys.Length; i++) - { - expectedRouteValues.Add(routeValueKeys[i], routeValues[i]); - } + var expectedRouteGroup = CreateRouteGroup(0, "{parameter1=1}/{parameter2=2}/{parameter3=3}/{*parameter4=4}"); + var routeValueKeys = new[] { "parameter1", "parameter2", "parameter3", "parameter4" }; + var expectedRouteValues = new RouteValueDictionary(); + for (var i = 0; i < routeValueKeys.Length; i++) + { + expectedRouteValues.Add(routeValueKeys[i], routeValues[i]); + } - var builder = CreateBuilder(); + var builder = CreateBuilder(); - // We setup the route entries in reverse order of precedence to ensure that when we - // try to route the request, the route with a higher precedence gets tried first. - foreach (var template in routes.Reverse()) - { - MapInboundEntry(builder, template); - } + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + foreach (var template in routes.Reverse()) + { + MapInboundEntry(builder, template); + } - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext(url); + var context = CreateRouteContext(url); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); - foreach (var entry in expectedRouteValues) - { - var data = Assert.Single(context.RouteData.Values, v => v.Key == entry.Key); - Assert.Equal(entry.Value, data.Value); - } + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + foreach (var entry in expectedRouteValues) + { + var data = Assert.Single(context.RouteData.Values, v => v.Key == entry.Key); + Assert.Equal(entry.Value, data.Value); } + } - [Fact] - public async Task TreeRouter_RouteAsync_DoesNotMatchRoutesWithIntermediateDefaultRouteValues() - { - // Arrange - var url = "/a/b"; + [Fact] + public async Task TreeRouter_RouteAsync_DoesNotMatchRoutesWithIntermediateDefaultRouteValues() + { + // Arrange + var url = "/a/b"; - var builder = CreateBuilder(); + var builder = CreateBuilder(); - MapInboundEntry(builder, "a/b/{parameter3=3}/d"); + MapInboundEntry(builder, "a/b/{parameter3=3}/d"); - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext(url); + var context = CreateRouteContext(url); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Null(context.Handler); - } + // Assert + Assert.Null(context.Handler); + } - [Theory] - [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a")] - [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b")] - [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c")] - [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d")] - public async Task TreeRouter_RouteAsync_DoesNotMatchRoutesWithMultipleIntermediateDefaultOrOptionalRouteValues(string template, string url) - { - // Arrange - var builder = CreateBuilder(); + [Theory] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a")] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b")] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c")] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d")] + public async Task TreeRouter_RouteAsync_DoesNotMatchRoutesWithMultipleIntermediateDefaultOrOptionalRouteValues(string template, string url) + { + // Arrange + var builder = CreateBuilder(); - MapInboundEntry(builder, template); + MapInboundEntry(builder, template); - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext(url); + var context = CreateRouteContext(url); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Null(context.Handler); - } + // Assert + Assert.Null(context.Handler); + } - [Theory] - [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e")] - [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e/f")] - public async Task RouteAsync_MatchRoutesWithMultipleIntermediateDefaultOrOptionalRouteValues_WhenAllIntermediateValuesAreProvided(string template, string url) - { - // Arrange - var builder = CreateBuilder(); + [Theory] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e")] + [InlineData("a/{b=3}/c/{d?}/e/{*f}", "/a/b/c/d/e/f")] + public async Task RouteAsync_MatchRoutesWithMultipleIntermediateDefaultOrOptionalRouteValues_WhenAllIntermediateValuesAreProvided(string template, string url) + { + // Arrange + var builder = CreateBuilder(); - MapInboundEntry(builder, template); + MapInboundEntry(builder, template); - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext(url); + var context = CreateRouteContext(url); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.NotNull(context.Handler); - } + // Assert + Assert.NotNull(context.Handler); + } - [Fact] - public async Task TreeRouter_RouteAsync_DoesNotMatchShorterUrl() - { - // Arrange - var routes = new[] { + [Fact] + public async Task TreeRouter_RouteAsync_DoesNotMatchShorterUrl() + { + // Arrange + var routes = new[] { "Literal1/Literal2/Literal3", }; - var builder = CreateBuilder(); + var builder = CreateBuilder(); - // We setup the route entries in reverse order of precedence to ensure that when we - // try to route the request, the route with a higher precedence gets tried first. - foreach (var template in routes.Reverse()) - { - MapInboundEntry(builder, template); - } + // We setup the route entries in reverse order of precedence to ensure that when we + // try to route the request, the route with a higher precedence gets tried first. + foreach (var template in routes.Reverse()) + { + MapInboundEntry(builder, template); + } - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext("/Literal1"); + var context = CreateRouteContext("/Literal1"); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Null(context.Handler); - } + // Assert + Assert.Null(context.Handler); + } - [Theory] - [InlineData("template/5", "template/{parameter:int}")] - [InlineData("template/5", "template/{parameter}")] - [InlineData("template/5", "template/{*parameter:int}")] - [InlineData("template/5", "template/{*parameter}")] - [InlineData("template/{parameter:int}", "template/{parameter}")] - [InlineData("template/{parameter:int}", "template/{*parameter:int}")] - [InlineData("template/{parameter:int}", "template/{*parameter}")] - [InlineData("template/{parameter}", "template/{*parameter:int}")] - [InlineData("template/{parameter}", "template/{*parameter}")] - [InlineData("template/{*parameter:int}", "template/{*parameter}")] - public async Task TreeRouter_RouteAsync_RespectsOrderOverPrecedence( - string firstTemplate, - string secondTemplate) - { - // Arrange - var expectedRouteGroup = CreateRouteGroup(0, secondTemplate); + [Theory] + [InlineData("template/5", "template/{parameter:int}")] + [InlineData("template/5", "template/{parameter}")] + [InlineData("template/5", "template/{*parameter:int}")] + [InlineData("template/5", "template/{*parameter}")] + [InlineData("template/{parameter:int}", "template/{parameter}")] + [InlineData("template/{parameter:int}", "template/{*parameter:int}")] + [InlineData("template/{parameter:int}", "template/{*parameter}")] + [InlineData("template/{parameter}", "template/{*parameter:int}")] + [InlineData("template/{parameter}", "template/{*parameter}")] + [InlineData("template/{*parameter:int}", "template/{*parameter}")] + public async Task TreeRouter_RouteAsync_RespectsOrderOverPrecedence( + string firstTemplate, + string secondTemplate) + { + // Arrange + var expectedRouteGroup = CreateRouteGroup(0, secondTemplate); - var builder = CreateBuilder(); + var builder = CreateBuilder(); - // We setup the route entries with a lower relative order and higher relative precedence - // first to ensure that when we try to route the request, the route with the higher - // relative order gets tried first. - MapInboundEntry(builder, firstTemplate, order: 1); - MapInboundEntry(builder, secondTemplate, order: 0); + // We setup the route entries with a lower relative order and higher relative precedence + // first to ensure that when we try to route the request, the route with the higher + // relative order gets tried first. + MapInboundEntry(builder, firstTemplate, order: 1); + MapInboundEntry(builder, secondTemplate, order: 0); - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext("/template/5"); + var context = CreateRouteContext("/template/5"); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); - } + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + } - [Theory] - [InlineData("///")] - [InlineData("/a//")] - [InlineData("/a/b//")] - [InlineData("//b//")] - [InlineData("///c")] - [InlineData("///c/")] - public async Task TryMatch_MultipleOptionalParameters_WithEmptyIntermediateSegmentsDoesNotMatch(string url) - { - // Arrange - var builder = CreateBuilder(); + [Theory] + [InlineData("///")] + [InlineData("/a//")] + [InlineData("/a/b//")] + [InlineData("//b//")] + [InlineData("///c")] + [InlineData("///c/")] + public async Task TryMatch_MultipleOptionalParameters_WithEmptyIntermediateSegmentsDoesNotMatch(string url) + { + // Arrange + var builder = CreateBuilder(); - MapInboundEntry(builder, "{controller?}/{action?}/{id?}"); + MapInboundEntry(builder, "{controller?}/{action?}/{id?}"); - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext(url); + var context = CreateRouteContext(url); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Null(context.Handler); - } + // Assert + Assert.Null(context.Handler); + } - [Theory] - [InlineData("")] - [InlineData("/")] - [InlineData("/a")] - [InlineData("/a/")] - [InlineData("/a/b")] - [InlineData("/a/b/")] - [InlineData("/a/b/c")] - [InlineData("/a/b/c/")] - public async Task TryMatch_MultipleOptionalParameters_WithIncrementalOptionalValues(string url) - { - // Arrange - var builder = CreateBuilder(); + [Theory] + [InlineData("")] + [InlineData("/")] + [InlineData("/a")] + [InlineData("/a/")] + [InlineData("/a/b")] + [InlineData("/a/b/")] + [InlineData("/a/b/c")] + [InlineData("/a/b/c/")] + public async Task TryMatch_MultipleOptionalParameters_WithIncrementalOptionalValues(string url) + { + // Arrange + var builder = CreateBuilder(); - MapInboundEntry(builder, "{controller?}/{action?}/{id?}"); + MapInboundEntry(builder, "{controller?}/{action?}/{id?}"); - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext(url); + var context = CreateRouteContext(url); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.NotNull(context.Handler); - } + // Assert + Assert.NotNull(context.Handler); + } - [Theory] - [InlineData("///")] - [InlineData("////")] - [InlineData("/a//")] - [InlineData("/a///")] - [InlineData("//b/")] - [InlineData("//b//")] - [InlineData("///c")] - [InlineData("///c/")] - public async Task TryMatch_MultipleParameters_WithEmptyValues(string url) - { - // Arrange - var builder = CreateBuilder(); + [Theory] + [InlineData("///")] + [InlineData("////")] + [InlineData("/a//")] + [InlineData("/a///")] + [InlineData("//b/")] + [InlineData("//b//")] + [InlineData("///c")] + [InlineData("///c/")] + public async Task TryMatch_MultipleParameters_WithEmptyValues(string url) + { + // Arrange + var builder = CreateBuilder(); - MapInboundEntry(builder, "{controller}/{action}/{id}"); + MapInboundEntry(builder, "{controller}/{action}/{id}"); - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext(url); + var context = CreateRouteContext(url); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Null(context.Handler); - } + // Assert + Assert.Null(context.Handler); + } - [Theory] - [InlineData("/a/b/c//")] - [InlineData("/a/b/c/////")] - public async Task TryMatch_CatchAllParameters_WithEmptyValuesAtTheEnd(string url) - { - // Arrange - var builder = CreateBuilder(); + [Theory] + [InlineData("/a/b/c//")] + [InlineData("/a/b/c/////")] + public async Task TryMatch_CatchAllParameters_WithEmptyValuesAtTheEnd(string url) + { + // Arrange + var builder = CreateBuilder(); - MapInboundEntry(builder, "{controller}/{action}/{*id}"); + MapInboundEntry(builder, "{controller}/{action}/{*id}"); - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext(url); + var context = CreateRouteContext(url); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.NotNull(context.Handler); - } + // Assert + Assert.NotNull(context.Handler); + } - [Theory] - [InlineData("/a/b//")] - [InlineData("/a/b///c")] - public async Task TryMatch_CatchAllParameters_WithEmptyValues(string url) - { - // Arrange - var builder = CreateBuilder(); + [Theory] + [InlineData("/a/b//")] + [InlineData("/a/b///c")] + public async Task TryMatch_CatchAllParameters_WithEmptyValues(string url) + { + // Arrange + var builder = CreateBuilder(); - MapInboundEntry(builder, "{controller}/{action}/{*id}"); + MapInboundEntry(builder, "{controller}/{action}/{*id}"); - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext(url); + var context = CreateRouteContext(url); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Null(context.Handler); - } + // Assert + Assert.Null(context.Handler); + } - [Theory] - [InlineData("{*path}", "/a", "a")] - [InlineData("{*path}", "/a/b/c", "a/b/c")] - [InlineData("a/{*path}", "/a/b", "b")] - [InlineData("a/{*path}", "/a/b/c/d", "b/c/d")] - [InlineData("a/{*path:regex(10/20/30)}", "/a/10/20/30", "10/20/30")] - public async Task TreeRouter_RouteAsync_MatchesWildCard_ForLargerPathSegments( - string template, - string requestPath, - string expectedResult) - { - // Arrange - var builder = CreateBuilder(); - MapInboundEntry(builder, template); - var route = builder.Build(); + [Theory] + [InlineData("{*path}", "/a", "a")] + [InlineData("{*path}", "/a/b/c", "a/b/c")] + [InlineData("a/{*path}", "/a/b", "b")] + [InlineData("a/{*path}", "/a/b/c/d", "b/c/d")] + [InlineData("a/{*path:regex(10/20/30)}", "/a/10/20/30", "10/20/30")] + public async Task TreeRouter_RouteAsync_MatchesWildCard_ForLargerPathSegments( + string template, + string requestPath, + string expectedResult) + { + // Arrange + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); - var context = CreateRouteContext(requestPath); + var context = CreateRouteContext(requestPath); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.NotNull(context.Handler); - Assert.Equal(expectedResult, context.RouteData.Values["path"]); - } + // Assert + Assert.NotNull(context.Handler); + Assert.Equal(expectedResult, context.RouteData.Values["path"]); + } - [Theory] - [InlineData("a/{*path}", "/a")] - [InlineData("a/{*path}", "/a/")] - public async Task TreeRouter_RouteAsync_MatchesCatchAll_NullValue( - string template, - string requestPath) - { - // Arrange - var builder = CreateBuilder(); - MapInboundEntry(builder, template); - var route = builder.Build(); + [Theory] + [InlineData("a/{*path}", "/a")] + [InlineData("a/{*path}", "/a/")] + public async Task TreeRouter_RouteAsync_MatchesCatchAll_NullValue( + string template, + string requestPath) + { + // Arrange + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); - var context = CreateRouteContext(requestPath); + var context = CreateRouteContext(requestPath); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.NotNull(context.Handler); - Assert.Null(context.RouteData.Values["path"]); - } + // Assert + Assert.NotNull(context.Handler); + Assert.Null(context.RouteData.Values["path"]); + } - [Theory] - [InlineData("a/{*path}", "/a")] - [InlineData("a/{*path}", "/a/")] - public async Task TreeRouter_RouteAsync_MatchesCatchAll_NullValue_DoesNotReplaceExistingValue( - string template, - string requestPath) - { - // Arrange - var builder = CreateBuilder(); - MapInboundEntry(builder, template); - var route = builder.Build(); + [Theory] + [InlineData("a/{*path}", "/a")] + [InlineData("a/{*path}", "/a/")] + public async Task TreeRouter_RouteAsync_MatchesCatchAll_NullValue_DoesNotReplaceExistingValue( + string template, + string requestPath) + { + // Arrange + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); - var context = CreateRouteContext(requestPath); - context.RouteData.Values["path"] = "existing-value"; + var context = CreateRouteContext(requestPath); + context.RouteData.Values["path"] = "existing-value"; - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.NotNull(context.Handler); - Assert.Equal("existing-value", context.RouteData.Values["path"]); - } + // Assert + Assert.NotNull(context.Handler); + Assert.Equal("existing-value", context.RouteData.Values["path"]); + } - [Theory] - [InlineData("a/{*path=default}", "/a")] - [InlineData("a/{*path=default}", "/a/")] - public async Task TreeRouter_RouteAsync_MatchesCatchAll_UsesDefaultValue( - string template, - string requestPath) - { - // Arrange - var builder = CreateBuilder(); - MapInboundEntry(builder, template); - var route = builder.Build(); + [Theory] + [InlineData("a/{*path=default}", "/a")] + [InlineData("a/{*path=default}", "/a/")] + public async Task TreeRouter_RouteAsync_MatchesCatchAll_UsesDefaultValue( + string template, + string requestPath) + { + // Arrange + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); - var context = CreateRouteContext(requestPath); - context.RouteData.Values["path"] = "existing-value"; + var context = CreateRouteContext(requestPath); + context.RouteData.Values["path"] = "existing-value"; - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.NotNull(context.Handler); - Assert.Equal("default", context.RouteData.Values["path"]); - } + // Assert + Assert.NotNull(context.Handler); + Assert.Equal("default", context.RouteData.Values["path"]); + } - [Theory] - [InlineData("template/5")] - [InlineData("template/{parameter:int}")] - [InlineData("template/{parameter}")] - [InlineData("template/{*parameter:int}")] - [InlineData("template/{*parameter}")] - public async Task TreeRouter_RouteAsync_RespectsOrder(string template) - { - // Arrange - var expectedRouteGroup = CreateRouteGroup(0, template); + [Theory] + [InlineData("template/5")] + [InlineData("template/{parameter:int}")] + [InlineData("template/{parameter}")] + [InlineData("template/{*parameter:int}")] + [InlineData("template/{*parameter}")] + public async Task TreeRouter_RouteAsync_RespectsOrder(string template) + { + // Arrange + var expectedRouteGroup = CreateRouteGroup(0, template); - var builder = CreateBuilder(); + var builder = CreateBuilder(); - // We setup the route entries with a lower relative order first to ensure that when - // we try to route the request, the route with the higher relative order gets tried first. - MapInboundEntry(builder, template, order: 1); - MapInboundEntry(builder, template, order: 0); + // We setup the route entries with a lower relative order first to ensure that when + // we try to route the request, the route with the higher relative order gets tried first. + MapInboundEntry(builder, template, order: 1); + MapInboundEntry(builder, template, order: 0); - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext("/template/5"); + var context = CreateRouteContext("/template/5"); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); - } + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + } - [Theory] - [InlineData("template/{first:int}", "template/{second:int}")] - [InlineData("template/{first}", "template/{second}")] - [InlineData("template/{*first:int}", "template/{*second:int}")] - [InlineData("template/{*first}", "template/{*second}")] - public async Task TreeRouter_RouteAsync_EnsuresStableOrdering(string first, string second) - { - // Arrange - var expectedRouteGroup = CreateRouteGroup(0, first); + [Theory] + [InlineData("template/{first:int}", "template/{second:int}")] + [InlineData("template/{first}", "template/{second}")] + [InlineData("template/{*first:int}", "template/{*second:int}")] + [InlineData("template/{*first}", "template/{*second}")] + public async Task TreeRouter_RouteAsync_EnsuresStableOrdering(string first, string second) + { + // Arrange + var expectedRouteGroup = CreateRouteGroup(0, first); - var builder = CreateBuilder(); + var builder = CreateBuilder(); - // We setup the route entries with a lower relative template order first to ensure that when - // we try to route the request, the route with the higher template order gets tried first. - MapInboundEntry(builder, first); - MapInboundEntry(builder, second); + // We setup the route entries with a lower relative template order first to ensure that when + // we try to route the request, the route with the higher template order gets tried first. + MapInboundEntry(builder, first); + MapInboundEntry(builder, second); - var route = builder.Build(); + var route = builder.Build(); - var context = CreateRouteContext("/template/5"); + var context = CreateRouteContext("/template/5"); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); - } + // Assert + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); + } - [Theory] - [InlineData("template/{parameter:int}", "/template/5", true)] - [InlineData("template/{parameter:int?}", "/template/5", true)] - [InlineData("template/{parameter:int?}", "/template", true)] - [InlineData("template/{parameter:int?}", "/template/qwer", false)] - public async Task TreeRouter_WithOptionalInlineConstraint( - string template, - string request, - bool expectedResult) - { - // Arrange - var expectedRouteGroup = CreateRouteGroup(0, template); + [Theory] + [InlineData("template/{parameter:int}", "/template/5", true)] + [InlineData("template/{parameter:int?}", "/template/5", true)] + [InlineData("template/{parameter:int?}", "/template", true)] + [InlineData("template/{parameter:int?}", "/template/qwer", false)] + public async Task TreeRouter_WithOptionalInlineConstraint( + string template, + string request, + bool expectedResult) + { + // Arrange + var expectedRouteGroup = CreateRouteGroup(0, template); - var builder = CreateBuilder(); - MapInboundEntry(builder, template); - var route = builder.Build(); + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); - var context = CreateRouteContext(request); + var context = CreateRouteContext(request); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - if (expectedResult) - { - Assert.NotNull(context.Handler); - Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); - } - else - { - Assert.Null(context.Handler); - } + // Assert + if (expectedResult) + { + Assert.NotNull(context.Handler); + Assert.Equal(expectedRouteGroup, context.RouteData.Values["test_route_group"]); } - - [Theory] - [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", "foo", "bar", null)] - [InlineData("moo/{p1?}", "/moo/foo", "foo", null, null)] - [InlineData("moo/{p1?}", "/moo", null, null, null)] - [InlineData("moo/{p1}.{p2?}", "/moo/foo", "foo", null, null)] - [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", "foo.", "bar", null)] - [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", "foo.moo", "bar", null)] - [InlineData("moo/{p1}.{p2}", "/moo/foo.bar", "foo", "bar", null)] - [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo.bar", "moo", "bar", null)] - [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", "moo", null, null)] - [InlineData("moo/.{p2?}", "/moo/.foo", null, "foo", null)] - [InlineData("moo/{p1}.{p2?}", "/moo/....", "..", ".", null)] - [InlineData("moo/{p1}.{p2?}", "/moo/.bar", ".bar", null, null)] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", "foo", "moo", null)] - [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] - [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", "foo", "moo", "bar")] - [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", "foo", null, "bar")] - [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", ".foo", null, "bar")] - public async Task TreeRouter_WithOptionalCompositeParameter_Valid( - string template, - string request, - string p1, - string p2, - string p3) + else { - // Arrange - var expectedRouteGroup = CreateRouteGroup(0, template); + Assert.Null(context.Handler); + } + } - var builder = CreateBuilder(); - MapInboundEntry(builder, template); - var route = builder.Build(); + [Theory] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.bar", "foo", "bar", null)] + [InlineData("moo/{p1?}", "/moo/foo", "foo", null, null)] + [InlineData("moo/{p1?}", "/moo", null, null, null)] + [InlineData("moo/{p1}.{p2?}", "/moo/foo", "foo", null, null)] + [InlineData("moo/{p1}.{p2?}", "/moo/foo..bar", "foo.", "bar", null)] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.moo.bar", "foo.moo", "bar", null)] + [InlineData("moo/{p1}.{p2}", "/moo/foo.bar", "foo", "bar", null)] + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo.bar", "moo", "bar", null)] + [InlineData("moo/foo.{p1}.{p2?}", "/moo/foo.moo", "moo", null, null)] + [InlineData("moo/.{p2?}", "/moo/.foo", null, "foo", null)] + [InlineData("moo/{p1}.{p2?}", "/moo/....", "..", ".", null)] + [InlineData("moo/{p1}.{p2?}", "/moo/.bar", ".bar", null, null)] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo", "foo", "moo", null)] + [InlineData("moo/{p1}.{p2}.{p3}.{p4?}", "/moo/foo.moo.bar", "foo", "moo", "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/foo.moo/bar", "foo", "moo", "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/foo/bar", "foo", null, "bar")] + [InlineData("{p1}.{p2?}/{p3}", "/.foo/bar", ".foo", null, "bar")] + public async Task TreeRouter_WithOptionalCompositeParameter_Valid( + string template, + string request, + string p1, + string p2, + string p3) + { + // Arrange + var expectedRouteGroup = CreateRouteGroup(0, template); - var context = CreateRouteContext(request); + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); - // Act - await route.RouteAsync(context); + var context = CreateRouteContext(request); - // Assert - Assert.NotNull(context.Handler); - if (p1 != null) - { - Assert.Equal(p1, context.RouteData.Values["p1"]); - } - if (p2 != null) - { - Assert.Equal(p2, context.RouteData.Values["p2"]); - } - if (p3 != null) - { - Assert.Equal(p3, context.RouteData.Values["p3"]); - } - } + // Act + await route.RouteAsync(context); - [Theory] - [InlineData("moo/{p1}.{p2?}", "/moo/foo.")] - [InlineData("moo/{p1}.{p2?}", "/moo/.")] - [InlineData("moo/{p1}.{p2}", "/foo.")] - [InlineData("moo/{p1}.{p2}", "/foo")] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")] - [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")] - [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")] - [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")] - [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")] - [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")] - [InlineData("moo/.{p2?}", "/moo/.")] - [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")] - public async Task TreeRouter_WithOptionalCompositeParameter_Invalid( - string template, - string request) + // Assert + Assert.NotNull(context.Handler); + if (p1 != null) + { + Assert.Equal(p1, context.RouteData.Values["p1"]); + } + if (p2 != null) { - // Arrange - var expectedRouteGroup = CreateRouteGroup(0, template); + Assert.Equal(p2, context.RouteData.Values["p2"]); + } + if (p3 != null) + { + Assert.Equal(p3, context.RouteData.Values["p3"]); + } + } - var builder = CreateBuilder(); - MapInboundEntry(builder, template); - var route = builder.Build(); + [Theory] + [InlineData("moo/{p1}.{p2?}", "/moo/foo.")] + [InlineData("moo/{p1}.{p2?}", "/moo/.")] + [InlineData("moo/{p1}.{p2}", "/foo.")] + [InlineData("moo/{p1}.{p2}", "/foo")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo.moo.")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/bar.foo.moo")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo.bar")] + [InlineData("moo/foo.{p2}.{p3?}", "/moo/kungfoo.moo")] + [InlineData("moo/{p1}.{p2}.{p3?}", "/moo/foo")] + [InlineData("{p1}.{p2?}/{p3}", "/foo./bar")] + [InlineData("moo/.{p2?}", "/moo/.")] + [InlineData("{p1}.{p2}/{p3}", "/.foo/bar")] + public async Task TreeRouter_WithOptionalCompositeParameter_Invalid( + string template, + string request) + { + // Arrange + var expectedRouteGroup = CreateRouteGroup(0, template); - var context = CreateRouteContext(request); + var builder = CreateBuilder(); + MapInboundEntry(builder, template); + var route = builder.Build(); - // Act - await route.RouteAsync(context); + var context = CreateRouteContext(request); - // Assert - Assert.Null(context.Handler); - } + // Act + await route.RouteAsync(context); - [Theory] - [InlineData("template", "{*url:alpha}", "/template?url=dingo&id=5")] - [InlineData("{*url:alpha}", "{*url}", "/dingo?id=5")] - [InlineData("{id}", "{*url}", "/5?url=dingo")] - [InlineData("{id}", "{*url:alpha}", "/5?url=dingo")] - [InlineData("{id:int}", "{id}", "/5?url=dingo")] - [InlineData("{id}", "{id:alpha}/{url}", "/5?url=dingo")] // constraint doesn't match - [InlineData("template/api/{*url}", "template/api", "/template/api/dingo?id=5")] - [InlineData("template/api", "template/{*url}", "/template/api?url=dingo&id=5")] - [InlineData("template/api", "template/api{id}location", "/template/api?url=dingo&id=5")] - [InlineData("template/api{id}location", "template/{id:int}", "/template/api5location?url=dingo")] - public void TreeRouter_GenerateLink(string firstTemplate, string secondTemplate, string expectedPath) - { - // Arrange - var values = new Dictionary + // Assert + Assert.Null(context.Handler); + } + + [Theory] + [InlineData("template", "{*url:alpha}", "/template?url=dingo&id=5")] + [InlineData("{*url:alpha}", "{*url}", "/dingo?id=5")] + [InlineData("{id}", "{*url}", "/5?url=dingo")] + [InlineData("{id}", "{*url:alpha}", "/5?url=dingo")] + [InlineData("{id:int}", "{id}", "/5?url=dingo")] + [InlineData("{id}", "{id:alpha}/{url}", "/5?url=dingo")] // constraint doesn't match + [InlineData("template/api/{*url}", "template/api", "/template/api/dingo?id=5")] + [InlineData("template/api", "template/{*url}", "/template/api?url=dingo&id=5")] + [InlineData("template/api", "template/api{id}location", "/template/api?url=dingo&id=5")] + [InlineData("template/api{id}location", "template/{id:int}", "/template/api5location?url=dingo")] + public void TreeRouter_GenerateLink(string firstTemplate, string secondTemplate, string expectedPath) + { + // Arrange + var values = new Dictionary { {"url", "dingo" }, {"id", 5 } }; - var route = CreateTreeRouter(firstTemplate, secondTemplate); - var context = CreateVirtualPathContext( - values: values, - ambientValues: null); + var route = CreateTreeRouter(firstTemplate, secondTemplate); + var context = CreateVirtualPathContext( + values: values, + ambientValues: null); - // Act - var result = route.GetVirtualPath(context); + // Act + var result = route.GetVirtualPath(context); - // Assert - Assert.NotNull(result); - Assert.Equal(expectedPath, result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - } - - [Fact] - public void TreeRouter_GenerateLink_LongerTemplateWithDefaultIsMoreSpecific() - { - // Arrange - var firstTemplate = "template"; - var secondTemplate = "template/{parameter:int=1003}"; - - var route = CreateTreeRouter(firstTemplate, secondTemplate); - var context = CreateVirtualPathContext( - values: null, - ambientValues: null); - - // Act - var result = route.GetVirtualPath(context); + // Assert + Assert.NotNull(result); + Assert.Equal(expectedPath, result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } - // Assert - Assert.NotNull(result); - // The Binder binds to /template - Assert.Equal("/template", result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - } + [Fact] + public void TreeRouter_GenerateLink_LongerTemplateWithDefaultIsMoreSpecific() + { + // Arrange + var firstTemplate = "template"; + var secondTemplate = "template/{parameter:int=1003}"; + + var route = CreateTreeRouter(firstTemplate, secondTemplate); + var context = CreateVirtualPathContext( + values: null, + ambientValues: null); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(result); + // The Binder binds to /template + Assert.Equal("/template", result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } - [Theory] - [InlineData("template/{parameter:int=5}", "template", "/template/5")] - [InlineData("template/{parameter}", "template", "/template/5")] - [InlineData("template/{parameter}/{id}", "template/{parameter}", "/template/5/1234")] - public void TreeRouter_GenerateLink_OrderingAgnostic( - string firstTemplate, - string secondTemplate, - string expectedPath) - { - // Arrange - var route = CreateTreeRouter(firstTemplate, secondTemplate); - var parameter = 5; - var id = 1234; - var values = new Dictionary + [Theory] + [InlineData("template/{parameter:int=5}", "template", "/template/5")] + [InlineData("template/{parameter}", "template", "/template/5")] + [InlineData("template/{parameter}/{id}", "template/{parameter}", "/template/5/1234")] + public void TreeRouter_GenerateLink_OrderingAgnostic( + string firstTemplate, + string secondTemplate, + string expectedPath) + { + // Arrange + var route = CreateTreeRouter(firstTemplate, secondTemplate); + var parameter = 5; + var id = 1234; + var values = new Dictionary { { nameof(parameter) , parameter}, { nameof(id), id } }; - var context = CreateVirtualPathContext( - values: null, - ambientValues: values); - - // Act - var result = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(result); - Assert.Equal(expectedPath, result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - } + var context = CreateVirtualPathContext( + values: null, + ambientValues: values); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedPath, result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } - [Theory] - [InlineData("template", "template/{parameter}", "/template/5")] - [InlineData("template/{parameter}", "template/{parameter}/{id}", "/template/5/1234")] - [InlineData("template", "template/{parameter:int=5}", "/template/5")] - public void TreeRouter_GenerateLink_UseAvailableVariables( - string firstTemplate, - string secondTemplate, - string expectedPath) - { - // Arrange - var route = CreateTreeRouter(firstTemplate, secondTemplate); - var parameter = 5; - var id = 1234; - var values = new Dictionary + [Theory] + [InlineData("template", "template/{parameter}", "/template/5")] + [InlineData("template/{parameter}", "template/{parameter}/{id}", "/template/5/1234")] + [InlineData("template", "template/{parameter:int=5}", "/template/5")] + public void TreeRouter_GenerateLink_UseAvailableVariables( + string firstTemplate, + string secondTemplate, + string expectedPath) + { + // Arrange + var route = CreateTreeRouter(firstTemplate, secondTemplate); + var parameter = 5; + var id = 1234; + var values = new Dictionary { { nameof(parameter) , parameter}, { nameof(id), id } }; - var context = CreateVirtualPathContext( - values: null, - ambientValues: values); + var context = CreateVirtualPathContext( + values: null, + ambientValues: values); + + // Act + var result = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedPath, result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } - // Act - var result = route.GetVirtualPath(context); + [Theory] + [InlineData("template/5", "template/{parameter:int}")] + [InlineData("template/5", "template/{parameter}")] + [InlineData("template/5", "template/{*parameter:int}")] + [InlineData("template/5", "template/{*parameter}")] + [InlineData("template/{parameter:int}", "template/{parameter}")] + [InlineData("template/{parameter:int}", "template/{*parameter:int}")] + [InlineData("template/{parameter:int}", "template/{*parameter}")] + [InlineData("template/{parameter}", "template/{*parameter:int}")] + [InlineData("template/{parameter}", "template/{*parameter}")] + [InlineData("template/{*parameter:int}", "template/{*parameter}")] + public void TreeRouter_GenerateLink_RespectsPrecedence(string firstTemplate, string secondTemplate) + { + // Arrange + var builder = CreateBuilder(); - // Assert - Assert.NotNull(result); - Assert.Equal(expectedPath, result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - } + // We setup the route entries in reverse order of precedence to ensure that when we + // try to generate a link, the route with a higher precedence gets tried first. + MapOutboundEntry(builder, secondTemplate); + MapOutboundEntry(builder, firstTemplate); - [Theory] - [InlineData("template/5", "template/{parameter:int}")] - [InlineData("template/5", "template/{parameter}")] - [InlineData("template/5", "template/{*parameter:int}")] - [InlineData("template/5", "template/{*parameter}")] - [InlineData("template/{parameter:int}", "template/{parameter}")] - [InlineData("template/{parameter:int}", "template/{*parameter:int}")] - [InlineData("template/{parameter:int}", "template/{*parameter}")] - [InlineData("template/{parameter}", "template/{*parameter:int}")] - [InlineData("template/{parameter}", "template/{*parameter}")] - [InlineData("template/{*parameter:int}", "template/{*parameter}")] - public void TreeRouter_GenerateLink_RespectsPrecedence(string firstTemplate, string secondTemplate) - { - // Arrange - var builder = CreateBuilder(); + var route = builder.Build(); - // We setup the route entries in reverse order of precedence to ensure that when we - // try to generate a link, the route with a higher precedence gets tried first. - MapOutboundEntry(builder, secondTemplate); - MapOutboundEntry(builder, firstTemplate); + var context = CreateVirtualPathContext(values: null, ambientValues: new { parameter = 5 }); - var route = builder.Build(); + // Act + var result = route.GetVirtualPath(context); - var context = CreateVirtualPathContext(values: null, ambientValues: new { parameter = 5 }); + // Assert + Assert.NotNull(result); + Assert.Equal("/template/5", result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } - // Act - var result = route.GetVirtualPath(context); + [Theory] + [InlineData("template/{parameter:int}", "/template/5", 5)] + [InlineData("template/{parameter:int?}", "/template/5", 5)] + [InlineData("template/{parameter:int?}", "/template", null)] + [InlineData("template/{parameter:int?}", null, "asdf")] + [InlineData("template/{parameter:alpha?}", "/template/asdf", "asdf")] + [InlineData("template/{parameter:alpha?}", "/template", null)] + [InlineData("template/{parameter:int:range(1,20)?}", "/template", null)] + [InlineData("template/{parameter:int:range(1,20)?}", "/template/5", 5)] + [InlineData("template/{parameter:int:range(1,20)?}", null, 21)] + public void TreeRouter_GenerateLink_OptionalInlineParameter( + string template, + string expectedPath, + object parameter) + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, template); + var route = builder.Build(); - // Assert - Assert.NotNull(result); - Assert.Equal("/template/5", result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); + VirtualPathContext context; + if (parameter != null) + { + context = CreateVirtualPathContext(values: null, ambientValues: new { parameter = parameter }); } - - [Theory] - [InlineData("template/{parameter:int}", "/template/5", 5)] - [InlineData("template/{parameter:int?}", "/template/5", 5)] - [InlineData("template/{parameter:int?}", "/template", null)] - [InlineData("template/{parameter:int?}", null, "asdf")] - [InlineData("template/{parameter:alpha?}", "/template/asdf", "asdf")] - [InlineData("template/{parameter:alpha?}", "/template", null)] - [InlineData("template/{parameter:int:range(1,20)?}", "/template", null)] - [InlineData("template/{parameter:int:range(1,20)?}", "/template/5", 5)] - [InlineData("template/{parameter:int:range(1,20)?}", null, 21)] - public void TreeRouter_GenerateLink_OptionalInlineParameter( - string template, - string expectedPath, - object parameter) + else { - // Arrange - var builder = CreateBuilder(); - MapOutboundEntry(builder, template); - var route = builder.Build(); - - VirtualPathContext context; - if (parameter != null) - { - context = CreateVirtualPathContext(values: null, ambientValues: new { parameter = parameter }); - } - else - { - context = CreateVirtualPathContext(values: null, ambientValues: null); - } + context = CreateVirtualPathContext(values: null, ambientValues: null); + } - // Act - var result = route.GetVirtualPath(context); + // Act + var result = route.GetVirtualPath(context); - // Assert - if (expectedPath == null) - { - Assert.Null(result); - } - else - { - Assert.NotNull(result); - Assert.Equal(expectedPath, result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - } + // Assert + if (expectedPath == null) + { + Assert.Null(result); } - - [Theory] - [InlineData("template/5", "template/{parameter:int}")] - [InlineData("template/5", "template/{parameter}")] - [InlineData("template/5", "template/{*parameter:int}")] - [InlineData("template/5", "template/{*parameter}")] - [InlineData("template/{parameter:int}", "template/{parameter}")] - [InlineData("template/{parameter:int}", "template/{*parameter:int}")] - [InlineData("template/{parameter:int}", "template/{*parameter}")] - [InlineData("template/{parameter}", "template/{*parameter:int}")] - [InlineData("template/{parameter}", "template/{*parameter}")] - [InlineData("template/{*parameter:int}", "template/{*parameter}")] - public void TreeRouter_GenerateLink_RespectsOrderOverPrecedence(string firstTemplate, string secondTemplate) + else { - // Arrange - var builder = CreateBuilder(); + Assert.NotNull(result); + Assert.Equal(expectedPath, result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } + } - // We setup the route entries with a lower relative order and higher relative precedence - // first to ensure that when we try to generate a link, the route with the higher - // relative order gets tried first. - MapOutboundEntry(builder, firstTemplate, order: 1); - MapOutboundEntry(builder, secondTemplate, order: 0); + [Theory] + [InlineData("template/5", "template/{parameter:int}")] + [InlineData("template/5", "template/{parameter}")] + [InlineData("template/5", "template/{*parameter:int}")] + [InlineData("template/5", "template/{*parameter}")] + [InlineData("template/{parameter:int}", "template/{parameter}")] + [InlineData("template/{parameter:int}", "template/{*parameter:int}")] + [InlineData("template/{parameter:int}", "template/{*parameter}")] + [InlineData("template/{parameter}", "template/{*parameter:int}")] + [InlineData("template/{parameter}", "template/{*parameter}")] + [InlineData("template/{*parameter:int}", "template/{*parameter}")] + public void TreeRouter_GenerateLink_RespectsOrderOverPrecedence(string firstTemplate, string secondTemplate) + { + // Arrange + var builder = CreateBuilder(); - var route = builder.Build(); + // We setup the route entries with a lower relative order and higher relative precedence + // first to ensure that when we try to generate a link, the route with the higher + // relative order gets tried first. + MapOutboundEntry(builder, firstTemplate, order: 1); + MapOutboundEntry(builder, secondTemplate, order: 0); - var context = CreateVirtualPathContext(null, ambientValues: new { parameter = 5 }); + var route = builder.Build(); - // Act - var result = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(null, ambientValues: new { parameter = 5 }); - // Assert - Assert.NotNull(result); - Assert.Equal("/template/5", result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - } + // Act + var result = route.GetVirtualPath(context); - [Theory] - [InlineData("template/5", "template/5")] - [InlineData("template/{first:int}", "template/{second:int}")] - [InlineData("template/{first}", "template/{second}")] - [InlineData("template/{*first:int}", "template/{*second:int}")] - [InlineData("template/{*first}", "template/{*second}")] - public void TreeRouter_GenerateLink_RespectsOrder(string firstTemplate, string secondTemplate) - { - // Arrange - var builder = CreateBuilder(); + // Assert + Assert.NotNull(result); + Assert.Equal("/template/5", result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } - // We setup the route entries with a lower relative order first to ensure that when - // we try to generate a link, the route with the higher relative order gets tried first. - MapOutboundEntry(builder, firstTemplate, requiredValues: null, order: 1); - MapOutboundEntry(builder, secondTemplate, requiredValues: null, order: 0); + [Theory] + [InlineData("template/5", "template/5")] + [InlineData("template/{first:int}", "template/{second:int}")] + [InlineData("template/{first}", "template/{second}")] + [InlineData("template/{*first:int}", "template/{*second:int}")] + [InlineData("template/{*first}", "template/{*second}")] + public void TreeRouter_GenerateLink_RespectsOrder(string firstTemplate, string secondTemplate) + { + // Arrange + var builder = CreateBuilder(); - var route = builder.Build(); + // We setup the route entries with a lower relative order first to ensure that when + // we try to generate a link, the route with the higher relative order gets tried first. + MapOutboundEntry(builder, firstTemplate, requiredValues: null, order: 1); + MapOutboundEntry(builder, secondTemplate, requiredValues: null, order: 0); - var context = CreateVirtualPathContext(values: null, ambientValues: new { first = 5, second = 5 }); + var route = builder.Build(); - // Act - var result = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(values: null, ambientValues: new { first = 5, second = 5 }); - // Assert - Assert.NotNull(result); - Assert.Equal("/template/5", result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - } + // Act + var result = route.GetVirtualPath(context); - [Theory] - [InlineData("first/5", "second/5")] - [InlineData("first/{first:int}", "second/{second:int}")] - [InlineData("first/{first}", "second/{second}")] - [InlineData("first/{*first:int}", "second/{*second:int}")] - [InlineData("first/{*first}", "second/{*second}")] - public void TreeRouter_GenerateLink_EnsuresStableOrder(string firstTemplate, string secondTemplate) - { - // Arrange - var builder = CreateBuilder(); + // Assert + Assert.NotNull(result); + Assert.Equal("/template/5", result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } - // We setup the route entries with a lower relative template order first to ensure that when - // we try to generate a link, the route with the higher template order gets tried first. - MapOutboundEntry(builder, secondTemplate, requiredValues: null, order: 0); - MapOutboundEntry(builder, firstTemplate, requiredValues: null, order: 0); + [Theory] + [InlineData("first/5", "second/5")] + [InlineData("first/{first:int}", "second/{second:int}")] + [InlineData("first/{first}", "second/{second}")] + [InlineData("first/{*first:int}", "second/{*second:int}")] + [InlineData("first/{*first}", "second/{*second}")] + public void TreeRouter_GenerateLink_EnsuresStableOrder(string firstTemplate, string secondTemplate) + { + // Arrange + var builder = CreateBuilder(); - var route = builder.Build(); + // We setup the route entries with a lower relative template order first to ensure that when + // we try to generate a link, the route with the higher template order gets tried first. + MapOutboundEntry(builder, secondTemplate, requiredValues: null, order: 0); + MapOutboundEntry(builder, firstTemplate, requiredValues: null, order: 0); - var context = CreateVirtualPathContext(values: null, ambientValues: new { first = 5, second = 5 }); + var route = builder.Build(); - // Act - var result = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(values: null, ambientValues: new { first = 5, second = 5 }); - // Assert - Assert.NotNull(result); - Assert.Equal("/first/5", result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - } + // Act + var result = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_GenerateLink_CreatesLinksForRoutesWithIntermediateDefaultRouteValues() - { - // Arrange - var builder = CreateBuilder(); + // Assert + Assert.NotNull(result); + Assert.Equal("/first/5", result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } - MapOutboundEntry(builder, template: "a/b/{parameter3=3}/d", requiredValues: null, order: 0); + [Fact] + public void TreeRouter_GenerateLink_CreatesLinksForRoutesWithIntermediateDefaultRouteValues() + { + // Arrange + var builder = CreateBuilder(); - var route = builder.Build(); + MapOutboundEntry(builder, template: "a/b/{parameter3=3}/d", requiredValues: null, order: 0); - var context = CreateVirtualPathContext(values: null, ambientValues: null); + var route = builder.Build(); - // Act - var result = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(values: null, ambientValues: null); - // Assert - Assert.NotNull(result); - Assert.Equal("/a/b/3/d", result.VirtualPath); - } + // Act + var result = route.GetVirtualPath(context); + // Assert + Assert.NotNull(result); + Assert.Equal("/a/b/3/d", result.VirtualPath); + } - [Fact] - public void TreeRouter_GeneratesLink_ForMultipleNamedEntriesWithTheSameTemplate() - { - // Arrange - var builder = CreateBuilder(); - MapOutboundEntry(builder, "Template", name: "NamedEntry", order: 1); - MapOutboundEntry(builder, "TEMPLATE", name: "NamedEntry", order: 2); + [Fact] + public void TreeRouter_GeneratesLink_ForMultipleNamedEntriesWithTheSameTemplate() + { + // Arrange + var builder = CreateBuilder(); - // Act & Assert (does not throw) - builder.Build(); - } + MapOutboundEntry(builder, "Template", name: "NamedEntry", order: 1); + MapOutboundEntry(builder, "TEMPLATE", name: "NamedEntry", order: 2); - [Fact] - public void TreeRouter_GenerateLink_WithName() - { - // Arrange - var builder = CreateBuilder(); + // Act & Assert (does not throw) + builder.Build(); + } - // The named route has a lower order which will ensure that we aren't trying the route as - // if it were an unnamed route. - MapOutboundEntry(builder, "named", requiredValues: null, order: 1, name: "NamedRoute"); - MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0); + [Fact] + public void TreeRouter_GenerateLink_WithName() + { + // Arrange + var builder = CreateBuilder(); - var route = builder.Build(); + // The named route has a lower order which will ensure that we aren't trying the route as + // if it were an unnamed route. + MapOutboundEntry(builder, "named", requiredValues: null, order: 1, name: "NamedRoute"); + MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0); - var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NamedRoute"); + var route = builder.Build(); - // Act - var result = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NamedRoute"); - // Assert - Assert.NotNull(result); - Assert.Equal("/named", result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - } + // Act + var result = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_DoesNotGenerateLink_IfThereIsNoRouteForAGivenName() - { - // Arrange - var builder = CreateBuilder(); + // Assert + Assert.NotNull(result); + Assert.Equal("/named", result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } - // The named route has a lower order which will ensure that we aren't trying the route as - // if it were an unnamed route. - MapOutboundEntry(builder, "named", requiredValues: null, order: 1, name: "NamedRoute"); + [Fact] + public void TreeRouter_DoesNotGenerateLink_IfThereIsNoRouteForAGivenName() + { + // Arrange + var builder = CreateBuilder(); - // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. - MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0); + // The named route has a lower order which will ensure that we aren't trying the route as + // if it were an unnamed route. + MapOutboundEntry(builder, "named", requiredValues: null, order: 1, name: "NamedRoute"); - var route = builder.Build(); + // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. + MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0); - var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NonExistingNamedRoute"); + var route = builder.Build(); - // Act - var result = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(values: null, ambientValues: null, name: "NonExistingNamedRoute"); - // Assert - Assert.Null(result); - } + // Act + var result = route.GetVirtualPath(context); - [Theory] - [InlineData("template/{parameter:int}", null)] - [InlineData("template/{parameter:int}", "NaN")] - [InlineData("template/{parameter}", null)] - [InlineData("template/{*parameter:int}", null)] - [InlineData("template/{*parameter:int}", "NaN")] - public void TreeRouter_DoesNotGenerateLink_IfValuesDoNotMatchNamedEntry(string template, string value) - { - // Arrange - var builder = CreateBuilder(); + // Assert + Assert.Null(result); + } - // The named route has a lower order which will ensure that we aren't trying the route as - // if it were an unnamed route. - MapOutboundEntry(builder, template, requiredValues: null, order: 1, name: "NamedRoute"); + [Theory] + [InlineData("template/{parameter:int}", null)] + [InlineData("template/{parameter:int}", "NaN")] + [InlineData("template/{parameter}", null)] + [InlineData("template/{*parameter:int}", null)] + [InlineData("template/{*parameter:int}", "NaN")] + public void TreeRouter_DoesNotGenerateLink_IfValuesDoNotMatchNamedEntry(string template, string value) + { + // Arrange + var builder = CreateBuilder(); - // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. - MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0); + // The named route has a lower order which will ensure that we aren't trying the route as + // if it were an unnamed route. + MapOutboundEntry(builder, template, requiredValues: null, order: 1, name: "NamedRoute"); - var route = builder.Build(); + // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. + MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0); - var ambientValues = value == null ? null : new { parameter = value }; - var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute"); + var route = builder.Build(); - // Act - var result = route.GetVirtualPath(context); + var ambientValues = value == null ? null : new { parameter = value }; + var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute"); - // Assert - Assert.Null(result); - } + // Act + var result = route.GetVirtualPath(context); - [Theory] - [InlineData("template/{parameter:int}", "5")] - [InlineData("template/{parameter}", "5")] - [InlineData("template/{*parameter:int}", "5")] - [InlineData("template/{*parameter}", "5")] - public void TreeRouter_GeneratesLink_IfValuesMatchNamedEntry(string template, string value) - { - // Arrange - var builder = CreateBuilder(); + // Assert + Assert.Null(result); + } - // The named route has a lower order which will ensure that we aren't trying the route as - // if it were an unnamed route. - MapOutboundEntry(builder, template, requiredValues: null, order: 1, name: "NamedRoute"); + [Theory] + [InlineData("template/{parameter:int}", "5")] + [InlineData("template/{parameter}", "5")] + [InlineData("template/{*parameter:int}", "5")] + [InlineData("template/{*parameter}", "5")] + public void TreeRouter_GeneratesLink_IfValuesMatchNamedEntry(string template, string value) + { + // Arrange + var builder = CreateBuilder(); - // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. - MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0); + // The named route has a lower order which will ensure that we aren't trying the route as + // if it were an unnamed route. + MapOutboundEntry(builder, template, requiredValues: null, order: 1, name: "NamedRoute"); - var route = builder.Build(); + // Add an unnamed entry to ensure we don't fall back to generating a link for an unnamed route. + MapOutboundEntry(builder, "unnamed", requiredValues: null, order: 0); - var ambientValues = value == null ? null : new { parameter = value }; - var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute"); + var route = builder.Build(); - // Act - var result = route.GetVirtualPath(context); + var ambientValues = value == null ? null : new { parameter = value }; + var context = CreateVirtualPathContext(values: null, ambientValues: ambientValues, name: "NamedRoute"); - // Assert - Assert.NotNull(result); - Assert.Equal("/template/5", result.VirtualPath); - Assert.Same(route, result.Router); - Assert.Empty(result.DataTokens); - } + // Act + var result = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_GenerateLink_NoRequiredValues() - { - // Arrange - var builder = CreateBuilder(); - MapOutboundEntry(builder, "api/Store", new { }); - var route = builder.Build(); + // Assert + Assert.NotNull(result); + Assert.Equal("/template/5", result.VirtualPath); + Assert.Same(route, result.Router); + Assert.Empty(result.DataTokens); + } - var context = CreateVirtualPathContext(new { }); + [Fact] + public void TreeRouter_GenerateLink_NoRequiredValues() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { }); + var route = builder.Build(); - // Act - var pathData = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(new { }); - // Assert - Assert.NotNull(pathData); - Assert.Equal("/api/Store", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + var pathData = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_GenerateLink_Match() - { - // Arrange - var builder = CreateBuilder(); - MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); - var route = builder.Build(); + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); + [Fact] + public void TreeRouter_GenerateLink_Match() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); + var route = builder.Build(); - // Act - var pathData = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); - // Assert - Assert.NotNull(pathData); - Assert.Equal("/api/Store", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + var pathData = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_GenerateLink_NoMatch() - { - // Arrange - var builder = CreateBuilder(); - MapOutboundEntry(builder, "api/Store", new { action = "Details", controller = "Store" }); - var route = builder.Build(); + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); + [Fact] + public void TreeRouter_GenerateLink_NoMatch() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Details", controller = "Store" }); + var route = builder.Build(); - // Act - var path = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); - // Assert - Assert.Null(path); - } + // Act + var path = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_GenerateLink_Match_WithAmbientValues() - { - // Arrange - var builder = CreateBuilder(); - MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); - var route = builder.Build(); + // Assert + Assert.Null(path); + } - var context = CreateVirtualPathContext(new { }, new { action = "Index", controller = "Store" }); + [Fact] + public void TreeRouter_GenerateLink_Match_WithAmbientValues() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); + var route = builder.Build(); - // Act - var pathData = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(new { }, new { action = "Index", controller = "Store" }); - // Assert - Assert.NotNull(pathData); - Assert.Equal("/api/Store", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + var pathData = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_GenerateLink_Match_HasTwoOptionalParametersWithoutValues() - { - // Arrange - var builder = CreateBuilder(); - MapOutboundEntry(builder, "Customers/SeparatePageModels/{handler?}/{id?}", new { page = "/Customers/SeparatePageModels/Index" }); - var route = builder.Build(); + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var context = CreateVirtualPathContext(new { page = "/Customers/SeparatePageModels/Index" }, new { page = "/Customers/SeparatePageModels/Edit", id = "17" }); + [Fact] + public void TreeRouter_GenerateLink_Match_HasTwoOptionalParametersWithoutValues() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "Customers/SeparatePageModels/{handler?}/{id?}", new { page = "/Customers/SeparatePageModels/Index" }); + var route = builder.Build(); - // Act - var pathData = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(new { page = "/Customers/SeparatePageModels/Index" }, new { page = "/Customers/SeparatePageModels/Edit", id = "17" }); - // Assert - Assert.NotNull(pathData); - Assert.Equal("/Customers/SeparatePageModels", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + var pathData = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_GenerateLink_Match_WithParameters() - { - // Arrange - var builder = CreateBuilder(); - MapOutboundEntry(builder, "api/Store/{action}", new { action = "Index", controller = "Store" }); - var route = builder.Build(); + // Assert + Assert.NotNull(pathData); + Assert.Equal("/Customers/SeparatePageModels", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); + [Fact] + public void TreeRouter_GenerateLink_Match_WithParameters() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store/{action}", new { action = "Index", controller = "Store" }); + var route = builder.Build(); - // Act - var pathData = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); - // Assert - Assert.NotNull(pathData); - Assert.Equal("/api/Store/Index", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + var pathData = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_GenerateLink_Match_WithMoreParameters() - { - // Arrange - var builder = CreateBuilder(); - MapOutboundEntry(builder, - "api/{area}/dosomething/{controller}/{action}", - new { action = "Index", controller = "Store", area = "AwesomeCo" }); - - var route = builder.Build(); - - var context = CreateVirtualPathContext( - new { action = "Index", controller = "Store" }, - new { area = "AwesomeCo" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal("/api/AwesomeCo/dosomething/Store/Index", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/Store/Index", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - [Fact] - public void TreeRouter_GenerateLink_Match_WithDefault() - { - // Arrange - var builder = CreateBuilder(); - MapOutboundEntry(builder, "api/Store/{action=Index}", new { action = "Index", controller = "Store" }); - var route = builder.Build(); + [Fact] + public void TreeRouter_GenerateLink_Match_WithMoreParameters() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, + "api/{area}/dosomething/{controller}/{action}", + new { action = "Index", controller = "Store", area = "AwesomeCo" }); + + var route = builder.Build(); + + var context = CreateVirtualPathContext( + new { action = "Index", controller = "Store" }, + new { area = "AwesomeCo" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/AwesomeCo/dosomething/Store/Index", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); + [Fact] + public void TreeRouter_GenerateLink_Match_WithDefault() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store/{action=Index}", new { action = "Index", controller = "Store" }); + var route = builder.Build(); - // Act - var pathData = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store" }); - // Assert - Assert.NotNull(pathData); - Assert.Equal("/api/Store", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + var pathData = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_GenerateLink_Match_WithConstraint() - { - // Arrange - var builder = CreateBuilder(); - MapOutboundEntry(builder, "api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" }); + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var route = builder.Build(); + [Fact] + public void TreeRouter_GenerateLink_Match_WithConstraint() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" }); - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = 5 }); + var route = builder.Build(); - // Act - var pathData = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = 5 }); - // Assert - Assert.NotNull(pathData); - Assert.Equal("/api/Store/Index/5", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + var pathData = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_GenerateLink_NoMatch_WithConstraint() - { - // Arrange - var builder = CreateBuilder(); - MapOutboundEntry(builder, "api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" }); - var route = builder.Build(); + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/Store/Index/5", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var next = new StubRouter(); - var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = "heyyyy" }); + [Fact] + public void TreeRouter_GenerateLink_NoMatch_WithConstraint() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store/{action}/{id:int}", new { action = "Index", controller = "Store" }); + var route = builder.Build(); - // Act - var path = route.GetVirtualPath(context); + var next = new StubRouter(); + var context = CreateVirtualPathContext(new { action = "Index", controller = "Store", id = "heyyyy" }); - // Assert - Assert.Null(path); - } + // Act + var path = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_GenerateLink_Match_WithMixedAmbientValues() - { - // Arrange - var builder = CreateBuilder(); - MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); - var route = builder.Build(); + // Assert + Assert.Null(path); + } - var context = CreateVirtualPathContext(new { action = "Index" }, new { controller = "Store" }); + [Fact] + public void TreeRouter_GenerateLink_Match_WithMixedAmbientValues() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); + var route = builder.Build(); - // Act - var pathData = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(new { action = "Index" }, new { controller = "Store" }); - // Assert - Assert.NotNull(pathData); - Assert.Equal("/api/Store", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + var pathData = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_GenerateLink_Match_WithQueryString() - { - // Arrange - var builder = CreateBuilder(); - MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); - var route = builder.Build(); + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var context = CreateVirtualPathContext(new { action = "Index", id = 5 }, new { controller = "Store" }); + [Fact] + public void TreeRouter_GenerateLink_Match_WithQueryString() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); + var route = builder.Build(); - // Act - var pathData = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(new { action = "Index", id = 5 }, new { controller = "Store" }); - // Assert - Assert.NotNull(pathData); - Assert.Equal("/api/Store?id=5", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + var pathData = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_GenerateLink_RejectedByFirstRoute() - { - // Arrange - var builder = CreateBuilder(); - MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); - MapOutboundEntry(builder, "api2/{controller}", new { action = "Index", controller = "Blog" }); + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api/Store?id=5", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var route = builder.Build(); + [Fact] + public void TreeRouter_GenerateLink_RejectedByFirstRoute() + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, "api/Store", new { action = "Index", controller = "Store" }); + MapOutboundEntry(builder, "api2/{controller}", new { action = "Index", controller = "Blog" }); - var context = CreateVirtualPathContext(new { action = "Index", controller = "Blog" }); + var route = builder.Build(); - // Act - var pathData = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(new { action = "Index", controller = "Blog" }); - // Assert - Assert.NotNull(pathData); - Assert.Equal("/api2/Blog", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + var pathData = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_GenerateLink_ToArea() - { - // Arrange - var builder = CreateBuilder(); - var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); - entry1.Precedence = 2; + // Assert + Assert.NotNull(pathData); + Assert.Equal("/api2/Blog", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" }); - entry2.Precedence = 1; + [Fact] + public void TreeRouter_GenerateLink_ToArea() + { + // Arrange + var builder = CreateBuilder(); + var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); + entry1.Precedence = 2; - var route = builder.Build(); + var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" }); + entry2.Precedence = 1; - var context = CreateVirtualPathContext(new { area = "Help", action = "Edit", controller = "Store" }); + var route = builder.Build(); - // Act - var pathData = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(new { area = "Help", action = "Edit", controller = "Store" }); - // Assert - Assert.NotNull(pathData); - Assert.Equal("/Help/Store", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + var pathData = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_GenerateLink_ToArea_PredecedenceReversed() - { - // Arrange - var builder = CreateBuilder(); - var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); - entry1.Precedence = 1; + // Assert + Assert.NotNull(pathData); + Assert.Equal("/Help/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" }); - entry2.Precedence = 2; + [Fact] + public void TreeRouter_GenerateLink_ToArea_PredecedenceReversed() + { + // Arrange + var builder = CreateBuilder(); + var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); + entry1.Precedence = 1; - var route = builder.Build(); + var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" }); + entry2.Precedence = 2; - var context = CreateVirtualPathContext(new { area = "Help", action = "Edit", controller = "Store" }); + var route = builder.Build(); - // Act - var pathData = route.GetVirtualPath(context); + var context = CreateVirtualPathContext(new { area = "Help", action = "Edit", controller = "Store" }); - // Assert - Assert.NotNull(pathData); - Assert.Equal("/Help/Store", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + var pathData = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_GenerateLink_ToArea_WithAmbientValues() - { - // Arrange - var builder = CreateBuilder(); - var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); - entry1.Precedence = 2; + // Assert + Assert.NotNull(pathData); + Assert.Equal("/Help/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" }); - entry2.Precedence = 1; + [Fact] + public void TreeRouter_GenerateLink_ToArea_WithAmbientValues() + { + // Arrange + var builder = CreateBuilder(); + var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); + entry1.Precedence = 2; - var route = builder.Build(); + var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" }); + entry2.Precedence = 1; - var context = CreateVirtualPathContext( - values: new { action = "Edit", controller = "Store" }, - ambientValues: new { area = "Help" }); + var route = builder.Build(); - // Act - var pathData = route.GetVirtualPath(context); + var context = CreateVirtualPathContext( + values: new { action = "Edit", controller = "Store" }, + ambientValues: new { area = "Help" }); - // Assert - Assert.NotNull(pathData); - Assert.Equal("/Help/Store", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + var pathData = route.GetVirtualPath(context); - [Fact] - public void TreeRouter_GenerateLink_OutOfArea_IgnoresAmbientValue() - { - // Arrange - var builder = CreateBuilder(); - var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); - entry1.Precedence = 2; + // Assert + Assert.NotNull(pathData); + Assert.Equal("/Help/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" }); - entry2.Precedence = 1; + [Fact] + public void TreeRouter_GenerateLink_OutOfArea_IgnoresAmbientValue() + { + // Arrange + var builder = CreateBuilder(); + var entry1 = MapOutboundEntry(builder, "Help/Store", new { area = "Help", action = "Edit", controller = "Store" }); + entry1.Precedence = 2; - var route = builder.Build(); + var entry2 = MapOutboundEntry(builder, "Store", new { area = (string)null, action = "Edit", controller = "Store" }); + entry2.Precedence = 1; - var context = CreateVirtualPathContext( - values: new { action = "Edit", controller = "Store" }, - ambientValues: new { area = "Blog" }); + var route = builder.Build(); - // Act - var pathData = route.GetVirtualPath(context); + var context = CreateVirtualPathContext( + values: new { action = "Edit", controller = "Store" }, + ambientValues: new { area = "Blog" }); - // Assert - Assert.NotNull(pathData); - Assert.Equal("/Store", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Act + var pathData = route.GetVirtualPath(context); - public static IEnumerable OptionalParamValues + // Assert + Assert.NotNull(pathData); + Assert.Equal("/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } + + public static IEnumerable OptionalParamValues + { + get { - get + return new object[][] { - return new object[][] - { // defaults // ambient values // values @@ -1667,459 +1667,458 @@ namespace Microsoft.AspNetCore.Routing.Tree new {val3 = "someval3" }, "/Test/someval1.someval2?val3=someval3", }, - }; - } + }; } + } - [Theory] - [MemberData(nameof(OptionalParamValues))] - public void TreeRouter_GenerateLink_Match_WithOptionalParameters( - string template, - object ambientValues, - object values, - string expected) - { - // Arrange - var builder = CreateBuilder(); - MapOutboundEntry(builder, template); - var route = builder.Build(); + [Theory] + [MemberData(nameof(OptionalParamValues))] + public void TreeRouter_GenerateLink_Match_WithOptionalParameters( + string template, + object ambientValues, + object values, + string expected) + { + // Arrange + var builder = CreateBuilder(); + MapOutboundEntry(builder, template); + var route = builder.Build(); - var context = CreateVirtualPathContext(values, ambientValues); + var context = CreateVirtualPathContext(values, ambientValues); - // Act - var pathData = route.GetVirtualPath(context); + // Act + var pathData = route.GetVirtualPath(context); - // Assert - Assert.NotNull(pathData); - Assert.Equal(expected, pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + // Assert + Assert.NotNull(pathData); + Assert.Equal(expected, pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - [Fact] - public async Task TreeRouter_ReplacesExistingRouteValues_IfNotNull() - { - // Arrange - var builder = CreateBuilder(); - MapInboundEntry(builder, "Foo/{*path}"); - var route = builder.Build(); + [Fact] + public async Task TreeRouter_ReplacesExistingRouteValues_IfNotNull() + { + // Arrange + var builder = CreateBuilder(); + MapInboundEntry(builder, "Foo/{*path}"); + var route = builder.Build(); - var context = CreateRouteContext("/Foo/Bar"); + var context = CreateRouteContext("/Foo/Bar"); - var originalRouteData = context.RouteData; - originalRouteData.Values.Add("path", "default"); + var originalRouteData = context.RouteData; + originalRouteData.Values.Add("path", "default"); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Equal("Bar", context.RouteData.Values["path"]); - } + // Assert + Assert.Equal("Bar", context.RouteData.Values["path"]); + } - [Fact] - public async Task TreeRouter_DoesNotReplaceExistingRouteValues_IfNull() - { - // Arrange - var builder = CreateBuilder(); - MapInboundEntry(builder, "Foo/{*path}"); - var route = builder.Build(); + [Fact] + public async Task TreeRouter_DoesNotReplaceExistingRouteValues_IfNull() + { + // Arrange + var builder = CreateBuilder(); + MapInboundEntry(builder, "Foo/{*path}"); + var route = builder.Build(); - var context = CreateRouteContext("/Foo/"); + var context = CreateRouteContext("/Foo/"); - var originalRouteData = context.RouteData; - originalRouteData.Values.Add("path", "default"); + var originalRouteData = context.RouteData; + originalRouteData.Values.Add("path", "default"); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Equal("default", context.RouteData.Values["path"]); - } + // Assert + Assert.Equal("default", context.RouteData.Values["path"]); + } - [Fact] - public async Task TreeRouter_SnapshotsRouteData() - { - // Arrange - RouteValueDictionary nestedValues = null; - List nestedRouters = null; - - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => - { - nestedValues = new RouteValueDictionary(c.RouteData.Values); - nestedRouters = new List(c.RouteData.Routers); - c.Handler = null; // Not a match + [Fact] + public async Task TreeRouter_SnapshotsRouteData() + { + // Arrange + RouteValueDictionary nestedValues = null; + List nestedRouters = null; + + var next = new Mock(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback(c => + { + nestedValues = new RouteValueDictionary(c.RouteData.Values); + nestedRouters = new List(c.RouteData.Routers); + c.Handler = null; // Not a match }) - .Returns(Task.CompletedTask); + .Returns(Task.CompletedTask); - var builder = CreateBuilder(); - MapInboundEntry(builder, "api/Store", handler: next.Object); - var route = builder.Build(); + var builder = CreateBuilder(); + MapInboundEntry(builder, "api/Store", handler: next.Object); + var route = builder.Build(); - var context = CreateRouteContext("/api/Store"); + var context = CreateRouteContext("/api/Store"); - var routeData = context.RouteData; - routeData.Values.Add("action", "Index"); + var routeData = context.RouteData; + routeData.Values.Add("action", "Index"); - var originalValues = new RouteValueDictionary(context.RouteData.Values); + var originalValues = new RouteValueDictionary(context.RouteData.Values); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.Equal(originalValues, context.RouteData.Values); - Assert.NotEqual(nestedValues, context.RouteData.Values); - } + // Assert + Assert.Equal(originalValues, context.RouteData.Values); + Assert.NotEqual(nestedValues, context.RouteData.Values); + } - [Fact] - public async Task TreeRouter_SnapshotsRouteData_ResetsWhenNotMatched() - { - // Arrange - RouteValueDictionary nestedValues = null; - List nestedRouters = null; - - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => - { - nestedValues = new RouteValueDictionary(c.RouteData.Values); - nestedRouters = new List(c.RouteData.Routers); - c.Handler = null; // Not a match + [Fact] + public async Task TreeRouter_SnapshotsRouteData_ResetsWhenNotMatched() + { + // Arrange + RouteValueDictionary nestedValues = null; + List nestedRouters = null; + + var next = new Mock(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback(c => + { + nestedValues = new RouteValueDictionary(c.RouteData.Values); + nestedRouters = new List(c.RouteData.Routers); + c.Handler = null; // Not a match }) - .Returns(Task.CompletedTask); + .Returns(Task.CompletedTask); - var builder = CreateBuilder(); - MapInboundEntry(builder, "api/Store", handler: next.Object); - var route = builder.Build(); + var builder = CreateBuilder(); + MapInboundEntry(builder, "api/Store", handler: next.Object); + var route = builder.Build(); - var context = CreateRouteContext("/api/Store"); + var context = CreateRouteContext("/api/Store"); - context.RouteData.Values.Add("action", "Index"); + context.RouteData.Values.Add("action", "Index"); - // Act - await route.RouteAsync(context); + // Act + await route.RouteAsync(context); - // Assert - Assert.NotEqual(nestedValues, context.RouteData.Values); + // Assert + Assert.NotEqual(nestedValues, context.RouteData.Values); - // The new routedata is a copy - Assert.Equal("Index", context.RouteData.Values["action"]); - Assert.Equal("Index", nestedValues["action"]); - Assert.DoesNotContain(context.RouteData.Values, kvp => kvp.Key == "test_route_group"); - Assert.Single(nestedValues, kvp => kvp.Key == "test_route_group"); + // The new routedata is a copy + Assert.Equal("Index", context.RouteData.Values["action"]); + Assert.Equal("Index", nestedValues["action"]); + Assert.DoesNotContain(context.RouteData.Values, kvp => kvp.Key == "test_route_group"); + Assert.Single(nestedValues, kvp => kvp.Key == "test_route_group"); - Assert.Empty(context.RouteData.Routers); + Assert.Empty(context.RouteData.Routers); - Assert.Single(nestedRouters); - Assert.Equal(next.Object.GetType(), nestedRouters[0].GetType()); - } + Assert.Single(nestedRouters); + Assert.Equal(next.Object.GetType(), nestedRouters[0].GetType()); + } - [Fact] - public async Task TreeRouter_SnapshotsRouteData_ResetsWhenThrows() - { - // Arrange - RouteValueDictionary nestedValues = null; - List nestedRouters = null; - - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => - { - nestedValues = new RouteValueDictionary(c.RouteData.Values); - nestedRouters = new List(c.RouteData.Routers); - throw new Exception(); - }) - .Returns(Task.CompletedTask); + [Fact] + public async Task TreeRouter_SnapshotsRouteData_ResetsWhenThrows() + { + // Arrange + RouteValueDictionary nestedValues = null; + List nestedRouters = null; + + var next = new Mock(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback(c => + { + nestedValues = new RouteValueDictionary(c.RouteData.Values); + nestedRouters = new List(c.RouteData.Routers); + throw new Exception(); + }) + .Returns(Task.CompletedTask); - var builder = CreateBuilder(); - MapInboundEntry(builder, "api/Store", handler: next.Object); - var route = builder.Build(); + var builder = CreateBuilder(); + MapInboundEntry(builder, "api/Store", handler: next.Object); + var route = builder.Build(); - var context = CreateRouteContext("/api/Store"); - context.RouteData.Values.Add("action", "Index"); + var context = CreateRouteContext("/api/Store"); + context.RouteData.Values.Add("action", "Index"); - // Act - await Assert.ThrowsAsync(() => route.RouteAsync(context)); + // Act + await Assert.ThrowsAsync(() => route.RouteAsync(context)); - // Assert - Assert.NotEqual(nestedValues, context.RouteData.Values); + // Assert + Assert.NotEqual(nestedValues, context.RouteData.Values); - Assert.Equal("Index", context.RouteData.Values["action"]); - Assert.Equal("Index", nestedValues["action"]); - Assert.DoesNotContain(context.RouteData.Values, kvp => kvp.Key == "test_route_group"); - Assert.Single(nestedValues, kvp => kvp.Key == "test_route_group"); + Assert.Equal("Index", context.RouteData.Values["action"]); + Assert.Equal("Index", nestedValues["action"]); + Assert.DoesNotContain(context.RouteData.Values, kvp => kvp.Key == "test_route_group"); + Assert.Single(nestedValues, kvp => kvp.Key == "test_route_group"); - Assert.Empty(context.RouteData.Routers); + Assert.Empty(context.RouteData.Routers); - Assert.Single(nestedRouters); - Assert.Equal(next.Object.GetType(), nestedRouters[0].GetType()); - } + Assert.Single(nestedRouters); + Assert.Equal(next.Object.GetType(), nestedRouters[0].GetType()); + } - [Fact] - public async Task TreeRouter_SnapshotsRouteData_ResetsBeforeMatchingEachRouteEntry() - { - // This test replicates a scenario raised as issue https://github.com/aspnet/Routing/issues/394 - // The RouteValueDictionary entries populated while matching route entries should not be left - // in place if the route entry turns out not to match, because that would leak unwanted state - // to subsequent route entries and might cause "An element with the key ... already exists" - // exceptions. - - // Arrange - RouteValueDictionary nestedValues = null; - var next = new Mock(); - next - .Setup(r => r.RouteAsync(It.IsAny())) - .Callback(c => - { - nestedValues = new RouteValueDictionary(c.RouteData.Values); - c.Handler = NullHandler; - }) - .Returns(Task.CompletedTask); + [Fact] + public async Task TreeRouter_SnapshotsRouteData_ResetsBeforeMatchingEachRouteEntry() + { + // This test replicates a scenario raised as issue https://github.com/aspnet/Routing/issues/394 + // The RouteValueDictionary entries populated while matching route entries should not be left + // in place if the route entry turns out not to match, because that would leak unwanted state + // to subsequent route entries and might cause "An element with the key ... already exists" + // exceptions. + + // Arrange + RouteValueDictionary nestedValues = null; + var next = new Mock(); + next + .Setup(r => r.RouteAsync(It.IsAny())) + .Callback(c => + { + nestedValues = new RouteValueDictionary(c.RouteData.Values); + c.Handler = NullHandler; + }) + .Returns(Task.CompletedTask); + + var builder = CreateBuilder(); + MapInboundEntry(builder, "cat_{category1}/prod1_{product}"); // Matches on first segment but not on second + MapInboundEntry(builder, "cat_{category2}/prod2_{product}", handler: next.Object); + var route = builder.Build(); + + var context = CreateRouteContext("/cat_examplecategory/prod2_exampleproduct"); + + // Act + await route.RouteAsync(context); + + // Assert + Assert.NotNull(nestedValues); + Assert.Equal("examplecategory", nestedValues["category2"]); + Assert.Equal("exampleproduct", nestedValues["product"]); + Assert.DoesNotContain(nestedValues, kvp => kvp.Key == "category1"); + } - var builder = CreateBuilder(); - MapInboundEntry(builder, "cat_{category1}/prod1_{product}"); // Matches on first segment but not on second - MapInboundEntry(builder, "cat_{category2}/prod2_{product}", handler: next.Object); - var route = builder.Build(); + [Fact] + public void TreeRouter_GenerateLink_MatchesNullRequiredValue_WithNullRequestValueString() + { + // Arrange + var builder = CreateBuilder(); + var entry = MapOutboundEntry( + builder, + "Help/Store", + requiredValues: new { area = (string)null, action = "Edit", controller = "Store" }); + var route = builder.Build(); + var context = CreateVirtualPathContext(new { area = (string)null, action = "Edit", controller = "Store" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/Help/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - var context = CreateRouteContext("/cat_examplecategory/prod2_exampleproduct"); + [Fact] + public void TreeRouter_GenerateLink_MatchesNullRequiredValue_WithEmptyRequestValueString() + { + // Arrange + var builder = CreateBuilder(); + var entry = MapOutboundEntry( + builder, + "Help/Store", + requiredValues: new { area = (string)null, action = "Edit", controller = "Store" }); + var route = builder.Build(); + var context = CreateVirtualPathContext(new { area = "", action = "Edit", controller = "Store" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/Help/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Act - await route.RouteAsync(context); + [Fact] + public void TreeRouter_GenerateLink_MatchesEmptyStringRequiredValue_WithNullRequestValueString() + { + // Arrange + var builder = CreateBuilder(); + var entry = MapOutboundEntry( + builder, + "Help/Store", + requiredValues: new { foo = "", action = "Edit", controller = "Store" }); + var route = builder.Build(); + var context = CreateVirtualPathContext(new { foo = (string)null, action = "Edit", controller = "Store" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/Help/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - // Assert - Assert.NotNull(nestedValues); - Assert.Equal("examplecategory", nestedValues["category2"]); - Assert.Equal("exampleproduct", nestedValues["product"]); - Assert.DoesNotContain(nestedValues, kvp => kvp.Key == "category1"); - } + [Fact] + public void TreeRouter_GenerateLink_MatchesEmptyStringRequiredValue_WithEmptyRequestValueString() + { + // Arrange + var builder = CreateBuilder(); + var entry = MapOutboundEntry( + builder, + "Help/Store", + requiredValues: new { foo = "", action = "Edit", controller = "Store" }); + var route = builder.Build(); + var context = CreateVirtualPathContext(new { foo = "", action = "Edit", controller = "Store" }); + + // Act + var pathData = route.GetVirtualPath(context); + + // Assert + Assert.NotNull(pathData); + Assert.Equal("/Help/Store", pathData.VirtualPath); + Assert.Same(route, pathData.Router); + Assert.Empty(pathData.DataTokens); + } - [Fact] - public void TreeRouter_GenerateLink_MatchesNullRequiredValue_WithNullRequestValueString() - { - // Arrange - var builder = CreateBuilder(); - var entry = MapOutboundEntry( - builder, - "Help/Store", - requiredValues: new { area = (string)null, action = "Edit", controller = "Store" }); - var route = builder.Build(); - var context = CreateVirtualPathContext(new { area = (string)null, action = "Edit", controller = "Store" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal("/Help/Store", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + private static RouteContext CreateRouteContext(string requestPath) + { + var request = new Mock(MockBehavior.Strict); + request.SetupGet(r => r.Path).Returns(new PathString(requestPath)); - [Fact] - public void TreeRouter_GenerateLink_MatchesNullRequiredValue_WithEmptyRequestValueString() - { - // Arrange - var builder = CreateBuilder(); - var entry = MapOutboundEntry( - builder, - "Help/Store", - requiredValues: new { area = (string)null, action = "Edit", controller = "Store" }); - var route = builder.Build(); - var context = CreateVirtualPathContext(new { area = "", action = "Edit", controller = "Store" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal("/Help/Store", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + var context = new Mock(MockBehavior.Strict); + context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) + .Returns(NullLoggerFactory.Instance); - [Fact] - public void TreeRouter_GenerateLink_MatchesEmptyStringRequiredValue_WithNullRequestValueString() - { - // Arrange - var builder = CreateBuilder(); - var entry = MapOutboundEntry( - builder, - "Help/Store", - requiredValues: new { foo = "", action = "Edit", controller = "Store" }); - var route = builder.Build(); - var context = CreateVirtualPathContext(new { foo = (string)null, action = "Edit", controller = "Store" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal("/Help/Store", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + context.SetupGet(c => c.Request).Returns(request.Object); - [Fact] - public void TreeRouter_GenerateLink_MatchesEmptyStringRequiredValue_WithEmptyRequestValueString() - { - // Arrange - var builder = CreateBuilder(); - var entry = MapOutboundEntry( - builder, - "Help/Store", - requiredValues: new { foo = "", action = "Edit", controller = "Store" }); - var route = builder.Build(); - var context = CreateVirtualPathContext(new { foo = "", action = "Edit", controller = "Store" }); - - // Act - var pathData = route.GetVirtualPath(context); - - // Assert - Assert.NotNull(pathData); - Assert.Equal("/Help/Store", pathData.VirtualPath); - Assert.Same(route, pathData.Router); - Assert.Empty(pathData.DataTokens); - } + return new RouteContext(context.Object); + } - private static RouteContext CreateRouteContext(string requestPath) - { - var request = new Mock(MockBehavior.Strict); - request.SetupGet(r => r.Path).Returns(new PathString(requestPath)); + private static VirtualPathContext CreateVirtualPathContext( + object values, + object ambientValues = null, + string name = null) + { + var mockHttpContext = new Mock(); + mockHttpContext.Setup(h => h.RequestServices.GetService(typeof(ILoggerFactory))) + .Returns(NullLoggerFactory.Instance); + + return new VirtualPathContext( + mockHttpContext.Object, + new RouteValueDictionary(ambientValues), + new RouteValueDictionary(values), + name); + } - var context = new Mock(MockBehavior.Strict); - context.Setup(m => m.RequestServices.GetService(typeof(ILoggerFactory))) - .Returns(NullLoggerFactory.Instance); + private static InboundRouteEntry MapInboundEntry( + TreeRouteBuilder builder, + string template, + int order = 0, + IRouter handler = null) + { + var entry = builder.MapInbound( + handler ?? new StubRouter(), + TemplateParser.Parse(template), + routeName: null, + order: order); - context.SetupGet(c => c.Request).Returns(request.Object); + // Add a generated 'route group' so we can identify later which entry matched. + entry.Defaults["test_route_group"] = CreateRouteGroup(order, template); - return new RouteContext(context.Object); - } + return entry; + } - private static VirtualPathContext CreateVirtualPathContext( - object values, - object ambientValues = null, - string name = null) - { - var mockHttpContext = new Mock(); - mockHttpContext.Setup(h => h.RequestServices.GetService(typeof(ILoggerFactory))) - .Returns(NullLoggerFactory.Instance); - - return new VirtualPathContext( - mockHttpContext.Object, - new RouteValueDictionary(ambientValues), - new RouteValueDictionary(values), - name); - } + private static OutboundRouteEntry MapOutboundEntry( + TreeRouteBuilder builder, + string template, + object requiredValues = null, + int order = 0, + string name = null, + IRouter handler = null) + { + var entry = builder.MapOutbound( + handler ?? new StubRouter(), + TemplateParser.Parse(template), + requiredLinkValues: new RouteValueDictionary(requiredValues), + routeName: name, + order: order); - private static InboundRouteEntry MapInboundEntry( - TreeRouteBuilder builder, - string template, - int order = 0, - IRouter handler = null) - { - var entry = builder.MapInbound( - handler ?? new StubRouter(), - TemplateParser.Parse(template), - routeName: null, - order: order); + // Add a generated 'route group' so we can identify later which entry matched. + entry.Defaults["test_route_group"] = CreateRouteGroup(order, template); - // Add a generated 'route group' so we can identify later which entry matched. - entry.Defaults["test_route_group"] = CreateRouteGroup(order, template); + return entry; + } - return entry; - } - private static OutboundRouteEntry MapOutboundEntry( - TreeRouteBuilder builder, - string template, - object requiredValues = null, - int order = 0, - string name = null, - IRouter handler = null) - { - var entry = builder.MapOutbound( - handler ?? new StubRouter(), - TemplateParser.Parse(template), - requiredLinkValues: new RouteValueDictionary(requiredValues), - routeName: name, - order: order); + private static string CreateRouteGroup(int order, string template) + { + return string.Format(CultureInfo.InvariantCulture, "{0}&{1}", order, template); + } - // Add a generated 'route group' so we can identify later which entry matched. - entry.Defaults["test_route_group"] = CreateRouteGroup(order, template); + private static DefaultInlineConstraintResolver CreateConstraintResolver() + { + var options = new RouteOptions(); + var optionsMock = new Mock>(); + optionsMock.SetupGet(o => o.Value).Returns(options); - return entry; - } + return new DefaultInlineConstraintResolver(optionsMock.Object, new TestServiceProvider()); + } + private static TreeRouteBuilder CreateBuilder() + { + var objectPoolProvider = new DefaultObjectPoolProvider(); + var objectPolicy = new UriBuilderContextPooledObjectPolicy(); + var objectPool = objectPoolProvider.Create(objectPolicy); + + var constraintResolver = CreateConstraintResolver(); + var builder = new TreeRouteBuilder( + NullLoggerFactory.Instance, + objectPool, + constraintResolver); + return builder; + } - private static string CreateRouteGroup(int order, string template) - { - return string.Format(CultureInfo.InvariantCulture, "{0}&{1}", order, template); - } + private static TreeRouter CreateTreeRouter( + string firstTemplate, + string secondTemplate) + { + var builder = CreateBuilder(); + MapOutboundEntry(builder, firstTemplate); + MapOutboundEntry(builder, secondTemplate); + return builder.Build(); + } - private static DefaultInlineConstraintResolver CreateConstraintResolver() - { - var options = new RouteOptions(); - var optionsMock = new Mock>(); - optionsMock.SetupGet(o => o.Value).Returns(options); + private class StubRouter : IRouter + { + public VirtualPathContext GenerationContext { get; set; } - return new DefaultInlineConstraintResolver(optionsMock.Object, new TestServiceProvider()); - } + public RouteContext MatchingContext { get; set; } - private static TreeRouteBuilder CreateBuilder() - { - var objectPoolProvider = new DefaultObjectPoolProvider(); - var objectPolicy = new UriBuilderContextPooledObjectPolicy(); - var objectPool = objectPoolProvider.Create(objectPolicy); - - var constraintResolver = CreateConstraintResolver(); - var builder = new TreeRouteBuilder( - NullLoggerFactory.Instance, - objectPool, - constraintResolver); - return builder; - } + public Func MatchingDelegate { get; set; } - private static TreeRouter CreateTreeRouter( - string firstTemplate, - string secondTemplate) + public VirtualPathData GetVirtualPath(VirtualPathContext context) { - var builder = CreateBuilder(); - MapOutboundEntry(builder, firstTemplate); - MapOutboundEntry(builder, secondTemplate); - return builder.Build(); + GenerationContext = context; + return null; } - private class StubRouter : IRouter + public Task RouteAsync(RouteContext context) { - public VirtualPathContext GenerationContext { get; set; } - - public RouteContext MatchingContext { get; set; } - - public Func MatchingDelegate { get; set; } - - public VirtualPathData GetVirtualPath(VirtualPathContext context) + if (MatchingDelegate == null) { - GenerationContext = context; - return null; + context.Handler = NullHandler; } - - public Task RouteAsync(RouteContext context) + else { - if (MatchingDelegate == null) - { - context.Handler = NullHandler; - } - else - { - context.Handler = MatchingDelegate(context) ? NullHandler : null; - } - - return Task.FromResult(true); + context.Handler = MatchingDelegate(context) ? NullHandler : null; } + + return Task.FromResult(true); } } } diff --git a/src/Http/Routing/test/UnitTests/UriBuildingContextTest.cs b/src/Http/Routing/test/UnitTests/UriBuildingContextTest.cs index e24fd5306c..7c30aabb01 100644 --- a/src/Http/Routing/test/UnitTests/UriBuildingContextTest.cs +++ b/src/Http/Routing/test/UnitTests/UriBuildingContextTest.cs @@ -4,97 +4,96 @@ using Microsoft.Extensions.WebEncoders.Testing; using Xunit; -namespace Microsoft.AspNetCore.Routing +namespace Microsoft.AspNetCore.Routing; + +public class UriBuildingContextTest { - public class UriBuildingContextTest + [Fact] + public void EncodeValue_EncodesEntireValue_WhenEncodeSlashes_IsFalse() { - [Fact] - public void EncodeValue_EncodesEntireValue_WhenEncodeSlashes_IsFalse() - { - // Arrange - var urlTestEncoder = new UrlTestEncoder(); - var value = "a/b b1/c"; - var expected = "/UrlEncode[[a/b b1/c]]"; - var uriBuilldingContext = new UriBuildingContext(urlTestEncoder); + // Arrange + var urlTestEncoder = new UrlTestEncoder(); + var value = "a/b b1/c"; + var expected = "/UrlEncode[[a/b b1/c]]"; + var uriBuilldingContext = new UriBuildingContext(urlTestEncoder); - // Act - uriBuilldingContext.EncodeValue(value, 0, value.Length, encodeSlashes: true); + // Act + uriBuilldingContext.EncodeValue(value, 0, value.Length, encodeSlashes: true); - // Assert - Assert.Equal(expected, uriBuilldingContext.ToString()); - } + // Assert + Assert.Equal(expected, uriBuilldingContext.ToString()); + } - [Fact] - public void EncodeValue_EncodesOnlySlashes_WhenEncodeSlashes_IsFalse() - { - // Arrange - var urlTestEncoder = new UrlTestEncoder(); - var value = "a/b b1/c"; - var expected = "/UrlEncode[[a]]/UrlEncode[[b b1]]/UrlEncode[[c]]"; - var uriBuilldingContext = new UriBuildingContext(urlTestEncoder); + [Fact] + public void EncodeValue_EncodesOnlySlashes_WhenEncodeSlashes_IsFalse() + { + // Arrange + var urlTestEncoder = new UrlTestEncoder(); + var value = "a/b b1/c"; + var expected = "/UrlEncode[[a]]/UrlEncode[[b b1]]/UrlEncode[[c]]"; + var uriBuilldingContext = new UriBuildingContext(urlTestEncoder); - // Act - uriBuilldingContext.EncodeValue(value, 0, value.Length, encodeSlashes: false); + // Act + uriBuilldingContext.EncodeValue(value, 0, value.Length, encodeSlashes: false); - // Assert - Assert.Equal(expected, uriBuilldingContext.ToString()); - } + // Assert + Assert.Equal(expected, uriBuilldingContext.ToString()); + } - [Theory] - [InlineData("a/b b1/c", 0, 2, "/UrlEncode[[a]]/")] - [InlineData("a/b b1/c", 3, 4, "/UrlEncode[[ b1]]/")] - [InlineData("a/b b1/c", 3, 5, "/UrlEncode[[ b1]]/UrlEncode[[c]]")] - [InlineData("a/b b1/c/", 8, 1, "/")] - [InlineData("/", 0, 1, "/")] - [InlineData("/a", 0, 2, "/UrlEncode[[a]]")] - [InlineData("a", 0, 1, "/UrlEncode[[a]]")] - [InlineData("a/", 0, 2, "/UrlEncode[[a]]/")] - public void EncodeValue_EncodesOnlySlashes_WithinSubsegment_WhenEncodeSlashes_IsFalse( - string value, - int startIndex, - int characterCount, - string expected) - { - // Arrange - var urlTestEncoder = new UrlTestEncoder(); - var uriBuilldingContext = new UriBuildingContext(urlTestEncoder); + [Theory] + [InlineData("a/b b1/c", 0, 2, "/UrlEncode[[a]]/")] + [InlineData("a/b b1/c", 3, 4, "/UrlEncode[[ b1]]/")] + [InlineData("a/b b1/c", 3, 5, "/UrlEncode[[ b1]]/UrlEncode[[c]]")] + [InlineData("a/b b1/c/", 8, 1, "/")] + [InlineData("/", 0, 1, "/")] + [InlineData("/a", 0, 2, "/UrlEncode[[a]]")] + [InlineData("a", 0, 1, "/UrlEncode[[a]]")] + [InlineData("a/", 0, 2, "/UrlEncode[[a]]/")] + public void EncodeValue_EncodesOnlySlashes_WithinSubsegment_WhenEncodeSlashes_IsFalse( + string value, + int startIndex, + int characterCount, + string expected) + { + // Arrange + var urlTestEncoder = new UrlTestEncoder(); + var uriBuilldingContext = new UriBuildingContext(urlTestEncoder); - // Act - uriBuilldingContext.EncodeValue(value, startIndex, characterCount, encodeSlashes: false); + // Act + uriBuilldingContext.EncodeValue(value, startIndex, characterCount, encodeSlashes: false); - // Assert - Assert.Equal(expected, uriBuilldingContext.ToString()); - } + // Assert + Assert.Equal(expected, uriBuilldingContext.ToString()); + } - [Theory] - [InlineData("/Author", false, false, "/UrlEncode[[Author]]")] - [InlineData("/Author", false, true, "/UrlEncode[[Author]]")] - [InlineData("/Author", true, false, "/UrlEncode[[Author]]/")] - [InlineData("/Author", true, true, "/UrlEncode[[Author]]/")] - [InlineData("/Author/", false, false, "/UrlEncode[[Author]]/")] - [InlineData("/Author/", false, true, "/UrlEncode[[Author/]]")] - [InlineData("/Author/", true, false, "/UrlEncode[[Author]]/")] - [InlineData("/Author/", true, true, "/UrlEncode[[Author/]]/")] - [InlineData("Author", false, false, "/UrlEncode[[Author]]")] - [InlineData("Author", false, true, "/UrlEncode[[Author]]")] - [InlineData("Author", true, false, "/UrlEncode[[Author]]/")] - [InlineData("Author", true, true, "/UrlEncode[[Author]]/")] - [InlineData("", false, false, "")] - [InlineData("", false, true, "")] - [InlineData("", true, false, "")] - [InlineData("", true, true, "")] - public void ToPathString(string url, bool appendTrailingSlash, bool encodeSlashes, string expected) - { - // Arrange - var urlTestEncoder = new UrlTestEncoder(); - var uriBuilldingContext = new UriBuildingContext(urlTestEncoder); - uriBuilldingContext.AppendTrailingSlash = appendTrailingSlash; + [Theory] + [InlineData("/Author", false, false, "/UrlEncode[[Author]]")] + [InlineData("/Author", false, true, "/UrlEncode[[Author]]")] + [InlineData("/Author", true, false, "/UrlEncode[[Author]]/")] + [InlineData("/Author", true, true, "/UrlEncode[[Author]]/")] + [InlineData("/Author/", false, false, "/UrlEncode[[Author]]/")] + [InlineData("/Author/", false, true, "/UrlEncode[[Author/]]")] + [InlineData("/Author/", true, false, "/UrlEncode[[Author]]/")] + [InlineData("/Author/", true, true, "/UrlEncode[[Author/]]/")] + [InlineData("Author", false, false, "/UrlEncode[[Author]]")] + [InlineData("Author", false, true, "/UrlEncode[[Author]]")] + [InlineData("Author", true, false, "/UrlEncode[[Author]]/")] + [InlineData("Author", true, true, "/UrlEncode[[Author]]/")] + [InlineData("", false, false, "")] + [InlineData("", false, true, "")] + [InlineData("", true, false, "")] + [InlineData("", true, true, "")] + public void ToPathString(string url, bool appendTrailingSlash, bool encodeSlashes, string expected) + { + // Arrange + var urlTestEncoder = new UrlTestEncoder(); + var uriBuilldingContext = new UriBuildingContext(urlTestEncoder); + uriBuilldingContext.AppendTrailingSlash = appendTrailingSlash; - // Act - uriBuilldingContext.Accept(url, encodeSlashes); + // Act + uriBuilldingContext.Accept(url, encodeSlashes); - // Assert - Assert.Equal(expected, uriBuilldingContext.ToPathString().Value); - } + // Assert + Assert.Equal(expected, uriBuilldingContext.ToPathString().Value); } } diff --git a/src/Http/Routing/test/testassets/Benchmarks/Program.cs b/src/Http/Routing/test/testassets/Benchmarks/Program.cs index 0a2901b150..40eeab110c 100644 --- a/src/Http/Routing/test/testassets/Benchmarks/Program.cs +++ b/src/Http/Routing/test/testassets/Benchmarks/Program.cs @@ -8,62 +8,61 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -namespace Benchmarks +namespace Benchmarks; + +public class Program { - public class Program + public static Task Main(string[] args) { - public static Task Main(string[] args) - { - return GetHostBuilder(args).Build().RunAsync(); - } - - public static IHostBuilder GetHostBuilder(string[] args) - { - var config = new ConfigurationBuilder() - .AddCommandLine(args) - .AddEnvironmentVariables(prefix: "RoutingBenchmarks_") - .Build(); + return GetHostBuilder(args).Build().RunAsync(); + } - // Consoler logger has a major impact on perf results, so do not use - // default builder. + public static IHostBuilder GetHostBuilder(string[] args) + { + var config = new ConfigurationBuilder() + .AddCommandLine(args) + .AddEnvironmentVariables(prefix: "RoutingBenchmarks_") + .Build(); - var hostBuilder = new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .UseKestrel() - .UseTestServer() - .UseConfiguration(config); - }); + // Consoler logger has a major impact on perf results, so do not use + // default builder. - var scenario = config["scenarios"]?.ToLowerInvariant(); - if (scenario == "plaintextdispatcher" || scenario == "plaintextendpointrouting") - { - hostBuilder.ConfigureWebHost(webHostBuilder => + var hostBuilder = new HostBuilder() + .ConfigureWebHost(webHostBuilder => { webHostBuilder - .UseStartup() - // for testing - .UseSetting("Startup", nameof(StartupUsingEndpointRouting)); + .UseKestrel() + .UseTestServer() + .UseConfiguration(config); }); - } - else if (scenario == "plaintextrouting" || scenario == "plaintextrouter") + + var scenario = config["scenarios"]?.ToLowerInvariant(); + if (scenario == "plaintextdispatcher" || scenario == "plaintextendpointrouting") + { + hostBuilder.ConfigureWebHost(webHostBuilder => { - hostBuilder.ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .UseStartup() - // for testing - .UseSetting("Startup", nameof(StartupUsingRouter)); - }); - } - else + webHostBuilder + .UseStartup() + // for testing + .UseSetting("Startup", nameof(StartupUsingEndpointRouting)); + }); + } + else if (scenario == "plaintextrouting" || scenario == "plaintextrouter") + { + hostBuilder.ConfigureWebHost(webHostBuilder => { - throw new InvalidOperationException( - $"Invalid scenario '{scenario}'. Allowed scenarios are PlaintextEndpointRouting and PlaintextRouter"); - } - - return hostBuilder; + webHostBuilder + .UseStartup() + // for testing + .UseSetting("Startup", nameof(StartupUsingRouter)); + }); + } + else + { + throw new InvalidOperationException( + $"Invalid scenario '{scenario}'. Allowed scenarios are PlaintextEndpointRouting and PlaintextRouter"); } + + return hostBuilder; } } diff --git a/src/Http/Routing/test/testassets/Benchmarks/StartupUsingEndpointRouting.cs b/src/Http/Routing/test/testassets/Benchmarks/StartupUsingEndpointRouting.cs index cbda5fdc45..4ab96e45d4 100644 --- a/src/Http/Routing/test/testassets/Benchmarks/StartupUsingEndpointRouting.cs +++ b/src/Http/Routing/test/testassets/Benchmarks/StartupUsingEndpointRouting.cs @@ -2,31 +2,31 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; -namespace Benchmarks +namespace Benchmarks; + +public class StartupUsingEndpointRouting { - public class StartupUsingEndpointRouting + private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!"); + + public void ConfigureServices(IServiceCollection services) { - private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!"); + services.AddRouting(); + } - public void ConfigureServices(IServiceCollection services) - { - services.AddRouting(); - } + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); - public void Configure(IApplicationBuilder app) + app.UseEndpoints(endpoints => { - app.UseRouting(); - - app.UseEndpoints(endpoints => + var endpointDataSource = new DefaultEndpointDataSource(new[] { - var endpointDataSource = new DefaultEndpointDataSource(new[] - { new RouteEndpoint( requestDelegate: (httpContext) => { @@ -41,10 +41,9 @@ namespace Benchmarks order: 0, metadata: EndpointMetadataCollection.Empty, displayName: "Plaintext"), - }); - - endpoints.DataSources.Add(endpointDataSource); }); - } + + endpoints.DataSources.Add(endpointDataSource); + }); } } diff --git a/src/Http/Routing/test/testassets/Benchmarks/StartupUsingRouter.cs b/src/Http/Routing/test/testassets/Benchmarks/StartupUsingRouter.cs index ff9a425a74..c1c9f8d5da 100644 --- a/src/Http/Routing/test/testassets/Benchmarks/StartupUsingRouter.cs +++ b/src/Http/Routing/test/testassets/Benchmarks/StartupUsingRouter.cs @@ -1,36 +1,35 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -using System.Text; -namespace Benchmarks +namespace Benchmarks; + +public class StartupUsingRouter { - public class StartupUsingRouter - { - private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!"); + private static readonly byte[] _helloWorldPayload = Encoding.UTF8.GetBytes("Hello, World!"); - public void ConfigureServices(IServiceCollection services) - { - services.AddRouting(); - } + public void ConfigureServices(IServiceCollection services) + { + services.AddRouting(); + } - public void Configure(IApplicationBuilder app) + public void Configure(IApplicationBuilder app) + { + app.UseRouter(routes => { - app.UseRouter(routes => + routes.MapRoute("/plaintext", (httpContext) => { - routes.MapRoute("/plaintext", (httpContext) => - { - var response = httpContext.Response; - var payloadLength = _helloWorldPayload.Length; - response.StatusCode = 200; - response.ContentType = "text/plain"; - response.ContentLength = payloadLength; - return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength); - }); + var response = httpContext.Response; + var payloadLength = _helloWorldPayload.Length; + response.StatusCode = 200; + response.ContentType = "text/plain"; + response.ContentLength = payloadLength; + return response.Body.WriteAsync(_helloWorldPayload, 0, payloadLength); }); - } + }); } -} \ No newline at end of file +} diff --git a/src/Http/Routing/test/testassets/RoutingSandbox/Framework/FrameworkConfigurationBuilder.cs b/src/Http/Routing/test/testassets/RoutingSandbox/Framework/FrameworkConfigurationBuilder.cs index 65bd086f5c..7e8fd3e2d5 100644 --- a/src/Http/Routing/test/testassets/RoutingSandbox/Framework/FrameworkConfigurationBuilder.cs +++ b/src/Http/Routing/test/testassets/RoutingSandbox/Framework/FrameworkConfigurationBuilder.cs @@ -4,35 +4,34 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing.Patterns; -namespace RoutingSandbox.Framework +namespace RoutingSandbox.Framework; + +public class FrameworkConfigurationBuilder { - public class FrameworkConfigurationBuilder - { - private readonly FrameworkEndpointDataSource _dataSource; + private readonly FrameworkEndpointDataSource _dataSource; - internal FrameworkConfigurationBuilder(FrameworkEndpointDataSource dataSource) - { - _dataSource = dataSource; - } + internal FrameworkConfigurationBuilder(FrameworkEndpointDataSource dataSource) + { + _dataSource = dataSource; + } - public void AddPattern(string pattern) - { - AddPattern(RoutePatternFactory.Parse(pattern)); - } + public void AddPattern(string pattern) + { + AddPattern(RoutePatternFactory.Parse(pattern)); + } - public void AddPattern(RoutePattern pattern) - { - _dataSource.Patterns.Add(pattern); - } + public void AddPattern(RoutePattern pattern) + { + _dataSource.Patterns.Add(pattern); + } - public void AddHubMethod(string hub, string method, RequestDelegate requestDelegate) + public void AddHubMethod(string hub, string method, RequestDelegate requestDelegate) + { + _dataSource.HubMethods.Add(new HubMethod { - _dataSource.HubMethods.Add(new HubMethod - { - Hub = hub, - Method = method, - RequestDelegate = requestDelegate - }); - } + Hub = hub, + Method = method, + RequestDelegate = requestDelegate + }); } } diff --git a/src/Http/Routing/test/testassets/RoutingSandbox/Framework/FrameworkEndpointDataSource.cs b/src/Http/Routing/test/testassets/RoutingSandbox/Framework/FrameworkEndpointDataSource.cs index 2432aefe58..5fa0249a88 100644 --- a/src/Http/Routing/test/testassets/RoutingSandbox/Framework/FrameworkEndpointDataSource.cs +++ b/src/Http/Routing/test/testassets/RoutingSandbox/Framework/FrameworkEndpointDataSource.cs @@ -12,90 +12,89 @@ using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; -namespace RoutingSandbox.Framework +namespace RoutingSandbox.Framework; + +internal class FrameworkEndpointDataSource : EndpointDataSource, IEndpointConventionBuilder { - internal class FrameworkEndpointDataSource : EndpointDataSource, IEndpointConventionBuilder - { - private readonly RoutePatternTransformer _routePatternTransformer; - private readonly List> _conventions; + private readonly RoutePatternTransformer _routePatternTransformer; + private readonly List> _conventions; - public List Patterns { get; } - public List HubMethods { get; } + public List Patterns { get; } + public List HubMethods { get; } - private List _endpoints; + private List _endpoints; - public FrameworkEndpointDataSource(RoutePatternTransformer routePatternTransformer) - { - _routePatternTransformer = routePatternTransformer; - _conventions = new List>(); + public FrameworkEndpointDataSource(RoutePatternTransformer routePatternTransformer) + { + _routePatternTransformer = routePatternTransformer; + _conventions = new List>(); - Patterns = new List(); - HubMethods = new List(); - } + Patterns = new List(); + HubMethods = new List(); + } - public override IReadOnlyList Endpoints + public override IReadOnlyList Endpoints + { + get { - get + if (_endpoints == null) { - if (_endpoints == null) - { - _endpoints = BuildEndpoints(); - } - - return _endpoints; + _endpoints = BuildEndpoints(); } + + return _endpoints; } + } + + private List BuildEndpoints() + { + List endpoints = new List(); - private List BuildEndpoints() + foreach (var hubMethod in HubMethods) { - List endpoints = new List(); + var requiredValues = new { hub = hubMethod.Hub, method = hubMethod.Method }; + var order = 1; - foreach (var hubMethod in HubMethods) + foreach (var pattern in Patterns) { - var requiredValues = new { hub = hubMethod.Hub, method = hubMethod.Method }; - var order = 1; + var resolvedPattern = _routePatternTransformer.SubstituteRequiredValues(pattern, requiredValues); + if (resolvedPattern == null) + { + continue; + } - foreach (var pattern in Patterns) + var endpointBuilder = new RouteEndpointBuilder( + hubMethod.RequestDelegate, + resolvedPattern, + order++); + endpointBuilder.DisplayName = $"{hubMethod.Hub}.{hubMethod.Method}"; + + foreach (var convention in _conventions) { - var resolvedPattern = _routePatternTransformer.SubstituteRequiredValues(pattern, requiredValues); - if (resolvedPattern == null) - { - continue; - } - - var endpointBuilder = new RouteEndpointBuilder( - hubMethod.RequestDelegate, - resolvedPattern, - order++); - endpointBuilder.DisplayName = $"{hubMethod.Hub}.{hubMethod.Method}"; - - foreach (var convention in _conventions) - { - convention(endpointBuilder); - } - - endpoints.Add(endpointBuilder.Build()); + convention(endpointBuilder); } - } - return endpoints; + endpoints.Add(endpointBuilder.Build()); + } } - public override IChangeToken GetChangeToken() - { - return NullChangeToken.Singleton; - } + return endpoints; + } - public void Add(Action convention) - { - _conventions.Add(convention); - } + public override IChangeToken GetChangeToken() + { + return NullChangeToken.Singleton; } - internal class HubMethod + public void Add(Action convention) { - public string Hub { get; set; } - public string Method { get; set; } - public RequestDelegate RequestDelegate { get; set; } + _conventions.Add(convention); } } + +internal class HubMethod +{ + public string Hub { get; set; } + public string Method { get; set; } + public RequestDelegate RequestDelegate { get; set; } +} diff --git a/src/Http/Routing/test/testassets/RoutingSandbox/Framework/FrameworkEndpointRouteBuilderExtensions.cs b/src/Http/Routing/test/testassets/RoutingSandbox/Framework/FrameworkEndpointRouteBuilderExtensions.cs index 47cb6ea28b..2c6676ff21 100644 --- a/src/Http/Routing/test/testassets/RoutingSandbox/Framework/FrameworkEndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/test/testassets/RoutingSandbox/Framework/FrameworkEndpointRouteBuilderExtensions.cs @@ -11,29 +11,28 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; -namespace RoutingSandbox.Framework +namespace RoutingSandbox.Framework; + +public static class FrameworkEndpointRouteBuilderExtensions { - public static class FrameworkEndpointRouteBuilderExtensions + public static IEndpointConventionBuilder MapFramework(this IEndpointRouteBuilder endpoints, Action configure) { - public static IEndpointConventionBuilder MapFramework(this IEndpointRouteBuilder endpoints, Action configure) + if (endpoints == null) + { + throw new ArgumentNullException(nameof(endpoints)); + } + if (configure == null) { - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } - if (configure == null) - { - throw new ArgumentNullException(nameof(configure)); - } + throw new ArgumentNullException(nameof(configure)); + } - var dataSource = endpoints.ServiceProvider.GetRequiredService(); + var dataSource = endpoints.ServiceProvider.GetRequiredService(); - var configurationBuilder = new FrameworkConfigurationBuilder(dataSource); - configure(configurationBuilder); + var configurationBuilder = new FrameworkConfigurationBuilder(dataSource); + configure(configurationBuilder); - endpoints.DataSources.Add(dataSource); + endpoints.DataSources.Add(dataSource); - return dataSource; - } + return dataSource; } } diff --git a/src/Http/Routing/test/testassets/RoutingSandbox/HelloExtension/EndpointRouteBuilderExtensions.cs b/src/Http/Routing/test/testassets/RoutingSandbox/HelloExtension/EndpointRouteBuilderExtensions.cs index 8134a8c3ab..ec63313241 100644 --- a/src/Http/Routing/test/testassets/RoutingSandbox/HelloExtension/EndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/test/testassets/RoutingSandbox/HelloExtension/EndpointRouteBuilderExtensions.cs @@ -9,22 +9,21 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Patterns; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +public static class EndpointRouteBuilderExtensions { - public static class EndpointRouteBuilderExtensions + public static IEndpointConventionBuilder MapHello(this IEndpointRouteBuilder endpoints, string pattern, string greeter) { - public static IEndpointConventionBuilder MapHello(this IEndpointRouteBuilder endpoints, string pattern, string greeter) + if (endpoints == null) { - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } + throw new ArgumentNullException(nameof(endpoints)); + } - var pipeline = endpoints.CreateApplicationBuilder() - .UseHello(greeter) - .Build(); + var pipeline = endpoints.CreateApplicationBuilder() + .UseHello(greeter) + .Build(); - return endpoints.Map(pattern, pipeline); - } + return endpoints.Map(pattern, pipeline); } } diff --git a/src/Http/Routing/test/testassets/RoutingSandbox/HelloExtension/HelloAppBuilderExtensions.cs b/src/Http/Routing/test/testassets/RoutingSandbox/HelloExtension/HelloAppBuilderExtensions.cs index 2c56b23a8c..6459bf9448 100644 --- a/src/Http/Routing/test/testassets/RoutingSandbox/HelloExtension/HelloAppBuilderExtensions.cs +++ b/src/Http/Routing/test/testassets/RoutingSandbox/HelloExtension/HelloAppBuilderExtensions.cs @@ -5,21 +5,20 @@ using System; using Microsoft.Extensions.Options; using RoutingSample.Web.HelloExtension; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +public static class HelloAppBuilderExtensions { - public static class HelloAppBuilderExtensions + public static IApplicationBuilder UseHello(this IApplicationBuilder app, string greeter) { - public static IApplicationBuilder UseHello(this IApplicationBuilder app, string greeter) + if (app == null) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - return app.UseMiddleware(Options.Create(new HelloOptions - { - Greeter = greeter - })); + throw new ArgumentNullException(nameof(app)); } + + return app.UseMiddleware(Options.Create(new HelloOptions + { + Greeter = greeter + })); } } diff --git a/src/Http/Routing/test/testassets/RoutingSandbox/HelloExtension/HelloMiddleware.cs b/src/Http/Routing/test/testassets/RoutingSandbox/HelloExtension/HelloMiddleware.cs index 7f2f28c601..725238e4ea 100644 --- a/src/Http/Routing/test/testassets/RoutingSandbox/HelloExtension/HelloMiddleware.cs +++ b/src/Http/Routing/test/testassets/RoutingSandbox/HelloExtension/HelloMiddleware.cs @@ -9,37 +9,36 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -namespace RoutingSample.Web.HelloExtension +namespace RoutingSample.Web.HelloExtension; + +public class HelloMiddleware { - public class HelloMiddleware + private readonly RequestDelegate _next; + private readonly HelloOptions _helloOptions; + private readonly byte[] _helloPayload; + + public HelloMiddleware(RequestDelegate next, IOptions helloOptions) { - private readonly RequestDelegate _next; - private readonly HelloOptions _helloOptions; - private readonly byte[] _helloPayload; + _next = next; + _helloOptions = helloOptions.Value; - public HelloMiddleware(RequestDelegate next, IOptions helloOptions) + var payload = new List(); + payload.AddRange(Encoding.UTF8.GetBytes("Hello")); + if (!string.IsNullOrEmpty(_helloOptions.Greeter)) { - _next = next; - _helloOptions = helloOptions.Value; - - var payload = new List(); - payload.AddRange(Encoding.UTF8.GetBytes("Hello")); - if (!string.IsNullOrEmpty(_helloOptions.Greeter)) - { - payload.Add((byte)' '); - payload.AddRange(Encoding.UTF8.GetBytes(_helloOptions.Greeter)); - } - _helloPayload = payload.ToArray(); + payload.Add((byte)' '); + payload.AddRange(Encoding.UTF8.GetBytes(_helloOptions.Greeter)); } + _helloPayload = payload.ToArray(); + } - public Task InvokeAsync(HttpContext context) - { - var response = context.Response; - var payloadLength = _helloPayload.Length; - response.StatusCode = 200; - response.ContentType = "text/plain"; - response.ContentLength = payloadLength; - return response.Body.WriteAsync(_helloPayload, 0, payloadLength); - } + public Task InvokeAsync(HttpContext context) + { + var response = context.Response; + var payloadLength = _helloPayload.Length; + response.StatusCode = 200; + response.ContentType = "text/plain"; + response.ContentLength = payloadLength; + return response.Body.WriteAsync(_helloPayload, 0, payloadLength); } } diff --git a/src/Http/Routing/test/testassets/RoutingSandbox/HelloExtension/HelloOptions.cs b/src/Http/Routing/test/testassets/RoutingSandbox/HelloExtension/HelloOptions.cs index 672b27769d..a97f4d1b03 100644 --- a/src/Http/Routing/test/testassets/RoutingSandbox/HelloExtension/HelloOptions.cs +++ b/src/Http/Routing/test/testassets/RoutingSandbox/HelloExtension/HelloOptions.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace RoutingSample.Web.HelloExtension +namespace RoutingSample.Web.HelloExtension; + +public class HelloOptions { - public class HelloOptions - { - public string Greeter { get; set; } - } + public string Greeter { get; set; } } diff --git a/src/Http/Routing/test/testassets/RoutingSandbox/Program.cs b/src/Http/Routing/test/testassets/RoutingSandbox/Program.cs index 4d02d0d815..684499d3bf 100644 --- a/src/Http/Routing/test/testassets/RoutingSandbox/Program.cs +++ b/src/Http/Routing/test/testassets/RoutingSandbox/Program.cs @@ -9,71 +9,70 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace RoutingSandbox +namespace RoutingSandbox; + +public class Program { - public class Program + public const string EndpointRoutingScenario = "endpointrouting"; + public const string RouterScenario = "router"; + + public static Task Main(string[] args) { - public const string EndpointRoutingScenario = "endpointrouting"; - public const string RouterScenario = "router"; + var host = GetHostBuilder(args).Build(); + return host.RunAsync(); + } - public static Task Main(string[] args) + // For unit testing + public static IHostBuilder GetHostBuilder(string[] args) + { + string scenario; + if (args.Length == 0) { - var host = GetHostBuilder(args).Build(); - return host.RunAsync(); - } + Console.WriteLine("Choose a sample to run:"); + Console.WriteLine($"1. {EndpointRoutingScenario}"); + Console.WriteLine($"2. {RouterScenario}"); + Console.WriteLine(); - // For unit testing - public static IHostBuilder GetHostBuilder(string[] args) + scenario = Console.ReadLine(); + } + else { - string scenario; - if (args.Length == 0) - { - Console.WriteLine("Choose a sample to run:"); - Console.WriteLine($"1. {EndpointRoutingScenario}"); - Console.WriteLine($"2. {RouterScenario}"); - Console.WriteLine(); - - scenario = Console.ReadLine(); - } - else - { - scenario = args[0]; - } - - Type startupType; - switch (scenario) - { - case "1": - case EndpointRoutingScenario: - startupType = typeof(UseEndpointRoutingStartup); - break; + scenario = args[0]; + } - case "2": - case RouterScenario: - startupType = typeof(UseRouterStartup); - break; + Type startupType; + switch (scenario) + { + case "1": + case EndpointRoutingScenario: + startupType = typeof(UseEndpointRoutingStartup); + break; - default: - Console.WriteLine($"unknown scenario {scenario}"); - Console.WriteLine($"usage: dotnet run -- ({EndpointRoutingScenario}|{RouterScenario})"); - throw new InvalidOperationException(); + case "2": + case RouterScenario: + startupType = typeof(UseRouterStartup); + break; - } + default: + Console.WriteLine($"unknown scenario {scenario}"); + Console.WriteLine($"usage: dotnet run -- ({EndpointRoutingScenario}|{RouterScenario})"); + throw new InvalidOperationException(); - return new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .UseKestrel() - .UseIISIntegration() - .UseContentRoot(Environment.CurrentDirectory) - .UseStartup(startupType); - }) - .ConfigureLogging(b => - { - b.AddConsole(); - b.SetMinimumLevel(LogLevel.Critical); - }); } + + return new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseKestrel() + .UseIISIntegration() + .UseContentRoot(Environment.CurrentDirectory) + .UseStartup(startupType); + }) + .ConfigureLogging(b => + { + b.AddConsole(); + b.SetMinimumLevel(LogLevel.Critical); + }); } } diff --git a/src/Http/Routing/test/testassets/RoutingSandbox/SlugifyParameterTransformer.cs b/src/Http/Routing/test/testassets/RoutingSandbox/SlugifyParameterTransformer.cs index 693db12bd2..8acba74542 100644 --- a/src/Http/Routing/test/testassets/RoutingSandbox/SlugifyParameterTransformer.cs +++ b/src/Http/Routing/test/testassets/RoutingSandbox/SlugifyParameterTransformer.cs @@ -6,14 +6,13 @@ using System.Globalization; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Routing; -namespace RoutingSandbox +namespace RoutingSandbox; + +public class SlugifyParameterTransformer : IOutboundParameterTransformer { - public class SlugifyParameterTransformer : IOutboundParameterTransformer + public string TransformOutbound(object value) { - public string TransformOutbound(object value) - { - // Slugify value - return value == null ? null : Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2", RegexOptions.None, TimeSpan.FromMilliseconds(100)).ToLowerInvariant(); - } + // Slugify value + return value == null ? null : Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2", RegexOptions.None, TimeSpan.FromMilliseconds(100)).ToLowerInvariant(); } } diff --git a/src/Http/Routing/test/testassets/RoutingSandbox/UseEndpointRoutingStartup.cs b/src/Http/Routing/test/testassets/RoutingSandbox/UseEndpointRoutingStartup.cs index 48744b5d1b..c53fd3300a 100644 --- a/src/Http/Routing/test/testassets/RoutingSandbox/UseEndpointRoutingStartup.cs +++ b/src/Http/Routing/test/testassets/RoutingSandbox/UseEndpointRoutingStartup.cs @@ -14,115 +14,114 @@ using Microsoft.AspNetCore.Routing.Internal; using Microsoft.Extensions.DependencyInjection; using RoutingSandbox.Framework; -namespace RoutingSandbox +namespace RoutingSandbox; + +public class UseEndpointRoutingStartup { - public class UseEndpointRoutingStartup - { - private static readonly byte[] _plainTextPayload = Encoding.UTF8.GetBytes("Plain text!"); + private static readonly byte[] _plainTextPayload = Encoding.UTF8.GetBytes("Plain text!"); - public void ConfigureServices(IServiceCollection services) + public void ConfigureServices(IServiceCollection services) + { + services.AddRouting(options => { - services.AddRouting(options => - { - options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); - }); - services.AddSingleton(); - } + options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer); + }); + services.AddSingleton(); + } - public void Configure(IApplicationBuilder app) + public void Configure(IApplicationBuilder app) + { + app.UseStaticFiles(); + app.UseRouting(); + app.UseEndpoints(endpoints => { - app.UseStaticFiles(); - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapHello("/helloworld", "World"); + endpoints.MapHello("/helloworld", "World"); - endpoints.MapGet( - "/", - (httpContext) => - { - var dataSource = httpContext.RequestServices.GetRequiredService(); + endpoints.MapGet( + "/", + (httpContext) => + { + var dataSource = httpContext.RequestServices.GetRequiredService(); - var sb = new StringBuilder(); - sb.AppendLine("Endpoints:"); - foreach (var endpoint in dataSource.Endpoints.OfType().OrderBy(e => e.RoutePattern.RawText, StringComparer.OrdinalIgnoreCase)) + var sb = new StringBuilder(); + sb.AppendLine("Endpoints:"); + foreach (var endpoint in dataSource.Endpoints.OfType().OrderBy(e => e.RoutePattern.RawText, StringComparer.OrdinalIgnoreCase)) + { + sb.AppendLine(FormattableString.Invariant($"- {endpoint.RoutePattern.RawText}")); + foreach (var metadata in endpoint.Metadata) { - sb.AppendLine(FormattableString.Invariant($"- {endpoint.RoutePattern.RawText}")); - foreach (var metadata in endpoint.Metadata) - { - sb.AppendLine(" " + metadata); - } + sb.AppendLine(" " + metadata); } + } - var response = httpContext.Response; - response.StatusCode = 200; - response.ContentType = "text/plain"; - return response.WriteAsync(sb.ToString()); - }); - endpoints.MapGet( - "/plaintext", - (httpContext) => - { - var response = httpContext.Response; - var payloadLength = _plainTextPayload.Length; - response.StatusCode = 200; - response.ContentType = "text/plain"; - response.ContentLength = payloadLength; - return response.Body.WriteAsync(_plainTextPayload, 0, payloadLength); - }); - endpoints.MapGet( - "/graph", - (httpContext) => + var response = httpContext.Response; + response.StatusCode = 200; + response.ContentType = "text/plain"; + return response.WriteAsync(sb.ToString()); + }); + endpoints.MapGet( + "/plaintext", + (httpContext) => + { + var response = httpContext.Response; + var payloadLength = _plainTextPayload.Length; + response.StatusCode = 200; + response.ContentType = "text/plain"; + response.ContentLength = payloadLength; + return response.Body.WriteAsync(_plainTextPayload, 0, payloadLength); + }); + endpoints.MapGet( + "/graph", + (httpContext) => + { + using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true)) { - using (var writer = new StreamWriter(httpContext.Response.Body, Encoding.UTF8, 1024, leaveOpen: true)) - { - var graphWriter = httpContext.RequestServices.GetRequiredService(); - var dataSource = httpContext.RequestServices.GetRequiredService(); - graphWriter.Write(dataSource, writer); - } + var graphWriter = httpContext.RequestServices.GetRequiredService(); + var dataSource = httpContext.RequestServices.GetRequiredService(); + graphWriter.Write(dataSource, writer); + } - return Task.CompletedTask; - }).WithDisplayName("DFA Graph"); + return Task.CompletedTask; + }).WithDisplayName("DFA Graph"); - endpoints.MapGet("/attributes", HandlerWithAttributes); + endpoints.MapGet("/attributes", HandlerWithAttributes); - endpoints.Map("/getwithattributes", Handler); + endpoints.Map("/getwithattributes", Handler); - endpoints.MapFramework(frameworkBuilder => - { - frameworkBuilder.AddPattern("/transform/{hub:slugify=TestHub}/{method:slugify=TestMethod}"); - frameworkBuilder.AddPattern("/{hub}/{method=TestMethod}"); + endpoints.MapFramework(frameworkBuilder => + { + frameworkBuilder.AddPattern("/transform/{hub:slugify=TestHub}/{method:slugify=TestMethod}"); + frameworkBuilder.AddPattern("/{hub}/{method=TestMethod}"); - frameworkBuilder.AddHubMethod("TestHub", "TestMethod", context => context.Response.WriteAsync("TestMethod!")); - frameworkBuilder.AddHubMethod("Login", "Authenticate", context => context.Response.WriteAsync("Authenticate!")); - frameworkBuilder.AddHubMethod("Login", "Logout", context => context.Response.WriteAsync("Logout!")); - }); + frameworkBuilder.AddHubMethod("TestHub", "TestMethod", context => context.Response.WriteAsync("TestMethod!")); + frameworkBuilder.AddHubMethod("Login", "Authenticate", context => context.Response.WriteAsync("Authenticate!")); + frameworkBuilder.AddHubMethod("Login", "Logout", context => context.Response.WriteAsync("Logout!")); }); + }); - } + } - [Authorize] - private Task HandlerWithAttributes(HttpContext context) - { - return context.Response.WriteAsync("I have ann authorize attribute"); - } + [Authorize] + private Task HandlerWithAttributes(HttpContext context) + { + return context.Response.WriteAsync("I have ann authorize attribute"); + } - [HttpGet] - private Task Handler(HttpContext context) - { - return context.Response.WriteAsync("I have a method metadata attribute"); - } + [HttpGet] + private Task Handler(HttpContext context) + { + return context.Response.WriteAsync("I have a method metadata attribute"); + } - private class AuthorizeAttribute : Attribute - { + private class AuthorizeAttribute : Attribute + { - } + } - private class HttpGetAttribute : Attribute, IHttpMethodMetadata - { - public bool AcceptCorsPreflight => false; + private class HttpGetAttribute : Attribute, IHttpMethodMetadata + { + public bool AcceptCorsPreflight => false; - public IReadOnlyList HttpMethods { get; } = new List { "GET" }; - } + public IReadOnlyList HttpMethods { get; } = new List { "GET" }; } } diff --git a/src/Http/Routing/test/testassets/RoutingSandbox/UseRouterStartup.cs b/src/Http/Routing/test/testassets/RoutingSandbox/UseRouterStartup.cs index a74b7f493e..59d3a577c5 100644 --- a/src/Http/Routing/test/testassets/RoutingSandbox/UseRouterStartup.cs +++ b/src/Http/Routing/test/testassets/RoutingSandbox/UseRouterStartup.cs @@ -9,35 +9,34 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.Extensions.DependencyInjection; -namespace RoutingSandbox +namespace RoutingSandbox; + +public class UseRouterStartup { - public class UseRouterStartup - { - private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10); + private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10); - public void ConfigureServices(IServiceCollection services) - { - services.AddRouting(); - } + public void ConfigureServices(IServiceCollection services) + { + services.AddRouting(); + } - public void Configure(IApplicationBuilder app) + public void Configure(IApplicationBuilder app) + { + app.UseRouter(routes => { - app.UseRouter(routes => + routes.DefaultHandler = new RouteHandler((httpContext) => { - routes.DefaultHandler = new RouteHandler((httpContext) => - { - var request = httpContext.Request; - return httpContext.Response.WriteAsync($"Verb = {request.Method.ToUpperInvariant()} - Path = {request.Path} - Route values - {string.Join(", ", httpContext.GetRouteData().Values)}"); - }); - - routes.MapGet("api/get/{id}", (request, response, routeData) => response.WriteAsync($"API Get {routeData.Values["id"]}")) - .MapMiddlewareRoute("api/middleware", (appBuilder) => appBuilder.Run(httpContext => httpContext.Response.WriteAsync("Middleware!"))) - .MapRoute( - name: "AllVerbs", - template: "api/all/{name}/{lastName?}", - defaults: new { lastName = "Doe" }, - constraints: new { lastName = new RegexRouteConstraint(new Regex("[a-zA-Z]{3}", RegexOptions.CultureInvariant, RegexMatchTimeout)) }); + var request = httpContext.Request; + return httpContext.Response.WriteAsync($"Verb = {request.Method.ToUpperInvariant()} - Path = {request.Path} - Route values - {string.Join(", ", httpContext.GetRouteData().Values)}"); }); - } + + routes.MapGet("api/get/{id}", (request, response, routeData) => response.WriteAsync($"API Get {routeData.Values["id"]}")) + .MapMiddlewareRoute("api/middleware", (appBuilder) => appBuilder.Run(httpContext => httpContext.Response.WriteAsync("Middleware!"))) + .MapRoute( + name: "AllVerbs", + template: "api/all/{name}/{lastName?}", + defaults: new { lastName = "Doe" }, + constraints: new { lastName = new RegexRouteConstraint(new Regex("[a-zA-Z]{3}", RegexOptions.CultureInvariant, RegexMatchTimeout)) }); + }); } } diff --git a/src/Http/Routing/test/testassets/RoutingWebSite/EndsWithStringRouteConstraint.cs b/src/Http/Routing/test/testassets/RoutingWebSite/EndsWithStringRouteConstraint.cs index 0fca7c55eb..7577b7a760 100644 --- a/src/Http/Routing/test/testassets/RoutingWebSite/EndsWithStringRouteConstraint.cs +++ b/src/Http/Routing/test/testassets/RoutingWebSite/EndsWithStringRouteConstraint.cs @@ -6,28 +6,27 @@ using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; -namespace RoutingWebSite +namespace RoutingWebSite; + +internal class EndsWithStringRouteConstraint : IRouteConstraint { - internal class EndsWithStringRouteConstraint : IRouteConstraint + private readonly string _endsWith; + + public EndsWithStringRouteConstraint(string endsWith) { - private readonly string _endsWith; + _endsWith = endsWith; + } - public EndsWithStringRouteConstraint(string endsWith) + public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) + { + var value = values[routeKey]; + if (value == null) { - _endsWith = endsWith; + return false; } - public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) - { - var value = values[routeKey]; - if (value == null) - { - return false; - } - - var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); - var endsWith = valueString.EndsWith(_endsWith, StringComparison.OrdinalIgnoreCase); - return endsWith; - } + var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); + var endsWith = valueString.EndsWith(_endsWith, StringComparison.OrdinalIgnoreCase); + return endsWith; } } diff --git a/src/Http/Routing/test/testassets/RoutingWebSite/HelloExtension/EndpointRouteBuilderExtensions.cs b/src/Http/Routing/test/testassets/RoutingWebSite/HelloExtension/EndpointRouteBuilderExtensions.cs index 7e221fbe08..3751c816a1 100644 --- a/src/Http/Routing/test/testassets/RoutingWebSite/HelloExtension/EndpointRouteBuilderExtensions.cs +++ b/src/Http/Routing/test/testassets/RoutingWebSite/HelloExtension/EndpointRouteBuilderExtensions.cs @@ -9,22 +9,21 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.AspNetCore.Routing.Patterns; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +public static class EndpointRouteBuilderExtensions { - public static class EndpointRouteBuilderExtensions + public static IEndpointConventionBuilder MapHello(this IEndpointRouteBuilder endpoints, string template, string greeter) { - public static IEndpointConventionBuilder MapHello(this IEndpointRouteBuilder endpoints, string template, string greeter) + if (endpoints == null) { - if (endpoints == null) - { - throw new ArgumentNullException(nameof(endpoints)); - } + throw new ArgumentNullException(nameof(endpoints)); + } - var pipeline = endpoints.CreateApplicationBuilder() - .UseHello(greeter) - .Build(); + var pipeline = endpoints.CreateApplicationBuilder() + .UseHello(greeter) + .Build(); - return endpoints.Map(template, pipeline).WithDisplayName("Hello " + greeter); - } + return endpoints.Map(template, pipeline).WithDisplayName("Hello " + greeter); } } diff --git a/src/Http/Routing/test/testassets/RoutingWebSite/HelloExtension/HelloAppBuilderExtensions.cs b/src/Http/Routing/test/testassets/RoutingWebSite/HelloExtension/HelloAppBuilderExtensions.cs index 6334ad18f0..a72705d7a4 100644 --- a/src/Http/Routing/test/testassets/RoutingWebSite/HelloExtension/HelloAppBuilderExtensions.cs +++ b/src/Http/Routing/test/testassets/RoutingWebSite/HelloExtension/HelloAppBuilderExtensions.cs @@ -5,21 +5,20 @@ using System; using Microsoft.Extensions.Options; using RoutingWebSite.HelloExtension; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.Builder; + +public static class HelloAppBuilderExtensions { - public static class HelloAppBuilderExtensions + public static IApplicationBuilder UseHello(this IApplicationBuilder app, string greeter) { - public static IApplicationBuilder UseHello(this IApplicationBuilder app, string greeter) + if (app == null) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - return app.UseMiddleware(Options.Create(new HelloOptions - { - Greeter = greeter - })); + throw new ArgumentNullException(nameof(app)); } + + return app.UseMiddleware(Options.Create(new HelloOptions + { + Greeter = greeter + })); } } diff --git a/src/Http/Routing/test/testassets/RoutingWebSite/HelloExtension/HelloMiddleware.cs b/src/Http/Routing/test/testassets/RoutingWebSite/HelloExtension/HelloMiddleware.cs index 2494ae7ea9..901f43ff3d 100644 --- a/src/Http/Routing/test/testassets/RoutingWebSite/HelloExtension/HelloMiddleware.cs +++ b/src/Http/Routing/test/testassets/RoutingWebSite/HelloExtension/HelloMiddleware.cs @@ -9,37 +9,36 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; -namespace RoutingWebSite.HelloExtension +namespace RoutingWebSite.HelloExtension; + +public class HelloMiddleware { - public class HelloMiddleware + private readonly RequestDelegate _next; + private readonly HelloOptions _helloOptions; + private readonly byte[] _helloPayload; + + public HelloMiddleware(RequestDelegate next, IOptions helloOptions) { - private readonly RequestDelegate _next; - private readonly HelloOptions _helloOptions; - private readonly byte[] _helloPayload; + _next = next; + _helloOptions = helloOptions.Value; - public HelloMiddleware(RequestDelegate next, IOptions helloOptions) + var payload = new List(); + payload.AddRange(Encoding.UTF8.GetBytes("Hello")); + if (!string.IsNullOrEmpty(_helloOptions.Greeter)) { - _next = next; - _helloOptions = helloOptions.Value; - - var payload = new List(); - payload.AddRange(Encoding.UTF8.GetBytes("Hello")); - if (!string.IsNullOrEmpty(_helloOptions.Greeter)) - { - payload.Add((byte)' '); - payload.AddRange(Encoding.UTF8.GetBytes(_helloOptions.Greeter)); - } - _helloPayload = payload.ToArray(); + payload.Add((byte)' '); + payload.AddRange(Encoding.UTF8.GetBytes(_helloOptions.Greeter)); } + _helloPayload = payload.ToArray(); + } - public Task InvokeAsync(HttpContext context) - { - var response = context.Response; - var payloadLength = _helloPayload.Length; - response.StatusCode = 200; - response.ContentType = "text/plain"; - response.ContentLength = payloadLength; - return response.Body.WriteAsync(_helloPayload, 0, payloadLength); - } + public Task InvokeAsync(HttpContext context) + { + var response = context.Response; + var payloadLength = _helloPayload.Length; + response.StatusCode = 200; + response.ContentType = "text/plain"; + response.ContentLength = payloadLength; + return response.Body.WriteAsync(_helloPayload, 0, payloadLength); } } diff --git a/src/Http/Routing/test/testassets/RoutingWebSite/HelloExtension/HelloOptions.cs b/src/Http/Routing/test/testassets/RoutingWebSite/HelloExtension/HelloOptions.cs index 7a216ddb36..d66b682572 100644 --- a/src/Http/Routing/test/testassets/RoutingWebSite/HelloExtension/HelloOptions.cs +++ b/src/Http/Routing/test/testassets/RoutingWebSite/HelloExtension/HelloOptions.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace RoutingWebSite.HelloExtension +namespace RoutingWebSite.HelloExtension; + +public class HelloOptions { - public class HelloOptions - { - public string Greeter { get; set; } - } + public string Greeter { get; set; } } diff --git a/src/Http/Routing/test/testassets/RoutingWebSite/MapFallbackStartup.cs b/src/Http/Routing/test/testassets/RoutingWebSite/MapFallbackStartup.cs index e3d02e5567..ca381179e9 100644 --- a/src/Http/Routing/test/testassets/RoutingWebSite/MapFallbackStartup.cs +++ b/src/Http/Routing/test/testassets/RoutingWebSite/MapFallbackStartup.cs @@ -5,32 +5,31 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -namespace RoutingWebSite +namespace RoutingWebSite; + +public class MapFallbackStartup { - public class MapFallbackStartup + public void ConfigureServices(IServiceCollection services) { - public void ConfigureServices(IServiceCollection services) - { - services.AddRouting(); - } + services.AddRouting(); + } - public void Configure(IApplicationBuilder app) + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + app.UseEndpoints(endpoints => { - app.UseRouting(); - app.UseEndpoints(endpoints => + endpoints.MapFallback("/prefix/{*path:nonfile}", (context) => { - endpoints.MapFallback("/prefix/{*path:nonfile}", (context) => - { - return context.Response.WriteAsync("FallbackCustomPattern"); - }); - - endpoints.MapFallback((context) => - { - return context.Response.WriteAsync("FallbackDefaultPattern"); - }); + return context.Response.WriteAsync("FallbackCustomPattern"); + }); - endpoints.MapHello("/helloworld", "World"); + endpoints.MapFallback((context) => + { + return context.Response.WriteAsync("FallbackDefaultPattern"); }); - } + + endpoints.MapHello("/helloworld", "World"); + }); } } diff --git a/src/Http/Routing/test/testassets/RoutingWebSite/Program.cs b/src/Http/Routing/test/testassets/RoutingWebSite/Program.cs index 594a6384c3..697d24735f 100644 --- a/src/Http/Routing/test/testassets/RoutingWebSite/Program.cs +++ b/src/Http/Routing/test/testassets/RoutingWebSite/Program.cs @@ -9,72 +9,71 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace RoutingWebSite +namespace RoutingWebSite; + +public class Program { - public class Program + public const string EndpointRoutingScenario = "endpointrouting"; + public const string RouterScenario = "router"; + + public static Task Main(string[] args) { - public const string EndpointRoutingScenario = "endpointrouting"; - public const string RouterScenario = "router"; + var host = GetHostBuilder(args).Build(); + return host.RunAsync(); + } - public static Task Main(string[] args) + // For unit testing + public static IHostBuilder GetHostBuilder(string[] args) + { + string scenario; + if (args.Length == 0) { - var host = GetHostBuilder(args).Build(); - return host.RunAsync(); - } + Console.WriteLine("Choose a sample to run:"); + Console.WriteLine($"1. {EndpointRoutingScenario}"); + Console.WriteLine($"2. {RouterScenario}"); + Console.WriteLine(); - // For unit testing - public static IHostBuilder GetHostBuilder(string[] args) + scenario = Console.ReadLine(); + } + else { - string scenario; - if (args.Length == 0) - { - Console.WriteLine("Choose a sample to run:"); - Console.WriteLine($"1. {EndpointRoutingScenario}"); - Console.WriteLine($"2. {RouterScenario}"); - Console.WriteLine(); - - scenario = Console.ReadLine(); - } - else - { - scenario = args[0]; - } - - Type startupType; - switch (scenario) - { - case "1": - case EndpointRoutingScenario: - startupType = typeof(UseEndpointRoutingStartup); - break; + scenario = args[0]; + } - case "2": - case RouterScenario: - startupType = typeof(UseRouterStartup); - break; + Type startupType; + switch (scenario) + { + case "1": + case EndpointRoutingScenario: + startupType = typeof(UseEndpointRoutingStartup); + break; - default: - Console.WriteLine($"unknown scenario {scenario}"); - Console.WriteLine($"usage: dotnet run -- ({EndpointRoutingScenario}|{RouterScenario})"); - throw new InvalidOperationException(); + case "2": + case RouterScenario: + startupType = typeof(UseRouterStartup); + break; - } + default: + Console.WriteLine($"unknown scenario {scenario}"); + Console.WriteLine($"usage: dotnet run -- ({EndpointRoutingScenario}|{RouterScenario})"); + throw new InvalidOperationException(); - return new HostBuilder() - .ConfigureWebHost(webHostBuilder => - { - webHostBuilder - .UseKestrel() - .UseIISIntegration() - .UseContentRoot(Environment.CurrentDirectory) - .UseStartup(startupType) - .UseTestServer(); - }) - .ConfigureLogging(b => - { - b.AddConsole(); - b.SetMinimumLevel(LogLevel.Critical); - }); } + + return new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseKestrel() + .UseIISIntegration() + .UseContentRoot(Environment.CurrentDirectory) + .UseStartup(startupType) + .UseTestServer(); + }) + .ConfigureLogging(b => + { + b.AddConsole(); + b.SetMinimumLevel(LogLevel.Critical); + }); } } diff --git a/src/Http/Routing/test/testassets/RoutingWebSite/UseEndpointRoutingStartup.cs b/src/Http/Routing/test/testassets/RoutingWebSite/UseEndpointRoutingStartup.cs index 67a9f27f32..62744ce1e0 100644 --- a/src/Http/Routing/test/testassets/RoutingWebSite/UseEndpointRoutingStartup.cs +++ b/src/Http/Routing/test/testassets/RoutingWebSite/UseEndpointRoutingStartup.cs @@ -16,163 +16,162 @@ using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -namespace RoutingWebSite +namespace RoutingWebSite; + +public class UseEndpointRoutingStartup { - public class UseEndpointRoutingStartup + private static readonly byte[] _plainTextPayload = Encoding.UTF8.GetBytes("Plain text!"); + + public void ConfigureServices(IServiceCollection services) { - private static readonly byte[] _plainTextPayload = Encoding.UTF8.GetBytes("Plain text!"); + services.AddTransient(); - public void ConfigureServices(IServiceCollection services) + services.AddRouting(options => { - services.AddTransient(); + options.ConstraintMap.Add("endsWith", typeof(EndsWithStringRouteConstraint)); + }); + } - services.AddRouting(options => - { - options.ConstraintMap.Add("endsWith", typeof(EndsWithStringRouteConstraint)); - }); - } + public void Configure(IApplicationBuilder app) + { + app.UseStaticFiles(); - public void Configure(IApplicationBuilder app) - { - app.UseStaticFiles(); + app.UseRouting(); - app.UseRouting(); + app.Map("/Branch1", branch => SetupBranch(branch, "Branch1")); + app.Map("/Branch2", branch => SetupBranch(branch, "Branch2")); - app.Map("/Branch1", branch => SetupBranch(branch, "Branch1")); - app.Map("/Branch2", branch => SetupBranch(branch, "Branch2")); + // Imagine some more stuff here... - // Imagine some more stuff here... + app.UseEndpoints(endpoints => + { + endpoints.MapHello("/helloworld", "World"); - app.UseEndpoints(endpoints => - { - endpoints.MapHello("/helloworld", "World"); + endpoints.MapGet( + "/", + (httpContext) => + { + var dataSource = httpContext.RequestServices.GetRequiredService(); - endpoints.MapGet( - "/", - (httpContext) => - { - var dataSource = httpContext.RequestServices.GetRequiredService(); - - var sb = new StringBuilder(); - sb.AppendLine("Endpoints:"); - foreach (var endpoint in dataSource.Endpoints.OfType().OrderBy(e => e.RoutePattern.RawText, StringComparer.OrdinalIgnoreCase)) - { - sb.AppendLine(FormattableString.Invariant($"- {endpoint.RoutePattern.RawText}")); - } - - var response = httpContext.Response; - response.StatusCode = 200; - response.ContentType = "text/plain"; - return response.WriteAsync(sb.ToString()); - }); - endpoints.MapGet( - "/plaintext", - (httpContext) => - { - var response = httpContext.Response; - var payloadLength = _plainTextPayload.Length; - response.StatusCode = 200; - response.ContentType = "text/plain"; - response.ContentLength = payloadLength; - return response.Body.WriteAsync(_plainTextPayload, 0, payloadLength); - }); - endpoints.MapGet( - "/convention", - (httpContext) => - { - var endpoint = httpContext.GetEndpoint(); - return httpContext.Response.WriteAsync((endpoint.Metadata.GetMetadata() != null) ? "Has metadata" : "No metadata"); - }).Add(b => - { - b.Metadata.Add(new CustomMetadata()); - }); - endpoints.MapGet( - "/withconstraints/{id:endsWith(_001)}", - (httpContext) => - { - var response = httpContext.Response; - response.StatusCode = 200; - response.ContentType = "text/plain"; - return response.WriteAsync("WithConstraints"); - }); - endpoints.MapGet( - "/withoptionalconstraints/{id:endsWith(_001)?}", - (httpContext) => - { - var response = httpContext.Response; - response.StatusCode = 200; - response.ContentType = "text/plain"; - return response.WriteAsync("withoptionalconstraints"); - }); - endpoints.MapGet( - "/WithSingleAsteriskCatchAll/{*path}", - (httpContext) => + var sb = new StringBuilder(); + sb.AppendLine("Endpoints:"); + foreach (var endpoint in dataSource.Endpoints.OfType().OrderBy(e => e.RoutePattern.RawText, StringComparer.OrdinalIgnoreCase)) { - var linkGenerator = httpContext.RequestServices.GetRequiredService(); - - var response = httpContext.Response; - response.StatusCode = 200; - response.ContentType = "text/plain"; - return response.WriteAsync( - "Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithSingleAsteriskCatchAll", new { })); - }).WithMetadata(new RouteNameMetadata(routeName: "WithSingleAsteriskCatchAll")); - endpoints.MapGet( - "/WithDoubleAsteriskCatchAll/{**path}", - (httpContext) => - { - var linkGenerator = httpContext.RequestServices.GetRequiredService(); - - var response = httpContext.Response; - response.StatusCode = 200; - response.ContentType = "text/plain"; - return response.WriteAsync( - "Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithDoubleAsteriskCatchAll", new { })); - }).WithMetadata(new RouteNameMetadata(routeName: "WithDoubleAsteriskCatchAll")); - - MapHostEndpoint(endpoints); - MapHostEndpoint(endpoints, "*.0.0.1"); - MapHostEndpoint(endpoints, "127.0.0.1"); - MapHostEndpoint(endpoints, "*.0.0.1:5000", "*.0.0.1:5001"); - MapHostEndpoint(endpoints, "contoso.com:*", "*.contoso.com:*"); - }); - } - - private class CustomMetadata - { - } + sb.AppendLine(FormattableString.Invariant($"- {endpoint.RoutePattern.RawText}")); + } - private IEndpointConventionBuilder MapHostEndpoint(IEndpointRouteBuilder endpoints, params string[] hosts) - { - var hostsDisplay = (hosts == null || hosts.Length == 0) - ? "*:*" - : string.Join(",", hosts.Select(h => h.Contains(':') ? h : h + ":*")); - - var conventionBuilder = endpoints.MapGet( - "api/DomainWildcard", - httpContext => + var response = httpContext.Response; + response.StatusCode = 200; + response.ContentType = "text/plain"; + return response.WriteAsync(sb.ToString()); + }); + endpoints.MapGet( + "/plaintext", + (httpContext) => { var response = httpContext.Response; + var payloadLength = _plainTextPayload.Length; response.StatusCode = 200; response.ContentType = "text/plain"; - return response.WriteAsync(hostsDisplay); + response.ContentLength = payloadLength; + return response.Body.WriteAsync(_plainTextPayload, 0, payloadLength); }); + endpoints.MapGet( + "/convention", + (httpContext) => + { + var endpoint = httpContext.GetEndpoint(); + return httpContext.Response.WriteAsync((endpoint.Metadata.GetMetadata() != null) ? "Has metadata" : "No metadata"); + }).Add(b => + { + b.Metadata.Add(new CustomMetadata()); + }); + endpoints.MapGet( + "/withconstraints/{id:endsWith(_001)}", + (httpContext) => + { + var response = httpContext.Response; + response.StatusCode = 200; + response.ContentType = "text/plain"; + return response.WriteAsync("WithConstraints"); + }); + endpoints.MapGet( + "/withoptionalconstraints/{id:endsWith(_001)?}", + (httpContext) => + { + var response = httpContext.Response; + response.StatusCode = 200; + response.ContentType = "text/plain"; + return response.WriteAsync("withoptionalconstraints"); + }); + endpoints.MapGet( + "/WithSingleAsteriskCatchAll/{*path}", + (httpContext) => + { + var linkGenerator = httpContext.RequestServices.GetRequiredService(); + + var response = httpContext.Response; + response.StatusCode = 200; + response.ContentType = "text/plain"; + return response.WriteAsync( + "Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithSingleAsteriskCatchAll", new { })); + }).WithMetadata(new RouteNameMetadata(routeName: "WithSingleAsteriskCatchAll")); + endpoints.MapGet( + "/WithDoubleAsteriskCatchAll/{**path}", + (httpContext) => + { + var linkGenerator = httpContext.RequestServices.GetRequiredService(); + + var response = httpContext.Response; + response.StatusCode = 200; + response.ContentType = "text/plain"; + return response.WriteAsync( + "Link: " + linkGenerator.GetPathByRouteValues(httpContext, "WithDoubleAsteriskCatchAll", new { })); + }).WithMetadata(new RouteNameMetadata(routeName: "WithDoubleAsteriskCatchAll")); + + MapHostEndpoint(endpoints); + MapHostEndpoint(endpoints, "*.0.0.1"); + MapHostEndpoint(endpoints, "127.0.0.1"); + MapHostEndpoint(endpoints, "*.0.0.1:5000", "*.0.0.1:5001"); + MapHostEndpoint(endpoints, "contoso.com:*", "*.contoso.com:*"); + }); + } - conventionBuilder.Add(endpointBuilder => + private class CustomMetadata + { + } + + private IEndpointConventionBuilder MapHostEndpoint(IEndpointRouteBuilder endpoints, params string[] hosts) + { + var hostsDisplay = (hosts == null || hosts.Length == 0) + ? "*:*" + : string.Join(",", hosts.Select(h => h.Contains(':') ? h : h + ":*")); + + var conventionBuilder = endpoints.MapGet( + "api/DomainWildcard", + httpContext => { - endpointBuilder.Metadata.Add(new HostAttribute(hosts)); - endpointBuilder.DisplayName += " HOST: " + hostsDisplay; + var response = httpContext.Response; + response.StatusCode = 200; + response.ContentType = "text/plain"; + return response.WriteAsync(hostsDisplay); }); - return conventionBuilder; - } + conventionBuilder.Add(endpointBuilder => + { + endpointBuilder.Metadata.Add(new HostAttribute(hosts)); + endpointBuilder.DisplayName += " HOST: " + hostsDisplay; + }); + + return conventionBuilder; + } - private void SetupBranch(IApplicationBuilder app, string name) + private void SetupBranch(IApplicationBuilder app, string name) + { + app.UseRouting(); + app.UseEndpoints(endpoints => { - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapGet("api/get/{id}", (context) => context.Response.WriteAsync($"{name} - API Get {context.Request.RouteValues["id"]}")); - }); - } + endpoints.MapGet("api/get/{id}", (context) => context.Response.WriteAsync($"{name} - API Get {context.Request.RouteValues["id"]}")); + }); } } diff --git a/src/Http/Routing/test/testassets/RoutingWebSite/UseRouterStartup.cs b/src/Http/Routing/test/testassets/RoutingWebSite/UseRouterStartup.cs index 82b0236fd2..edc223a152 100644 --- a/src/Http/Routing/test/testassets/RoutingWebSite/UseRouterStartup.cs +++ b/src/Http/Routing/test/testassets/RoutingWebSite/UseRouterStartup.cs @@ -9,46 +9,45 @@ using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Constraints; using Microsoft.Extensions.DependencyInjection; -namespace RoutingWebSite +namespace RoutingWebSite; + +public class UseRouterStartup { - public class UseRouterStartup - { - private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10); + private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromSeconds(10); - public void ConfigureServices(IServiceCollection services) - { - services.AddRouting(); - } + public void ConfigureServices(IServiceCollection services) + { + services.AddRouting(); + } - public void Configure(IApplicationBuilder app) + public void Configure(IApplicationBuilder app) + { + app.UseRouter(routes => { - app.UseRouter(routes => + routes.DefaultHandler = new RouteHandler((httpContext) => { - routes.DefaultHandler = new RouteHandler((httpContext) => - { - var request = httpContext.Request; - return httpContext.Response.WriteAsync($"Verb = {request.Method.ToUpperInvariant()} - Path = {request.Path} - Route values - {string.Join(", ", httpContext.GetRouteData().Values)}"); - }); - - routes.MapGet("api/get/{id}", (request, response, routeData) => response.WriteAsync($"API Get {routeData.Values["id"]}")) - .MapMiddlewareRoute("api/middleware", (appBuilder) => appBuilder.Run(httpContext => httpContext.Response.WriteAsync("Middleware!"))) - .MapRoute( - name: "AllVerbs", - template: "api/all/{name}/{lastName?}", - defaults: new { lastName = "Doe" }, - constraints: new { lastName = new RegexRouteConstraint(new Regex("[a-zA-Z]{3}", RegexOptions.CultureInvariant, RegexMatchTimeout)) }); + var request = httpContext.Request; + return httpContext.Response.WriteAsync($"Verb = {request.Method.ToUpperInvariant()} - Path = {request.Path} - Route values - {string.Join(", ", httpContext.GetRouteData().Values)}"); }); - app.Map("/Branch1", branch => SetupBranch(branch, "Branch1")); - app.Map("/Branch2", branch => SetupBranch(branch, "Branch2")); - } + routes.MapGet("api/get/{id}", (request, response, routeData) => response.WriteAsync($"API Get {routeData.Values["id"]}")) + .MapMiddlewareRoute("api/middleware", (appBuilder) => appBuilder.Run(httpContext => httpContext.Response.WriteAsync("Middleware!"))) + .MapRoute( + name: "AllVerbs", + template: "api/all/{name}/{lastName?}", + defaults: new { lastName = "Doe" }, + constraints: new { lastName = new RegexRouteConstraint(new Regex("[a-zA-Z]{3}", RegexOptions.CultureInvariant, RegexMatchTimeout)) }); + }); - private void SetupBranch(IApplicationBuilder app, string name) + app.Map("/Branch1", branch => SetupBranch(branch, "Branch1")); + app.Map("/Branch2", branch => SetupBranch(branch, "Branch2")); + } + + private void SetupBranch(IApplicationBuilder app, string name) + { + app.UseRouter(routes => { - app.UseRouter(routes => - { - routes.MapGet("api/get/{id}", (request, response, routeData) => response.WriteAsync($"{name} - API Get {routeData.Values["id"]}")); - }); - } + routes.MapGet("api/get/{id}", (request, response, routeData) => response.WriteAsync($"{name} - API Get {routeData.Values["id"]}")); + }); } } diff --git a/src/Http/Routing/tools/Swaggatherer/Program.cs b/src/Http/Routing/tools/Swaggatherer/Program.cs index b0eb68aa63..6dc96fad82 100644 --- a/src/Http/Routing/tools/Swaggatherer/Program.cs +++ b/src/Http/Routing/tools/Swaggatherer/Program.cs @@ -1,14 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Swaggatherer +namespace Swaggatherer; + +internal static class Program { - internal static class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - var application = new SwaggathererApplication(); - application.Execute(args); - } + var application = new SwaggathererApplication(); + application.Execute(args); } } diff --git a/src/Http/Routing/tools/Swaggatherer/RouteEntry.cs b/src/Http/Routing/tools/Swaggatherer/RouteEntry.cs index 0a0a2110d7..fc954d364e 100644 --- a/src/Http/Routing/tools/Swaggatherer/RouteEntry.cs +++ b/src/Http/Routing/tools/Swaggatherer/RouteEntry.cs @@ -3,13 +3,12 @@ using Microsoft.AspNetCore.Routing.Template; -namespace Swaggatherer +namespace Swaggatherer; + +internal class RouteEntry { - internal class RouteEntry - { - public RouteTemplate Template { get; set; } - public string Method { get; set; } - public decimal Precedence { get; set; } - public string RequestUrl { get; set; } - } + public RouteTemplate Template { get; set; } + public string Method { get; set; } + public decimal Precedence { get; set; } + public string RequestUrl { get; set; } } diff --git a/src/Http/Routing/tools/Swaggatherer/SwaggathererApplication.cs b/src/Http/Routing/tools/Swaggatherer/SwaggathererApplication.cs index e15e7b0bda..0b49bb15ad 100644 --- a/src/Http/Routing/tools/Swaggatherer/SwaggathererApplication.cs +++ b/src/Http/Routing/tools/Swaggatherer/SwaggathererApplication.cs @@ -10,252 +10,251 @@ using Microsoft.Extensions.CommandLineUtils; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace Swaggatherer +namespace Swaggatherer; + +internal class SwaggathererApplication : CommandLineApplication { - internal class SwaggathererApplication : CommandLineApplication + public SwaggathererApplication() { - public SwaggathererApplication() - { - Invoke = InvokeCore; + Invoke = InvokeCore; + + HttpMethods = Option("-m|--method", "allow multiple endpoints with different http method", CommandOptionType.NoValue); + Input = Option("-i", "input swagger 2.0 JSON file", CommandOptionType.MultipleValue); + InputDirectory = Option("-d", "input directory", CommandOptionType.SingleValue); + Output = Option("-o", "output", CommandOptionType.SingleValue); + + HelpOption("-h|--help"); + } + + public CommandOption Input { get; } - HttpMethods = Option("-m|--method", "allow multiple endpoints with different http method", CommandOptionType.NoValue); - Input = Option("-i", "input swagger 2.0 JSON file", CommandOptionType.MultipleValue); - InputDirectory = Option("-d", "input directory", CommandOptionType.SingleValue); - Output = Option("-o", "output", CommandOptionType.SingleValue); + public CommandOption InputDirectory { get; } - HelpOption("-h|--help"); + // Support multiple endpoints that are distinguished only by http method. + public CommandOption HttpMethods { get; } + + public CommandOption Output { get; } + + private int InvokeCore() + { + if (!Input.HasValue() && !InputDirectory.HasValue()) + { + ShowHelp(); + return 1; } - public CommandOption Input { get; } + if (Input.HasValue() && InputDirectory.HasValue()) + { + ShowHelp(); + return 1; + } - public CommandOption InputDirectory { get; } + if (!Output.HasValue()) + { + Output.Values.Add("Out.generated.cs"); + } - // Support multiple endpoints that are distinguished only by http method. - public CommandOption HttpMethods { get; } + if (InputDirectory.HasValue()) + { + Input.Values.AddRange(Directory.EnumerateFiles(InputDirectory.Value(), "*.json", SearchOption.AllDirectories)); + } - public CommandOption Output { get; } + Console.WriteLine($"Processing {Input.Values.Count} files..."); + var entries = new List(); + for (var i = 0; i < Input.Values.Count; i++) + { + var input = ReadInput(Input.Values[i]); + ParseEntries(input, entries); + } - private int InvokeCore() + // We don't yet want to support complex segments. + for (var i = entries.Count - 1; i >= 0; i--) { - if (!Input.HasValue() && !InputDirectory.HasValue()) + if (HasComplexSegment(entries[i])) { - ShowHelp(); - return 1; + Out.WriteLine("Skipping route with complex segment: " + entries[i].Template.TemplateText); + entries.RemoveAt(i); } + } - if (Input.HasValue() && InputDirectory.HasValue()) - { - ShowHelp(); - return 1; - } + // The data that we're provided by might be unambiguous. + // Remove any routes that would be ambiguous in our system. + var routesByPrecedence = new Dictionary>(); + for (var i = entries.Count - 1; i >= 0; i--) + { + var entry = entries[i]; + var precedence = RoutePrecedence.ComputeInbound(entries[i].Template); - if (!Output.HasValue()) + if (!routesByPrecedence.TryGetValue(precedence, out var matches)) { - Output.Values.Add("Out.generated.cs"); + matches = new List(); + routesByPrecedence.Add(precedence, matches); } - if (InputDirectory.HasValue()) + if (IsDuplicateTemplate(entry, matches)) { - Input.Values.AddRange(Directory.EnumerateFiles(InputDirectory.Value(), "*.json", SearchOption.AllDirectories)); + Out.WriteLine("Duplicate route template: " + entries[i].Template.TemplateText); + entries.RemoveAt(i); + continue; } - Console.WriteLine($"Processing {Input.Values.Count} files..."); - var entries = new List(); - for (var i = 0; i < Input.Values.Count; i++) - { - var input = ReadInput(Input.Values[i]); - ParseEntries(input, entries); - } + matches.Add(entry); + } - // We don't yet want to support complex segments. - for (var i = entries.Count - 1; i >= 0; i--) + // We're not too sophisticated with how we generate parameter values, just hoping for + // the best. For parameters we generate a segment that is the same length as the parameter name + // but with a minimum of 5 characters to avoid collisions. + for (var i = entries.Count - 1; i >= 0; i--) + { + entries[i].RequestUrl = GenerateRequestUrl(entries[i].Template); + if (entries[i].RequestUrl == null) { - if (HasComplexSegment(entries[i])) - { - Out.WriteLine("Skipping route with complex segment: " + entries[i].Template.TemplateText); - entries.RemoveAt(i); - } + Out.WriteLine("Failed to create a request for: " + entries[i].Template.TemplateText); + entries.RemoveAt(i); + continue; } + } - // The data that we're provided by might be unambiguous. - // Remove any routes that would be ambiguous in our system. - var routesByPrecedence = new Dictionary>(); - for (var i = entries.Count - 1; i >= 0; i--) - { - var entry = entries[i]; - var precedence = RoutePrecedence.ComputeInbound(entries[i].Template); - - if (!routesByPrecedence.TryGetValue(precedence, out var matches)) - { - matches = new List(); - routesByPrecedence.Add(precedence, matches); - } + Sort(entries); - if (IsDuplicateTemplate(entry, matches)) - { - Out.WriteLine("Duplicate route template: " + entries[i].Template.TemplateText); - entries.RemoveAt(i); - continue; - } + var text = Template.Execute(entries); + File.WriteAllText(Output.Value(), text); + return 0; + } - matches.Add(entry); + private JObject ReadInput(string input) + { + using (var reader = File.OpenText(input)) + { + try + { + return JObject.Load(new JsonTextReader(reader)); } - - // We're not too sophisticated with how we generate parameter values, just hoping for - // the best. For parameters we generate a segment that is the same length as the parameter name - // but with a minimum of 5 characters to avoid collisions. - for (var i = entries.Count - 1; i >= 0; i--) + catch (JsonReaderException ex) { - entries[i].RequestUrl = GenerateRequestUrl(entries[i].Template); - if (entries[i].RequestUrl == null) - { - Out.WriteLine("Failed to create a request for: " + entries[i].Template.TemplateText); - entries.RemoveAt(i); - continue; - } + Out.WriteLine($"Error reading: {input}"); + Out.WriteLine(ex); + return new JObject(); } - - Sort(entries); - - var text = Template.Execute(entries); - File.WriteAllText(Output.Value(), text); - return 0; } + } - private JObject ReadInput(string input) + private void ParseEntries(JObject input, List entries) + { + var basePath = ""; + if (input["basePath"] is JProperty basePathProperty) { - using (var reader = File.OpenText(input)) - { - try - { - return JObject.Load(new JsonTextReader(reader)); - } - catch (JsonReaderException ex) - { - Out.WriteLine($"Error reading: {input}"); - Out.WriteLine(ex); - return new JObject(); - } - } + basePath = basePathProperty.Value(); } - private void ParseEntries(JObject input, List entries) + if (input["paths"] is JObject paths) { - var basePath = ""; - if (input["basePath"] is JProperty basePathProperty) + foreach (var path in paths.Properties()) { - basePath = basePathProperty.Value(); - } - - if (input["paths"] is JObject paths) - { - foreach (var path in paths.Properties()) + foreach (var method in ((JObject)path.Value).Properties()) { - foreach (var method in ((JObject)path.Value).Properties()) + var template = basePath + path.Name; + var parsed = TemplateParser.Parse(template); + entries.Add(new RouteEntry() { - var template = basePath + path.Name; - var parsed = TemplateParser.Parse(template); - entries.Add(new RouteEntry() - { - Method = HttpMethods.HasValue() ? method.Name.ToString() : null, - Template = parsed, - Precedence = RoutePrecedence.ComputeInbound(parsed), - }); - } + Method = HttpMethods.HasValue() ? method.Name.ToString() : null, + Template = parsed, + Precedence = RoutePrecedence.ComputeInbound(parsed), + }); } } } + } - private bool HasComplexSegment(RouteEntry entry) + private bool HasComplexSegment(RouteEntry entry) + { + for (var i = 0; i < entry.Template.Segments.Count; i++) { - for (var i = 0; i < entry.Template.Segments.Count; i++) + if (!entry.Template.Segments[i].IsSimple) { - if (!entry.Template.Segments[i].IsSimple) - { - return true; - } + return true; } - - return false; } - private bool IsDuplicateTemplate(RouteEntry entry, List others) + return false; + } + + private bool IsDuplicateTemplate(RouteEntry entry, List others) + { + for (var j = 0; j < others.Count; j++) { - for (var j = 0; j < others.Count; j++) - { - // This is another route with the same precedence. It is guaranteed to have the same number of segments - // of the same kinds and in the same order. We just need to check the literals. - var other = others[j]; + // This is another route with the same precedence. It is guaranteed to have the same number of segments + // of the same kinds and in the same order. We just need to check the literals. + var other = others[j]; - var isSame = true; - for (var k = 0; k < entry.Template.Segments.Count; k++) + var isSame = true; + for (var k = 0; k < entry.Template.Segments.Count; k++) + { + if (!string.Equals( + entry.Template.Segments[k].Parts[0].Text, + other.Template.Segments[k].Parts[0].Text, + StringComparison.OrdinalIgnoreCase)) { - if (!string.Equals( - entry.Template.Segments[k].Parts[0].Text, - other.Template.Segments[k].Parts[0].Text, - StringComparison.OrdinalIgnoreCase)) - { - isSame = false; - break; - } - - if (HttpMethods.HasValue() && - !string.Equals(entry.Method, other.Method, StringComparison.OrdinalIgnoreCase)) - { - isSame = false; - break; - } + isSame = false; + break; } - if (isSame) + if (HttpMethods.HasValue() && + !string.Equals(entry.Method, other.Method, StringComparison.OrdinalIgnoreCase)) { - return true; + isSame = false; + break; } } - return false; - } - - private static void Sort(List entries) - { - // We need to sort these in precedence order for the linear matchers. - entries.Sort((x, y) => + if (isSame) { - var comparison = RoutePrecedence.ComputeInbound(x.Template).CompareTo(RoutePrecedence.ComputeInbound(y.Template)); - if (comparison != 0) - { - return comparison; - } - - return string.Compare(x.Template.TemplateText, y.Template.TemplateText, StringComparison.Ordinal); - }); + return true; + } } - private static string GenerateRequestUrl(RouteTemplate template) + return false; + } + + private static void Sort(List entries) + { + // We need to sort these in precedence order for the linear matchers. + entries.Sort((x, y) => { - if (template.Segments.Count == 0) + var comparison = RoutePrecedence.ComputeInbound(x.Template).CompareTo(RoutePrecedence.ComputeInbound(y.Template)); + if (comparison != 0) { - return "/"; + return comparison; } - var url = new StringBuilder(); - for (var i = 0; i < template.Segments.Count; i++) - { - // We don't yet handle complex segments - var part = template.Segments[i].Parts[0]; - - url.Append('/'); - url.Append(part.IsLiteral ? part.Text : GenerateParameterValue(part)); - } + return string.Compare(x.Template.TemplateText, y.Template.TemplateText, StringComparison.Ordinal); + }); + } - return url.ToString(); + private static string GenerateRequestUrl(RouteTemplate template) + { + if (template.Segments.Count == 0) + { + return "/"; } - private static string GenerateParameterValue(TemplatePart part) + var url = new StringBuilder(); + for (var i = 0; i < template.Segments.Count; i++) { - var text = Guid.NewGuid().ToString(); - var length = Math.Min(text.Length, Math.Max(5, part.Name.Length)); - return text.Substring(0, length); + // We don't yet handle complex segments + var part = template.Segments[i].Parts[0]; + + url.Append('/'); + url.Append(part.IsLiteral ? part.Text : GenerateParameterValue(part)); } + + return url.ToString(); + } + + private static string GenerateParameterValue(TemplatePart part) + { + var text = Guid.NewGuid().ToString(); + var length = Math.Min(text.Length, Math.Max(5, part.Name.Length)); + return text.Substring(0, length); } } diff --git a/src/Http/Routing/tools/Swaggatherer/Template.cs b/src/Http/Routing/tools/Swaggatherer/Template.cs index 8f2ba4c198..ef1ee847d7 100644 --- a/src/Http/Routing/tools/Swaggatherer/Template.cs +++ b/src/Http/Routing/tools/Swaggatherer/Template.cs @@ -5,66 +5,66 @@ using System; using System.Collections.Generic; using System.Globalization; -namespace Swaggatherer +namespace Swaggatherer; + +internal static class Template { - internal static class Template + public static string Execute(IReadOnlyList entries) { - public static string Execute(IReadOnlyList entries) + var controllerCount = 0; + var templatesVisited = new Dictionary( + StringComparer.OrdinalIgnoreCase); + + var setupEndpointsLines = new List(); + for (var i = 0; i < entries.Count; i++) { - var controllerCount = 0; - var templatesVisited = new Dictionary( - StringComparer.OrdinalIgnoreCase); + var entry = entries[i]; - var setupEndpointsLines = new List(); - for (var i = 0; i < entries.Count; i++) + // In attribute routing, same template is used for all actions within that controller. The following + // simulates that where we only increment the controller count when a new endpoint for a new template + // is being created. + var template = entry.Template.TemplateText; + if (!templatesVisited.TryGetValue(template, out var visitedTemplateInfo)) { - var entry = entries[i]; - - // In attribute routing, same template is used for all actions within that controller. The following - // simulates that where we only increment the controller count when a new endpoint for a new template - // is being created. - var template = entry.Template.TemplateText; - if (!templatesVisited.TryGetValue(template, out var visitedTemplateInfo)) - { - controllerCount++; - visitedTemplateInfo = (controllerCount, 0); - } - - // Increment the action count within a controller template - visitedTemplateInfo.ActionIndex++; - templatesVisited[template] = visitedTemplateInfo; - - var controllerName = $"Controller{visitedTemplateInfo.ControllerIndex}"; - var actionName = $"Action{visitedTemplateInfo.ActionIndex}"; - - var httpMethodText = entry.Method == null ? "httpMethod: null" : $"\"{entry.Method.ToUpperInvariant()}\""; - setupEndpointsLines.Add($" Endpoints[{i}] = CreateEndpoint(\"{template}\", \"{controllerName}\", \"{actionName}\", {httpMethodText});"); + controllerCount++; + visitedTemplateInfo = (controllerCount, 0); } - var setupRequestsLines = new List(); - for (var i = 0; i < entries.Count; i++) - { - var entry = entries[i]; - setupRequestsLines.Add($" Requests[{i}] = new DefaultHttpContext();"); - setupRequestsLines.Add($" Requests[{i}].RequestServices = CreateServices();"); + // Increment the action count within a controller template + visitedTemplateInfo.ActionIndex++; + templatesVisited[template] = visitedTemplateInfo; - if (entry.Method != null) - { - setupRequestsLines.Add($" Requests[{i}].Request.Method = HttpMethods.GetCanonicalizedValue({entries[i].Method});"); - } + var controllerName = $"Controller{visitedTemplateInfo.ControllerIndex}"; + var actionName = $"Action{visitedTemplateInfo.ActionIndex}"; - setupRequestsLines.Add($" Requests[{i}].Request.Path = \"{entries[i].RequestUrl}\";"); - } + var httpMethodText = entry.Method == null ? "httpMethod: null" : $"\"{entry.Method.ToUpperInvariant()}\""; + setupEndpointsLines.Add($" Endpoints[{i}] = CreateEndpoint(\"{template}\", \"{controllerName}\", \"{actionName}\", {httpMethodText});"); + } + + var setupRequestsLines = new List(); + for (var i = 0; i < entries.Count; i++) + { + var entry = entries[i]; + setupRequestsLines.Add($" Requests[{i}] = new DefaultHttpContext();"); + setupRequestsLines.Add($" Requests[{i}].RequestServices = CreateServices();"); - var setupMatcherLines = new List(); - for (var i = 0; i < entries.Count; i++) + if (entry.Method != null) { - setupMatcherLines.Add($" builder.AddEndpoint(Endpoints[{i}]);"); + setupRequestsLines.Add($" Requests[{i}].Request.Method = HttpMethods.GetCanonicalizedValue({entries[i].Method});"); } - return string.Format( - CultureInfo.InvariantCulture, - @" + setupRequestsLines.Add($" Requests[{i}].Request.Path = \"{entries[i].RequestUrl}\";"); + } + + var setupMatcherLines = new List(); + for (var i = 0; i < entries.Count; i++) + { + setupMatcherLines.Add($" builder.AddEndpoint(Endpoints[{i}]);"); + } + + return string.Format( + CultureInfo.InvariantCulture, + @" // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. @@ -129,10 +129,9 @@ namespace Microsoft.AspNetCore.Routing }} }} }}", - string.Join(Environment.NewLine, setupEndpointsLines), - string.Join(Environment.NewLine, setupRequestsLines), - string.Join(Environment.NewLine, setupMatcherLines), - entries.Count); - } +string.Join(Environment.NewLine, setupEndpointsLines), +string.Join(Environment.NewLine, setupRequestsLines), +string.Join(Environment.NewLine, setupMatcherLines), +entries.Count); } } diff --git a/src/Http/Shared/CookieHeaderParserShared.cs b/src/Http/Shared/CookieHeaderParserShared.cs index 9f9c3fac4a..f107a04b8e 100644 --- a/src/Http/Shared/CookieHeaderParserShared.cs +++ b/src/Http/Shared/CookieHeaderParserShared.cs @@ -5,237 +5,236 @@ using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +internal static class CookieHeaderParserShared { - internal static class CookieHeaderParserShared + public static bool TryParseValues(StringValues values, IDictionary store, bool enableCookieNameEncoding, bool supportsMultipleValues) { - public static bool TryParseValues(StringValues values, IDictionary store, bool enableCookieNameEncoding, bool supportsMultipleValues) + // If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller + // can ignore the value. + if (values.Count == 0) { - // If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller - // can ignore the value. - if (values.Count == 0) - { - return false; - } - var hasFoundValue = false; - - for (var i = 0; i < values.Count; i++) - { - var value = values[i]; - var index = 0; - - while (!string.IsNullOrEmpty(value) && index < value.Length) - { - if (TryParseValue(value, ref index, supportsMultipleValues, out var parsedName, out var parsedValue)) - { - // The entry may not contain an actual value, like " , " - var name = enableCookieNameEncoding ? Uri.UnescapeDataString(parsedName.Value.Value!) : parsedName.Value.Value!; - store[name] = Uri.UnescapeDataString(parsedValue.Value.Value!); - hasFoundValue = true; - } - else - { - // Skip the invalid values and keep trying. - index++; - } - } - } - - return hasFoundValue; + return false; } + var hasFoundValue = false; - public static bool TryParseValue(StringSegment value, ref int index, bool supportsMultipleValues, [NotNullWhen(true)] out StringSegment? parsedName, [NotNullWhen(true)] out StringSegment? parsedValue) + for (var i = 0; i < values.Count; i++) { - parsedName = null; - parsedValue = null; + var value = values[i]; + var index = 0; - // If multiple values are supported (i.e. list of values), then accept an empty string: The header may - // be added multiple times to the request/response message. E.g. - // Accept: text/xml; q=1 - // Accept: - // Accept: text/plain; q=0.2 - if (StringSegment.IsNullOrEmpty(value) || (index == value.Length)) + while (!string.IsNullOrEmpty(value) && index < value.Length) { - return supportsMultipleValues; - } - - var current = GetNextNonEmptyOrWhitespaceIndex(value, index, supportsMultipleValues, out var separatorFound); - - if (separatorFound && !supportsMultipleValues) - { - return false; // leading separators not allowed if we don't support multiple values. - } - - if (current == value.Length) - { - if (supportsMultipleValues) + if (TryParseValue(value, ref index, supportsMultipleValues, out var parsedName, out var parsedValue)) + { + // The entry may not contain an actual value, like " , " + var name = enableCookieNameEncoding ? Uri.UnescapeDataString(parsedName.Value.Value!) : parsedName.Value.Value!; + store[name] = Uri.UnescapeDataString(parsedValue.Value.Value!); + hasFoundValue = true; + } + else { - index = current; + // Skip the invalid values and keep trying. + index++; } - return supportsMultipleValues; } + } - if (!TryGetCookieLength(value, ref current, out parsedName, out parsedValue)) - { - return false; - } + return hasFoundValue; + } - current = GetNextNonEmptyOrWhitespaceIndex(value, current, supportsMultipleValues, out separatorFound); + public static bool TryParseValue(StringSegment value, ref int index, bool supportsMultipleValues, [NotNullWhen(true)] out StringSegment? parsedName, [NotNullWhen(true)] out StringSegment? parsedValue) + { + parsedName = null; + parsedValue = null; + + // If multiple values are supported (i.e. list of values), then accept an empty string: The header may + // be added multiple times to the request/response message. E.g. + // Accept: text/xml; q=1 + // Accept: + // Accept: text/plain; q=0.2 + if (StringSegment.IsNullOrEmpty(value) || (index == value.Length)) + { + return supportsMultipleValues; + } + + var current = GetNextNonEmptyOrWhitespaceIndex(value, index, supportsMultipleValues, out var separatorFound); + + if (separatorFound && !supportsMultipleValues) + { + return false; // leading separators not allowed if we don't support multiple values. + } - // If we support multiple values and we've not reached the end of the string, then we must have a separator. - if ((separatorFound && !supportsMultipleValues) || (!separatorFound && (current < value.Length))) + if (current == value.Length) + { + if (supportsMultipleValues) { - return false; + index = current; } + return supportsMultipleValues; + } - index = current; - - return true; + if (!TryGetCookieLength(value, ref current, out parsedName, out parsedValue)) + { + return false; } - private static int GetNextNonEmptyOrWhitespaceIndex(StringSegment input, int startIndex, bool skipEmptyValues, out bool separatorFound) + current = GetNextNonEmptyOrWhitespaceIndex(value, current, supportsMultipleValues, out separatorFound); + + // If we support multiple values and we've not reached the end of the string, then we must have a separator. + if ((separatorFound && !supportsMultipleValues) || (!separatorFound && (current < value.Length))) { - Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length. + return false; + } - separatorFound = false; - var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex); + index = current; - if ((current == input.Length) || (input[current] != ',' && input[current] != ';')) - { - return current; - } + return true; + } - // If we have a separator, skip the separator and all following whitespaces. If we support - // empty values, continue until the current character is neither a separator nor a whitespace. - separatorFound = true; - current++; // skip delimiter. - current = current + HttpRuleParser.GetWhitespaceLength(input, current); + private static int GetNextNonEmptyOrWhitespaceIndex(StringSegment input, int startIndex, bool skipEmptyValues, out bool separatorFound) + { + Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length. - if (skipEmptyValues) - { - // Most headers only split on ',', but cookies primarily split on ';' - while ((current < input.Length) && ((input[current] == ',') || (input[current] == ';'))) - { - current++; // skip delimiter. - current = current + HttpRuleParser.GetWhitespaceLength(input, current); - } - } + separatorFound = false; + var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex); + if ((current == input.Length) || (input[current] != ',' && input[current] != ';')) + { return current; } - // name=value; name="value" - internal static bool TryGetCookieLength(StringSegment input, ref int offset, [NotNullWhen(true)] out StringSegment? parsedName, [NotNullWhen(true)] out StringSegment? parsedValue) - { - Contract.Requires(offset >= 0); - - parsedName = null; - parsedValue = null; + // If we have a separator, skip the separator and all following whitespaces. If we support + // empty values, continue until the current character is neither a separator nor a whitespace. + separatorFound = true; + current++; // skip delimiter. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); - if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length)) + if (skipEmptyValues) + { + // Most headers only split on ',', but cookies primarily split on ';' + while ((current < input.Length) && ((input[current] == ',') || (input[current] == ';'))) { - return false; + current++; // skip delimiter. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); } + } - // The caller should have already consumed any leading whitespace, commas, etc.. + return current; + } - // Name=value; + // name=value; name="value" + internal static bool TryGetCookieLength(StringSegment input, ref int offset, [NotNullWhen(true)] out StringSegment? parsedName, [NotNullWhen(true)] out StringSegment? parsedValue) + { + Contract.Requires(offset >= 0); - // Name - var itemLength = HttpRuleParser.GetTokenLength(input, offset); - if (itemLength == 0) - { - return false; - } + parsedName = null; + parsedValue = null; - parsedName = input.Subsegment(offset, itemLength); - offset += itemLength; + if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length)) + { + return false; + } - // = (no spaces) - if (!ReadEqualsSign(input, ref offset)) - { - return false; - } + // The caller should have already consumed any leading whitespace, commas, etc.. - // value or "quoted value" - // The value may be empty - parsedValue = GetCookieValue(input, ref offset); + // Name=value; - return true; + // Name + var itemLength = HttpRuleParser.GetTokenLength(input, offset); + if (itemLength == 0) + { + return false; } - // cookie-value = *cookie-octet / ( DQUOTE* cookie-octet DQUOTE ) - // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E - // ; US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash - internal static StringSegment GetCookieValue(StringSegment input, ref int offset) + parsedName = input.Subsegment(offset, itemLength); + offset += itemLength; + + // = (no spaces) + if (!ReadEqualsSign(input, ref offset)) { - Contract.Requires(offset >= 0); - Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - offset))); + return false; + } - var startIndex = offset; + // value or "quoted value" + // The value may be empty + parsedValue = GetCookieValue(input, ref offset); - if (offset >= input.Length) - { - return StringSegment.Empty; - } - var inQuotes = false; + return true; + } - if (input[offset] == '"') - { - inQuotes = true; - offset++; - } + // cookie-value = *cookie-octet / ( DQUOTE* cookie-octet DQUOTE ) + // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E + // ; US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash + internal static StringSegment GetCookieValue(StringSegment input, ref int offset) + { + Contract.Requires(offset >= 0); + Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - offset))); - while (offset < input.Length) - { - var c = input[offset]; - if (!IsCookieValueChar(c)) - { - break; - } + var startIndex = offset; - offset++; - } + if (offset >= input.Length) + { + return StringSegment.Empty; + } + var inQuotes = false; - if (inQuotes) - { - if (offset == input.Length || input[offset] != '"') - { - // Missing final quote - return StringSegment.Empty; - } - offset++; - } + if (input[offset] == '"') + { + inQuotes = true; + offset++; + } - var length = offset - startIndex; - if (offset > startIndex) + while (offset < input.Length) + { + var c = input[offset]; + if (!IsCookieValueChar(c)) { - return input.Subsegment(startIndex, length); + break; } - return StringSegment.Empty; + offset++; } - private static bool ReadEqualsSign(StringSegment input, ref int offset) + if (inQuotes) { - // = (no spaces) - if (offset >= input.Length || input[offset] != '=') + if (offset == input.Length || input[offset] != '"') { - return false; + // Missing final quote + return StringSegment.Empty; } offset++; - return true; } - // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E - // ; US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash - private static bool IsCookieValueChar(char c) + var length = offset - startIndex; + if (offset > startIndex) { - if (c < 0x21 || c > 0x7E) - { - return false; - } - return !(c == '"' || c == ',' || c == ';' || c == '\\'); + return input.Subsegment(startIndex, length); + } + + return StringSegment.Empty; + } + + private static bool ReadEqualsSign(StringSegment input, ref int offset) + { + // = (no spaces) + if (offset >= input.Length || input[offset] != '=') + { + return false; + } + offset++; + return true; + } + + // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E + // ; US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash + private static bool IsCookieValueChar(char c) + { + if (c < 0x21 || c > 0x7E) + { + return false; } + return !(c == '"' || c == ',' || c == ';' || c == '\\'); } } diff --git a/src/Http/Shared/HttpParseResult.cs b/src/Http/Shared/HttpParseResult.cs index ce550ed193..af01f87546 100644 --- a/src/Http/Shared/HttpParseResult.cs +++ b/src/Http/Shared/HttpParseResult.cs @@ -1,12 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +internal enum HttpParseResult { - internal enum HttpParseResult - { - Parsed, - NotParsed, - InvalidFormat, - } + Parsed, + NotParsed, + InvalidFormat, } diff --git a/src/Http/Shared/HttpRuleParser.cs b/src/Http/Shared/HttpRuleParser.cs index 42cc2e1a2e..02b9b4dd09 100644 --- a/src/Http/Shared/HttpRuleParser.cs +++ b/src/Http/Shared/HttpRuleParser.cs @@ -7,13 +7,13 @@ using System.Globalization; using System.Text; using Microsoft.Extensions.Primitives; -namespace Microsoft.Net.Http.Headers +namespace Microsoft.Net.Http.Headers; + +internal static class HttpRuleParser { - internal static class HttpRuleParser - { - private static readonly bool[] TokenChars = CreateTokenChars(); - private const int MaxNestedCount = 5; - private static readonly string[] DateFormats = new string[] { + private static readonly bool[] TokenChars = CreateTokenChars(); + private const int MaxNestedCount = 5; + private static readonly string[] DateFormats = new string[] { // "r", // RFC 1123, required output format but too strict for input "ddd, d MMM yyyy H:m:s 'GMT'", // RFC 1123 (r, except it allows both 1 and 01 for date and time) "ddd, d MMM yyyy H:m:s", // RFC 1123, no zone - assume GMT @@ -35,311 +35,310 @@ namespace Microsoft.Net.Http.Headers "d MMM yyyy H:m:s", // RFC 5322 no day-of-week, no zone }; - internal const char CR = '\r'; - internal const char LF = '\n'; - internal const char SP = ' '; - internal const char Tab = '\t'; - internal const int MaxInt64Digits = 19; - internal const int MaxInt32Digits = 10; - - // iso-8859-1, Western European (ISO) - internal static readonly Encoding DefaultHttpEncoding = Encoding.GetEncoding("iso-8859-1"); + internal const char CR = '\r'; + internal const char LF = '\n'; + internal const char SP = ' '; + internal const char Tab = '\t'; + internal const int MaxInt64Digits = 19; + internal const int MaxInt32Digits = 10; - private static bool[] CreateTokenChars() - { - // token = 1* - // CTL = + // iso-8859-1, Western European (ISO) + internal static readonly Encoding DefaultHttpEncoding = Encoding.GetEncoding("iso-8859-1"); - var tokenChars = new bool[128]; // everything is false + private static bool[] CreateTokenChars() + { + // token = 1* + // CTL = - for (var i = 33; i < 127; i++) // skip Space (32) & DEL (127) - { - tokenChars[i] = true; - } + var tokenChars = new bool[128]; // everything is false - // remove separators: these are not valid token characters - tokenChars[(byte)'('] = false; - tokenChars[(byte)')'] = false; - tokenChars[(byte)'<'] = false; - tokenChars[(byte)'>'] = false; - tokenChars[(byte)'@'] = false; - tokenChars[(byte)','] = false; - tokenChars[(byte)';'] = false; - tokenChars[(byte)':'] = false; - tokenChars[(byte)'\\'] = false; - tokenChars[(byte)'"'] = false; - tokenChars[(byte)'/'] = false; - tokenChars[(byte)'['] = false; - tokenChars[(byte)']'] = false; - tokenChars[(byte)'?'] = false; - tokenChars[(byte)'='] = false; - tokenChars[(byte)'{'] = false; - tokenChars[(byte)'}'] = false; - - return tokenChars; + for (var i = 33; i < 127; i++) // skip Space (32) & DEL (127) + { + tokenChars[i] = true; } - internal static bool IsTokenChar(char character) - { - // Must be between 'space' (32) and 'DEL' (127) - if (character > 127) - { - return false; - } + // remove separators: these are not valid token characters + tokenChars[(byte)'('] = false; + tokenChars[(byte)')'] = false; + tokenChars[(byte)'<'] = false; + tokenChars[(byte)'>'] = false; + tokenChars[(byte)'@'] = false; + tokenChars[(byte)','] = false; + tokenChars[(byte)';'] = false; + tokenChars[(byte)':'] = false; + tokenChars[(byte)'\\'] = false; + tokenChars[(byte)'"'] = false; + tokenChars[(byte)'/'] = false; + tokenChars[(byte)'['] = false; + tokenChars[(byte)']'] = false; + tokenChars[(byte)'?'] = false; + tokenChars[(byte)'='] = false; + tokenChars[(byte)'{'] = false; + tokenChars[(byte)'}'] = false; + + return tokenChars; + } - return TokenChars[character]; + internal static bool IsTokenChar(char character) + { + // Must be between 'space' (32) and 'DEL' (127) + if (character > 127) + { + return false; } - [Pure] - internal static int GetTokenLength(StringSegment input, int startIndex) + return TokenChars[character]; + } + + [Pure] + internal static int GetTokenLength(StringSegment input, int startIndex) + { + Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - startIndex))); + + if (startIndex >= input.Length) { - Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - startIndex))); + return 0; + } + + var current = startIndex; - if (startIndex >= input.Length) + while (current < input.Length) + { + if (!IsTokenChar(input[current])) { - return 0; + return current - startIndex; } + current++; + } + return input.Length - startIndex; + } - var current = startIndex; + internal static int GetWhitespaceLength(StringSegment input, int startIndex) + { + Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - startIndex))); - while (current < input.Length) - { - if (!IsTokenChar(input[current])) - { - return current - startIndex; - } - current++; - } - return input.Length - startIndex; + if (startIndex >= input.Length) + { + return 0; } - internal static int GetWhitespaceLength(StringSegment input, int startIndex) + var current = startIndex; + + char c; + while (current < input.Length) { - Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - startIndex))); + c = input[current]; - if (startIndex >= input.Length) + if ((c == SP) || (c == Tab)) { - return 0; + current++; + continue; } - var current = startIndex; - - char c; - while (current < input.Length) + if (c == CR) { - c = input[current]; - - if ((c == SP) || (c == Tab)) + // If we have a #13 char, it must be followed by #10 and then at least one SP or HT. + if ((current + 2 < input.Length) && (input[current + 1] == LF)) { - current++; - continue; - } - - if (c == CR) - { - // If we have a #13 char, it must be followed by #10 and then at least one SP or HT. - if ((current + 2 < input.Length) && (input[current + 1] == LF)) + var spaceOrTab = input[current + 2]; + if ((spaceOrTab == SP) || (spaceOrTab == Tab)) { - var spaceOrTab = input[current + 2]; - if ((spaceOrTab == SP) || (spaceOrTab == Tab)) - { - current += 3; - continue; - } + current += 3; + continue; } } - - return current - startIndex; } - // All characters between startIndex and the end of the string are LWS characters. - return input.Length - startIndex; + return current - startIndex; + } + + // All characters between startIndex and the end of the string are LWS characters. + return input.Length - startIndex; + } + + internal static int GetNumberLength(StringSegment input, int startIndex, bool allowDecimal) + { + Contract.Requires((startIndex >= 0) && (startIndex < input.Length)); + Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - startIndex))); + + var current = startIndex; + char c; + + // If decimal values are not allowed, we pretend to have read the '.' character already. I.e. if a dot is + // found in the string, parsing will be aborted. + var haveDot = !allowDecimal; + + // The RFC doesn't allow decimal values starting with dot. I.e. value ".123" is invalid. It must be in the + // form "0.123". Also, there are no negative values defined in the RFC. So we'll just parse non-negative + // values. + // The RFC only allows decimal dots not ',' characters as decimal separators. Therefore value "1,23" is + // considered invalid and must be represented as "1.23". + if (input[current] == '.') + { + return 0; } - internal static int GetNumberLength(StringSegment input, int startIndex, bool allowDecimal) + while (current < input.Length) { - Contract.Requires((startIndex >= 0) && (startIndex < input.Length)); - Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - startIndex))); - - var current = startIndex; - char c; - - // If decimal values are not allowed, we pretend to have read the '.' character already. I.e. if a dot is - // found in the string, parsing will be aborted. - var haveDot = !allowDecimal; - - // The RFC doesn't allow decimal values starting with dot. I.e. value ".123" is invalid. It must be in the - // form "0.123". Also, there are no negative values defined in the RFC. So we'll just parse non-negative - // values. - // The RFC only allows decimal dots not ',' characters as decimal separators. Therefore value "1,23" is - // considered invalid and must be represented as "1.23". - if (input[current] == '.') + c = input[current]; + if ((c >= '0') && (c <= '9')) { - return 0; + current++; } - - while (current < input.Length) + else if (!haveDot && (c == '.')) { - c = input[current]; - if ((c >= '0') && (c <= '9')) - { - current++; - } - else if (!haveDot && (c == '.')) - { - // Note that value "1." is valid. - haveDot = true; - current++; - } - else - { - break; - } + // Note that value "1." is valid. + haveDot = true; + current++; + } + else + { + break; } - - return current - startIndex; } - internal static HttpParseResult GetQuotedStringLength(StringSegment input, int startIndex, out int length) + return current - startIndex; + } + + internal static HttpParseResult GetQuotedStringLength(StringSegment input, int startIndex, out int length) + { + var nestedCount = 0; + return GetExpressionLength(input, startIndex, '"', '"', false, ref nestedCount, out length); + } + + // quoted-pair = "\" CHAR + // CHAR = + internal static HttpParseResult GetQuotedPairLength(StringSegment input, int startIndex, out int length) + { + Contract.Requires((startIndex >= 0) && (startIndex < input.Length)); + Contract.Ensures((Contract.ValueAtReturn(out length) >= 0) && + (Contract.ValueAtReturn(out length) <= (input.Length - startIndex))); + + length = 0; + + if (input[startIndex] != '\\') { - var nestedCount = 0; - return GetExpressionLength(input, startIndex, '"', '"', false, ref nestedCount, out length); + return HttpParseResult.NotParsed; } - // quoted-pair = "\" CHAR - // CHAR = - internal static HttpParseResult GetQuotedPairLength(StringSegment input, int startIndex, out int length) + // Quoted-char has 2 characters. Check whether there are 2 chars left ('\' + char) + // If so, check whether the character is in the range 0-127. If not, it's an invalid value. + if ((startIndex + 2 > input.Length) || (input[startIndex + 1] > 127)) { - Contract.Requires((startIndex >= 0) && (startIndex < input.Length)); - Contract.Ensures((Contract.ValueAtReturn(out length) >= 0) && - (Contract.ValueAtReturn(out length) <= (input.Length - startIndex))); + return HttpParseResult.InvalidFormat; + } - length = 0; + // We don't care what the char next to '\' is. + length = 2; + return HttpParseResult.Parsed; + } - if (input[startIndex] != '\\') - { - return HttpParseResult.NotParsed; - } + // Try the various date formats in the order listed above. + // We should accept a wide verity of common formats, but only output RFC 1123 style dates. + internal static bool TryStringToDate(StringSegment input, out DateTimeOffset result) => + DateTimeOffset.TryParseExact(input.ToString(), DateFormats, DateTimeFormatInfo.InvariantInfo, + DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, out result); + + // TEXT = + // LWS = [CRLF] 1*( SP | HT ) + // CTL = + // + // Since we don't really care about the content of a quoted string or comment, we're more tolerant and + // allow these characters. We only want to find the delimiters ('"' for quoted string and '(', ')' for comment). + // + // 'nestedCount': Comments can be nested. We allow a depth of up to 5 nested comments, i.e. something like + // "(((((comment)))))". If we wouldn't define a limit an attacker could send a comment with hundreds of nested + // comments, resulting in a stack overflow exception. In addition having more than 1 nested comment (if any) + // is unusual. + private static HttpParseResult GetExpressionLength( + StringSegment input, + int startIndex, + char openChar, + char closeChar, + bool supportsNesting, + ref int nestedCount, + out int length) + { + Contract.Requires(input != null); + Contract.Requires((startIndex >= 0) && (startIndex < input.Length)); + Contract.Ensures((Contract.Result() != HttpParseResult.Parsed) || + (Contract.ValueAtReturn(out length) > 0)); - // Quoted-char has 2 characters. Check whether there are 2 chars left ('\' + char) - // If so, check whether the character is in the range 0-127. If not, it's an invalid value. - if ((startIndex + 2 > input.Length) || (input[startIndex + 1] > 127)) - { - return HttpParseResult.InvalidFormat; - } + length = 0; - // We don't care what the char next to '\' is. - length = 2; - return HttpParseResult.Parsed; + if (input[startIndex] != openChar) + { + return HttpParseResult.NotParsed; } - // Try the various date formats in the order listed above. - // We should accept a wide verity of common formats, but only output RFC 1123 style dates. - internal static bool TryStringToDate(StringSegment input, out DateTimeOffset result) => - DateTimeOffset.TryParseExact(input.ToString(), DateFormats, DateTimeFormatInfo.InvariantInfo, - DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, out result); - - // TEXT = - // LWS = [CRLF] 1*( SP | HT ) - // CTL = - // - // Since we don't really care about the content of a quoted string or comment, we're more tolerant and - // allow these characters. We only want to find the delimiters ('"' for quoted string and '(', ')' for comment). - // - // 'nestedCount': Comments can be nested. We allow a depth of up to 5 nested comments, i.e. something like - // "(((((comment)))))". If we wouldn't define a limit an attacker could send a comment with hundreds of nested - // comments, resulting in a stack overflow exception. In addition having more than 1 nested comment (if any) - // is unusual. - private static HttpParseResult GetExpressionLength( - StringSegment input, - int startIndex, - char openChar, - char closeChar, - bool supportsNesting, - ref int nestedCount, - out int length) + var current = startIndex + 1; // Start parsing with the character next to the first open-char + while (current < input.Length) { - Contract.Requires(input != null); - Contract.Requires((startIndex >= 0) && (startIndex < input.Length)); - Contract.Ensures((Contract.Result() != HttpParseResult.Parsed) || - (Contract.ValueAtReturn(out length) > 0)); - - length = 0; - - if (input[startIndex] != openChar) + // Only check whether we have a quoted char, if we have at least 3 characters left to read (i.e. + // quoted char + closing char). Otherwise the closing char may be considered part of the quoted char. + var quotedPairLength = 0; + if ((current + 2 < input.Length) && + (GetQuotedPairLength(input, current, out quotedPairLength) == HttpParseResult.Parsed)) { - return HttpParseResult.NotParsed; + // We ignore invalid quoted-pairs. Invalid quoted-pairs may mean that it looked like a quoted pair, + // but we actually have a quoted-string: e.g. "\ü" ('\' followed by a char >127 - quoted-pair only + // allows ASCII chars after '\'; qdtext allows both '\' and >127 chars). + current = current + quotedPairLength; + continue; } - var current = startIndex + 1; // Start parsing with the character next to the first open-char - while (current < input.Length) + // If we support nested expressions and we find an open-char, then parse the nested expressions. + if (supportsNesting && (input[current] == openChar)) { - // Only check whether we have a quoted char, if we have at least 3 characters left to read (i.e. - // quoted char + closing char). Otherwise the closing char may be considered part of the quoted char. - var quotedPairLength = 0; - if ((current + 2 < input.Length) && - (GetQuotedPairLength(input, current, out quotedPairLength) == HttpParseResult.Parsed)) - { - // We ignore invalid quoted-pairs. Invalid quoted-pairs may mean that it looked like a quoted pair, - // but we actually have a quoted-string: e.g. "\ü" ('\' followed by a char >127 - quoted-pair only - // allows ASCII chars after '\'; qdtext allows both '\' and >127 chars). - current = current + quotedPairLength; - continue; - } - - // If we support nested expressions and we find an open-char, then parse the nested expressions. - if (supportsNesting && (input[current] == openChar)) + nestedCount++; + try { - nestedCount++; - try + // Check if we exceeded the number of nested calls. + if (nestedCount > MaxNestedCount) { - // Check if we exceeded the number of nested calls. - if (nestedCount > MaxNestedCount) - { - return HttpParseResult.InvalidFormat; - } - - var nestedLength = 0; - var nestedResult = GetExpressionLength(input, current, openChar, closeChar, - supportsNesting, ref nestedCount, out nestedLength); - - switch (nestedResult) - { - case HttpParseResult.Parsed: - current += nestedLength; // add the length of the nested expression and continue. - break; - - case HttpParseResult.NotParsed: - Contract.Assert(false, "'NotParsed' is unexpected: We started nested expression " + - "parsing, because we found the open-char. So either it's a valid nested " + - "expression or it has invalid format."); - break; - - case HttpParseResult.InvalidFormat: - // If the nested expression is invalid, we can't continue, so we fail with invalid format. - return HttpParseResult.InvalidFormat; - - default: - Contract.Assert(false, "Unknown enum result: " + nestedResult); - break; - } + return HttpParseResult.InvalidFormat; } - finally + + var nestedLength = 0; + var nestedResult = GetExpressionLength(input, current, openChar, closeChar, + supportsNesting, ref nestedCount, out nestedLength); + + switch (nestedResult) { - nestedCount--; + case HttpParseResult.Parsed: + current += nestedLength; // add the length of the nested expression and continue. + break; + + case HttpParseResult.NotParsed: + Contract.Assert(false, "'NotParsed' is unexpected: We started nested expression " + + "parsing, because we found the open-char. So either it's a valid nested " + + "expression or it has invalid format."); + break; + + case HttpParseResult.InvalidFormat: + // If the nested expression is invalid, we can't continue, so we fail with invalid format. + return HttpParseResult.InvalidFormat; + + default: + Contract.Assert(false, "Unknown enum result: " + nestedResult); + break; } } - - if (input[current] == closeChar) + finally { - length = current - startIndex + 1; - return HttpParseResult.Parsed; + nestedCount--; } - current++; } - // We didn't see the final quote, therefore we have an invalid expression string. - return HttpParseResult.InvalidFormat; + if (input[current] == closeChar) + { + length = current - startIndex + 1; + return HttpParseResult.Parsed; + } + current++; } + + // We didn't see the final quote, therefore we have an invalid expression string. + return HttpParseResult.InvalidFormat; } } diff --git a/src/Http/Shared/StreamCopyOperationInternal.cs b/src/Http/Shared/StreamCopyOperationInternal.cs index 50e22fcfa9..128069dcac 100644 --- a/src/Http/Shared/StreamCopyOperationInternal.cs +++ b/src/Http/Shared/StreamCopyOperationInternal.cs @@ -8,80 +8,79 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.Http +namespace Microsoft.AspNetCore.Http; + +// FYI: In most cases the source will be a FileStream and the destination will be to the network. +internal static class StreamCopyOperationInternal { - // FYI: In most cases the source will be a FileStream and the destination will be to the network. - internal static class StreamCopyOperationInternal + private const int DefaultBufferSize = 4096; + + /// Asynchronously reads the given number of bytes from the source stream and writes them to another stream. + /// A task that represents the asynchronous copy operation. + /// The stream from which the contents will be copied. + /// The stream to which the contents of the current stream will be copied. + /// The count of bytes to be copied. + /// The token to monitor for cancellation requests. The default value is . + public static Task CopyToAsync(Stream source, Stream destination, long? count, CancellationToken cancel) { - private const int DefaultBufferSize = 4096; + return CopyToAsync(source, destination, count, DefaultBufferSize, cancel); + } - /// Asynchronously reads the given number of bytes from the source stream and writes them to another stream. - /// A task that represents the asynchronous copy operation. - /// The stream from which the contents will be copied. - /// The stream to which the contents of the current stream will be copied. - /// The count of bytes to be copied. - /// The token to monitor for cancellation requests. The default value is . - public static Task CopyToAsync(Stream source, Stream destination, long? count, CancellationToken cancel) - { - return CopyToAsync(source, destination, count, DefaultBufferSize, cancel); - } + /// Asynchronously reads the given number of bytes from the source stream and writes them to another stream, using a specified buffer size. + /// A task that represents the asynchronous copy operation. + /// The stream from which the contents will be copied. + /// The stream to which the contents of the current stream will be copied. + /// The count of bytes to be copied. + /// The size, in bytes, of the buffer. This value must be greater than zero. The default size is 4096. + /// The token to monitor for cancellation requests. The default value is . + public static async Task CopyToAsync(Stream source, Stream destination, long? count, int bufferSize, CancellationToken cancel) + { + var bytesRemaining = count; - /// Asynchronously reads the given number of bytes from the source stream and writes them to another stream, using a specified buffer size. - /// A task that represents the asynchronous copy operation. - /// The stream from which the contents will be copied. - /// The stream to which the contents of the current stream will be copied. - /// The count of bytes to be copied. - /// The size, in bytes, of the buffer. This value must be greater than zero. The default size is 4096. - /// The token to monitor for cancellation requests. The default value is . - public static async Task CopyToAsync(Stream source, Stream destination, long? count, int bufferSize, CancellationToken cancel) + var buffer = ArrayPool.Shared.Rent(bufferSize); + try { - var bytesRemaining = count; + Debug.Assert(source != null); + Debug.Assert(destination != null); + Debug.Assert(!bytesRemaining.HasValue || bytesRemaining.GetValueOrDefault() >= 0); + Debug.Assert(buffer != null); - var buffer = ArrayPool.Shared.Rent(bufferSize); - try + while (true) { - Debug.Assert(source != null); - Debug.Assert(destination != null); - Debug.Assert(!bytesRemaining.HasValue || bytesRemaining.GetValueOrDefault() >= 0); - Debug.Assert(buffer != null); - - while (true) + // The natural end of the range. + if (bytesRemaining.HasValue && bytesRemaining.GetValueOrDefault() <= 0) { - // The natural end of the range. - if (bytesRemaining.HasValue && bytesRemaining.GetValueOrDefault() <= 0) - { - return; - } + return; + } - cancel.ThrowIfCancellationRequested(); + cancel.ThrowIfCancellationRequested(); - var readLength = buffer.Length; - if (bytesRemaining.HasValue) - { - readLength = (int)Math.Min(bytesRemaining.GetValueOrDefault(), (long)readLength); - } - var read = await source.ReadAsync(buffer.AsMemory(0, readLength), cancel); + var readLength = buffer.Length; + if (bytesRemaining.HasValue) + { + readLength = (int)Math.Min(bytesRemaining.GetValueOrDefault(), (long)readLength); + } + var read = await source.ReadAsync(buffer.AsMemory(0, readLength), cancel); - if (bytesRemaining.HasValue) - { - bytesRemaining -= read; - } + if (bytesRemaining.HasValue) + { + bytesRemaining -= read; + } - // End of the source stream. - if (read == 0) - { - return; - } + // End of the source stream. + if (read == 0) + { + return; + } - cancel.ThrowIfCancellationRequested(); + cancel.ThrowIfCancellationRequested(); - await destination.WriteAsync(buffer.AsMemory(0, read), cancel); - } - } - finally - { - ArrayPool.Shared.Return(buffer); + await destination.WriteAsync(buffer.AsMemory(0, read), cancel); } } + finally + { + ArrayPool.Shared.Return(buffer); + } } } diff --git a/src/Http/WebUtilities/perf/Microbenchmarks/FormPipeReaderInternalsBenchmark.cs b/src/Http/WebUtilities/perf/Microbenchmarks/FormPipeReaderInternalsBenchmark.cs index 8b3c20c2c0..fc3137bff9 100644 --- a/src/Http/WebUtilities/perf/Microbenchmarks/FormPipeReaderInternalsBenchmark.cs +++ b/src/Http/WebUtilities/perf/Microbenchmarks/FormPipeReaderInternalsBenchmark.cs @@ -5,40 +5,39 @@ using System.Buffers; using System.Text; using BenchmarkDotNet.Attributes; -namespace Microsoft.AspNetCore.WebUtilities.Microbenchmarks +namespace Microsoft.AspNetCore.WebUtilities.Microbenchmarks; + +/// +/// Test internal parsing speed of FormPipeReader without pipe +/// +public class FormPipeReaderInternalsBenchmark { - /// - /// Test internal parsing speed of FormPipeReader without pipe - /// - public class FormPipeReaderInternalsBenchmark + private readonly byte[] _singleUtf8 = Encoding.UTF8.GetBytes("foo=bar&baz=boo&haha=hehe&lol=temp"); + private readonly byte[] _firstUtf8 = Encoding.UTF8.GetBytes("foo=bar&baz=bo"); + private readonly byte[] _secondUtf8 = Encoding.UTF8.GetBytes("o&haha=hehe&lol=temp"); + private FormPipeReader _formPipeReader; + + [IterationSetup] + public void Setup() + { + _formPipeReader = new FormPipeReader(null); + } + + [Benchmark] + public void ReadUtf8Data() { - private readonly byte[] _singleUtf8 = Encoding.UTF8.GetBytes("foo=bar&baz=boo&haha=hehe&lol=temp"); - private readonly byte[] _firstUtf8 = Encoding.UTF8.GetBytes("foo=bar&baz=bo"); - private readonly byte[] _secondUtf8 = Encoding.UTF8.GetBytes("o&haha=hehe&lol=temp"); - private FormPipeReader _formPipeReader; - - [IterationSetup] - public void Setup() - { - _formPipeReader = new FormPipeReader(null); - } - - [Benchmark] - public void ReadUtf8Data() - { - var buffer = new ReadOnlySequence(_singleUtf8); - KeyValueAccumulator accum = default; - - _formPipeReader.ParseFormValues(ref buffer, ref accum, isFinalBlock: true); - } - - [Benchmark] - public void ReadUtf8MultipleBlockData() - { - var buffer = ReadOnlySequenceFactory.CreateSegments(_firstUtf8, _secondUtf8); - KeyValueAccumulator accum = default; - - _formPipeReader.ParseFormValues(ref buffer, ref accum, isFinalBlock: true); - } + var buffer = new ReadOnlySequence(_singleUtf8); + KeyValueAccumulator accum = default; + + _formPipeReader.ParseFormValues(ref buffer, ref accum, isFinalBlock: true); + } + + [Benchmark] + public void ReadUtf8MultipleBlockData() + { + var buffer = ReadOnlySequenceFactory.CreateSegments(_firstUtf8, _secondUtf8); + KeyValueAccumulator accum = default; + + _formPipeReader.ParseFormValues(ref buffer, ref accum, isFinalBlock: true); } } diff --git a/src/Http/WebUtilities/perf/Microbenchmarks/FormReaderBenchmark.cs b/src/Http/WebUtilities/perf/Microbenchmarks/FormReaderBenchmark.cs index d520dd60fa..8d7e59a44f 100644 --- a/src/Http/WebUtilities/perf/Microbenchmarks/FormReaderBenchmark.cs +++ b/src/Http/WebUtilities/perf/Microbenchmarks/FormReaderBenchmark.cs @@ -11,39 +11,38 @@ using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +public class FormReaderBenchmark { - public class FormReaderBenchmark + [Benchmark] + public async Task ReadSmallFormAsyncStream() { - [Benchmark] - public async Task ReadSmallFormAsyncStream() - { - var bytes = Encoding.UTF8.GetBytes("foo=bar&baz=boo"); - var stream = new MemoryStream(bytes); + var bytes = Encoding.UTF8.GetBytes("foo=bar&baz=boo"); + var stream = new MemoryStream(bytes); - for (var i = 0; i < 1000; i++) - { - var formReader = new FormReader(stream); - await formReader.ReadFormAsync(); - stream.Position = 0; - } + for (var i = 0; i < 1000; i++) + { + var formReader = new FormReader(stream); + await formReader.ReadFormAsync(); + stream.Position = 0; } + } - [Benchmark] - public async Task ReadSmallFormAsyncPipe() - { - var pipe = new Pipe(); - var bytes = Encoding.UTF8.GetBytes("foo=bar&baz=boo"); + [Benchmark] + public async Task ReadSmallFormAsyncPipe() + { + var pipe = new Pipe(); + var bytes = Encoding.UTF8.GetBytes("foo=bar&baz=boo"); - for (var i = 0; i < 1000; i++) - { - pipe.Writer.Write(bytes); - pipe.Writer.Complete(); - var formReader = new FormPipeReader(pipe.Reader); - await formReader.ReadFormAsync(); - pipe.Reader.Complete(); - pipe.Reset(); - } + for (var i = 0; i < 1000; i++) + { + pipe.Writer.Write(bytes); + pipe.Writer.Complete(); + var formReader = new FormPipeReader(pipe.Reader); + await formReader.ReadFormAsync(); + pipe.Reader.Complete(); + pipe.Reset(); } } } diff --git a/src/Http/WebUtilities/perf/Microbenchmarks/HttpRequestStreamReaderReadLineBenchmark.cs b/src/Http/WebUtilities/perf/Microbenchmarks/HttpRequestStreamReaderReadLineBenchmark.cs index a4a6c7aa85..f90d48caf4 100644 --- a/src/Http/WebUtilities/perf/Microbenchmarks/HttpRequestStreamReaderReadLineBenchmark.cs +++ b/src/Http/WebUtilities/perf/Microbenchmarks/HttpRequestStreamReaderReadLineBenchmark.cs @@ -7,49 +7,48 @@ using System.Text; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +public class HttpRequestStreamReaderReadLineBenchmark { - public class HttpRequestStreamReaderReadLineBenchmark + private MemoryStream _stream; + + [Params(200, 1000, 1025, 1600)] // Default buffer length is 1024 + public int Length { get; set; } + + [GlobalSetup] + public void GlobalSetup() + { + var data = new char[Length]; + + data[Length - 2] = '\r'; + data[Length - 1] = '\n'; + + _stream = new MemoryStream(Encoding.UTF8.GetBytes(data)); + } + + [Benchmark] + public async Task ReadLineAsync() + { + var reader = CreateReader(); + var result = await reader.ReadLineAsync(); + Debug.Assert(result.Length == Length - 2); + return result; + } + + [Benchmark] + public string ReadLine() + { + var reader = CreateReader(); + var result = reader.ReadLine(); + Debug.Assert(result.Length == Length - 2); + return result; + } + + [Benchmark] + public HttpRequestStreamReader CreateReader() { - private MemoryStream _stream; - - [Params(200, 1000, 1025, 1600)] // Default buffer length is 1024 - public int Length { get; set; } - - [GlobalSetup] - public void GlobalSetup() - { - var data = new char[Length]; - - data[Length - 2] = '\r'; - data[Length - 1] = '\n'; - - _stream = new MemoryStream(Encoding.UTF8.GetBytes(data)); - } - - [Benchmark] - public async Task ReadLineAsync() - { - var reader = CreateReader(); - var result = await reader.ReadLineAsync(); - Debug.Assert(result.Length == Length - 2); - return result; - } - - [Benchmark] - public string ReadLine() - { - var reader = CreateReader(); - var result = reader.ReadLine(); - Debug.Assert(result.Length == Length - 2); - return result; - } - - [Benchmark] - public HttpRequestStreamReader CreateReader() - { - _stream.Seek(0, SeekOrigin.Begin); - return new HttpRequestStreamReader(_stream, Encoding.UTF8); - } + _stream.Seek(0, SeekOrigin.Begin); + return new HttpRequestStreamReader(_stream, Encoding.UTF8); } } diff --git a/src/Http/WebUtilities/src/AspNetCoreTempDirectory.cs b/src/Http/WebUtilities/src/AspNetCoreTempDirectory.cs index 77944c647e..6a0fcc04c6 100644 --- a/src/Http/WebUtilities/src/AspNetCoreTempDirectory.cs +++ b/src/Http/WebUtilities/src/AspNetCoreTempDirectory.cs @@ -6,34 +6,33 @@ using System; using System.IO; -namespace Microsoft.AspNetCore.Internal +namespace Microsoft.AspNetCore.Internal; + +internal static class AspNetCoreTempDirectory { - internal static class AspNetCoreTempDirectory - { - private static string? _tempDirectory; + private static string? _tempDirectory; - public static string TempDirectory + public static string TempDirectory + { + get { - get + if (_tempDirectory == null) { - if (_tempDirectory == null) - { - // Look for folders in the following order. - var temp = Environment.GetEnvironmentVariable("ASPNETCORE_TEMP") ?? // ASPNETCORE_TEMP - User set temporary location. - Path.GetTempPath(); // Fall back. - - if (!Directory.Exists(temp)) - { - throw new DirectoryNotFoundException(temp); - } + // Look for folders in the following order. + var temp = Environment.GetEnvironmentVariable("ASPNETCORE_TEMP") ?? // ASPNETCORE_TEMP - User set temporary location. + Path.GetTempPath(); // Fall back. - _tempDirectory = temp; + if (!Directory.Exists(temp)) + { + throw new DirectoryNotFoundException(temp); } - return _tempDirectory; + _tempDirectory = temp; } - } - public static Func TempDirectoryFactory => () => TempDirectory; + return _tempDirectory; + } } + + public static Func TempDirectoryFactory => () => TempDirectory; } diff --git a/src/Http/WebUtilities/src/Base64UrlTextEncoder.cs b/src/Http/WebUtilities/src/Base64UrlTextEncoder.cs index 8c601b2668..13450fbb18 100644 --- a/src/Http/WebUtilities/src/Base64UrlTextEncoder.cs +++ b/src/Http/WebUtilities/src/Base64UrlTextEncoder.cs @@ -1,33 +1,32 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +/// +/// Encodes and decodes using base64 url encoding. +/// +public static class Base64UrlTextEncoder { /// - /// Encodes and decodes using base64 url encoding. + /// Encodes supplied data into Base64 and replaces any URL encodable characters into non-URL encodable + /// characters. /// - public static class Base64UrlTextEncoder + /// Data to be encoded. + /// Base64 encoded string modified with non-URL encodable characters + public static string Encode(byte[] data) { - /// - /// Encodes supplied data into Base64 and replaces any URL encodable characters into non-URL encodable - /// characters. - /// - /// Data to be encoded. - /// Base64 encoded string modified with non-URL encodable characters - public static string Encode(byte[] data) - { - return WebEncoders.Base64UrlEncode(data); - } + return WebEncoders.Base64UrlEncode(data); + } - /// - /// Decodes supplied string by replacing the non-URL encodable characters with URL encodable characters and - /// then decodes the Base64 string. - /// - /// The string to be decoded. - /// The decoded data. - public static byte[] Decode(string text) - { - return WebEncoders.Base64UrlDecode(text); - } + /// + /// Decodes supplied string by replacing the non-URL encodable characters with URL encodable characters and + /// then decodes the Base64 string. + /// + /// The string to be decoded. + /// The decoded data. + public static byte[] Decode(string text) + { + return WebEncoders.Base64UrlDecode(text); } } diff --git a/src/Http/WebUtilities/src/BufferedReadStream.cs b/src/Http/WebUtilities/src/BufferedReadStream.cs index e14c36b51d..4020c8242c 100644 --- a/src/Http/WebUtilities/src/BufferedReadStream.cs +++ b/src/Http/WebUtilities/src/BufferedReadStream.cs @@ -8,429 +8,428 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +/// +/// A Stream that wraps another stream and allows reading lines. +/// The data is buffered in memory. +/// +public class BufferedReadStream : Stream { + private const byte CR = (byte)'\r'; + private const byte LF = (byte)'\n'; + + private readonly Stream _inner; + private readonly byte[] _buffer; + private readonly ArrayPool _bytePool; + private int _bufferOffset; + private int _bufferCount; + private bool _disposed; + /// - /// A Stream that wraps another stream and allows reading lines. - /// The data is buffered in memory. + /// Creates a new stream. /// - public class BufferedReadStream : Stream + /// The stream to wrap. + /// Size of buffer in bytes. + public BufferedReadStream(Stream inner, int bufferSize) + : this(inner, bufferSize, ArrayPool.Shared) { - private const byte CR = (byte)'\r'; - private const byte LF = (byte)'\n'; - - private readonly Stream _inner; - private readonly byte[] _buffer; - private readonly ArrayPool _bytePool; - private int _bufferOffset; - private int _bufferCount; - private bool _disposed; - - /// - /// Creates a new stream. - /// - /// The stream to wrap. - /// Size of buffer in bytes. - public BufferedReadStream(Stream inner, int bufferSize) - : this(inner, bufferSize, ArrayPool.Shared) - { - } + } - /// - /// Creates a new stream. - /// - /// The stream to wrap. - /// Size of buffer in bytes. - /// ArrayPool for the buffer. - public BufferedReadStream(Stream inner, int bufferSize, ArrayPool bytePool) + /// + /// Creates a new stream. + /// + /// The stream to wrap. + /// Size of buffer in bytes. + /// ArrayPool for the buffer. + public BufferedReadStream(Stream inner, int bufferSize, ArrayPool bytePool) + { + if (inner == null) { - if (inner == null) - { - throw new ArgumentNullException(nameof(inner)); - } - - _inner = inner; - _bytePool = bytePool; - _buffer = bytePool.Rent(bufferSize); + throw new ArgumentNullException(nameof(inner)); } - /// - /// The currently buffered data. - /// - public ArraySegment BufferedData - { - get { return new ArraySegment(_buffer, _bufferOffset, _bufferCount); } - } + _inner = inner; + _bytePool = bytePool; + _buffer = bytePool.Rent(bufferSize); + } - /// - public override bool CanRead - { - get { return _inner.CanRead || _bufferCount > 0; } - } + /// + /// The currently buffered data. + /// + public ArraySegment BufferedData + { + get { return new ArraySegment(_buffer, _bufferOffset, _bufferCount); } + } - /// - public override bool CanSeek - { - get { return _inner.CanSeek; } - } + /// + public override bool CanRead + { + get { return _inner.CanRead || _bufferCount > 0; } + } - /// - public override bool CanTimeout - { - get { return _inner.CanTimeout; } - } + /// + public override bool CanSeek + { + get { return _inner.CanSeek; } + } - /// - public override bool CanWrite - { - get { return _inner.CanWrite; } - } + /// + public override bool CanTimeout + { + get { return _inner.CanTimeout; } + } - /// - public override long Length - { - get { return _inner.Length; } - } + /// + public override bool CanWrite + { + get { return _inner.CanWrite; } + } + + /// + public override long Length + { + get { return _inner.Length; } + } - /// - public override long Position + /// + public override long Position + { + get { return _inner.Position - _bufferCount; } + set { - get { return _inner.Position - _bufferCount; } - set + if (value < 0) { - if (value < 0) - { - throw new ArgumentOutOfRangeException(nameof(value), value, "Position must be positive."); - } - if (value == Position) - { - return; - } + throw new ArgumentOutOfRangeException(nameof(value), value, "Position must be positive."); + } + if (value == Position) + { + return; + } - // Backwards? - if (value <= _inner.Position) + // Backwards? + if (value <= _inner.Position) + { + // Forward within the buffer? + var innerOffset = (int)(_inner.Position - value); + if (innerOffset <= _bufferCount) { - // Forward within the buffer? - var innerOffset = (int)(_inner.Position - value); - if (innerOffset <= _bufferCount) - { - // Yes, just skip some of the buffered data - _bufferOffset += innerOffset; - _bufferCount -= innerOffset; - } - else - { - // No, reset the buffer - _bufferOffset = 0; - _bufferCount = 0; - _inner.Position = value; - } + // Yes, just skip some of the buffered data + _bufferOffset += innerOffset; + _bufferCount -= innerOffset; } else { - // Forward, reset the buffer + // No, reset the buffer _bufferOffset = 0; _bufferCount = 0; _inner.Position = value; } } - } - - /// - public override long Seek(long offset, SeekOrigin origin) - { - if (origin == SeekOrigin.Begin) - { - Position = offset; - } - else if (origin == SeekOrigin.Current) + else { - Position = Position + offset; + // Forward, reset the buffer + _bufferOffset = 0; + _bufferCount = 0; + _inner.Position = value; } - else // if (origin == SeekOrigin.End) - { - Position = Length + offset; - } - return Position; } + } - /// - public override void SetLength(long value) + /// + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) { - _inner.SetLength(value); + Position = offset; } - - /// - protected override void Dispose(bool disposing) + else if (origin == SeekOrigin.Current) { - if (!_disposed) - { - _disposed = true; - _bytePool.Return(_buffer); - - if (disposing) - { - _inner.Dispose(); - } - } + Position = Position + offset; } - - /// - public override void Flush() + else // if (origin == SeekOrigin.End) { - _inner.Flush(); + Position = Length + offset; } + return Position; + } - /// - public override Task FlushAsync(CancellationToken cancellationToken) - { - return _inner.FlushAsync(cancellationToken); - } + /// + public override void SetLength(long value) + { + _inner.SetLength(value); + } - /// - public override void Write(byte[] buffer, int offset, int count) + /// + protected override void Dispose(bool disposing) + { + if (!_disposed) { - _inner.Write(buffer, offset, count); - } + _disposed = true; + _bytePool.Return(_buffer); - /// - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _inner.WriteAsync(buffer, offset, count, cancellationToken); + if (disposing) + { + _inner.Dispose(); + } } + } - /// - public override int Read(byte[] buffer, int offset, int count) - { - ValidateBuffer(buffer, offset, count); + /// + public override void Flush() + { + _inner.Flush(); + } - // Drain buffer - if (_bufferCount > 0) - { - int toCopy = Math.Min(_bufferCount, count); - Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); - _bufferOffset += toCopy; - _bufferCount -= toCopy; - return toCopy; - } + /// + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _inner.FlushAsync(cancellationToken); + } - return _inner.Read(buffer, offset, count); - } + /// + public override void Write(byte[] buffer, int offset, int count) + { + _inner.Write(buffer, offset, count); + } - /// - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.WriteAsync(buffer, offset, count, cancellationToken); + } + + /// + public override int Read(byte[] buffer, int offset, int count) + { + ValidateBuffer(buffer, offset, count); + + // Drain buffer + if (_bufferCount > 0) { - ValidateBuffer(buffer, offset, count); - return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + int toCopy = Math.Min(_bufferCount, count); + Buffer.BlockCopy(_buffer, _bufferOffset, buffer, offset, toCopy); + _bufferOffset += toCopy; + _bufferCount -= toCopy; + return toCopy; } - /// - public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) - { - // Drain buffer - if (_bufferCount > 0) - { - int toCopy = Math.Min(_bufferCount, buffer.Length); - _buffer.AsMemory(_bufferOffset, toCopy).CopyTo(buffer); - _bufferOffset += toCopy; - _bufferCount -= toCopy; - return toCopy; - } + return _inner.Read(buffer, offset, count); + } + + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ValidateBuffer(buffer, offset, count); + return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + } - return await _inner.ReadAsync(buffer, cancellationToken); + /// + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) + { + // Drain buffer + if (_bufferCount > 0) + { + int toCopy = Math.Min(_bufferCount, buffer.Length); + _buffer.AsMemory(_bufferOffset, toCopy).CopyTo(buffer); + _bufferOffset += toCopy; + _bufferCount -= toCopy; + return toCopy; } - /// - /// Ensures that the buffer is not empty. - /// - /// Returns true if the buffer is not empty; false otherwise. - public bool EnsureBuffered() + return await _inner.ReadAsync(buffer, cancellationToken); + } + + /// + /// Ensures that the buffer is not empty. + /// + /// Returns true if the buffer is not empty; false otherwise. + public bool EnsureBuffered() + { + if (_bufferCount > 0) { - if (_bufferCount > 0) - { - return true; - } - // Downshift to make room - _bufferOffset = 0; - _bufferCount = _inner.Read(_buffer, 0, _buffer.Length); - return _bufferCount > 0; + return true; } + // Downshift to make room + _bufferOffset = 0; + _bufferCount = _inner.Read(_buffer, 0, _buffer.Length); + return _bufferCount > 0; + } - /// - /// Ensures that the buffer is not empty. - /// - /// Cancellation token. - /// Returns true if the buffer is not empty; false otherwise. - public async Task EnsureBufferedAsync(CancellationToken cancellationToken) + /// + /// Ensures that the buffer is not empty. + /// + /// Cancellation token. + /// Returns true if the buffer is not empty; false otherwise. + public async Task EnsureBufferedAsync(CancellationToken cancellationToken) + { + if (_bufferCount > 0) { - if (_bufferCount > 0) - { - return true; - } - // Downshift to make room - _bufferOffset = 0; - _bufferCount = await _inner.ReadAsync(_buffer.AsMemory(), cancellationToken); - return _bufferCount > 0; + return true; } + // Downshift to make room + _bufferOffset = 0; + _bufferCount = await _inner.ReadAsync(_buffer.AsMemory(), cancellationToken); + return _bufferCount > 0; + } - /// - /// Ensures that a minimum amount of buffered data is available. - /// - /// Minimum amount of buffered data. - /// Returns true if the minimum amount of buffered data is available; false otherwise. - public bool EnsureBuffered(int minCount) + /// + /// Ensures that a minimum amount of buffered data is available. + /// + /// Minimum amount of buffered data. + /// Returns true if the minimum amount of buffered data is available; false otherwise. + public bool EnsureBuffered(int minCount) + { + if (minCount > _buffer.Length) { - if (minCount > _buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(minCount), minCount, "The value must be smaller than the buffer size: " + _buffer.Length); - } - while (_bufferCount < minCount) + throw new ArgumentOutOfRangeException(nameof(minCount), minCount, "The value must be smaller than the buffer size: " + _buffer.Length); + } + while (_bufferCount < minCount) + { + // Downshift to make room + if (_bufferOffset > 0) { - // Downshift to make room - if (_bufferOffset > 0) - { - if (_bufferCount > 0) - { - Buffer.BlockCopy(_buffer, _bufferOffset, _buffer, 0, _bufferCount); - } - _bufferOffset = 0; - } - int read = _inner.Read(_buffer, _bufferOffset + _bufferCount, _buffer.Length - _bufferCount - _bufferOffset); - _bufferCount += read; - if (read == 0) + if (_bufferCount > 0) { - return false; + Buffer.BlockCopy(_buffer, _bufferOffset, _buffer, 0, _bufferCount); } + _bufferOffset = 0; + } + int read = _inner.Read(_buffer, _bufferOffset + _bufferCount, _buffer.Length - _bufferCount - _bufferOffset); + _bufferCount += read; + if (read == 0) + { + return false; } - return true; } + return true; + } - /// - /// Ensures that a minimum amount of buffered data is available. - /// - /// Minimum amount of buffered data. - /// Cancellation token. - /// Returns true if the minimum amount of buffered data is available; false otherwise. - public async Task EnsureBufferedAsync(int minCount, CancellationToken cancellationToken) + /// + /// Ensures that a minimum amount of buffered data is available. + /// + /// Minimum amount of buffered data. + /// Cancellation token. + /// Returns true if the minimum amount of buffered data is available; false otherwise. + public async Task EnsureBufferedAsync(int minCount, CancellationToken cancellationToken) + { + if (minCount > _buffer.Length) { - if (minCount > _buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(minCount), minCount, "The value must be smaller than the buffer size: " + _buffer.Length); - } - while (_bufferCount < minCount) + throw new ArgumentOutOfRangeException(nameof(minCount), minCount, "The value must be smaller than the buffer size: " + _buffer.Length); + } + while (_bufferCount < minCount) + { + // Downshift to make room + if (_bufferOffset > 0) { - // Downshift to make room - if (_bufferOffset > 0) + if (_bufferCount > 0) { - if (_bufferCount > 0) - { - Buffer.BlockCopy(_buffer, _bufferOffset, _buffer, 0, _bufferCount); - } - _bufferOffset = 0; - } - int read = await _inner.ReadAsync(_buffer.AsMemory(_bufferOffset + _bufferCount, _buffer.Length - _bufferCount - _bufferOffset), cancellationToken); - _bufferCount += read; - if (read == 0) - { - return false; + Buffer.BlockCopy(_buffer, _bufferOffset, _buffer, 0, _bufferCount); } + _bufferOffset = 0; + } + int read = await _inner.ReadAsync(_buffer.AsMemory(_bufferOffset + _bufferCount, _buffer.Length - _bufferCount - _bufferOffset), cancellationToken); + _bufferCount += read; + if (read == 0) + { + return false; } - return true; } + return true; + } - /// - /// Reads a line. A line is defined as a sequence of characters followed by - /// a carriage return immediately followed by a line feed. The resulting string does not - /// contain the terminating carriage return and line feed. - /// - /// Maximum allowed line length. - /// A line. - public string ReadLine(int lengthLimit) + /// + /// Reads a line. A line is defined as a sequence of characters followed by + /// a carriage return immediately followed by a line feed. The resulting string does not + /// contain the terminating carriage return and line feed. + /// + /// Maximum allowed line length. + /// A line. + public string ReadLine(int lengthLimit) + { + CheckDisposed(); + using (var builder = new MemoryStream(200)) { - CheckDisposed(); - using (var builder = new MemoryStream(200)) - { - bool foundCR = false, foundCRLF = false; + bool foundCR = false, foundCRLF = false; - while (!foundCRLF && EnsureBuffered()) + while (!foundCRLF && EnsureBuffered()) + { + if (builder.Length > lengthLimit) { - if (builder.Length > lengthLimit) - { - throw new InvalidDataException($"Line length limit {lengthLimit} exceeded."); - } - ProcessLineChar(builder, ref foundCR, ref foundCRLF); + throw new InvalidDataException($"Line length limit {lengthLimit} exceeded."); } - - return DecodeLine(builder, foundCRLF); + ProcessLineChar(builder, ref foundCR, ref foundCRLF); } + + return DecodeLine(builder, foundCRLF); } + } - /// - /// Reads a line. A line is defined as a sequence of characters followed by - /// a carriage return immediately followed by a line feed. The resulting string does not - /// contain the terminating carriage return and line feed. - /// - /// Maximum allowed line length. - /// Cancellation token. - /// A line. - public async Task ReadLineAsync(int lengthLimit, CancellationToken cancellationToken) + /// + /// Reads a line. A line is defined as a sequence of characters followed by + /// a carriage return immediately followed by a line feed. The resulting string does not + /// contain the terminating carriage return and line feed. + /// + /// Maximum allowed line length. + /// Cancellation token. + /// A line. + public async Task ReadLineAsync(int lengthLimit, CancellationToken cancellationToken) + { + CheckDisposed(); + using (var builder = new MemoryStream(200)) { - CheckDisposed(); - using (var builder = new MemoryStream(200)) - { - bool foundCR = false, foundCRLF = false; + bool foundCR = false, foundCRLF = false; - while (!foundCRLF && await EnsureBufferedAsync(cancellationToken)) + while (!foundCRLF && await EnsureBufferedAsync(cancellationToken)) + { + if (builder.Length > lengthLimit) { - if (builder.Length > lengthLimit) - { - throw new InvalidDataException($"Line length limit {lengthLimit} exceeded."); - } - - ProcessLineChar(builder, ref foundCR, ref foundCRLF); + throw new InvalidDataException($"Line length limit {lengthLimit} exceeded."); } - return DecodeLine(builder, foundCRLF); + ProcessLineChar(builder, ref foundCR, ref foundCRLF); } - } - private void ProcessLineChar(MemoryStream builder, ref bool foundCR, ref bool foundCRLF) - { - var b = _buffer[_bufferOffset]; - builder.WriteByte(b); - _bufferOffset++; - _bufferCount--; - if (b == LF && foundCR) - { - foundCRLF = true; - return; - } - foundCR = b == CR; + return DecodeLine(builder, foundCRLF); } + } - private static string DecodeLine(MemoryStream builder, bool foundCRLF) + private void ProcessLineChar(MemoryStream builder, ref bool foundCR, ref bool foundCRLF) + { + var b = _buffer[_bufferOffset]; + builder.WriteByte(b); + _bufferOffset++; + _bufferCount--; + if (b == LF && foundCR) { - // Drop the final CRLF, if any - var length = foundCRLF ? builder.Length - 2 : builder.Length; - return Encoding.UTF8.GetString(builder.ToArray(), 0, (int)length); + foundCRLF = true; + return; } + foundCR = b == CR; + } + + private static string DecodeLine(MemoryStream builder, bool foundCRLF) + { + // Drop the final CRLF, if any + var length = foundCRLF ? builder.Length - 2 : builder.Length; + return Encoding.UTF8.GetString(builder.ToArray(), 0, (int)length); + } - private void CheckDisposed() + private void CheckDisposed() + { + if (_disposed) { - if (_disposed) - { - throw new ObjectDisposedException(nameof(BufferedReadStream)); - } + throw new ObjectDisposedException(nameof(BufferedReadStream)); } + } - private static void ValidateBuffer(byte[] buffer, int offset, int count) + private static void ValidateBuffer(byte[] buffer, int offset, int count) + { + // Delegate most of our validation. + var ignored = new ArraySegment(buffer, offset, count); + if (count == 0) { - // Delegate most of our validation. - var ignored = new ArraySegment(buffer, offset, count); - if (count == 0) - { - throw new ArgumentOutOfRangeException(nameof(count), "The value must be greater than zero."); - } + throw new ArgumentOutOfRangeException(nameof(count), "The value must be greater than zero."); } } } diff --git a/src/Http/WebUtilities/src/FileBufferingReadStream.cs b/src/Http/WebUtilities/src/FileBufferingReadStream.cs index 720b8921b5..96133c39e8 100644 --- a/src/Http/WebUtilities/src/FileBufferingReadStream.cs +++ b/src/Http/WebUtilities/src/FileBufferingReadStream.cs @@ -10,498 +10,497 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Internal; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +/// +/// A Stream that wraps another stream and enables rewinding by buffering the content as it is read. +/// The content is buffered in memory up to a certain size and then spooled to a temp file on disk. +/// The temp file will be deleted on Dispose. +/// +public class FileBufferingReadStream : Stream { + private const int _maxRentedBufferSize = 1024 * 1024; // 1MB + private readonly Stream _inner; + private readonly ArrayPool _bytePool; + private readonly int _memoryThreshold; + private readonly long? _bufferLimit; + private string? _tempFileDirectory; + private readonly Func? _tempFileDirectoryAccessor; + private string? _tempFileName; + + private Stream _buffer; + private byte[]? _rentedBuffer; + private bool _inMemory = true; + private bool _completelyBuffered; + + private bool _disposed; + /// - /// A Stream that wraps another stream and enables rewinding by buffering the content as it is read. - /// The content is buffered in memory up to a certain size and then spooled to a temp file on disk. - /// The temp file will be deleted on Dispose. + /// Initializes a new instance of . /// - public class FileBufferingReadStream : Stream + /// The wrapping . + /// The maximum size to buffer in memory. + public FileBufferingReadStream(Stream inner, int memoryThreshold) + : this(inner, memoryThreshold, bufferLimit: null, tempFileDirectoryAccessor: AspNetCoreTempDirectory.TempDirectoryFactory) { - private const int _maxRentedBufferSize = 1024 * 1024; // 1MB - private readonly Stream _inner; - private readonly ArrayPool _bytePool; - private readonly int _memoryThreshold; - private readonly long? _bufferLimit; - private string? _tempFileDirectory; - private readonly Func? _tempFileDirectoryAccessor; - private string? _tempFileName; - - private Stream _buffer; - private byte[]? _rentedBuffer; - private bool _inMemory = true; - private bool _completelyBuffered; - - private bool _disposed; - - /// - /// Initializes a new instance of . - /// - /// The wrapping . - /// The maximum size to buffer in memory. - public FileBufferingReadStream(Stream inner, int memoryThreshold) - : this(inner, memoryThreshold, bufferLimit: null, tempFileDirectoryAccessor: AspNetCoreTempDirectory.TempDirectoryFactory) - { - } + } + + /// + /// Initializes a new instance of . + /// + /// The wrapping . + /// The maximum size to buffer in memory. + /// The maximum size that will be buffered before this throws. + /// Provides the temporary directory to which files are buffered to. + public FileBufferingReadStream( + Stream inner, + int memoryThreshold, + long? bufferLimit, + Func tempFileDirectoryAccessor) + : this(inner, memoryThreshold, bufferLimit, tempFileDirectoryAccessor, ArrayPool.Shared) + { + } - /// - /// Initializes a new instance of . - /// - /// The wrapping . - /// The maximum size to buffer in memory. - /// The maximum size that will be buffered before this throws. - /// Provides the temporary directory to which files are buffered to. - public FileBufferingReadStream( - Stream inner, - int memoryThreshold, - long? bufferLimit, - Func tempFileDirectoryAccessor) - : this(inner, memoryThreshold, bufferLimit, tempFileDirectoryAccessor, ArrayPool.Shared) + /// + /// Initializes a new instance of . + /// + /// The wrapping . + /// The maximum size to buffer in memory. + /// The maximum size that will be buffered before this throws. + /// Provides the temporary directory to which files are buffered to. + /// The to use. + public FileBufferingReadStream( + Stream inner, + int memoryThreshold, + long? bufferLimit, + Func tempFileDirectoryAccessor, + ArrayPool bytePool) + { + if (inner == null) { + throw new ArgumentNullException(nameof(inner)); } - /// - /// Initializes a new instance of . - /// - /// The wrapping . - /// The maximum size to buffer in memory. - /// The maximum size that will be buffered before this throws. - /// Provides the temporary directory to which files are buffered to. - /// The to use. - public FileBufferingReadStream( - Stream inner, - int memoryThreshold, - long? bufferLimit, - Func tempFileDirectoryAccessor, - ArrayPool bytePool) + if (tempFileDirectoryAccessor == null) { - if (inner == null) - { - throw new ArgumentNullException(nameof(inner)); - } - - if (tempFileDirectoryAccessor == null) - { - throw new ArgumentNullException(nameof(tempFileDirectoryAccessor)); - } - - _bytePool = bytePool; - if (memoryThreshold <= _maxRentedBufferSize) - { - _rentedBuffer = bytePool.Rent(memoryThreshold); - _buffer = new MemoryStream(_rentedBuffer); - _buffer.SetLength(0); - } - else - { - _buffer = new MemoryStream(); - } - - _inner = inner; - _memoryThreshold = memoryThreshold; - _bufferLimit = bufferLimit; - _tempFileDirectoryAccessor = tempFileDirectoryAccessor; + throw new ArgumentNullException(nameof(tempFileDirectoryAccessor)); } - /// - /// Initializes a new instance of . - /// - /// The wrapping . - /// The maximum size to buffer in memory. - /// The maximum size that will be buffered before this throws. - /// The temporary directory to which files are buffered to. - public FileBufferingReadStream( - Stream inner, - int memoryThreshold, - long? bufferLimit, - string tempFileDirectory) - : this(inner, memoryThreshold, bufferLimit, tempFileDirectory, ArrayPool.Shared) + _bytePool = bytePool; + if (memoryThreshold <= _maxRentedBufferSize) { + _rentedBuffer = bytePool.Rent(memoryThreshold); + _buffer = new MemoryStream(_rentedBuffer); + _buffer.SetLength(0); } - - /// - /// Initializes a new instance of . - /// - /// The wrapping . - /// The maximum size to buffer in memory. - /// The maximum size that will be buffered before this throws. - /// The temporary directory to which files are buffered to. - /// The to use. - public FileBufferingReadStream( - Stream inner, - int memoryThreshold, - long? bufferLimit, - string tempFileDirectory, - ArrayPool bytePool) + else { - if (inner == null) - { - throw new ArgumentNullException(nameof(inner)); - } + _buffer = new MemoryStream(); + } - if (tempFileDirectory == null) - { - throw new ArgumentNullException(nameof(tempFileDirectory)); - } + _inner = inner; + _memoryThreshold = memoryThreshold; + _bufferLimit = bufferLimit; + _tempFileDirectoryAccessor = tempFileDirectoryAccessor; + } - _bytePool = bytePool; - if (memoryThreshold <= _maxRentedBufferSize) - { - _rentedBuffer = bytePool.Rent(memoryThreshold); - _buffer = new MemoryStream(_rentedBuffer); - _buffer.SetLength(0); - } - else - { - _buffer = new MemoryStream(); - } + /// + /// Initializes a new instance of . + /// + /// The wrapping . + /// The maximum size to buffer in memory. + /// The maximum size that will be buffered before this throws. + /// The temporary directory to which files are buffered to. + public FileBufferingReadStream( + Stream inner, + int memoryThreshold, + long? bufferLimit, + string tempFileDirectory) + : this(inner, memoryThreshold, bufferLimit, tempFileDirectory, ArrayPool.Shared) + { + } - _inner = inner; - _memoryThreshold = memoryThreshold; - _bufferLimit = bufferLimit; - _tempFileDirectory = tempFileDirectory; + /// + /// Initializes a new instance of . + /// + /// The wrapping . + /// The maximum size to buffer in memory. + /// The maximum size that will be buffered before this throws. + /// The temporary directory to which files are buffered to. + /// The to use. + public FileBufferingReadStream( + Stream inner, + int memoryThreshold, + long? bufferLimit, + string tempFileDirectory, + ArrayPool bytePool) + { + if (inner == null) + { + throw new ArgumentNullException(nameof(inner)); } - /// - /// The maximum amount of memory in bytes to allocate before switching to a file on disk. - /// - /// - /// Defaults to 32kb. - /// - public int MemoryThreshold => _memoryThreshold; - - /// - /// Gets a value that determines if the contents are buffered entirely in memory. - /// - public bool InMemory + if (tempFileDirectory == null) { - get { return _inMemory; } + throw new ArgumentNullException(nameof(tempFileDirectory)); } - /// - /// Gets a value that determines where the contents are buffered on disk. - /// - public string? TempFileName + _bytePool = bytePool; + if (memoryThreshold <= _maxRentedBufferSize) { - get { return _tempFileName; } + _rentedBuffer = bytePool.Rent(memoryThreshold); + _buffer = new MemoryStream(_rentedBuffer); + _buffer.SetLength(0); } - - /// - public override bool CanRead + else { - get { return true; } + _buffer = new MemoryStream(); } - /// - public override bool CanSeek + _inner = inner; + _memoryThreshold = memoryThreshold; + _bufferLimit = bufferLimit; + _tempFileDirectory = tempFileDirectory; + } + + /// + /// The maximum amount of memory in bytes to allocate before switching to a file on disk. + /// + /// + /// Defaults to 32kb. + /// + public int MemoryThreshold => _memoryThreshold; + + /// + /// Gets a value that determines if the contents are buffered entirely in memory. + /// + public bool InMemory + { + get { return _inMemory; } + } + + /// + /// Gets a value that determines where the contents are buffered on disk. + /// + public string? TempFileName + { + get { return _tempFileName; } + } + + /// + public override bool CanRead + { + get { return true; } + } + + /// + public override bool CanSeek + { + get { return true; } + } + + /// + public override bool CanWrite + { + get { return false; } + } + + /// + /// The total bytes read from and buffered by the stream so far, it will not represent the full + /// data length until the stream is fully buffered. e.g. using stream.DrainAsync(). + /// + public override long Length + { + get { return _buffer.Length; } + } + + /// + public override long Position + { + get { return _buffer.Position; } + // Note this will not allow seeking forward beyond the end of the buffer. + set { - get { return true; } + ThrowIfDisposed(); + _buffer.Position = value; } + } - /// - public override bool CanWrite + /// + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + if (!_completelyBuffered && origin == SeekOrigin.End) { - get { return false; } + // Can't seek from the end until we've finished consuming the inner stream + throw new NotSupportedException("The content has not been fully buffered yet."); } - - /// - /// The total bytes read from and buffered by the stream so far, it will not represent the full - /// data length until the stream is fully buffered. e.g. using stream.DrainAsync(). - /// - public override long Length + else if (!_completelyBuffered && origin == SeekOrigin.Current && offset + Position > Length) { - get { return _buffer.Length; } + // Can't seek past the end of the buffer until we've finished consuming the inner stream + throw new NotSupportedException("The content has not been fully buffered yet."); } - - /// - public override long Position + else if (!_completelyBuffered && origin == SeekOrigin.Begin && offset > Length) { - get { return _buffer.Position; } - // Note this will not allow seeking forward beyond the end of the buffer. - set - { - ThrowIfDisposed(); - _buffer.Position = value; - } + // Can't seek past the end of the buffer until we've finished consuming the inner stream + throw new NotSupportedException("The content has not been fully buffered yet."); } + return _buffer.Seek(offset, origin); + } - /// - public override long Seek(long offset, SeekOrigin origin) + private Stream CreateTempFile() + { + if (_tempFileDirectory == null) { - ThrowIfDisposed(); - if (!_completelyBuffered && origin == SeekOrigin.End) - { - // Can't seek from the end until we've finished consuming the inner stream - throw new NotSupportedException("The content has not been fully buffered yet."); - } - else if (!_completelyBuffered && origin == SeekOrigin.Current && offset + Position > Length) - { - // Can't seek past the end of the buffer until we've finished consuming the inner stream - throw new NotSupportedException("The content has not been fully buffered yet."); - } - else if (!_completelyBuffered && origin == SeekOrigin.Begin && offset > Length) - { - // Can't seek past the end of the buffer until we've finished consuming the inner stream - throw new NotSupportedException("The content has not been fully buffered yet."); - } - return _buffer.Seek(offset, origin); + Debug.Assert(_tempFileDirectoryAccessor != null); + _tempFileDirectory = _tempFileDirectoryAccessor(); + Debug.Assert(_tempFileDirectory != null); } - private Stream CreateTempFile() - { - if (_tempFileDirectory == null) - { - Debug.Assert(_tempFileDirectoryAccessor != null); - _tempFileDirectory = _tempFileDirectoryAccessor(); - Debug.Assert(_tempFileDirectory != null); - } + _tempFileName = Path.Combine(_tempFileDirectory, "ASPNETCORE_" + Guid.NewGuid().ToString() + ".tmp"); + return new FileStream(_tempFileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Delete, 1024 * 16, + FileOptions.Asynchronous | FileOptions.DeleteOnClose | FileOptions.SequentialScan); + } - _tempFileName = Path.Combine(_tempFileDirectory, "ASPNETCORE_" + Guid.NewGuid().ToString() + ".tmp"); - return new FileStream(_tempFileName, FileMode.Create, FileAccess.ReadWrite, FileShare.Delete, 1024 * 16, - FileOptions.Asynchronous | FileOptions.DeleteOnClose | FileOptions.SequentialScan); - } + /// + public override int Read(Span buffer) + { + ThrowIfDisposed(); - /// - public override int Read(Span buffer) + if (_buffer.Position < _buffer.Length || _completelyBuffered) { - ThrowIfDisposed(); - - if (_buffer.Position < _buffer.Length || _completelyBuffered) - { - // Just read from the buffer - return _buffer.Read(buffer); - } + // Just read from the buffer + return _buffer.Read(buffer); + } - var read = _inner.Read(buffer); + var read = _inner.Read(buffer); - if (_bufferLimit.HasValue && _bufferLimit - read < _buffer.Length) - { - throw new IOException("Buffer limit exceeded."); - } + if (_bufferLimit.HasValue && _bufferLimit - read < _buffer.Length) + { + throw new IOException("Buffer limit exceeded."); + } - // We're about to go over the threshold, switch to a file - if (_inMemory && _memoryThreshold - read < _buffer.Length) + // We're about to go over the threshold, switch to a file + if (_inMemory && _memoryThreshold - read < _buffer.Length) + { + _inMemory = false; + var oldBuffer = _buffer; + _buffer = CreateTempFile(); + if (_rentedBuffer == null) { - _inMemory = false; - var oldBuffer = _buffer; - _buffer = CreateTempFile(); - if (_rentedBuffer == null) + // Copy data from the in memory buffer to the file stream using a pooled buffer + oldBuffer.Position = 0; + var rentedBuffer = _bytePool.Rent(Math.Min((int)oldBuffer.Length, _maxRentedBufferSize)); + try { - // Copy data from the in memory buffer to the file stream using a pooled buffer - oldBuffer.Position = 0; - var rentedBuffer = _bytePool.Rent(Math.Min((int)oldBuffer.Length, _maxRentedBufferSize)); - try + var copyRead = oldBuffer.Read(rentedBuffer); + while (copyRead > 0) { - var copyRead = oldBuffer.Read(rentedBuffer); - while (copyRead > 0) - { - _buffer.Write(rentedBuffer.AsSpan(0, copyRead)); - copyRead = oldBuffer.Read(rentedBuffer); - } - } - finally - { - _bytePool.Return(rentedBuffer); + _buffer.Write(rentedBuffer.AsSpan(0, copyRead)); + copyRead = oldBuffer.Read(rentedBuffer); } } - else + finally { - _buffer.Write(_rentedBuffer.AsSpan(0, (int)oldBuffer.Length)); - _bytePool.Return(_rentedBuffer); - _rentedBuffer = null; + _bytePool.Return(rentedBuffer); } } - - if (read > 0) - { - _buffer.Write(buffer.Slice(0, read)); - } else { - _completelyBuffered = true; + _buffer.Write(_rentedBuffer.AsSpan(0, (int)oldBuffer.Length)); + _bytePool.Return(_rentedBuffer); + _rentedBuffer = null; } - - return read; } - /// - public override int Read(byte[] buffer, int offset, int count) + if (read > 0) { - return Read(buffer.AsSpan(offset, count)); + _buffer.Write(buffer.Slice(0, read)); } - - /// - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + else { - return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + _completelyBuffered = true; } - /// - [SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads.", Justification = "Required to maintain compatibility")] - public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) - { - ThrowIfDisposed(); + return read; + } - if (_buffer.Position < _buffer.Length || _completelyBuffered) - { - // Just read from the buffer - return await _buffer.ReadAsync(buffer, cancellationToken); - } + /// + public override int Read(byte[] buffer, int offset, int count) + { + return Read(buffer.AsSpan(offset, count)); + } - var read = await _inner.ReadAsync(buffer, cancellationToken); + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + } - if (_bufferLimit.HasValue && _bufferLimit - read < _buffer.Length) - { - throw new IOException("Buffer limit exceeded."); - } + /// + [SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads.", Justification = "Required to maintain compatibility")] + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + if (_buffer.Position < _buffer.Length || _completelyBuffered) + { + // Just read from the buffer + return await _buffer.ReadAsync(buffer, cancellationToken); + } + + var read = await _inner.ReadAsync(buffer, cancellationToken); - if (_inMemory && _memoryThreshold - read < _buffer.Length) + if (_bufferLimit.HasValue && _bufferLimit - read < _buffer.Length) + { + throw new IOException("Buffer limit exceeded."); + } + + if (_inMemory && _memoryThreshold - read < _buffer.Length) + { + _inMemory = false; + var oldBuffer = _buffer; + _buffer = CreateTempFile(); + if (_rentedBuffer == null) { - _inMemory = false; - var oldBuffer = _buffer; - _buffer = CreateTempFile(); - if (_rentedBuffer == null) + oldBuffer.Position = 0; + var rentedBuffer = _bytePool.Rent(Math.Min((int)oldBuffer.Length, _maxRentedBufferSize)); + try { - oldBuffer.Position = 0; - var rentedBuffer = _bytePool.Rent(Math.Min((int)oldBuffer.Length, _maxRentedBufferSize)); - try - { - // oldBuffer is a MemoryStream, no need to do async reads. - var copyRead = oldBuffer.Read(rentedBuffer); - while (copyRead > 0) - { - await _buffer.WriteAsync(rentedBuffer.AsMemory(0, copyRead), cancellationToken); - copyRead = oldBuffer.Read(rentedBuffer); - } - } - finally + // oldBuffer is a MemoryStream, no need to do async reads. + var copyRead = oldBuffer.Read(rentedBuffer); + while (copyRead > 0) { - _bytePool.Return(rentedBuffer); + await _buffer.WriteAsync(rentedBuffer.AsMemory(0, copyRead), cancellationToken); + copyRead = oldBuffer.Read(rentedBuffer); } } - else + finally { - await _buffer.WriteAsync(_rentedBuffer.AsMemory(0, (int)oldBuffer.Length), cancellationToken); - _bytePool.Return(_rentedBuffer); - _rentedBuffer = null; + _bytePool.Return(rentedBuffer); } } - - if (read > 0) - { - await _buffer.WriteAsync(buffer.Slice(0, read), cancellationToken); - } else { - _completelyBuffered = true; + await _buffer.WriteAsync(_rentedBuffer.AsMemory(0, (int)oldBuffer.Length), cancellationToken); + _bytePool.Return(_rentedBuffer); + _rentedBuffer = null; } - - return read; } - /// - public override void Write(byte[] buffer, int offset, int count) + if (read > 0) { - throw new NotSupportedException(); + await _buffer.WriteAsync(buffer.Slice(0, read), cancellationToken); } - - /// - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + else { - throw new NotSupportedException(); + _completelyBuffered = true; } - /// - public override void SetLength(long value) - { - throw new NotSupportedException(); - } + return read; + } + + /// + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + /// + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + public override void Flush() + { + throw new NotSupportedException(); + } + + /// + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + // Set a minimum buffer size of 4K since the base Stream implementation has weird behavior when the stream is + // seekable *and* the length is 0 (it passes in a buffer size of 1). + // See https://github.com/dotnet/runtime/blob/222415c56c9ea73530444768c0e68413eb374f5d/src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs#L164-L184 + bufferSize = Math.Max(4096, bufferSize); - /// - public override void Flush() + // If we're completed buffered then copy from the underlying source + if (_completelyBuffered) { - throw new NotSupportedException(); + return _buffer.CopyToAsync(destination, bufferSize, cancellationToken); } - /// - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + async Task CopyToAsyncImpl() { - // Set a minimum buffer size of 4K since the base Stream implementation has weird behavior when the stream is - // seekable *and* the length is 0 (it passes in a buffer size of 1). - // See https://github.com/dotnet/runtime/blob/222415c56c9ea73530444768c0e68413eb374f5d/src/libraries/System.Private.CoreLib/src/System/IO/Stream.cs#L164-L184 - bufferSize = Math.Max(4096, bufferSize); - - // If we're completed buffered then copy from the underlying source - if (_completelyBuffered) - { - return _buffer.CopyToAsync(destination, bufferSize, cancellationToken); - } - - async Task CopyToAsyncImpl() + // At least a 4K buffer + byte[] buffer = _bytePool.Rent(bufferSize); + try { - // At least a 4K buffer - byte[] buffer = _bytePool.Rent(bufferSize); - try + while (true) { - while (true) + int bytesRead = await ReadAsync(buffer, cancellationToken); + if (bytesRead == 0) { - int bytesRead = await ReadAsync(buffer, cancellationToken); - if (bytesRead == 0) - { - break; - } - await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken); + break; } - } - finally - { - _bytePool.Return(buffer); + await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken); } } - - return CopyToAsyncImpl(); + finally + { + _bytePool.Return(buffer); + } } - /// - protected override void Dispose(bool disposing) + return CopyToAsyncImpl(); + } + + /// + protected override void Dispose(bool disposing) + { + if (!_disposed) { - if (!_disposed) + _disposed = true; + if (_rentedBuffer != null) { - _disposed = true; - if (_rentedBuffer != null) - { - _bytePool.Return(_rentedBuffer); - } + _bytePool.Return(_rentedBuffer); + } - if (disposing) - { - _buffer.Dispose(); - } + if (disposing) + { + _buffer.Dispose(); } } + } - /// - public override async ValueTask DisposeAsync() + /// + public override async ValueTask DisposeAsync() + { + if (!_disposed) { - if (!_disposed) + _disposed = true; + if (_rentedBuffer != null) { - _disposed = true; - if (_rentedBuffer != null) - { - _bytePool.Return(_rentedBuffer); - } - - await _buffer.DisposeAsync(); + _bytePool.Return(_rentedBuffer); } + + await _buffer.DisposeAsync(); } + } - private void ThrowIfDisposed() + private void ThrowIfDisposed() + { + if (_disposed) { - if (_disposed) - { - throw new ObjectDisposedException(nameof(FileBufferingReadStream)); - } + throw new ObjectDisposedException(nameof(FileBufferingReadStream)); } } } diff --git a/src/Http/WebUtilities/src/FileBufferingWriteStream.cs b/src/Http/WebUtilities/src/FileBufferingWriteStream.cs index 28c4c27ab7..f3512522ce 100644 --- a/src/Http/WebUtilities/src/FileBufferingWriteStream.cs +++ b/src/Http/WebUtilities/src/FileBufferingWriteStream.cs @@ -11,305 +11,304 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Internal; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +/// +/// A that buffers content to be written to disk. Use +/// to write buffered content to a target . +/// +public sealed class FileBufferingWriteStream : Stream { + private const int DefaultMemoryThreshold = 32 * 1024; // 32k + + private readonly int _memoryThreshold; + private readonly long? _bufferLimit; + private readonly Func _tempFileDirectoryAccessor; + /// - /// A that buffers content to be written to disk. Use - /// to write buffered content to a target . + /// Initializes a new instance of . /// - public sealed class FileBufferingWriteStream : Stream + /// + /// The maximum amount of memory in bytes to allocate before switching to a file on disk. + /// Defaults to 32kb. + /// + /// + /// The maximum amount of bytes that the is allowed to buffer. + /// + /// Provides the location of the directory to write buffered contents to. + /// When unspecified, uses the value specified by the environment variable ASPNETCORE_TEMP if available, otherwise + /// uses the value returned by . + /// + public FileBufferingWriteStream( + int memoryThreshold = DefaultMemoryThreshold, + long? bufferLimit = null, + Func? tempFileDirectoryAccessor = null) { - private const int DefaultMemoryThreshold = 32 * 1024; // 32k - - private readonly int _memoryThreshold; - private readonly long? _bufferLimit; - private readonly Func _tempFileDirectoryAccessor; - - /// - /// Initializes a new instance of . - /// - /// - /// The maximum amount of memory in bytes to allocate before switching to a file on disk. - /// Defaults to 32kb. - /// - /// - /// The maximum amount of bytes that the is allowed to buffer. - /// - /// Provides the location of the directory to write buffered contents to. - /// When unspecified, uses the value specified by the environment variable ASPNETCORE_TEMP if available, otherwise - /// uses the value returned by . - /// - public FileBufferingWriteStream( - int memoryThreshold = DefaultMemoryThreshold, - long? bufferLimit = null, - Func? tempFileDirectoryAccessor = null) + if (memoryThreshold < 0) + { + throw new ArgumentOutOfRangeException(nameof(memoryThreshold)); + } + + if (bufferLimit != null && bufferLimit < memoryThreshold) { - if (memoryThreshold < 0) - { - throw new ArgumentOutOfRangeException(nameof(memoryThreshold)); - } - - if (bufferLimit != null && bufferLimit < memoryThreshold) - { - // We would expect a limit at least as much as memoryThreshold - throw new ArgumentOutOfRangeException(nameof(bufferLimit), $"{nameof(bufferLimit)} must be larger than {nameof(memoryThreshold)}."); - } - - _memoryThreshold = memoryThreshold; - _bufferLimit = bufferLimit; - _tempFileDirectoryAccessor = tempFileDirectoryAccessor ?? AspNetCoreTempDirectory.TempDirectoryFactory; - PagedByteBuffer = new PagedByteBuffer(ArrayPool.Shared); + // We would expect a limit at least as much as memoryThreshold + throw new ArgumentOutOfRangeException(nameof(bufferLimit), $"{nameof(bufferLimit)} must be larger than {nameof(memoryThreshold)}."); } - /// - /// The maximum amount of memory in bytes to allocate before switching to a file on disk. - /// - /// - /// Defaults to 32kb. - /// - public int MemoryThreshold => _memoryThreshold; + _memoryThreshold = memoryThreshold; + _bufferLimit = bufferLimit; + _tempFileDirectoryAccessor = tempFileDirectoryAccessor ?? AspNetCoreTempDirectory.TempDirectoryFactory; + PagedByteBuffer = new PagedByteBuffer(ArrayPool.Shared); + } + + /// + /// The maximum amount of memory in bytes to allocate before switching to a file on disk. + /// + /// + /// Defaults to 32kb. + /// + public int MemoryThreshold => _memoryThreshold; - /// - public override bool CanRead => false; + /// + public override bool CanRead => false; - /// - public override bool CanSeek => false; + /// + public override bool CanSeek => false; - /// - public override bool CanWrite => true; + /// + public override bool CanWrite => true; - /// - public override long Length => PagedByteBuffer.Length + (FileStream?.Length ?? 0); + /// + public override long Length => PagedByteBuffer.Length + (FileStream?.Length ?? 0); - /// - public override long Position - { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); - } + /// + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } - internal PagedByteBuffer PagedByteBuffer { get; } + internal PagedByteBuffer PagedByteBuffer { get; } - internal FileStream? FileStream { get; private set; } + internal FileStream? FileStream { get; private set; } - internal bool Disposed { get; private set; } + internal bool Disposed { get; private set; } - /// - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + /// + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); - /// - public override int Read(byte[] buffer, int offset, int count) - => throw new NotSupportedException(); + /// + public override int Read(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); - /// - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - => throw new NotSupportedException(); + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => throw new NotSupportedException(); + + /// + public override void Write(byte[] buffer, int offset, int count) + { + ThrowArgumentException(buffer, offset, count); + ThrowIfDisposed(); - /// - public override void Write(byte[] buffer, int offset, int count) + if (_bufferLimit.HasValue && _bufferLimit - Length < count) { - ThrowArgumentException(buffer, offset, count); - ThrowIfDisposed(); - - if (_bufferLimit.HasValue && _bufferLimit - Length < count) - { - Dispose(); - throw new IOException("Buffer limit exceeded."); - } - - // Allow buffering in memory if we're below the memory threshold once the current buffer is written. - var allowMemoryBuffer = (_memoryThreshold - count) >= PagedByteBuffer.Length; - if (allowMemoryBuffer) - { - // Buffer content in the MemoryStream if it has capacity. - PagedByteBuffer.Add(buffer, offset, count); - Debug.Assert(PagedByteBuffer.Length <= _memoryThreshold); - } - else - { - // If the MemoryStream is incapable of accommodating the content to be written - // spool to disk. - EnsureFileStream(); - - // Spool memory content to disk. - PagedByteBuffer.MoveTo(FileStream); - - FileStream.Write(buffer, offset, count); - } + Dispose(); + throw new IOException("Buffer limit exceeded."); } - /// - public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + // Allow buffering in memory if we're below the memory threshold once the current buffer is written. + var allowMemoryBuffer = (_memoryThreshold - count) >= PagedByteBuffer.Length; + if (allowMemoryBuffer) { - ThrowArgumentException(buffer, offset, count); - await WriteAsync(buffer.AsMemory(offset, count), cancellationToken); + // Buffer content in the MemoryStream if it has capacity. + PagedByteBuffer.Add(buffer, offset, count); + Debug.Assert(PagedByteBuffer.Length <= _memoryThreshold); } + else + { + // If the MemoryStream is incapable of accommodating the content to be written + // spool to disk. + EnsureFileStream(); + + // Spool memory content to disk. + PagedByteBuffer.MoveTo(FileStream); + + FileStream.Write(buffer, offset, count); + } + } + + /// + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + ThrowArgumentException(buffer, offset, count); + await WriteAsync(buffer.AsMemory(offset, count), cancellationToken); + } - /// - [SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads", Justification = "This is a method overload.")] - public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + /// + [SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads", Justification = "This is a method overload.")] + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + if (_bufferLimit.HasValue && _bufferLimit - Length < buffer.Length) { - ThrowIfDisposed(); - - if (_bufferLimit.HasValue && _bufferLimit - Length < buffer.Length) - { - Dispose(); - throw new IOException("Buffer limit exceeded."); - } - - // Allow buffering in memory if we're below the memory threshold once the current buffer is written. - var allowMemoryBuffer = (_memoryThreshold - buffer.Length) >= PagedByteBuffer.Length; - if (allowMemoryBuffer) - { - // Buffer content in the MemoryStream if it has capacity. - PagedByteBuffer.Add(buffer); - Debug.Assert(PagedByteBuffer.Length <= _memoryThreshold); - } - else - { - // If the MemoryStream is incapable of accommodating the content to be written - // spool to disk. - EnsureFileStream(); - - // Spool memory content to disk. - await PagedByteBuffer.MoveToAsync(FileStream, cancellationToken); - await FileStream.WriteAsync(buffer, cancellationToken); - } + Dispose(); + throw new IOException("Buffer limit exceeded."); } - /// - public override void Flush() + // Allow buffering in memory if we're below the memory threshold once the current buffer is written. + var allowMemoryBuffer = (_memoryThreshold - buffer.Length) >= PagedByteBuffer.Length; + if (allowMemoryBuffer) { - // Do nothing. + // Buffer content in the MemoryStream if it has capacity. + PagedByteBuffer.Add(buffer); + Debug.Assert(PagedByteBuffer.Length <= _memoryThreshold); } + else + { + // If the MemoryStream is incapable of accommodating the content to be written + // spool to disk. + EnsureFileStream(); - /// - public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; + // Spool memory content to disk. + await PagedByteBuffer.MoveToAsync(FileStream, cancellationToken); + await FileStream.WriteAsync(buffer, cancellationToken); + } + } - /// - public override void SetLength(long value) => throw new NotSupportedException(); + /// + public override void Flush() + { + // Do nothing. + } - /// - /// Drains buffered content to . - /// - /// The to drain buffered contents to. - /// The . - /// A that represents the asynchronous drain operation. - public async Task DrainBufferAsync(Stream destination, CancellationToken cancellationToken = default) - { - // When not null, FileStream always has "older" spooled content. The PagedByteBuffer always has "newer" - // unspooled content. Copy the FileStream content first when available. - if (FileStream != null) - { - // We make a new stream for async reads from disk and async writes to the destination - await using var readStream = new FileStream(FileStream.Name, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.ReadWrite, bufferSize: 1, useAsync: true); + /// + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public override void SetLength(long value) => throw new NotSupportedException(); - await readStream.CopyToAsync(destination, cancellationToken); + /// + /// Drains buffered content to . + /// + /// The to drain buffered contents to. + /// The . + /// A that represents the asynchronous drain operation. + public async Task DrainBufferAsync(Stream destination, CancellationToken cancellationToken = default) + { + // When not null, FileStream always has "older" spooled content. The PagedByteBuffer always has "newer" + // unspooled content. Copy the FileStream content first when available. + if (FileStream != null) + { + // We make a new stream for async reads from disk and async writes to the destination + await using var readStream = new FileStream(FileStream.Name, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.ReadWrite, bufferSize: 1, useAsync: true); - // This is created with delete on close - await FileStream.DisposeAsync(); - FileStream = null; - } + await readStream.CopyToAsync(destination, cancellationToken); - await PagedByteBuffer.MoveToAsync(destination, cancellationToken); + // This is created with delete on close + await FileStream.DisposeAsync(); + FileStream = null; } - /// - /// Drains buffered content to . - /// - /// The to drain buffered contents to. - /// The . - /// A that represents the asynchronous drain operation. - [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] - public async Task DrainBufferAsync(PipeWriter destination, CancellationToken cancellationToken = default) + await PagedByteBuffer.MoveToAsync(destination, cancellationToken); + } + + /// + /// Drains buffered content to . + /// + /// The to drain buffered contents to. + /// The . + /// A that represents the asynchronous drain operation. + [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")] + public async Task DrainBufferAsync(PipeWriter destination, CancellationToken cancellationToken = default) + { + // When not null, FileStream always has "older" spooled content. The PagedByteBuffer always has "newer" + // unspooled content. Copy the FileStream content first when available. + if (FileStream != null) { - // When not null, FileStream always has "older" spooled content. The PagedByteBuffer always has "newer" - // unspooled content. Copy the FileStream content first when available. - if (FileStream != null) - { - // We make a new stream for async reads from disk and async writes to the destination - await using var readStream = new FileStream(FileStream.Name, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.ReadWrite, bufferSize: 1, useAsync: true); + // We make a new stream for async reads from disk and async writes to the destination + await using var readStream = new FileStream(FileStream.Name, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.ReadWrite, bufferSize: 1, useAsync: true); + + await readStream.CopyToAsync(destination, cancellationToken); - await readStream.CopyToAsync(destination, cancellationToken); + // This is created with delete on close + await FileStream.DisposeAsync(); + FileStream = null; + } + + await PagedByteBuffer.MoveToAsync(destination, cancellationToken); + } - // This is created with delete on close - await FileStream.DisposeAsync(); - FileStream = null; - } + /// + protected override void Dispose(bool disposing) + { + if (!Disposed) + { + Disposed = true; - await PagedByteBuffer.MoveToAsync(destination, cancellationToken); + PagedByteBuffer.Dispose(); + FileStream?.Dispose(); } + } - /// - protected override void Dispose(bool disposing) + /// + public override async ValueTask DisposeAsync() + { + if (!Disposed) { - if (!Disposed) - { - Disposed = true; + Disposed = true; - PagedByteBuffer.Dispose(); - FileStream?.Dispose(); - } + PagedByteBuffer.Dispose(); + await (FileStream?.DisposeAsync() ?? default); } + } - /// - public override async ValueTask DisposeAsync() + [MemberNotNull(nameof(FileStream))] + private void EnsureFileStream() + { + if (FileStream == null) { - if (!Disposed) - { - Disposed = true; + var tempFileDirectory = _tempFileDirectoryAccessor(); + var tempFileName = Path.Combine(tempFileDirectory, "ASPNETCORE_" + Guid.NewGuid() + ".tmp"); + FileStream = new FileStream( + tempFileName, + FileMode.Create, + FileAccess.Write, + FileShare.Delete | FileShare.ReadWrite, + bufferSize: 1, + FileOptions.SequentialScan | FileOptions.DeleteOnClose); + } + } - PagedByteBuffer.Dispose(); - await (FileStream?.DisposeAsync() ?? default); - } + private void ThrowIfDisposed() + { + if (Disposed) + { + throw new ObjectDisposedException(nameof(FileBufferingWriteStream)); + } + } + + private static void ThrowArgumentException(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); } - [MemberNotNull(nameof(FileStream))] - private void EnsureFileStream() + if (offset < 0) { - if (FileStream == null) - { - var tempFileDirectory = _tempFileDirectoryAccessor(); - var tempFileName = Path.Combine(tempFileDirectory, "ASPNETCORE_" + Guid.NewGuid() + ".tmp"); - FileStream = new FileStream( - tempFileName, - FileMode.Create, - FileAccess.Write, - FileShare.Delete | FileShare.ReadWrite, - bufferSize: 1, - FileOptions.SequentialScan | FileOptions.DeleteOnClose); - } + throw new ArgumentOutOfRangeException(nameof(offset)); } - private void ThrowIfDisposed() + if (count < 0) { - if (Disposed) - { - throw new ObjectDisposedException(nameof(FileBufferingWriteStream)); - } + throw new ArgumentOutOfRangeException(nameof(count)); } - private static void ThrowArgumentException(byte[] buffer, int offset, int count) + if (buffer.Length - offset < count) { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - if (offset < 0) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } - - if (count < 0) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - if (buffer.Length - offset < count) - { - throw new ArgumentOutOfRangeException(nameof(offset)); - } + throw new ArgumentOutOfRangeException(nameof(offset)); } } } diff --git a/src/Http/WebUtilities/src/FileMultipartSection.cs b/src/Http/WebUtilities/src/FileMultipartSection.cs index 09fe352345..2e8914c2dc 100644 --- a/src/Http/WebUtilities/src/FileMultipartSection.cs +++ b/src/Http/WebUtilities/src/FileMultipartSection.cs @@ -5,66 +5,65 @@ using System; using System.IO; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +/// +/// Represents a file multipart section +/// +public class FileMultipartSection { + private readonly ContentDispositionHeaderValue _contentDispositionHeader; + /// - /// Represents a file multipart section + /// Creates a new instance of the class /// - public class FileMultipartSection + /// The section from which to create the + /// Reparses the content disposition header + public FileMultipartSection(MultipartSection section) + : this(section, section.GetContentDispositionHeader()) { - private readonly ContentDispositionHeaderValue _contentDispositionHeader; + } - /// - /// Creates a new instance of the class - /// - /// The section from which to create the - /// Reparses the content disposition header - public FileMultipartSection(MultipartSection section) - :this(section, section.GetContentDispositionHeader()) + /// + /// Creates a new instance of the class + /// + /// The section from which to create the + /// An already parsed content disposition header + public FileMultipartSection(MultipartSection section, ContentDispositionHeaderValue? header) + { + if (header is null || !header.IsFileDisposition()) { + throw new ArgumentException("Argument must be a file section", nameof(section)); } - /// - /// Creates a new instance of the class - /// - /// The section from which to create the - /// An already parsed content disposition header - public FileMultipartSection(MultipartSection section, ContentDispositionHeaderValue? header) - { - if (header is null || !header.IsFileDisposition()) - { - throw new ArgumentException("Argument must be a file section", nameof(section)); - } - - Section = section; - _contentDispositionHeader = header; + Section = section; + _contentDispositionHeader = header; - Name = HeaderUtilities.RemoveQuotes(_contentDispositionHeader.Name).ToString(); - FileName = HeaderUtilities.RemoveQuotes( - _contentDispositionHeader.FileNameStar.HasValue ? - _contentDispositionHeader.FileNameStar : - _contentDispositionHeader.FileName).ToString(); - } + Name = HeaderUtilities.RemoveQuotes(_contentDispositionHeader.Name).ToString(); + FileName = HeaderUtilities.RemoveQuotes( + _contentDispositionHeader.FileNameStar.HasValue ? + _contentDispositionHeader.FileNameStar : + _contentDispositionHeader.FileName).ToString(); + } - /// - /// Gets the original section from which this object was created - /// - public MultipartSection Section { get; } + /// + /// Gets the original section from which this object was created + /// + public MultipartSection Section { get; } - /// - /// Gets the file stream from the section body - /// - public Stream? FileStream => Section.Body; + /// + /// Gets the file stream from the section body + /// + public Stream? FileStream => Section.Body; - /// - /// Gets the name of the section - /// - public string Name { get; } + /// + /// Gets the name of the section + /// + public string Name { get; } - /// - /// Gets the name of the file from the section - /// - public string FileName { get; } + /// + /// Gets the name of the file from the section + /// + public string FileName { get; } - } } diff --git a/src/Http/WebUtilities/src/FormMultipartSection.cs b/src/Http/WebUtilities/src/FormMultipartSection.cs index 5792e02629..b72cf91903 100644 --- a/src/Http/WebUtilities/src/FormMultipartSection.cs +++ b/src/Http/WebUtilities/src/FormMultipartSection.cs @@ -5,59 +5,58 @@ using System; using System.Threading.Tasks; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +/// +/// Represents a form multipart section +/// +public class FormMultipartSection { + private readonly ContentDispositionHeaderValue _contentDispositionHeader; + /// - /// Represents a form multipart section + /// Creates a new instance of the class /// - public class FormMultipartSection + /// The section from which to create the + /// Reparses the content disposition header + public FormMultipartSection(MultipartSection section) + : this(section, section.GetContentDispositionHeader()) { - private readonly ContentDispositionHeaderValue _contentDispositionHeader; - - /// - /// Creates a new instance of the class - /// - /// The section from which to create the - /// Reparses the content disposition header - public FormMultipartSection(MultipartSection section) - : this(section, section.GetContentDispositionHeader()) - { - } + } - /// - /// Creates a new instance of the class - /// - /// The section from which to create the - /// An already parsed content disposition header - public FormMultipartSection(MultipartSection section, ContentDispositionHeaderValue? header) + /// + /// Creates a new instance of the class + /// + /// The section from which to create the + /// An already parsed content disposition header + public FormMultipartSection(MultipartSection section, ContentDispositionHeaderValue? header) + { + if (header == null || !header.IsFormDisposition()) { - if (header == null || !header.IsFormDisposition()) - { - throw new ArgumentException("Argument must be a form section", nameof(section)); - } - - Section = section; - _contentDispositionHeader = header; - Name = HeaderUtilities.RemoveQuotes(_contentDispositionHeader.Name).ToString(); + throw new ArgumentException("Argument must be a form section", nameof(section)); } - /// - /// Gets the original section from which this object was created - /// - public MultipartSection Section { get; } - - /// - /// The form name - /// - public string Name { get; } - - /// - /// Gets the form value - /// - /// The form value - public Task GetValueAsync() - { - return Section.ReadAsStringAsync(); - } + Section = section; + _contentDispositionHeader = header; + Name = HeaderUtilities.RemoveQuotes(_contentDispositionHeader.Name).ToString(); + } + + /// + /// Gets the original section from which this object was created + /// + public MultipartSection Section { get; } + + /// + /// The form name + /// + public string Name { get; } + + /// + /// Gets the form value + /// + /// The form value + public Task GetValueAsync() + { + return Section.ReadAsStringAsync(); } } diff --git a/src/Http/WebUtilities/src/FormPipeReader.cs b/src/Http/WebUtilities/src/FormPipeReader.cs index 9181a2df98..cd7268be14 100644 --- a/src/Http/WebUtilities/src/FormPipeReader.cs +++ b/src/Http/WebUtilities/src/FormPipeReader.cs @@ -15,423 +15,422 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +/// +/// Used to read an 'application/x-www-form-urlencoded' form. +/// Internally reads from a PipeReader. +/// +public class FormPipeReader { + private const int StackAllocThreshold = 128; + private const int DefaultValueCountLimit = 1024; + private const int DefaultKeyLengthLimit = 1024 * 2; + private const int DefaultValueLengthLimit = 1024 * 1024 * 4; + + // Used for UTF8/ASCII (precalculated for fast path) + // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static + private static ReadOnlySpan UTF8EqualEncoded => new byte[] { (byte)'=' }; + private static ReadOnlySpan UTF8AndEncoded => new byte[] { (byte)'&' }; + + // Used for other encodings + private readonly byte[]? _otherEqualEncoding; + private readonly byte[]? _otherAndEncoding; + + private readonly PipeReader _pipeReader; + private readonly Encoding _encoding; + + /// + /// Initializes a new instance of . + /// + /// The to read from. + public FormPipeReader(PipeReader pipeReader) + : this(pipeReader, Encoding.UTF8) + { + } + /// - /// Used to read an 'application/x-www-form-urlencoded' form. - /// Internally reads from a PipeReader. + /// Initializes a new instance of . /// - public class FormPipeReader + /// The to read from. + /// The . + public FormPipeReader(PipeReader pipeReader, Encoding encoding) { - private const int StackAllocThreshold = 128; - private const int DefaultValueCountLimit = 1024; - private const int DefaultKeyLengthLimit = 1024 * 2; - private const int DefaultValueLengthLimit = 1024 * 1024 * 4; - - // Used for UTF8/ASCII (precalculated for fast path) - // This uses C# compiler's ability to refer to static data directly. For more information see https://vcsjones.dev/2019/02/01/csharp-readonly-span-bytes-static - private static ReadOnlySpan UTF8EqualEncoded => new byte[] { (byte)'=' }; - private static ReadOnlySpan UTF8AndEncoded => new byte[] { (byte)'&' }; - - // Used for other encodings - private readonly byte[]? _otherEqualEncoding; - private readonly byte[]? _otherAndEncoding; - - private readonly PipeReader _pipeReader; - private readonly Encoding _encoding; - - /// - /// Initializes a new instance of . - /// - /// The to read from. - public FormPipeReader(PipeReader pipeReader) - : this(pipeReader, Encoding.UTF8) + // https://docs.microsoft.com/en-us/dotnet/core/compatibility/syslib-warnings/syslib0001 + if (encoding is Encoding { CodePage: 65000 }) { + throw new ArgumentException("UTF7 is unsupported and insecure. Please select a different encoding."); } - /// - /// Initializes a new instance of . - /// - /// The to read from. - /// The . - public FormPipeReader(PipeReader pipeReader, Encoding encoding) + _pipeReader = pipeReader; + _encoding = encoding; + + if (_encoding != Encoding.UTF8 && _encoding != Encoding.ASCII) { - // https://docs.microsoft.com/en-us/dotnet/core/compatibility/syslib-warnings/syslib0001 - if (encoding is Encoding { CodePage: 65000 }) - { - throw new ArgumentException("UTF7 is unsupported and insecure. Please select a different encoding."); - } + _otherEqualEncoding = _encoding.GetBytes("="); + _otherAndEncoding = _encoding.GetBytes("&"); + } + } - _pipeReader = pipeReader; - _encoding = encoding; + /// + /// The limit on the number of form values to allow in ReadForm or ReadFormAsync. + /// + public int ValueCountLimit { get; set; } = DefaultValueCountLimit; - if (_encoding != Encoding.UTF8 && _encoding != Encoding.ASCII) - { - _otherEqualEncoding = _encoding.GetBytes("="); - _otherAndEncoding = _encoding.GetBytes("&"); - } - } + /// + /// The limit on the length of form keys. + /// + public int KeyLengthLimit { get; set; } = DefaultKeyLengthLimit; - /// - /// The limit on the number of form values to allow in ReadForm or ReadFormAsync. - /// - public int ValueCountLimit { get; set; } = DefaultValueCountLimit; - - /// - /// The limit on the length of form keys. - /// - public int KeyLengthLimit { get; set; } = DefaultKeyLengthLimit; - - /// - /// The limit on the length of form values. - /// - public int ValueLengthLimit { get; set; } = DefaultValueLengthLimit; - - /// - /// Parses an HTTP form body. - /// - /// The . - /// The collection containing the parsed HTTP form body. - public async Task> ReadFormAsync(CancellationToken cancellationToken = default) + /// + /// The limit on the length of form values. + /// + public int ValueLengthLimit { get; set; } = DefaultValueLengthLimit; + + /// + /// Parses an HTTP form body. + /// + /// The . + /// The collection containing the parsed HTTP form body. + public async Task> ReadFormAsync(CancellationToken cancellationToken = default) + { + KeyValueAccumulator accumulator = default; + while (true) { - KeyValueAccumulator accumulator = default; - while (true) - { - var readResult = await _pipeReader.ReadAsync(cancellationToken); + var readResult = await _pipeReader.ReadAsync(cancellationToken); - var buffer = readResult.Buffer; + var buffer = readResult.Buffer; - if (!buffer.IsEmpty) + if (!buffer.IsEmpty) + { + try { - try - { - ParseFormValues(ref buffer, ref accumulator, readResult.IsCompleted); - } - catch - { - _pipeReader.AdvanceTo(buffer.Start, buffer.End); - throw; - } + ParseFormValues(ref buffer, ref accumulator, readResult.IsCompleted); } - - if (readResult.IsCompleted) + catch { - _pipeReader.AdvanceTo(buffer.End); - - if (!buffer.IsEmpty) - { - throw new InvalidOperationException("End of body before form was fully parsed."); - } - break; + _pipeReader.AdvanceTo(buffer.Start, buffer.End); + throw; } - - _pipeReader.AdvanceTo(buffer.Start, buffer.End); } - return accumulator.GetResults(); - } - - [MethodImpl(MethodImplOptions.NoInlining)] - internal void ParseFormValues( - ref ReadOnlySequence buffer, - ref KeyValueAccumulator accumulator, - bool isFinalBlock) - { - if (buffer.IsSingleSegment) + if (readResult.IsCompleted) { - ParseFormValuesFast(buffer.FirstSpan, - ref accumulator, - isFinalBlock, - out var consumed); + _pipeReader.AdvanceTo(buffer.End); - buffer = buffer.Slice(consumed); - return; + if (!buffer.IsEmpty) + { + throw new InvalidOperationException("End of body before form was fully parsed."); + } + break; } - ParseValuesSlow(ref buffer, + _pipeReader.AdvanceTo(buffer.Start, buffer.End); + } + + return accumulator.GetResults(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + internal void ParseFormValues( + ref ReadOnlySequence buffer, + ref KeyValueAccumulator accumulator, + bool isFinalBlock) + { + if (buffer.IsSingleSegment) + { + ParseFormValuesFast(buffer.FirstSpan, ref accumulator, - isFinalBlock); + isFinalBlock, + out var consumed); + + buffer = buffer.Slice(consumed); + return; } - // Fast parsing for single span in ReadOnlySequence - private void ParseFormValuesFast(ReadOnlySpan span, - ref KeyValueAccumulator accumulator, - bool isFinalBlock, - out int consumed) + ParseValuesSlow(ref buffer, + ref accumulator, + isFinalBlock); + } + + // Fast parsing for single span in ReadOnlySequence + private void ParseFormValuesFast(ReadOnlySpan span, + ref KeyValueAccumulator accumulator, + bool isFinalBlock, + out int consumed) + { + ReadOnlySpan key; + ReadOnlySpan value; + consumed = 0; + var equalsDelimiter = GetEqualsForEncoding(); + var andDelimiter = GetAndForEncoding(); + + while (span.Length > 0) { - ReadOnlySpan key; - ReadOnlySpan value; - consumed = 0; - var equalsDelimiter = GetEqualsForEncoding(); - var andDelimiter = GetAndForEncoding(); + // Find the end of the key=value pair. + var ampersand = span.IndexOf(andDelimiter); + ReadOnlySpan keyValuePair; + int equals; + var foundAmpersand = ampersand != -1; - while (span.Length > 0) + if (foundAmpersand) { - // Find the end of the key=value pair. - var ampersand = span.IndexOf(andDelimiter); - ReadOnlySpan keyValuePair; - int equals; - var foundAmpersand = ampersand != -1; - - if (foundAmpersand) - { - keyValuePair = span.Slice(0, ampersand); - span = span.Slice(keyValuePair.Length + andDelimiter.Length); - consumed += keyValuePair.Length + andDelimiter.Length; - } - else + keyValuePair = span.Slice(0, ampersand); + span = span.Slice(keyValuePair.Length + andDelimiter.Length); + consumed += keyValuePair.Length + andDelimiter.Length; + } + else + { + // We can't know that what is currently read is the end of the form value, that's only the case if this is the final block + // If we're not in the final block, then consume nothing + if (!isFinalBlock) { - // We can't know that what is currently read is the end of the form value, that's only the case if this is the final block - // If we're not in the final block, then consume nothing - if (!isFinalBlock) + // Don't buffer indefinitely + if ((uint)span.Length > (uint)KeyLengthLimit + (uint)ValueLengthLimit) { - // Don't buffer indefinitely - if ((uint)span.Length > (uint)KeyLengthLimit + (uint)ValueLengthLimit) - { - ThrowKeyOrValueTooLargeException(); - } - return; + ThrowKeyOrValueTooLargeException(); } - - keyValuePair = span; - span = default; - consumed += keyValuePair.Length; + return; } - equals = keyValuePair.IndexOf(equalsDelimiter); + keyValuePair = span; + span = default; + consumed += keyValuePair.Length; + } - if (equals == -1) - { - // Too long for the whole segment to be a key. - if (keyValuePair.Length > KeyLengthLimit) - { - ThrowKeyTooLargeException(); - } + equals = keyValuePair.IndexOf(equalsDelimiter); - // There is no more data, this segment must be "key" with no equals or value. - key = keyValuePair; - value = default; + if (equals == -1) + { + // Too long for the whole segment to be a key. + if (keyValuePair.Length > KeyLengthLimit) + { + ThrowKeyTooLargeException(); } - else + + // There is no more data, this segment must be "key" with no equals or value. + key = keyValuePair; + value = default; + } + else + { + key = keyValuePair.Slice(0, equals); + if (key.Length > KeyLengthLimit) { - key = keyValuePair.Slice(0, equals); - if (key.Length > KeyLengthLimit) - { - ThrowKeyTooLargeException(); - } + ThrowKeyTooLargeException(); + } - value = keyValuePair.Slice(equals + equalsDelimiter.Length); - if (value.Length > ValueLengthLimit) - { - ThrowValueTooLargeException(); - } + value = keyValuePair.Slice(equals + equalsDelimiter.Length); + if (value.Length > ValueLengthLimit) + { + ThrowValueTooLargeException(); } + } - var decodedKey = GetDecodedString(key); - var decodedValue = GetDecodedString(value); + var decodedKey = GetDecodedString(key); + var decodedValue = GetDecodedString(value); - AppendAndVerify(ref accumulator, decodedKey, decodedValue); - } + AppendAndVerify(ref accumulator, decodedKey, decodedValue); } + } - // For multi-segment parsing of a read only sequence - private void ParseValuesSlow( - ref ReadOnlySequence buffer, - ref KeyValueAccumulator accumulator, - bool isFinalBlock) - { - var sequenceReader = new SequenceReader(buffer); - ReadOnlySequence keyValuePair; + // For multi-segment parsing of a read only sequence + private void ParseValuesSlow( + ref ReadOnlySequence buffer, + ref KeyValueAccumulator accumulator, + bool isFinalBlock) + { + var sequenceReader = new SequenceReader(buffer); + ReadOnlySequence keyValuePair; - var consumed = sequenceReader.Position; - var consumedBytes = default(long); - var equalsDelimiter = GetEqualsForEncoding(); - var andDelimiter = GetAndForEncoding(); + var consumed = sequenceReader.Position; + var consumedBytes = default(long); + var equalsDelimiter = GetEqualsForEncoding(); + var andDelimiter = GetAndForEncoding(); - while (!sequenceReader.End) + while (!sequenceReader.End) + { + if (!sequenceReader.TryReadTo(out keyValuePair, andDelimiter)) { - if (!sequenceReader.TryReadTo(out keyValuePair, andDelimiter)) + if (!isFinalBlock) { - if (!isFinalBlock) + // Don't buffer indefinitely + if ((uint)(sequenceReader.Consumed - consumedBytes) > (uint)KeyLengthLimit + (uint)ValueLengthLimit) { - // Don't buffer indefinitely - if ((uint)(sequenceReader.Consumed - consumedBytes) > (uint)KeyLengthLimit + (uint)ValueLengthLimit) - { - ThrowKeyOrValueTooLargeException(); - } - break; + ThrowKeyOrValueTooLargeException(); } - - // This must be the final key=value pair - keyValuePair = buffer.Slice(sequenceReader.Position); - sequenceReader.Advance(keyValuePair.Length); + break; } - if (keyValuePair.IsSingleSegment) - { - ParseFormValuesFast(keyValuePair.FirstSpan, ref accumulator, isFinalBlock: true, out var segmentConsumed); - Debug.Assert(segmentConsumed == keyValuePair.FirstSpan.Length); - consumedBytes = sequenceReader.Consumed; - consumed = sequenceReader.Position; - continue; - } + // This must be the final key=value pair + keyValuePair = buffer.Slice(sequenceReader.Position); + sequenceReader.Advance(keyValuePair.Length); + } + + if (keyValuePair.IsSingleSegment) + { + ParseFormValuesFast(keyValuePair.FirstSpan, ref accumulator, isFinalBlock: true, out var segmentConsumed); + Debug.Assert(segmentConsumed == keyValuePair.FirstSpan.Length); + consumedBytes = sequenceReader.Consumed; + consumed = sequenceReader.Position; + continue; + } - var keyValueReader = new SequenceReader(keyValuePair); - ReadOnlySequence value; + var keyValueReader = new SequenceReader(keyValuePair); + ReadOnlySequence value; - if (keyValueReader.TryReadTo(out ReadOnlySequence key, equalsDelimiter)) + if (keyValueReader.TryReadTo(out ReadOnlySequence key, equalsDelimiter)) + { + if (key.Length > KeyLengthLimit) { - if (key.Length > KeyLengthLimit) - { - ThrowKeyTooLargeException(); - } + ThrowKeyTooLargeException(); + } - value = keyValuePair.Slice(keyValueReader.Position); - if (value.Length > ValueLengthLimit) - { - ThrowValueTooLargeException(); - } + value = keyValuePair.Slice(keyValueReader.Position); + if (value.Length > ValueLengthLimit) + { + ThrowValueTooLargeException(); } - else + } + else + { + // Too long for the whole segment to be a key. + if (keyValuePair.Length > KeyLengthLimit) { - // Too long for the whole segment to be a key. - if (keyValuePair.Length > KeyLengthLimit) - { - ThrowKeyTooLargeException(); - } - - // There is no more data, this segment must be "key" with no equals or value. - key = keyValuePair; - value = default; + ThrowKeyTooLargeException(); } - var decodedKey = GetDecodedStringFromReadOnlySequence(key); - var decodedValue = GetDecodedStringFromReadOnlySequence(value); + // There is no more data, this segment must be "key" with no equals or value. + key = keyValuePair; + value = default; + } - AppendAndVerify(ref accumulator, decodedKey, decodedValue); + var decodedKey = GetDecodedStringFromReadOnlySequence(key); + var decodedValue = GetDecodedStringFromReadOnlySequence(value); - consumedBytes = sequenceReader.Consumed; - consumed = sequenceReader.Position; - } + AppendAndVerify(ref accumulator, decodedKey, decodedValue); - buffer = buffer.Slice(consumed); + consumedBytes = sequenceReader.Consumed; + consumed = sequenceReader.Position; } - private void ThrowKeyOrValueTooLargeException() - { - throw new InvalidDataException($"Form key length limit {KeyLengthLimit} or value length limit {ValueLengthLimit} exceeded."); - } + buffer = buffer.Slice(consumed); + } + + private void ThrowKeyOrValueTooLargeException() + { + throw new InvalidDataException($"Form key length limit {KeyLengthLimit} or value length limit {ValueLengthLimit} exceeded."); + } + + private void ThrowKeyTooLargeException() + { + throw new InvalidDataException($"Form key length limit {KeyLengthLimit} exceeded."); + } + + private void ThrowValueTooLargeException() + { + throw new InvalidDataException($"Form value length limit {ValueLengthLimit} exceeded."); + } - private void ThrowKeyTooLargeException() + [SkipLocalsInit] + private string GetDecodedStringFromReadOnlySequence(in ReadOnlySequence ros) + { + if (ros.IsSingleSegment) { - throw new InvalidDataException($"Form key length limit {KeyLengthLimit} exceeded."); + return GetDecodedString(ros.FirstSpan); } - private void ThrowValueTooLargeException() + if (ros.Length < StackAllocThreshold) { - throw new InvalidDataException($"Form value length limit {ValueLengthLimit} exceeded."); + Span buffer = stackalloc byte[StackAllocThreshold].Slice(0, (int)ros.Length); + ros.CopyTo(buffer); + return GetDecodedString(buffer); } - - [SkipLocalsInit] - private string GetDecodedStringFromReadOnlySequence(in ReadOnlySequence ros) + else { - if (ros.IsSingleSegment) - { - return GetDecodedString(ros.FirstSpan); - } + var byteArray = ArrayPool.Shared.Rent((int)ros.Length); - if (ros.Length < StackAllocThreshold) + try { - Span buffer = stackalloc byte[StackAllocThreshold].Slice(0, (int)ros.Length); + Span buffer = byteArray.AsSpan(0, (int)ros.Length); ros.CopyTo(buffer); return GetDecodedString(buffer); } - else + finally { - var byteArray = ArrayPool.Shared.Rent((int)ros.Length); - - try - { - Span buffer = byteArray.AsSpan(0, (int)ros.Length); - ros.CopyTo(buffer); - return GetDecodedString(buffer); - } - finally - { - ArrayPool.Shared.Return(byteArray); - } + ArrayPool.Shared.Return(byteArray); } } + } - // Check that key/value constraints are met and appends value to accumulator. - private void AppendAndVerify(ref KeyValueAccumulator accumulator, string decodedKey, string decodedValue) - { - accumulator.Append(decodedKey, decodedValue); + // Check that key/value constraints are met and appends value to accumulator. + private void AppendAndVerify(ref KeyValueAccumulator accumulator, string decodedKey, string decodedValue) + { + accumulator.Append(decodedKey, decodedValue); - if (accumulator.ValueCount > ValueCountLimit) - { - throw new InvalidDataException($"Form value count limit {ValueCountLimit} exceeded."); - } + if (accumulator.ValueCount > ValueCountLimit) + { + throw new InvalidDataException($"Form value count limit {ValueCountLimit} exceeded."); } + } - private string GetDecodedString(ReadOnlySpan readOnlySpan) + private string GetDecodedString(ReadOnlySpan readOnlySpan) + { + if (readOnlySpan.Length == 0) { - if (readOnlySpan.Length == 0) - { - return string.Empty; - } - else if (_encoding == Encoding.UTF8 || _encoding == Encoding.ASCII) - { - // UrlDecoder only works on UTF8 (and implicitly ASCII) + return string.Empty; + } + else if (_encoding == Encoding.UTF8 || _encoding == Encoding.ASCII) + { + // UrlDecoder only works on UTF8 (and implicitly ASCII) - // We need to create a Span from a ReadOnlySpan. This cast is safe because the memory is still held by the pipe - // We will also create a string from it by the end of the function. - var span = MemoryMarshal.CreateSpan(ref Unsafe.AsRef(readOnlySpan[0]), readOnlySpan.Length); + // We need to create a Span from a ReadOnlySpan. This cast is safe because the memory is still held by the pipe + // We will also create a string from it by the end of the function. + var span = MemoryMarshal.CreateSpan(ref Unsafe.AsRef(readOnlySpan[0]), readOnlySpan.Length); - try - { - var bytes = UrlDecoder.DecodeInPlace(span, isFormEncoding: true); - span = span.Slice(0, bytes); + try + { + var bytes = UrlDecoder.DecodeInPlace(span, isFormEncoding: true); + span = span.Slice(0, bytes); - return _encoding.GetString(span); - } - catch (InvalidOperationException ex) - { - throw new InvalidDataException("The form value contains invalid characters.", ex); - } + return _encoding.GetString(span); } - else + catch (InvalidOperationException ex) { - // Slow path for Unicode and other encodings. - // Just do raw string replacement. - var decodedString = _encoding.GetString(readOnlySpan); - decodedString = decodedString.Replace('+', ' '); - return Uri.UnescapeDataString(decodedString); + throw new InvalidDataException("The form value contains invalid characters.", ex); } } + else + { + // Slow path for Unicode and other encodings. + // Just do raw string replacement. + var decodedString = _encoding.GetString(readOnlySpan); + decodedString = decodedString.Replace('+', ' '); + return Uri.UnescapeDataString(decodedString); + } + } - private ReadOnlySpan GetEqualsForEncoding() + private ReadOnlySpan GetEqualsForEncoding() + { + if (_encoding == Encoding.UTF8 || _encoding == Encoding.ASCII) { - if (_encoding == Encoding.UTF8 || _encoding == Encoding.ASCII) - { - return UTF8EqualEncoded; - } - else - { - return _otherEqualEncoding; - } + return UTF8EqualEncoded; } + else + { + return _otherEqualEncoding; + } + } - private ReadOnlySpan GetAndForEncoding() + private ReadOnlySpan GetAndForEncoding() + { + if (_encoding == Encoding.UTF8 || _encoding == Encoding.ASCII) { - if (_encoding == Encoding.UTF8 || _encoding == Encoding.ASCII) - { - return UTF8AndEncoded; - } - else - { - return _otherAndEncoding; - } + return UTF8AndEncoded; + } + else + { + return _otherAndEncoding; } } } diff --git a/src/Http/WebUtilities/src/FormReader.cs b/src/Http/WebUtilities/src/FormReader.cs index 04afda2277..376d07c33c 100644 --- a/src/Http/WebUtilities/src/FormReader.cs +++ b/src/Http/WebUtilities/src/FormReader.cs @@ -11,343 +11,342 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +/// +/// Used to read an 'application/x-www-form-urlencoded' form. +/// +public class FormReader : IDisposable { /// - /// Used to read an 'application/x-www-form-urlencoded' form. + /// Gets the default value for . + /// Defaults to 1024. + /// + public const int DefaultValueCountLimit = 1024; + + /// + /// Gets the default value for . + /// Defaults to 2,048 bytes‬, which is approximately 2KB. + /// + public const int DefaultKeyLengthLimit = 1024 * 2; + + /// + /// Gets the default value for . + /// Defaults to 4,194,304 bytes‬, which is approximately 4MB. + /// + public const int DefaultValueLengthLimit = 1024 * 1024 * 4; + + private const int _rentedCharPoolLength = 8192; + private readonly TextReader _reader; + private readonly char[] _buffer; + private readonly ArrayPool _charPool; + private readonly StringBuilder _builder = new StringBuilder(); + private int _bufferOffset; + private int _bufferCount; + private string? _currentKey; + private string? _currentValue; + private bool _endOfStream; + private bool _disposed; + + /// + /// Initializes a new instance of . + /// + /// The data to read. + public FormReader(string data) + : this(data, ArrayPool.Shared) + { + } + + /// + /// Initializes a new instance of . /// - public class FormReader : IDisposable + /// The data to read. + /// The to use. + public FormReader(string data, ArrayPool charPool) { - /// - /// Gets the default value for . - /// Defaults to 1024. - /// - public const int DefaultValueCountLimit = 1024; - - /// - /// Gets the default value for . - /// Defaults to 2,048 bytes‬, which is approximately 2KB. - /// - public const int DefaultKeyLengthLimit = 1024 * 2; - - /// - /// Gets the default value for . - /// Defaults to 4,194,304 bytes‬, which is approximately 4MB. - /// - public const int DefaultValueLengthLimit = 1024 * 1024 * 4; - - private const int _rentedCharPoolLength = 8192; - private readonly TextReader _reader; - private readonly char[] _buffer; - private readonly ArrayPool _charPool; - private readonly StringBuilder _builder = new StringBuilder(); - private int _bufferOffset; - private int _bufferCount; - private string? _currentKey; - private string? _currentValue; - private bool _endOfStream; - private bool _disposed; - - /// - /// Initializes a new instance of . - /// - /// The data to read. - public FormReader(string data) - : this(data, ArrayPool.Shared) + if (data == null) { + throw new ArgumentNullException(nameof(data)); } - /// - /// Initializes a new instance of . - /// - /// The data to read. - /// The to use. - public FormReader(string data, ArrayPool charPool) - { - if (data == null) - { - throw new ArgumentNullException(nameof(data)); - } + _buffer = charPool.Rent(_rentedCharPoolLength); + _charPool = charPool; + _reader = new StringReader(data); + } - _buffer = charPool.Rent(_rentedCharPoolLength); - _charPool = charPool; - _reader = new StringReader(data); - } + /// + /// Initializes a new instance of . + /// + /// The to read. Assumes a utf-8 encoded stream. + public FormReader(Stream stream) + : this(stream, Encoding.UTF8, ArrayPool.Shared) + { + } - /// - /// Initializes a new instance of . - /// - /// The to read. Assumes a utf-8 encoded stream. - public FormReader(Stream stream) - : this(stream, Encoding.UTF8, ArrayPool.Shared) + /// + /// Initializes a new instance of . + /// + /// The to read. + /// The character encoding to use. + public FormReader(Stream stream, Encoding encoding) + : this(stream, encoding, ArrayPool.Shared) + { + } + + /// + /// Initializes a new instance of . + /// + /// The to read. + /// The character encoding to use. + /// The to use. + public FormReader(Stream stream, Encoding encoding, ArrayPool charPool) + { + if (stream == null) { + throw new ArgumentNullException(nameof(stream)); } - /// - /// Initializes a new instance of . - /// - /// The to read. - /// The character encoding to use. - public FormReader(Stream stream, Encoding encoding) - : this(stream, encoding, ArrayPool.Shared) + if (encoding == null) { + throw new ArgumentNullException(nameof(encoding)); } - /// - /// Initializes a new instance of . - /// - /// The to read. - /// The character encoding to use. - /// The to use. - public FormReader(Stream stream, Encoding encoding, ArrayPool charPool) - { - if (stream == null) - { - throw new ArgumentNullException(nameof(stream)); - } + _buffer = charPool.Rent(_rentedCharPoolLength); + _charPool = charPool; + _reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024 * 2, leaveOpen: true); + } - if (encoding == null) - { - throw new ArgumentNullException(nameof(encoding)); - } + /// + /// The limit on the number of form values to allow in ReadForm or ReadFormAsync. + /// + public int ValueCountLimit { get; set; } = DefaultValueCountLimit; - _buffer = charPool.Rent(_rentedCharPoolLength); - _charPool = charPool; - _reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024 * 2, leaveOpen: true); - } + /// + /// The limit on the length of form keys. + /// + public int KeyLengthLimit { get; set; } = DefaultKeyLengthLimit; + + /// + /// The limit on the length of form values. + /// + public int ValueLengthLimit { get; set; } = DefaultValueLengthLimit; - /// - /// The limit on the number of form values to allow in ReadForm or ReadFormAsync. - /// - public int ValueCountLimit { get; set; } = DefaultValueCountLimit; - - /// - /// The limit on the length of form keys. - /// - public int KeyLengthLimit { get; set; } = DefaultKeyLengthLimit; - - /// - /// The limit on the length of form values. - /// - public int ValueLengthLimit { get; set; } = DefaultValueLengthLimit; - - // Format: key1=value1&key2=value2 - /// - /// Reads the next key value pair from the form. - /// For unbuffered data use the async overload instead. - /// - /// The next key value pair, or null when the end of the form is reached. - public KeyValuePair? ReadNextPair() + // Format: key1=value1&key2=value2 + /// + /// Reads the next key value pair from the form. + /// For unbuffered data use the async overload instead. + /// + /// The next key value pair, or null when the end of the form is reached. + public KeyValuePair? ReadNextPair() + { + ReadNextPairImpl(); + if (ReadSucceeded()) { - ReadNextPairImpl(); - if (ReadSucceeded()) - { - return new KeyValuePair(_currentKey, _currentValue); - } - return null; + return new KeyValuePair(_currentKey, _currentValue); } + return null; + } - private void ReadNextPairImpl() + private void ReadNextPairImpl() + { + StartReadNextPair(); + while (!_endOfStream) { - StartReadNextPair(); - while (!_endOfStream) + // Empty + if (_bufferCount == 0) + { + Buffer(); + } + if (TryReadNextPair()) { - // Empty - if (_bufferCount == 0) - { - Buffer(); - } - if (TryReadNextPair()) - { - break; - } + break; } } + } - // Format: key1=value1&key2=value2 - /// - /// Asynchronously reads the next key value pair from the form. - /// - /// - /// The next key value pair, or null when the end of the form is reached. - public async Task?> ReadNextPairAsync(CancellationToken cancellationToken = new CancellationToken()) + // Format: key1=value1&key2=value2 + /// + /// Asynchronously reads the next key value pair from the form. + /// + /// + /// The next key value pair, or null when the end of the form is reached. + public async Task?> ReadNextPairAsync(CancellationToken cancellationToken = new CancellationToken()) + { + await ReadNextPairAsyncImpl(cancellationToken); + if (ReadSucceeded()) { - await ReadNextPairAsyncImpl(cancellationToken); - if (ReadSucceeded()) - { - return new KeyValuePair(_currentKey, _currentValue); - } - return null; + return new KeyValuePair(_currentKey, _currentValue); } + return null; + } - private async Task ReadNextPairAsyncImpl(CancellationToken cancellationToken = new CancellationToken()) + private async Task ReadNextPairAsyncImpl(CancellationToken cancellationToken = new CancellationToken()) + { + StartReadNextPair(); + while (!_endOfStream) { - StartReadNextPair(); - while (!_endOfStream) + // Empty + if (_bufferCount == 0) + { + await BufferAsync(cancellationToken); + } + if (TryReadNextPair()) { - // Empty - if (_bufferCount == 0) - { - await BufferAsync(cancellationToken); - } - if (TryReadNextPair()) - { - break; - } + break; } } + } - private void StartReadNextPair() - { - _currentKey = null; - _currentValue = null; - } + private void StartReadNextPair() + { + _currentKey = null; + _currentValue = null; + } - private bool TryReadNextPair() + private bool TryReadNextPair() + { + if (_currentKey == null) { - if (_currentKey == null) + if (!TryReadWord('=', KeyLengthLimit, out _currentKey)) { - if (!TryReadWord('=', KeyLengthLimit, out _currentKey)) - { - return false; - } - - if (_bufferCount == 0) - { - return false; - } + return false; } - if (_currentValue == null) + if (_bufferCount == 0) { - if (!TryReadWord('&', ValueLengthLimit, out _currentValue)) - { - return false; - } + return false; } - return true; } - private bool TryReadWord(char separator, int limit, [NotNullWhen(true)] out string? value) + if (_currentValue == null) { - do + if (!TryReadWord('&', ValueLengthLimit, out _currentValue)) { - if (ReadChar(separator, limit, out value)) - { - return true; - } - } while (_bufferCount > 0); - return false; + return false; + } } + return true; + } - private bool ReadChar(char separator, int limit, [NotNullWhen(true)] out string? word) + private bool TryReadWord(char separator, int limit, [NotNullWhen(true)] out string? value) + { + do { - // End - if (_bufferCount == 0) - { - word = BuildWord(); - return true; - } - - var c = _buffer[_bufferOffset++]; - _bufferCount--; - - if (c == separator) + if (ReadChar(separator, limit, out value)) { - word = BuildWord(); return true; } - if (_builder.Length >= limit) - { - throw new InvalidDataException($"Form key or value length limit {limit} exceeded."); - } - _builder.Append(c); - word = null; - return false; - } + } while (_bufferCount > 0); + return false; + } - // '+' un-escapes to ' ', %HH un-escapes as ASCII (or utf-8?) - private string BuildWord() + private bool ReadChar(char separator, int limit, [NotNullWhen(true)] out string? word) + { + // End + if (_bufferCount == 0) { - _builder.Replace('+', ' '); - var result = _builder.ToString(); - _builder.Clear(); - return Uri.UnescapeDataString(result); // TODO: Replace this, it's not completely accurate. + word = BuildWord(); + return true; } - private void Buffer() - { - _bufferOffset = 0; - _bufferCount = _reader.Read(_buffer, 0, _buffer.Length); - _endOfStream = _bufferCount == 0; - } + var c = _buffer[_bufferOffset++]; + _bufferCount--; - private async Task BufferAsync(CancellationToken cancellationToken) + if (c == separator) { - // TODO: StreamReader doesn't support cancellation? - cancellationToken.ThrowIfCancellationRequested(); - _bufferOffset = 0; - _bufferCount = await _reader.ReadAsync(_buffer, 0, _buffer.Length); - _endOfStream = _bufferCount == 0; + word = BuildWord(); + return true; } - - /// - /// Parses text from an HTTP form body. - /// - /// The collection containing the parsed HTTP form body. - public Dictionary ReadForm() + if (_builder.Length >= limit) { - var accumulator = new KeyValueAccumulator(); - while (!_endOfStream) - { - ReadNextPairImpl(); - Append(ref accumulator); - } - return accumulator.GetResults(); + throw new InvalidDataException($"Form key or value length limit {limit} exceeded."); } + _builder.Append(c); + word = null; + return false; + } + + // '+' un-escapes to ' ', %HH un-escapes as ASCII (or utf-8?) + private string BuildWord() + { + _builder.Replace('+', ' '); + var result = _builder.ToString(); + _builder.Clear(); + return Uri.UnescapeDataString(result); // TODO: Replace this, it's not completely accurate. + } + + private void Buffer() + { + _bufferOffset = 0; + _bufferCount = _reader.Read(_buffer, 0, _buffer.Length); + _endOfStream = _bufferCount == 0; + } + + private async Task BufferAsync(CancellationToken cancellationToken) + { + // TODO: StreamReader doesn't support cancellation? + cancellationToken.ThrowIfCancellationRequested(); + _bufferOffset = 0; + _bufferCount = await _reader.ReadAsync(_buffer, 0, _buffer.Length); + _endOfStream = _bufferCount == 0; + } - /// - /// Parses an HTTP form body. - /// - /// The . - /// The collection containing the parsed HTTP form body. - public async Task> ReadFormAsync(CancellationToken cancellationToken = new CancellationToken()) + /// + /// Parses text from an HTTP form body. + /// + /// The collection containing the parsed HTTP form body. + public Dictionary ReadForm() + { + var accumulator = new KeyValueAccumulator(); + while (!_endOfStream) { - var accumulator = new KeyValueAccumulator(); - while (!_endOfStream) - { - await ReadNextPairAsyncImpl(cancellationToken); - Append(ref accumulator); - } - return accumulator.GetResults(); + ReadNextPairImpl(); + Append(ref accumulator); } + return accumulator.GetResults(); + } - [MemberNotNullWhen(true, nameof(_currentKey), nameof(_currentValue))] - private bool ReadSucceeded() + /// + /// Parses an HTTP form body. + /// + /// The . + /// The collection containing the parsed HTTP form body. + public async Task> ReadFormAsync(CancellationToken cancellationToken = new CancellationToken()) + { + var accumulator = new KeyValueAccumulator(); + while (!_endOfStream) { - return _currentKey != null && _currentValue != null; + await ReadNextPairAsyncImpl(cancellationToken); + Append(ref accumulator); } + return accumulator.GetResults(); + } - private void Append(ref KeyValueAccumulator accumulator) + [MemberNotNullWhen(true, nameof(_currentKey), nameof(_currentValue))] + private bool ReadSucceeded() + { + return _currentKey != null && _currentValue != null; + } + + private void Append(ref KeyValueAccumulator accumulator) + { + if (ReadSucceeded()) { - if (ReadSucceeded()) + accumulator.Append(_currentKey, _currentValue); + if (accumulator.ValueCount > ValueCountLimit) { - accumulator.Append(_currentKey, _currentValue); - if (accumulator.ValueCount > ValueCountLimit) - { - throw new InvalidDataException($"Form value count limit {ValueCountLimit} exceeded."); - } + throw new InvalidDataException($"Form value count limit {ValueCountLimit} exceeded."); } } + } - /// - public void Dispose() + /// + public void Dispose() + { + if (!_disposed) { - if (!_disposed) - { - _disposed = true; - _charPool.Return(_buffer); - } + _disposed = true; + _charPool.Return(_buffer); } } } diff --git a/src/Http/WebUtilities/src/HttpRequestStreamReader.cs b/src/Http/WebUtilities/src/HttpRequestStreamReader.cs index 428d967e7d..c017bd315d 100644 --- a/src/Http/WebUtilities/src/HttpRequestStreamReader.cs +++ b/src/Http/WebUtilities/src/HttpRequestStreamReader.cs @@ -10,473 +10,451 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +/// +/// A to read the HTTP request stream. +/// +public class HttpRequestStreamReader : TextReader { - /// - /// A to read the HTTP request stream. - /// - public class HttpRequestStreamReader : TextReader - { - private const int DefaultBufferSize = 1024; + private const int DefaultBufferSize = 1024; - private readonly Stream _stream; - private readonly Encoding _encoding; - private readonly Decoder _decoder; + private readonly Stream _stream; + private readonly Encoding _encoding; + private readonly Decoder _decoder; - private readonly ArrayPool _bytePool; - private readonly ArrayPool _charPool; + private readonly ArrayPool _bytePool; + private readonly ArrayPool _charPool; - private readonly int _byteBufferSize; - private readonly byte[] _byteBuffer; - private readonly char[] _charBuffer; + private readonly int _byteBufferSize; + private readonly byte[] _byteBuffer; + private readonly char[] _charBuffer; - private int _charBufferIndex; - private int _charsRead; - private int _bytesRead; + private int _charBufferIndex; + private int _charsRead; + private int _bytesRead; - private bool _isBlocked; - private bool _disposed; + private bool _isBlocked; + private bool _disposed; - /// - /// Initializes a new instance of . - /// - /// The HTTP request . - /// The character encoding to use. - public HttpRequestStreamReader(Stream stream, Encoding encoding) - : this(stream, encoding, DefaultBufferSize, ArrayPool.Shared, ArrayPool.Shared) - { - } + /// + /// Initializes a new instance of . + /// + /// The HTTP request . + /// The character encoding to use. + public HttpRequestStreamReader(Stream stream, Encoding encoding) + : this(stream, encoding, DefaultBufferSize, ArrayPool.Shared, ArrayPool.Shared) + { + } - /// - /// Initializes a new instance of . - /// - /// The HTTP request . - /// The character encoding to use. - /// The minimum buffer size. - public HttpRequestStreamReader(Stream stream, Encoding encoding, int bufferSize) - : this(stream, encoding, bufferSize, ArrayPool.Shared, ArrayPool.Shared) + /// + /// Initializes a new instance of . + /// + /// The HTTP request . + /// The character encoding to use. + /// The minimum buffer size. + public HttpRequestStreamReader(Stream stream, Encoding encoding, int bufferSize) + : this(stream, encoding, bufferSize, ArrayPool.Shared, ArrayPool.Shared) + { + } + + /// + /// Initializes a new instance of . + /// + /// The HTTP request . + /// The character encoding to use. + /// The minimum buffer size. + /// The byte array pool to use. + /// The char array pool to use. + public HttpRequestStreamReader( + Stream stream, + Encoding encoding, + int bufferSize, + ArrayPool bytePool, + ArrayPool charPool) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + _encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); + _bytePool = bytePool ?? throw new ArgumentNullException(nameof(bytePool)); + _charPool = charPool ?? throw new ArgumentNullException(nameof(charPool)); + + if (bufferSize <= 0) { + throw new ArgumentOutOfRangeException(nameof(bufferSize)); } - - /// - /// Initializes a new instance of . - /// - /// The HTTP request . - /// The character encoding to use. - /// The minimum buffer size. - /// The byte array pool to use. - /// The char array pool to use. - public HttpRequestStreamReader( - Stream stream, - Encoding encoding, - int bufferSize, - ArrayPool bytePool, - ArrayPool charPool) + if (!stream.CanRead) { - _stream = stream ?? throw new ArgumentNullException(nameof(stream)); - _encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); - _bytePool = bytePool ?? throw new ArgumentNullException(nameof(bytePool)); - _charPool = charPool ?? throw new ArgumentNullException(nameof(charPool)); + throw new ArgumentException(Resources.HttpRequestStreamReader_StreamNotReadable, nameof(stream)); + } - if (bufferSize <= 0) - { - throw new ArgumentOutOfRangeException(nameof(bufferSize)); - } - if (!stream.CanRead) - { - throw new ArgumentException(Resources.HttpRequestStreamReader_StreamNotReadable, nameof(stream)); - } + _byteBufferSize = bufferSize; - _byteBufferSize = bufferSize; + _decoder = encoding.GetDecoder(); + _byteBuffer = _bytePool.Rent(bufferSize); - _decoder = encoding.GetDecoder(); - _byteBuffer = _bytePool.Rent(bufferSize); + try + { + var requiredLength = encoding.GetMaxCharCount(bufferSize); + _charBuffer = _charPool.Rent(requiredLength); + } + catch + { + _bytePool.Return(_byteBuffer); - try + if (_charBuffer != null) { - var requiredLength = encoding.GetMaxCharCount(bufferSize); - _charBuffer = _charPool.Rent(requiredLength); + _charPool.Return(_charBuffer); } - catch - { - _bytePool.Return(_byteBuffer); - if (_charBuffer != null) - { - _charPool.Return(_charBuffer); - } - - throw; - } + throw; } + } - /// - protected override void Dispose(bool disposing) + /// + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) { - if (disposing && !_disposed) - { - _disposed = true; + _disposed = true; - _bytePool.Return(_byteBuffer); - _charPool.Return(_charBuffer); - } + _bytePool.Return(_byteBuffer); + _charPool.Return(_charBuffer); + } - base.Dispose(disposing); + base.Dispose(disposing); + } + + /// + public override int Peek() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); } - /// - public override int Peek() + if (_charBufferIndex == _charsRead) { - if (_disposed) + if (_isBlocked || ReadIntoBuffer() == 0) { - throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); + return -1; } + } - if (_charBufferIndex == _charsRead) - { - if (_isBlocked || ReadIntoBuffer() == 0) - { - return -1; - } - } + return _charBuffer[_charBufferIndex]; + } - return _charBuffer[_charBufferIndex]; + /// + public override int Read() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); } - /// - public override int Read() + if (_charBufferIndex == _charsRead) { - if (_disposed) + if (ReadIntoBuffer() == 0) { - throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); + return -1; } + } - if (_charBufferIndex == _charsRead) - { - if (ReadIntoBuffer() == 0) - { - return -1; - } - } + return _charBuffer[_charBufferIndex++]; + } - return _charBuffer[_charBufferIndex++]; + /// + public override int Read(char[] buffer, int index, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); } - /// - public override int Read(char[] buffer, int index, int count) + if (index < 0) { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } + throw new ArgumentOutOfRangeException(nameof(index)); + } - if (index < 0) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } + if (count < 0 || index + count > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } - if (count < 0 || index + count > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } + var span = new Span(buffer, index, count); + return Read(span); + } - var span = new Span(buffer, index, count); - return Read(span); + /// + public override int Read(Span buffer) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); } - /// - public override int Read(Span buffer) + var count = buffer.Length; + var charsRead = 0; + while (count > 0) { - if (buffer == null) + var charsRemaining = _charsRead - _charBufferIndex; + if (charsRemaining == 0) { - throw new ArgumentNullException(nameof(buffer)); + charsRemaining = ReadIntoBuffer(); } - if (_disposed) + if (charsRemaining == 0) { - throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); + break; // We're at EOF } - var count = buffer.Length; - var charsRead = 0; - while (count > 0) + if (charsRemaining > count) { - var charsRemaining = _charsRead - _charBufferIndex; - if (charsRemaining == 0) - { - charsRemaining = ReadIntoBuffer(); - } - - if (charsRemaining == 0) - { - break; // We're at EOF - } - - if (charsRemaining > count) - { - charsRemaining = count; - } + charsRemaining = count; + } - var source = new ReadOnlySpan(_charBuffer, _charBufferIndex, charsRemaining); - source.CopyTo(buffer); + var source = new ReadOnlySpan(_charBuffer, _charBufferIndex, charsRemaining); + source.CopyTo(buffer); - _charBufferIndex += charsRemaining; + _charBufferIndex += charsRemaining; - charsRead += charsRemaining; - count -= charsRemaining; + charsRead += charsRemaining; + count -= charsRemaining; - buffer = buffer.Slice(charsRemaining, count); + buffer = buffer.Slice(charsRemaining, count); - // If we got back fewer chars than we asked for, then it's likely the underlying stream is blocked. - // Send the data back to the caller so they can process it. - if (_isBlocked) - { - break; - } + // If we got back fewer chars than we asked for, then it's likely the underlying stream is blocked. + // Send the data back to the caller so they can process it. + if (_isBlocked) + { + break; } + } - return charsRead; + return charsRead; + } + + /// + public override Task ReadAsync(char[] buffer, int index, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); } - /// - public override Task ReadAsync(char[] buffer, int index, int count) + if (index < 0) { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } + throw new ArgumentOutOfRangeException(nameof(index)); + } - if (index < 0) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } + if (count < 0 || index + count > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } - if (count < 0 || index + count > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } + var memory = new Memory(buffer, index, count); + return ReadAsync(memory).AsTask(); + } - var memory = new Memory(buffer, index, count); - return ReadAsync(memory).AsTask(); + /// + [SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads.", Justification = "Required to maintain compatibility")] + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); } - /// - [SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads.", Justification = "Required to maintain compatibility")] - public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + if (_charBufferIndex == _charsRead && await ReadIntoBufferAsync() == 0) { - if (_disposed) - { - throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); - } + return 0; + } - if (_charBufferIndex == _charsRead && await ReadIntoBufferAsync() == 0) - { - return 0; - } + var count = buffer.Length; - var count = buffer.Length; + var charsRead = 0; + while (count > 0) + { + // n is the characters available in _charBuffer + var charsRemaining = _charsRead - _charBufferIndex; - var charsRead = 0; - while (count > 0) + // charBuffer is empty, let's read from the stream + if (charsRemaining == 0) { - // n is the characters available in _charBuffer - var charsRemaining = _charsRead - _charBufferIndex; + _charsRead = 0; + _charBufferIndex = 0; + _bytesRead = 0; - // charBuffer is empty, let's read from the stream - if (charsRemaining == 0) + // We loop here so that we read in enough bytes to yield at least 1 char. + // We break out of the loop if the stream is blocked (EOF is reached). + do { - _charsRead = 0; - _charBufferIndex = 0; - _bytesRead = 0; - - // We loop here so that we read in enough bytes to yield at least 1 char. - // We break out of the loop if the stream is blocked (EOF is reached). - do + Debug.Assert(charsRemaining == 0); + _bytesRead = await _stream.ReadAsync(_byteBuffer.AsMemory(0, _byteBufferSize), cancellationToken); + if (_bytesRead == 0) // EOF { - Debug.Assert(charsRemaining == 0); - _bytesRead = await _stream.ReadAsync(_byteBuffer.AsMemory(0, _byteBufferSize), cancellationToken); - if (_bytesRead == 0) // EOF - { - _isBlocked = true; - break; - } - - // _isBlocked == whether we read fewer bytes than we asked for. - _isBlocked = (_bytesRead < _byteBufferSize); - - Debug.Assert(charsRemaining == 0); - - _charBufferIndex = 0; - charsRemaining = _decoder.GetChars( - _byteBuffer, - 0, - _bytesRead, - _charBuffer, - 0); - - Debug.Assert(charsRemaining > 0); - - _charsRead += charsRemaining; // Number of chars in StreamReader's buffer. + _isBlocked = true; + break; } - while (charsRemaining == 0); - if (charsRemaining == 0) - { - break; // We're at EOF - } - } - - // Got more chars in charBuffer than the user requested - if (charsRemaining > count) - { - charsRemaining = count; - } + // _isBlocked == whether we read fewer bytes than we asked for. + _isBlocked = (_bytesRead < _byteBufferSize); - var source = new Memory(_charBuffer, _charBufferIndex, charsRemaining); - source.CopyTo(buffer); + Debug.Assert(charsRemaining == 0); - _charBufferIndex += charsRemaining; + _charBufferIndex = 0; + charsRemaining = _decoder.GetChars( + _byteBuffer, + 0, + _bytesRead, + _charBuffer, + 0); - charsRead += charsRemaining; - count -= charsRemaining; + Debug.Assert(charsRemaining > 0); - buffer = buffer.Slice(charsRemaining, count); + _charsRead += charsRemaining; // Number of chars in StreamReader's buffer. + } + while (charsRemaining == 0); - // This function shouldn't block for an indefinite amount of time, - // or reading from a network stream won't work right. If we got - // fewer bytes than we requested, then we want to break right here. - if (_isBlocked) + if (charsRemaining == 0) { - break; + break; // We're at EOF } } - return charsRead; - } - - /// - public override async Task ReadLineAsync() - { - if (_disposed) + // Got more chars in charBuffer than the user requested + if (charsRemaining > count) { - throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); + charsRemaining = count; } - StringBuilder? sb = null; - var consumeLineFeed = false; + var source = new Memory(_charBuffer, _charBufferIndex, charsRemaining); + source.CopyTo(buffer); - while (true) - { - if (_charBufferIndex == _charsRead) - { - if (await ReadIntoBufferAsync() == 0) - { - // reached EOF, we need to return null if we were at EOF from the beginning - return sb?.ToString(); - } - } + _charBufferIndex += charsRemaining; - var stepResult = ReadLineStep(ref sb, ref consumeLineFeed); + charsRead += charsRemaining; + count -= charsRemaining; - if (stepResult.Completed) - { - return stepResult.Result ?? sb?.ToString(); - } + buffer = buffer.Slice(charsRemaining, count); - continue; + // This function shouldn't block for an indefinite amount of time, + // or reading from a network stream won't work right. If we got + // fewer bytes than we requested, then we want to break right here. + if (_isBlocked) + { + break; } } - // Reads a line. A line is defined as a sequence of characters followed by - // a carriage return ('\r'), a line feed ('\n'), or a carriage return - // immediately followed by a line feed. The resulting string does not - // contain the terminating carriage return and/or line feed. The returned - // value is null if the end of the input stream has been reached. - /// - public override string? ReadLine() + return charsRead; + } + + /// + public override async Task ReadLineAsync() + { + if (_disposed) { - if (_disposed) - { - throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); - } + throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); + } - StringBuilder? sb = null; - var consumeLineFeed = false; + StringBuilder? sb = null; + var consumeLineFeed = false; - while (true) + while (true) + { + if (_charBufferIndex == _charsRead) { - if (_charBufferIndex == _charsRead) + if (await ReadIntoBufferAsync() == 0) { - if (ReadIntoBuffer() == 0) - { - // reached EOF, we need to return null if we were at EOF from the beginning - return sb?.ToString(); - } + // reached EOF, we need to return null if we were at EOF from the beginning + return sb?.ToString(); } + } - var stepResult = ReadLineStep(ref sb, ref consumeLineFeed); + var stepResult = ReadLineStep(ref sb, ref consumeLineFeed); - if (stepResult.Completed) - { - return stepResult.Result ?? sb?.ToString(); - } + if (stepResult.Completed) + { + return stepResult.Result ?? sb?.ToString(); } + + continue; } + } - private ReadLineStepResult ReadLineStep(ref StringBuilder? sb, ref bool consumeLineFeed) + // Reads a line. A line is defined as a sequence of characters followed by + // a carriage return ('\r'), a line feed ('\n'), or a carriage return + // immediately followed by a line feed. The resulting string does not + // contain the terminating carriage return and/or line feed. The returned + // value is null if the end of the input stream has been reached. + /// + public override string? ReadLine() + { + if (_disposed) { - const char carriageReturn = '\r'; - const char lineFeed = '\n'; + throw new ObjectDisposedException(nameof(HttpRequestStreamReader)); + } - if (consumeLineFeed) + StringBuilder? sb = null; + var consumeLineFeed = false; + + while (true) + { + if (_charBufferIndex == _charsRead) { - if (_charBuffer[_charBufferIndex] == lineFeed) + if (ReadIntoBuffer() == 0) { - _charBufferIndex++; + // reached EOF, we need to return null if we were at EOF from the beginning + return sb?.ToString(); } - return ReadLineStepResult.Done; } - var span = new Span(_charBuffer, _charBufferIndex, _charsRead - _charBufferIndex); + var stepResult = ReadLineStep(ref sb, ref consumeLineFeed); - var index = span.IndexOfAny(carriageReturn, lineFeed); + if (stepResult.Completed) + { + return stepResult.Result ?? sb?.ToString(); + } + } + } + + private ReadLineStepResult ReadLineStep(ref StringBuilder? sb, ref bool consumeLineFeed) + { + const char carriageReturn = '\r'; + const char lineFeed = '\n'; - if (index != -1) + if (consumeLineFeed) + { + if (_charBuffer[_charBufferIndex] == lineFeed) { - if (span[index] == carriageReturn) - { - span = span.Slice(0, index); - _charBufferIndex += index + 1; + _charBufferIndex++; + } + return ReadLineStepResult.Done; + } - if (_charBufferIndex < _charsRead) - { - // consume following line feed - if (_charBuffer[_charBufferIndex] == lineFeed) - { - _charBufferIndex++; - } - - if (sb != null) - { - sb.Append(span); - return ReadLineStepResult.Done; - } - - // perf: if the new line is found in first pass, we skip the StringBuilder - return ReadLineStepResult.FromResult(span.ToString()); - } + var span = new Span(_charBuffer, _charBufferIndex, _charsRead - _charBufferIndex); - // we where at the end of buffer, we need to read more to check for a line feed to consume - sb ??= new StringBuilder(); - sb.Append(span); - consumeLineFeed = true; - return ReadLineStepResult.Continue; - } + var index = span.IndexOfAny(carriageReturn, lineFeed); + + if (index != -1) + { + if (span[index] == carriageReturn) + { + span = span.Slice(0, index); + _charBufferIndex += index + 1; - if (span[index] == lineFeed) + if (_charBufferIndex < _charsRead) { - span = span.Slice(0, index); - _charBufferIndex += index + 1; + // consume following line feed + if (_charBuffer[_charBufferIndex] == lineFeed) + { + _charBufferIndex++; + } if (sb != null) { @@ -487,102 +465,123 @@ namespace Microsoft.AspNetCore.WebUtilities // perf: if the new line is found in first pass, we skip the StringBuilder return ReadLineStepResult.FromResult(span.ToString()); } - } - sb ??= new StringBuilder(); - sb.Append(span); - _charBufferIndex = _charsRead; - - return ReadLineStepResult.Continue; - } - - private int ReadIntoBuffer() - { - _charsRead = 0; - _charBufferIndex = 0; - _bytesRead = 0; + // we where at the end of buffer, we need to read more to check for a line feed to consume + sb ??= new StringBuilder(); + sb.Append(span); + consumeLineFeed = true; + return ReadLineStepResult.Continue; + } - do + if (span[index] == lineFeed) { - _bytesRead = _stream.Read(_byteBuffer, 0, _byteBufferSize); - if (_bytesRead == 0) // We're at EOF + span = span.Slice(0, index); + _charBufferIndex += index + 1; + + if (sb != null) { - return _charsRead; + sb.Append(span); + return ReadLineStepResult.Done; } - _isBlocked = (_bytesRead < _byteBufferSize); - _charsRead += _decoder.GetChars( - _byteBuffer, - 0, - _bytesRead, - _charBuffer, - _charsRead); + // perf: if the new line is found in first pass, we skip the StringBuilder + return ReadLineStepResult.FromResult(span.ToString()); } - while (_charsRead == 0); - - return _charsRead; } - private async Task ReadIntoBufferAsync() - { - _charsRead = 0; - _charBufferIndex = 0; - _bytesRead = 0; + sb ??= new StringBuilder(); + sb.Append(span); + _charBufferIndex = _charsRead; - do - { - _bytesRead = await _stream.ReadAsync(_byteBuffer.AsMemory(0, _byteBufferSize)).ConfigureAwait(false); - if (_bytesRead == 0) - { - // We're at EOF - return _charsRead; - } + return ReadLineStepResult.Continue; + } - // _isBlocked == whether we read fewer bytes than we asked for. - _isBlocked = (_bytesRead < _byteBufferSize); + private int ReadIntoBuffer() + { + _charsRead = 0; + _charBufferIndex = 0; + _bytesRead = 0; - _charsRead += _decoder.GetChars( - _byteBuffer, - 0, - _bytesRead, - _charBuffer, - _charsRead); + do + { + _bytesRead = _stream.Read(_byteBuffer, 0, _byteBufferSize); + if (_bytesRead == 0) // We're at EOF + { + return _charsRead; } - while (_charsRead == 0); - return _charsRead; + _isBlocked = (_bytesRead < _byteBufferSize); + _charsRead += _decoder.GetChars( + _byteBuffer, + 0, + _bytesRead, + _charBuffer, + _charsRead); } + while (_charsRead == 0); + + return _charsRead; + } + + private async Task ReadIntoBufferAsync() + { + _charsRead = 0; + _charBufferIndex = 0; + _bytesRead = 0; - /// - public override async Task ReadToEndAsync() + do { - StringBuilder sb = new StringBuilder(_charsRead - _charBufferIndex); - do + _bytesRead = await _stream.ReadAsync(_byteBuffer.AsMemory(0, _byteBufferSize)).ConfigureAwait(false); + if (_bytesRead == 0) { - int tmpCharPos = _charBufferIndex; - sb.Append(_charBuffer, tmpCharPos, _charsRead - tmpCharPos); - _charBufferIndex = _charsRead; // We consumed these characters - await ReadIntoBufferAsync().ConfigureAwait(false); - } while (_charsRead > 0); + // We're at EOF + return _charsRead; + } - return sb.ToString(); + // _isBlocked == whether we read fewer bytes than we asked for. + _isBlocked = (_bytesRead < _byteBufferSize); + + _charsRead += _decoder.GetChars( + _byteBuffer, + 0, + _bytesRead, + _charBuffer, + _charsRead); } + while (_charsRead == 0); - private readonly struct ReadLineStepResult + return _charsRead; + } + + /// + public override async Task ReadToEndAsync() + { + StringBuilder sb = new StringBuilder(_charsRead - _charBufferIndex); + do { - public static readonly ReadLineStepResult Done = new ReadLineStepResult(true, null); - public static readonly ReadLineStepResult Continue = new ReadLineStepResult(false, null); + int tmpCharPos = _charBufferIndex; + sb.Append(_charBuffer, tmpCharPos, _charsRead - tmpCharPos); + _charBufferIndex = _charsRead; // We consumed these characters + await ReadIntoBufferAsync().ConfigureAwait(false); + } while (_charsRead > 0); - public static ReadLineStepResult FromResult(string value) => new ReadLineStepResult(true, value); + return sb.ToString(); + } - private ReadLineStepResult(bool completed, string? result) - { - Completed = completed; - Result = result; - } + private readonly struct ReadLineStepResult + { + public static readonly ReadLineStepResult Done = new ReadLineStepResult(true, null); + public static readonly ReadLineStepResult Continue = new ReadLineStepResult(false, null); - public bool Completed { get; } - public string? Result { get; } + public static ReadLineStepResult FromResult(string value) => new ReadLineStepResult(true, value); + + private ReadLineStepResult(bool completed, string? result) + { + Completed = completed; + Result = result; } + + public bool Completed { get; } + public string? Result { get; } } } diff --git a/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs b/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs index 2c5316f2ff..6960429e85 100644 --- a/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs +++ b/src/Http/WebUtilities/src/HttpResponseStreamWriter.cs @@ -11,590 +11,589 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +/// +/// Writes to the HTTP response using the supplied . +/// It does not write the BOM and also does not close the stream. +/// +public class HttpResponseStreamWriter : TextWriter { + internal const int DefaultBufferSize = 16 * 1024; + + private readonly Stream _stream; + private readonly Encoder _encoder; + private readonly ArrayPool _bytePool; + private readonly ArrayPool _charPool; + private readonly int _charBufferSize; + + private readonly byte[] _byteBuffer; + private readonly char[] _charBuffer; + + private int _charBufferCount; + private bool _disposed; + /// - /// Writes to the HTTP response using the supplied . - /// It does not write the BOM and also does not close the stream. + /// Initializes a new instance of . /// - public class HttpResponseStreamWriter : TextWriter + /// The HTTP response . + /// The character encoding to use. + public HttpResponseStreamWriter(Stream stream, Encoding encoding) + : this(stream, encoding, DefaultBufferSize, ArrayPool.Shared, ArrayPool.Shared) { - internal const int DefaultBufferSize = 16 * 1024; - - private readonly Stream _stream; - private readonly Encoder _encoder; - private readonly ArrayPool _bytePool; - private readonly ArrayPool _charPool; - private readonly int _charBufferSize; - - private readonly byte[] _byteBuffer; - private readonly char[] _charBuffer; - - private int _charBufferCount; - private bool _disposed; - - /// - /// Initializes a new instance of . - /// - /// The HTTP response . - /// The character encoding to use. - public HttpResponseStreamWriter(Stream stream, Encoding encoding) - : this(stream, encoding, DefaultBufferSize, ArrayPool.Shared, ArrayPool.Shared) - { - } - - /// - /// Initializes a new instance of . - /// - /// The HTTP response . - /// The character encoding to use. - /// The minimum buffer size. - public HttpResponseStreamWriter(Stream stream, Encoding encoding, int bufferSize) - : this(stream, encoding, bufferSize, ArrayPool.Shared, ArrayPool.Shared) - { - } - - /// - /// Initializes a new instance of . - /// - /// The HTTP response . - /// The character encoding to use. - /// The minimum buffer size. - /// The byte array pool. - /// The char array pool. - public HttpResponseStreamWriter( - Stream stream, - Encoding encoding, - int bufferSize, - ArrayPool bytePool, - ArrayPool charPool) - { - _stream = stream ?? throw new ArgumentNullException(nameof(stream)); - Encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); - _bytePool = bytePool ?? throw new ArgumentNullException(nameof(bytePool)); - _charPool = charPool ?? throw new ArgumentNullException(nameof(charPool)); - - if (bufferSize <= 0) - { - throw new ArgumentOutOfRangeException(nameof(bufferSize)); - } - if (!_stream.CanWrite) - { - throw new ArgumentException(Resources.HttpResponseStreamWriter_StreamNotWritable, nameof(stream)); - } - - _charBufferSize = bufferSize; - - _encoder = encoding.GetEncoder(); - _charBuffer = charPool.Rent(bufferSize); + } - try - { - var requiredLength = encoding.GetMaxByteCount(bufferSize); - _byteBuffer = bytePool.Rent(requiredLength); - } - catch - { - charPool.Return(_charBuffer); + /// + /// Initializes a new instance of . + /// + /// The HTTP response . + /// The character encoding to use. + /// The minimum buffer size. + public HttpResponseStreamWriter(Stream stream, Encoding encoding, int bufferSize) + : this(stream, encoding, bufferSize, ArrayPool.Shared, ArrayPool.Shared) + { + } - if (_byteBuffer != null) - { - bytePool.Return(_byteBuffer); - } + /// + /// Initializes a new instance of . + /// + /// The HTTP response . + /// The character encoding to use. + /// The minimum buffer size. + /// The byte array pool. + /// The char array pool. + public HttpResponseStreamWriter( + Stream stream, + Encoding encoding, + int bufferSize, + ArrayPool bytePool, + ArrayPool charPool) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + Encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); + _bytePool = bytePool ?? throw new ArgumentNullException(nameof(bytePool)); + _charPool = charPool ?? throw new ArgumentNullException(nameof(charPool)); - throw; - } + if (bufferSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(bufferSize)); + } + if (!_stream.CanWrite) + { + throw new ArgumentException(Resources.HttpResponseStreamWriter_StreamNotWritable, nameof(stream)); } - /// - public override Encoding Encoding { get; } + _charBufferSize = bufferSize; + + _encoder = encoding.GetEncoder(); + _charBuffer = charPool.Rent(bufferSize); - /// - public override void Write(char value) + try { - if (_disposed) - { - throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); - } + var requiredLength = encoding.GetMaxByteCount(bufferSize); + _byteBuffer = bytePool.Rent(requiredLength); + } + catch + { + charPool.Return(_charBuffer); - if (_charBufferCount == _charBufferSize) + if (_byteBuffer != null) { - FlushInternal(flushEncoder: false); + bytePool.Return(_byteBuffer); } - _charBuffer[_charBufferCount] = value; - _charBufferCount++; + throw; } + } - /// - public override void Write(char[] values, int index, int count) + /// + public override Encoding Encoding { get; } + + /// + public override void Write(char value) + { + if (_disposed) { - if (_disposed) - { - throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); - } + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } - if (values == null) - { - return; - } + if (_charBufferCount == _charBufferSize) + { + FlushInternal(flushEncoder: false); + } - while (count > 0) - { - if (_charBufferCount == _charBufferSize) - { - FlushInternal(flushEncoder: false); - } + _charBuffer[_charBufferCount] = value; + _charBufferCount++; + } - CopyToCharBuffer(values, ref index, ref count); - } + /// + public override void Write(char[] values, int index, int count) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); } - /// - public override void Write(ReadOnlySpan value) + if (values == null) { - if (_disposed) - { - throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); - } + return; + } - var remaining = value.Length; - while (remaining > 0) + while (count > 0) + { + if (_charBufferCount == _charBufferSize) { - if (_charBufferCount == _charBufferSize) - { - FlushInternal(flushEncoder: false); - } + FlushInternal(flushEncoder: false); + } - var written = CopyToCharBuffer(value); + CopyToCharBuffer(values, ref index, ref count); + } + } - remaining -= written; - value = value.Slice(written); - }; + /// + public override void Write(ReadOnlySpan value) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); } - /// - public override void Write(string? value) + var remaining = value.Length; + while (remaining > 0) { - if (_disposed) + if (_charBufferCount == _charBufferSize) { - throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + FlushInternal(flushEncoder: false); } - if (value == null) - { - return; - } + var written = CopyToCharBuffer(value); - var count = value.Length; - var index = 0; - while (count > 0) - { - if (_charBufferCount == _charBufferSize) - { - FlushInternal(flushEncoder: false); - } + remaining -= written; + value = value.Slice(written); + }; + } - CopyToCharBuffer(value, ref index, ref count); - } + /// + public override void Write(string? value) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); } - /// - public override void WriteLine(ReadOnlySpan value) + if (value == null) { - if (_disposed) - { - throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); - } - - Write(value); - Write(NewLine); + return; } - /// - public override Task WriteAsync(char value) + var count = value.Length; + var index = 0; + while (count > 0) { - if (_disposed) - { - return GetObjectDisposedTask(); - } - if (_charBufferCount == _charBufferSize) { - return WriteAsyncAwaited(value); - } - else - { - // Enough room in buffer, no need to go async - _charBuffer[_charBufferCount] = value; - _charBufferCount++; - return Task.CompletedTask; + FlushInternal(flushEncoder: false); } + + CopyToCharBuffer(value, ref index, ref count); } + } - private async Task WriteAsyncAwaited(char value) + /// + public override void WriteLine(ReadOnlySpan value) + { + if (_disposed) { - Debug.Assert(_charBufferCount == _charBufferSize); + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } - await FlushInternalAsync(flushEncoder: false); + Write(value); + Write(NewLine); + } + /// + public override Task WriteAsync(char value) + { + if (_disposed) + { + return GetObjectDisposedTask(); + } + + if (_charBufferCount == _charBufferSize) + { + return WriteAsyncAwaited(value); + } + else + { + // Enough room in buffer, no need to go async _charBuffer[_charBufferCount] = value; _charBufferCount++; + return Task.CompletedTask; } + } - /// - public override Task WriteAsync(char[] values, int index, int count) - { - if (_disposed) - { - return GetObjectDisposedTask(); - } + private async Task WriteAsyncAwaited(char value) + { + Debug.Assert(_charBufferCount == _charBufferSize); - if (values == null || count == 0) - { - return Task.CompletedTask; - } + await FlushInternalAsync(flushEncoder: false); - var remaining = _charBufferSize - _charBufferCount; - if (remaining >= count) - { - // Enough room in buffer, no need to go async - CopyToCharBuffer(values, ref index, ref count); - return Task.CompletedTask; - } - else - { - return WriteAsyncAwaited(values, index, count); - } - } + _charBuffer[_charBufferCount] = value; + _charBufferCount++; + } - private async Task WriteAsyncAwaited(char[] values, int index, int count) + /// + public override Task WriteAsync(char[] values, int index, int count) + { + if (_disposed) { - Debug.Assert(count > 0); - Debug.Assert(_charBufferSize - _charBufferCount < count); - - while (count > 0) - { - if (_charBufferCount == _charBufferSize) - { - await FlushInternalAsync(flushEncoder: false); - } + return GetObjectDisposedTask(); + } - CopyToCharBuffer(values, ref index, ref count); - } + if (values == null || count == 0) + { + return Task.CompletedTask; } - /// - public override Task WriteAsync(string? value) + var remaining = _charBufferSize - _charBufferCount; + if (remaining >= count) { - if (_disposed) - { - return GetObjectDisposedTask(); - } + // Enough room in buffer, no need to go async + CopyToCharBuffer(values, ref index, ref count); + return Task.CompletedTask; + } + else + { + return WriteAsyncAwaited(values, index, count); + } + } - if (string.IsNullOrEmpty(value)) - { - return Task.CompletedTask; - } + private async Task WriteAsyncAwaited(char[] values, int index, int count) + { + Debug.Assert(count > 0); + Debug.Assert(_charBufferSize - _charBufferCount < count); - var remaining = _charBufferSize - _charBufferCount; - if (remaining >= value.Length) - { - // Enough room in buffer, no need to go async - CopyToCharBuffer(value); - return Task.CompletedTask; - } - else + while (count > 0) + { + if (_charBufferCount == _charBufferSize) { - return WriteAsyncAwaited(value); + await FlushInternalAsync(flushEncoder: false); } + + CopyToCharBuffer(values, ref index, ref count); } + } - private async Task WriteAsyncAwaited(string value) + /// + public override Task WriteAsync(string? value) + { + if (_disposed) { - var count = value.Length; - - Debug.Assert(count > 0); - Debug.Assert(_charBufferSize - _charBufferCount < count); - - var index = 0; - while (count > 0) - { - if (_charBufferCount == _charBufferSize) - { - await FlushInternalAsync(flushEncoder: false); - } + return GetObjectDisposedTask(); + } - CopyToCharBuffer(value, ref index, ref count); - } + if (string.IsNullOrEmpty(value)) + { + return Task.CompletedTask; } - /// - [SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads.", Justification = "Required to maintain compatibility")] - public override Task WriteAsync(ReadOnlyMemory value, CancellationToken cancellationToken = default) + var remaining = _charBufferSize - _charBufferCount; + if (remaining >= value.Length) { - if (_disposed) - { - return GetObjectDisposedTask(); - } + // Enough room in buffer, no need to go async + CopyToCharBuffer(value); + return Task.CompletedTask; + } + else + { + return WriteAsyncAwaited(value); + } + } - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } + private async Task WriteAsyncAwaited(string value) + { + var count = value.Length; - if (value.IsEmpty) - { - return Task.CompletedTask; - } + Debug.Assert(count > 0); + Debug.Assert(_charBufferSize - _charBufferCount < count); - var remaining = _charBufferSize - _charBufferCount; - if (remaining >= value.Length) - { - // Enough room in buffer, no need to go async - CopyToCharBuffer(value.Span); - return Task.CompletedTask; - } - else + var index = 0; + while (count > 0) + { + if (_charBufferCount == _charBufferSize) { - return WriteAsyncAwaited(value); + await FlushInternalAsync(flushEncoder: false); } + + CopyToCharBuffer(value, ref index, ref count); } + } - private async Task WriteAsyncAwaited(ReadOnlyMemory value) + /// + [SuppressMessage("ApiDesign", "RS0027:Public API with optional parameter(s) should have the most parameters amongst its public overloads.", Justification = "Required to maintain compatibility")] + public override Task WriteAsync(ReadOnlyMemory value, CancellationToken cancellationToken = default) + { + if (_disposed) { - Debug.Assert(value.Length > 0); - Debug.Assert(_charBufferSize - _charBufferCount < value.Length); + return GetObjectDisposedTask(); + } - var remaining = value.Length; - while (remaining > 0) - { - if (_charBufferCount == _charBufferSize) - { - await FlushInternalAsync(flushEncoder: false); - } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } - var written = CopyToCharBuffer(value.Span); + if (value.IsEmpty) + { + return Task.CompletedTask; + } - remaining -= written; - value = value.Slice(written); - }; + var remaining = _charBufferSize - _charBufferCount; + if (remaining >= value.Length) + { + // Enough room in buffer, no need to go async + CopyToCharBuffer(value.Span); + return Task.CompletedTask; + } + else + { + return WriteAsyncAwaited(value); } + } - /// - public override Task WriteLineAsync(ReadOnlyMemory value, CancellationToken cancellationToken = default) + private async Task WriteAsyncAwaited(ReadOnlyMemory value) + { + Debug.Assert(value.Length > 0); + Debug.Assert(_charBufferSize - _charBufferCount < value.Length); + + var remaining = value.Length; + while (remaining > 0) { - if (_disposed) + if (_charBufferCount == _charBufferSize) { - return GetObjectDisposedTask(); + await FlushInternalAsync(flushEncoder: false); } - if (cancellationToken.IsCancellationRequested) - { - return Task.FromCanceled(cancellationToken); - } + var written = CopyToCharBuffer(value.Span); - if (value.IsEmpty && NewLine.Length == 0) - { - return Task.CompletedTask; - } + remaining -= written; + value = value.Slice(written); + }; + } - var remaining = _charBufferSize - _charBufferCount; - if (remaining >= value.Length + NewLine.Length) - { - // Enough room in buffer, no need to go async - CopyToCharBuffer(value.Span); - CopyToCharBuffer(NewLine); - return Task.CompletedTask; - } - else - { - return WriteLineAsyncAwaited(value); - } + /// + public override Task WriteLineAsync(ReadOnlyMemory value, CancellationToken cancellationToken = default) + { + if (_disposed) + { + return GetObjectDisposedTask(); } - private async Task WriteLineAsyncAwaited(ReadOnlyMemory value) + if (cancellationToken.IsCancellationRequested) { - await WriteAsync(value); - await WriteAsync(NewLine); + return Task.FromCanceled(cancellationToken); } - // We want to flush the stream when Flush/FlushAsync is explicitly - // called by the user (example: from a Razor view). - - /// - public override void Flush() + if (value.IsEmpty && NewLine.Length == 0) { - if (_disposed) - { - throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); - } - - FlushInternal(flushEncoder: true); + return Task.CompletedTask; } - /// - public override Task FlushAsync() + var remaining = _charBufferSize - _charBufferCount; + if (remaining >= value.Length + NewLine.Length) { - if (_disposed) - { - return GetObjectDisposedTask(); - } - - return FlushInternalAsync(flushEncoder: true); + // Enough room in buffer, no need to go async + CopyToCharBuffer(value.Span); + CopyToCharBuffer(NewLine); + return Task.CompletedTask; } - - /// - protected override void Dispose(bool disposing) + else { - if (disposing && !_disposed) - { - _disposed = true; - try - { - FlushInternal(flushEncoder: true); - } - finally - { - _bytePool.Return(_byteBuffer); - _charPool.Return(_charBuffer); - } - } - - base.Dispose(disposing); + return WriteLineAsyncAwaited(value); } + } + + private async Task WriteLineAsyncAwaited(ReadOnlyMemory value) + { + await WriteAsync(value); + await WriteAsync(NewLine); + } + + // We want to flush the stream when Flush/FlushAsync is explicitly + // called by the user (example: from a Razor view). - /// - public override async ValueTask DisposeAsync() + /// + public override void Flush() + { + if (_disposed) { - if (!_disposed) - { - _disposed = true; - try - { - await FlushInternalAsync(flushEncoder: true); - } - finally - { - _bytePool.Return(_byteBuffer); - _charPool.Return(_charBuffer); - } - } + throw new ObjectDisposedException(nameof(HttpResponseStreamWriter)); + } - await base.DisposeAsync(); + FlushInternal(flushEncoder: true); + } + + /// + public override Task FlushAsync() + { + if (_disposed) + { + return GetObjectDisposedTask(); } - // Note: our FlushInternal method does NOT flush the underlying stream. This would result in - // chunking. - private void FlushInternal(bool flushEncoder) + return FlushInternalAsync(flushEncoder: true); + } + + /// + protected override void Dispose(bool disposing) + { + if (disposing && !_disposed) { - if (_charBufferCount == 0) + _disposed = true; + try { - return; + FlushInternal(flushEncoder: true); } - - var count = _encoder.GetBytes( - _charBuffer, - 0, - _charBufferCount, - _byteBuffer, - 0, - flush: flushEncoder); - - _charBufferCount = 0; - - if (count > 0) + finally { - _stream.Write(_byteBuffer, 0, count); + _bytePool.Return(_byteBuffer); + _charPool.Return(_charBuffer); } } - // Note: our FlushInternalAsync method does NOT flush the underlying stream. This would result in - // chunking. - private async Task FlushInternalAsync(bool flushEncoder) + base.Dispose(disposing); + } + + /// + public override async ValueTask DisposeAsync() + { + if (!_disposed) { - if (_charBufferCount == 0) + _disposed = true; + try { - return; + await FlushInternalAsync(flushEncoder: true); } - - var count = _encoder.GetBytes( - _charBuffer, - 0, - _charBufferCount, - _byteBuffer, - 0, - flush: flushEncoder); - - _charBufferCount = 0; - - if (count > 0) + finally { - await _stream.WriteAsync(_byteBuffer.AsMemory(0, count)); + _bytePool.Return(_byteBuffer); + _charPool.Return(_charBuffer); } } - private void CopyToCharBuffer(string value) + await base.DisposeAsync(); + } + + // Note: our FlushInternal method does NOT flush the underlying stream. This would result in + // chunking. + private void FlushInternal(bool flushEncoder) + { + if (_charBufferCount == 0) { - Debug.Assert(_charBufferSize - _charBufferCount >= value.Length); + return; + } + + var count = _encoder.GetBytes( + _charBuffer, + 0, + _charBufferCount, + _byteBuffer, + 0, + flush: flushEncoder); - value.CopyTo( - sourceIndex: 0, - destination: _charBuffer, - destinationIndex: _charBufferCount, - count: value.Length); + _charBufferCount = 0; - _charBufferCount += value.Length; + if (count > 0) + { + _stream.Write(_byteBuffer, 0, count); } + } - private void CopyToCharBuffer(string value, ref int index, ref int count) + // Note: our FlushInternalAsync method does NOT flush the underlying stream. This would result in + // chunking. + private async Task FlushInternalAsync(bool flushEncoder) + { + if (_charBufferCount == 0) { - var remaining = Math.Min(_charBufferSize - _charBufferCount, count); + return; + } - value.CopyTo( - sourceIndex: index, - destination: _charBuffer, - destinationIndex: _charBufferCount, - count: remaining); + var count = _encoder.GetBytes( + _charBuffer, + 0, + _charBufferCount, + _byteBuffer, + 0, + flush: flushEncoder); - _charBufferCount += remaining; - index += remaining; - count -= remaining; - } + _charBufferCount = 0; - private void CopyToCharBuffer(char[] values, ref int index, ref int count) + if (count > 0) { - var remaining = Math.Min(_charBufferSize - _charBufferCount, count); + await _stream.WriteAsync(_byteBuffer.AsMemory(0, count)); + } + } - Buffer.BlockCopy( - src: values, - srcOffset: index * sizeof(char), - dst: _charBuffer, - dstOffset: _charBufferCount * sizeof(char), - count: remaining * sizeof(char)); + private void CopyToCharBuffer(string value) + { + Debug.Assert(_charBufferSize - _charBufferCount >= value.Length); - _charBufferCount += remaining; - index += remaining; - count -= remaining; - } + value.CopyTo( + sourceIndex: 0, + destination: _charBuffer, + destinationIndex: _charBufferCount, + count: value.Length); - private int CopyToCharBuffer(ReadOnlySpan value) - { - var remaining = Math.Min(_charBufferSize - _charBufferCount, value.Length); + _charBufferCount += value.Length; + } - var source = value.Slice(0, remaining); - var destination = new Span(_charBuffer, _charBufferCount, remaining); - source.CopyTo(destination); + private void CopyToCharBuffer(string value, ref int index, ref int count) + { + var remaining = Math.Min(_charBufferSize - _charBufferCount, count); - _charBufferCount += remaining; + value.CopyTo( + sourceIndex: index, + destination: _charBuffer, + destinationIndex: _charBufferCount, + count: remaining); - return remaining; - } + _charBufferCount += remaining; + index += remaining; + count -= remaining; + } - [MethodImpl(MethodImplOptions.NoInlining)] - private static Task GetObjectDisposedTask() - { - return Task.FromException(new ObjectDisposedException(nameof(HttpResponseStreamWriter))); - } + private void CopyToCharBuffer(char[] values, ref int index, ref int count) + { + var remaining = Math.Min(_charBufferSize - _charBufferCount, count); + + Buffer.BlockCopy( + src: values, + srcOffset: index * sizeof(char), + dst: _charBuffer, + dstOffset: _charBufferCount * sizeof(char), + count: remaining * sizeof(char)); + + _charBufferCount += remaining; + index += remaining; + count -= remaining; + } + + private int CopyToCharBuffer(ReadOnlySpan value) + { + var remaining = Math.Min(_charBufferSize - _charBufferCount, value.Length); + + var source = value.Slice(0, remaining); + var destination = new Span(_charBuffer, _charBufferCount, remaining); + source.CopyTo(destination); + + _charBufferCount += remaining; + + return remaining; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static Task GetObjectDisposedTask() + { + return Task.FromException(new ObjectDisposedException(nameof(HttpResponseStreamWriter))); } } diff --git a/src/Http/WebUtilities/src/KeyValueAccumulator.cs b/src/Http/WebUtilities/src/KeyValueAccumulator.cs index 280e606616..3bc603c7ea 100644 --- a/src/Http/WebUtilities/src/KeyValueAccumulator.cs +++ b/src/Http/WebUtilities/src/KeyValueAccumulator.cs @@ -5,106 +5,105 @@ using System; using System.Collections.Generic; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +/// +/// This API supports infrastructure and is not intended to be used +/// directly from your code. This API may change or be removed in future releases. +/// +public struct KeyValueAccumulator { + private Dictionary _accumulator; + private Dictionary> _expandingAccumulator; + /// /// This API supports infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// - public struct KeyValueAccumulator + public void Append(string key, string value) { - private Dictionary _accumulator; - private Dictionary> _expandingAccumulator; + if (_accumulator == null) + { + _accumulator = new Dictionary(StringComparer.OrdinalIgnoreCase); + } - /// - /// This API supports infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public void Append(string key, string value) + StringValues values; + if (_accumulator.TryGetValue(key, out values)) { - if (_accumulator == null) + if (values.Count == 0) { - _accumulator = new Dictionary(StringComparer.OrdinalIgnoreCase); + // Marker entry for this key to indicate entry already in expanding list dictionary + _expandingAccumulator[key].Add(value); } - - StringValues values; - if (_accumulator.TryGetValue(key, out values)) + else if (values.Count == 1) { - if (values.Count == 0) - { - // Marker entry for this key to indicate entry already in expanding list dictionary - _expandingAccumulator[key].Add(value); - } - else if (values.Count == 1) + // Second value for this key + _accumulator[key] = new string[] { values[0]!, value }; + } + else + { + // Third value for this key + // Add zero count entry and move to data to expanding list dictionary + _accumulator[key] = default(StringValues); + + if (_expandingAccumulator == null) { - // Second value for this key - _accumulator[key] = new string[] { values[0]!, value }; + _expandingAccumulator = new Dictionary>(StringComparer.OrdinalIgnoreCase); } - else - { - // Third value for this key - // Add zero count entry and move to data to expanding list dictionary - _accumulator[key] = default(StringValues); - - if (_expandingAccumulator == null) - { - _expandingAccumulator = new Dictionary>(StringComparer.OrdinalIgnoreCase); - } - // Already 3 entries so use starting allocated as 8; then use List's expansion mechanism for more - var list = new List(8); - var array = values.ToArray(); + // Already 3 entries so use starting allocated as 8; then use List's expansion mechanism for more + var list = new List(8); + var array = values.ToArray(); - list.Add(array[0]!); - list.Add(array[1]!); - list.Add(value); + list.Add(array[0]!); + list.Add(array[1]!); + list.Add(value); - _expandingAccumulator[key] = list; - } - } - else - { - // First value for this key - _accumulator[key] = new StringValues(value); + _expandingAccumulator[key] = list; } - - ValueCount++; + } + else + { + // First value for this key + _accumulator[key] = new StringValues(value); } - /// - /// This API supports infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public bool HasValues => ValueCount > 0; + ValueCount++; + } - /// - /// This API supports infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public int KeyCount => _accumulator?.Count ?? 0; + /// + /// This API supports infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public bool HasValues => ValueCount > 0; - /// - /// This API supports infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public int ValueCount { get; private set; } + /// + /// This API supports infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public int KeyCount => _accumulator?.Count ?? 0; - /// - /// This API supports infrastructure and is not intended to be used - /// directly from your code. This API may change or be removed in future releases. - /// - public Dictionary GetResults() + /// + /// This API supports infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public int ValueCount { get; private set; } + + /// + /// This API supports infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public Dictionary GetResults() + { + if (_expandingAccumulator != null) { - if (_expandingAccumulator != null) + // Coalesce count 3+ multi-value entries into _accumulator dictionary + foreach (var entry in _expandingAccumulator) { - // Coalesce count 3+ multi-value entries into _accumulator dictionary - foreach (var entry in _expandingAccumulator) - { - _accumulator[entry.Key] = new StringValues(entry.Value.ToArray()); - } + _accumulator[entry.Key] = new StringValues(entry.Value.ToArray()); } - - return _accumulator ?? new Dictionary(0, StringComparer.OrdinalIgnoreCase); } + + return _accumulator ?? new Dictionary(0, StringComparer.OrdinalIgnoreCase); } } diff --git a/src/Http/WebUtilities/src/MultipartBoundary.cs b/src/Http/WebUtilities/src/MultipartBoundary.cs index 2b9346d2a5..a5abf1a15c 100644 --- a/src/Http/WebUtilities/src/MultipartBoundary.cs +++ b/src/Http/WebUtilities/src/MultipartBoundary.cs @@ -4,69 +4,68 @@ using System; using System.Text; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +internal class MultipartBoundary { - internal class MultipartBoundary - { - private readonly int[] _skipTable = new int[256]; - private readonly string _boundary; - private bool _expectLeadingCrlf; + private readonly int[] _skipTable = new int[256]; + private readonly string _boundary; + private bool _expectLeadingCrlf; - public MultipartBoundary(string boundary, bool expectLeadingCrlf = true) + public MultipartBoundary(string boundary, bool expectLeadingCrlf = true) + { + if (boundary == null) { - if (boundary == null) - { - throw new ArgumentNullException(nameof(boundary)); - } - - _boundary = boundary; - _expectLeadingCrlf = expectLeadingCrlf; - Initialize(_boundary, _expectLeadingCrlf); + throw new ArgumentNullException(nameof(boundary)); } - private void Initialize(string boundary, bool expectLeadingCrlf) - { - if (expectLeadingCrlf) - { - BoundaryBytes = Encoding.UTF8.GetBytes("\r\n--" + boundary); - } - else - { - BoundaryBytes = Encoding.UTF8.GetBytes("--" + boundary); - } - FinalBoundaryLength = BoundaryBytes.Length + 2; // Include the final '--' terminator. + _boundary = boundary; + _expectLeadingCrlf = expectLeadingCrlf; + Initialize(_boundary, _expectLeadingCrlf); + } - var length = BoundaryBytes.Length; - for (var i = 0; i < _skipTable.Length; ++i) - { - _skipTable[i] = length; - } - for (var i = 0; i < length; ++i) - { - _skipTable[BoundaryBytes[i]] = Math.Max(1, length - 1 - i); - } + private void Initialize(string boundary, bool expectLeadingCrlf) + { + if (expectLeadingCrlf) + { + BoundaryBytes = Encoding.UTF8.GetBytes("\r\n--" + boundary); } + else + { + BoundaryBytes = Encoding.UTF8.GetBytes("--" + boundary); + } + FinalBoundaryLength = BoundaryBytes.Length + 2; // Include the final '--' terminator. - public int GetSkipValue(byte input) + var length = BoundaryBytes.Length; + for (var i = 0; i < _skipTable.Length; ++i) + { + _skipTable[i] = length; + } + for (var i = 0; i < length; ++i) { - return _skipTable[input]; + _skipTable[BoundaryBytes[i]] = Math.Max(1, length - 1 - i); } + } + + public int GetSkipValue(byte input) + { + return _skipTable[input]; + } - public bool ExpectLeadingCrlf + public bool ExpectLeadingCrlf + { + get { return _expectLeadingCrlf; } + set { - get { return _expectLeadingCrlf; } - set + if (value != _expectLeadingCrlf) { - if (value != _expectLeadingCrlf) - { - _expectLeadingCrlf = value; - Initialize(_boundary, _expectLeadingCrlf); - } + _expectLeadingCrlf = value; + Initialize(_boundary, _expectLeadingCrlf); } } + } - public byte[] BoundaryBytes { get; private set; } = default!; // This gets initialized as part of Initialize called from in the ctor. + public byte[] BoundaryBytes { get; private set; } = default!; // This gets initialized as part of Initialize called from in the ctor. - public int FinalBoundaryLength { get; private set; } - } + public int FinalBoundaryLength { get; private set; } } diff --git a/src/Http/WebUtilities/src/MultipartReader.cs b/src/Http/WebUtilities/src/MultipartReader.cs index b2eb2efdbb..4de7165dc5 100644 --- a/src/Http/WebUtilities/src/MultipartReader.cs +++ b/src/Http/WebUtilities/src/MultipartReader.cs @@ -9,139 +9,138 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +// https://www.ietf.org/rfc/rfc2046.txt +/// +/// Reads multipart form content from the specified . +/// +public class MultipartReader { - // https://www.ietf.org/rfc/rfc2046.txt /// - /// Reads multipart form content from the specified . + /// Gets the default value for . + /// Defaults to 16‬. + /// + public const int DefaultHeadersCountLimit = 16; + + /// + /// Gets the default value for . + /// Defaults to 16,384‬ bytes‬, which is approximately 16KB. + /// + public const int DefaultHeadersLengthLimit = 1024 * 16; + private const int DefaultBufferSize = 1024 * 4; + + private readonly BufferedReadStream _stream; + private readonly MultipartBoundary _boundary; + private MultipartReaderStream _currentStream; + + /// + /// Initializes a new instance of . + /// + /// The multipart boundary. + /// The containing multipart data. + public MultipartReader(string boundary, Stream stream) + : this(boundary, stream, DefaultBufferSize) + { + } + + /// + /// Initializes a new instance of . /// - public class MultipartReader + /// The multipart boundary. + /// The containing multipart data. + /// The minimum buffer size to use. + public MultipartReader(string boundary, Stream stream, int bufferSize) { - /// - /// Gets the default value for . - /// Defaults to 16‬. - /// - public const int DefaultHeadersCountLimit = 16; - - /// - /// Gets the default value for . - /// Defaults to 16,384‬ bytes‬, which is approximately 16KB. - /// - public const int DefaultHeadersLengthLimit = 1024 * 16; - private const int DefaultBufferSize = 1024 * 4; - - private readonly BufferedReadStream _stream; - private readonly MultipartBoundary _boundary; - private MultipartReaderStream _currentStream; - - /// - /// Initializes a new instance of . - /// - /// The multipart boundary. - /// The containing multipart data. - public MultipartReader(string boundary, Stream stream) - : this(boundary, stream, DefaultBufferSize) + if (boundary == null) { + throw new ArgumentNullException(nameof(boundary)); } - /// - /// Initializes a new instance of . - /// - /// The multipart boundary. - /// The containing multipart data. - /// The minimum buffer size to use. - public MultipartReader(string boundary, Stream stream, int bufferSize) + if (stream == null) { - if (boundary == null) - { - throw new ArgumentNullException(nameof(boundary)); - } + throw new ArgumentNullException(nameof(stream)); + } - if (stream == null) - { - throw new ArgumentNullException(nameof(stream)); - } + if (bufferSize < boundary.Length + 8) // Size of the boundary + leading and trailing CRLF + leading and trailing '--' markers. + { + throw new ArgumentOutOfRangeException(nameof(bufferSize), bufferSize, "Insufficient buffer space, the buffer must be larger than the boundary: " + boundary); + } + _stream = new BufferedReadStream(stream, bufferSize); + _boundary = new MultipartBoundary(boundary, false); + // This stream will drain any preamble data and remove the first boundary marker. + // TODO: HeadersLengthLimit can't be modified until after the constructor. + _currentStream = new MultipartReaderStream(_stream, _boundary) { LengthLimit = HeadersLengthLimit }; + } - if (bufferSize < boundary.Length + 8) // Size of the boundary + leading and trailing CRLF + leading and trailing '--' markers. - { - throw new ArgumentOutOfRangeException(nameof(bufferSize), bufferSize, "Insufficient buffer space, the buffer must be larger than the boundary: " + boundary); - } - _stream = new BufferedReadStream(stream, bufferSize); - _boundary = new MultipartBoundary(boundary, false); - // This stream will drain any preamble data and remove the first boundary marker. - // TODO: HeadersLengthLimit can't be modified until after the constructor. - _currentStream = new MultipartReaderStream(_stream, _boundary) { LengthLimit = HeadersLengthLimit }; + /// + /// The limit for the number of headers to read. + /// + public int HeadersCountLimit { get; set; } = DefaultHeadersCountLimit; + + /// + /// The combined size limit for headers per multipart section. + /// + public int HeadersLengthLimit { get; set; } = DefaultHeadersLengthLimit; + + /// + /// The optional limit for the total response body length. + /// + public long? BodyLengthLimit { get; set; } + + /// + /// Reads the next . + /// + /// The token to monitor for cancellation requests. + /// The default value is . + /// + public async Task ReadNextSectionAsync(CancellationToken cancellationToken = new CancellationToken()) + { + // Drain the prior section. + await _currentStream.DrainAsync(cancellationToken); + // If we're at the end return null + if (_currentStream.FinalBoundaryFound) + { + // There may be trailer data after the last boundary. + await _stream.DrainAsync(HeadersLengthLimit, cancellationToken); + return null; } + var headers = await ReadHeadersAsync(cancellationToken); + _boundary.ExpectLeadingCrlf = true; + _currentStream = new MultipartReaderStream(_stream, _boundary) { LengthLimit = BodyLengthLimit }; + long? baseStreamOffset = _stream.CanSeek ? (long?)_stream.Position : null; + return new MultipartSection() { Headers = headers, Body = _currentStream, BaseStreamOffset = baseStreamOffset }; + } - /// - /// The limit for the number of headers to read. - /// - public int HeadersCountLimit { get; set; } = DefaultHeadersCountLimit; - - /// - /// The combined size limit for headers per multipart section. - /// - public int HeadersLengthLimit { get; set; } = DefaultHeadersLengthLimit; - - /// - /// The optional limit for the total response body length. - /// - public long? BodyLengthLimit { get; set; } - - /// - /// Reads the next . - /// - /// The token to monitor for cancellation requests. - /// The default value is . - /// - public async Task ReadNextSectionAsync(CancellationToken cancellationToken = new CancellationToken()) + private async Task> ReadHeadersAsync(CancellationToken cancellationToken) + { + int totalSize = 0; + var accumulator = new KeyValueAccumulator(); + var line = await _stream.ReadLineAsync(HeadersLengthLimit - totalSize, cancellationToken); + while (!string.IsNullOrEmpty(line)) { - // Drain the prior section. - await _currentStream.DrainAsync(cancellationToken); - // If we're at the end return null - if (_currentStream.FinalBoundaryFound) + if (HeadersLengthLimit - totalSize < line.Length) { - // There may be trailer data after the last boundary. - await _stream.DrainAsync(HeadersLengthLimit, cancellationToken); - return null; + throw new InvalidDataException($"Multipart headers length limit {HeadersLengthLimit} exceeded."); + } + totalSize += line.Length; + int splitIndex = line.IndexOf(':'); + if (splitIndex <= 0) + { + throw new InvalidDataException($"Invalid header line: {line}"); } - var headers = await ReadHeadersAsync(cancellationToken); - _boundary.ExpectLeadingCrlf = true; - _currentStream = new MultipartReaderStream(_stream, _boundary) { LengthLimit = BodyLengthLimit }; - long? baseStreamOffset = _stream.CanSeek ? (long?)_stream.Position : null; - return new MultipartSection() { Headers = headers, Body = _currentStream, BaseStreamOffset = baseStreamOffset }; - } - private async Task> ReadHeadersAsync(CancellationToken cancellationToken) - { - int totalSize = 0; - var accumulator = new KeyValueAccumulator(); - var line = await _stream.ReadLineAsync(HeadersLengthLimit - totalSize, cancellationToken); - while (!string.IsNullOrEmpty(line)) + var name = line.Substring(0, splitIndex); + var value = line.Substring(splitIndex + 1, line.Length - splitIndex - 1).Trim(); + accumulator.Append(name, value); + if (accumulator.KeyCount > HeadersCountLimit) { - if (HeadersLengthLimit - totalSize < line.Length) - { - throw new InvalidDataException($"Multipart headers length limit {HeadersLengthLimit} exceeded."); - } - totalSize += line.Length; - int splitIndex = line.IndexOf(':'); - if (splitIndex <= 0) - { - throw new InvalidDataException($"Invalid header line: {line}"); - } - - var name = line.Substring(0, splitIndex); - var value = line.Substring(splitIndex + 1, line.Length - splitIndex - 1).Trim(); - accumulator.Append(name, value); - if (accumulator.KeyCount > HeadersCountLimit) - { - throw new InvalidDataException($"Multipart headers count limit {HeadersCountLimit} exceeded."); - } - - line = await _stream.ReadLineAsync(HeadersLengthLimit - totalSize, cancellationToken); + throw new InvalidDataException($"Multipart headers count limit {HeadersCountLimit} exceeded."); } - return accumulator.GetResults(); + line = await _stream.ReadLineAsync(HeadersLengthLimit - totalSize, cancellationToken); } + + return accumulator.GetResults(); } } diff --git a/src/Http/WebUtilities/src/MultipartReaderStream.cs b/src/Http/WebUtilities/src/MultipartReaderStream.cs index 7dc0379ae0..9e472d534d 100644 --- a/src/Http/WebUtilities/src/MultipartReaderStream.cs +++ b/src/Http/WebUtilities/src/MultipartReaderStream.cs @@ -8,329 +8,328 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +internal sealed class MultipartReaderStream : Stream { - internal sealed class MultipartReaderStream : Stream + private readonly MultipartBoundary _boundary; + private readonly BufferedReadStream _innerStream; + private readonly ArrayPool _bytePool; + + private readonly long _innerOffset; + private long _position; + private long _observedLength; + private bool _finished; + + /// + /// Creates a stream that reads until it reaches the given boundary pattern. + /// + /// The . + /// The boundary pattern to use. + public MultipartReaderStream(BufferedReadStream stream, MultipartBoundary boundary) + : this(stream, boundary, ArrayPool.Shared) + { + } + + /// + /// Creates a stream that reads until it reaches the given boundary pattern. + /// + /// The . + /// The boundary pattern to use. + /// The ArrayPool pool to use for temporary byte arrays. + public MultipartReaderStream(BufferedReadStream stream, MultipartBoundary boundary, ArrayPool bytePool) { - private readonly MultipartBoundary _boundary; - private readonly BufferedReadStream _innerStream; - private readonly ArrayPool _bytePool; - - private readonly long _innerOffset; - private long _position; - private long _observedLength; - private bool _finished; - - /// - /// Creates a stream that reads until it reaches the given boundary pattern. - /// - /// The . - /// The boundary pattern to use. - public MultipartReaderStream(BufferedReadStream stream, MultipartBoundary boundary) - : this(stream, boundary, ArrayPool.Shared) + if (stream == null) { + throw new ArgumentNullException(nameof(stream)); } - /// - /// Creates a stream that reads until it reaches the given boundary pattern. - /// - /// The . - /// The boundary pattern to use. - /// The ArrayPool pool to use for temporary byte arrays. - public MultipartReaderStream(BufferedReadStream stream, MultipartBoundary boundary, ArrayPool bytePool) + if (boundary == null) { - if (stream == null) - { - throw new ArgumentNullException(nameof(stream)); - } - - if (boundary == null) - { - throw new ArgumentNullException(nameof(boundary)); - } - - _bytePool = bytePool; - _innerStream = stream; - _innerOffset = _innerStream.CanSeek ? _innerStream.Position : 0; - _boundary = boundary; + throw new ArgumentNullException(nameof(boundary)); } - public bool FinalBoundaryFound { get; private set; } + _bytePool = bytePool; + _innerStream = stream; + _innerOffset = _innerStream.CanSeek ? _innerStream.Position : 0; + _boundary = boundary; + } - public long? LengthLimit { get; set; } + public bool FinalBoundaryFound { get; private set; } - public override bool CanRead - { - get { return true; } - } + public long? LengthLimit { get; set; } - public override bool CanSeek - { - get { return _innerStream.CanSeek; } - } + public override bool CanRead + { + get { return true; } + } - public override bool CanWrite - { - get { return false; } - } + public override bool CanSeek + { + get { return _innerStream.CanSeek; } + } - public override long Length - { - get { return _observedLength; } - } + public override bool CanWrite + { + get { return false; } + } - public override long Position - { - get { return _position; } - set - { - if (value < 0) - { - throw new ArgumentOutOfRangeException(nameof(value), value, "The Position must be positive."); - } - if (value > _observedLength) - { - throw new ArgumentOutOfRangeException(nameof(value), value, "The Position must be less than length."); - } - _position = value; - if (_position < _observedLength) - { - _finished = false; - } - } - } + public override long Length + { + get { return _observedLength; } + } - public override long Seek(long offset, SeekOrigin origin) + public override long Position + { + get { return _position; } + set { - if (origin == SeekOrigin.Begin) + if (value < 0) { - Position = offset; + throw new ArgumentOutOfRangeException(nameof(value), value, "The Position must be positive."); } - else if (origin == SeekOrigin.Current) + if (value > _observedLength) { - Position = Position + offset; + throw new ArgumentOutOfRangeException(nameof(value), value, "The Position must be less than length."); } - else // if (origin == SeekOrigin.End) + _position = value; + if (_position < _observedLength) { - Position = Length + offset; + _finished = false; } - return Position; } + } - public override void SetLength(long value) + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) { - throw new NotSupportedException(); + Position = offset; } - - public override void Write(byte[] buffer, int offset, int count) + else if (origin == SeekOrigin.Current) { - throw new NotSupportedException(); + Position = Position + offset; } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + else // if (origin == SeekOrigin.End) { - throw new NotSupportedException(); + Position = Length + offset; } + return Position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } - public override void Flush() + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + private void PositionInnerStream() + { + if (_innerStream.CanSeek && _innerStream.Position != (_innerOffset + _position)) { - throw new NotSupportedException(); + _innerStream.Position = _innerOffset + _position; } + } - private void PositionInnerStream() + private int UpdatePosition(int read) + { + _position += read; + if (_observedLength < _position) { - if (_innerStream.CanSeek && _innerStream.Position != (_innerOffset + _position)) + _observedLength = _position; + if (LengthLimit.HasValue && _observedLength > LengthLimit.GetValueOrDefault()) { - _innerStream.Position = _innerOffset + _position; + throw new InvalidDataException($"Multipart body length limit {LengthLimit.GetValueOrDefault()} exceeded."); } } + return read; + } - private int UpdatePosition(int read) + public override int Read(byte[] buffer, int offset, int count) + { + if (_finished) { - _position += read; - if (_observedLength < _position) - { - _observedLength = _position; - if (LengthLimit.HasValue && _observedLength > LengthLimit.GetValueOrDefault()) - { - throw new InvalidDataException($"Multipart body length limit {LengthLimit.GetValueOrDefault()} exceeded."); - } - } - return read; + return 0; } - public override int Read(byte[] buffer, int offset, int count) + PositionInnerStream(); + if (!_innerStream.EnsureBuffered(_boundary.FinalBoundaryLength)) { - if (_finished) - { - return 0; - } + throw new IOException("Unexpected end of Stream, the content may have already been read by another component. "); + } + var bufferedData = _innerStream.BufferedData; - PositionInnerStream(); - if (!_innerStream.EnsureBuffered(_boundary.FinalBoundaryLength)) + // scan for a boundary match, full or partial. + int read; + if (SubMatch(bufferedData, _boundary.BoundaryBytes, out var matchOffset, out var matchCount)) + { + // We found a possible match, return any data before it. + if (matchOffset > bufferedData.Offset) { - throw new IOException("Unexpected end of Stream, the content may have already been read by another component. "); + read = _innerStream.Read(buffer, offset, Math.Min(count, matchOffset - bufferedData.Offset)); + return UpdatePosition(read); } - var bufferedData = _innerStream.BufferedData; - - // scan for a boundary match, full or partial. - int read; - if (SubMatch(bufferedData, _boundary.BoundaryBytes, out var matchOffset, out var matchCount)) - { - // We found a possible match, return any data before it. - if (matchOffset > bufferedData.Offset) - { - read = _innerStream.Read(buffer, offset, Math.Min(count, matchOffset - bufferedData.Offset)); - return UpdatePosition(read); - } - var length = _boundary.BoundaryBytes.Length; - Debug.Assert(matchCount == length); + var length = _boundary.BoundaryBytes.Length; + Debug.Assert(matchCount == length); - // "The boundary may be followed by zero or more characters of - // linear whitespace. It is then terminated by either another CRLF" - // or -- for the final boundary. - var boundary = _bytePool.Rent(length); - read = _innerStream.Read(boundary, 0, length); - _bytePool.Return(boundary); - Debug.Assert(read == length); // It should have all been buffered + // "The boundary may be followed by zero or more characters of + // linear whitespace. It is then terminated by either another CRLF" + // or -- for the final boundary. + var boundary = _bytePool.Rent(length); + read = _innerStream.Read(boundary, 0, length); + _bytePool.Return(boundary); + Debug.Assert(read == length); // It should have all been buffered - var remainder = _innerStream.ReadLine(lengthLimit: 100); // Whitespace may exceed the buffer. - remainder = remainder.Trim(); - if (string.Equals("--", remainder, StringComparison.Ordinal)) - { - FinalBoundaryFound = true; - } - Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); - _finished = true; - return 0; + var remainder = _innerStream.ReadLine(lengthLimit: 100); // Whitespace may exceed the buffer. + remainder = remainder.Trim(); + if (string.Equals("--", remainder, StringComparison.Ordinal)) + { + FinalBoundaryFound = true; } + Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); + _finished = true; + return 0; + } + + // No possible boundary match within the buffered data, return the data from the buffer. + read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count)); + return UpdatePosition(read); + } - // No possible boundary match within the buffered data, return the data from the buffer. - read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count)); - return UpdatePosition(read); + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_finished) + { + return 0; } - public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + PositionInnerStream(); + if (!await _innerStream.EnsureBufferedAsync(_boundary.FinalBoundaryLength, cancellationToken)) { - if (_finished) - { - return 0; - } + throw new IOException("Unexpected end of Stream, the content may have already been read by another component. "); + } + var bufferedData = _innerStream.BufferedData; - PositionInnerStream(); - if (!await _innerStream.EnsureBufferedAsync(_boundary.FinalBoundaryLength, cancellationToken)) + // scan for a boundary match, full or partial. + int matchOffset; + int matchCount; + int read; + if (SubMatch(bufferedData, _boundary.BoundaryBytes, out matchOffset, out matchCount)) + { + // We found a possible match, return any data before it. + if (matchOffset > bufferedData.Offset) { - throw new IOException("Unexpected end of Stream, the content may have already been read by another component. "); + // Sync, it's already buffered + read = _innerStream.Read(buffer, offset, Math.Min(count, matchOffset - bufferedData.Offset)); + return UpdatePosition(read); } - var bufferedData = _innerStream.BufferedData; - - // scan for a boundary match, full or partial. - int matchOffset; - int matchCount; - int read; - if (SubMatch(bufferedData, _boundary.BoundaryBytes, out matchOffset, out matchCount)) - { - // We found a possible match, return any data before it. - if (matchOffset > bufferedData.Offset) - { - // Sync, it's already buffered - read = _innerStream.Read(buffer, offset, Math.Min(count, matchOffset - bufferedData.Offset)); - return UpdatePosition(read); - } - var length = _boundary.BoundaryBytes!.Length; - Debug.Assert(matchCount == length); + var length = _boundary.BoundaryBytes!.Length; + Debug.Assert(matchCount == length); - // "The boundary may be followed by zero or more characters of - // linear whitespace. It is then terminated by either another CRLF" - // or -- for the final boundary. - var boundary = _bytePool.Rent(length); - read = _innerStream.Read(boundary, 0, length); - _bytePool.Return(boundary); - Debug.Assert(read == length); // It should have all been buffered + // "The boundary may be followed by zero or more characters of + // linear whitespace. It is then terminated by either another CRLF" + // or -- for the final boundary. + var boundary = _bytePool.Rent(length); + read = _innerStream.Read(boundary, 0, length); + _bytePool.Return(boundary); + Debug.Assert(read == length); // It should have all been buffered - var remainder = await _innerStream.ReadLineAsync(lengthLimit: 100, cancellationToken: cancellationToken); // Whitespace may exceed the buffer. - remainder = remainder.Trim(); - if (string.Equals("--", remainder, StringComparison.Ordinal)) - { - FinalBoundaryFound = true; - } - Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); - - _finished = true; - return 0; + var remainder = await _innerStream.ReadLineAsync(lengthLimit: 100, cancellationToken: cancellationToken); // Whitespace may exceed the buffer. + remainder = remainder.Trim(); + if (string.Equals("--", remainder, StringComparison.Ordinal)) + { + FinalBoundaryFound = true; } + Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder); - // No possible boundary match within the buffered data, return the data from the buffer. - read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count)); - return UpdatePosition(read); + _finished = true; + return 0; } - // Does segment1 contain all of matchBytes, or does it end with the start of matchBytes? - // 1: AAAAABBBBBCCCCC - // 2: BBBBB - // Or: - // 1: AAAAABBB - // 2: BBBBB - private bool SubMatch(ArraySegment segment1, byte[] matchBytes, out int matchOffset, out int matchCount) + // No possible boundary match within the buffered data, return the data from the buffer. + read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count)); + return UpdatePosition(read); + } + + // Does segment1 contain all of matchBytes, or does it end with the start of matchBytes? + // 1: AAAAABBBBBCCCCC + // 2: BBBBB + // Or: + // 1: AAAAABBB + // 2: BBBBB + private bool SubMatch(ArraySegment segment1, byte[] matchBytes, out int matchOffset, out int matchCount) + { + // clear matchCount to zero + matchCount = 0; + + // case 1: does segment1 fully contain matchBytes? { - // clear matchCount to zero - matchCount = 0; + var matchBytesLengthMinusOne = matchBytes.Length - 1; + var matchBytesLastByte = matchBytes[matchBytesLengthMinusOne]; + var segmentEndMinusMatchBytesLength = segment1.Offset + segment1.Count - matchBytes.Length; - // case 1: does segment1 fully contain matchBytes? + matchOffset = segment1.Offset; + while (matchOffset < segmentEndMinusMatchBytesLength) { - var matchBytesLengthMinusOne = matchBytes.Length - 1; - var matchBytesLastByte = matchBytes[matchBytesLengthMinusOne]; - var segmentEndMinusMatchBytesLength = segment1.Offset + segment1.Count - matchBytes.Length; - - matchOffset = segment1.Offset; - while (matchOffset < segmentEndMinusMatchBytesLength) + var lookaheadTailChar = segment1.Array![matchOffset + matchBytesLengthMinusOne]; + if (lookaheadTailChar == matchBytesLastByte && + CompareBuffers(segment1.Array, matchOffset, matchBytes, 0, matchBytesLengthMinusOne) == 0) { - var lookaheadTailChar = segment1.Array![matchOffset + matchBytesLengthMinusOne]; - if (lookaheadTailChar == matchBytesLastByte && - CompareBuffers(segment1.Array, matchOffset, matchBytes, 0, matchBytesLengthMinusOne) == 0) - { - matchCount = matchBytes.Length; - return true; - } - matchOffset += _boundary.GetSkipValue(lookaheadTailChar); + matchCount = matchBytes.Length; + return true; } + matchOffset += _boundary.GetSkipValue(lookaheadTailChar); } + } - // case 2: does segment1 end with the start of matchBytes? - var segmentEnd = segment1.Offset + segment1.Count; + // case 2: does segment1 end with the start of matchBytes? + var segmentEnd = segment1.Offset + segment1.Count; - matchCount = 0; - for (; matchOffset < segmentEnd; matchOffset++) + matchCount = 0; + for (; matchOffset < segmentEnd; matchOffset++) + { + var countLimit = segmentEnd - matchOffset; + for (matchCount = 0; matchCount < matchBytes.Length && matchCount < countLimit; matchCount++) { - var countLimit = segmentEnd - matchOffset; - for (matchCount = 0; matchCount < matchBytes.Length && matchCount < countLimit; matchCount++) - { - if (matchBytes[matchCount] != segment1.Array![matchOffset + matchCount]) - { - matchCount = 0; - break; - } - } - if (matchCount > 0) + if (matchBytes[matchCount] != segment1.Array![matchOffset + matchCount]) { + matchCount = 0; break; } } - return matchCount > 0; + if (matchCount > 0) + { + break; + } } + return matchCount > 0; + } - private static int CompareBuffers(byte[] buffer1, int offset1, byte[] buffer2, int offset2, int count) + private static int CompareBuffers(byte[] buffer1, int offset1, byte[] buffer2, int offset2, int count) + { + for (; count-- > 0; offset1++, offset2++) { - for (; count-- > 0; offset1++, offset2++) + if (buffer1[offset1] != buffer2[offset2]) { - if (buffer1[offset1] != buffer2[offset2]) - { - return buffer1[offset1] - buffer2[offset2]; - } + return buffer1[offset1] - buffer2[offset2]; } - return 0; } + return 0; } } diff --git a/src/Http/WebUtilities/src/MultipartSection.cs b/src/Http/WebUtilities/src/MultipartSection.cs index 4808075a17..6db3c40516 100644 --- a/src/Http/WebUtilities/src/MultipartSection.cs +++ b/src/Http/WebUtilities/src/MultipartSection.cs @@ -6,57 +6,56 @@ using System.IO; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +/// +/// A multipart section read by . +/// +public class MultipartSection { /// - /// A multipart section read by . + /// Gets the value of the Content-Type header. /// - public class MultipartSection + public string? ContentType { - /// - /// Gets the value of the Content-Type header. - /// - public string? ContentType + get { - get + if (Headers != null && Headers.TryGetValue(HeaderNames.ContentType, out var values)) { - if (Headers != null && Headers.TryGetValue(HeaderNames.ContentType, out var values)) - { - return values; - } - return null; + return values; } + return null; } + } - /// - /// Gets the value of the Content-Disposition header. - /// - public string? ContentDisposition + /// + /// Gets the value of the Content-Disposition header. + /// + public string? ContentDisposition + { + get { - get + if (Headers != null && Headers.TryGetValue(HeaderNames.ContentDisposition, out var values)) { - if (Headers != null && Headers.TryGetValue(HeaderNames.ContentDisposition, out var values)) - { - return values; - } - return null; + return values; } + return null; } + } - /// - /// Gets or sets the multipart header collection. - /// - public Dictionary? Headers { get; set; } + /// + /// Gets or sets the multipart header collection. + /// + public Dictionary? Headers { get; set; } - /// - /// Gets or sets the body. - /// - public Stream Body { get; set; } = default!; + /// + /// Gets or sets the body. + /// + public Stream Body { get; set; } = default!; - /// - /// The position where the body starts in the total multipart body. - /// This may not be available if the total multipart body is not seekable. - /// - public long? BaseStreamOffset { get; set; } - } + /// + /// The position where the body starts in the total multipart body. + /// This may not be available if the total multipart body is not seekable. + /// + public long? BaseStreamOffset { get; set; } } diff --git a/src/Http/WebUtilities/src/MultipartSectionConverterExtensions.cs b/src/Http/WebUtilities/src/MultipartSectionConverterExtensions.cs index e690160ee6..6e53f8e896 100644 --- a/src/Http/WebUtilities/src/MultipartSectionConverterExtensions.cs +++ b/src/Http/WebUtilities/src/MultipartSectionConverterExtensions.cs @@ -4,70 +4,69 @@ using System; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +/// +/// Various extensions for converting multipart sections +/// +public static class MultipartSectionConverterExtensions { /// - /// Various extensions for converting multipart sections + /// Converts the section to a file section /// - public static class MultipartSectionConverterExtensions + /// The section to convert + /// A file section + public static FileMultipartSection? AsFileSection(this MultipartSection section) { - /// - /// Converts the section to a file section - /// - /// The section to convert - /// A file section - public static FileMultipartSection? AsFileSection(this MultipartSection section) + if (section == null) { - if (section == null) - { - throw new ArgumentNullException(nameof(section)); - } - - try - { - return new FileMultipartSection(section); - } - catch - { - return null; - } + throw new ArgumentNullException(nameof(section)); } - /// - /// Converts the section to a form section - /// - /// The section to convert - /// A form section - public static FormMultipartSection? AsFormDataSection(this MultipartSection section) + try { - if (section == null) - { - throw new ArgumentNullException(nameof(section)); - } + return new FileMultipartSection(section); + } + catch + { + return null; + } + } - try - { - return new FormMultipartSection(section); - } - catch - { - return null; - } + /// + /// Converts the section to a form section + /// + /// The section to convert + /// A form section + public static FormMultipartSection? AsFormDataSection(this MultipartSection section) + { + if (section == null) + { + throw new ArgumentNullException(nameof(section)); } - /// - /// Retrieves and parses the content disposition header from a section - /// - /// The section from which to retrieve - /// A if the header was found, null otherwise - public static ContentDispositionHeaderValue? GetContentDispositionHeader(this MultipartSection section) + try { - if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var header)) - { - return null; - } + return new FormMultipartSection(section); + } + catch + { + return null; + } + } - return header; + /// + /// Retrieves and parses the content disposition header from a section + /// + /// The section from which to retrieve + /// A if the header was found, null otherwise + public static ContentDispositionHeaderValue? GetContentDispositionHeader(this MultipartSection section) + { + if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var header)) + { + return null; } + + return header; } } diff --git a/src/Http/WebUtilities/src/MultipartSectionStreamExtensions.cs b/src/Http/WebUtilities/src/MultipartSectionStreamExtensions.cs index d692a0341c..3eccad1df7 100644 --- a/src/Http/WebUtilities/src/MultipartSectionStreamExtensions.cs +++ b/src/Http/WebUtilities/src/MultipartSectionStreamExtensions.cs @@ -7,48 +7,47 @@ using System.Text; using System.Threading.Tasks; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +/// +/// Various extension methods for dealing with the section body stream +/// +public static class MultipartSectionStreamExtensions { /// - /// Various extension methods for dealing with the section body stream + /// Reads the body of the section as a string /// - public static class MultipartSectionStreamExtensions + /// The section to read from + /// The body steam as string + public static async Task ReadAsStringAsync(this MultipartSection section) { - /// - /// Reads the body of the section as a string - /// - /// The section to read from - /// The body steam as string - public static async Task ReadAsStringAsync(this MultipartSection section) + if (section == null) { - if (section == null) - { - throw new ArgumentNullException(nameof(section)); - } + throw new ArgumentNullException(nameof(section)); + } - if (section.Body is null) - { - throw new ArgumentException("Multipart section must have a body to be read.", nameof(section)); - } + if (section.Body is null) + { + throw new ArgumentException("Multipart section must have a body to be read.", nameof(section)); + } - MediaTypeHeaderValue.TryParse(section.ContentType, out var sectionMediaType); + MediaTypeHeaderValue.TryParse(section.ContentType, out var sectionMediaType); - var streamEncoding = sectionMediaType?.Encoding; - // https://docs.microsoft.com/en-us/dotnet/core/compatibility/syslib-warnings/syslib0001 - if (streamEncoding == null || streamEncoding.CodePage == 65000) - { - streamEncoding = Encoding.UTF8; - } + var streamEncoding = sectionMediaType?.Encoding; + // https://docs.microsoft.com/en-us/dotnet/core/compatibility/syslib-warnings/syslib0001 + if (streamEncoding == null || streamEncoding.CodePage == 65000) + { + streamEncoding = Encoding.UTF8; + } - using (var reader = new StreamReader( - section.Body, - streamEncoding, - detectEncodingFromByteOrderMarks: true, - bufferSize: 1024, - leaveOpen: true)) - { - return await reader.ReadToEndAsync(); - } + using (var reader = new StreamReader( + section.Body, + streamEncoding, + detectEncodingFromByteOrderMarks: true, + bufferSize: 1024, + leaveOpen: true)) + { + return await reader.ReadToEndAsync(); } } } diff --git a/src/Http/WebUtilities/src/PagedByteBuffer.cs b/src/Http/WebUtilities/src/PagedByteBuffer.cs index 8410be771a..0a22364e34 100644 --- a/src/Http/WebUtilities/src/PagedByteBuffer.cs +++ b/src/Http/WebUtilities/src/PagedByteBuffer.cs @@ -9,142 +9,141 @@ using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +internal sealed class PagedByteBuffer : IDisposable { - internal sealed class PagedByteBuffer : IDisposable - { - internal const int PageSize = 1024; - private readonly ArrayPool _arrayPool; - private byte[]? _currentPage; - private int _currentPageIndex; + internal const int PageSize = 1024; + private readonly ArrayPool _arrayPool; + private byte[]? _currentPage; + private int _currentPageIndex; - public PagedByteBuffer(ArrayPool arrayPool) - { - _arrayPool = arrayPool; - Pages = new List(); - } + public PagedByteBuffer(ArrayPool arrayPool) + { + _arrayPool = arrayPool; + Pages = new List(); + } - public int Length { get; private set; } + public int Length { get; private set; } - internal bool Disposed { get; private set; } + internal bool Disposed { get; private set; } - internal List Pages { get; } + internal List Pages { get; } - private byte[] CurrentPage + private byte[] CurrentPage + { + get { - get + if (_currentPage == null || _currentPageIndex == _currentPage.Length) { - if (_currentPage == null || _currentPageIndex == _currentPage.Length) - { - _currentPage = _arrayPool.Rent(PageSize); - Pages.Add(_currentPage); - _currentPageIndex = 0; - } - - return _currentPage; + _currentPage = _arrayPool.Rent(PageSize); + Pages.Add(_currentPage); + _currentPageIndex = 0; } + + return _currentPage; } + } - public void Add(byte[] buffer, int offset, int count) - => Add(buffer.AsMemory(offset, count)); + public void Add(byte[] buffer, int offset, int count) + => Add(buffer.AsMemory(offset, count)); - public void Add(ReadOnlyMemory memory) - { - ThrowIfDisposed(); + public void Add(ReadOnlyMemory memory) + { + ThrowIfDisposed(); - while (!memory.IsEmpty) - { - var currentPage = CurrentPage; - var copyLength = Math.Min(memory.Length, currentPage.Length - _currentPageIndex); + while (!memory.IsEmpty) + { + var currentPage = CurrentPage; + var copyLength = Math.Min(memory.Length, currentPage.Length - _currentPageIndex); - memory.Slice(0, copyLength).CopyTo(currentPage.AsMemory(_currentPageIndex, copyLength)); + memory.Slice(0, copyLength).CopyTo(currentPage.AsMemory(_currentPageIndex, copyLength)); - Length += copyLength; - _currentPageIndex += copyLength; + Length += copyLength; + _currentPageIndex += copyLength; - memory = memory.Slice(copyLength); - } + memory = memory.Slice(copyLength); } + } + + public void MoveTo(Stream stream) + { + ThrowIfDisposed(); - public void MoveTo(Stream stream) + for (var i = 0; i < Pages.Count; i++) { - ThrowIfDisposed(); + var page = Pages[i]; + var length = (i == Pages.Count - 1) ? + _currentPageIndex : + page.Length; - for (var i = 0; i < Pages.Count; i++) - { - var page = Pages[i]; - var length = (i == Pages.Count - 1) ? - _currentPageIndex : - page.Length; + stream.Write(page, 0, length); + } - stream.Write(page, 0, length); - } + ClearBuffers(); + } - ClearBuffers(); - } + public async Task MoveToAsync(PipeWriter writer, CancellationToken cancellationToken) + { + ThrowIfDisposed(); - public async Task MoveToAsync(PipeWriter writer, CancellationToken cancellationToken) + for (var i = 0; i < Pages.Count; i++) { - ThrowIfDisposed(); + var page = Pages[i]; + var length = (i == Pages.Count - 1) ? + _currentPageIndex : + page.Length; - for (var i = 0; i < Pages.Count; i++) - { - var page = Pages[i]; - var length = (i == Pages.Count - 1) ? - _currentPageIndex : - page.Length; + await writer.WriteAsync(page.AsMemory(0, length), cancellationToken); + } - await writer.WriteAsync(page.AsMemory(0, length), cancellationToken); - } + ClearBuffers(); + } - ClearBuffers(); - } + public async Task MoveToAsync(Stream stream, CancellationToken cancellationToken) + { + ThrowIfDisposed(); - public async Task MoveToAsync(Stream stream, CancellationToken cancellationToken) + for (var i = 0; i < Pages.Count; i++) { - ThrowIfDisposed(); + var page = Pages[i]; + var length = (i == Pages.Count - 1) ? + _currentPageIndex : + page.Length; - for (var i = 0; i < Pages.Count; i++) - { - var page = Pages[i]; - var length = (i == Pages.Count - 1) ? - _currentPageIndex : - page.Length; + await stream.WriteAsync(page.AsMemory(0, length), cancellationToken); + } - await stream.WriteAsync(page.AsMemory(0, length), cancellationToken); - } + ClearBuffers(); + } + public void Dispose() + { + if (!Disposed) + { + Disposed = true; ClearBuffers(); } + } - public void Dispose() + private void ClearBuffers() + { + for (var i = 0; i < Pages.Count; i++) { - if (!Disposed) - { - Disposed = true; - ClearBuffers(); - } + _arrayPool.Return(Pages[i]); } - private void ClearBuffers() - { - for (var i = 0; i < Pages.Count; i++) - { - _arrayPool.Return(Pages[i]); - } - - Pages.Clear(); - _currentPage = null; - Length = 0; - _currentPageIndex = 0; - } + Pages.Clear(); + _currentPage = null; + Length = 0; + _currentPageIndex = 0; + } - private void ThrowIfDisposed() + private void ThrowIfDisposed() + { + if (Disposed) { - if (Disposed) - { - throw new ObjectDisposedException(nameof(PagedByteBuffer)); - } + throw new ObjectDisposedException(nameof(PagedByteBuffer)); } } } diff --git a/src/Http/WebUtilities/src/QueryHelpers.cs b/src/Http/WebUtilities/src/QueryHelpers.cs index 48ecd616bf..133cbdcb73 100644 --- a/src/Http/WebUtilities/src/QueryHelpers.cs +++ b/src/Http/WebUtilities/src/QueryHelpers.cs @@ -9,183 +9,182 @@ using System.Text.Encodings.Web; using Microsoft.AspNetCore.Internal; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +/// +/// Provides methods for parsing and manipulating query strings. +/// +public static class QueryHelpers { /// - /// Provides methods for parsing and manipulating query strings. + /// Append the given query key and value to the URI. /// - public static class QueryHelpers + /// The base URI. + /// The name of the query key. + /// The query value. + /// The combined result. + /// is null. + /// is null. + /// is null. + public static string AddQueryString(string uri, string name, string value) { - /// - /// Append the given query key and value to the URI. - /// - /// The base URI. - /// The name of the query key. - /// The query value. - /// The combined result. - /// is null. - /// is null. - /// is null. - public static string AddQueryString(string uri, string name, string value) + if (uri == null) { - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } + throw new ArgumentNullException(nameof(uri)); + } - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + return AddQueryString( + uri, new[] { new KeyValuePair(name, value) }); + } - return AddQueryString( - uri, new[] { new KeyValuePair(name, value) }); + /// + /// Append the given query keys and values to the URI. + /// + /// The base URI. + /// A dictionary of query keys and values to append. + /// The combined result. + /// is null. + /// is null. + public static string AddQueryString(string uri, IDictionary queryString) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); } - /// - /// Append the given query keys and values to the URI. - /// - /// The base URI. - /// A dictionary of query keys and values to append. - /// The combined result. - /// is null. - /// is null. - public static string AddQueryString(string uri, IDictionary queryString) + if (queryString == null) { - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } + throw new ArgumentNullException(nameof(queryString)); + } - if (queryString == null) - { - throw new ArgumentNullException(nameof(queryString)); - } + return AddQueryString(uri, (IEnumerable>)queryString); + } - return AddQueryString(uri, (IEnumerable>)queryString); + /// + /// Append the given query keys and values to the URI. + /// + /// The base URI. + /// A collection of query names and values to append. + /// The combined result. + /// is null. + /// is null. + public static string AddQueryString(string uri, IEnumerable> queryString) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); } - /// - /// Append the given query keys and values to the URI. - /// - /// The base URI. - /// A collection of query names and values to append. - /// The combined result. - /// is null. - /// is null. - public static string AddQueryString(string uri, IEnumerable> queryString) + if (queryString == null) { - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } + throw new ArgumentNullException(nameof(queryString)); + } - if (queryString == null) - { - throw new ArgumentNullException(nameof(queryString)); - } + return AddQueryString(uri, queryString.SelectMany(kvp => kvp.Value, (kvp, v) => KeyValuePair.Create(kvp.Key, v))); + } - return AddQueryString(uri, queryString.SelectMany(kvp => kvp.Value, (kvp, v) => KeyValuePair.Create(kvp.Key, v))); + /// + /// Append the given query keys and values to the URI. + /// + /// The base URI. + /// A collection of name value query pairs to append. + /// The combined result. + /// is null. + /// is null. + public static string AddQueryString( + string uri, + IEnumerable> queryString) + { + if (uri == null) + { + throw new ArgumentNullException(nameof(uri)); } - /// - /// Append the given query keys and values to the URI. - /// - /// The base URI. - /// A collection of name value query pairs to append. - /// The combined result. - /// is null. - /// is null. - public static string AddQueryString( - string uri, - IEnumerable> queryString) + if (queryString == null) { - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } - - if (queryString == null) - { - throw new ArgumentNullException(nameof(queryString)); - } + throw new ArgumentNullException(nameof(queryString)); + } - var anchorIndex = uri.IndexOf('#'); - var uriToBeAppended = uri; - var anchorText = ""; - // If there is an anchor, then the query string must be inserted before its first occurrence. - if (anchorIndex != -1) - { - anchorText = uri.Substring(anchorIndex); - uriToBeAppended = uri.Substring(0, anchorIndex); - } + var anchorIndex = uri.IndexOf('#'); + var uriToBeAppended = uri; + var anchorText = ""; + // If there is an anchor, then the query string must be inserted before its first occurrence. + if (anchorIndex != -1) + { + anchorText = uri.Substring(anchorIndex); + uriToBeAppended = uri.Substring(0, anchorIndex); + } - var queryIndex = uriToBeAppended.IndexOf('?'); - var hasQuery = queryIndex != -1; + var queryIndex = uriToBeAppended.IndexOf('?'); + var hasQuery = queryIndex != -1; - var sb = new StringBuilder(); - sb.Append(uriToBeAppended); - foreach (var parameter in queryString) + var sb = new StringBuilder(); + sb.Append(uriToBeAppended); + foreach (var parameter in queryString) + { + if (parameter.Value == null) { - if (parameter.Value == null) - { - continue; - } - - sb.Append(hasQuery ? '&' : '?'); - sb.Append(UrlEncoder.Default.Encode(parameter.Key)); - sb.Append('='); - sb.Append(UrlEncoder.Default.Encode(parameter.Value)); - hasQuery = true; + continue; } - sb.Append(anchorText); - return sb.ToString(); + sb.Append(hasQuery ? '&' : '?'); + sb.Append(UrlEncoder.Default.Encode(parameter.Key)); + sb.Append('='); + sb.Append(UrlEncoder.Default.Encode(parameter.Value)); + hasQuery = true; } - /// - /// Parse a query string into its component key and value parts. - /// - /// The raw query string value, with or without the leading '?'. - /// A collection of parsed keys and values. - public static Dictionary ParseQuery(string? queryString) - { - var result = ParseNullableQuery(queryString); + sb.Append(anchorText); + return sb.ToString(); + } - if (result == null) - { - return new Dictionary(); - } + /// + /// Parse a query string into its component key and value parts. + /// + /// The raw query string value, with or without the leading '?'. + /// A collection of parsed keys and values. + public static Dictionary ParseQuery(string? queryString) + { + var result = ParseNullableQuery(queryString); - return result; + if (result == null) + { + return new Dictionary(); } - /// - /// Parse a query string into its component key and value parts. - /// - /// The raw query string value, with or without the leading '?'. - /// A collection of parsed keys and values, null if there are no entries. - public static Dictionary? ParseNullableQuery(string? queryString) - { - var accumulator = new KeyValueAccumulator(); - var enumerable = new QueryStringEnumerable(queryString); + return result; + } - foreach (var pair in enumerable) - { - accumulator.Append(pair.DecodeName().ToString(), pair.DecodeValue().ToString()); - } + /// + /// Parse a query string into its component key and value parts. + /// + /// The raw query string value, with or without the leading '?'. + /// A collection of parsed keys and values, null if there are no entries. + public static Dictionary? ParseNullableQuery(string? queryString) + { + var accumulator = new KeyValueAccumulator(); + var enumerable = new QueryStringEnumerable(queryString); - if (!accumulator.HasValues) - { - return null; - } + foreach (var pair in enumerable) + { + accumulator.Append(pair.DecodeName().ToString(), pair.DecodeValue().ToString()); + } - return accumulator.GetResults(); + if (!accumulator.HasValues) + { + return null; } + + return accumulator.GetResults(); } } diff --git a/src/Http/WebUtilities/src/ReasonPhrases.cs b/src/Http/WebUtilities/src/ReasonPhrases.cs index 08178d3ba4..5f0f5161e4 100644 --- a/src/Http/WebUtilities/src/ReasonPhrases.cs +++ b/src/Http/WebUtilities/src/ReasonPhrases.cs @@ -3,93 +3,92 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +/// +/// Provides access to HTTP status code reason phrases as listed in +/// http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml. +/// +public static class ReasonPhrases { - /// - /// Provides access to HTTP status code reason phrases as listed in - /// http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml. - /// - public static class ReasonPhrases + // Status Codes listed at http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + private static readonly Dictionary Phrases = new() { - // Status Codes listed at http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml - private static readonly Dictionary Phrases = new() - { - { 100, "Continue" }, - { 101, "Switching Protocols" }, - { 102, "Processing" }, + { 100, "Continue" }, + { 101, "Switching Protocols" }, + { 102, "Processing" }, - { 200, "OK" }, - { 201, "Created" }, - { 202, "Accepted" }, - { 203, "Non-Authoritative Information" }, - { 204, "No Content" }, - { 205, "Reset Content" }, - { 206, "Partial Content" }, - { 207, "Multi-Status" }, - { 208, "Already Reported" }, - { 226, "IM Used" }, + { 200, "OK" }, + { 201, "Created" }, + { 202, "Accepted" }, + { 203, "Non-Authoritative Information" }, + { 204, "No Content" }, + { 205, "Reset Content" }, + { 206, "Partial Content" }, + { 207, "Multi-Status" }, + { 208, "Already Reported" }, + { 226, "IM Used" }, - { 300, "Multiple Choices" }, - { 301, "Moved Permanently" }, - { 302, "Found" }, - { 303, "See Other" }, - { 304, "Not Modified" }, - { 305, "Use Proxy" }, - { 306, "Switch Proxy" }, - { 307, "Temporary Redirect" }, - { 308, "Permanent Redirect" }, + { 300, "Multiple Choices" }, + { 301, "Moved Permanently" }, + { 302, "Found" }, + { 303, "See Other" }, + { 304, "Not Modified" }, + { 305, "Use Proxy" }, + { 306, "Switch Proxy" }, + { 307, "Temporary Redirect" }, + { 308, "Permanent Redirect" }, - { 400, "Bad Request" }, - { 401, "Unauthorized" }, - { 402, "Payment Required" }, - { 403, "Forbidden" }, - { 404, "Not Found" }, - { 405, "Method Not Allowed" }, - { 406, "Not Acceptable" }, - { 407, "Proxy Authentication Required" }, - { 408, "Request Timeout" }, - { 409, "Conflict" }, - { 410, "Gone" }, - { 411, "Length Required" }, - { 412, "Precondition Failed" }, - { 413, "Payload Too Large" }, - { 414, "URI Too Long" }, - { 415, "Unsupported Media Type" }, - { 416, "Range Not Satisfiable" }, - { 417, "Expectation Failed" }, - { 418, "I'm a teapot" }, - { 419, "Authentication Timeout" }, - { 421, "Misdirected Request" }, - { 422, "Unprocessable Entity" }, - { 423, "Locked" }, - { 424, "Failed Dependency" }, - { 426, "Upgrade Required" }, - { 428, "Precondition Required" }, - { 429, "Too Many Requests" }, - { 431, "Request Header Fields Too Large" }, - { 451, "Unavailable For Legal Reasons" }, + { 400, "Bad Request" }, + { 401, "Unauthorized" }, + { 402, "Payment Required" }, + { 403, "Forbidden" }, + { 404, "Not Found" }, + { 405, "Method Not Allowed" }, + { 406, "Not Acceptable" }, + { 407, "Proxy Authentication Required" }, + { 408, "Request Timeout" }, + { 409, "Conflict" }, + { 410, "Gone" }, + { 411, "Length Required" }, + { 412, "Precondition Failed" }, + { 413, "Payload Too Large" }, + { 414, "URI Too Long" }, + { 415, "Unsupported Media Type" }, + { 416, "Range Not Satisfiable" }, + { 417, "Expectation Failed" }, + { 418, "I'm a teapot" }, + { 419, "Authentication Timeout" }, + { 421, "Misdirected Request" }, + { 422, "Unprocessable Entity" }, + { 423, "Locked" }, + { 424, "Failed Dependency" }, + { 426, "Upgrade Required" }, + { 428, "Precondition Required" }, + { 429, "Too Many Requests" }, + { 431, "Request Header Fields Too Large" }, + { 451, "Unavailable For Legal Reasons" }, - { 500, "Internal Server Error" }, - { 501, "Not Implemented" }, - { 502, "Bad Gateway" }, - { 503, "Service Unavailable" }, - { 504, "Gateway Timeout" }, - { 505, "HTTP Version Not Supported" }, - { 506, "Variant Also Negotiates" }, - { 507, "Insufficient Storage" }, - { 508, "Loop Detected" }, - { 510, "Not Extended" }, - { 511, "Network Authentication Required" }, - }; + { 500, "Internal Server Error" }, + { 501, "Not Implemented" }, + { 502, "Bad Gateway" }, + { 503, "Service Unavailable" }, + { 504, "Gateway Timeout" }, + { 505, "HTTP Version Not Supported" }, + { 506, "Variant Also Negotiates" }, + { 507, "Insufficient Storage" }, + { 508, "Loop Detected" }, + { 510, "Not Extended" }, + { 511, "Network Authentication Required" }, + }; - /// - /// Gets the reason phrase for the specified status code. - /// - /// The status code. - /// The reason phrase, or if the status code is unknown. - public static string GetReasonPhrase(int statusCode) - { - return Phrases.TryGetValue(statusCode, out var phrase) ? phrase : string.Empty; - } + /// + /// Gets the reason phrase for the specified status code. + /// + /// The status code. + /// The reason phrase, or if the status code is unknown. + public static string GetReasonPhrase(int statusCode) + { + return Phrases.TryGetValue(statusCode, out var phrase) ? phrase : string.Empty; } } diff --git a/src/Http/WebUtilities/src/StreamHelperExtensions.cs b/src/Http/WebUtilities/src/StreamHelperExtensions.cs index 8eb8c44766..9d7679e1aa 100644 --- a/src/Http/WebUtilities/src/StreamHelperExtensions.cs +++ b/src/Http/WebUtilities/src/StreamHelperExtensions.cs @@ -7,79 +7,78 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +/// +/// HTTP extension methods for . +/// +public static class StreamHelperExtensions { + private const int _maxReadBufferSize = 1024 * 4; + /// - /// HTTP extension methods for . + /// Reads the specified to the end. + /// + /// This API is effective when used in conjunction with buffering. It allows + /// a buffered request stream to be synchronously read after it has been completely drained. + /// /// - public static class StreamHelperExtensions + /// The to completely read. + /// The token to monitor for cancellation requests. + public static Task DrainAsync(this Stream stream, CancellationToken cancellationToken) { - private const int _maxReadBufferSize = 1024 * 4; - - /// - /// Reads the specified to the end. - /// - /// This API is effective when used in conjunction with buffering. It allows - /// a buffered request stream to be synchronously read after it has been completely drained. - /// - /// - /// The to completely read. - /// The token to monitor for cancellation requests. - public static Task DrainAsync(this Stream stream, CancellationToken cancellationToken) - { - return stream.DrainAsync(ArrayPool.Shared, null, cancellationToken); - } + return stream.DrainAsync(ArrayPool.Shared, null, cancellationToken); + } - /// - /// Reads the specified to the end. - /// - /// This API is effective when used in conjunction with buffering. It allows - /// a buffered request stream to be synchronously read after it has been completely drained. - /// - /// - /// The to completely read. - /// The maximum number of bytes to read. Throws if the is larger than this limit. - /// The token to monitor for cancellation requests. - public static Task DrainAsync(this Stream stream, long? limit, CancellationToken cancellationToken) - { - return stream.DrainAsync(ArrayPool.Shared, limit, cancellationToken); - } + /// + /// Reads the specified to the end. + /// + /// This API is effective when used in conjunction with buffering. It allows + /// a buffered request stream to be synchronously read after it has been completely drained. + /// + /// + /// The to completely read. + /// The maximum number of bytes to read. Throws if the is larger than this limit. + /// The token to monitor for cancellation requests. + public static Task DrainAsync(this Stream stream, long? limit, CancellationToken cancellationToken) + { + return stream.DrainAsync(ArrayPool.Shared, limit, cancellationToken); + } - /// - /// Reads the specified to the end. - /// - /// This API is effective when used in conjunction with buffering. It allows - /// a buffered request stream to be synchronously read after it has been completely drained. - /// - /// - /// The to completely read. - /// The byte array pool to use. - /// The maximum number of bytes to read. Throws if the is larger than this limit. - /// The token to monitor for cancellation requests. - public static async Task DrainAsync(this Stream stream, ArrayPool bytePool, long? limit, CancellationToken cancellationToken) + /// + /// Reads the specified to the end. + /// + /// This API is effective when used in conjunction with buffering. It allows + /// a buffered request stream to be synchronously read after it has been completely drained. + /// + /// + /// The to completely read. + /// The byte array pool to use. + /// The maximum number of bytes to read. Throws if the is larger than this limit. + /// The token to monitor for cancellation requests. + public static async Task DrainAsync(this Stream stream, ArrayPool bytePool, long? limit, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var buffer = bytePool.Rent(_maxReadBufferSize); + long total = 0; + try { - cancellationToken.ThrowIfCancellationRequested(); - var buffer = bytePool.Rent(_maxReadBufferSize); - long total = 0; - try + var read = await stream.ReadAsync(buffer.AsMemory(), cancellationToken); + while (read > 0) { - var read = await stream.ReadAsync(buffer.AsMemory(), cancellationToken); - while (read > 0) + // Not all streams support cancellation directly. + cancellationToken.ThrowIfCancellationRequested(); + if (limit.HasValue && limit.GetValueOrDefault() - total < read) { - // Not all streams support cancellation directly. - cancellationToken.ThrowIfCancellationRequested(); - if (limit.HasValue && limit.GetValueOrDefault() - total < read) - { - throw new InvalidDataException($"The stream exceeded the data limit {limit.GetValueOrDefault()}."); - } - total += read; - read = await stream.ReadAsync(buffer.AsMemory(), cancellationToken); + throw new InvalidDataException($"The stream exceeded the data limit {limit.GetValueOrDefault()}."); } + total += read; + read = await stream.ReadAsync(buffer.AsMemory(), cancellationToken); } - finally - { - bytePool.Return(buffer); - } + } + finally + { + bytePool.Return(buffer); } } } diff --git a/src/Http/WebUtilities/test/AspNetCoreTempDirectoryTests.cs b/src/Http/WebUtilities/test/AspNetCoreTempDirectoryTests.cs index f4ec699568..9cdff4273e 100644 --- a/src/Http/WebUtilities/test/AspNetCoreTempDirectoryTests.cs +++ b/src/Http/WebUtilities/test/AspNetCoreTempDirectoryTests.cs @@ -4,16 +4,15 @@ using System.IO; using Xunit; -namespace Microsoft.AspNetCore.Internal +namespace Microsoft.AspNetCore.Internal; + +public class AspNetCoreTempDirectoryTests { - public class AspNetCoreTempDirectoryTests + [Fact] + public void GetTempDirectory_Returns_Valid_Location() { - [Fact] - public void GetTempDirectory_Returns_Valid_Location() - { - var tempDirectory = AspNetCoreTempDirectory.TempDirectory; - Assert.NotNull(tempDirectory); - Assert.True(Directory.Exists(tempDirectory)); - } + var tempDirectory = AspNetCoreTempDirectory.TempDirectory; + Assert.NotNull(tempDirectory); + Assert.True(Directory.Exists(tempDirectory)); } } diff --git a/src/Http/WebUtilities/test/FileBufferingReadStreamTests.cs b/src/Http/WebUtilities/test/FileBufferingReadStreamTests.cs index 7d6c422292..c79bc50585 100644 --- a/src/Http/WebUtilities/test/FileBufferingReadStreamTests.cs +++ b/src/Http/WebUtilities/test/FileBufferingReadStreamTests.cs @@ -9,508 +9,507 @@ using System.Threading.Tasks; using Moq; using Xunit; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +public class FileBufferingReadStreamTests { - public class FileBufferingReadStreamTests + private Stream MakeStream(int size) { - private Stream MakeStream(int size) - { - // TODO: Fill with random data? Make readonly? - return new MemoryStream(new byte[size]); - } + // TODO: Fill with random data? Make readonly? + return new MemoryStream(new byte[size]); + } - [Fact] - public void FileBufferingReadStream_Properties_ExpectedValues() + [Fact] + public void FileBufferingReadStream_Properties_ExpectedValues() + { + var inner = MakeStream(1024 * 2); + using (var stream = new FileBufferingReadStream(inner, 1024, null, Directory.GetCurrentDirectory())) { - var inner = MakeStream(1024 * 2); - using (var stream = new FileBufferingReadStream(inner, 1024, null, Directory.GetCurrentDirectory())) - { - Assert.True(stream.CanRead); - Assert.True(stream.CanSeek); - Assert.False(stream.CanWrite); - Assert.Equal(0, stream.Length); // Nothing buffered yet - Assert.Equal(0, stream.Position); - Assert.True(stream.InMemory); - Assert.Null(stream.TempFileName); - } + Assert.True(stream.CanRead); + Assert.True(stream.CanSeek); + Assert.False(stream.CanWrite); + Assert.Equal(0, stream.Length); // Nothing buffered yet + Assert.Equal(0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); } + } - [Fact] - public void FileBufferingReadStream_SyncReadUnderThreshold_DoesntCreateFile() + [Fact] + public void FileBufferingReadStream_SyncReadUnderThreshold_DoesntCreateFile() + { + var inner = MakeStream(1024 * 2); + using (var stream = new FileBufferingReadStream(inner, 1024 * 3, null, Directory.GetCurrentDirectory())) { - var inner = MakeStream(1024 * 2); - using (var stream = new FileBufferingReadStream(inner, 1024 * 3, null, Directory.GetCurrentDirectory())) - { - var bytes = new byte[1000]; - var read0 = stream.Read(bytes, 0, bytes.Length); - Assert.Equal(bytes.Length, read0); - Assert.Equal(read0, stream.Length); - Assert.Equal(read0, stream.Position); - Assert.True(stream.InMemory); - Assert.Null(stream.TempFileName); - - var read1 = stream.Read(bytes, 0, bytes.Length); - Assert.Equal(bytes.Length, read1); - Assert.Equal(read0 + read1, stream.Length); - Assert.Equal(read0 + read1, stream.Position); - Assert.True(stream.InMemory); - Assert.Null(stream.TempFileName); - - var read2 = stream.Read(bytes, 0, bytes.Length); - Assert.Equal(inner.Length - read0 - read1, read2); - Assert.Equal(read0 + read1 + read2, stream.Length); - Assert.Equal(read0 + read1 + read2, stream.Position); - Assert.True(stream.InMemory); - Assert.Null(stream.TempFileName); - - var read3 = stream.Read(bytes, 0, bytes.Length); - Assert.Equal(0, read3); - } + var bytes = new byte[1000]; + var read0 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read2 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(inner.Length - read0 - read1, read2); + Assert.Equal(read0 + read1 + read2, stream.Length); + Assert.Equal(read0 + read1 + read2, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read3 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(0, read3); } + } - [Fact] - public void FileBufferingReadStream_SyncReadOverThreshold_CreatesFile() + [Fact] + public void FileBufferingReadStream_SyncReadOverThreshold_CreatesFile() + { + var inner = MakeStream(1024 * 2); + string tempFileName; + using (var stream = new FileBufferingReadStream(inner, 1024, null, GetCurrentDirectory())) { - var inner = MakeStream(1024 * 2); - string tempFileName; - using (var stream = new FileBufferingReadStream(inner, 1024, null, GetCurrentDirectory())) - { - var bytes = new byte[1000]; - var read0 = stream.Read(bytes, 0, bytes.Length); - Assert.Equal(bytes.Length, read0); - Assert.Equal(read0, stream.Length); - Assert.Equal(read0, stream.Position); - Assert.True(stream.InMemory); - Assert.Null(stream.TempFileName); - - var read1 = stream.Read(bytes, 0, bytes.Length); - Assert.Equal(bytes.Length, read1); - Assert.Equal(read0 + read1, stream.Length); - Assert.Equal(read0 + read1, stream.Position); - Assert.False(stream.InMemory); - Assert.NotNull(stream.TempFileName); - tempFileName = stream.TempFileName!; - Assert.True(File.Exists(tempFileName)); - - var read2 = stream.Read(bytes, 0, bytes.Length); - Assert.Equal(inner.Length - read0 - read1, read2); - Assert.Equal(read0 + read1 + read2, stream.Length); - Assert.Equal(read0 + read1 + read2, stream.Position); - Assert.False(stream.InMemory); - Assert.NotNull(stream.TempFileName); - Assert.True(File.Exists(tempFileName)); - - var read3 = stream.Read(bytes, 0, bytes.Length); - Assert.Equal(0, read3); - } - - Assert.False(File.Exists(tempFileName)); + var bytes = new byte[1000]; + var read0 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + tempFileName = stream.TempFileName!; + Assert.True(File.Exists(tempFileName)); + + var read2 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(inner.Length - read0 - read1, read2); + Assert.Equal(read0 + read1 + read2, stream.Length); + Assert.Equal(read0 + read1 + read2, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + Assert.True(File.Exists(tempFileName)); + + var read3 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(0, read3); } - [Fact] - public void FileBufferingReadStream_SyncReadWithInMemoryLimit_EnforcesLimit() + Assert.False(File.Exists(tempFileName)); + } + + [Fact] + public void FileBufferingReadStream_SyncReadWithInMemoryLimit_EnforcesLimit() + { + var inner = MakeStream(1024 * 2); + using (var stream = new FileBufferingReadStream(inner, 1024, 900, Directory.GetCurrentDirectory())) { - var inner = MakeStream(1024 * 2); - using (var stream = new FileBufferingReadStream(inner, 1024, 900, Directory.GetCurrentDirectory())) - { - var bytes = new byte[500]; - var read0 = stream.Read(bytes, 0, bytes.Length); - Assert.Equal(bytes.Length, read0); - Assert.Equal(read0, stream.Length); - Assert.Equal(read0, stream.Position); - Assert.True(stream.InMemory); - Assert.Null(stream.TempFileName); - - var exception = Assert.Throws(() => stream.Read(bytes, 0, bytes.Length)); - Assert.Equal("Buffer limit exceeded.", exception.Message); - Assert.True(stream.InMemory); - Assert.Null(stream.TempFileName); - Assert.False(File.Exists(stream.TempFileName)); - } + var bytes = new byte[500]; + var read0 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var exception = Assert.Throws(() => stream.Read(bytes, 0, bytes.Length)); + Assert.Equal("Buffer limit exceeded.", exception.Message); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + Assert.False(File.Exists(stream.TempFileName)); } + } - [Fact] - public void FileBufferingReadStream_SyncReadWithOnDiskLimit_EnforcesLimit() + [Fact] + public void FileBufferingReadStream_SyncReadWithOnDiskLimit_EnforcesLimit() + { + var inner = MakeStream(1024 * 2); + string tempFileName; + using (var stream = new FileBufferingReadStream(inner, 512, 1024, GetCurrentDirectory())) { - var inner = MakeStream(1024 * 2); - string tempFileName; - using (var stream = new FileBufferingReadStream(inner, 512, 1024, GetCurrentDirectory())) - { - var bytes = new byte[500]; - var read0 = stream.Read(bytes, 0, bytes.Length); - Assert.Equal(bytes.Length, read0); - Assert.Equal(read0, stream.Length); - Assert.Equal(read0, stream.Position); - Assert.True(stream.InMemory); - Assert.Null(stream.TempFileName); - - var read1 = stream.Read(bytes, 0, bytes.Length); - Assert.Equal(bytes.Length, read1); - Assert.Equal(read0 + read1, stream.Length); - Assert.Equal(read0 + read1, stream.Position); - Assert.False(stream.InMemory); - Assert.NotNull(stream.TempFileName); - tempFileName = stream.TempFileName!; - Assert.True(File.Exists(tempFileName)); - - var exception = Assert.Throws(() => stream.Read(bytes, 0, bytes.Length)); - Assert.Equal("Buffer limit exceeded.", exception.Message); - Assert.False(stream.InMemory); - Assert.NotNull(stream.TempFileName); - } - - Assert.False(File.Exists(tempFileName)); + var bytes = new byte[500]; + var read0 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = stream.Read(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + tempFileName = stream.TempFileName!; + Assert.True(File.Exists(tempFileName)); + + var exception = Assert.Throws(() => stream.Read(bytes, 0, bytes.Length)); + Assert.Equal("Buffer limit exceeded.", exception.Message); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); } - /////////////////// + Assert.False(File.Exists(tempFileName)); + } + + /////////////////// - [Fact] - public async Task FileBufferingReadStream_AsyncReadUnderThreshold_DoesntCreateFile() + [Fact] + public async Task FileBufferingReadStream_AsyncReadUnderThreshold_DoesntCreateFile() + { + var inner = MakeStream(1024 * 2); + using (var stream = new FileBufferingReadStream(inner, 1024 * 3, null, Directory.GetCurrentDirectory())) { - var inner = MakeStream(1024 * 2); - using (var stream = new FileBufferingReadStream(inner, 1024 * 3, null, Directory.GetCurrentDirectory())) - { - var bytes = new byte[1000]; - var read0 = await stream.ReadAsync(bytes, 0, bytes.Length); - Assert.Equal(bytes.Length, read0); - Assert.Equal(read0, stream.Length); - Assert.Equal(read0, stream.Position); - Assert.True(stream.InMemory); - Assert.Null(stream.TempFileName); - - var read1 = await stream.ReadAsync(bytes, 0, bytes.Length); - Assert.Equal(bytes.Length, read1); - Assert.Equal(read0 + read1, stream.Length); - Assert.Equal(read0 + read1, stream.Position); - Assert.True(stream.InMemory); - Assert.Null(stream.TempFileName); - - var read2 = await stream.ReadAsync(bytes, 0, bytes.Length); - Assert.Equal(inner.Length - read0 - read1, read2); - Assert.Equal(read0 + read1 + read2, stream.Length); - Assert.Equal(read0 + read1 + read2, stream.Position); - Assert.True(stream.InMemory); - Assert.Null(stream.TempFileName); - - var read3 = await stream.ReadAsync(bytes, 0, bytes.Length); - Assert.Equal(0, read3); - } + var bytes = new byte[1000]; + var read0 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read2 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(inner.Length - read0 - read1, read2); + Assert.Equal(read0 + read1 + read2, stream.Length); + Assert.Equal(read0 + read1 + read2, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read3 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(0, read3); } + } - [Fact] - public async Task FileBufferingReadStream_AsyncReadOverThreshold_CreatesFile() + [Fact] + public async Task FileBufferingReadStream_AsyncReadOverThreshold_CreatesFile() + { + var inner = MakeStream(1024 * 2); + string tempFileName; + using (var stream = new FileBufferingReadStream(inner, 1024, null, GetCurrentDirectory())) { - var inner = MakeStream(1024 * 2); - string tempFileName; - using (var stream = new FileBufferingReadStream(inner, 1024, null, GetCurrentDirectory())) - { - var bytes = new byte[1000]; - var read0 = await stream.ReadAsync(bytes, 0, bytes.Length); - Assert.Equal(bytes.Length, read0); - Assert.Equal(read0, stream.Length); - Assert.Equal(read0, stream.Position); - Assert.True(stream.InMemory); - Assert.Null(stream.TempFileName); - - var read1 = await stream.ReadAsync(bytes, 0, bytes.Length); - Assert.Equal(bytes.Length, read1); - Assert.Equal(read0 + read1, stream.Length); - Assert.Equal(read0 + read1, stream.Position); - Assert.False(stream.InMemory); - Assert.NotNull(stream.TempFileName); - tempFileName = stream.TempFileName!; - Assert.True(File.Exists(tempFileName)); - - var read2 = await stream.ReadAsync(bytes, 0, bytes.Length); - Assert.Equal(inner.Length - read0 - read1, read2); - Assert.Equal(read0 + read1 + read2, stream.Length); - Assert.Equal(read0 + read1 + read2, stream.Position); - Assert.False(stream.InMemory); - Assert.NotNull(stream.TempFileName); - Assert.True(File.Exists(tempFileName)); - - var read3 = await stream.ReadAsync(bytes, 0, bytes.Length); - Assert.Equal(0, read3); - } - - Assert.False(File.Exists(tempFileName)); + var bytes = new byte[1000]; + var read0 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + tempFileName = stream.TempFileName!; + Assert.True(File.Exists(tempFileName)); + + var read2 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(inner.Length - read0 - read1, read2); + Assert.Equal(read0 + read1 + read2, stream.Length); + Assert.Equal(read0 + read1 + read2, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + Assert.True(File.Exists(tempFileName)); + + var read3 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(0, read3); } - [Fact] - public async Task FileBufferingReadStream_AsyncReadWithInMemoryLimit_EnforcesLimit() + Assert.False(File.Exists(tempFileName)); + } + + [Fact] + public async Task FileBufferingReadStream_AsyncReadWithInMemoryLimit_EnforcesLimit() + { + var inner = MakeStream(1024 * 2); + using (var stream = new FileBufferingReadStream(inner, 1024, 900, Directory.GetCurrentDirectory())) { - var inner = MakeStream(1024 * 2); - using (var stream = new FileBufferingReadStream(inner, 1024, 900, Directory.GetCurrentDirectory())) - { - var bytes = new byte[500]; - var read0 = await stream.ReadAsync(bytes, 0, bytes.Length); - Assert.Equal(bytes.Length, read0); - Assert.Equal(read0, stream.Length); - Assert.Equal(read0, stream.Position); - Assert.True(stream.InMemory); - Assert.Null(stream.TempFileName); - - var exception = await Assert.ThrowsAsync(() => stream.ReadAsync(bytes, 0, bytes.Length)); - Assert.Equal("Buffer limit exceeded.", exception.Message); - Assert.True(stream.InMemory); - Assert.Null(stream.TempFileName); - Assert.False(File.Exists(stream.TempFileName)); - } + var bytes = new byte[500]; + var read0 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var exception = await Assert.ThrowsAsync(() => stream.ReadAsync(bytes, 0, bytes.Length)); + Assert.Equal("Buffer limit exceeded.", exception.Message); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + Assert.False(File.Exists(stream.TempFileName)); } + } - [Fact] - public async Task FileBufferingReadStream_AsyncReadWithOnDiskLimit_EnforcesLimit() + [Fact] + public async Task FileBufferingReadStream_AsyncReadWithOnDiskLimit_EnforcesLimit() + { + var inner = MakeStream(1024 * 2); + string tempFileName; + using (var stream = new FileBufferingReadStream(inner, 512, 1024, GetCurrentDirectory())) { - var inner = MakeStream(1024 * 2); - string tempFileName; - using (var stream = new FileBufferingReadStream(inner, 512, 1024, GetCurrentDirectory())) - { - var bytes = new byte[500]; - var read0 = await stream.ReadAsync(bytes, 0, bytes.Length); - Assert.Equal(bytes.Length, read0); - Assert.Equal(read0, stream.Length); - Assert.Equal(read0, stream.Position); - Assert.True(stream.InMemory); - Assert.Null(stream.TempFileName); - - var read1 = await stream.ReadAsync(bytes, 0, bytes.Length); - Assert.Equal(bytes.Length, read1); - Assert.Equal(read0 + read1, stream.Length); - Assert.Equal(read0 + read1, stream.Position); - Assert.False(stream.InMemory); - Assert.NotNull(stream.TempFileName); - tempFileName = stream.TempFileName!; - Assert.True(File.Exists(tempFileName)); - - var exception = await Assert.ThrowsAsync(() => stream.ReadAsync(bytes, 0, bytes.Length)); - Assert.Equal("Buffer limit exceeded.", exception.Message); - Assert.False(stream.InMemory); - Assert.NotNull(stream.TempFileName); - } - - Assert.False(File.Exists(tempFileName)); + var bytes = new byte[500]; + var read0 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read0); + Assert.Equal(read0, stream.Length); + Assert.Equal(read0, stream.Position); + Assert.True(stream.InMemory); + Assert.Null(stream.TempFileName); + + var read1 = await stream.ReadAsync(bytes, 0, bytes.Length); + Assert.Equal(bytes.Length, read1); + Assert.Equal(read0 + read1, stream.Length); + Assert.Equal(read0 + read1, stream.Position); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); + tempFileName = stream.TempFileName!; + Assert.True(File.Exists(tempFileName)); + + var exception = await Assert.ThrowsAsync(() => stream.ReadAsync(bytes, 0, bytes.Length)); + Assert.Equal("Buffer limit exceeded.", exception.Message); + Assert.False(stream.InMemory); + Assert.NotNull(stream.TempFileName); } - [Fact] - public void FileBufferingReadStream_UsingMemoryStream_RentsAndReturnsRentedBuffer_WhenCopyingFromMemoryStreamDuringRead() + Assert.False(File.Exists(tempFileName)); + } + + [Fact] + public void FileBufferingReadStream_UsingMemoryStream_RentsAndReturnsRentedBuffer_WhenCopyingFromMemoryStreamDuringRead() + { + var inner = MakeStream(1024 * 1024 + 25); + string tempFileName; + var arrayPool = new Mock>(); + arrayPool.Setup(p => p.Rent(It.IsAny())) + .Returns((int m) => ArrayPool.Shared.Rent(m)); + arrayPool.Setup(p => p.Return(It.IsAny(), It.IsAny())) + .Callback((byte[] bytes, bool clear) => ArrayPool.Shared.Return(bytes, clear)); + + using (var stream = new FileBufferingReadStream(inner, 1024 * 1024 + 1, 2 * 1024 * 1024, GetCurrentDirectory(), arrayPool.Object)) { - var inner = MakeStream(1024 * 1024 + 25); - string tempFileName; - var arrayPool = new Mock>(); - arrayPool.Setup(p => p.Rent(It.IsAny())) - .Returns((int m) => ArrayPool.Shared.Rent(m)); - arrayPool.Setup(p => p.Return(It.IsAny(), It.IsAny())) - .Callback((byte[] bytes, bool clear) => ArrayPool.Shared.Return(bytes, clear)); - - using (var stream = new FileBufferingReadStream(inner, 1024 * 1024 + 1, 2 * 1024 * 1024, GetCurrentDirectory(), arrayPool.Object)) - { - arrayPool.Verify(v => v.Rent(It.IsAny()), Times.Never()); - - stream.Read(new byte[1024 * 1024]); - Assert.False(File.Exists(stream.TempFileName), "tempFile should not be created as yet"); - - stream.Read(new byte[4]); - Assert.True(File.Exists(stream.TempFileName), "tempFile should be created"); - tempFileName = stream.TempFileName!; - - arrayPool.Verify(v => v.Rent(It.IsAny()), Times.Once()); - arrayPool.Verify(v => v.Return(It.IsAny(), It.IsAny()), Times.Once()); - } - - Assert.False(File.Exists(tempFileName)); + arrayPool.Verify(v => v.Rent(It.IsAny()), Times.Never()); + + stream.Read(new byte[1024 * 1024]); + Assert.False(File.Exists(stream.TempFileName), "tempFile should not be created as yet"); + + stream.Read(new byte[4]); + Assert.True(File.Exists(stream.TempFileName), "tempFile should be created"); + tempFileName = stream.TempFileName!; + + arrayPool.Verify(v => v.Rent(It.IsAny()), Times.Once()); + arrayPool.Verify(v => v.Return(It.IsAny(), It.IsAny()), Times.Once()); } - [Fact] - public async Task FileBufferingReadStream_UsingMemoryStream_RentsAndReturnsRentedBuffer_WhenCopyingFromMemoryStreamDuringReadAsync() + Assert.False(File.Exists(tempFileName)); + } + + [Fact] + public async Task FileBufferingReadStream_UsingMemoryStream_RentsAndReturnsRentedBuffer_WhenCopyingFromMemoryStreamDuringReadAsync() + { + var inner = MakeStream(1024 * 1024 + 25); + string tempFileName; + var arrayPool = new Mock>(); + arrayPool.Setup(p => p.Rent(It.IsAny())) + .Returns((int m) => ArrayPool.Shared.Rent(m)); + arrayPool.Setup(p => p.Return(It.IsAny(), It.IsAny())) + .Callback((byte[] bytes, bool clear) => ArrayPool.Shared.Return(bytes, clear)); + + using (var stream = new FileBufferingReadStream(inner, 1024 * 1024 + 1, 2 * 1024 * 1024, GetCurrentDirectory(), arrayPool.Object)) { - var inner = MakeStream(1024 * 1024 + 25); - string tempFileName; - var arrayPool = new Mock>(); - arrayPool.Setup(p => p.Rent(It.IsAny())) - .Returns((int m) => ArrayPool.Shared.Rent(m)); - arrayPool.Setup(p => p.Return(It.IsAny(), It.IsAny())) - .Callback((byte[] bytes, bool clear) => ArrayPool.Shared.Return(bytes, clear)); - - using (var stream = new FileBufferingReadStream(inner, 1024 * 1024 + 1, 2 * 1024 * 1024, GetCurrentDirectory(), arrayPool.Object)) - { - arrayPool.Verify(v => v.Rent(It.IsAny()), Times.Never()); - - await stream.ReadAsync(new byte[1024 * 1024]); - Assert.False(File.Exists(stream.TempFileName), "tempFile should not be created as yet"); - - await stream.ReadAsync(new byte[4]); - Assert.True(File.Exists(stream.TempFileName), "tempFile should be created"); - tempFileName = stream.TempFileName!; - - arrayPool.Verify(v => v.Rent(It.IsAny()), Times.Once()); - arrayPool.Verify(v => v.Return(It.IsAny(), It.IsAny()), Times.Once()); - } - - Assert.False(File.Exists(tempFileName)); + arrayPool.Verify(v => v.Rent(It.IsAny()), Times.Never()); + + await stream.ReadAsync(new byte[1024 * 1024]); + Assert.False(File.Exists(stream.TempFileName), "tempFile should not be created as yet"); + + await stream.ReadAsync(new byte[4]); + Assert.True(File.Exists(stream.TempFileName), "tempFile should be created"); + tempFileName = stream.TempFileName!; + + arrayPool.Verify(v => v.Rent(It.IsAny()), Times.Once()); + arrayPool.Verify(v => v.Return(It.IsAny(), It.IsAny()), Times.Once()); } - [Fact] - public async Task CopyToAsyncWorks() - { - // 4K is the lower bound on buffer sizes - var bufferSize = 4096; - var mostExpectedWrites = 8; - var data = Enumerable.Range(0, bufferSize * mostExpectedWrites).Select(b => (byte)b).ToArray(); - var inner = new MemoryStream(data); + Assert.False(File.Exists(tempFileName)); + } - using var stream = new FileBufferingReadStream(inner, 1024 * 1024, bufferLimit: null, GetCurrentDirectory()); + [Fact] + public async Task CopyToAsyncWorks() + { + // 4K is the lower bound on buffer sizes + var bufferSize = 4096; + var mostExpectedWrites = 8; + var data = Enumerable.Range(0, bufferSize * mostExpectedWrites).Select(b => (byte)b).ToArray(); + var inner = new MemoryStream(data); - var withoutBufferMs = new NumberOfWritesMemoryStream(); - await stream.CopyToAsync(withoutBufferMs); + using var stream = new FileBufferingReadStream(inner, 1024 * 1024, bufferLimit: null, GetCurrentDirectory()); - var withBufferMs = new NumberOfWritesMemoryStream(); - stream.Position = 0; - await stream.CopyToAsync(withBufferMs); + var withoutBufferMs = new NumberOfWritesMemoryStream(); + await stream.CopyToAsync(withoutBufferMs); - Assert.Equal(data, withoutBufferMs.ToArray()); - Assert.Equal(mostExpectedWrites, withoutBufferMs.NumberOfWrites); - Assert.Equal(data, withBufferMs.ToArray()); - Assert.InRange(withBufferMs.NumberOfWrites, 1, mostExpectedWrites); - } + var withBufferMs = new NumberOfWritesMemoryStream(); + stream.Position = 0; + await stream.CopyToAsync(withBufferMs); - [Fact] - public async Task CopyToAsyncWorksWithFileThreshold() - { - // 4K is the lower bound on buffer sizes - var bufferSize = 4096; - var mostExpectedWrites = 8; - var data = Enumerable.Range(0, bufferSize * mostExpectedWrites).Select(b => (byte)b).Reverse().ToArray(); - var inner = new MemoryStream(data); + Assert.Equal(data, withoutBufferMs.ToArray()); + Assert.Equal(mostExpectedWrites, withoutBufferMs.NumberOfWrites); + Assert.Equal(data, withBufferMs.ToArray()); + Assert.InRange(withBufferMs.NumberOfWrites, 1, mostExpectedWrites); + } - using var stream = new FileBufferingReadStream(inner, 100, bufferLimit: null, GetCurrentDirectory()); + [Fact] + public async Task CopyToAsyncWorksWithFileThreshold() + { + // 4K is the lower bound on buffer sizes + var bufferSize = 4096; + var mostExpectedWrites = 8; + var data = Enumerable.Range(0, bufferSize * mostExpectedWrites).Select(b => (byte)b).Reverse().ToArray(); + var inner = new MemoryStream(data); - var withoutBufferMs = new NumberOfWritesMemoryStream(); - await stream.CopyToAsync(withoutBufferMs); + using var stream = new FileBufferingReadStream(inner, 100, bufferLimit: null, GetCurrentDirectory()); - var withBufferMs = new NumberOfWritesMemoryStream(); - stream.Position = 0; - await stream.CopyToAsync(withBufferMs); + var withoutBufferMs = new NumberOfWritesMemoryStream(); + await stream.CopyToAsync(withoutBufferMs); - Assert.Equal(data, withoutBufferMs.ToArray()); - Assert.Equal(mostExpectedWrites, withoutBufferMs.NumberOfWrites); - Assert.Equal(data, withBufferMs.ToArray()); - Assert.InRange(withBufferMs.NumberOfWrites, 1, mostExpectedWrites); - } + var withBufferMs = new NumberOfWritesMemoryStream(); + stream.Position = 0; + await stream.CopyToAsync(withBufferMs); - [Fact] - public async Task ReadAsyncThenCopyToAsyncWorks() - { - var data = Enumerable.Range(0, 1024).Select(b => (byte)b).ToArray(); - var inner = new MemoryStream(data); + Assert.Equal(data, withoutBufferMs.ToArray()); + Assert.Equal(mostExpectedWrites, withoutBufferMs.NumberOfWrites); + Assert.Equal(data, withBufferMs.ToArray()); + Assert.InRange(withBufferMs.NumberOfWrites, 1, mostExpectedWrites); + } - using var stream = new FileBufferingReadStream(inner, 1024 * 1024, bufferLimit: null, GetCurrentDirectory()); + [Fact] + public async Task ReadAsyncThenCopyToAsyncWorks() + { + var data = Enumerable.Range(0, 1024).Select(b => (byte)b).ToArray(); + var inner = new MemoryStream(data); - var withoutBufferMs = new MemoryStream(); - var buffer = new byte[100]; - await stream.ReadAsync(buffer); - await stream.CopyToAsync(withoutBufferMs); + using var stream = new FileBufferingReadStream(inner, 1024 * 1024, bufferLimit: null, GetCurrentDirectory()); - Assert.Equal(data.AsMemory(0, 100).ToArray(), buffer); - Assert.Equal(data.AsMemory(100).ToArray(), withoutBufferMs.ToArray()); - } + var withoutBufferMs = new MemoryStream(); + var buffer = new byte[100]; + await stream.ReadAsync(buffer); + await stream.CopyToAsync(withoutBufferMs); - [Fact] - public async Task ReadThenCopyToAsyncWorks() - { - var data = Enumerable.Range(0, 1024).Select(b => (byte)b).ToArray(); - var inner = new MemoryStream(data); + Assert.Equal(data.AsMemory(0, 100).ToArray(), buffer); + Assert.Equal(data.AsMemory(100).ToArray(), withoutBufferMs.ToArray()); + } - using var stream = new FileBufferingReadStream(inner, 1024 * 1024, bufferLimit: null, GetCurrentDirectory()); + [Fact] + public async Task ReadThenCopyToAsyncWorks() + { + var data = Enumerable.Range(0, 1024).Select(b => (byte)b).ToArray(); + var inner = new MemoryStream(data); - var withoutBufferMs = new MemoryStream(); - var buffer = new byte[100]; - var read = stream.Read(buffer); - await stream.CopyToAsync(withoutBufferMs); + using var stream = new FileBufferingReadStream(inner, 1024 * 1024, bufferLimit: null, GetCurrentDirectory()); - Assert.Equal(100, read); - Assert.Equal(data.AsMemory(0, read).ToArray(), buffer); - Assert.Equal(data.AsMemory(read).ToArray(), withoutBufferMs.ToArray()); - } + var withoutBufferMs = new MemoryStream(); + var buffer = new byte[100]; + var read = stream.Read(buffer); + await stream.CopyToAsync(withoutBufferMs); - [Fact] - public async Task ReadThenSeekThenCopyToAsyncWorks() - { - var data = Enumerable.Range(0, 1024).Select(b => (byte)b).ToArray(); - var inner = new MemoryStream(data); + Assert.Equal(100, read); + Assert.Equal(data.AsMemory(0, read).ToArray(), buffer); + Assert.Equal(data.AsMemory(read).ToArray(), withoutBufferMs.ToArray()); + } - using var stream = new FileBufferingReadStream(inner, 1024 * 1024, bufferLimit: null, GetCurrentDirectory()); + [Fact] + public async Task ReadThenSeekThenCopyToAsyncWorks() + { + var data = Enumerable.Range(0, 1024).Select(b => (byte)b).ToArray(); + var inner = new MemoryStream(data); - var withoutBufferMs = new MemoryStream(); - var buffer = new byte[100]; - var read = stream.Read(buffer); - stream.Position = 0; - await stream.CopyToAsync(withoutBufferMs); + using var stream = new FileBufferingReadStream(inner, 1024 * 1024, bufferLimit: null, GetCurrentDirectory()); - Assert.Equal(100, read); - Assert.Equal(data.AsMemory(0, read).ToArray(), buffer); - Assert.Equal(data.ToArray(), withoutBufferMs.ToArray()); - } + var withoutBufferMs = new MemoryStream(); + var buffer = new byte[100]; + var read = stream.Read(buffer); + stream.Position = 0; + await stream.CopyToAsync(withoutBufferMs); - [Fact] - public void PartialReadThenSeekReplaysBuffer() - { - var data = Enumerable.Range(0, 1024).Select(b => (byte)b).ToArray(); - var inner = new MemoryStream(data); - - using var stream = new FileBufferingReadStream(inner, 1024 * 1024, bufferLimit: null, GetCurrentDirectory()); - - var withoutBufferMs = new MemoryStream(); - var buffer = new byte[100]; - var read1 = stream.Read(buffer); - stream.Position = 0; - var buffer2 = new byte[200]; - var read2 = stream.Read(buffer2); - Assert.Equal(100, read1); - Assert.Equal(100, read2); - Assert.Equal(data.AsMemory(0, read1).ToArray(), buffer); - Assert.Equal(data.AsMemory(0, read2).ToArray(), buffer2.AsMemory(0, read2).ToArray()); - } + Assert.Equal(100, read); + Assert.Equal(data.AsMemory(0, read).ToArray(), buffer); + Assert.Equal(data.ToArray(), withoutBufferMs.ToArray()); + } - [Fact] - public async Task PartialReadAsyncThenSeekReplaysBuffer() - { - var data = Enumerable.Range(0, 1024).Select(b => (byte)b).ToArray(); - var inner = new MemoryStream(data); - - using var stream = new FileBufferingReadStream(inner, 1024 * 1024, bufferLimit: null, GetCurrentDirectory()); - - var withoutBufferMs = new MemoryStream(); - var buffer = new byte[100]; - var read1 = await stream.ReadAsync(buffer); - stream.Position = 0; - var buffer2 = new byte[200]; - var read2 = await stream.ReadAsync(buffer2); - Assert.Equal(100, read1); - Assert.Equal(100, read2); - Assert.Equal(data.AsMemory(0, read1).ToArray(), buffer); - Assert.Equal(data.AsMemory(0, read2).ToArray(), buffer2.AsMemory(0, read2).ToArray()); - } + [Fact] + public void PartialReadThenSeekReplaysBuffer() + { + var data = Enumerable.Range(0, 1024).Select(b => (byte)b).ToArray(); + var inner = new MemoryStream(data); + + using var stream = new FileBufferingReadStream(inner, 1024 * 1024, bufferLimit: null, GetCurrentDirectory()); + + var withoutBufferMs = new MemoryStream(); + var buffer = new byte[100]; + var read1 = stream.Read(buffer); + stream.Position = 0; + var buffer2 = new byte[200]; + var read2 = stream.Read(buffer2); + Assert.Equal(100, read1); + Assert.Equal(100, read2); + Assert.Equal(data.AsMemory(0, read1).ToArray(), buffer); + Assert.Equal(data.AsMemory(0, read2).ToArray(), buffer2.AsMemory(0, read2).ToArray()); + } + + [Fact] + public async Task PartialReadAsyncThenSeekReplaysBuffer() + { + var data = Enumerable.Range(0, 1024).Select(b => (byte)b).ToArray(); + var inner = new MemoryStream(data); + + using var stream = new FileBufferingReadStream(inner, 1024 * 1024, bufferLimit: null, GetCurrentDirectory()); + + var withoutBufferMs = new MemoryStream(); + var buffer = new byte[100]; + var read1 = await stream.ReadAsync(buffer); + stream.Position = 0; + var buffer2 = new byte[200]; + var read2 = await stream.ReadAsync(buffer2); + Assert.Equal(100, read1); + Assert.Equal(100, read2); + Assert.Equal(data.AsMemory(0, read1).ToArray(), buffer); + Assert.Equal(data.AsMemory(0, read2).ToArray(), buffer2.AsMemory(0, read2).ToArray()); + } + + private static string GetCurrentDirectory() + { + return AppContext.BaseDirectory; + } + + private class NumberOfWritesMemoryStream : MemoryStream + { + public int NumberOfWrites { get; set; } - private static string GetCurrentDirectory() + public override void Write(byte[] buffer, int offset, int count) { - return AppContext.BaseDirectory; + NumberOfWrites++; + base.Write(buffer, offset, count); } - private class NumberOfWritesMemoryStream : MemoryStream + public override void Write(ReadOnlySpan source) { - public int NumberOfWrites { get; set; } - - public override void Write(byte[] buffer, int offset, int count) - { - NumberOfWrites++; - base.Write(buffer, offset, count); - } - - public override void Write(ReadOnlySpan source) - { - NumberOfWrites++; - base.Write(source); - } + NumberOfWrites++; + base.Write(source); } } } diff --git a/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs b/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs index 9459a19295..d4d0043cde 100644 --- a/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs +++ b/src/Http/WebUtilities/test/FileBufferingWriteStreamTests.cs @@ -10,393 +10,392 @@ using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Xunit; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +public class FileBufferingWriteStreamTests : IDisposable { - public class FileBufferingWriteStreamTests : IDisposable + private readonly string TempDirectory = Path.Combine(Path.GetTempPath(), "FileBufferingWriteTests", Path.GetRandomFileName()); + + public FileBufferingWriteStreamTests() { - private readonly string TempDirectory = Path.Combine(Path.GetTempPath(), "FileBufferingWriteTests", Path.GetRandomFileName()); + Directory.CreateDirectory(TempDirectory); + } - public FileBufferingWriteStreamTests() - { - Directory.CreateDirectory(TempDirectory); - } + [Fact] + public void Write_BuffersContentToMemory() + { + // Arrange + using var bufferingStream = new FileBufferingWriteStream(tempFileDirectoryAccessor: () => TempDirectory); + var input = Encoding.UTF8.GetBytes("Hello world"); - [Fact] - public void Write_BuffersContentToMemory() - { - // Arrange - using var bufferingStream = new FileBufferingWriteStream(tempFileDirectoryAccessor: () => TempDirectory); - var input = Encoding.UTF8.GetBytes("Hello world"); + // Act + bufferingStream.Write(input, 0, input.Length); - // Act - bufferingStream.Write(input, 0, input.Length); + // Assert + Assert.Equal(input.Length, bufferingStream.Length); - // Assert - Assert.Equal(input.Length, bufferingStream.Length); + // We should have written content to memory + var pagedByteBuffer = bufferingStream.PagedByteBuffer; + Assert.Equal(input, ReadBufferedContent(pagedByteBuffer)); - // We should have written content to memory - var pagedByteBuffer = bufferingStream.PagedByteBuffer; - Assert.Equal(input, ReadBufferedContent(pagedByteBuffer)); + // No files should not have been created. + Assert.Null(bufferingStream.FileStream); + } - // No files should not have been created. - Assert.Null(bufferingStream.FileStream); - } + [Fact] + public void Write_BeforeMemoryThresholdIsReached_WritesToMemory() + { + // Arrange + var input = new byte[] { 1, 2, }; + using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory); - [Fact] - public void Write_BeforeMemoryThresholdIsReached_WritesToMemory() - { - // Arrange - var input = new byte[] { 1, 2, }; - using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory); + // Act + bufferingStream.Write(input, 0, 2); - // Act - bufferingStream.Write(input, 0, 2); + // Assert + var pageBuffer = bufferingStream.PagedByteBuffer; + var fileStream = bufferingStream.FileStream; - // Assert - var pageBuffer = bufferingStream.PagedByteBuffer; - var fileStream = bufferingStream.FileStream; + Assert.Equal(input.Length, bufferingStream.Length); - Assert.Equal(input.Length, bufferingStream.Length); + // File should have been created. + Assert.Null(fileStream); - // File should have been created. - Assert.Null(fileStream); + // No content should be in the memory stream + Assert.Equal(2, pageBuffer.Length); + Assert.Equal(input, ReadBufferedContent(pageBuffer)); + } - // No content should be in the memory stream - Assert.Equal(2, pageBuffer.Length); - Assert.Equal(input, ReadBufferedContent(pageBuffer)); - } + [Fact] + public void Write_BuffersContentToDisk_WhenMemoryThresholdIsReached() + { + // Arrange + var input = new byte[] { 1, 2, 3, }; + using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory); + bufferingStream.Write(input, 0, 2); - [Fact] - public void Write_BuffersContentToDisk_WhenMemoryThresholdIsReached() - { - // Arrange - var input = new byte[] { 1, 2, 3, }; - using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory); - bufferingStream.Write(input, 0, 2); + // Act + bufferingStream.Write(input, 2, 1); - // Act - bufferingStream.Write(input, 2, 1); + // Assert + var pageBuffer = bufferingStream.PagedByteBuffer; + var fileStream = bufferingStream.FileStream; - // Assert - var pageBuffer = bufferingStream.PagedByteBuffer; - var fileStream = bufferingStream.FileStream; + // File should have been created. + Assert.NotNull(fileStream); + var fileBytes = ReadFileContent(fileStream!); + Assert.Equal(input, fileBytes); - // File should have been created. - Assert.NotNull(fileStream); - var fileBytes = ReadFileContent(fileStream!); - Assert.Equal(input, fileBytes); + // No content should be in the memory stream + Assert.Equal(0, pageBuffer.Length); + } - // No content should be in the memory stream - Assert.Equal(0, pageBuffer.Length); - } + [Fact] + public void Write_BuffersContentToDisk_WhenWriteWillOverflowMemoryThreshold() + { + // Arrange + var input = new byte[] { 1, 2, 3, }; + using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory); - [Fact] - public void Write_BuffersContentToDisk_WhenWriteWillOverflowMemoryThreshold() - { - // Arrange - var input = new byte[] { 1, 2, 3, }; - using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory); + // Act + bufferingStream.Write(input, 0, input.Length); - // Act - bufferingStream.Write(input, 0, input.Length); + // Assert + var pageBuffer = bufferingStream.PagedByteBuffer; + var fileStream = bufferingStream.FileStream; - // Assert - var pageBuffer = bufferingStream.PagedByteBuffer; - var fileStream = bufferingStream.FileStream; + // File should have been created. + Assert.NotNull(fileStream); + var fileBytes = ReadFileContent(fileStream!); + Assert.Equal(input, fileBytes); - // File should have been created. - Assert.NotNull(fileStream); - var fileBytes = ReadFileContent(fileStream!); - Assert.Equal(input, fileBytes); + // No content should be in the memory stream + Assert.Equal(0, pageBuffer.Length); + } - // No content should be in the memory stream - Assert.Equal(0, pageBuffer.Length); - } + [Fact] + public void Write_AfterMemoryThresholdIsReached_BuffersToMemory() + { + // Arrange + var input = new byte[] { 1, 2, 3, 4, 5, 6, 7 }; + using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 4, tempFileDirectoryAccessor: () => TempDirectory); - [Fact] - public void Write_AfterMemoryThresholdIsReached_BuffersToMemory() - { - // Arrange - var input = new byte[] { 1, 2, 3, 4, 5, 6, 7 }; - using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 4, tempFileDirectoryAccessor: () => TempDirectory); + // Act + bufferingStream.Write(input, 0, 5); + bufferingStream.Write(input, 5, 2); - // Act - bufferingStream.Write(input, 0, 5); - bufferingStream.Write(input, 5, 2); + // Assert + var pageBuffer = bufferingStream.PagedByteBuffer; + var fileStream = bufferingStream.FileStream; - // Assert - var pageBuffer = bufferingStream.PagedByteBuffer; - var fileStream = bufferingStream.FileStream; + // File should have been created. + Assert.NotNull(fileStream); + var fileBytes = ReadFileContent(fileStream!); + Assert.Equal(new byte[] { 1, 2, 3, 4, 5, }, fileBytes); - // File should have been created. - Assert.NotNull(fileStream); - var fileBytes = ReadFileContent(fileStream!); - Assert.Equal(new byte[] { 1, 2, 3, 4, 5, }, fileBytes); + Assert.Equal(new byte[] { 6, 7 }, ReadBufferedContent(pageBuffer)); + } - Assert.Equal(new byte[] { 6, 7 }, ReadBufferedContent(pageBuffer)); - } + [Fact] + public async Task WriteAsync_BuffersContentToMemory() + { + // Arrange + using var bufferingStream = new FileBufferingWriteStream(tempFileDirectoryAccessor: () => TempDirectory); + var input = Encoding.UTF8.GetBytes("Hello world"); - [Fact] - public async Task WriteAsync_BuffersContentToMemory() - { - // Arrange - using var bufferingStream = new FileBufferingWriteStream(tempFileDirectoryAccessor: () => TempDirectory); - var input = Encoding.UTF8.GetBytes("Hello world"); + // Act + await bufferingStream.WriteAsync(input, 0, input.Length); - // Act - await bufferingStream.WriteAsync(input, 0, input.Length); + // Assert + // We should have written content to memory + var pagedByteBuffer = bufferingStream.PagedByteBuffer; + Assert.Equal(input, ReadBufferedContent(pagedByteBuffer)); - // Assert - // We should have written content to memory - var pagedByteBuffer = bufferingStream.PagedByteBuffer; - Assert.Equal(input, ReadBufferedContent(pagedByteBuffer)); + // No files should not have been created. + Assert.Null(bufferingStream.FileStream); + } - // No files should not have been created. - Assert.Null(bufferingStream.FileStream); - } + [Fact] + public async Task WriteAsync_BeforeMemoryThresholdIsReached_WritesToMemory() + { + // Arrange + var input = new byte[] { 1, 2, }; + using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory); - [Fact] - public async Task WriteAsync_BeforeMemoryThresholdIsReached_WritesToMemory() - { - // Arrange - var input = new byte[] { 1, 2, }; - using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory); + // Act + await bufferingStream.WriteAsync(input, 0, 2); - // Act - await bufferingStream.WriteAsync(input, 0, 2); + // Assert + var pageBuffer = bufferingStream.PagedByteBuffer; + var fileStream = bufferingStream.FileStream; - // Assert - var pageBuffer = bufferingStream.PagedByteBuffer; - var fileStream = bufferingStream.FileStream; + // File should have been created. + Assert.Null(fileStream); - // File should have been created. - Assert.Null(fileStream); + // No content should be in the memory stream + Assert.Equal(2, pageBuffer.Length); + Assert.Equal(input, ReadBufferedContent(pageBuffer)); + } - // No content should be in the memory stream - Assert.Equal(2, pageBuffer.Length); - Assert.Equal(input, ReadBufferedContent(pageBuffer)); - } + [Fact] + public async Task WriteAsync_BuffersContentToDisk_WhenMemoryThresholdIsReached() + { + // Arrange + var input = new byte[] { 1, 2, 3, }; + using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory); + bufferingStream.Write(input, 0, 2); - [Fact] - public async Task WriteAsync_BuffersContentToDisk_WhenMemoryThresholdIsReached() - { - // Arrange - var input = new byte[] { 1, 2, 3, }; - using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory); - bufferingStream.Write(input, 0, 2); + // Act + await bufferingStream.WriteAsync(input, 2, 1); - // Act - await bufferingStream.WriteAsync(input, 2, 1); + // Assert + var pageBuffer = bufferingStream.PagedByteBuffer; + var fileStream = bufferingStream.FileStream; - // Assert - var pageBuffer = bufferingStream.PagedByteBuffer; - var fileStream = bufferingStream.FileStream; + // File should have been created. + Assert.NotNull(fileStream); + var fileBytes = ReadFileContent(fileStream!); + Assert.Equal(input, fileBytes); - // File should have been created. - Assert.NotNull(fileStream); - var fileBytes = ReadFileContent(fileStream!); - Assert.Equal(input, fileBytes); + // No content should be in the memory stream + Assert.Equal(0, pageBuffer.Length); + } - // No content should be in the memory stream - Assert.Equal(0, pageBuffer.Length); - } + [Fact] + public async Task WriteAsync_BuffersContentToDisk_WhenWriteWillOverflowMemoryThreshold() + { + // Arrange + var input = new byte[] { 1, 2, 3, }; + using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory); - [Fact] - public async Task WriteAsync_BuffersContentToDisk_WhenWriteWillOverflowMemoryThreshold() - { - // Arrange - var input = new byte[] { 1, 2, 3, }; - using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, tempFileDirectoryAccessor: () => TempDirectory); + // Act + await bufferingStream.WriteAsync(input, 0, input.Length); - // Act - await bufferingStream.WriteAsync(input, 0, input.Length); + // Assert + var pageBuffer = bufferingStream.PagedByteBuffer; + var fileStream = bufferingStream.FileStream; - // Assert - var pageBuffer = bufferingStream.PagedByteBuffer; - var fileStream = bufferingStream.FileStream; + // File should have been created. + Assert.NotNull(fileStream); + var fileBytes = ReadFileContent(fileStream!); + Assert.Equal(input, fileBytes); - // File should have been created. - Assert.NotNull(fileStream); - var fileBytes = ReadFileContent(fileStream!); - Assert.Equal(input, fileBytes); + // No content should be in the memory stream + Assert.Equal(0, pageBuffer.Length); + } - // No content should be in the memory stream - Assert.Equal(0, pageBuffer.Length); - } + [Fact] + public async Task WriteAsync_AfterMemoryThresholdIsReached_BuffersToMemory() + { + // Arrange + var input = new byte[] { 1, 2, 3, 4, 5, 6, 7 }; + using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 4, tempFileDirectoryAccessor: () => TempDirectory); - [Fact] - public async Task WriteAsync_AfterMemoryThresholdIsReached_BuffersToMemory() - { - // Arrange - var input = new byte[] { 1, 2, 3, 4, 5, 6, 7 }; - using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 4, tempFileDirectoryAccessor: () => TempDirectory); + // Act + await bufferingStream.WriteAsync(input, 0, 5); + await bufferingStream.WriteAsync(input, 5, 2); - // Act - await bufferingStream.WriteAsync(input, 0, 5); - await bufferingStream.WriteAsync(input, 5, 2); + // Assert + var pageBuffer = bufferingStream.PagedByteBuffer; + var fileStream = bufferingStream.FileStream; - // Assert - var pageBuffer = bufferingStream.PagedByteBuffer; - var fileStream = bufferingStream.FileStream; + // File should have been created. + Assert.NotNull(fileStream); + var fileBytes = ReadFileContent(fileStream!); - // File should have been created. - Assert.NotNull(fileStream); - var fileBytes = ReadFileContent(fileStream!); + Assert.Equal(input.Length, bufferingStream.Length); - Assert.Equal(input.Length, bufferingStream.Length); + Assert.Equal(new byte[] { 1, 2, 3, 4, 5, }, fileBytes); + Assert.Equal(new byte[] { 6, 7 }, ReadBufferedContent(pageBuffer)); + } - Assert.Equal(new byte[] { 1, 2, 3, 4, 5, }, fileBytes); - Assert.Equal(new byte[] { 6, 7 }, ReadBufferedContent(pageBuffer)); - } + [Fact] + public void Write_Throws_IfSingleWriteExceedsBufferLimit() + { + // Arrange + var input = new byte[20]; + var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory); - [Fact] - public void Write_Throws_IfSingleWriteExceedsBufferLimit() - { - // Arrange - var input = new byte[20]; - var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory); + // Act + var exception = Assert.Throws(() => bufferingStream.Write(input, 0, input.Length)); + Assert.Equal("Buffer limit exceeded.", exception.Message); - // Act - var exception = Assert.Throws(() => bufferingStream.Write(input, 0, input.Length)); - Assert.Equal("Buffer limit exceeded.", exception.Message); + Assert.True(bufferingStream.Disposed); + } - Assert.True(bufferingStream.Disposed); - } + [Fact] + public void Write_Throws_IfWriteCumulativeWritesExceedsBuffersLimit() + { + // Arrange + var input = new byte[6]; + var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory); - [Fact] - public void Write_Throws_IfWriteCumulativeWritesExceedsBuffersLimit() - { - // Arrange - var input = new byte[6]; - var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory); + // Act + bufferingStream.Write(input, 0, input.Length); + var exception = Assert.Throws(() => bufferingStream.Write(input, 0, input.Length)); + Assert.Equal("Buffer limit exceeded.", exception.Message); - // Act - bufferingStream.Write(input, 0, input.Length); - var exception = Assert.Throws(() => bufferingStream.Write(input, 0, input.Length)); - Assert.Equal("Buffer limit exceeded.", exception.Message); + // Verify we return the buffer. + Assert.True(bufferingStream.Disposed); + } - // Verify we return the buffer. - Assert.True(bufferingStream.Disposed); - } + [Fact] + public void Write_DoesNotThrow_IfBufferLimitIsReached() + { + // Arrange + var input = new byte[5]; + using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory); - [Fact] - public void Write_DoesNotThrow_IfBufferLimitIsReached() - { - // Arrange - var input = new byte[5]; - using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory); + // Act + bufferingStream.Write(input, 0, input.Length); + bufferingStream.Write(input, 0, input.Length); // Should get to exactly the buffer limit, which is fine - // Act - bufferingStream.Write(input, 0, input.Length); - bufferingStream.Write(input, 0, input.Length); // Should get to exactly the buffer limit, which is fine + // If we got here, the test succeeded. + } - // If we got here, the test succeeded. - } + [Fact] + public async Task WriteAsync_Throws_IfSingleWriteExceedsBufferLimit() + { + // Arrange + var input = new byte[20]; + var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory); - [Fact] - public async Task WriteAsync_Throws_IfSingleWriteExceedsBufferLimit() - { - // Arrange - var input = new byte[20]; - var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory); + // Act + var exception = await Assert.ThrowsAsync(() => bufferingStream.WriteAsync(input, 0, input.Length)); + Assert.Equal("Buffer limit exceeded.", exception.Message); - // Act - var exception = await Assert.ThrowsAsync(() => bufferingStream.WriteAsync(input, 0, input.Length)); - Assert.Equal("Buffer limit exceeded.", exception.Message); + Assert.True(bufferingStream.Disposed); + } - Assert.True(bufferingStream.Disposed); - } + [Fact] + public async Task WriteAsync_Throws_IfWriteCumulativeWritesExceedsBuffersLimit() + { + // Arrange + var input = new byte[6]; + var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory); - [Fact] - public async Task WriteAsync_Throws_IfWriteCumulativeWritesExceedsBuffersLimit() - { - // Arrange - var input = new byte[6]; - var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory); + // Act + await bufferingStream.WriteAsync(input, 0, input.Length); + var exception = await Assert.ThrowsAsync(() => bufferingStream.WriteAsync(input, 0, input.Length)); + Assert.Equal("Buffer limit exceeded.", exception.Message); - // Act - await bufferingStream.WriteAsync(input, 0, input.Length); - var exception = await Assert.ThrowsAsync(() => bufferingStream.WriteAsync(input, 0, input.Length)); - Assert.Equal("Buffer limit exceeded.", exception.Message); + // Verify we return the buffer. + Assert.True(bufferingStream.Disposed); + } - // Verify we return the buffer. - Assert.True(bufferingStream.Disposed); - } + [Fact] + public async Task WriteAsync_DoesNotThrow_IfBufferLimitIsReached() + { + // Arrange + var input = new byte[5]; + using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory); - [Fact] - public async Task WriteAsync_DoesNotThrow_IfBufferLimitIsReached() - { - // Arrange - var input = new byte[5]; - using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 2, bufferLimit: 10, tempFileDirectoryAccessor: () => TempDirectory); + // Act + await bufferingStream.WriteAsync(input, 0, input.Length); + await bufferingStream.WriteAsync(input, 0, input.Length); // Should get to exactly the buffer limit, which is fine - // Act - await bufferingStream.WriteAsync(input, 0, input.Length); - await bufferingStream.WriteAsync(input, 0, input.Length); // Should get to exactly the buffer limit, which is fine + // If we got here, the test succeeded. + } - // If we got here, the test succeeded. - } + [Fact] + public async Task DrainBufferAsync_CopiesContentFromMemoryStream() + { + // Arrange + var input = new byte[] { 1, 2, 3, 4, 5 }; + using var bufferingStream = new FileBufferingWriteStream(tempFileDirectoryAccessor: () => TempDirectory); + bufferingStream.Write(input, 0, input.Length); + var memoryStream = new MemoryStream(); + + // Act + await bufferingStream.DrainBufferAsync(memoryStream, default); + + // Assert + Assert.Equal(input, memoryStream.ToArray()); + Assert.Equal(0, bufferingStream.Length); + } - [Fact] - public async Task DrainBufferAsync_CopiesContentFromMemoryStream() - { - // Arrange - var input = new byte[] { 1, 2, 3, 4, 5 }; - using var bufferingStream = new FileBufferingWriteStream(tempFileDirectoryAccessor: () => TempDirectory); - bufferingStream.Write(input, 0, input.Length); - var memoryStream = new MemoryStream(); - - // Act - await bufferingStream.DrainBufferAsync(memoryStream, default); - - // Assert - Assert.Equal(input, memoryStream.ToArray()); - Assert.Equal(0, bufferingStream.Length); - } + [Fact] + public async Task DrainBufferAsync_WithContentInDisk_CopiesContentFromMemoryStream() + { + // Arrange + var input = Enumerable.Repeat((byte)0xca, 30).ToArray(); + using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 21, tempFileDirectoryAccessor: () => TempDirectory); + bufferingStream.Write(input, 0, input.Length); + var memoryStream = new MemoryStream(); + + // Act + await bufferingStream.DrainBufferAsync(memoryStream, default); + + // Assert + Assert.Equal(input, memoryStream.ToArray()); + Assert.Equal(0, bufferingStream.Length); + } - [Fact] - public async Task DrainBufferAsync_WithContentInDisk_CopiesContentFromMemoryStream() + public void Dispose() + { + try { - // Arrange - var input = Enumerable.Repeat((byte)0xca, 30).ToArray(); - using var bufferingStream = new FileBufferingWriteStream(memoryThreshold: 21, tempFileDirectoryAccessor: () => TempDirectory); - bufferingStream.Write(input, 0, input.Length); - var memoryStream = new MemoryStream(); - - // Act - await bufferingStream.DrainBufferAsync(memoryStream, default); - - // Assert - Assert.Equal(input, memoryStream.ToArray()); - Assert.Equal(0, bufferingStream.Length); + Directory.Delete(TempDirectory, recursive: true); } - - public void Dispose() + catch { - try - { - Directory.Delete(TempDirectory, recursive: true); - } - catch - { - } } + } - private static byte[] ReadFileContent(FileStream fileStream) - { - var fs = new FileStream(fileStream.Name, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.ReadWrite); - using var memoryStream = new MemoryStream(); - fs.CopyTo(memoryStream); + private static byte[] ReadFileContent(FileStream fileStream) + { + var fs = new FileStream(fileStream.Name, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.ReadWrite); + using var memoryStream = new MemoryStream(); + fs.CopyTo(memoryStream); - return memoryStream.ToArray(); - } + return memoryStream.ToArray(); + } - private static byte[] ReadBufferedContent(PagedByteBuffer buffer) - { - using var memoryStream = new MemoryStream(); - buffer.MoveTo(memoryStream); + private static byte[] ReadBufferedContent(PagedByteBuffer buffer) + { + using var memoryStream = new MemoryStream(); + buffer.MoveTo(memoryStream); - return memoryStream.ToArray(); - } + return memoryStream.ToArray(); } } diff --git a/src/Http/WebUtilities/test/FormPipeReaderTests.cs b/src/Http/WebUtilities/test/FormPipeReaderTests.cs index a34deb1d92..07c160da40 100644 --- a/src/Http/WebUtilities/test/FormPipeReaderTests.cs +++ b/src/Http/WebUtilities/test/FormPipeReaderTests.cs @@ -6,610 +6,609 @@ using System.IO.Pipelines; using System.Text; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +public class FormPipeReaderTests { - public class FormPipeReaderTests + [Fact] + public async Task ReadFormAsync_EmptyKeyAtEndAllowed() { - [Fact] - public async Task ReadFormAsync_EmptyKeyAtEndAllowed() - { - var bodyPipe = await MakePipeReader("=bar"); + var bodyPipe = await MakePipeReader("=bar"); - var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe)); + var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe)); - Assert.Equal("bar", formCollection[""].ToString()); - } + Assert.Equal("bar", formCollection[""].ToString()); + } - [Fact] - public async Task ReadFormAsync_EmptyKeyWithAdditionalEntryAllowed() - { - var bodyPipe = await MakePipeReader("=bar&baz=2"); + [Fact] + public async Task ReadFormAsync_EmptyKeyWithAdditionalEntryAllowed() + { + var bodyPipe = await MakePipeReader("=bar&baz=2"); - var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe)); + var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe)); - Assert.Equal("bar", formCollection[""].ToString()); - Assert.Equal("2", formCollection["baz"].ToString()); - } + Assert.Equal("bar", formCollection[""].ToString()); + Assert.Equal("2", formCollection["baz"].ToString()); + } - [Fact] - public async Task ReadFormAsync_EmptyValueAtEndAllowed() - { - var bodyPipe = await MakePipeReader("foo="); + [Fact] + public async Task ReadFormAsync_EmptyValueAtEndAllowed() + { + var bodyPipe = await MakePipeReader("foo="); - var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe)); + var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe)); - Assert.Equal("", formCollection["foo"].ToString()); - } + Assert.Equal("", formCollection["foo"].ToString()); + } - [Fact] - public async Task ReadFormAsync_EmptyValueWithoutEqualsAtEndAllowed() - { - var bodyPipe = await MakePipeReader("foo"); + [Fact] + public async Task ReadFormAsync_EmptyValueWithoutEqualsAtEndAllowed() + { + var bodyPipe = await MakePipeReader("foo"); - var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe)); + var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe)); - Assert.Equal("", formCollection["foo"].ToString()); - } + Assert.Equal("", formCollection["foo"].ToString()); + } - [Fact] - public async Task ReadFormAsync_EmptyValueWithAdditionalEntryAllowed() - { - var bodyPipe = await MakePipeReader("foo=&baz=2"); + [Fact] + public async Task ReadFormAsync_EmptyValueWithAdditionalEntryAllowed() + { + var bodyPipe = await MakePipeReader("foo=&baz=2"); - var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe)); + var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe)); - Assert.Equal("", formCollection["foo"].ToString()); - Assert.Equal("2", formCollection["baz"].ToString()); - } + Assert.Equal("", formCollection["foo"].ToString()); + Assert.Equal("2", formCollection["baz"].ToString()); + } - [Fact] - public async Task ReadFormAsync_EmptyValueWithoutEqualsWithAdditionalEntryAllowed() - { - var bodyPipe = await MakePipeReader("foo&baz=2"); + [Fact] + public async Task ReadFormAsync_EmptyValueWithoutEqualsWithAdditionalEntryAllowed() + { + var bodyPipe = await MakePipeReader("foo&baz=2"); - var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe)); + var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe)); - Assert.Equal("", formCollection["foo"].ToString()); - Assert.Equal("2", formCollection["baz"].ToString()); - } + Assert.Equal("", formCollection["foo"].ToString()); + Assert.Equal("2", formCollection["baz"].ToString()); + } - [Fact] - public async Task ReadFormAsync_ValueContainsInvalidCharacters_Throw() - { - var bodyPipe = await MakePipeReader("%00"); + [Fact] + public async Task ReadFormAsync_ValueContainsInvalidCharacters_Throw() + { + var bodyPipe = await MakePipeReader("%00"); - var exception = await Assert.ThrowsAsync( - () => ReadFormAsync(new FormPipeReader(bodyPipe))); + var exception = await Assert.ThrowsAsync( + () => ReadFormAsync(new FormPipeReader(bodyPipe))); - Assert.Equal("The form value contains invalid characters.", exception.Message); - Assert.IsType(exception.InnerException); - } + Assert.Equal("The form value contains invalid characters.", exception.Message); + Assert.IsType(exception.InnerException); + } - [Fact] - public async Task ReadFormAsync_ValueCountLimitMet_Success() - { - var bodyPipe = await MakePipeReader("foo=1&bar=2&baz=3"); + [Fact] + public async Task ReadFormAsync_ValueCountLimitMet_Success() + { + var bodyPipe = await MakePipeReader("foo=1&bar=2&baz=3"); - var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe) { ValueCountLimit = 3 }); + var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe) { ValueCountLimit = 3 }); - Assert.Equal("1", formCollection["foo"].ToString()); - Assert.Equal("2", formCollection["bar"].ToString()); - Assert.Equal("3", formCollection["baz"].ToString()); - Assert.Equal(3, formCollection.Count); - } + Assert.Equal("1", formCollection["foo"].ToString()); + Assert.Equal("2", formCollection["bar"].ToString()); + Assert.Equal("3", formCollection["baz"].ToString()); + Assert.Equal(3, formCollection.Count); + } - [Fact] - public async Task ReadFormAsync_ValueCountLimitExceeded_Throw() - { - var content = "foo=1&baz=2&bar=3&baz=4&baf=5"; - var bodyPipe = await MakePipeReader(content); + [Fact] + public async Task ReadFormAsync_ValueCountLimitExceeded_Throw() + { + var content = "foo=1&baz=2&bar=3&baz=4&baf=5"; + var bodyPipe = await MakePipeReader(content); - var exception = await Assert.ThrowsAsync( - () => ReadFormAsync(new FormPipeReader(bodyPipe) { ValueCountLimit = 3 })); - Assert.Equal("Form value count limit 3 exceeded.", exception.Message); + var exception = await Assert.ThrowsAsync( + () => ReadFormAsync(new FormPipeReader(bodyPipe) { ValueCountLimit = 3 })); + Assert.Equal("Form value count limit 3 exceeded.", exception.Message); - // The body pipe is still readable and has not advanced. - var readResult = await bodyPipe.ReadAsync(); - Assert.Equal(Encoding.UTF8.GetBytes(content), readResult.Buffer.ToArray()); - } + // The body pipe is still readable and has not advanced. + var readResult = await bodyPipe.ReadAsync(); + Assert.Equal(Encoding.UTF8.GetBytes(content), readResult.Buffer.ToArray()); + } - [Fact] - public async Task ReadFormAsync_ValueCountLimitExceededSameKey_Throw() - { - var content = "baz=1&baz=2&baz=3&baz=4"; - var bodyPipe = await MakePipeReader(content); + [Fact] + public async Task ReadFormAsync_ValueCountLimitExceededSameKey_Throw() + { + var content = "baz=1&baz=2&baz=3&baz=4"; + var bodyPipe = await MakePipeReader(content); - var exception = await Assert.ThrowsAsync( - () => ReadFormAsync(new FormPipeReader(bodyPipe) { ValueCountLimit = 3 })); - Assert.Equal("Form value count limit 3 exceeded.", exception.Message); + var exception = await Assert.ThrowsAsync( + () => ReadFormAsync(new FormPipeReader(bodyPipe) { ValueCountLimit = 3 })); + Assert.Equal("Form value count limit 3 exceeded.", exception.Message); - // The body pipe is still readable and has not advanced. - var readResult = await bodyPipe.ReadAsync(); - Assert.Equal(Encoding.UTF8.GetBytes(content), readResult.Buffer.ToArray()); - } + // The body pipe is still readable and has not advanced. + var readResult = await bodyPipe.ReadAsync(); + Assert.Equal(Encoding.UTF8.GetBytes(content), readResult.Buffer.ToArray()); + } - [Fact] - public async Task ReadFormAsync_KeyLengthLimitMet_Success() - { - var bodyPipe = await MakePipeReader("fooooooooo=1&bar=2&baz=3&baz=4"); + [Fact] + public async Task ReadFormAsync_KeyLengthLimitMet_Success() + { + var bodyPipe = await MakePipeReader("fooooooooo=1&bar=2&baz=3&baz=4"); - var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe) { KeyLengthLimit = 10 }); + var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe) { KeyLengthLimit = 10 }); - Assert.Equal("1", formCollection["fooooooooo"].ToString()); - Assert.Equal("2", formCollection["bar"].ToString()); - Assert.Equal("3,4", formCollection["baz"].ToString()); - Assert.Equal(3, formCollection.Count); - } + Assert.Equal("1", formCollection["fooooooooo"].ToString()); + Assert.Equal("2", formCollection["bar"].ToString()); + Assert.Equal("3,4", formCollection["baz"].ToString()); + Assert.Equal(3, formCollection.Count); + } - [Fact] - public async Task ReadFormAsync_KeyLengthLimitExceeded_Throw() - { - var content = "foo=1&baz12345678=2"; - var bodyPipe = await MakePipeReader(content); + [Fact] + public async Task ReadFormAsync_KeyLengthLimitExceeded_Throw() + { + var content = "foo=1&baz12345678=2"; + var bodyPipe = await MakePipeReader(content); - var exception = await Assert.ThrowsAsync( - () => ReadFormAsync(new FormPipeReader(bodyPipe) { KeyLengthLimit = 10 })); - Assert.Equal("Form key length limit 10 exceeded.", exception.Message); + var exception = await Assert.ThrowsAsync( + () => ReadFormAsync(new FormPipeReader(bodyPipe) { KeyLengthLimit = 10 })); + Assert.Equal("Form key length limit 10 exceeded.", exception.Message); - // The body pipe is still readable and has not advanced. - var readResult = await bodyPipe.ReadAsync(); - Assert.Equal(Encoding.UTF8.GetBytes(content), readResult.Buffer.ToArray()); - } + // The body pipe is still readable and has not advanced. + var readResult = await bodyPipe.ReadAsync(); + Assert.Equal(Encoding.UTF8.GetBytes(content), readResult.Buffer.ToArray()); + } - [Fact] - public async Task ReadFormAsync_ValueLengthLimitMet_Success() - { - var bodyPipe = await MakePipeReader("foo=1&bar=1234567890&baz=3&baz=4"); + [Fact] + public async Task ReadFormAsync_ValueLengthLimitMet_Success() + { + var bodyPipe = await MakePipeReader("foo=1&bar=1234567890&baz=3&baz=4"); - var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe) { ValueLengthLimit = 10 }); + var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe) { ValueLengthLimit = 10 }); - Assert.Equal("1", formCollection["foo"].ToString()); - Assert.Equal("1234567890", formCollection["bar"].ToString()); - Assert.Equal("3,4", formCollection["baz"].ToString()); - Assert.Equal(3, formCollection.Count); - } + Assert.Equal("1", formCollection["foo"].ToString()); + Assert.Equal("1234567890", formCollection["bar"].ToString()); + Assert.Equal("3,4", formCollection["baz"].ToString()); + Assert.Equal(3, formCollection.Count); + } - [Fact] - public async Task ReadFormAsync_ValueLengthLimitExceeded_Throw() - { - var content = "foo=1&baz=12345678901"; - var bodyPipe = await MakePipeReader(content); + [Fact] + public async Task ReadFormAsync_ValueLengthLimitExceeded_Throw() + { + var content = "foo=1&baz=12345678901"; + var bodyPipe = await MakePipeReader(content); - var exception = await Assert.ThrowsAsync( - () => ReadFormAsync(new FormPipeReader(bodyPipe) { ValueLengthLimit = 10 })); - Assert.Equal("Form value length limit 10 exceeded.", exception.Message); + var exception = await Assert.ThrowsAsync( + () => ReadFormAsync(new FormPipeReader(bodyPipe) { ValueLengthLimit = 10 })); + Assert.Equal("Form value length limit 10 exceeded.", exception.Message); - // The body pipe is still readable and has not advanced. - var readResult = await bodyPipe.ReadAsync(); - Assert.Equal(Encoding.UTF8.GetBytes(content), readResult.Buffer.ToArray()); - } + // The body pipe is still readable and has not advanced. + var readResult = await bodyPipe.ReadAsync(); + Assert.Equal(Encoding.UTF8.GetBytes(content), readResult.Buffer.ToArray()); + } - [Fact] - public async Task ReadFormAsync_ValueLengthLimitExceededAcrossBufferBoundary_Throw() - { - Pipe bodyPipe = new Pipe(); + [Fact] + public async Task ReadFormAsync_ValueLengthLimitExceededAcrossBufferBoundary_Throw() + { + Pipe bodyPipe = new Pipe(); - var content1 = "foo=1&baz=1234567890"; - var content2 = "1"; + var content1 = "foo=1&baz=1234567890"; + var content2 = "1"; - await bodyPipe.Writer.WriteAsync(Encoding.UTF8.GetBytes(content1)); - await bodyPipe.Writer.FlushAsync(); + await bodyPipe.Writer.WriteAsync(Encoding.UTF8.GetBytes(content1)); + await bodyPipe.Writer.FlushAsync(); - var readTask = Assert.ThrowsAsync( - () => ReadFormAsync(new FormPipeReader(bodyPipe.Reader) { ValueLengthLimit = 10 })); + var readTask = Assert.ThrowsAsync( + () => ReadFormAsync(new FormPipeReader(bodyPipe.Reader) { ValueLengthLimit = 10 })); - await bodyPipe.Writer.WriteAsync(Encoding.UTF8.GetBytes(content2)); - bodyPipe.Writer.Complete(); + await bodyPipe.Writer.WriteAsync(Encoding.UTF8.GetBytes(content2)); + bodyPipe.Writer.Complete(); - var exception = await readTask; - Assert.Equal("Form value length limit 10 exceeded.", exception.Message); + var exception = await readTask; + Assert.Equal("Form value length limit 10 exceeded.", exception.Message); - // The body pipe is still readable and has not advanced. - var readResult = await bodyPipe.Reader.ReadAsync(); - Assert.Equal(Encoding.UTF8.GetBytes("baz=12345678901"), readResult.Buffer.ToArray()); - } + // The body pipe is still readable and has not advanced. + var readResult = await bodyPipe.Reader.ReadAsync(); + Assert.Equal(Encoding.UTF8.GetBytes("baz=12345678901"), readResult.Buffer.ToArray()); + } - // https://en.wikipedia.org/wiki/Percent-encoding - [Theory] - [InlineData("++=hello", " ", "hello")] - [InlineData("a=1+1", "a", "1 1")] - [InlineData("%22%25%2D%2E%3C%3E%5C%5E%5F%60%7B%7C%7D%7E=%22%25%2D%2E%3C%3E%5C%5E%5F%60%7B%7C%7D%7E", "\"%-.<>\\^_`{|}~", "\"%-.<>\\^_`{|}~")] - [InlineData("a=%41", "a", "A")] // ascii encoded hex - [InlineData("a=%C3%A1", "a", "\u00e1")] // utf8 code points - [InlineData("a=%u20AC", "a", "%u20AC")] // utf16 not supported - public async Task ReadForm_Decoding(string formData, string key, string expectedValue) - { - var bodyPipe = await MakePipeReader(text: formData); + // https://en.wikipedia.org/wiki/Percent-encoding + [Theory] + [InlineData("++=hello", " ", "hello")] + [InlineData("a=1+1", "a", "1 1")] + [InlineData("%22%25%2D%2E%3C%3E%5C%5E%5F%60%7B%7C%7D%7E=%22%25%2D%2E%3C%3E%5C%5E%5F%60%7B%7C%7D%7E", "\"%-.<>\\^_`{|}~", "\"%-.<>\\^_`{|}~")] + [InlineData("a=%41", "a", "A")] // ascii encoded hex + [InlineData("a=%C3%A1", "a", "\u00e1")] // utf8 code points + [InlineData("a=%u20AC", "a", "%u20AC")] // utf16 not supported + public async Task ReadForm_Decoding(string formData, string key, string expectedValue) + { + var bodyPipe = await MakePipeReader(text: formData); - var form = await ReadFormAsync(new FormPipeReader(bodyPipe)); + var form = await ReadFormAsync(new FormPipeReader(bodyPipe)); - Assert.Equal(expectedValue, form[key]); - } + Assert.Equal(expectedValue, form[key]); + } - [Theory] - [MemberData(nameof(Encodings))] - public void TryParseFormValues_SingleSegmentWorks(Encoding encoding) - { - var readOnlySequence = new ReadOnlySequence(encoding.GetBytes("foo=bar&baz=boo")); + [Theory] + [MemberData(nameof(Encodings))] + public void TryParseFormValues_SingleSegmentWorks(Encoding encoding) + { + var readOnlySequence = new ReadOnlySequence(encoding.GetBytes("foo=bar&baz=boo")); - KeyValueAccumulator accumulator = default; - var formReader = new FormPipeReader(null!, encoding); + KeyValueAccumulator accumulator = default; + var formReader = new FormPipeReader(null!, encoding); - formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); - Assert.True(readOnlySequence.IsEmpty); + formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); + Assert.True(readOnlySequence.IsEmpty); - Assert.Equal(2, accumulator.KeyCount); - var dict = accumulator.GetResults(); - Assert.Equal("bar", dict["foo"]); - Assert.Equal("boo", dict["baz"]); - } + Assert.Equal(2, accumulator.KeyCount); + var dict = accumulator.GetResults(); + Assert.Equal("bar", dict["foo"]); + Assert.Equal("boo", dict["baz"]); + } - [Theory] - [MemberData(nameof(Encodings))] - public void TryParseFormValues_Works(Encoding encoding) - { - var readOnlySequence = ReadOnlySequenceFactory.SingleSegmentFactory.CreateWithContent(encoding.GetBytes("foo=bar&baz=boo&t=")); + [Theory] + [MemberData(nameof(Encodings))] + public void TryParseFormValues_Works(Encoding encoding) + { + var readOnlySequence = ReadOnlySequenceFactory.SingleSegmentFactory.CreateWithContent(encoding.GetBytes("foo=bar&baz=boo&t=")); - KeyValueAccumulator accumulator = default; + KeyValueAccumulator accumulator = default; - var formReader = new FormPipeReader(null!, encoding); - formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); - Assert.True(readOnlySequence.IsEmpty); + var formReader = new FormPipeReader(null!, encoding); + formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); + Assert.True(readOnlySequence.IsEmpty); - Assert.Equal(3, accumulator.KeyCount); - var dict = accumulator.GetResults(); - Assert.Equal("bar", dict["foo"]); - Assert.Equal("boo", dict["baz"]); - Assert.Equal("", dict["t"]); - } + Assert.Equal(3, accumulator.KeyCount); + var dict = accumulator.GetResults(); + Assert.Equal("bar", dict["foo"]); + Assert.Equal("boo", dict["baz"]); + Assert.Equal("", dict["t"]); + } - [Theory] - [MemberData(nameof(Encodings))] - public void TryParseFormValues_LimitsCanBeLarge(Encoding encoding) - { - var readOnlySequence = ReadOnlySequenceFactory.SingleSegmentFactory.CreateWithContent(encoding.GetBytes("foo=bar&baz=boo&t=")); - - KeyValueAccumulator accumulator = default; - - var formReader = new FormPipeReader(null!, encoding); - formReader.KeyLengthLimit = int.MaxValue; - formReader.ValueLengthLimit = int.MaxValue; - formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: false); - formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); - Assert.True(readOnlySequence.IsEmpty); - - Assert.Equal(3, accumulator.KeyCount); - var dict = accumulator.GetResults(); - Assert.Equal("bar", dict["foo"]); - Assert.Equal("boo", dict["baz"]); - Assert.Equal("", dict["t"]); - } + [Theory] + [MemberData(nameof(Encodings))] + public void TryParseFormValues_LimitsCanBeLarge(Encoding encoding) + { + var readOnlySequence = ReadOnlySequenceFactory.SingleSegmentFactory.CreateWithContent(encoding.GetBytes("foo=bar&baz=boo&t=")); + + KeyValueAccumulator accumulator = default; + + var formReader = new FormPipeReader(null!, encoding); + formReader.KeyLengthLimit = int.MaxValue; + formReader.ValueLengthLimit = int.MaxValue; + formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: false); + formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); + Assert.True(readOnlySequence.IsEmpty); + + Assert.Equal(3, accumulator.KeyCount); + var dict = accumulator.GetResults(); + Assert.Equal("bar", dict["foo"]); + Assert.Equal("boo", dict["baz"]); + Assert.Equal("", dict["t"]); + } - [Theory] - [MemberData(nameof(Encodings))] - public void TryParseFormValues_SplitAcrossSegmentsWorks(Encoding encoding) - { - var readOnlySequence = ReadOnlySequenceFactory.SegmentPerByteFactory.CreateWithContent(encoding.GetBytes("foo=bar&baz=boo&t=")); + [Theory] + [MemberData(nameof(Encodings))] + public void TryParseFormValues_SplitAcrossSegmentsWorks(Encoding encoding) + { + var readOnlySequence = ReadOnlySequenceFactory.SegmentPerByteFactory.CreateWithContent(encoding.GetBytes("foo=bar&baz=boo&t=")); - KeyValueAccumulator accumulator = default; + KeyValueAccumulator accumulator = default; - var formReader = new FormPipeReader(null!, encoding); - formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); - Assert.True(readOnlySequence.IsEmpty); + var formReader = new FormPipeReader(null!, encoding); + formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); + Assert.True(readOnlySequence.IsEmpty); - Assert.Equal(3, accumulator.KeyCount); - var dict = accumulator.GetResults(); - Assert.Equal("bar", dict["foo"]); - Assert.Equal("boo", dict["baz"]); - Assert.Equal("", dict["t"]); - } + Assert.Equal(3, accumulator.KeyCount); + var dict = accumulator.GetResults(); + Assert.Equal("bar", dict["foo"]); + Assert.Equal("boo", dict["baz"]); + Assert.Equal("", dict["t"]); + } - [Theory] - [MemberData(nameof(Encodings))] - public void TryParseFormValues_SplitAcrossSegmentsWorks_LimitsCanBeLarge(Encoding encoding) - { - var readOnlySequence = ReadOnlySequenceFactory.SegmentPerByteFactory.CreateWithContent(encoding.GetBytes("foo=bar&baz=boo&t=")); - - KeyValueAccumulator accumulator = default; - - var formReader = new FormPipeReader(null!, encoding); - formReader.KeyLengthLimit = int.MaxValue; - formReader.ValueLengthLimit = int.MaxValue; - formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: false); - formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); - Assert.True(readOnlySequence.IsEmpty); - - Assert.Equal(3, accumulator.KeyCount); - var dict = accumulator.GetResults(); - Assert.Equal("bar", dict["foo"]); - Assert.Equal("boo", dict["baz"]); - Assert.Equal("", dict["t"]); - } + [Theory] + [MemberData(nameof(Encodings))] + public void TryParseFormValues_SplitAcrossSegmentsWorks_LimitsCanBeLarge(Encoding encoding) + { + var readOnlySequence = ReadOnlySequenceFactory.SegmentPerByteFactory.CreateWithContent(encoding.GetBytes("foo=bar&baz=boo&t=")); + + KeyValueAccumulator accumulator = default; + + var formReader = new FormPipeReader(null!, encoding); + formReader.KeyLengthLimit = int.MaxValue; + formReader.ValueLengthLimit = int.MaxValue; + formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: false); + formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); + Assert.True(readOnlySequence.IsEmpty); + + Assert.Equal(3, accumulator.KeyCount); + var dict = accumulator.GetResults(); + Assert.Equal("bar", dict["foo"]); + Assert.Equal("boo", dict["baz"]); + Assert.Equal("", dict["t"]); + } - [Theory] - [MemberData(nameof(Encodings))] - public void TryParseFormValues_MultiSegmentWithArrayPoolAcrossSegmentsWorks(Encoding encoding) - { - var readOnlySequence = ReadOnlySequenceFactory.SegmentPerByteFactory.CreateWithContent(encoding.GetBytes("foo=bar&baz=bo" + new string('a', 128))); + [Theory] + [MemberData(nameof(Encodings))] + public void TryParseFormValues_MultiSegmentWithArrayPoolAcrossSegmentsWorks(Encoding encoding) + { + var readOnlySequence = ReadOnlySequenceFactory.SegmentPerByteFactory.CreateWithContent(encoding.GetBytes("foo=bar&baz=bo" + new string('a', 128))); - KeyValueAccumulator accumulator = default; + KeyValueAccumulator accumulator = default; - var formReader = new FormPipeReader(null!, encoding); - formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); - Assert.True(readOnlySequence.IsEmpty); + var formReader = new FormPipeReader(null!, encoding); + formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); + Assert.True(readOnlySequence.IsEmpty); - Assert.Equal(2, accumulator.KeyCount); - var dict = accumulator.GetResults(); - Assert.Equal("bar", dict["foo"]); - Assert.Equal("bo" + new string('a', 128), dict["baz"]); - } + Assert.Equal(2, accumulator.KeyCount); + var dict = accumulator.GetResults(); + Assert.Equal("bar", dict["foo"]); + Assert.Equal("bo" + new string('a', 128), dict["baz"]); + } - [Theory] - [MemberData(nameof(Encodings))] - public void TryParseFormValues_MultiSegmentSplitAcrossSegmentsWithPlusesWorks(Encoding encoding) - { - var readOnlySequence = ReadOnlySequenceFactory.SegmentPerByteFactory.CreateWithContent(encoding.GetBytes("+++=+++&++++=++++&+=")); + [Theory] + [MemberData(nameof(Encodings))] + public void TryParseFormValues_MultiSegmentSplitAcrossSegmentsWithPlusesWorks(Encoding encoding) + { + var readOnlySequence = ReadOnlySequenceFactory.SegmentPerByteFactory.CreateWithContent(encoding.GetBytes("+++=+++&++++=++++&+=")); - KeyValueAccumulator accumulator = default; + KeyValueAccumulator accumulator = default; - var formReader = new FormPipeReader(null!, encoding); - formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); - Assert.True(readOnlySequence.IsEmpty); + var formReader = new FormPipeReader(null!, encoding); + formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); + Assert.True(readOnlySequence.IsEmpty); - Assert.Equal(3, accumulator.KeyCount); - var dict = accumulator.GetResults(); - Assert.Equal(" ", dict[" "]); - Assert.Equal(" ", dict[" "]); - Assert.Equal("", dict[" "]); - } + Assert.Equal(3, accumulator.KeyCount); + var dict = accumulator.GetResults(); + Assert.Equal(" ", dict[" "]); + Assert.Equal(" ", dict[" "]); + Assert.Equal("", dict[" "]); + } - [Theory] - [MemberData(nameof(Encodings))] - public void TryParseFormValues_DecodedPlusesWorks(Encoding encoding) - { - var readOnlySequence = ReadOnlySequenceFactory.SingleSegmentFactory.CreateWithContent(encoding.GetBytes("++%2B=+++%2B&++++=++++&+=")); + [Theory] + [MemberData(nameof(Encodings))] + public void TryParseFormValues_DecodedPlusesWorks(Encoding encoding) + { + var readOnlySequence = ReadOnlySequenceFactory.SingleSegmentFactory.CreateWithContent(encoding.GetBytes("++%2B=+++%2B&++++=++++&+=")); - KeyValueAccumulator accumulator = default; + KeyValueAccumulator accumulator = default; - var formReader = new FormPipeReader(null!, encoding); - formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); - Assert.True(readOnlySequence.IsEmpty); + var formReader = new FormPipeReader(null!, encoding); + formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); + Assert.True(readOnlySequence.IsEmpty); - Assert.Equal(3, accumulator.KeyCount); - var dict = accumulator.GetResults(); - Assert.Equal(" ", dict[" "]); - Assert.Equal(" +", dict[" +"]); - Assert.Equal("", dict[" "]); - } + Assert.Equal(3, accumulator.KeyCount); + var dict = accumulator.GetResults(); + Assert.Equal(" ", dict[" "]); + Assert.Equal(" +", dict[" +"]); + Assert.Equal("", dict[" "]); + } - [Theory] - [MemberData(nameof(Encodings))] - public void TryParseFormValues_SplitAcrossSegmentsThatNeedDecodingWorks(Encoding encoding) - { - var readOnlySequence = ReadOnlySequenceFactory.SegmentPerByteFactory.CreateWithContent(encoding.GetBytes("\"%-.<>\\^_`{|}~=\"%-.<>\\^_`{|}~&\"%-.<>\\^_`{|}=wow")); + [Theory] + [MemberData(nameof(Encodings))] + public void TryParseFormValues_SplitAcrossSegmentsThatNeedDecodingWorks(Encoding encoding) + { + var readOnlySequence = ReadOnlySequenceFactory.SegmentPerByteFactory.CreateWithContent(encoding.GetBytes("\"%-.<>\\^_`{|}~=\"%-.<>\\^_`{|}~&\"%-.<>\\^_`{|}=wow")); - KeyValueAccumulator accumulator = default; + KeyValueAccumulator accumulator = default; - var formReader = new FormPipeReader(null!, encoding); - formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); - Assert.True(readOnlySequence.IsEmpty); + var formReader = new FormPipeReader(null!, encoding); + formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); + Assert.True(readOnlySequence.IsEmpty); - Assert.Equal(2, accumulator.KeyCount); - var dict = accumulator.GetResults(); - Assert.Equal("\"%-.<>\\^_`{|}~", dict["\"%-.<>\\^_`{|}~"]); - Assert.Equal("wow", dict["\"%-.<>\\^_`{|}"]); - } + Assert.Equal(2, accumulator.KeyCount); + var dict = accumulator.GetResults(); + Assert.Equal("\"%-.<>\\^_`{|}~", dict["\"%-.<>\\^_`{|}~"]); + Assert.Equal("wow", dict["\"%-.<>\\^_`{|}"]); + } - [Fact] - public void TryParseFormValues_MultiSegmentFastPathWorks() - { - var readOnlySequence = ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("foo=bar&"), Encoding.UTF8.GetBytes("baz=boo")); + [Fact] + public void TryParseFormValues_MultiSegmentFastPathWorks() + { + var readOnlySequence = ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("foo=bar&"), Encoding.UTF8.GetBytes("baz=boo")); - KeyValueAccumulator accumulator = default; + KeyValueAccumulator accumulator = default; - var formReader = new FormPipeReader(null!); - formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); - Assert.True(readOnlySequence.IsEmpty); + var formReader = new FormPipeReader(null!); + formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); + Assert.True(readOnlySequence.IsEmpty); - Assert.Equal(2, accumulator.KeyCount); - var dict = accumulator.GetResults(); - Assert.Equal("bar", dict["foo"]); - Assert.Equal("boo", dict["baz"]); - } + Assert.Equal(2, accumulator.KeyCount); + var dict = accumulator.GetResults(); + Assert.Equal("bar", dict["foo"]); + Assert.Equal("boo", dict["baz"]); + } - [Fact] - public void TryParseFormValues_ExceedKeyLengthThrows() - { - var readOnlySequence = ReadOnlySequenceFactory.SingleSegmentFactory.CreateWithContent(Encoding.UTF8.GetBytes("foo=bar&baz=boo&t=")); + [Fact] + public void TryParseFormValues_ExceedKeyLengthThrows() + { + var readOnlySequence = ReadOnlySequenceFactory.SingleSegmentFactory.CreateWithContent(Encoding.UTF8.GetBytes("foo=bar&baz=boo&t=")); - KeyValueAccumulator accumulator = default; + KeyValueAccumulator accumulator = default; - var formReader = new FormPipeReader(null!); - formReader.KeyLengthLimit = 2; + var formReader = new FormPipeReader(null!); + formReader.KeyLengthLimit = 2; - var exception = Assert.Throws(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true)); - Assert.Equal("Form key length limit 2 exceeded.", exception.Message); - } + var exception = Assert.Throws(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true)); + Assert.Equal("Form key length limit 2 exceeded.", exception.Message); + } - [Fact] - public void TryParseFormValues_ExceedKeyLengthThrowsInSplitSegment() - { - var readOnlySequence = ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("fo=bar&ba"), Encoding.UTF8.GetBytes("z=boo&t=")); + [Fact] + public void TryParseFormValues_ExceedKeyLengthThrowsInSplitSegment() + { + var readOnlySequence = ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("fo=bar&ba"), Encoding.UTF8.GetBytes("z=boo&t=")); - KeyValueAccumulator accumulator = default; + KeyValueAccumulator accumulator = default; - var formReader = new FormPipeReader(null!); - formReader.KeyLengthLimit = 2; + var formReader = new FormPipeReader(null!); + formReader.KeyLengthLimit = 2; - var exception = Assert.Throws(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true)); - Assert.Equal("Form key length limit 2 exceeded.", exception.Message); - } + var exception = Assert.Throws(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true)); + Assert.Equal("Form key length limit 2 exceeded.", exception.Message); + } - [Fact] - public void TryParseFormValues_ExceedValueLengthThrows() - { - var readOnlySequence = ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("foo=bar&baz=boo&t=")); + [Fact] + public void TryParseFormValues_ExceedValueLengthThrows() + { + var readOnlySequence = ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("foo=bar&baz=boo&t=")); - KeyValueAccumulator accumulator = default; + KeyValueAccumulator accumulator = default; - var formReader = new FormPipeReader(null!); - formReader.ValueLengthLimit = 2; + var formReader = new FormPipeReader(null!); + formReader.ValueLengthLimit = 2; - var exception = Assert.Throws(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true)); - Assert.Equal("Form value length limit 2 exceeded.", exception.Message); - } + var exception = Assert.Throws(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true)); + Assert.Equal("Form value length limit 2 exceeded.", exception.Message); + } - [Fact] - public void TryParseFormValues_ExceedValueLengthThrowsInSplitSegment() - { - var readOnlySequence = ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("foo=ba&baz=bo"), Encoding.UTF8.GetBytes("o&t=")); + [Fact] + public void TryParseFormValues_ExceedValueLengthThrowsInSplitSegment() + { + var readOnlySequence = ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("foo=ba&baz=bo"), Encoding.UTF8.GetBytes("o&t=")); - KeyValueAccumulator accumulator = default; + KeyValueAccumulator accumulator = default; - var formReader = new FormPipeReader(null!); - formReader.ValueLengthLimit = 2; + var formReader = new FormPipeReader(null!); + formReader.ValueLengthLimit = 2; - var exception = Assert.Throws(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true)); - Assert.Equal("Form value length limit 2 exceeded.", exception.Message); - } + var exception = Assert.Throws(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true)); + Assert.Equal("Form value length limit 2 exceeded.", exception.Message); + } - [Fact] - public void TryParseFormValues_ExceedKeyLengthThrowsInSplitSegmentEnd() - { - var readOnlySequence = ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("foo=ba&baz=bo"), Encoding.UTF8.GetBytes("o&asdfasdfasd=")); + [Fact] + public void TryParseFormValues_ExceedKeyLengthThrowsInSplitSegmentEnd() + { + var readOnlySequence = ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("foo=ba&baz=bo"), Encoding.UTF8.GetBytes("o&asdfasdfasd=")); - KeyValueAccumulator accumulator = default; + KeyValueAccumulator accumulator = default; - var formReader = new FormPipeReader(null!); - formReader.KeyLengthLimit = 10; + var formReader = new FormPipeReader(null!); + formReader.KeyLengthLimit = 10; - var exception = Assert.Throws(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true)); - Assert.Equal("Form key length limit 10 exceeded.", exception.Message); - } + var exception = Assert.Throws(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true)); + Assert.Equal("Form key length limit 10 exceeded.", exception.Message); + } - [Fact] - public void TryParseFormValues_ExceedValueLengthThrowsInSplitSegmentEnd() - { - var readOnlySequence = ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("foo=ba&baz=bo"), Encoding.UTF8.GetBytes("o&t=asdfasdfasd")); + [Fact] + public void TryParseFormValues_ExceedValueLengthThrowsInSplitSegmentEnd() + { + var readOnlySequence = ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("foo=ba&baz=bo"), Encoding.UTF8.GetBytes("o&t=asdfasdfasd")); - KeyValueAccumulator accumulator = default; + KeyValueAccumulator accumulator = default; - var formReader = new FormPipeReader(null!); - formReader.ValueLengthLimit = 10; + var formReader = new FormPipeReader(null!); + formReader.ValueLengthLimit = 10; - var exception = Assert.Throws(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true)); - Assert.Equal("Form value length limit 10 exceeded.", exception.Message); - } + var exception = Assert.Throws(() => formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true)); + Assert.Equal("Form value length limit 10 exceeded.", exception.Message); + } - [Fact] - public async Task ResetPipeWorks() + [Fact] + public async Task ResetPipeWorks() + { + // Same test that is in the benchmark + var pipe = new Pipe(); + var bytes = Encoding.UTF8.GetBytes("foo=bar&baz=boo"); + + for (var i = 0; i < 1000; i++) { - // Same test that is in the benchmark - var pipe = new Pipe(); - var bytes = Encoding.UTF8.GetBytes("foo=bar&baz=boo"); - - for (var i = 0; i < 1000; i++) - { - pipe.Writer.Write(bytes); - pipe.Writer.Complete(); - var formReader = new FormPipeReader(pipe.Reader); - await formReader.ReadFormAsync(); - pipe.Reader.Complete(); - pipe.Reset(); - } + pipe.Writer.Write(bytes); + pipe.Writer.Complete(); + var formReader = new FormPipeReader(pipe.Reader); + await formReader.ReadFormAsync(); + pipe.Reader.Complete(); + pipe.Reset(); } + } + + [Theory] + [MemberData(nameof(IncompleteFormKeys))] + public void ParseFormWithIncompleteKeyWhenIsFinalBlockSucceeds(ReadOnlySequence readOnlySequence) + { + KeyValueAccumulator accumulator = default; - [Theory] - [MemberData(nameof(IncompleteFormKeys))] - public void ParseFormWithIncompleteKeyWhenIsFinalBlockSucceeds(ReadOnlySequence readOnlySequence) + var formReader = new FormPipeReader(null!) { - KeyValueAccumulator accumulator = default; + KeyLengthLimit = 3 + }; - var formReader = new FormPipeReader(null!) - { - KeyLengthLimit = 3 - }; + formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); + Assert.True(readOnlySequence.IsEmpty); - formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); - Assert.True(readOnlySequence.IsEmpty); + IDictionary values = accumulator.GetResults(); + Assert.Contains("fo", values); + Assert.Equal("bar", values["fo"]); + Assert.Contains("ba", values); + Assert.Equal("", values["ba"]); + } - IDictionary values = accumulator.GetResults(); - Assert.Contains("fo", values); - Assert.Equal("bar", values["fo"]); - Assert.Contains("ba", values); - Assert.Equal("", values["ba"]); - } + [Theory] + [MemberData(nameof(IncompleteFormValues))] + public void ParseFormWithIncompleteValueWhenIsFinalBlockSucceeds(ReadOnlySequence readOnlySequence) + { + KeyValueAccumulator accumulator = default; - [Theory] - [MemberData(nameof(IncompleteFormValues))] - public void ParseFormWithIncompleteValueWhenIsFinalBlockSucceeds(ReadOnlySequence readOnlySequence) + var formReader = new FormPipeReader(null!) { - KeyValueAccumulator accumulator = default; + ValueLengthLimit = 3 + }; - var formReader = new FormPipeReader(null!) - { - ValueLengthLimit = 3 - }; + formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); + Assert.True(readOnlySequence.IsEmpty); - formReader.ParseFormValues(ref readOnlySequence, ref accumulator, isFinalBlock: true); - Assert.True(readOnlySequence.IsEmpty); - - IDictionary values = accumulator.GetResults(); - Assert.Contains("fo", values); - Assert.Equal("bar", values["fo"]); - Assert.Contains("b", values); - Assert.Equal("", values["b"]); - } + IDictionary values = accumulator.GetResults(); + Assert.Contains("fo", values); + Assert.Equal("bar", values["fo"]); + Assert.Contains("b", values); + Assert.Equal("", values["b"]); + } - [Fact] - public async Task ReadFormAsync_AccumulatesEmptyKeys() - { - var bodyPipe = await MakePipeReader("&&&"); + [Fact] + public async Task ReadFormAsync_AccumulatesEmptyKeys() + { + var bodyPipe = await MakePipeReader("&&&"); - var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe)); + var formCollection = await ReadFormAsync(new FormPipeReader(bodyPipe)); - Assert.Single(formCollection); - } + Assert.Single(formCollection); + } - public static TheoryData> IncompleteFormKeys => - new TheoryData> - { + public static TheoryData> IncompleteFormKeys => + new TheoryData> + { { ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("fo=bar&b"), Encoding.UTF8.GetBytes("a")) }, { new ReadOnlySequence(Encoding.UTF8.GetBytes("fo=bar&ba")) } - }; + }; - public static TheoryData> IncompleteFormValues => - new TheoryData> - { + public static TheoryData> IncompleteFormValues => + new TheoryData> + { { ReadOnlySequenceFactory.CreateSegments(Encoding.UTF8.GetBytes("fo=bar&b"), Encoding.UTF8.GetBytes("=")) }, { new ReadOnlySequence(Encoding.UTF8.GetBytes("fo=bar&b=")) } - }; + }; - public static TheoryData Encodings => - new TheoryData - { + public static TheoryData Encodings => + new TheoryData + { { Encoding.UTF8 }, { Encoding.UTF32 }, { Encoding.ASCII }, { Encoding.Unicode } - }; + }; - internal virtual Task> ReadFormAsync(FormPipeReader reader) - { - return reader.ReadFormAsync(); - } + internal virtual Task> ReadFormAsync(FormPipeReader reader) + { + return reader.ReadFormAsync(); + } - private static async Task MakePipeReader(string text) - { - var formContent = Encoding.UTF8.GetBytes(text); - Pipe bodyPipe = new Pipe(); + private static async Task MakePipeReader(string text) + { + var formContent = Encoding.UTF8.GetBytes(text); + Pipe bodyPipe = new Pipe(); - await bodyPipe.Writer.WriteAsync(formContent); + await bodyPipe.Writer.WriteAsync(formContent); - // Complete the writer so the reader will complete after processing all data. - bodyPipe.Writer.Complete(); - return bodyPipe.Reader; - } + // Complete the writer so the reader will complete after processing all data. + bodyPipe.Writer.Complete(); + return bodyPipe.Reader; } } diff --git a/src/Http/WebUtilities/test/FormReaderAsyncTest.cs b/src/Http/WebUtilities/test/FormReaderAsyncTest.cs index ddc6716cfc..5a204d2bbc 100644 --- a/src/Http/WebUtilities/test/FormReaderAsyncTest.cs +++ b/src/Http/WebUtilities/test/FormReaderAsyncTest.cs @@ -5,18 +5,17 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +public class FormReaderAsyncTest : FormReaderTests { - public class FormReaderAsyncTest : FormReaderTests + protected override async Task> ReadFormAsync(FormReader reader) { - protected override async Task> ReadFormAsync(FormReader reader) - { - return await reader.ReadFormAsync(); - } + return await reader.ReadFormAsync(); + } - protected override async Task?> ReadPair(FormReader reader) - { - return await reader.ReadNextPairAsync(); - } + protected override async Task?> ReadPair(FormReader reader) + { + return await reader.ReadNextPairAsync(); } -} \ No newline at end of file +} diff --git a/src/Http/WebUtilities/test/FormReaderTests.cs b/src/Http/WebUtilities/test/FormReaderTests.cs index 116d15ae81..8fc4d07c72 100644 --- a/src/Http/WebUtilities/test/FormReaderTests.cs +++ b/src/Http/WebUtilities/test/FormReaderTests.cs @@ -8,223 +8,222 @@ using System.Threading.Tasks; using Microsoft.Extensions.Primitives; using Xunit; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +public class FormReaderTests { - public class FormReaderTests + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_EmptyKeyAtEndAllowed(bool bufferRequest) { - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_EmptyKeyAtEndAllowed(bool bufferRequest) - { - var body = MakeStream(bufferRequest, "=bar"); + var body = MakeStream(bufferRequest, "=bar"); - var formCollection = await ReadFormAsync(new FormReader(body)); + var formCollection = await ReadFormAsync(new FormReader(body)); - Assert.Equal("bar", formCollection[""].ToString()); - } + Assert.Equal("bar", formCollection[""].ToString()); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_EmptyKeyWithAdditionalEntryAllowed(bool bufferRequest) - { - var body = MakeStream(bufferRequest, "=bar&baz=2"); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_EmptyKeyWithAdditionalEntryAllowed(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "=bar&baz=2"); - var formCollection = await ReadFormAsync(new FormReader(body)); + var formCollection = await ReadFormAsync(new FormReader(body)); - Assert.Equal("bar", formCollection[""].ToString()); - Assert.Equal("2", formCollection["baz"].ToString()); - } + Assert.Equal("bar", formCollection[""].ToString()); + Assert.Equal("2", formCollection["baz"].ToString()); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_EmptyValuedAtEndAllowed(bool bufferRequest) - { - var body = MakeStream(bufferRequest, "foo="); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_EmptyValuedAtEndAllowed(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo="); - var formCollection = await ReadFormAsync(new FormReader(body)); + var formCollection = await ReadFormAsync(new FormReader(body)); - Assert.Equal("", formCollection["foo"].ToString()); - } + Assert.Equal("", formCollection["foo"].ToString()); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_EmptyValuedWithAdditionalEntryAllowed(bool bufferRequest) - { - var body = MakeStream(bufferRequest, "foo=&baz=2"); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_EmptyValuedWithAdditionalEntryAllowed(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=&baz=2"); - var formCollection = await ReadFormAsync(new FormReader(body)); + var formCollection = await ReadFormAsync(new FormReader(body)); - Assert.Equal("", formCollection["foo"].ToString()); - Assert.Equal("2", formCollection["baz"].ToString()); - } + Assert.Equal("", formCollection["foo"].ToString()); + Assert.Equal("2", formCollection["baz"].ToString()); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_ValueCountLimitMet_Success(bool bufferRequest) - { - var body = MakeStream(bufferRequest, "foo=1&bar=2&baz=3"); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueCountLimitMet_Success(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&bar=2&baz=3"); - var formCollection = await ReadFormAsync(new FormReader(body) { ValueCountLimit = 3 }); + var formCollection = await ReadFormAsync(new FormReader(body) { ValueCountLimit = 3 }); - Assert.Equal("1", formCollection["foo"].ToString()); - Assert.Equal("2", formCollection["bar"].ToString()); - Assert.Equal("3", formCollection["baz"].ToString()); - Assert.Equal(3, formCollection.Count); - } + Assert.Equal("1", formCollection["foo"].ToString()); + Assert.Equal("2", formCollection["bar"].ToString()); + Assert.Equal("3", formCollection["baz"].ToString()); + Assert.Equal(3, formCollection.Count); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_ValueCountLimitExceeded_Throw(bool bufferRequest) - { - var body = MakeStream(bufferRequest, "foo=1&baz=2&bar=3&baz=4&baf=5"); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueCountLimitExceeded_Throw(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&baz=2&bar=3&baz=4&baf=5"); - var exception = await Assert.ThrowsAsync( - () => ReadFormAsync(new FormReader(body) { ValueCountLimit = 3 })); - Assert.Equal("Form value count limit 3 exceeded.", exception.Message); - } + var exception = await Assert.ThrowsAsync( + () => ReadFormAsync(new FormReader(body) { ValueCountLimit = 3 })); + Assert.Equal("Form value count limit 3 exceeded.", exception.Message); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_ValueCountLimitExceededSameKey_Throw(bool bufferRequest) - { - var body = MakeStream(bufferRequest, "baz=1&baz=2&baz=3&baz=4"); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueCountLimitExceededSameKey_Throw(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "baz=1&baz=2&baz=3&baz=4"); - var exception = await Assert.ThrowsAsync( - () => ReadFormAsync(new FormReader(body) { ValueCountLimit = 3 })); - Assert.Equal("Form value count limit 3 exceeded.", exception.Message); - } + var exception = await Assert.ThrowsAsync( + () => ReadFormAsync(new FormReader(body) { ValueCountLimit = 3 })); + Assert.Equal("Form value count limit 3 exceeded.", exception.Message); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_KeyLengthLimitMet_Success(bool bufferRequest) - { - var body = MakeStream(bufferRequest, "foo=1&bar=2&baz=3&baz=4"); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_KeyLengthLimitMet_Success(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&bar=2&baz=3&baz=4"); - var formCollection = await ReadFormAsync(new FormReader(body) { KeyLengthLimit = 10 }); + var formCollection = await ReadFormAsync(new FormReader(body) { KeyLengthLimit = 10 }); - Assert.Equal("1", formCollection["foo"].ToString()); - Assert.Equal("2", formCollection["bar"].ToString()); - Assert.Equal("3,4", formCollection["baz"].ToString()); - Assert.Equal(3, formCollection.Count); - } + Assert.Equal("1", formCollection["foo"].ToString()); + Assert.Equal("2", formCollection["bar"].ToString()); + Assert.Equal("3,4", formCollection["baz"].ToString()); + Assert.Equal(3, formCollection.Count); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_KeyLengthLimitExceeded_Throw(bool bufferRequest) - { - var body = MakeStream(bufferRequest, "foo=1&baz1234567890=2"); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_KeyLengthLimitExceeded_Throw(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&baz1234567890=2"); - var exception = await Assert.ThrowsAsync( - () => ReadFormAsync(new FormReader(body) { KeyLengthLimit = 10 })); - Assert.Equal("Form key or value length limit 10 exceeded.", exception.Message); - } + var exception = await Assert.ThrowsAsync( + () => ReadFormAsync(new FormReader(body) { KeyLengthLimit = 10 })); + Assert.Equal("Form key or value length limit 10 exceeded.", exception.Message); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_ValueLengthLimitMet_Success(bool bufferRequest) - { - var body = MakeStream(bufferRequest, "foo=1&bar=1234567890&baz=3&baz=4"); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueLengthLimitMet_Success(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&bar=1234567890&baz=3&baz=4"); - var formCollection = await ReadFormAsync(new FormReader(body) { ValueLengthLimit = 10 }); + var formCollection = await ReadFormAsync(new FormReader(body) { ValueLengthLimit = 10 }); - Assert.Equal("1", formCollection["foo"].ToString()); - Assert.Equal("1234567890", formCollection["bar"].ToString()); - Assert.Equal("3,4", formCollection["baz"].ToString()); - Assert.Equal(3, formCollection.Count); - } + Assert.Equal("1", formCollection["foo"].ToString()); + Assert.Equal("1234567890", formCollection["bar"].ToString()); + Assert.Equal("3,4", formCollection["baz"].ToString()); + Assert.Equal(3, formCollection.Count); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadFormAsync_ValueLengthLimitExceeded_Throw(bool bufferRequest) - { - var body = MakeStream(bufferRequest, "foo=1&baz=1234567890123"); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadFormAsync_ValueLengthLimitExceeded_Throw(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=1&baz=1234567890123"); - var exception = await Assert.ThrowsAsync( - () => ReadFormAsync(new FormReader(body) { ValueLengthLimit = 10 })); - Assert.Equal("Form key or value length limit 10 exceeded.", exception.Message); - } + var exception = await Assert.ThrowsAsync( + () => ReadFormAsync(new FormReader(body) { ValueLengthLimit = 10 })); + Assert.Equal("Form key or value length limit 10 exceeded.", exception.Message); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadNextPair_ReadsAllPairs(bool bufferRequest) - { - var body = MakeStream(bufferRequest, "foo=&baz=2"); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadNextPair_ReadsAllPairs(bool bufferRequest) + { + var body = MakeStream(bufferRequest, "foo=&baz=2"); - var reader = new FormReader(body); + var reader = new FormReader(body); - var pair = (KeyValuePair)await ReadPair(reader); + var pair = (KeyValuePair)await ReadPair(reader); - Assert.Equal("foo", pair.Key); - Assert.Equal("", pair.Value); + Assert.Equal("foo", pair.Key); + Assert.Equal("", pair.Value); - pair = (KeyValuePair)await ReadPair(reader); + pair = (KeyValuePair)await ReadPair(reader); - Assert.Equal("baz", pair.Key); - Assert.Equal("2", pair.Value); + Assert.Equal("baz", pair.Key); + Assert.Equal("2", pair.Value); - Assert.Null(await ReadPair(reader)); - } + Assert.Null(await ReadPair(reader)); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task ReadNextPair_ReturnsNullOnEmptyStream(bool bufferRequest) - { - var body = MakeStream(bufferRequest, ""); + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ReadNextPair_ReturnsNullOnEmptyStream(bool bufferRequest) + { + var body = MakeStream(bufferRequest, ""); - var reader = new FormReader(body); + var reader = new FormReader(body); - Assert.Null(await ReadPair(reader)); - } + Assert.Null(await ReadPair(reader)); + } - // https://en.wikipedia.org/wiki/Percent-encoding - [Theory] - [InlineData("++=hello", " ", "hello")] - [InlineData("a=1+1", "a", "1 1")] - [InlineData("%22%25%2D%2E%3C%3E%5C%5E%5F%60%7B%7C%7D%7E=%22%25%2D%2E%3C%3E%5C%5E%5F%60%7B%7C%7D%7E", "\"%-.<>\\^_`{|}~", "\"%-.<>\\^_`{|}~")] - [InlineData("a=%41", "a", "A")] // ascii encoded hex - [InlineData("a=%C3%A1", "a", "\u00e1")] // utf8 code points - [InlineData("a=%u20AC", "a", "%u20AC")] // utf16 not supported - public async Task ReadForm_Decoding(string formData, string key, string expectedValue) - { - var body = MakeStream(bufferRequest: false, text: formData); + // https://en.wikipedia.org/wiki/Percent-encoding + [Theory] + [InlineData("++=hello", " ", "hello")] + [InlineData("a=1+1", "a", "1 1")] + [InlineData("%22%25%2D%2E%3C%3E%5C%5E%5F%60%7B%7C%7D%7E=%22%25%2D%2E%3C%3E%5C%5E%5F%60%7B%7C%7D%7E", "\"%-.<>\\^_`{|}~", "\"%-.<>\\^_`{|}~")] + [InlineData("a=%41", "a", "A")] // ascii encoded hex + [InlineData("a=%C3%A1", "a", "\u00e1")] // utf8 code points + [InlineData("a=%u20AC", "a", "%u20AC")] // utf16 not supported + public async Task ReadForm_Decoding(string formData, string key, string expectedValue) + { + var body = MakeStream(bufferRequest: false, text: formData); - var form = await ReadFormAsync(new FormReader(body)); + var form = await ReadFormAsync(new FormReader(body)); - Assert.Equal(expectedValue, form[key]); - } + Assert.Equal(expectedValue, form[key]); + } - protected virtual Task> ReadFormAsync(FormReader reader) - { - return Task.FromResult(reader.ReadForm()); - } + protected virtual Task> ReadFormAsync(FormReader reader) + { + return Task.FromResult(reader.ReadForm()); + } - protected virtual Task?> ReadPair(FormReader reader) - { - return Task.FromResult(reader.ReadNextPair()); - } + protected virtual Task?> ReadPair(FormReader reader) + { + return Task.FromResult(reader.ReadNextPair()); + } - private static Stream MakeStream(bool bufferRequest, string text) + private static Stream MakeStream(bool bufferRequest, string text) + { + var formContent = Encoding.UTF8.GetBytes(text); + Stream body = new MemoryStream(formContent); + if (!bufferRequest) { - var formContent = Encoding.UTF8.GetBytes(text); - Stream body = new MemoryStream(formContent); - if (!bufferRequest) - { - body = new NonSeekableReadStream(body); - } - return body; + body = new NonSeekableReadStream(body); } + return body; } -} \ No newline at end of file +} diff --git a/src/Http/WebUtilities/test/HttpRequestStreamReaderTest.cs b/src/Http/WebUtilities/test/HttpRequestStreamReaderTest.cs index 1f970bce91..b31d7f339d 100644 --- a/src/Http/WebUtilities/test/HttpRequestStreamReaderTest.cs +++ b/src/Http/WebUtilities/test/HttpRequestStreamReaderTest.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Moq; using System; using System.Buffers; using System.Collections.Generic; @@ -9,15 +8,16 @@ using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; +using Moq; using Xunit; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +public class HttpRequestStreamReaderTest { - public class HttpRequestStreamReaderTest + private static readonly char[] CharData = new char[] { - private static readonly char[] CharData = new char[] - { char.MinValue, char.MaxValue, '\t', @@ -43,535 +43,534 @@ namespace Microsoft.AspNetCore.WebUtilities '\n', 'K', '\u00E6', - }; + }; - [Fact] - public static async Task ReadToEndAsync() - { - // Arrange - var reader = new HttpRequestStreamReader(GetLargeStream(), Encoding.UTF8); + [Fact] + public static async Task ReadToEndAsync() + { + // Arrange + var reader = new HttpRequestStreamReader(GetLargeStream(), Encoding.UTF8); - var result = await reader.ReadToEndAsync(); + var result = await reader.ReadToEndAsync(); - Assert.Equal(5000, result.Length); - } + Assert.Equal(5000, result.Length); + } - [Fact] - public static async Task ReadToEndAsync_Reads_Asynchronously() - { - // Arrange - var stream = new AsyncOnlyStreamWrapper(GetLargeStream()); - var reader = new HttpRequestStreamReader(stream, Encoding.UTF8); - var streamReader = new StreamReader(GetLargeStream()); - string expected = await streamReader.ReadToEndAsync(); + [Fact] + public static async Task ReadToEndAsync_Reads_Asynchronously() + { + // Arrange + var stream = new AsyncOnlyStreamWrapper(GetLargeStream()); + var reader = new HttpRequestStreamReader(stream, Encoding.UTF8); + var streamReader = new StreamReader(GetLargeStream()); + string expected = await streamReader.ReadToEndAsync(); - // Act - var actual = await reader.ReadToEndAsync(); + // Act + var actual = await reader.ReadToEndAsync(); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); + } - [Fact] - public static void TestRead() - { - // Arrange - var reader = CreateReader(); + [Fact] + public static void TestRead() + { + // Arrange + var reader = CreateReader(); - // Act & Assert - for (var i = 0; i < CharData.Length; i++) - { - var tmp = reader.Read(); - Assert.Equal((int)CharData[i], tmp); - } + // Act & Assert + for (var i = 0; i < CharData.Length; i++) + { + var tmp = reader.Read(); + Assert.Equal((int)CharData[i], tmp); } + } - [Fact] - public static void TestPeek() - { - // Arrange - var reader = CreateReader(); + [Fact] + public static void TestPeek() + { + // Arrange + var reader = CreateReader(); - // Act & Assert - for (var i = 0; i < CharData.Length; i++) - { - var peek = reader.Peek(); - Assert.Equal((int)CharData[i], peek); + // Act & Assert + for (var i = 0; i < CharData.Length; i++) + { + var peek = reader.Peek(); + Assert.Equal((int)CharData[i], peek); - reader.Read(); - } + reader.Read(); } + } - [Fact] - public static void EmptyStream() - { - // Arrange - var reader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8); - var buffer = new char[10]; - - // Act - var read = reader.Read(buffer, 0, 1); + [Fact] + public static void EmptyStream() + { + // Arrange + var reader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8); + var buffer = new char[10]; - // Assert - Assert.Equal(0, read); - } + // Act + var read = reader.Read(buffer, 0, 1); - [Fact] - public static void Read_ReadAllCharactersAtOnce() - { - // Arrange - var reader = CreateReader(); - var chars = new char[CharData.Length]; + // Assert + Assert.Equal(0, read); + } - // Act - var read = reader.Read(chars, 0, chars.Length); + [Fact] + public static void Read_ReadAllCharactersAtOnce() + { + // Arrange + var reader = CreateReader(); + var chars = new char[CharData.Length]; - // Assert - Assert.Equal(chars.Length, read); - for (var i = 0; i < CharData.Length; i++) - { - Assert.Equal(CharData[i], chars[i]); - } - } + // Act + var read = reader.Read(chars, 0, chars.Length); - [Fact] - public static async Task ReadAsync_ReadInTwoChunks() + // Assert + Assert.Equal(chars.Length, read); + for (var i = 0; i < CharData.Length; i++) { - // Arrange - var reader = CreateReader(); - var chars = new char[CharData.Length]; + Assert.Equal(CharData[i], chars[i]); + } + } - // Act - var read = await reader.ReadAsync(chars, 4, 3); + [Fact] + public static async Task ReadAsync_ReadInTwoChunks() + { + // Arrange + var reader = CreateReader(); + var chars = new char[CharData.Length]; - // Assert - Assert.Equal(3, read); - for (var i = 0; i < 3; i++) - { - Assert.Equal(CharData[i], chars[i + 4]); - } - } + // Act + var read = await reader.ReadAsync(chars, 4, 3); - [Theory] - [MemberData(nameof(ReadLineData))] - public static async Task ReadLine_ReadMultipleLines(Func> action) + // Assert + Assert.Equal(3, read); + for (var i = 0; i < 3; i++) { - // Arrange - var reader = CreateReader(); - var valueString = new string(CharData); + Assert.Equal(CharData[i], chars[i + 4]); + } + } - // Act & Assert - var data = await action(reader); - Assert.Equal(valueString.Substring(0, valueString.IndexOf('\r')), data); + [Theory] + [MemberData(nameof(ReadLineData))] + public static async Task ReadLine_ReadMultipleLines(Func> action) + { + // Arrange + var reader = CreateReader(); + var valueString = new string(CharData); - data = await action(reader); - Assert.Equal(valueString.Substring(valueString.IndexOf('\r') + 1, 3), data); + // Act & Assert + var data = await action(reader); + Assert.Equal(valueString.Substring(0, valueString.IndexOf('\r')), data); - data = await action(reader); - Assert.Equal(valueString.Substring(valueString.IndexOf('\n') + 1, 2), data); + data = await action(reader); + Assert.Equal(valueString.Substring(valueString.IndexOf('\r') + 1, 3), data); - data = await action(reader); - Assert.Equal((valueString.Substring(valueString.LastIndexOf('\n') + 1)), data); - } + data = await action(reader); + Assert.Equal(valueString.Substring(valueString.IndexOf('\n') + 1, 2), data); - [Theory] - [MemberData(nameof(ReadLineData))] - public static async Task ReadLine_ReadWithNoNewlines(Func> action) - { - // Arrange - var reader = CreateReader(); - var valueString = new string(CharData); - var temp = new char[10]; + data = await action(reader); + Assert.Equal((valueString.Substring(valueString.LastIndexOf('\n') + 1)), data); + } - // Act - reader.Read(temp, 0, 1); - var data = await action(reader); + [Theory] + [MemberData(nameof(ReadLineData))] + public static async Task ReadLine_ReadWithNoNewlines(Func> action) + { + // Arrange + var reader = CreateReader(); + var valueString = new string(CharData); + var temp = new char[10]; - // Assert - Assert.Equal(valueString.Substring(1, valueString.IndexOf('\r') - 1), data); - } + // Act + reader.Read(temp, 0, 1); + var data = await action(reader); - [Theory] - [MemberData(nameof(ReadLineData))] - public static async Task ReadLine_MultipleContinuousLines(Func> action) - { - // Arrange - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - writer.Write("\n\n\r\r\n\r"); - writer.Flush(); - stream.Position = 0; + // Assert + Assert.Equal(valueString.Substring(1, valueString.IndexOf('\r') - 1), data); + } - var reader = new HttpRequestStreamReader(stream, Encoding.UTF8); + [Theory] + [MemberData(nameof(ReadLineData))] + public static async Task ReadLine_MultipleContinuousLines(Func> action) + { + // Arrange + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write("\n\n\r\r\n\r"); + writer.Flush(); + stream.Position = 0; - // Act & Assert - for (var i = 0; i < 5; i++) - { - var data = await action(reader); - Assert.Equal(string.Empty, data); - } + var reader = new HttpRequestStreamReader(stream, Encoding.UTF8); - var eof = await action(reader); - Assert.Null(eof); + // Act & Assert + for (var i = 0; i < 5; i++) + { + var data = await action(reader); + Assert.Equal(string.Empty, data); } - [Theory] - [MemberData(nameof(ReadLineData))] - public static async Task ReadLine_CarriageReturnAndLineFeedAcrossBufferBundaries(Func> action) - { - // Arrange - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - writer.Write("123456789\r\nfoo"); - writer.Flush(); - stream.Position = 0; + var eof = await action(reader); + Assert.Null(eof); + } - var reader = new HttpRequestStreamReader(stream, Encoding.UTF8, 10); + [Theory] + [MemberData(nameof(ReadLineData))] + public static async Task ReadLine_CarriageReturnAndLineFeedAcrossBufferBundaries(Func> action) + { + // Arrange + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write("123456789\r\nfoo"); + writer.Flush(); + stream.Position = 0; - // Act & Assert - var data = await action(reader); - Assert.Equal("123456789", data); + var reader = new HttpRequestStreamReader(stream, Encoding.UTF8, 10); - data = await action(reader); - Assert.Equal("foo", data); + // Act & Assert + var data = await action(reader); + Assert.Equal("123456789", data); - var eof = await action(reader); - Assert.Null(eof); - } + data = await action(reader); + Assert.Equal("foo", data); - [Theory] - [MemberData(nameof(ReadLineData))] - public static async Task ReadLine_EOF(Func> action) - { - // Arrange - var stream = new MemoryStream(); - var reader = new HttpRequestStreamReader(stream, Encoding.UTF8); + var eof = await action(reader); + Assert.Null(eof); + } - // Act & Assert - var eof = await action(reader); - Assert.Null(eof); - } + [Theory] + [MemberData(nameof(ReadLineData))] + public static async Task ReadLine_EOF(Func> action) + { + // Arrange + var stream = new MemoryStream(); + var reader = new HttpRequestStreamReader(stream, Encoding.UTF8); - [Theory] - [MemberData(nameof(ReadLineData))] - public static async Task ReadLine_NewLineOnly(Func> action) - { - // Arrange - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - writer.Write("\r\n"); - writer.Flush(); - stream.Position = 0; - - var reader = new HttpRequestStreamReader(stream, Encoding.UTF8); - - // Act & Assert - var empty = await action(reader); - Assert.Equal(string.Empty, empty); - } + // Act & Assert + var eof = await action(reader); + Assert.Null(eof); + } - [Fact] - public static void Read_Span_ReadAllCharactersAtOnce() - { - // Arrange - var reader = CreateReader(); - var chars = new char[CharData.Length]; - var span = new Span(chars); + [Theory] + [MemberData(nameof(ReadLineData))] + public static async Task ReadLine_NewLineOnly(Func> action) + { + // Arrange + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write("\r\n"); + writer.Flush(); + stream.Position = 0; + + var reader = new HttpRequestStreamReader(stream, Encoding.UTF8); + + // Act & Assert + var empty = await action(reader); + Assert.Equal(string.Empty, empty); + } - // Act - var read = reader.Read(span); + [Fact] + public static void Read_Span_ReadAllCharactersAtOnce() + { + // Arrange + var reader = CreateReader(); + var chars = new char[CharData.Length]; + var span = new Span(chars); - // Assert - Assert.Equal(chars.Length, read); - for (var i = 0; i < CharData.Length; i++) - { - Assert.Equal(CharData[i], chars[i]); - } - } + // Act + var read = reader.Read(span); - [Fact] - public static void Read_Span_WithMoreDataThanInternalBufferSize() + // Assert + Assert.Equal(chars.Length, read); + for (var i = 0; i < CharData.Length; i++) { - // Arrange - var reader = CreateReader(10); - var chars = new char[CharData.Length]; - var span = new Span(chars); + Assert.Equal(CharData[i], chars[i]); + } + } - // Act - var read = reader.Read(span); + [Fact] + public static void Read_Span_WithMoreDataThanInternalBufferSize() + { + // Arrange + var reader = CreateReader(10); + var chars = new char[CharData.Length]; + var span = new Span(chars); - // Assert - Assert.Equal(chars.Length, read); - for (var i = 0; i < CharData.Length; i++) - { - Assert.Equal(CharData[i], chars[i]); - } - } + // Act + var read = reader.Read(span); - [Fact] - public static async Task ReadAsync_Memory_ReadAllCharactersAtOnce() + // Assert + Assert.Equal(chars.Length, read); + for (var i = 0; i < CharData.Length; i++) { - // Arrange - var reader = CreateReader(); - var chars = new char[CharData.Length]; - var memory = new Memory(chars); + Assert.Equal(CharData[i], chars[i]); + } + } - // Act - var read = await reader.ReadAsync(memory); + [Fact] + public static async Task ReadAsync_Memory_ReadAllCharactersAtOnce() + { + // Arrange + var reader = CreateReader(); + var chars = new char[CharData.Length]; + var memory = new Memory(chars); - // Assert - Assert.Equal(chars.Length, read); - for (var i = 0; i < CharData.Length; i++) - { - Assert.Equal(CharData[i], chars[i]); - } - } + // Act + var read = await reader.ReadAsync(memory); - [Fact] - public static async Task ReadAsync_Memory_WithMoreDataThanInternalBufferSize() + // Assert + Assert.Equal(chars.Length, read); + for (var i = 0; i < CharData.Length; i++) { - // Arrange - var reader = CreateReader(10); - var chars = new char[CharData.Length]; - var memory = new Memory(chars); + Assert.Equal(CharData[i], chars[i]); + } + } - // Act - var read = await reader.ReadAsync(memory); + [Fact] + public static async Task ReadAsync_Memory_WithMoreDataThanInternalBufferSize() + { + // Arrange + var reader = CreateReader(10); + var chars = new char[CharData.Length]; + var memory = new Memory(chars); - // Assert - Assert.Equal(chars.Length, read); - for (var i = 0; i < CharData.Length; i++) - { - Assert.Equal(CharData[i], chars[i]); - } - } + // Act + var read = await reader.ReadAsync(memory); - [Theory] - [MemberData(nameof(HttpRequestNullData))] - public static void NullInputsInConstructor_ExpectArgumentNullException(Stream stream, Encoding encoding, ArrayPool bytePool, ArrayPool charPool) + // Assert + Assert.Equal(chars.Length, read); + for (var i = 0; i < CharData.Length; i++) { - Assert.Throws(() => - { - var httpRequestStreamReader = new HttpRequestStreamReader(stream, encoding, 1, bytePool, charPool); - }); + Assert.Equal(CharData[i], chars[i]); } + } - [Theory] - [InlineData(0)] - [InlineData(-1)] - public static void NegativeOrZeroBufferSize_ExpectArgumentOutOfRangeException(int size) + [Theory] + [MemberData(nameof(HttpRequestNullData))] + public static void NullInputsInConstructor_ExpectArgumentNullException(Stream stream, Encoding encoding, ArrayPool bytePool, ArrayPool charPool) + { + Assert.Throws(() => { - Assert.Throws(() => - { - var httpRequestStreamReader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8, size, ArrayPool.Shared, ArrayPool.Shared); - }); - } + var httpRequestStreamReader = new HttpRequestStreamReader(stream, encoding, 1, bytePool, charPool); + }); + } - [Fact] - public static void StreamCannotRead_ExpectArgumentException() + [Theory] + [InlineData(0)] + [InlineData(-1)] + public static void NegativeOrZeroBufferSize_ExpectArgumentOutOfRangeException(int size) + { + Assert.Throws(() => { - var mockStream = new Mock(); - mockStream.Setup(m => m.CanRead).Returns(false); - Assert.Throws(() => - { - var httpRequestStreamReader = new HttpRequestStreamReader(mockStream.Object, Encoding.UTF8, 1, ArrayPool.Shared, ArrayPool.Shared); - }); - } + var httpRequestStreamReader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8, size, ArrayPool.Shared, ArrayPool.Shared); + }); + } - [Theory] - [MemberData(nameof(HttpRequestDisposeData))] - public static void StreamDisposed_ExpectedObjectDisposedException(Action action) + [Fact] + public static void StreamCannotRead_ExpectArgumentException() + { + var mockStream = new Mock(); + mockStream.Setup(m => m.CanRead).Returns(false); + Assert.Throws(() => { - var httpRequestStreamReader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8, 10, ArrayPool.Shared, ArrayPool.Shared); - httpRequestStreamReader.Dispose(); + var httpRequestStreamReader = new HttpRequestStreamReader(mockStream.Object, Encoding.UTF8, 1, ArrayPool.Shared, ArrayPool.Shared); + }); + } - Assert.Throws(() => - { - action(httpRequestStreamReader); - }); - } + [Theory] + [MemberData(nameof(HttpRequestDisposeData))] + public static void StreamDisposed_ExpectedObjectDisposedException(Action action) + { + var httpRequestStreamReader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8, 10, ArrayPool.Shared, ArrayPool.Shared); + httpRequestStreamReader.Dispose(); - [Theory] - [MemberData(nameof(HttpRequestDisposeDataAsync))] - public static async Task StreamDisposed_ExpectObjectDisposedExceptionAsync(Func action) + Assert.Throws(() => { - var httpRequestStreamReader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8, 10, ArrayPool.Shared, ArrayPool.Shared); - httpRequestStreamReader.Dispose(); - - await Assert.ThrowsAsync(() => action(httpRequestStreamReader)); - } + action(httpRequestStreamReader); + }); + } - private static HttpRequestStreamReader CreateReader() - { - MemoryStream stream = CreateStream(); - return new HttpRequestStreamReader(stream, Encoding.UTF8); - } + [Theory] + [MemberData(nameof(HttpRequestDisposeDataAsync))] + public static async Task StreamDisposed_ExpectObjectDisposedExceptionAsync(Func action) + { + var httpRequestStreamReader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8, 10, ArrayPool.Shared, ArrayPool.Shared); + httpRequestStreamReader.Dispose(); - private static HttpRequestStreamReader CreateReader(int bufferSize) - { - MemoryStream stream = CreateStream(); - return new HttpRequestStreamReader(stream, Encoding.UTF8, bufferSize); - } + await Assert.ThrowsAsync(() => action(httpRequestStreamReader)); + } - private static MemoryStream CreateStream() - { - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - writer.Write(CharData); - writer.Flush(); - stream.Position = 0; - return stream; - } + private static HttpRequestStreamReader CreateReader() + { + MemoryStream stream = CreateStream(); + return new HttpRequestStreamReader(stream, Encoding.UTF8); + } - private static MemoryStream GetSmallStream() - { - var testData = new byte[] { 72, 69, 76, 76, 79 }; - return new MemoryStream(testData); - } + private static HttpRequestStreamReader CreateReader(int bufferSize) + { + MemoryStream stream = CreateStream(); + return new HttpRequestStreamReader(stream, Encoding.UTF8, bufferSize); + } - private static MemoryStream GetLargeStream() - { - var testData = new byte[] { 72, 69, 76, 76, 79 }; - // System.Collections.Generic. + private static MemoryStream CreateStream() + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(CharData); + writer.Flush(); + stream.Position = 0; + return stream; + } - var data = new List(); - for (var i = 0; i < 1000; i++) - { - data.AddRange(testData); - } + private static MemoryStream GetSmallStream() + { + var testData = new byte[] { 72, 69, 76, 76, 79 }; + return new MemoryStream(testData); + } - return new MemoryStream(data.ToArray()); - } + private static MemoryStream GetLargeStream() + { + var testData = new byte[] { 72, 69, 76, 76, 79 }; + // System.Collections.Generic. - public static IEnumerable HttpRequestNullData() + var data = new List(); + for (var i = 0; i < 1000; i++) { - yield return new object?[] { null, Encoding.UTF8, ArrayPool.Shared, ArrayPool.Shared }; - yield return new object?[] { new MemoryStream(), null, ArrayPool.Shared, ArrayPool.Shared }; - yield return new object?[] { new MemoryStream(), Encoding.UTF8, null, ArrayPool.Shared }; - yield return new object?[] { new MemoryStream(), Encoding.UTF8, ArrayPool.Shared, null }; + data.AddRange(testData); } - public static IEnumerable HttpRequestDisposeData() - { - yield return new object[] { new Action((httpRequestStreamReader) => + return new MemoryStream(data.ToArray()); + } + + public static IEnumerable HttpRequestNullData() + { + yield return new object?[] { null, Encoding.UTF8, ArrayPool.Shared, ArrayPool.Shared }; + yield return new object?[] { new MemoryStream(), null, ArrayPool.Shared, ArrayPool.Shared }; + yield return new object?[] { new MemoryStream(), Encoding.UTF8, null, ArrayPool.Shared }; + yield return new object?[] { new MemoryStream(), Encoding.UTF8, ArrayPool.Shared, null }; + } + + public static IEnumerable HttpRequestDisposeData() + { + yield return new object[] { new Action((httpRequestStreamReader) => { var res = httpRequestStreamReader.Read(); })}; - yield return new object[] { new Action((httpRequestStreamReader) => + yield return new object[] { new Action((httpRequestStreamReader) => { var res = httpRequestStreamReader.Read(new char[10], 0, 1); })}; - yield return new object[] { new Action((httpRequestStreamReader) => + yield return new object[] { new Action((httpRequestStreamReader) => { var res = httpRequestStreamReader.Read(new Span(new char[10], 0, 1)); })}; - yield return new object[] { new Action((httpRequestStreamReader) => + yield return new object[] { new Action((httpRequestStreamReader) => { var res = httpRequestStreamReader.Peek(); })}; - } + } - public static IEnumerable HttpRequestDisposeDataAsync() - { - yield return new object[] { new Func(async (httpRequestStreamReader) => + public static IEnumerable HttpRequestDisposeDataAsync() + { + yield return new object[] { new Func(async (httpRequestStreamReader) => { await httpRequestStreamReader.ReadAsync(new char[10], 0, 1); })}; - yield return new object[] { new Func(async (httpRequestStreamReader) => + yield return new object[] { new Func(async (httpRequestStreamReader) => { await httpRequestStreamReader.ReadAsync(new Memory(new char[10], 0, 1)); })}; - } + } - public static IEnumerable ReadLineData() - { - yield return new object[] { new Func>((httpRequestStreamReader) => + public static IEnumerable ReadLineData() + { + yield return new object[] { new Func>((httpRequestStreamReader) => Task.FromResult(httpRequestStreamReader.ReadLine()) )}; - yield return new object[] { new Func>((httpRequestStreamReader) => + yield return new object[] { new Func>((httpRequestStreamReader) => httpRequestStreamReader.ReadLineAsync() )}; - } + } - private class AsyncOnlyStreamWrapper : Stream - { - private readonly Stream _inner; + private class AsyncOnlyStreamWrapper : Stream + { + private readonly Stream _inner; - public AsyncOnlyStreamWrapper(Stream inner) - { - _inner = inner; - } + public AsyncOnlyStreamWrapper(Stream inner) + { + _inner = inner; + } - public override bool CanRead => _inner.CanRead; + public override bool CanRead => _inner.CanRead; - public override bool CanSeek => _inner.CanSeek; + public override bool CanSeek => _inner.CanSeek; - public override bool CanWrite => _inner.CanWrite; + public override bool CanWrite => _inner.CanWrite; - public override long Length => _inner.Length; + public override long Length => _inner.Length; - public override long Position - { - get => _inner.Position; - set => _inner.Position = value; - } + public override long Position + { + get => _inner.Position; + set => _inner.Position = value; + } - public override void Flush() - { - throw SyncOperationForbiddenException(); - } + public override void Flush() + { + throw SyncOperationForbiddenException(); + } - public override Task FlushAsync(CancellationToken cancellationToken) - { - return _inner.FlushAsync(cancellationToken); - } + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _inner.FlushAsync(cancellationToken); + } - public override int Read(byte[] buffer, int offset, int count) - { - throw SyncOperationForbiddenException(); - } + public override int Read(byte[] buffer, int offset, int count) + { + throw SyncOperationForbiddenException(); + } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _inner.ReadAsync(buffer, offset, count, cancellationToken); - } + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.ReadAsync(buffer, offset, count, cancellationToken); + } - public override long Seek(long offset, SeekOrigin origin) - { - return _inner.Seek(offset, origin); - } + public override long Seek(long offset, SeekOrigin origin) + { + return _inner.Seek(offset, origin); + } - public override void SetLength(long value) - { - _inner.SetLength(value); - } + public override void SetLength(long value) + { + _inner.SetLength(value); + } - public override void Write(byte[] buffer, int offset, int count) - { - throw SyncOperationForbiddenException(); - } + public override void Write(byte[] buffer, int offset, int count) + { + throw SyncOperationForbiddenException(); + } - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _inner.WriteAsync(buffer, offset, count, cancellationToken); - } + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _inner.WriteAsync(buffer, offset, count, cancellationToken); + } - protected override void Dispose(bool disposing) - { - _inner.Dispose(); - } + protected override void Dispose(bool disposing) + { + _inner.Dispose(); + } - public override ValueTask DisposeAsync() - { - return _inner.DisposeAsync(); - } + public override ValueTask DisposeAsync() + { + return _inner.DisposeAsync(); + } - private Exception SyncOperationForbiddenException() - { - return new InvalidOperationException("The stream cannot be accessed synchronously"); - } + private Exception SyncOperationForbiddenException() + { + return new InvalidOperationException("The stream cannot be accessed synchronously"); } } } diff --git a/src/Http/WebUtilities/test/HttpResponseStreamWriterTest.cs b/src/Http/WebUtilities/test/HttpResponseStreamWriterTest.cs index 84e3fcdbea..3115cf1ace 100644 --- a/src/Http/WebUtilities/test/HttpResponseStreamWriterTest.cs +++ b/src/Http/WebUtilities/test/HttpResponseStreamWriterTest.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Moq; using System; using System.Buffers; using System.Collections.Generic; @@ -9,739 +8,739 @@ using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; +using Moq; using Xunit; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +public class HttpResponseStreamWriterTest { - public class HttpResponseStreamWriterTest + private const int DefaultCharacterChunkSize = HttpResponseStreamWriter.DefaultBufferSize; + + [Fact] + public async Task DoesNotWriteBOM() { - private const int DefaultCharacterChunkSize = HttpResponseStreamWriter.DefaultBufferSize; + // Arrange + var memoryStream = new MemoryStream(); + var encodingWithBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true); + var writer = new HttpResponseStreamWriter(memoryStream, encodingWithBOM); + var expectedData = new byte[] { 97, 98, 99, 100 }; // without BOM - [Fact] - public async Task DoesNotWriteBOM() + // Act + using (writer) { - // Arrange - var memoryStream = new MemoryStream(); - var encodingWithBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: true); - var writer = new HttpResponseStreamWriter(memoryStream, encodingWithBOM); - var expectedData = new byte[] { 97, 98, 99, 100 }; // without BOM - - // Act - using (writer) - { - await writer.WriteAsync("abcd"); - } - - // Assert - Assert.Equal(expectedData, memoryStream.ToArray()); + await writer.WriteAsync("abcd"); } - [Fact] - public async Task DoesNotFlush_UnderlyingStream_OnDisposingWriter() - { - // Arrange - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - - // Act - await writer.WriteAsync("Hello"); - writer.Dispose(); - - // Assert - Assert.Equal(0, stream.FlushCallCount); - Assert.Equal(0, stream.FlushAsyncCallCount); - } - - [Fact] - public async Task DoesNotDispose_UnderlyingStream_OnDisposingWriter() - { - // Arrange - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - - // Act - await writer.WriteAsync("Hello world"); - writer.Dispose(); + // Assert + Assert.Equal(expectedData, memoryStream.ToArray()); + } - // Assert - Assert.Equal(0, stream.DisposeCallCount); - } + [Fact] + public async Task DoesNotFlush_UnderlyingStream_OnDisposingWriter() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - [Theory] - [InlineData(1023)] - [InlineData(1024)] - [InlineData(1050)] - [InlineData(2048)] - public async Task FlushesBuffer_OnClose(int byteLength) - { - // Arrange - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - await writer.WriteAsync(new string('a', byteLength)); + // Act + await writer.WriteAsync("Hello"); + writer.Dispose(); - // Act - writer.Dispose(); + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.FlushAsyncCallCount); + } - // Assert - Assert.Equal(0, stream.FlushCallCount); - Assert.Equal(0, stream.FlushAsyncCallCount); - Assert.Equal(byteLength, stream.Length); - } + [Fact] + public async Task DoesNotDispose_UnderlyingStream_OnDisposingWriter() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - [Theory] - [InlineData(1023)] - [InlineData(1024)] - [InlineData(1050)] - [InlineData(2048)] - public async Task FlushesBuffer_OnDispose(int byteLength) - { - // Arrange - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - await writer.WriteAsync(new string('a', byteLength)); + // Act + await writer.WriteAsync("Hello world"); + writer.Dispose(); - // Act - writer.Dispose(); + // Assert + Assert.Equal(0, stream.DisposeCallCount); + } - // Assert - Assert.Equal(0, stream.FlushCallCount); - Assert.Equal(0, stream.FlushAsyncCallCount); - Assert.Equal(byteLength, stream.Length); - } + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task FlushesBuffer_OnClose(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + await writer.WriteAsync(new string('a', byteLength)); + + // Act + writer.Dispose(); + + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(byteLength, stream.Length); + } - [Fact] - public void NoDataWritten_Flush_DoesNotFlushUnderlyingStream() - { - // Arrange - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task FlushesBuffer_OnDispose(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + await writer.WriteAsync(new string('a', byteLength)); + + // Act + writer.Dispose(); + + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(byteLength, stream.Length); + } - // Act - writer.Flush(); + [Fact] + public void NoDataWritten_Flush_DoesNotFlushUnderlyingStream() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - // Assert - Assert.Equal(0, stream.FlushCallCount); - Assert.Equal(0, stream.Length); - } + // Act + writer.Flush(); - [Theory] - [InlineData(1023)] - [InlineData(1024)] - [InlineData(1050)] - [InlineData(2048)] - public void FlushesBuffer_ButNotStream_OnFlush(int byteLength) - { - // Arrange - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - writer.Write(new string('a', byteLength)); + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.Length); + } - var expectedWriteCount = Math.Ceiling((double)byteLength / HttpResponseStreamWriter.DefaultBufferSize); + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public void FlushesBuffer_ButNotStream_OnFlush(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + writer.Write(new string('a', byteLength)); - // Act - writer.Flush(); + var expectedWriteCount = Math.Ceiling((double)byteLength / HttpResponseStreamWriter.DefaultBufferSize); - // Assert - Assert.Equal(0, stream.FlushCallCount); - Assert.Equal(expectedWriteCount, stream.WriteCallCount); - Assert.Equal(byteLength, stream.Length); - } + // Act + writer.Flush(); - [Fact] - public async Task NoDataWritten_FlushAsync_DoesNotFlushUnderlyingStream() - { - // Arrange - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + // Assert + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(expectedWriteCount, stream.WriteCallCount); + Assert.Equal(byteLength, stream.Length); + } - // Act - await writer.FlushAsync(); + [Fact] + public async Task NoDataWritten_FlushAsync_DoesNotFlushUnderlyingStream() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - // Assert - Assert.Equal(0, stream.FlushAsyncCallCount); - Assert.Equal(0, stream.Length); - } + // Act + await writer.FlushAsync(); - [Theory] - [InlineData(HttpResponseStreamWriter.DefaultBufferSize - 1)] - [InlineData(HttpResponseStreamWriter.DefaultBufferSize)] - [InlineData(HttpResponseStreamWriter.DefaultBufferSize + 1)] - [InlineData(HttpResponseStreamWriter.DefaultBufferSize * 2)] - public async Task FlushesBuffer_ButNotStream_OnFlushAsync(int byteLength) - { - // Arrange - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - await writer.WriteAsync(new string('a', byteLength)); + // Assert + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(0, stream.Length); + } - var expectedWriteCount = Math.Ceiling((double)byteLength / HttpResponseStreamWriter.DefaultBufferSize); + [Theory] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize - 1)] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize)] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize + 1)] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize * 2)] + public async Task FlushesBuffer_ButNotStream_OnFlushAsync(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + await writer.WriteAsync(new string('a', byteLength)); - // Act - await writer.FlushAsync(); + var expectedWriteCount = Math.Ceiling((double)byteLength / HttpResponseStreamWriter.DefaultBufferSize); - // Assert - Assert.Equal(0, stream.FlushAsyncCallCount); - Assert.Equal(expectedWriteCount, stream.WriteAsyncCallCount); - Assert.Equal(byteLength, stream.Length); - } + // Act + await writer.FlushAsync(); - [Theory] - [InlineData(1023)] - [InlineData(1024)] - public async Task FlushWriteThrows_DontFlushInDispose(int byteLength) - { - // Arrange - var stream = new TestMemoryStream() { ThrowOnWrite = true }; - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + // Assert + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(expectedWriteCount, stream.WriteAsyncCallCount); + Assert.Equal(byteLength, stream.Length); + } - await writer.WriteAsync(new string('a', byteLength)); - await Assert.ThrowsAsync(() => writer.FlushAsync()); + [Theory] + [InlineData(1023)] + [InlineData(1024)] + public async Task FlushWriteThrows_DontFlushInDispose(int byteLength) + { + // Arrange + var stream = new TestMemoryStream() { ThrowOnWrite = true }; + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + await writer.WriteAsync(new string('a', byteLength)); + await Assert.ThrowsAsync(() => writer.FlushAsync()); + + // Act + writer.Dispose(); + + // Assert + Assert.Equal(1, stream.WriteAsyncCallCount); + Assert.Equal(0, stream.WriteCallCount); + Assert.Equal(0, stream.FlushCallCount); + Assert.Equal(0, stream.FlushAsyncCallCount); + Assert.Equal(0, stream.Length); + } - // Act - writer.Dispose(); - - // Assert - Assert.Equal(1, stream.WriteAsyncCallCount); - Assert.Equal(0, stream.WriteCallCount); - Assert.Equal(0, stream.FlushCallCount); - Assert.Equal(0, stream.FlushAsyncCallCount); - Assert.Equal(0, stream.Length); - } + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public void WriteChar_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - [Theory] - [InlineData(1023)] - [InlineData(1024)] - [InlineData(1050)] - [InlineData(2048)] - public void WriteChar_WritesToStream(int byteLength) + // Act + using (writer) { - // Arrange - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - - // Act - using (writer) + for (var i = 0; i < byteLength; i++) { - for (var i = 0; i < byteLength; i++) - { - writer.Write('a'); - } + writer.Write('a'); } - - // Assert - Assert.Equal(byteLength, stream.Length); } - [Theory] - [InlineData(1023)] - [InlineData(1024)] - [InlineData(1050)] - [InlineData(2048)] - public void WriteCharArray_WritesToStream(int byteLength) - { - // Arrange - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + // Assert + Assert.Equal(byteLength, stream.Length); + } - // Act - using (writer) - { - writer.Write((new string('a', byteLength)).ToCharArray()); - } + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public void WriteCharArray_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - // Assert - Assert.Equal(byteLength, stream.Length); + // Act + using (writer) + { + writer.Write((new string('a', byteLength)).ToCharArray()); } - [Theory] - [InlineData(1023)] - [InlineData(1024)] - [InlineData(1050)] - [InlineData(2048)] - [InlineData(HttpResponseStreamWriter.DefaultBufferSize + 1)] - public void WriteReadOnlySpanChar_WritesToStream(int byteLength) - { - // Arrange - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + // Assert + Assert.Equal(byteLength, stream.Length); + } - // Act - using (writer) - { - var array = new string('a', byteLength).ToCharArray(); - var span = new ReadOnlySpan(array); - writer.Write(span); - } + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize + 1)] + public void WriteReadOnlySpanChar_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - // Assert - Assert.Equal(byteLength, stream.Length); + // Act + using (writer) + { + var array = new string('a', byteLength).ToCharArray(); + var span = new ReadOnlySpan(array); + writer.Write(span); } - [Theory] - [InlineData(1022, "\n")] - [InlineData(1023, "\n")] - [InlineData(1024, "\n")] - [InlineData(1050, "\n")] - [InlineData(2047, "\n")] - [InlineData(2048, "\n")] - [InlineData(1021, "\r\n")] - [InlineData(1022, "\r\n")] - [InlineData(1023, "\r\n")] - [InlineData(1024, "\r\n")] - [InlineData(1050, "\r\n")] - [InlineData(2046, "\r\n")] - [InlineData(2048, "\r\n")] - [InlineData(HttpResponseStreamWriter.DefaultBufferSize + 1, "\r\n")] - public void WriteLineReadOnlySpanChar_WritesToStream(int byteLength, string newLine) - { - // Arrange - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + // Assert + Assert.Equal(byteLength, stream.Length); + } - writer.NewLine = newLine; - // Act - using (writer) - { - var array = new string('a', byteLength).ToCharArray(); - var span = new ReadOnlySpan(array); - writer.WriteLine(span); - } + [Theory] + [InlineData(1022, "\n")] + [InlineData(1023, "\n")] + [InlineData(1024, "\n")] + [InlineData(1050, "\n")] + [InlineData(2047, "\n")] + [InlineData(2048, "\n")] + [InlineData(1021, "\r\n")] + [InlineData(1022, "\r\n")] + [InlineData(1023, "\r\n")] + [InlineData(1024, "\r\n")] + [InlineData(1050, "\r\n")] + [InlineData(2046, "\r\n")] + [InlineData(2048, "\r\n")] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize + 1, "\r\n")] + public void WriteLineReadOnlySpanChar_WritesToStream(int byteLength, string newLine) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - // Assert - Assert.Equal(byteLength + newLine.Length, stream.Length); + writer.NewLine = newLine; + // Act + using (writer) + { + var array = new string('a', byteLength).ToCharArray(); + var span = new ReadOnlySpan(array); + writer.WriteLine(span); } - [Theory] - [InlineData(1023)] - [InlineData(1024)] - [InlineData(1050)] - [InlineData(2048)] - public async Task WriteCharAsync_WritesToStream(int byteLength) - { - // Arrange - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + // Assert + Assert.Equal(byteLength + newLine.Length, stream.Length); + } - // Act - using (writer) + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task WriteCharAsync_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + + // Act + using (writer) + { + for (var i = 0; i < byteLength; i++) { - for (var i = 0; i < byteLength; i++) - { - await writer.WriteAsync('a'); - } + await writer.WriteAsync('a'); } - - // Assert - Assert.Equal(byteLength, stream.Length); } - [Theory] - [InlineData(1023)] - [InlineData(1024)] - [InlineData(1050)] - [InlineData(2048)] - public async Task WriteCharArrayAsync_WritesToStream(int byteLength) - { - // Arrange - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + // Assert + Assert.Equal(byteLength, stream.Length); + } - // Act - using (writer) - { - await writer.WriteAsync((new string('a', byteLength)).ToCharArray()); - } + [Theory] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + public async Task WriteCharArrayAsync_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - // Assert - Assert.Equal(byteLength, stream.Length); + // Act + using (writer) + { + await writer.WriteAsync((new string('a', byteLength)).ToCharArray()); } - [Theory] - [InlineData(0)] - [InlineData(1023)] - [InlineData(1024)] - [InlineData(1050)] - [InlineData(2048)] - [InlineData(HttpResponseStreamWriter.DefaultBufferSize + 1)] - public async Task WriteReadOnlyMemoryAsync_WritesToStream(int byteLength) - { - // Arrange - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + // Assert + Assert.Equal(byteLength, stream.Length); + } - // Act - using (writer) - { - var array = new string('a', byteLength).ToCharArray(); - var memory = new ReadOnlyMemory(array); - await writer.WriteAsync(memory); - } + [Theory] + [InlineData(0)] + [InlineData(1023)] + [InlineData(1024)] + [InlineData(1050)] + [InlineData(2048)] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize + 1)] + public async Task WriteReadOnlyMemoryAsync_WritesToStream(int byteLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - // Assert - Assert.Equal(byteLength, stream.Length); + // Act + using (writer) + { + var array = new string('a', byteLength).ToCharArray(); + var memory = new ReadOnlyMemory(array); + await writer.WriteAsync(memory); } - [Fact] - public async Task WriteReadOnlyMemoryAsync_TokenCanceled_ReturnsCanceledTask() - { - // Arrange - var stream = new TestMemoryStream(); - using var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - var memory = new ReadOnlyMemory(new char[] { 'a' }); - var cancellationToken = new CancellationToken(true); + // Assert + Assert.Equal(byteLength, stream.Length); + } - // Act - await Assert.ThrowsAsync(async () => await writer.WriteAsync(memory, cancellationToken)); + [Fact] + public async Task WriteReadOnlyMemoryAsync_TokenCanceled_ReturnsCanceledTask() + { + // Arrange + var stream = new TestMemoryStream(); + using var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + var memory = new ReadOnlyMemory(new char[] { 'a' }); + var cancellationToken = new CancellationToken(true); - // Assert - Assert.Equal(0, stream.Length); - } + // Act + await Assert.ThrowsAsync(async () => await writer.WriteAsync(memory, cancellationToken)); - [Theory] - [InlineData(0, 1)] - [InlineData(1022, 1)] - [InlineData(1023, 1)] - [InlineData(1024, 1)] - [InlineData(1050, 1)] - [InlineData(2047, 1)] - [InlineData(2048, 1)] - [InlineData(1021, 2)] - [InlineData(1022, 2)] - [InlineData(1023, 2)] - [InlineData(1024, 2)] - [InlineData(1024, 1023)] - [InlineData(1024, 1024)] - [InlineData(1024, 1050)] - [InlineData(1050, 2)] - [InlineData(2046, 2)] - [InlineData(2048, 2)] - [InlineData(HttpResponseStreamWriter.DefaultBufferSize + 1, 1)] - [InlineData(HttpResponseStreamWriter.DefaultBufferSize + 1, 2)] - [InlineData(HttpResponseStreamWriter.DefaultBufferSize + 1, HttpResponseStreamWriter.DefaultBufferSize)] - public async Task WriteLineReadOnlyMemoryAsync_WritesToStream(int byteLength, int newLineLength) - { - // Arrange - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - writer.NewLine = new string('\n', newLineLength); + // Assert + Assert.Equal(0, stream.Length); + } - // Act - using (writer) - { - var array = new string('a', byteLength).ToCharArray(); - var memory = new ReadOnlyMemory(array); - await writer.WriteLineAsync(memory); - } + [Theory] + [InlineData(0, 1)] + [InlineData(1022, 1)] + [InlineData(1023, 1)] + [InlineData(1024, 1)] + [InlineData(1050, 1)] + [InlineData(2047, 1)] + [InlineData(2048, 1)] + [InlineData(1021, 2)] + [InlineData(1022, 2)] + [InlineData(1023, 2)] + [InlineData(1024, 2)] + [InlineData(1024, 1023)] + [InlineData(1024, 1024)] + [InlineData(1024, 1050)] + [InlineData(1050, 2)] + [InlineData(2046, 2)] + [InlineData(2048, 2)] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize + 1, 1)] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize + 1, 2)] + [InlineData(HttpResponseStreamWriter.DefaultBufferSize + 1, HttpResponseStreamWriter.DefaultBufferSize)] + public async Task WriteLineReadOnlyMemoryAsync_WritesToStream(int byteLength, int newLineLength) + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + writer.NewLine = new string('\n', newLineLength); - // Assert - Assert.Equal(byteLength + newLineLength, stream.Length); + // Act + using (writer) + { + var array = new string('a', byteLength).ToCharArray(); + var memory = new ReadOnlyMemory(array); + await writer.WriteLineAsync(memory); } - [Fact] - public async Task WriteLineReadOnlyMemoryAsync_TokenCanceled_ReturnsCanceledTask() - { - // Arrange - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); - var memory = new ReadOnlyMemory(new char[] { 'a' }); - var cancellationToken = new CancellationToken(true); + // Assert + Assert.Equal(byteLength + newLineLength, stream.Length); + } - // Act - using (writer) - { - await Assert.ThrowsAsync(async () => await writer.WriteLineAsync(memory, cancellationToken)); - } + [Fact] + public async Task WriteLineReadOnlyMemoryAsync_TokenCanceled_ReturnsCanceledTask() + { + // Arrange + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.UTF8); + var memory = new ReadOnlyMemory(new char[] { 'a' }); + var cancellationToken = new CancellationToken(true); - // Assert - Assert.Equal(0, stream.Length); + // Act + using (writer) + { + await Assert.ThrowsAsync(async () => await writer.WriteLineAsync(memory, cancellationToken)); } - [Theory] - [InlineData("你好世界", "utf-16")] - [InlineData("హలో ప్రపంచ", "iso-8859-1")] - [InlineData("வணக்கம் உலக", "utf-32")] - public async Task WritesData_InExpectedEncoding(string data, string encodingName) - { - // Arrange - var encoding = Encoding.GetEncoding(encodingName); - var expectedBytes = encoding.GetBytes(data); - var stream = new MemoryStream(); - var writer = new HttpResponseStreamWriter(stream, encoding); + // Assert + Assert.Equal(0, stream.Length); + } - // Act - using (writer) - { - await writer.WriteAsync(data); - } + [Theory] + [InlineData("你好世界", "utf-16")] + [InlineData("హలో ప్రపంచ", "iso-8859-1")] + [InlineData("வணக்கம் உலக", "utf-32")] + public async Task WritesData_InExpectedEncoding(string data, string encodingName) + { + // Arrange + var encoding = Encoding.GetEncoding(encodingName); + var expectedBytes = encoding.GetBytes(data); + var stream = new MemoryStream(); + var writer = new HttpResponseStreamWriter(stream, encoding); - // Assert - Assert.Equal(expectedBytes, stream.ToArray()); + // Act + using (writer) + { + await writer.WriteAsync(data); } - [Theory] - [InlineData('ん', 1023, "utf-8")] - [InlineData('ん', 1024, "utf-8")] - [InlineData('ん', 1050, "utf-8")] - [InlineData('你', 1023, "utf-16")] - [InlineData('你', 1024, "utf-16")] - [InlineData('你', 1050, "utf-16")] - [InlineData('హ', 1023, "iso-8859-1")] - [InlineData('హ', 1024, "iso-8859-1")] - [InlineData('హ', 1050, "iso-8859-1")] - [InlineData('வ', 1023, "utf-32")] - [InlineData('வ', 1024, "utf-32")] - [InlineData('வ', 1050, "utf-32")] - public async Task WritesData_OfDifferentLength_InExpectedEncoding( - char character, - int charCount, - string encodingName) - { - // Arrange - var encoding = Encoding.GetEncoding(encodingName); - string data = new string(character, charCount); - var expectedBytes = encoding.GetBytes(data); - var stream = new MemoryStream(); - var writer = new HttpResponseStreamWriter(stream, encoding); + // Assert + Assert.Equal(expectedBytes, stream.ToArray()); + } - // Act - using (writer) - { - await writer.WriteAsync(data); - } + [Theory] + [InlineData('ん', 1023, "utf-8")] + [InlineData('ん', 1024, "utf-8")] + [InlineData('ん', 1050, "utf-8")] + [InlineData('你', 1023, "utf-16")] + [InlineData('你', 1024, "utf-16")] + [InlineData('你', 1050, "utf-16")] + [InlineData('హ', 1023, "iso-8859-1")] + [InlineData('హ', 1024, "iso-8859-1")] + [InlineData('హ', 1050, "iso-8859-1")] + [InlineData('வ', 1023, "utf-32")] + [InlineData('வ', 1024, "utf-32")] + [InlineData('வ', 1050, "utf-32")] + public async Task WritesData_OfDifferentLength_InExpectedEncoding( + char character, + int charCount, + string encodingName) + { + // Arrange + var encoding = Encoding.GetEncoding(encodingName); + string data = new string(character, charCount); + var expectedBytes = encoding.GetBytes(data); + var stream = new MemoryStream(); + var writer = new HttpResponseStreamWriter(stream, encoding); - // Assert - Assert.Equal(expectedBytes, stream.ToArray()); + // Act + using (writer) + { + await writer.WriteAsync(data); } - // None of the code in HttpResponseStreamWriter differs significantly when using pooled buffers. - // - // This test effectively verifies that things are correctly constructed and disposed. Pooled buffers - // throw on the finalizer thread if not disposed, so that's why it's complicated. - [Fact] - public void HttpResponseStreamWriter_UsingPooledBuffers() - { - // Arrange - var encoding = Encoding.UTF8; - var stream = new MemoryStream(); - - var expectedBytes = encoding.GetBytes("Hello, World!"); - - using (var writer = new HttpResponseStreamWriter( - stream, - encoding, - 1024, - ArrayPool.Shared, - ArrayPool.Shared)) - { - // Act - writer.Write("Hello, World!"); - } + // Assert + Assert.Equal(expectedBytes, stream.ToArray()); + } - // Assert - Assert.Equal(expectedBytes, stream.ToArray()); - } + // None of the code in HttpResponseStreamWriter differs significantly when using pooled buffers. + // + // This test effectively verifies that things are correctly constructed and disposed. Pooled buffers + // throw on the finalizer thread if not disposed, so that's why it's complicated. + [Fact] + public void HttpResponseStreamWriter_UsingPooledBuffers() + { + // Arrange + var encoding = Encoding.UTF8; + var stream = new MemoryStream(); - [Theory] - [InlineData(DefaultCharacterChunkSize)] - [InlineData(DefaultCharacterChunkSize * 2)] - [InlineData(DefaultCharacterChunkSize * 3)] - public async Task HttpResponseStreamWriter_WritesDataCorrectly_ForCharactersHavingSurrogatePairs(int characterSize) - { - // Arrange - // Here "𐐀" (called Deseret Long I) actually represents 2 characters. Try to make this character split across - // the boundary - var content = new string('a', characterSize - 1) + "𐐀"; - var stream = new TestMemoryStream(); - var writer = new HttpResponseStreamWriter(stream, Encoding.Unicode); + var expectedBytes = encoding.GetBytes("Hello, World!"); + using (var writer = new HttpResponseStreamWriter( + stream, + encoding, + 1024, + ArrayPool.Shared, + ArrayPool.Shared)) + { // Act - await writer.WriteAsync(content); - await writer.FlushAsync(); - - // Assert - stream.Seek(0, SeekOrigin.Begin); - var streamReader = new StreamReader(stream, Encoding.Unicode); - var actualContent = await streamReader.ReadToEndAsync(); - Assert.Equal(content, actualContent); + writer.Write("Hello, World!"); } - [Theory] - [MemberData(nameof(HttpResponseStreamWriterData))] - public static void NullInputsInConstructor_ExpectArgumentNullException(Stream stream, Encoding encoding, ArrayPool bytePool, ArrayPool charPool) - { - Assert.Throws(() => - { - var httpRequestStreamReader = new HttpResponseStreamWriter(stream, encoding, 1, bytePool, charPool); - }); - } + // Assert + Assert.Equal(expectedBytes, stream.ToArray()); + } + + [Theory] + [InlineData(DefaultCharacterChunkSize)] + [InlineData(DefaultCharacterChunkSize * 2)] + [InlineData(DefaultCharacterChunkSize * 3)] + public async Task HttpResponseStreamWriter_WritesDataCorrectly_ForCharactersHavingSurrogatePairs(int characterSize) + { + // Arrange + // Here "𐐀" (called Deseret Long I) actually represents 2 characters. Try to make this character split across + // the boundary + var content = new string('a', characterSize - 1) + "𐐀"; + var stream = new TestMemoryStream(); + var writer = new HttpResponseStreamWriter(stream, Encoding.Unicode); + + // Act + await writer.WriteAsync(content); + await writer.FlushAsync(); + + // Assert + stream.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(stream, Encoding.Unicode); + var actualContent = await streamReader.ReadToEndAsync(); + Assert.Equal(content, actualContent); + } - [Theory] - [InlineData(0)] - [InlineData(-1)] - public static void NegativeOrZeroBufferSize_ExpectArgumentOutOfRangeException(int size) + [Theory] + [MemberData(nameof(HttpResponseStreamWriterData))] + public static void NullInputsInConstructor_ExpectArgumentNullException(Stream stream, Encoding encoding, ArrayPool bytePool, ArrayPool charPool) + { + Assert.Throws(() => { - Assert.Throws(() => - { - var httpRequestStreamReader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8, size, ArrayPool.Shared, ArrayPool.Shared); - }); - } + var httpRequestStreamReader = new HttpResponseStreamWriter(stream, encoding, 1, bytePool, charPool); + }); + } - [Fact] - public static void StreamCannotRead_ExpectArgumentException() + [Theory] + [InlineData(0)] + [InlineData(-1)] + public static void NegativeOrZeroBufferSize_ExpectArgumentOutOfRangeException(int size) + { + Assert.Throws(() => { - var mockStream = new Mock(); - mockStream.Setup(m => m.CanWrite).Returns(false); - Assert.Throws(() => - { - var httpRequestStreamReader = new HttpRequestStreamReader(mockStream.Object, Encoding.UTF8, 1, ArrayPool.Shared, ArrayPool.Shared); - }); - } + var httpRequestStreamReader = new HttpRequestStreamReader(new MemoryStream(), Encoding.UTF8, size, ArrayPool.Shared, ArrayPool.Shared); + }); + } - [Theory] - [MemberData(nameof(HttpResponseDisposeData))] - public static void StreamDisposed_ExpectedObjectDisposedException(Action action) + [Fact] + public static void StreamCannotRead_ExpectArgumentException() + { + var mockStream = new Mock(); + mockStream.Setup(m => m.CanWrite).Returns(false); + Assert.Throws(() => { - var httpResponseStreamWriter = new HttpResponseStreamWriter(new MemoryStream(), Encoding.UTF8, 10, ArrayPool.Shared, ArrayPool.Shared); - httpResponseStreamWriter.Dispose(); + var httpRequestStreamReader = new HttpRequestStreamReader(mockStream.Object, Encoding.UTF8, 1, ArrayPool.Shared, ArrayPool.Shared); + }); + } - Assert.Throws(() => - { - action(httpResponseStreamWriter); - }); - } + [Theory] + [MemberData(nameof(HttpResponseDisposeData))] + public static void StreamDisposed_ExpectedObjectDisposedException(Action action) + { + var httpResponseStreamWriter = new HttpResponseStreamWriter(new MemoryStream(), Encoding.UTF8, 10, ArrayPool.Shared, ArrayPool.Shared); + httpResponseStreamWriter.Dispose(); - [Theory] - [MemberData(nameof(HttpResponseDisposeDataAsync))] - public static async Task StreamDisposed_ExpectedObjectDisposedExceptionAsync(Func function) + Assert.Throws(() => { - var httpResponseStreamWriter = new HttpResponseStreamWriter(new MemoryStream(), Encoding.UTF8, 10, ArrayPool.Shared, ArrayPool.Shared); - httpResponseStreamWriter.Dispose(); - - await Assert.ThrowsAsync(() => - { - return function(httpResponseStreamWriter); - }); - } + action(httpResponseStreamWriter); + }); + } + [Theory] + [MemberData(nameof(HttpResponseDisposeDataAsync))] + public static async Task StreamDisposed_ExpectedObjectDisposedExceptionAsync(Func function) + { + var httpResponseStreamWriter = new HttpResponseStreamWriter(new MemoryStream(), Encoding.UTF8, 10, ArrayPool.Shared, ArrayPool.Shared); + httpResponseStreamWriter.Dispose(); - private class TestMemoryStream : MemoryStream + await Assert.ThrowsAsync(() => { - public int FlushCallCount { get; private set; } + return function(httpResponseStreamWriter); + }); + } - public int FlushAsyncCallCount { get; private set; } - public int CloseCallCount { get; private set; } + private class TestMemoryStream : MemoryStream + { + public int FlushCallCount { get; private set; } - public int DisposeCallCount { get; private set; } + public int FlushAsyncCallCount { get; private set; } - public int WriteCallCount { get; private set; } + public int CloseCallCount { get; private set; } - public int WriteAsyncCallCount { get; private set; } + public int DisposeCallCount { get; private set; } - public bool ThrowOnWrite { get; set; } + public int WriteCallCount { get; private set; } - public override void Flush() - { - FlushCallCount++; - base.Flush(); - } + public int WriteAsyncCallCount { get; private set; } - public override Task FlushAsync(CancellationToken cancellationToken) - { - FlushAsyncCallCount++; - return base.FlushAsync(cancellationToken); - } + public bool ThrowOnWrite { get; set; } - public override void Write(byte[] buffer, int offset, int count) - { - WriteCallCount++; - if (ThrowOnWrite) - { - throw new IOException("Test IOException"); - } - base.Write(buffer, offset, count); - } + public override void Flush() + { + FlushCallCount++; + base.Flush(); + } - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - => WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + public override Task FlushAsync(CancellationToken cancellationToken) + { + FlushAsyncCallCount++; + return base.FlushAsync(cancellationToken); + } - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + public override void Write(byte[] buffer, int offset, int count) + { + WriteCallCount++; + if (ThrowOnWrite) { - WriteAsyncCallCount++; - if (ThrowOnWrite) - { - throw new IOException("Test IOException"); - } - return base.WriteAsync(buffer, cancellationToken); + throw new IOException("Test IOException"); } + base.Write(buffer, offset, count); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); - protected override void Dispose(bool disposing) + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + WriteAsyncCallCount++; + if (ThrowOnWrite) { - DisposeCallCount++; - base.Dispose(disposing); + throw new IOException("Test IOException"); } + return base.WriteAsync(buffer, cancellationToken); } - public static IEnumerable HttpResponseStreamWriterData() + protected override void Dispose(bool disposing) { - yield return new object?[] { null, Encoding.UTF8, ArrayPool.Shared, ArrayPool.Shared }; - yield return new object?[] { new MemoryStream(), null, ArrayPool.Shared, ArrayPool.Shared }; - yield return new object?[] { new MemoryStream(), Encoding.UTF8, null, ArrayPool.Shared }; - yield return new object?[] { new MemoryStream(), Encoding.UTF8, ArrayPool.Shared, null }; + DisposeCallCount++; + base.Dispose(disposing); } + } - public static IEnumerable HttpResponseDisposeData() - { - yield return new object[] { new Action((httpResponseStreamWriter) => + public static IEnumerable HttpResponseStreamWriterData() + { + yield return new object?[] { null, Encoding.UTF8, ArrayPool.Shared, ArrayPool.Shared }; + yield return new object?[] { new MemoryStream(), null, ArrayPool.Shared, ArrayPool.Shared }; + yield return new object?[] { new MemoryStream(), Encoding.UTF8, null, ArrayPool.Shared }; + yield return new object?[] { new MemoryStream(), Encoding.UTF8, ArrayPool.Shared, null }; + } + + public static IEnumerable HttpResponseDisposeData() + { + yield return new object[] { new Action((httpResponseStreamWriter) => { httpResponseStreamWriter.Write('a'); })}; - yield return new object[] { new Action((httpResponseStreamWriter) => + yield return new object[] { new Action((httpResponseStreamWriter) => { httpResponseStreamWriter.Write(new char[] { 'a', 'b' }, 0, 1); })}; - yield return new object[] { new Action((httpResponseStreamWriter) => + yield return new object[] { new Action((httpResponseStreamWriter) => { httpResponseStreamWriter.Write("hello"); })}; - yield return new object[] { new Action((httpResponseStreamWriter) => + yield return new object[] { new Action((httpResponseStreamWriter) => { httpResponseStreamWriter.Write(new ReadOnlySpan(new char[] { 'a', 'b' })); })}; - yield return new object[] { new Action((httpResponseStreamWriter) => + yield return new object[] { new Action((httpResponseStreamWriter) => { httpResponseStreamWriter.Flush(); })}; - } + } - public static IEnumerable HttpResponseDisposeDataAsync() - { - yield return new object[] { new Func(async (httpResponseStreamWriter) => + public static IEnumerable HttpResponseDisposeDataAsync() + { + yield return new object[] { new Func(async (httpResponseStreamWriter) => { await httpResponseStreamWriter.WriteAsync('a'); })}; - yield return new object[] { new Func(async (httpResponseStreamWriter) => + yield return new object[] { new Func(async (httpResponseStreamWriter) => { await httpResponseStreamWriter.WriteAsync(new char[] { 'a', 'b' }, 0, 1); })}; - yield return new object[] { new Func(async (httpResponseStreamWriter) => + yield return new object[] { new Func(async (httpResponseStreamWriter) => { await httpResponseStreamWriter.WriteAsync("hello"); })}; - yield return new object[] { new Func(async (httpResponseStreamWriter) => + yield return new object[] { new Func(async (httpResponseStreamWriter) => { await httpResponseStreamWriter.WriteAsync(new ReadOnlyMemory(new char[] { 'a', 'b' })); })}; - yield return new object[] { new Func(async (httpResponseStreamWriter) => + yield return new object[] { new Func(async (httpResponseStreamWriter) => { await httpResponseStreamWriter.WriteLineAsync(new ReadOnlyMemory(new char[] { 'a', 'b' })); })}; - yield return new object[] { new Func(async (httpResponseStreamWriter) => + yield return new object[] { new Func(async (httpResponseStreamWriter) => { await httpResponseStreamWriter.FlushAsync(); })}; - } } } diff --git a/src/Http/WebUtilities/test/MultipartReaderTests.cs b/src/Http/WebUtilities/test/MultipartReaderTests.cs index 36ffa5653a..206c02831a 100644 --- a/src/Http/WebUtilities/test/MultipartReaderTests.cs +++ b/src/Http/WebUtilities/test/MultipartReaderTests.cs @@ -9,39 +9,39 @@ using System.Text; using System.Threading.Tasks; using Xunit; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +public class MultipartReaderTests { - public class MultipartReaderTests - { - private const string Boundary = "9051914041544843365972754266"; - // Note that CRLF (\r\n) is required. You can't use multi-line C# strings here because the line breaks on Linux are just LF. - private const string OnePartBody = + private const string Boundary = "9051914041544843365972754266"; + // Note that CRLF (\r\n) is required. You can't use multi-line C# strings here because the line breaks on Linux are just LF. + private const string OnePartBody = "--9051914041544843365972754266\r\n" + "Content-Disposition: form-data; name=\"text\"\r\n" + "\r\n" + "text default\r\n" + "--9051914041544843365972754266--\r\n"; - private const string OnePartBodyTwoHeaders = + private const string OnePartBodyTwoHeaders = "--9051914041544843365972754266\r\n" + "Content-Disposition: form-data; name=\"text\"\r\n" + "Custom-header: custom-value\r\n" + "\r\n" + "text default\r\n" + "--9051914041544843365972754266--\r\n"; - private const string OnePartBodyWithTrailingWhitespace = + private const string OnePartBodyWithTrailingWhitespace = "--9051914041544843365972754266 \r\n" + "Content-Disposition: form-data; name=\"text\"\r\n" + "\r\n" + "text default\r\n" + "--9051914041544843365972754266--\r\n"; - // It's non-compliant but common to leave off the last CRLF. - private const string OnePartBodyWithoutFinalCRLF = + // It's non-compliant but common to leave off the last CRLF. + private const string OnePartBodyWithoutFinalCRLF = "--9051914041544843365972754266\r\n" + "Content-Disposition: form-data; name=\"text\"\r\n" + "\r\n" + "text default\r\n" + "--9051914041544843365972754266--"; - private const string TwoPartBody = + private const string TwoPartBody = "--9051914041544843365972754266\r\n" + "Content-Disposition: form-data; name=\"text\"\r\n" + "\r\n" + @@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.WebUtilities "Content of a.txt.\r\n" + "\r\n" + "--9051914041544843365972754266--\r\n"; - private const string TwoPartBodyWithUnicodeFileName = + private const string TwoPartBodyWithUnicodeFileName = "--9051914041544843365972754266\r\n" + "Content-Disposition: form-data; name=\"text\"\r\n" + "\r\n" + @@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.WebUtilities "Content of a.txt.\r\n" + "\r\n" + "--9051914041544843365972754266--\r\n"; - private const string ThreePartBody = + private const string ThreePartBody = "--9051914041544843365972754266\r\n" + "Content-Disposition: form-data; name=\"text\"\r\n" + "\r\n" + @@ -84,7 +84,7 @@ namespace Microsoft.AspNetCore.WebUtilities "\r\n" + "--9051914041544843365972754266--\r\n"; - private const string TwoPartBodyIncompleteBuffer = + private const string TwoPartBodyIncompleteBuffer = "--9051914041544843365972754266\r\n" + "Content-Disposition: form-data; name=\"text\"\r\n" + "\r\n" + @@ -97,289 +97,288 @@ namespace Microsoft.AspNetCore.WebUtilities "\r\n" + "--9051914041544843365"; - private static MemoryStream MakeStream(string text) - { - return new MemoryStream(Encoding.UTF8.GetBytes(text)); - } + private static MemoryStream MakeStream(string text) + { + return new MemoryStream(Encoding.UTF8.GetBytes(text)); + } - private static string GetString(byte[] buffer, int count) - { - return Encoding.ASCII.GetString(buffer, 0, count); - } + private static string GetString(byte[] buffer, int count) + { + return Encoding.ASCII.GetString(buffer, 0, count); + } - [Fact] - public async Task MultipartReader_ReadSinglePartBody_Success() - { - var stream = MakeStream(OnePartBody); - var reader = new MultipartReader(Boundary, stream); - - var section = await reader.ReadNextSectionAsync(); - Assert.NotNull(section); - Assert.Single(section.Headers); - Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); - var buffer = new MemoryStream(); - await section.Body.CopyToAsync(buffer); - Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); - - Assert.Null(await reader.ReadNextSectionAsync()); - } - - [Fact] - public async Task MultipartReader_HeaderCountExceeded_Throws() - { - var stream = MakeStream(OnePartBodyTwoHeaders); - var reader = new MultipartReader(Boundary, stream) - { - HeadersCountLimit = 1, - }; - - var exception = await Assert.ThrowsAsync(() => reader.ReadNextSectionAsync()); - Assert.Equal("Multipart headers count limit 1 exceeded.", exception.Message); - } - - [Fact] - public async Task MultipartReader_HeadersLengthExceeded_Throws() - { - var stream = MakeStream(OnePartBodyTwoHeaders); - var reader = new MultipartReader(Boundary, stream) - { - HeadersLengthLimit = 60, - }; - - var exception = await Assert.ThrowsAsync(() => reader.ReadNextSectionAsync()); - Assert.Equal("Line length limit 17 exceeded.", exception.Message); - } - - [Fact] - public async Task MultipartReader_ReadSinglePartBodyWithTrailingWhitespace_Success() - { - var stream = MakeStream(OnePartBodyWithTrailingWhitespace); - var reader = new MultipartReader(Boundary, stream); - - var section = await reader.ReadNextSectionAsync(); - Assert.NotNull(section); - Assert.Single(section.Headers); - Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); - var buffer = new MemoryStream(); - await section.Body.CopyToAsync(buffer); - Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); - - Assert.Null(await reader.ReadNextSectionAsync()); - } - - [Fact] - public async Task MultipartReader_ReadSinglePartBodyWithoutLastCRLF_Success() - { - var stream = MakeStream(OnePartBodyWithoutFinalCRLF); - var reader = new MultipartReader(Boundary, stream); - - var section = await reader.ReadNextSectionAsync(); - Assert.NotNull(section); - Assert.Single(section.Headers); - Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); - var buffer = new MemoryStream(); - await section.Body.CopyToAsync(buffer); - Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); - - Assert.Null(await reader.ReadNextSectionAsync()); - } - - [Fact] - public async Task MultipartReader_ReadTwoPartBody_Success() - { - var stream = MakeStream(TwoPartBody); - var reader = new MultipartReader(Boundary, stream); - - var section = await reader.ReadNextSectionAsync(); - Assert.NotNull(section); - Assert.Single(section.Headers); - Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); - var buffer = new MemoryStream(); - await section.Body.CopyToAsync(buffer); - Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); - - section = await reader.ReadNextSectionAsync(); - Assert.NotNull(section); - Assert.Equal(2, section.Headers.Count); - Assert.Equal("form-data; name=\"file1\"; filename=\"a.txt\"", section.Headers["Content-Disposition"][0]); - Assert.Equal("text/plain", section.Headers["Content-Type"][0]); - buffer = new MemoryStream(); - await section.Body.CopyToAsync(buffer); - Assert.Equal("Content of a.txt.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); - - Assert.Null(await reader.ReadNextSectionAsync()); - } - - [Fact] - public async Task MultipartReader_ReadTwoPartBodyWithUnicodeFileName_Success() + [Fact] + public async Task MultipartReader_ReadSinglePartBody_Success() + { + var stream = MakeStream(OnePartBody); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MultipartReader_HeaderCountExceeded_Throws() + { + var stream = MakeStream(OnePartBodyTwoHeaders); + var reader = new MultipartReader(Boundary, stream) { - var stream = MakeStream(TwoPartBodyWithUnicodeFileName); - var reader = new MultipartReader(Boundary, stream); - - var section = await reader.ReadNextSectionAsync(); - Assert.NotNull(section); - Assert.Single(section.Headers); - Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); - var buffer = new MemoryStream(); - await section.Body.CopyToAsync(buffer); - Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); - - section = await reader.ReadNextSectionAsync(); - Assert.NotNull(section); - Assert.Equal(2, section.Headers.Count); - Assert.Equal("form-data; name=\"file1\"; filename=\"a色.txt\"", section.Headers["Content-Disposition"][0]); - Assert.Equal("text/plain", section.Headers["Content-Type"][0]); - buffer = new MemoryStream(); - await section.Body.CopyToAsync(buffer); - Assert.Equal("Content of a.txt.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); - - Assert.Null(await reader.ReadNextSectionAsync()); - } - - [Fact] - public async Task MultipartReader_ThreePartBody_Success() + HeadersCountLimit = 1, + }; + + var exception = await Assert.ThrowsAsync(() => reader.ReadNextSectionAsync()); + Assert.Equal("Multipart headers count limit 1 exceeded.", exception.Message); + } + + [Fact] + public async Task MultipartReader_HeadersLengthExceeded_Throws() + { + var stream = MakeStream(OnePartBodyTwoHeaders); + var reader = new MultipartReader(Boundary, stream) { - var stream = MakeStream(ThreePartBody); - var reader = new MultipartReader(Boundary, stream); - - var section = await reader.ReadNextSectionAsync(); - Assert.NotNull(section); - Assert.Single(section.Headers); - Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); - var buffer = new MemoryStream(); - await section.Body.CopyToAsync(buffer); - Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); - - section = await reader.ReadNextSectionAsync(); - Assert.NotNull(section); - Assert.Equal(2, section.Headers.Count); - Assert.Equal("form-data; name=\"file1\"; filename=\"a.txt\"", section.Headers["Content-Disposition"][0]); - Assert.Equal("text/plain", section.Headers["Content-Type"][0]); - buffer = new MemoryStream(); - await section.Body.CopyToAsync(buffer); - Assert.Equal("Content of a.txt.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); - - section = await reader.ReadNextSectionAsync(); - Assert.NotNull(section); - Assert.Equal(2, section.Headers.Count); - Assert.Equal("form-data; name=\"file2\"; filename=\"a.html\"", section.Headers["Content-Disposition"][0]); - Assert.Equal("text/html", section.Headers["Content-Type"][0]); - buffer = new MemoryStream(); - await section.Body.CopyToAsync(buffer); - Assert.Equal("Content of a.html.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); - - Assert.Null(await reader.ReadNextSectionAsync()); - } - - [Fact] - public void MultipartReader_BufferSizeMustBeLargerThanBoundary_Throws() + HeadersLengthLimit = 60, + }; + + var exception = await Assert.ThrowsAsync(() => reader.ReadNextSectionAsync()); + Assert.Equal("Line length limit 17 exceeded.", exception.Message); + } + + [Fact] + public async Task MultipartReader_ReadSinglePartBodyWithTrailingWhitespace_Success() + { + var stream = MakeStream(OnePartBodyWithTrailingWhitespace); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MultipartReader_ReadSinglePartBodyWithoutLastCRLF_Success() + { + var stream = MakeStream(OnePartBodyWithoutFinalCRLF); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MultipartReader_ReadTwoPartBody_Success() + { + var stream = MakeStream(TwoPartBody); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file1\"; filename=\"a.txt\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/plain", section.Headers["Content-Type"][0]); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("Content of a.txt.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MultipartReader_ReadTwoPartBodyWithUnicodeFileName_Success() + { + var stream = MakeStream(TwoPartBodyWithUnicodeFileName); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file1\"; filename=\"a色.txt\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/plain", section.Headers["Content-Type"][0]); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("Content of a.txt.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MultipartReader_ThreePartBody_Success() + { + var stream = MakeStream(ThreePartBody); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file1\"; filename=\"a.txt\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/plain", section.Headers["Content-Type"][0]); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("Content of a.txt.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); + + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file2\"; filename=\"a.html\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/html", section.Headers["Content-Type"][0]); + buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("Content of a.html.\r\n", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public void MultipartReader_BufferSizeMustBeLargerThanBoundary_Throws() + { + var stream = MakeStream(ThreePartBody); + Assert.Throws(() => { - var stream = MakeStream(ThreePartBody); - Assert.Throws(() => - { - var reader = new MultipartReader(Boundary, stream, 5); - }); - } - - [Fact] - public async Task MultipartReader_TwoPartBodyIncompleteBuffer_TwoSectionsReadSuccessfullyThirdSectionThrows() + var reader = new MultipartReader(Boundary, stream, 5); + }); + } + + [Fact] + public async Task MultipartReader_TwoPartBodyIncompleteBuffer_TwoSectionsReadSuccessfullyThirdSectionThrows() + { + var stream = MakeStream(TwoPartBodyIncompleteBuffer); + var reader = new MultipartReader(Boundary, stream); + var buffer = new byte[128]; + + //first section can be read successfully + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); + var read = section.Body.Read(buffer, 0, buffer.Length); + Assert.Equal("text default", GetString(buffer, read)); + + //second section can be read successfully (even though the bottom boundary is truncated) + section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Equal(2, section.Headers.Count); + Assert.Equal("form-data; name=\"file1\"; filename=\"a.txt\"", section.Headers["Content-Disposition"][0]); + Assert.Equal("text/plain", section.Headers["Content-Type"][0]); + read = section.Body.Read(buffer, 0, buffer.Length); + Assert.Equal("Content of a.txt.\r\n", GetString(buffer, read)); + + await Assert.ThrowsAsync(async () => { - var stream = MakeStream(TwoPartBodyIncompleteBuffer); - var reader = new MultipartReader(Boundary, stream); - var buffer = new byte[128]; - - //first section can be read successfully - var section = await reader.ReadNextSectionAsync(); - Assert.NotNull(section); - Assert.Single(section.Headers); - Assert.Equal("form-data; name=\"text\"", section.Headers["Content-Disposition"][0]); - var read = section.Body.Read(buffer, 0, buffer.Length); - Assert.Equal("text default", GetString(buffer, read)); - - //second section can be read successfully (even though the bottom boundary is truncated) - section = await reader.ReadNextSectionAsync(); - Assert.NotNull(section); - Assert.Equal(2, section.Headers.Count); - Assert.Equal("form-data; name=\"file1\"; filename=\"a.txt\"", section.Headers["Content-Disposition"][0]); - Assert.Equal("text/plain", section.Headers["Content-Type"][0]); - read = section.Body.Read(buffer, 0, buffer.Length); - Assert.Equal("Content of a.txt.\r\n", GetString(buffer, read)); - - await Assert.ThrowsAsync(async () => - { // we'll be unable to ensure enough bytes are buffered to even contain a final boundary section = await reader.ReadNextSectionAsync(); - }); - } + }); + } - [Fact] - public async Task MultipartReader_ReadInvalidUtf8Header_ReplacementCharacters() - { - var body1 = + [Fact] + public async Task MultipartReader_ReadInvalidUtf8Header_ReplacementCharacters() + { + var body1 = "--9051914041544843365972754266\r\n" + "Content-Disposition: form-data; name=\"text\" filename=\"a"; - var body2 = + var body2 = ".txt\"\r\n" + "\r\n" + "text default\r\n" + "--9051914041544843365972754266--\r\n"; - var stream = new MemoryStream(); - var bytes = Encoding.UTF8.GetBytes(body1); - stream.Write(bytes, 0, bytes.Length); - - // Write an invalid utf-8 segment in the middle - stream.Write(new byte[] { 0xC1, 0x21 }, 0, 2); - - bytes = Encoding.UTF8.GetBytes(body2); - stream.Write(bytes, 0, bytes.Length); - stream.Seek(0, SeekOrigin.Begin); - var reader = new MultipartReader(Boundary, stream); - - var section = await reader.ReadNextSectionAsync(); - Assert.NotNull(section); - Assert.Single(section.Headers); - Assert.Equal("form-data; name=\"text\" filename=\"a\uFFFD!.txt\"", section.Headers["Content-Disposition"][0]); - var buffer = new MemoryStream(); - await section.Body.CopyToAsync(buffer); - Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); - - Assert.Null(await reader.ReadNextSectionAsync()); - } - - [Fact] - public async Task MultipartReader_ReadInvalidUtf8SurrogateHeader_ReplacementCharacters() - { - var body1 = + var stream = new MemoryStream(); + var bytes = Encoding.UTF8.GetBytes(body1); + stream.Write(bytes, 0, bytes.Length); + + // Write an invalid utf-8 segment in the middle + stream.Write(new byte[] { 0xC1, 0x21 }, 0, 2); + + bytes = Encoding.UTF8.GetBytes(body2); + stream.Write(bytes, 0, bytes.Length); + stream.Seek(0, SeekOrigin.Begin); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\" filename=\"a\uFFFD!.txt\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); + } + + [Fact] + public async Task MultipartReader_ReadInvalidUtf8SurrogateHeader_ReplacementCharacters() + { + var body1 = "--9051914041544843365972754266\r\n" + "Content-Disposition: form-data; name=\"text\" filename=\"a"; - var body2 = + var body2 = ".txt\"\r\n" + "\r\n" + "text default\r\n" + "--9051914041544843365972754266--\r\n"; - var stream = new MemoryStream(); - var bytes = Encoding.UTF8.GetBytes(body1); - stream.Write(bytes, 0, bytes.Length); - - // Write an invalid utf-8 segment in the middle - stream.Write(new byte[] { 0xED, 0xA0, 85 }, 0, 3); - - bytes = Encoding.UTF8.GetBytes(body2); - stream.Write(bytes, 0, bytes.Length); - stream.Seek(0, SeekOrigin.Begin); - var reader = new MultipartReader(Boundary, stream); - - var section = await reader.ReadNextSectionAsync(); - Assert.NotNull(section); - Assert.Single(section.Headers); - Assert.Equal("form-data; name=\"text\" filename=\"a\uFFFD\uFFFDU.txt\"", section.Headers["Content-Disposition"][0]); - var buffer = new MemoryStream(); - await section.Body.CopyToAsync(buffer); - Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); - - Assert.Null(await reader.ReadNextSectionAsync()); - } + var stream = new MemoryStream(); + var bytes = Encoding.UTF8.GetBytes(body1); + stream.Write(bytes, 0, bytes.Length); + + // Write an invalid utf-8 segment in the middle + stream.Write(new byte[] { 0xED, 0xA0, 85 }, 0, 3); + + bytes = Encoding.UTF8.GetBytes(body2); + stream.Write(bytes, 0, bytes.Length); + stream.Seek(0, SeekOrigin.Begin); + var reader = new MultipartReader(Boundary, stream); + + var section = await reader.ReadNextSectionAsync(); + Assert.NotNull(section); + Assert.Single(section.Headers); + Assert.Equal("form-data; name=\"text\" filename=\"a\uFFFD\uFFFDU.txt\"", section.Headers["Content-Disposition"][0]); + var buffer = new MemoryStream(); + await section.Body.CopyToAsync(buffer); + Assert.Equal("text default", Encoding.ASCII.GetString(buffer.ToArray())); + + Assert.Null(await reader.ReadNextSectionAsync()); } } diff --git a/src/Http/WebUtilities/test/NonSeekableReadStream.cs b/src/Http/WebUtilities/test/NonSeekableReadStream.cs index 74467d4ea0..80b18da18c 100644 --- a/src/Http/WebUtilities/test/NonSeekableReadStream.cs +++ b/src/Http/WebUtilities/test/NonSeekableReadStream.cs @@ -6,69 +6,68 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +public class NonSeekableReadStream : Stream { - public class NonSeekableReadStream : Stream + private readonly Stream _inner; + + public NonSeekableReadStream(byte[] data) + : this(new MemoryStream(data)) + { + } + + public NonSeekableReadStream(Stream inner) + { + _inner = inner; + } + + public override bool CanRead => _inner.CanRead; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + count = Math.Max(count, 1); + return _inner.Read(buffer, offset, count); + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { - private readonly Stream _inner; - - public NonSeekableReadStream(byte[] data) - : this(new MemoryStream(data)) - { - } - - public NonSeekableReadStream(Stream inner) - { - _inner = inner; - } - - public override bool CanRead => _inner.CanRead; - - public override bool CanSeek => false; - - public override bool CanWrite => false; - - public override long Length - { - get { throw new NotSupportedException(); } - } - - public override long Position - { - get { throw new NotSupportedException(); } - set { throw new NotSupportedException(); } - } - - public override void Flush() - { - throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - count = Math.Max(count, 1); - return _inner.Read(buffer, offset, count); - } - - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - count = Math.Max(count, 1); - return _inner.ReadAsync(buffer, offset, count, cancellationToken); - } + count = Math.Max(count, 1); + return _inner.ReadAsync(buffer, offset, count, cancellationToken); } } diff --git a/src/Http/WebUtilities/test/PagedByteBufferTest.cs b/src/Http/WebUtilities/test/PagedByteBufferTest.cs index fbbadb4ba1..93993b4b54 100644 --- a/src/Http/WebUtilities/test/PagedByteBufferTest.cs +++ b/src/Http/WebUtilities/test/PagedByteBufferTest.cs @@ -9,240 +9,239 @@ using System.Threading.Tasks; using Moq; using Xunit; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +public class PagedByteBufferTest { - public class PagedByteBufferTest + [Fact] + public void Add_CreatesNewPage() { - [Fact] - public void Add_CreatesNewPage() - { - // Arrange - var input = Encoding.UTF8.GetBytes("Hello world"); - using var buffer = new PagedByteBuffer(ArrayPool.Shared); + // Arrange + var input = Encoding.UTF8.GetBytes("Hello world"); + using var buffer = new PagedByteBuffer(ArrayPool.Shared); - // Act - buffer.Add(input, 0, input.Length); + // Act + buffer.Add(input, 0, input.Length); - // Assert - Assert.Single(buffer.Pages); - Assert.Equal(input.Length, buffer.Length); - Assert.Equal(input, ReadBufferedContent(buffer)); - } + // Assert + Assert.Single(buffer.Pages); + Assert.Equal(input.Length, buffer.Length); + Assert.Equal(input, ReadBufferedContent(buffer)); + } - [Fact] - public void Add_AppendsToExistingPage() - { - // Arrange - var input1 = Encoding.UTF8.GetBytes("Hello"); - var input2 = Encoding.UTF8.GetBytes("world"); - using var buffer = new PagedByteBuffer(ArrayPool.Shared); - buffer.Add(input1, 0, input1.Length); + [Fact] + public void Add_AppendsToExistingPage() + { + // Arrange + var input1 = Encoding.UTF8.GetBytes("Hello"); + var input2 = Encoding.UTF8.GetBytes("world"); + using var buffer = new PagedByteBuffer(ArrayPool.Shared); + buffer.Add(input1, 0, input1.Length); + + // Act + buffer.Add(input2, 0, input2.Length); + + // Assert + Assert.Single(buffer.Pages); + Assert.Equal(10, buffer.Length); + Assert.Equal(Enumerable.Concat(input1, input2).ToArray(), ReadBufferedContent(buffer)); + } - // Act - buffer.Add(input2, 0, input2.Length); + [Fact] + public void Add_WithOffsets() + { + // Arrange + var input = new byte[] { 1, 2, 3, 4, 5 }; + using var buffer = new PagedByteBuffer(ArrayPool.Shared); - // Assert - Assert.Single(buffer.Pages); - Assert.Equal(10, buffer.Length); - Assert.Equal(Enumerable.Concat(input1, input2).ToArray(), ReadBufferedContent(buffer)); - } + // Act + buffer.Add(input, 1, 3); - [Fact] - public void Add_WithOffsets() - { - // Arrange - var input = new byte[] { 1, 2, 3, 4, 5 }; - using var buffer = new PagedByteBuffer(ArrayPool.Shared); + // Assert + Assert.Single(buffer.Pages); + Assert.Equal(3, buffer.Length); + Assert.Equal(new byte[] { 2, 3, 4 }, ReadBufferedContent(buffer)); + } - // Act - buffer.Add(input, 1, 3); + [Fact] + public void Add_FillsUpBuffer() + { + // Arrange + var input1 = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize - 1).ToArray(); + var input2 = new byte[] { 0xca }; + using var buffer = new PagedByteBuffer(ArrayPool.Shared); + buffer.Add(input1, 0, input1.Length); + + // Act + buffer.Add(input2, 0, 1); + + // Assert + Assert.Single(buffer.Pages); + Assert.Equal(PagedByteBuffer.PageSize, buffer.Length); + Assert.Equal(Enumerable.Concat(input1, input2).ToArray(), ReadBufferedContent(buffer)); + } - // Assert - Assert.Single(buffer.Pages); - Assert.Equal(3, buffer.Length); - Assert.Equal(new byte[] { 2, 3, 4 }, ReadBufferedContent(buffer)); - } + [Fact] + public void Add_AppendsToMultiplePages() + { + // Arrange + var input = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize + 10).ToArray(); + using var buffer = new PagedByteBuffer(ArrayPool.Shared); - [Fact] - public void Add_FillsUpBuffer() - { - // Arrange - var input1 = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize - 1).ToArray(); - var input2 = new byte[] { 0xca }; - using var buffer = new PagedByteBuffer(ArrayPool.Shared); - buffer.Add(input1, 0, input1.Length); + // Act + buffer.Add(input, 0, input.Length); - // Act - buffer.Add(input2, 0, 1); + // Assert + Assert.Equal(2, buffer.Pages.Count); + Assert.Equal(PagedByteBuffer.PageSize + 10, buffer.Length); + Assert.Equal(input.ToArray(), ReadBufferedContent(buffer)); + } - // Assert - Assert.Single(buffer.Pages); - Assert.Equal(PagedByteBuffer.PageSize, buffer.Length); - Assert.Equal(Enumerable.Concat(input1, input2).ToArray(), ReadBufferedContent(buffer)); - } + [Fact] + public void MoveTo_CopiesContentToStream() + { + // Arrange + var input = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize * 3 + 10).ToArray(); + using var buffer = new PagedByteBuffer(ArrayPool.Shared); + buffer.Add(input, 0, input.Length); + var stream = new MemoryStream(); - [Fact] - public void Add_AppendsToMultiplePages() - { - // Arrange - var input = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize + 10).ToArray(); - using var buffer = new PagedByteBuffer(ArrayPool.Shared); + // Act + buffer.MoveTo(stream); - // Act - buffer.Add(input, 0, input.Length); + // Assert + Assert.Equal(input, stream.ToArray()); - // Assert - Assert.Equal(2, buffer.Pages.Count); - Assert.Equal(PagedByteBuffer.PageSize + 10, buffer.Length); - Assert.Equal(input.ToArray(), ReadBufferedContent(buffer)); - } + // Verify moving new content works. + var newInput = Enumerable.Repeat((byte)0xcb, PagedByteBuffer.PageSize * 2 + 13).ToArray(); + buffer.Add(newInput, 0, newInput.Length); - [Fact] - public void MoveTo_CopiesContentToStream() - { - // Arrange - var input = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize * 3 + 10).ToArray(); - using var buffer = new PagedByteBuffer(ArrayPool.Shared); - buffer.Add(input, 0, input.Length); - var stream = new MemoryStream(); + stream.SetLength(0); + buffer.MoveTo(stream); - // Act - buffer.MoveTo(stream); + Assert.Equal(newInput, stream.ToArray()); + } - // Assert - Assert.Equal(input, stream.ToArray()); + [Fact] + public async Task MoveToAsync_CopiesContentToStream() + { + // Arrange + var input = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize * 3 + 10).ToArray(); + using var buffer = new PagedByteBuffer(ArrayPool.Shared); + buffer.Add(input, 0, input.Length); + var stream = new MemoryStream(); - // Verify moving new content works. - var newInput = Enumerable.Repeat((byte)0xcb, PagedByteBuffer.PageSize * 2 + 13).ToArray(); - buffer.Add(newInput, 0, newInput.Length); + // Act + await buffer.MoveToAsync(stream, default); - stream.SetLength(0); - buffer.MoveTo(stream); + // Assert + Assert.Equal(input, stream.ToArray()); - Assert.Equal(newInput, stream.ToArray()); - } + // Verify adding and moving new content works. + var newInput = Enumerable.Repeat((byte)0xcb, PagedByteBuffer.PageSize * 2 + 13).ToArray(); + buffer.Add(newInput, 0, newInput.Length); + stream.SetLength(0); + await buffer.MoveToAsync(stream, default); - [Fact] - public async Task MoveToAsync_CopiesContentToStream() - { - // Arrange - var input = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize * 3 + 10).ToArray(); - using var buffer = new PagedByteBuffer(ArrayPool.Shared); - buffer.Add(input, 0, input.Length); - var stream = new MemoryStream(); + Assert.Equal(newInput, stream.ToArray()); + } + + [Fact] + public async Task MoveToAsync_ClearsBuffers() + { + // Arrange + var input = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize * 3 + 10).ToArray(); + using var buffer = new PagedByteBuffer(ArrayPool.Shared); + buffer.Add(input, 0, input.Length); + var stream = new MemoryStream(); + + // Act + await buffer.MoveToAsync(stream, default); + + // Assert + Assert.Equal(input, stream.ToArray()); + + // Verify copying it again works. + Assert.Equal(0, buffer.Length); + Assert.False(buffer.Disposed); + Assert.Empty(buffer.Pages); + } + [Fact] + public void MoveTo_WithClear_ReturnsBuffers() + { + // Arrange + var input = new byte[] { 1, }; + var arrayPool = new Mock>(); + var byteArray = new byte[PagedByteBuffer.PageSize]; + arrayPool.Setup(p => p.Rent(PagedByteBuffer.PageSize)) + .Returns(byteArray); + arrayPool.Setup(p => p.Return(byteArray, false)).Verifiable(); + var memoryStream = new MemoryStream(); + + using (var buffer = new PagedByteBuffer(arrayPool.Object)) + { // Act - await buffer.MoveToAsync(stream, default); + buffer.Add(input, 0, input.Length); + buffer.MoveTo(memoryStream); // Assert - Assert.Equal(input, stream.ToArray()); - - // Verify adding and moving new content works. - var newInput = Enumerable.Repeat((byte)0xcb, PagedByteBuffer.PageSize * 2 + 13).ToArray(); - buffer.Add(newInput, 0, newInput.Length); - stream.SetLength(0); - await buffer.MoveToAsync(stream, default); - - Assert.Equal(newInput, stream.ToArray()); + Assert.Equal(input, memoryStream.ToArray()); } - [Fact] - public async Task MoveToAsync_ClearsBuffers() - { - // Arrange - var input = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize * 3 + 10).ToArray(); - using var buffer = new PagedByteBuffer(ArrayPool.Shared); - buffer.Add(input, 0, input.Length); - var stream = new MemoryStream(); + arrayPool.Verify(p => p.Rent(It.IsAny()), Times.Once()); + arrayPool.Verify(p => p.Return(It.IsAny(), It.IsAny()), Times.Once()); + } + [Fact] + public async Task MoveToAsync_ReturnsBuffers() + { + // Arrange + var input = new byte[] { 1, }; + var arrayPool = new Mock>(); + var byteArray = new byte[PagedByteBuffer.PageSize]; + arrayPool.Setup(p => p.Rent(PagedByteBuffer.PageSize)) + .Returns(byteArray); + var memoryStream = new MemoryStream(); + + using (var buffer = new PagedByteBuffer(arrayPool.Object)) + { // Act - await buffer.MoveToAsync(stream, default); + buffer.Add(input, 0, input.Length); + await buffer.MoveToAsync(memoryStream, default); // Assert - Assert.Equal(input, stream.ToArray()); - - // Verify copying it again works. - Assert.Equal(0, buffer.Length); - Assert.False(buffer.Disposed); - Assert.Empty(buffer.Pages); + Assert.Equal(input, memoryStream.ToArray()); } - [Fact] - public void MoveTo_WithClear_ReturnsBuffers() - { - // Arrange - var input = new byte[] { 1, }; - var arrayPool = new Mock>(); - var byteArray = new byte[PagedByteBuffer.PageSize]; - arrayPool.Setup(p => p.Rent(PagedByteBuffer.PageSize)) - .Returns(byteArray); - arrayPool.Setup(p => p.Return(byteArray, false)).Verifiable(); - var memoryStream = new MemoryStream(); - - using (var buffer = new PagedByteBuffer(arrayPool.Object)) - { - // Act - buffer.Add(input, 0, input.Length); - buffer.MoveTo(memoryStream); - - // Assert - Assert.Equal(input, memoryStream.ToArray()); - } - - arrayPool.Verify(p => p.Rent(It.IsAny()), Times.Once()); - arrayPool.Verify(p => p.Return(It.IsAny(), It.IsAny()), Times.Once()); - } - - [Fact] - public async Task MoveToAsync_ReturnsBuffers() - { - // Arrange - var input = new byte[] { 1, }; - var arrayPool = new Mock>(); - var byteArray = new byte[PagedByteBuffer.PageSize]; - arrayPool.Setup(p => p.Rent(PagedByteBuffer.PageSize)) - .Returns(byteArray); - var memoryStream = new MemoryStream(); - - using (var buffer = new PagedByteBuffer(arrayPool.Object)) - { - // Act - buffer.Add(input, 0, input.Length); - await buffer.MoveToAsync(memoryStream, default); - - // Assert - Assert.Equal(input, memoryStream.ToArray()); - } - - arrayPool.Verify(p => p.Rent(It.IsAny()), Times.Once()); - arrayPool.Verify(p => p.Return(It.IsAny(), It.IsAny()), Times.Once()); - } + arrayPool.Verify(p => p.Rent(It.IsAny()), Times.Once()); + arrayPool.Verify(p => p.Return(It.IsAny(), It.IsAny()), Times.Once()); + } - [Fact] - public void Dispose_ReturnsBuffers_ExactlyOnce() - { - // Arrange - var input = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize * 3 + 10).ToArray(); - var arrayPool = new Mock>(); - arrayPool.Setup(p => p.Rent(PagedByteBuffer.PageSize)) - .Returns(new byte[PagedByteBuffer.PageSize]); + [Fact] + public void Dispose_ReturnsBuffers_ExactlyOnce() + { + // Arrange + var input = Enumerable.Repeat((byte)0xba, PagedByteBuffer.PageSize * 3 + 10).ToArray(); + var arrayPool = new Mock>(); + arrayPool.Setup(p => p.Rent(PagedByteBuffer.PageSize)) + .Returns(new byte[PagedByteBuffer.PageSize]); - var buffer = new PagedByteBuffer(arrayPool.Object); + var buffer = new PagedByteBuffer(arrayPool.Object); - // Act - buffer.Add(input, 0, input.Length); - buffer.Dispose(); - buffer.Dispose(); + // Act + buffer.Add(input, 0, input.Length); + buffer.Dispose(); + buffer.Dispose(); - arrayPool.Verify(p => p.Rent(It.IsAny()), Times.Exactly(4)); - arrayPool.Verify(p => p.Return(It.IsAny(), It.IsAny()), Times.Exactly(4)); - } + arrayPool.Verify(p => p.Rent(It.IsAny()), Times.Exactly(4)); + arrayPool.Verify(p => p.Return(It.IsAny(), It.IsAny()), Times.Exactly(4)); + } - private static byte[] ReadBufferedContent(PagedByteBuffer buffer) - { - using var stream = new MemoryStream(); - buffer.MoveTo(stream); - return stream.ToArray(); - } + private static byte[] ReadBufferedContent(PagedByteBuffer buffer) + { + using var stream = new MemoryStream(); + buffer.MoveTo(stream); + return stream.ToArray(); } } diff --git a/src/Http/WebUtilities/test/QueryHelpersTests.cs b/src/Http/WebUtilities/test/QueryHelpersTests.cs index 7c395d9e6c..2db84da7e9 100644 --- a/src/Http/WebUtilities/test/QueryHelpersTests.cs +++ b/src/Http/WebUtilities/test/QueryHelpersTests.cs @@ -7,159 +7,159 @@ using System.Linq; using Microsoft.Extensions.Primitives; using Xunit; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +public class QueryHelperTests { - public class QueryHelperTests + [Fact] + public void ParseQueryWithUniqueKeysWorks() + { + var collection = QueryHelpers.ParseQuery("?key1=value1&key2=value2"); + Assert.Equal(2, collection.Count); + Assert.Equal("value1", collection["key1"].FirstOrDefault()); + Assert.Equal("value2", collection["key2"].FirstOrDefault()); + } + + [Fact] + public void ParseQueryWithoutQuestionmarkWorks() + { + var collection = QueryHelpers.ParseQuery("key1=value1&key2=value2"); + Assert.Equal(2, collection.Count); + Assert.Equal("value1", collection["key1"].FirstOrDefault()); + Assert.Equal("value2", collection["key2"].FirstOrDefault()); + } + + [Fact] + public void ParseQueryWithDuplicateKeysGroups() + { + var collection = QueryHelpers.ParseQuery("?key1=valueA&key2=valueB&key1=valueC"); + Assert.Equal(2, collection.Count); + Assert.Equal(new[] { "valueA", "valueC" }, collection["key1"]); + Assert.Equal("valueB", collection["key2"].FirstOrDefault()); + } + + [Fact] + public void ParseQueryWithEmptyValuesWorks() + { + var collection = QueryHelpers.ParseQuery("?key1=&key2="); + Assert.Equal(2, collection.Count); + Assert.Equal(string.Empty, collection["key1"].FirstOrDefault()); + Assert.Equal(string.Empty, collection["key2"].FirstOrDefault()); + } + + [Fact] + public void ParseQueryWithEmptyKeyWorks() + { + var collection = QueryHelpers.ParseQuery("?=value1&="); + Assert.Single(collection); + Assert.Equal(new[] { "value1", "" }, collection[""]); + } + + [Fact] + public void ParseQueryWithEncodedKeyWorks() + { + var collection = QueryHelpers.ParseQuery("?fields+%5BtodoItems%5D"); + Assert.Single(collection); + Assert.Equal("", collection["fields [todoItems]"].FirstOrDefault()); + } + + [Fact] + public void ParseQueryWithEncodedValueWorks() + { + var collection = QueryHelpers.ParseQuery("?=fields+%5BtodoItems%5D"); + Assert.Single(collection); + Assert.Equal("fields [todoItems]", collection[""].FirstOrDefault()); + } + + [Fact] + public void ParseQueryWithEncodedKeyEmptyValueWorks() { - [Fact] - public void ParseQueryWithUniqueKeysWorks() - { - var collection = QueryHelpers.ParseQuery("?key1=value1&key2=value2"); - Assert.Equal(2, collection.Count); - Assert.Equal("value1", collection["key1"].FirstOrDefault()); - Assert.Equal("value2", collection["key2"].FirstOrDefault()); - } - - [Fact] - public void ParseQueryWithoutQuestionmarkWorks() - { - var collection = QueryHelpers.ParseQuery("key1=value1&key2=value2"); - Assert.Equal(2, collection.Count); - Assert.Equal("value1", collection["key1"].FirstOrDefault()); - Assert.Equal("value2", collection["key2"].FirstOrDefault()); - } - - [Fact] - public void ParseQueryWithDuplicateKeysGroups() - { - var collection = QueryHelpers.ParseQuery("?key1=valueA&key2=valueB&key1=valueC"); - Assert.Equal(2, collection.Count); - Assert.Equal(new[] { "valueA", "valueC" }, collection["key1"]); - Assert.Equal("valueB", collection["key2"].FirstOrDefault()); - } - - [Fact] - public void ParseQueryWithEmptyValuesWorks() - { - var collection = QueryHelpers.ParseQuery("?key1=&key2="); - Assert.Equal(2, collection.Count); - Assert.Equal(string.Empty, collection["key1"].FirstOrDefault()); - Assert.Equal(string.Empty, collection["key2"].FirstOrDefault()); - } - - [Fact] - public void ParseQueryWithEmptyKeyWorks() - { - var collection = QueryHelpers.ParseQuery("?=value1&="); - Assert.Single(collection); - Assert.Equal(new[] { "value1", "" }, collection[""]); - } - - [Fact] - public void ParseQueryWithEncodedKeyWorks() - { - var collection = QueryHelpers.ParseQuery("?fields+%5BtodoItems%5D"); - Assert.Single(collection); - Assert.Equal("", collection["fields [todoItems]"].FirstOrDefault()); - } - - [Fact] - public void ParseQueryWithEncodedValueWorks() - { - var collection = QueryHelpers.ParseQuery("?=fields+%5BtodoItems%5D"); - Assert.Single(collection); - Assert.Equal("fields [todoItems]", collection[""].FirstOrDefault()); - } - - [Fact] - public void ParseQueryWithEncodedKeyEmptyValueWorks() - { - var collection = QueryHelpers.ParseQuery("?fields+%5BtodoItems%5D="); - Assert.Single(collection); - Assert.Equal("", collection["fields [todoItems]"].FirstOrDefault()); - } - - [Fact] - public void ParseQueryWithEncodedKeyEncodedValueWorks() - { - var collection = QueryHelpers.ParseQuery("?fields+%5BtodoItems%5D=%5B+1+%5D"); - Assert.Single(collection); - Assert.Equal("[ 1 ]", collection["fields [todoItems]"].FirstOrDefault()); - } - - [Fact] - public void ParseQueryWithEncodedKeyEncodedValuesWorks() - { - var collection = QueryHelpers.ParseQuery("?fields+%5BtodoItems%5D=%5B+1+%5D&fields+%5BtodoItems%5D=%5B+2+%5D"); - Assert.Single(collection); - Assert.Equal(new[] { "[ 1 ]", "[ 2 ]" }, collection["fields [todoItems]"]); - } - - [Theory] - [InlineData("?")] - [InlineData("")] - [InlineData(null)] - public void ParseEmptyOrNullQueryWorks(string? queryString) - { - var collection = QueryHelpers.ParseQuery(queryString); - Assert.Empty(collection); - } - - [Fact] - public void AddQueryStringWithNullValueThrows() - { - Assert.Throws("value" ,() => QueryHelpers.AddQueryString("http://contoso.com/", "hello", null!)); - } - - [Theory] - [InlineData("http://contoso.com/", "http://contoso.com/?hello=world")] - [InlineData("http://contoso.com/someaction", "http://contoso.com/someaction?hello=world")] - [InlineData("http://contoso.com/someaction?q=test", "http://contoso.com/someaction?q=test&hello=world")] - [InlineData( - "http://contoso.com/someaction?q=test#anchor", - "http://contoso.com/someaction?q=test&hello=world#anchor")] - [InlineData("http://contoso.com/someaction#anchor", "http://contoso.com/someaction?hello=world#anchor")] - [InlineData("http://contoso.com/#anchor", "http://contoso.com/?hello=world#anchor")] - [InlineData( - "http://contoso.com/someaction?q=test#anchor?value", - "http://contoso.com/someaction?q=test&hello=world#anchor?value")] - [InlineData( - "http://contoso.com/someaction#anchor?stuff", - "http://contoso.com/someaction?hello=world#anchor?stuff")] - [InlineData( - "http://contoso.com/someaction?name?something", - "http://contoso.com/someaction?name?something&hello=world")] - [InlineData( - "http://contoso.com/someaction#name#something", - "http://contoso.com/someaction?hello=world#name#something")] - public void AddQueryStringWithKeyAndValue(string uri, string expectedUri) - { - var result = QueryHelpers.AddQueryString(uri, "hello", "world"); - Assert.Equal(expectedUri, result); - } - - [Theory] - [InlineData("http://contoso.com/", "http://contoso.com/?hello=world&some=text&another=")] - [InlineData("http://contoso.com/someaction", "http://contoso.com/someaction?hello=world&some=text&another=")] - [InlineData("http://contoso.com/someaction?q=1", "http://contoso.com/someaction?q=1&hello=world&some=text&another=")] - [InlineData("http://contoso.com/some#action", "http://contoso.com/some?hello=world&some=text&another=#action")] - [InlineData("http://contoso.com/some?q=1#action", "http://contoso.com/some?q=1&hello=world&some=text&another=#action")] - [InlineData("http://contoso.com/#action", "http://contoso.com/?hello=world&some=text&another=#action")] - [InlineData( - "http://contoso.com/someaction?q=test#anchor?value", - "http://contoso.com/someaction?q=test&hello=world&some=text&another=#anchor?value")] - [InlineData( - "http://contoso.com/someaction#anchor?stuff", - "http://contoso.com/someaction?hello=world&some=text&another=#anchor?stuff")] - [InlineData( - "http://contoso.com/someaction?name?something", - "http://contoso.com/someaction?name?something&hello=world&some=text&another=")] - [InlineData( - "http://contoso.com/someaction#name#something", - "http://contoso.com/someaction?hello=world&some=text&another=#name#something")] - public void AddQueryStringWithDictionary(string uri, string expectedUri) - { - var queryStrings = new Dictionary() + var collection = QueryHelpers.ParseQuery("?fields+%5BtodoItems%5D="); + Assert.Single(collection); + Assert.Equal("", collection["fields [todoItems]"].FirstOrDefault()); + } + + [Fact] + public void ParseQueryWithEncodedKeyEncodedValueWorks() + { + var collection = QueryHelpers.ParseQuery("?fields+%5BtodoItems%5D=%5B+1+%5D"); + Assert.Single(collection); + Assert.Equal("[ 1 ]", collection["fields [todoItems]"].FirstOrDefault()); + } + + [Fact] + public void ParseQueryWithEncodedKeyEncodedValuesWorks() + { + var collection = QueryHelpers.ParseQuery("?fields+%5BtodoItems%5D=%5B+1+%5D&fields+%5BtodoItems%5D=%5B+2+%5D"); + Assert.Single(collection); + Assert.Equal(new[] { "[ 1 ]", "[ 2 ]" }, collection["fields [todoItems]"]); + } + + [Theory] + [InlineData("?")] + [InlineData("")] + [InlineData(null)] + public void ParseEmptyOrNullQueryWorks(string? queryString) + { + var collection = QueryHelpers.ParseQuery(queryString); + Assert.Empty(collection); + } + + [Fact] + public void AddQueryStringWithNullValueThrows() + { + Assert.Throws("value", () => QueryHelpers.AddQueryString("http://contoso.com/", "hello", null!)); + } + + [Theory] + [InlineData("http://contoso.com/", "http://contoso.com/?hello=world")] + [InlineData("http://contoso.com/someaction", "http://contoso.com/someaction?hello=world")] + [InlineData("http://contoso.com/someaction?q=test", "http://contoso.com/someaction?q=test&hello=world")] + [InlineData( + "http://contoso.com/someaction?q=test#anchor", + "http://contoso.com/someaction?q=test&hello=world#anchor")] + [InlineData("http://contoso.com/someaction#anchor", "http://contoso.com/someaction?hello=world#anchor")] + [InlineData("http://contoso.com/#anchor", "http://contoso.com/?hello=world#anchor")] + [InlineData( + "http://contoso.com/someaction?q=test#anchor?value", + "http://contoso.com/someaction?q=test&hello=world#anchor?value")] + [InlineData( + "http://contoso.com/someaction#anchor?stuff", + "http://contoso.com/someaction?hello=world#anchor?stuff")] + [InlineData( + "http://contoso.com/someaction?name?something", + "http://contoso.com/someaction?name?something&hello=world")] + [InlineData( + "http://contoso.com/someaction#name#something", + "http://contoso.com/someaction?hello=world#name#something")] + public void AddQueryStringWithKeyAndValue(string uri, string expectedUri) + { + var result = QueryHelpers.AddQueryString(uri, "hello", "world"); + Assert.Equal(expectedUri, result); + } + + [Theory] + [InlineData("http://contoso.com/", "http://contoso.com/?hello=world&some=text&another=")] + [InlineData("http://contoso.com/someaction", "http://contoso.com/someaction?hello=world&some=text&another=")] + [InlineData("http://contoso.com/someaction?q=1", "http://contoso.com/someaction?q=1&hello=world&some=text&another=")] + [InlineData("http://contoso.com/some#action", "http://contoso.com/some?hello=world&some=text&another=#action")] + [InlineData("http://contoso.com/some?q=1#action", "http://contoso.com/some?q=1&hello=world&some=text&another=#action")] + [InlineData("http://contoso.com/#action", "http://contoso.com/?hello=world&some=text&another=#action")] + [InlineData( + "http://contoso.com/someaction?q=test#anchor?value", + "http://contoso.com/someaction?q=test&hello=world&some=text&another=#anchor?value")] + [InlineData( + "http://contoso.com/someaction#anchor?stuff", + "http://contoso.com/someaction?hello=world&some=text&another=#anchor?stuff")] + [InlineData( + "http://contoso.com/someaction?name?something", + "http://contoso.com/someaction?name?something&hello=world&some=text&another=")] + [InlineData( + "http://contoso.com/someaction#name#something", + "http://contoso.com/someaction?hello=world&some=text&another=#name#something")] + public void AddQueryStringWithDictionary(string uri, string expectedUri) + { + var queryStrings = new Dictionary() { { "hello", "world" }, { "some", "text" }, @@ -167,40 +167,39 @@ namespace Microsoft.AspNetCore.WebUtilities { "invisible", null } }; - var result = QueryHelpers.AddQueryString(uri, queryStrings); - Assert.Equal(expectedUri, result); - } - - [Theory] - [InlineData("http://contoso.com/", "http://contoso.com/?param1=value1¶m1=¶m1=value3¶m2=")] - [InlineData("http://contoso.com/someaction", "http://contoso.com/someaction?param1=value1¶m1=¶m1=value3¶m2=")] - [InlineData("http://contoso.com/someaction?param2=1", "http://contoso.com/someaction?param2=1¶m1=value1¶m1=¶m1=value3¶m2=")] - [InlineData("http://contoso.com/some#action", "http://contoso.com/some?param1=value1¶m1=¶m1=value3¶m2=#action")] - [InlineData("http://contoso.com/some?param2=1#action", "http://contoso.com/some?param2=1¶m1=value1¶m1=¶m1=value3¶m2=#action")] - [InlineData("http://contoso.com/#action", "http://contoso.com/?param1=value1¶m1=¶m1=value3¶m2=#action")] - [InlineData( - "http://contoso.com/someaction?q=test#anchor?value", - "http://contoso.com/someaction?q=test¶m1=value1¶m1=¶m1=value3¶m2=#anchor?value")] - [InlineData( - "http://contoso.com/someaction#anchor?stuff", - "http://contoso.com/someaction?param1=value1¶m1=¶m1=value3¶m2=#anchor?stuff")] - [InlineData( - "http://contoso.com/someaction?name?something", - "http://contoso.com/someaction?name?something¶m1=value1¶m1=¶m1=value3¶m2=")] - [InlineData( - "http://contoso.com/someaction#name#something", - "http://contoso.com/someaction?param1=value1¶m1=¶m1=value3¶m2=#name#something")] - public void AddQueryStringWithEnumerableOfKeysAndStringValues(string uri, string expectedUri) - { - var queryStrings = new Dictionary() + var result = QueryHelpers.AddQueryString(uri, queryStrings); + Assert.Equal(expectedUri, result); + } + + [Theory] + [InlineData("http://contoso.com/", "http://contoso.com/?param1=value1¶m1=¶m1=value3¶m2=")] + [InlineData("http://contoso.com/someaction", "http://contoso.com/someaction?param1=value1¶m1=¶m1=value3¶m2=")] + [InlineData("http://contoso.com/someaction?param2=1", "http://contoso.com/someaction?param2=1¶m1=value1¶m1=¶m1=value3¶m2=")] + [InlineData("http://contoso.com/some#action", "http://contoso.com/some?param1=value1¶m1=¶m1=value3¶m2=#action")] + [InlineData("http://contoso.com/some?param2=1#action", "http://contoso.com/some?param2=1¶m1=value1¶m1=¶m1=value3¶m2=#action")] + [InlineData("http://contoso.com/#action", "http://contoso.com/?param1=value1¶m1=¶m1=value3¶m2=#action")] + [InlineData( + "http://contoso.com/someaction?q=test#anchor?value", + "http://contoso.com/someaction?q=test¶m1=value1¶m1=¶m1=value3¶m2=#anchor?value")] + [InlineData( + "http://contoso.com/someaction#anchor?stuff", + "http://contoso.com/someaction?param1=value1¶m1=¶m1=value3¶m2=#anchor?stuff")] + [InlineData( + "http://contoso.com/someaction?name?something", + "http://contoso.com/someaction?name?something¶m1=value1¶m1=¶m1=value3¶m2=")] + [InlineData( + "http://contoso.com/someaction#name#something", + "http://contoso.com/someaction?param1=value1¶m1=¶m1=value3¶m2=#name#something")] + public void AddQueryStringWithEnumerableOfKeysAndStringValues(string uri, string expectedUri) + { + var queryStrings = new Dictionary() { { "param1", new StringValues(new [] { "value1", string.Empty, "value3" }) }, { "param2", string.Empty }, { "param3", StringValues.Empty } }; - var result = QueryHelpers.AddQueryString(uri, queryStrings); - Assert.Equal(expectedUri, result); - } + var result = QueryHelpers.AddQueryString(uri, queryStrings); + Assert.Equal(expectedUri, result); } } diff --git a/src/Http/WebUtilities/test/WebEncodersTests.cs b/src/Http/WebUtilities/test/WebEncodersTests.cs index e619c3d597..e261c54aa9 100644 --- a/src/Http/WebUtilities/test/WebEncodersTests.cs +++ b/src/Http/WebUtilities/test/WebEncodersTests.cs @@ -4,61 +4,60 @@ using System; using Xunit; -namespace Microsoft.AspNetCore.WebUtilities +namespace Microsoft.AspNetCore.WebUtilities; + +public class WebEncodersTests { - public class WebEncodersTests - { - [Theory] - [InlineData("", 1, 0)] - [InlineData("", 0, 1)] - [InlineData("0123456789", 9, 2)] - [InlineData("0123456789", Int32.MaxValue, 2)] - [InlineData("0123456789", 9, -1)] - public void Base64UrlDecode_BadOffsets(string input, int offset, int count) + [Theory] + [InlineData("", 1, 0)] + [InlineData("", 0, 1)] + [InlineData("0123456789", 9, 2)] + [InlineData("0123456789", Int32.MaxValue, 2)] + [InlineData("0123456789", 9, -1)] + public void Base64UrlDecode_BadOffsets(string input, int offset, int count) + { + // Act & assert + Assert.ThrowsAny(() => { - // Act & assert - Assert.ThrowsAny(() => - { - var retVal = WebEncoders.Base64UrlDecode(input, offset, count); - }); - } + var retVal = WebEncoders.Base64UrlDecode(input, offset, count); + }); + } - [Theory] - [InlineData(0, 1, 0)] - [InlineData(0, 0, 1)] - [InlineData(10, 9, 2)] - [InlineData(10, Int32.MaxValue, 2)] - [InlineData(10, 9, -1)] - public void Base64UrlEncode_BadOffsets(int inputLength, int offset, int count) - { - // Arrange - byte[] input = new byte[inputLength]; + [Theory] + [InlineData(0, 1, 0)] + [InlineData(0, 0, 1)] + [InlineData(10, 9, 2)] + [InlineData(10, Int32.MaxValue, 2)] + [InlineData(10, 9, -1)] + public void Base64UrlEncode_BadOffsets(int inputLength, int offset, int count) + { + // Arrange + byte[] input = new byte[inputLength]; - // Act & assert - Assert.ThrowsAny(() => - { - var retVal = WebEncoders.Base64UrlEncode(input, offset, count); - }); - } + // Act & assert + Assert.ThrowsAny(() => + { + var retVal = WebEncoders.Base64UrlEncode(input, offset, count); + }); + } - [Fact] - public void DataOfVariousLengthRoundTripCorrectly() + [Fact] + public void DataOfVariousLengthRoundTripCorrectly() + { + for (int length = 0; length != 256; ++length) { - for (int length = 0; length != 256; ++length) + var data = new byte[length]; + for (int index = 0; index != length; ++index) { - var data = new byte[length]; - for (int index = 0; index != length; ++index) - { - data[index] = (byte)(5 + length + (index * 23)); - } - string text = WebEncoders.Base64UrlEncode(data); - byte[] result = WebEncoders.Base64UrlDecode(text); + data[index] = (byte)(5 + length + (index * 23)); + } + string text = WebEncoders.Base64UrlEncode(data); + byte[] result = WebEncoders.Base64UrlDecode(text); - for (int index = 0; index != length; ++index) - { - Assert.Equal(data[index], result[index]); - } + for (int index = 0; index != length; ++index) + { + Assert.Equal(data[index], result[index]); } } } diff --git a/src/Http/samples/SampleApp/Program.cs b/src/Http/samples/SampleApp/Program.cs index 6631cc36ce..55ffa3ebff 100644 --- a/src/Http/samples/SampleApp/Program.cs +++ b/src/Http/samples/SampleApp/Program.cs @@ -5,20 +5,19 @@ using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; -namespace SampleApp +namespace SampleApp; + +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - var query = new QueryBuilder() + var query = new QueryBuilder() { { "hello", "world" } }.ToQueryString(); - var uri = UriHelper.BuildAbsolute("http", new HostString("contoso.com"), query: query); + var uri = UriHelper.BuildAbsolute("http", new HostString("contoso.com"), query: query); - Console.WriteLine(uri); - } + Console.WriteLine(uri); } } -- cgit v1.2.3