Fix MVC integration with UseEndpoint (#8047)

This commit is contained in:
James Newton-King 2018-07-11 11:47:33 +12:00 committed by GitHub
parent 1e7be641ae
commit 183ecd85d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 144 additions and 147 deletions

View File

@ -122,7 +122,6 @@ namespace Microsoft.AspNetCore.Builder
configureRoutes(routeBuilder); configureRoutes(routeBuilder);
mvcEndpointDataSource.ConventionalEndpointInfos.AddRange(routeBuilder.EndpointInfos); mvcEndpointDataSource.ConventionalEndpointInfos.AddRange(routeBuilder.EndpointInfos);
mvcEndpointDataSource.InitializeEndpoints();
return app.UseEndpoint(); return app.UseEndpoint();
} }

View File

@ -24,8 +24,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders; private readonly IActionDescriptorChangeProvider[] _actionDescriptorChangeProviders;
private readonly List<Endpoint> _endpoints; private readonly List<Endpoint> _endpoints;
private readonly object _lock = new object();
private IChangeToken _changeToken; private IChangeToken _changeToken;
private bool _initialized;
public MvcEndpointDataSource( public MvcEndpointDataSource(
IActionDescriptorCollectionProvider actions, IActionDescriptorCollectionProvider actions,
@ -62,7 +64,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ConventionalEndpointInfos = new List<MvcEndpointInfo>(); ConventionalEndpointInfos = new List<MvcEndpointInfo>();
} }
public void InitializeEndpoints() private void InitializeEndpoints()
{ {
foreach (var action in _actions.ActionDescriptors.Items) foreach (var action in _actions.ActionDescriptors.Items)
{ {
@ -387,8 +389,27 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
} }
public override IReadOnlyList<Endpoint> Endpoints => _endpoints; public override IReadOnlyList<Endpoint> Endpoints
{
get
{
if (!_initialized)
{
lock (_lock)
{
if (!_initialized)
{
InitializeEndpoints();
_initialized = true;
}
}
}
return _endpoints;
}
}
// REVIEW: Infos added after endpoints are initialized will not be used
public List<MvcEndpointInfo> ConventionalEndpointInfos { get; } public List<MvcEndpointInfo> ConventionalEndpointInfos { get; }
private class RouteNameMetadata : IRouteNameMetadata private class RouteNameMetadata : IRouteNameMetadata

View File

@ -59,10 +59,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object); var dataSource = CreateMvcEndpointDataSource(mockDescriptorProvider.Object);
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
var endpoint = Assert.Single(dataSource.Endpoints); var endpoint = Assert.Single(endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint); var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
var endpointValue = matcherEndpoint.RequiredValues["Name"]; var endpointValue = matcherEndpoint.RequiredValues["Name"];
@ -115,10 +115,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
new MvcEndpointInvokerFactory(actionInvokerProviderMock.Object)); new MvcEndpointInvokerFactory(actionInvokerProviderMock.Object));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
var endpoint = Assert.Single(dataSource.Endpoints); var endpoint = Assert.Single(endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint); var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
var invokerDelegate = matcherEndpoint.Invoker((next) => Task.CompletedTask); var invokerDelegate = matcherEndpoint.Invoker((next) => Task.CompletedTask);
@ -192,7 +192,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute)); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
var inspectors = finalEndpointTemplates var inspectors = finalEndpointTemplates
@ -200,7 +200,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
.ToArray(); .ToArray();
// Assert // Assert
Assert.Collection(dataSource.Endpoints, inspectors); Assert.Collection(endpoints, inspectors);
} }
[Theory] [Theory]
@ -219,7 +219,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute)); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, endpointInfoRoute));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
var inspectors = finalEndpointTemplates var inspectors = finalEndpointTemplates
@ -227,7 +227,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
.ToArray(); .ToArray();
// Assert // Assert
Assert.Collection(dataSource.Endpoints, inspectors); Assert.Collection(endpoints, inspectors);
} }
[Fact] [Fact]
@ -243,10 +243,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
new RouteValueDictionary(new { action = "TestAction" }))); new RouteValueDictionary(new { action = "TestAction" })));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
Assert.Collection(dataSource.Endpoints, Assert.Collection(endpoints,
(e) => Assert.Equal("TestController", Assert.IsType<MatcherEndpoint>(e).Template), (e) => Assert.Equal("TestController", Assert.IsType<MatcherEndpoint>(e).Template),
(e) => Assert.Equal("TestController/TestAction", Assert.IsType<MatcherEndpoint>(e).Template)); (e) => Assert.Equal("TestController/TestAction", Assert.IsType<MatcherEndpoint>(e).Template));
} }
@ -266,10 +266,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
constraints: new RouteValueDictionary(new { action = "(TestAction1|TestAction2)" }))); constraints: new RouteValueDictionary(new { action = "(TestAction1|TestAction2)" })));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
Assert.Collection(dataSource.Endpoints, Assert.Collection(endpoints,
(e) => Assert.Equal("TestController/TestAction1", Assert.IsType<MatcherEndpoint>(e).Template), (e) => Assert.Equal("TestController/TestAction1", Assert.IsType<MatcherEndpoint>(e).Template),
(e) => Assert.Equal("TestController/TestAction2", Assert.IsType<MatcherEndpoint>(e).Template)); (e) => Assert.Equal("TestController/TestAction2", Assert.IsType<MatcherEndpoint>(e).Template));
} }
@ -291,14 +291,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
endpointInfoRoute)); endpointInfoRoute));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
var inspectors = finalEndpointTemplates var inspectors = finalEndpointTemplates
.Select(t => new Action<Endpoint>(e => Assert.Equal(t, Assert.IsType<MatcherEndpoint>(e).Template))) .Select(t => new Action<Endpoint>(e => Assert.Equal(t, Assert.IsType<MatcherEndpoint>(e).Template)))
.ToArray(); .ToArray();
// Assert // Assert
Assert.Collection(dataSource.Endpoints, inspectors); Assert.Collection(endpoints, inspectors);
} }
[Fact] [Fact]
@ -312,10 +312,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
CreateEndpointInfo(string.Empty, "named/{controller}/{action}/{id?}")); CreateEndpointInfo(string.Empty, "named/{controller}/{action}/{id?}"));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
var endpoint = Assert.Single(dataSource.Endpoints); var endpoint = Assert.Single(endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint); var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
var routeNameMetadata = matcherEndpoint.Metadata.GetMetadata<IRouteNameMetadata>(); var routeNameMetadata = matcherEndpoint.Metadata.GetMetadata<IRouteNameMetadata>();
Assert.Null(routeNameMetadata); Assert.Null(routeNameMetadata);
@ -333,11 +333,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
CreateEndpointInfo("namedRoute", "named/{controller}/{action}/{id?}")); CreateEndpointInfo("namedRoute", "named/{controller}/{action}/{id?}"));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
Assert.Collection( Assert.Collection(
dataSource.Endpoints, endpoints,
(ep) => (ep) =>
{ {
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(ep); var matcherEndpoint = Assert.IsType<MatcherEndpoint>(ep);
@ -372,11 +372,11 @@ namespace Microsoft.AspNetCore.Mvc.Internal
"named/{controller}/{action}/{id?}")); "named/{controller}/{action}/{id?}"));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
Assert.Collection( Assert.Collection(
dataSource.Endpoints, endpoints,
(ep) => (ep) =>
{ {
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(ep); var matcherEndpoint = Assert.IsType<MatcherEndpoint>(ep);
@ -413,10 +413,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}")); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}"));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
Assert.Empty(dataSource.Endpoints); Assert.Empty(endpoints);
} }
// Since area, controller, action and page are special, check to see if the followin test succeeds for a // Since area, controller, action and page are special, check to see if the followin test succeeds for a
@ -431,10 +431,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}")); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}"));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
Assert.Empty(dataSource.Endpoints); Assert.Empty(endpoints);
} }
[Fact] [Fact]
@ -447,10 +447,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area}/{controller}/{action}")); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area}/{controller}/{action}"));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
Assert.Empty(dataSource.Endpoints); Assert.Empty(endpoints);
} }
[Fact] [Fact]
@ -463,10 +463,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area=admin}/{controller}/{action}")); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area=admin}/{controller}/{action}"));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
Assert.Empty(dataSource.Endpoints); Assert.Empty(endpoints);
} }
[Fact] [Fact]
@ -479,10 +479,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area}/{controller}/{action}")); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{area}/{controller}/{action}"));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
Assert.Empty(dataSource.Endpoints); Assert.Empty(endpoints);
} }
[Fact] [Fact]
@ -495,10 +495,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}")); dataSource.ConventionalEndpointInfos.Add(CreateEndpointInfo(string.Empty, "{controller}/{action}"));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
var endpoint = Assert.Single(dataSource.Endpoints); var endpoint = Assert.Single(endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint); var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
Assert.Equal("Foo/Bar", matcherEndpoint.Template); Assert.Equal("Foo/Bar", matcherEndpoint.Template);
AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults); AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults);
@ -517,10 +517,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
CreateEndpointInfo(string.Empty, "{controller=Home}/{action=Index}")); CreateEndpointInfo(string.Empty, "{controller=Home}/{action=Index}"));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
var endpoint = Assert.Single(dataSource.Endpoints); var endpoint = Assert.Single(endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint); var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
Assert.Equal("Foo/Bar", matcherEndpoint.Template); Assert.Equal("Foo/Bar", matcherEndpoint.Template);
AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults); AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults);
@ -540,10 +540,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
CreateEndpointInfo(string.Empty, "{controller=Home}/{action=Index}/{subscription=general}")); CreateEndpointInfo(string.Empty, "{controller=Home}/{action=Index}/{subscription=general}"));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
var endpoint = Assert.Single(dataSource.Endpoints); var endpoint = Assert.Single(endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint); var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
Assert.Equal("Foo/Bar/{subscription=general}", matcherEndpoint.Template); Assert.Equal("Foo/Bar/{subscription=general}", matcherEndpoint.Template);
AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults); AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults);
@ -562,10 +562,10 @@ namespace Microsoft.AspNetCore.Mvc.Internal
CreateEndpointInfo(string.Empty, "{controller=Home}/{action=Index}")); CreateEndpointInfo(string.Empty, "{controller=Home}/{action=Index}"));
// Act // Act
dataSource.InitializeEndpoints(); var endpoints = dataSource.Endpoints;
// Assert // Assert
var endpoint = Assert.Single(dataSource.Endpoints); var endpoint = Assert.Single(endpoints);
var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint); var matcherEndpoint = Assert.IsType<MatcherEndpoint>(endpoint);
Assert.Equal("Foo/Bar", matcherEndpoint.Template); Assert.Equal("Foo/Bar", matcherEndpoint.Template);
AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults); AssertIsSubset(expectedDefaults, matcherEndpoint.Defaults);

View File

@ -1,103 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Routing;
using Newtonsoft.Json;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
public class RouteDataTest : IClassFixture<MvcTestFixture<BasicWebSite.Startup>>
{
public RouteDataTest(MvcTestFixture<BasicWebSite.Startup> fixture)
{
Client = fixture.CreateDefaultClient();
}
public HttpClient Client { get; }
[Fact]
public async Task RouteData_Routers_ConventionalRoute()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/Routing/Conventional");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ResultData>(body);
Assert.Equal(
new string[]
{
typeof(RouteCollection).FullName,
typeof(Route).FullName,
typeof(MvcRouteHandler).FullName,
},
result.Routers);
}
[Fact]
public async Task RouteData_Routers_AttributeRoute()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/Routing/Attribute");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ResultData>(body);
Assert.Equal(new string[]
{
typeof(RouteCollection).FullName,
typeof(AttributeRoute).FullName,
typeof(MvcAttributeRouteHandler).FullName,
},
result.Routers);
}
// Verifies that components in the MVC pipeline can modify datatokens
// without impacting any static data.
//
// This does two request, to verify that the data in the route is not modified
[Fact]
public async Task RouteData_DataTokens_FilterCanSetDataTokens()
{
// Arrange
var response = await Client.GetAsync("http://localhost/Routing/DataTokens");
// Guard
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ResultData>(body);
Assert.Single(result.DataTokens);
Assert.Single(result.DataTokens, kvp => kvp.Key == "actionName" && ((string)kvp.Value) == "DataTokens");
// Act
response = await Client.GetAsync("http://localhost/Routing/Conventional");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
body = await response.Content.ReadAsStringAsync();
result = JsonConvert.DeserializeObject<ResultData>(body);
Assert.Single(result.DataTokens);
Assert.Single(result.DataTokens, kvp => kvp.Key == "actionName" && ((string)kvp.Value) == "Conventional");
}
private class ResultData
{
public Dictionary<string, object> DataTokens { get; set; }
public string[] Routers { get; set; }
}
}
}

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing;
using Newtonsoft.Json; using Newtonsoft.Json;
using Xunit; using Xunit;
@ -22,6 +23,85 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public HttpClient Client { get; } public HttpClient Client { get; }
[Fact]
public async Task RouteData_Routers_ConventionalRoute()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/RouteData/Conventional");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ResultData>(body);
Assert.Equal(
new string[]
{
typeof(RouteCollection).FullName,
typeof(Route).FullName,
typeof(MvcRouteHandler).FullName,
},
result.Routers);
}
[Fact]
public async Task RouteData_Routers_AttributeRoute()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/RouteData/Attribute");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ResultData>(body);
Assert.Equal(new string[]
{
typeof(RouteCollection).FullName,
typeof(AttributeRoute).FullName,
typeof(MvcAttributeRouteHandler).FullName,
},
result.Routers);
}
// Verifies that components in the MVC pipeline can modify datatokens
// without impacting any static data.
//
// This does two request, to verify that the data in the route is not modified
[Fact]
public async Task RouteData_DataTokens_FilterCanSetDataTokens()
{
// Arrange
var response = await Client.GetAsync("http://localhost/RouteData/DataTokens");
// Guard
var body = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ResultData>(body);
Assert.Single(result.DataTokens);
Assert.Single(result.DataTokens, kvp => kvp.Key == "actionName" && ((string)kvp.Value) == "DataTokens");
// Act
response = await Client.GetAsync("http://localhost/RouteData/Conventional");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
body = await response.Content.ReadAsStringAsync();
result = JsonConvert.DeserializeObject<ResultData>(body);
Assert.Single(result.DataTokens);
Assert.Single(result.DataTokens, kvp => kvp.Key == "actionName" && ((string)kvp.Value) == "Conventional");
}
private class ResultData
{
public Dictionary<string, object> DataTokens { get; set; }
public string[] Routers { get; set; }
}
[Fact] [Fact]
public virtual async Task ConventionalRoutedController_ActionIsReachable() public virtual async Task ConventionalRoutedController_ActionIsReachable()
{ {

View File

@ -6,16 +6,16 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
namespace BasicWebSite namespace RoutingWebSite.Controllers
{ {
public class RoutingController : Controller public class RouteDataController : Controller
{ {
public object Conventional() public object Conventional()
{ {
return GetData(); return GetData();
} }
[Route("Routing/Attribute")] [Route("RouteData/Attribute")]
public object Attribute() public object Attribute()
{ {
return GetData(); return GetData();