From 661583f6944a9689c6ba482904cc0f337683e20a Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 4 Nov 2014 14:56:39 -0800 Subject: [PATCH] Fix for #1447 - Adding functional tests that verify per-request services These tests verify that per-request services can be injected into assets that users provide/implements (filters, constraints, controllers, views, etc). The purpose is to verify that the services are correctly resolved from the per-request service container, and don't have state that lingers and influences the next request. This is important because changing the lifetime of a framework services could easily impact the lifetimes of others, and ultimately of something the user created. --- Mvc.sln | 15 ++- .../RequestServicesTest.cs | 98 +++++++++++++++++++ .../project.json | 1 + .../Controllers/OtherController.cs | 43 ++++++++ .../RequestScopedServiceController.cs | 20 ++++ .../RequesScopedFilter.cs | 28 ++++++ .../RequestIdMiddleware.cs | 36 +++++++ .../RequestIdService.cs | 23 +++++ .../RequestIdViewComponent.cs | 18 ++++ .../RequestScopedActionConstraint.cs | 45 +++++++++ .../RequestScopedTagHelper.cs | 21 ++++ .../RequestServicesWebSite.kproj | 17 ++++ .../RequestServicesWebSite/Startup.cs | 28 ++++++ .../Views/Other/TagHelper.cshtml | 3 + .../Views/Other/View.cshtml | 3 + .../Views/Other/ViewComponent.cshtml | 1 + .../RequestServicesWebSite/project.json | 13 +++ 17 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 test/Microsoft.AspNet.Mvc.FunctionalTests/RequestServicesTest.cs create mode 100644 test/WebSites/RequestServicesWebSite/Controllers/OtherController.cs create mode 100644 test/WebSites/RequestServicesWebSite/Controllers/RequestScopedServiceController.cs create mode 100644 test/WebSites/RequestServicesWebSite/RequesScopedFilter.cs create mode 100644 test/WebSites/RequestServicesWebSite/RequestIdMiddleware.cs create mode 100644 test/WebSites/RequestServicesWebSite/RequestIdService.cs create mode 100644 test/WebSites/RequestServicesWebSite/RequestIdViewComponent.cs create mode 100644 test/WebSites/RequestServicesWebSite/RequestScopedActionConstraint.cs create mode 100644 test/WebSites/RequestServicesWebSite/RequestScopedTagHelper.cs create mode 100644 test/WebSites/RequestServicesWebSite/RequestServicesWebSite.kproj create mode 100644 test/WebSites/RequestServicesWebSite/Startup.cs create mode 100644 test/WebSites/RequestServicesWebSite/Views/Other/TagHelper.cshtml create mode 100644 test/WebSites/RequestServicesWebSite/Views/Other/View.cshtml create mode 100644 test/WebSites/RequestServicesWebSite/Views/Other/ViewComponent.cshtml create mode 100644 test/WebSites/RequestServicesWebSite/project.json diff --git a/Mvc.sln b/Mvc.sln index 252acd505d..63f283dac8 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22209.0 +VisualStudioVersion = 14.0.22213.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -108,6 +108,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.TagHel EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "PrecompilationWebSite", "test\WebSites\PrecompilationWebSite\PrecompilationWebSite.kproj", "{59E1BE90-92C1-4D35-ADCC-B69F49077C81}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "RequestServicesWebSite", "test\WebSites\RequestServicesWebSite\RequestServicesWebSite.kproj", "{F12E9CF0-4958-40C6-A6CD-759185157F84}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -578,6 +580,16 @@ Global {59E1BE90-92C1-4D35-ADCC-B69F49077C81}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {59E1BE90-92C1-4D35-ADCC-B69F49077C81}.Release|Mixed Platforms.Build.0 = Release|Any CPU {59E1BE90-92C1-4D35-ADCC-B69F49077C81}.Release|x86.ActiveCfg = Release|Any CPU + {F12E9CF0-4958-40C6-A6CD-759185157F84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F12E9CF0-4958-40C6-A6CD-759185157F84}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F12E9CF0-4958-40C6-A6CD-759185157F84}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {F12E9CF0-4958-40C6-A6CD-759185157F84}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {F12E9CF0-4958-40C6-A6CD-759185157F84}.Debug|x86.ActiveCfg = Debug|Any CPU + {F12E9CF0-4958-40C6-A6CD-759185157F84}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F12E9CF0-4958-40C6-A6CD-759185157F84}.Release|Any CPU.Build.0 = Release|Any CPU + {F12E9CF0-4958-40C6-A6CD-759185157F84}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {F12E9CF0-4958-40C6-A6CD-759185157F84}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {F12E9CF0-4958-40C6-A6CD-759185157F84}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -630,5 +642,6 @@ Global {B2347320-308E-4D2B-AEC8-005DFA68B0C9} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {860119ED-3DB1-424D-8D0A-30132A8A7D96} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {59E1BE90-92C1-4D35-ADCC-B69F49077C81} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} + {F12E9CF0-4958-40C6-A6CD-759185157F84} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} EndGlobalSection EndGlobal diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/RequestServicesTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/RequestServicesTest.cs new file mode 100644 index 0000000000..47711e9dff --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/RequestServicesTest.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + // Each of these tests makes two requests, because we want each test to verify that the data is + // PER-REQUEST and does not linger around to impact the next request. + public class RequestServicesTest + { + private readonly IServiceProvider _provider = TestHelper.CreateServices(nameof(RequestServicesWebSite)); + private readonly Action _app = new RequestServicesWebSite.Startup().Configure; + + [Theory] + [InlineData("http://localhost/RequestScoped/FromController")] + [InlineData("http://localhost/Other/FromFilter")] + [InlineData("http://localhost/Other/FromView")] + [InlineData("http://localhost/Other/FromViewComponent")] + public async Task RequestServices(string url) + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + // Act & Assert + for (var i = 0; i < 2; i++) + { + var requestId = Guid.NewGuid().ToString(); + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.TryAddWithoutValidation("RequestId", requestId); + + var response = await client.SendAsync(request); + + var body = (await response.Content.ReadAsStringAsync()).Trim(); + Assert.Equal(requestId, body); + } + } + + [Fact] + public async Task RequestServices_TagHelper() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var url = "http://localhost/Other/FromTagHelper"; + + // Act & Assert + for (var i = 0; i < 2; i++) + { + var requestId = Guid.NewGuid().ToString(); + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.TryAddWithoutValidation("RequestId", requestId); + + var response = await client.SendAsync(request); + + var body = (await response.Content.ReadAsStringAsync()).Trim(); + + var expected = "" + requestId + ""; + Assert.Equal(expected, body); + } + } + + [Fact] + public async Task RequestServices_ActionConstraint() + { + // Arrange + var server = TestServer.Create(_provider, _app); + var client = server.CreateClient(); + + var url = "http://localhost/Other/FromActionConstraint"; + + // Act & Assert + var requestId1 = "b40f6ec1-8a6b-41c1-b3fe-928f581ebaf5"; + var request1 = new HttpRequestMessage(HttpMethod.Get, url); + request1.Headers.TryAddWithoutValidation("RequestId", requestId1); + + var response1 = await client.SendAsync(request1); + + var body1 = (await response1.Content.ReadAsStringAsync()).Trim(); + Assert.Equal(requestId1, body1); + + var requestId2 = Guid.NewGuid().ToString(); + var request2 = new HttpRequestMessage(HttpMethod.Get, url); + request2.Headers.TryAddWithoutValidation("RequestId", requestId2); + + var response2 = await client.SendAsync(request2); + Assert.Equal(HttpStatusCode.NotFound, response2.StatusCode); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index 71059af8ee..0b0098189b 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -21,6 +21,7 @@ "RoutingWebSite": "1.0.0", "RazorWebSite": "1.0.0", "RazorInstrumentationWebsite": "1.0.0", + "RequestServicesWebSite": "1.0.0", "TagHelpersWebSite": "1.0.0", "UrlHelperWebSite": "1.0.0", "ValueProvidersSite": "1.0.0", diff --git a/test/WebSites/RequestServicesWebSite/Controllers/OtherController.cs b/test/WebSites/RequestServicesWebSite/Controllers/OtherController.cs new file mode 100644 index 0000000000..e48080c0e5 --- /dev/null +++ b/test/WebSites/RequestServicesWebSite/Controllers/OtherController.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace RequestServicesWebSite +{ + [Route("Other/[action]")] + public class OtherController : Controller + { + // This only matches a specific requestId value + [HttpGet] + [RequestScopedActionConstraint("b40f6ec1-8a6b-41c1-b3fe-928f581ebaf5")] + public string FromActionConstraint() + { + return "b40f6ec1-8a6b-41c1-b3fe-928f581ebaf5"; + } + + [HttpGet] + [TypeFilter(typeof(RequestScopedFilter))] + public void FromFilter() + { + } + + [HttpGet] + public IActionResult FromView() + { + return View("View"); + } + + [HttpGet] + public IActionResult FromTagHelper() + { + return View("TagHelper"); + } + + [HttpGet] + public IActionResult FromViewComponent() + { + return View("ViewComponent"); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RequestServicesWebSite/Controllers/RequestScopedServiceController.cs b/test/WebSites/RequestServicesWebSite/Controllers/RequestScopedServiceController.cs new file mode 100644 index 0000000000..2586890afa --- /dev/null +++ b/test/WebSites/RequestServicesWebSite/Controllers/RequestScopedServiceController.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace RequestServicesWebSite +{ + [Route("RequestScoped/[action]")] + public class RequestScopedServiceController + { + [Activate] + public RequestIdService RequestIdService { get; set; } + + [HttpGet] + public string FromController() + { + return RequestIdService.RequestId; + } + } +} \ No newline at end of file diff --git a/test/WebSites/RequestServicesWebSite/RequesScopedFilter.cs b/test/WebSites/RequestServicesWebSite/RequesScopedFilter.cs new file mode 100644 index 0000000000..6a4c73a6a4 --- /dev/null +++ b/test/WebSites/RequestServicesWebSite/RequesScopedFilter.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Mvc; + +namespace RequestServicesWebSite +{ + public class RequestScopedFilter : IActionFilter + { + private readonly RequestIdService _requestIdService; + + public RequestScopedFilter(RequestIdService requestIdService) + { + _requestIdService = requestIdService; + } + + public void OnActionExecuted(ActionExecutedContext context) + { + throw new NotImplementedException(); + } + + public void OnActionExecuting(ActionExecutingContext context) + { + context.Result = new ObjectResult(_requestIdService.RequestId); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RequestServicesWebSite/RequestIdMiddleware.cs b/test/WebSites/RequestServicesWebSite/RequestIdMiddleware.cs new file mode 100644 index 0000000000..ef78e8721d --- /dev/null +++ b/test/WebSites/RequestServicesWebSite/RequestIdMiddleware.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.Framework.DependencyInjection; + +namespace RequestServicesWebSite +{ + // Initializes a scoped-service with a request Id from a header + public class RequestIdMiddleware + { + private readonly RequestDelegate _next; + + public RequestIdMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext context) + { + var requestIdService = context.RequestServices.GetService(); + if (requestIdService.RequestId != null) + { + throw new InvalidOperationException("RequestId should be null here"); + } + + var requestId = context.Request.Headers["RequestId"]; + requestIdService.RequestId = requestId; + + await _next(context); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RequestServicesWebSite/RequestIdService.cs b/test/WebSites/RequestServicesWebSite/RequestIdService.cs new file mode 100644 index 0000000000..f1d9735f0d --- /dev/null +++ b/test/WebSites/RequestServicesWebSite/RequestIdService.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Http; +using Microsoft.Framework.DependencyInjection; + +namespace RequestServicesWebSite +{ + public class RequestIdService + { + // This service can only be instantiated by a request-scoped container + public RequestIdService(IServiceProvider services, IContextAccessor contextAccessor) + { + if (contextAccessor.Value.RequestServices != services) + { + throw new InvalidOperationException(); + } + } + + public string RequestId { get; set; } + } +} \ No newline at end of file diff --git a/test/WebSites/RequestServicesWebSite/RequestIdViewComponent.cs b/test/WebSites/RequestServicesWebSite/RequestIdViewComponent.cs new file mode 100644 index 0000000000..0762cae18d --- /dev/null +++ b/test/WebSites/RequestServicesWebSite/RequestIdViewComponent.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace RequestServicesWebSite +{ + public class RequestIdViewComponent : ViewComponent + { + [Activate] + public RequestIdService RequestIdService { get; set; } + + public IViewComponentResult Invoke() + { + return Content(RequestIdService.RequestId); + } + } +} \ No newline at end of file diff --git a/test/WebSites/RequestServicesWebSite/RequestScopedActionConstraint.cs b/test/WebSites/RequestServicesWebSite/RequestScopedActionConstraint.cs new file mode 100644 index 0000000000..ef9e373af8 --- /dev/null +++ b/test/WebSites/RequestServicesWebSite/RequestScopedActionConstraint.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Mvc; +using Microsoft.Framework.DependencyInjection; + +namespace RequestServicesWebSite +{ + // Only matches when the requestId is the same as the one passed in the constructor. + public class RequestScopedActionConstraintAttribute : Attribute, IActionConstraintFactory + { + private readonly string _requestId; + + public RequestScopedActionConstraintAttribute(string requestId) + { + _requestId = requestId; + } + + public IActionConstraint CreateInstance(IServiceProvider services) + { + var activator = services.GetService(); + return activator.CreateInstance(services, _requestId); + } + + private class Constraint : IActionConstraint + { + private readonly RequestIdService _requestIdService; + private readonly string _requestId; + + public Constraint(RequestIdService requestIdService, string requestId) + { + _requestIdService = requestIdService; + _requestId = requestId; + } + + public int Order { get; private set; } + + public bool Accept(ActionConstraintContext context) + { + return _requestId == _requestIdService.RequestId; + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/RequestServicesWebSite/RequestScopedTagHelper.cs b/test/WebSites/RequestServicesWebSite/RequestScopedTagHelper.cs new file mode 100644 index 0000000000..1b918cfe6e --- /dev/null +++ b/test/WebSites/RequestServicesWebSite/RequestScopedTagHelper.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.AspNet.Razor.TagHelpers; + +namespace RequestServicesWebSite +{ + [ContentBehavior(ContentBehavior.Replace)] + public class RequestScopedTagHelper : TagHelper + { + [Activate] + public RequestIdService RequestIdService { get; set; } + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + output.Content = RequestIdService.RequestId; + } + } +} \ No newline at end of file diff --git a/test/WebSites/RequestServicesWebSite/RequestServicesWebSite.kproj b/test/WebSites/RequestServicesWebSite/RequestServicesWebSite.kproj new file mode 100644 index 0000000000..b46e11ec4e --- /dev/null +++ b/test/WebSites/RequestServicesWebSite/RequestServicesWebSite.kproj @@ -0,0 +1,17 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + f12e9cf0-4958-40c6-a6cd-759185157f84 + RequestServicesWebSite + + + 2.0 + 61370 + + + \ No newline at end of file diff --git a/test/WebSites/RequestServicesWebSite/Startup.cs b/test/WebSites/RequestServicesWebSite/Startup.cs new file mode 100644 index 0000000000..ccb8d7f72b --- /dev/null +++ b/test/WebSites/RequestServicesWebSite/Startup.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Builder; +using Microsoft.Framework.DependencyInjection; + +namespace RequestServicesWebSite +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + var configuration = app.GetTestConfiguration(); + + app.UseServices(services => + { + services.AddMvc(configuration); + + services.AddScoped(); + }); + + // Initializes the RequestId service for each request + app.UseMiddleware(); + + app.UseMvc(); + } + } +} diff --git a/test/WebSites/RequestServicesWebSite/Views/Other/TagHelper.cshtml b/test/WebSites/RequestServicesWebSite/Views/Other/TagHelper.cshtml new file mode 100644 index 0000000000..b459fc044f --- /dev/null +++ b/test/WebSites/RequestServicesWebSite/Views/Other/TagHelper.cshtml @@ -0,0 +1,3 @@ +@using RequestServicesWebSite +@addtaghelper "RequestServicesWebSite.RequestScopedTagHelper, RequestServicesWebSite" + \ No newline at end of file diff --git a/test/WebSites/RequestServicesWebSite/Views/Other/View.cshtml b/test/WebSites/RequestServicesWebSite/Views/Other/View.cshtml new file mode 100644 index 0000000000..dbd7c419b4 --- /dev/null +++ b/test/WebSites/RequestServicesWebSite/Views/Other/View.cshtml @@ -0,0 +1,3 @@ +@using RequestServicesWebSite +@inject RequestIdService RequestIdService +@RequestIdService.RequestId \ No newline at end of file diff --git a/test/WebSites/RequestServicesWebSite/Views/Other/ViewComponent.cshtml b/test/WebSites/RequestServicesWebSite/Views/Other/ViewComponent.cshtml new file mode 100644 index 0000000000..1fed10c20f --- /dev/null +++ b/test/WebSites/RequestServicesWebSite/Views/Other/ViewComponent.cshtml @@ -0,0 +1 @@ +@Component.Invoke("RequestId") \ No newline at end of file diff --git a/test/WebSites/RequestServicesWebSite/project.json b/test/WebSites/RequestServicesWebSite/project.json new file mode 100644 index 0000000000..072410a212 --- /dev/null +++ b/test/WebSites/RequestServicesWebSite/project.json @@ -0,0 +1,13 @@ +{ + "webroot": "wwwroot", + "exclude": "wwwroot/**/*", + "dependencies": { + "Microsoft.AspNet.Mvc": "6.0.0-*", + "Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0", + "Microsoft.AspNet.Server.IIS": "1.0.0-*" + }, + "frameworks" : { + "aspnet50" : { }, + "aspnetcore50" : { } + } +}