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.
This commit is contained in:
parent
ca92700a6f
commit
661583f694
15
Mvc.sln
15
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
|
||||
|
|
|
|||
|
|
@ -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<IApplicationBuilder> _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 = "<requestscoped>" + requestId + "</requestscoped>";
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<RequestIdService>();
|
||||
if (requestIdService.RequestId != null)
|
||||
{
|
||||
throw new InvalidOperationException("RequestId should be null here");
|
||||
}
|
||||
|
||||
var requestId = context.Request.Headers["RequestId"];
|
||||
requestIdService.RequestId = requestId;
|
||||
|
||||
await _next(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<HttpContext> contextAccessor)
|
||||
{
|
||||
if (contextAccessor.Value.RequestServices != services)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
public string RequestId { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ITypeActivator>();
|
||||
return activator.CreateInstance<Constraint>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>f12e9cf0-4958-40c6-a6cd-759185157f84</ProjectGuid>
|
||||
<RootNamespace>RequestServicesWebSite</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<DevelopmentServerPort>61370</DevelopmentServerPort>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
||||
|
|
@ -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<RequestIdService>();
|
||||
});
|
||||
|
||||
// Initializes the RequestId service for each request
|
||||
app.UseMiddleware<RequestIdMiddleware>();
|
||||
|
||||
app.UseMvc();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
@using RequestServicesWebSite
|
||||
@addtaghelper "RequestServicesWebSite.RequestScopedTagHelper, RequestServicesWebSite"
|
||||
<requestscoped></requestscoped>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
@using RequestServicesWebSite
|
||||
@inject RequestIdService RequestIdService
|
||||
@RequestIdService.RequestId
|
||||
|
|
@ -0,0 +1 @@
|
|||
@Component.Invoke("RequestId")
|
||||
|
|
@ -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" : { }
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue