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:
Ryan Nowak 2014-11-04 14:56:39 -08:00
parent ca92700a6f
commit 661583f694
17 changed files with 412 additions and 1 deletions

15
Mvc.sln
View File

@ -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

View File

@ -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);
}
}
}

View File

@ -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",

View File

@ -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");
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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();
}
}
}

View File

@ -0,0 +1,3 @@
@using RequestServicesWebSite
@addtaghelper "RequestServicesWebSite.RequestScopedTagHelper, RequestServicesWebSite"
<requestscoped></requestscoped>

View File

@ -0,0 +1,3 @@
@using RequestServicesWebSite
@inject RequestIdService RequestIdService
@RequestIdService.RequestId

View File

@ -0,0 +1 @@
@Component.Invoke("RequestId")

View File

@ -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" : { }
}
}