diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs index 786ce62b61..2d181e9754 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcEndpointDataSource.cs @@ -293,6 +293,25 @@ namespace Microsoft.AspNetCore.Mvc.Internal return invoker.InvokeAsync(); }; + var metadataCollection = BuildEndpointMetadata(action, routeName, source); + var endpoint = new MatcherEndpoint( + next => invokerDelegate, + template, + new RouteValueDictionary(nonInlineDefaults), + new RouteValueDictionary(action.RouteValues), + order, + metadataCollection, + action.DisplayName); + + // Use defaults after the endpoint is created as it merges both the inline and + // non-inline defaults into one. + EnsureRequiredValuesInDefaults(endpoint.RequiredValues, endpoint.Defaults); + + return endpoint; + } + + private static EndpointMetadataCollection BuildEndpointMetadata(ActionDescriptor action, string routeName, object source) + { var metadata = new List(); // REVIEW: Used for debugging. Consider removing before release metadata.Add(source); @@ -312,30 +331,27 @@ namespace Microsoft.AspNetCore.Mvc.Internal if (action.ActionConstraints != null && action.ActionConstraints.Count > 0) { + // REVIEW: What is the best way to pick up endpoint constraints of an ActionDescriptor? + // Currently they need to implement IActionConstraintMetadata foreach (var actionConstraint in action.ActionConstraints) { if (actionConstraint is HttpMethodActionConstraint httpMethodActionConstraint) { metadata.Add(new HttpMethodEndpointConstraint(httpMethodActionConstraint.HttpMethods)); } + else if (actionConstraint is IEndpointConstraintMetadata) + { + // The constraint might have been added earlier, e.g. it is also a filter descriptor + if (!metadata.Contains(actionConstraint)) + { + metadata.Add(actionConstraint); + } + } } } var metadataCollection = new EndpointMetadataCollection(metadata); - var endpoint = new MatcherEndpoint( - next => invokerDelegate, - template, - new RouteValueDictionary(nonInlineDefaults), - new RouteValueDictionary(action.RouteValues), - order, - metadataCollection, - action.DisplayName); - - // Use defaults after the endpoint is created as it merges both the inline and - // non-inline defaults into one. - EnsureRequiredValuesInDefaults(endpoint.RequiredValues, endpoint.Defaults); - - return endpoint; + return metadataCollection; } // Ensure required values are a subset of defaults diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningDispatchingTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningDispatchingTests.cs new file mode 100644 index 0000000000..ff0c8e700c --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningDispatchingTests.cs @@ -0,0 +1,13 @@ +// 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. + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public class VersioningDispatchingTests : VersioningTestsBase + { + public VersioningDispatchingTests(MvcTestFixture fixture) + : base(fixture) + { + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTests.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTests.cs index 02a86d0871..8490fda117 100644 --- a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTests.cs +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTests.cs @@ -1,562 +1,13 @@ // 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 Newtonsoft.Json; -using Xunit; - namespace Microsoft.AspNetCore.Mvc.FunctionalTests { - public class VersioningTests : IClassFixture> + public class VersioningTests : VersioningTestsBase { public VersioningTests(MvcTestFixture fixture) + : base(fixture) { - Client = fixture.CreateDefaultClient(); - } - - public HttpClient Client { get; } - - [Theory] - [InlineData("1")] - [InlineData("2")] - public async Task AttributeRoutedAction_WithVersionedRoutes_IsNotAmbiguous(string version) - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/api/Addresses?version=" + version); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Contains("api/addresses", result.ExpectedUrls); - Assert.Equal("Address", result.Controller); - Assert.Equal("GetV" + version, result.Action); - } - - [Theory] - [InlineData("1")] - [InlineData("2")] - public async Task AttributeRoutedAction_WithAmbiguousVersionedRoutes_CanBeDisambiguatedUsingOrder(string version) - { - // Arrange - var query = "?version=" + version; - var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/api/Addresses/All" + query); - - // Act - - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Contains("/api/addresses/all?version=" + version, result.ExpectedUrls); - Assert.Equal("Address", result.Controller); - Assert.Equal("GetAllV" + version, result.Action); - } - - [Fact] - public async Task VersionedApi_CanReachV1Operations_OnTheSameController_WithNoVersionSpecified() - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Tickets"); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Tickets", result.Controller); - Assert.Equal("Get", result.Action); - - Assert.DoesNotContain("id", result.RouteValues.Keys); - } - - [Fact] - public async Task VersionedApi_CanReachV1Operations_OnTheSameController_WithVersionSpecified() - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Tickets?version=2"); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Tickets", result.Controller); - Assert.Equal("Get", result.Action); - } - - [Fact] - public async Task VersionedApi_CanReachV1OperationsWithParameters_OnTheSameController() - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Tickets/5"); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Tickets", result.Controller); - Assert.Equal("GetById", result.Action); - } - - [Fact] - public async Task VersionedApi_CanReachV1OperationsWithParameters_OnTheSameController_WithVersionSpecified() - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Tickets/5?version=2"); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Tickets", result.Controller); - Assert.Equal("GetById", result.Action); - Assert.NotEmpty(result.RouteValues); - - Assert.Contains( - new KeyValuePair("id", "5"), - result.RouteValues); - } - - [Theory] - [InlineData("2")] - [InlineData("3")] - [InlineData("4")] - public async Task VersionedApi_CanReachOtherVersionOperations_OnTheSameController(string version) - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Tickets?version=" + version); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Tickets", result.Controller); - Assert.Equal("Post", result.Action); - Assert.NotEmpty(result.RouteValues); - - Assert.DoesNotContain( - new KeyValuePair("id", "5"), - result.RouteValues); - } - - [Fact] - public async Task VersionedApi_CanNotReachOtherVersionOperations_OnTheSameController_WithNoVersionSpecified() - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Tickets"); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - - var body = await response.Content.ReadAsByteArrayAsync(); - Assert.Empty(body); - } - - [Theory] - [InlineData("PUT", "Put", "2")] - [InlineData("PUT", "Put", "3")] - [InlineData("PUT", "Put", "4")] - [InlineData("DELETE", "Delete", "2")] - [InlineData("DELETE", "Delete", "3")] - [InlineData("DELETE", "Delete", "4")] - public async Task VersionedApi_CanReachOtherVersionOperationsWithParameters_OnTheSameController( - string method, - string action, - string version) - { - // Arrange - var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Tickets/5?version=" + version); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Tickets", result.Controller); - Assert.Equal(action, result.Action); - Assert.NotEmpty(result.RouteValues); - - Assert.Contains( - new KeyValuePair("id", "5"), - result.RouteValues); - } - - [Theory] - [InlineData("PUT")] - [InlineData("DELETE")] - public async Task VersionedApi_CanNotReachOtherVersionOperationsWithParameters_OnTheSameController_WithNoVersionSpecified(string method) - { - // Arrange - var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Tickets/5"); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - - var body = await response.Content.ReadAsByteArrayAsync(); - Assert.Empty(body); - } - - [Theory] - [InlineData("3")] - [InlineData("4")] - [InlineData("5")] - public async Task VersionedApi_CanUseOrderToDisambiguate_OverlappingVersionRanges(string version) - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Books?version=" + version); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Books", result.Controller); - Assert.Equal("GetBreakingChange", result.Action); - } - - [Theory] - [InlineData("1")] - [InlineData("2")] - [InlineData("6")] - public async Task VersionedApi_OverlappingVersionRanges_FallsBackToLowerOrderAction(string version) - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Books?version=" + version); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Books", result.Controller); - Assert.Equal("Get", result.Action); - } - - - [Theory] - [InlineData("GET", "Get")] - [InlineData("POST", "Post")] - public async Task VersionedApi_CanReachV1Operations_OnTheOriginalController_WithNoVersionSpecified(string method, string action) - { - // Arrange - var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Movies"); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Movies", result.Controller); - Assert.Equal(action, result.Action); - } - - [Theory] - [InlineData("GET", "Get")] - [InlineData("POST", "Post")] - public async Task VersionedApi_CanReachV1Operations_OnTheOriginalController_WithVersionSpecified(string method, string action) - { - // Arrange - var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Movies?version=2"); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Movies", result.Controller); - Assert.Equal(action, result.Action); - } - - [Theory] - [InlineData("GET", "GetById")] - [InlineData("PUT", "Put")] - [InlineData("DELETE", "Delete")] - public async Task VersionedApi_CanReachV1OperationsWithParameters_OnTheOriginalController(string method, string action) - { - // Arrange - var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Movies/5"); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Movies", result.Controller); - Assert.Equal(action, result.Action); - } - - [Theory] - [InlineData("GET", "GetById")] - [InlineData("DELETE", "Delete")] - public async Task VersionedApi_CanReachV1OperationsWithParameters_OnTheOriginalController_WithVersionSpecified(string method, string action) - { - // Arrange - var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Movies/5?version=2"); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Movies", result.Controller); - Assert.Equal(action, result.Action); - } - - [Fact] - public async Task VersionedApi_CanReachOtherVersionOperationsWithParameters_OnTheV2Controller() - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Put, "http://localhost/Movies/5?version=2"); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("MoviesV2", result.Controller); - Assert.Equal("Put", result.Action); - Assert.NotEmpty(result.RouteValues); - } - - [Theory] - [InlineData("v1/Pets")] - [InlineData("v2/Pets")] - public async Task VersionedApi_CanHaveTwoRoutesWithVersionOnTheUrl_OnTheSameAction(string url) - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/" + url); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Pets", result.Controller); - Assert.Equal("Get", result.Action); - } - - [Theory] - [InlineData("v1/Pets/5", "V1")] - [InlineData("v2/Pets/5", "V2")] - public async Task VersionedApi_CanHaveTwoRoutesWithVersionOnTheUrl_OnDifferentActions(string url, string version) - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/" + url); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Pets", result.Controller); - Assert.Equal("Get" + version, result.Action); - } - - [Theory] - [InlineData("v1/Pets", "V1")] - [InlineData("v2/Pets", "V2")] - public async Task VersionedApi_CanHaveTwoRoutesWithVersionOnTheUrl_OnDifferentActions_WithInlineConstraint(string url, string version) - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Post, "http://localhost/" + url); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Pets", result.Controller); - Assert.Equal("Post" + version, result.Action); - } - - [Theory] - [InlineData("Customers/5", "?version=1", "Get")] - [InlineData("Customers/5", "?version=2", "Get")] - [InlineData("Customers/5", "?version=3", "GetV3ToV5")] - [InlineData("Customers/5", "?version=4", "GetV3ToV5")] - [InlineData("Customers/5", "?version=5", "GetV3ToV5")] - public async Task VersionedApi_CanProvideVersioningInformation_UsingPlainActionConstraint(string url, string query, string actionName) - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/" + url + query); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Customers", result.Controller); - Assert.Equal(actionName, result.Action); - } - - [Fact] - public async Task VersionedApi_ConstraintOrder_IsRespected() - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Post, "http://localhost/" + "Customers?version=2"); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Customers", result.Controller); - Assert.Equal("AnyV2OrHigher", result.Action); - } - - [Fact] - public async Task VersionedApi_CanUseConstraintOrder_ToChangeSelectedAction() - { - // Arrange - var message = new HttpRequestMessage(HttpMethod.Delete, "http://localhost/" + "Customers/5?version=2"); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Customers", result.Controller); - Assert.Equal("Delete", result.Action); - } - - [Theory] - [InlineData("1")] - [InlineData("2")] - public async Task VersionedApi_MultipleVersionsUsingAttributeRouting_OnTheSameMethod(string version) - { - // Arrange - var path = "/" + version + "/Vouchers?version=" + version; - var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost" + path); - - // Act - var response = await Client.SendAsync(message); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(body); - - Assert.Equal("Vouchers", result.Controller); - Assert.Equal("GetVouchersMultipleVersions", result.Action); - - var actualUrl = Assert.Single(result.ExpectedUrls); - Assert.Equal(path, actualUrl); - } - - private class RoutingResult - { - public string[] ExpectedUrls { get; set; } - - public string ActualUrl { get; set; } - - public Dictionary RouteValues { get; set; } - - public string RouteName { get; set; } - - public string Action { get; set; } - - public string Controller { get; set; } - - public string Link { get; set; } } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTestsBase.cs b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTestsBase.cs new file mode 100644 index 0000000000..46cfdbe4a8 --- /dev/null +++ b/test/Microsoft.AspNetCore.Mvc.FunctionalTests/VersioningTestsBase.cs @@ -0,0 +1,568 @@ +// 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.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNetCore.Mvc.FunctionalTests +{ + public abstract class VersioningTestsBase : IClassFixture> where TStartup : class + { + protected VersioningTestsBase(MvcTestFixture fixture) + { + var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder); + Client = factory.CreateDefaultClient(); + } + + private static void ConfigureWebHostBuilder(IWebHostBuilder builder) => + builder.UseStartup(); + + public HttpClient Client { get; } + + [Theory] + [InlineData("1")] + [InlineData("2")] + public async Task AttributeRoutedAction_WithVersionedRoutes_IsNotAmbiguous(string version) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/api/Addresses?version=" + version); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Contains("api/addresses", result.ExpectedUrls); + Assert.Equal("Address", result.Controller); + Assert.Equal("GetV" + version, result.Action); + } + + [Theory] + [InlineData("1")] + [InlineData("2")] + public async Task AttributeRoutedAction_WithAmbiguousVersionedRoutes_CanBeDisambiguatedUsingOrder(string version) + { + // Arrange + var query = "?version=" + version; + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/api/Addresses/All" + query); + + // Act + + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Contains("/api/addresses/all?version=" + version, result.ExpectedUrls); + Assert.Equal("Address", result.Controller); + Assert.Equal("GetAllV" + version, result.Action); + } + + [Fact] + public async Task VersionedApi_CanReachV1Operations_OnTheSameController_WithNoVersionSpecified() + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Tickets"); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Tickets", result.Controller); + Assert.Equal("Get", result.Action); + + Assert.DoesNotContain("id", result.RouteValues.Keys); + } + + [Fact] + public async Task VersionedApi_CanReachV1Operations_OnTheSameController_WithVersionSpecified() + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Tickets?version=2"); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Tickets", result.Controller); + Assert.Equal("Get", result.Action); + } + + [Fact] + public async Task VersionedApi_CanReachV1OperationsWithParameters_OnTheSameController() + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Tickets/5"); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Tickets", result.Controller); + Assert.Equal("GetById", result.Action); + } + + [Fact] + public async Task VersionedApi_CanReachV1OperationsWithParameters_OnTheSameController_WithVersionSpecified() + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Tickets/5?version=2"); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Tickets", result.Controller); + Assert.Equal("GetById", result.Action); + Assert.NotEmpty(result.RouteValues); + + Assert.Contains( + new KeyValuePair("id", "5"), + result.RouteValues); + } + + [Theory] + [InlineData("2")] + [InlineData("3")] + [InlineData("4")] + public async Task VersionedApi_CanReachOtherVersionOperations_OnTheSameController(string version) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Tickets?version=" + version); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Tickets", result.Controller); + Assert.Equal("Post", result.Action); + Assert.NotEmpty(result.RouteValues); + + Assert.DoesNotContain( + new KeyValuePair("id", "5"), + result.RouteValues); + } + + [Fact] + public async Task VersionedApi_CanNotReachOtherVersionOperations_OnTheSameController_WithNoVersionSpecified() + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Tickets"); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + var body = await response.Content.ReadAsByteArrayAsync(); + Assert.Empty(body); + } + + [Theory] + [InlineData("PUT", "Put", "2")] + [InlineData("PUT", "Put", "3")] + [InlineData("PUT", "Put", "4")] + [InlineData("DELETE", "Delete", "2")] + [InlineData("DELETE", "Delete", "3")] + [InlineData("DELETE", "Delete", "4")] + public async Task VersionedApi_CanReachOtherVersionOperationsWithParameters_OnTheSameController( + string method, + string action, + string version) + { + // Arrange + var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Tickets/5?version=" + version); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Tickets", result.Controller); + Assert.Equal(action, result.Action); + Assert.NotEmpty(result.RouteValues); + + Assert.Contains( + new KeyValuePair("id", "5"), + result.RouteValues); + } + + [Theory] + [InlineData("PUT")] + [InlineData("DELETE")] + public async Task VersionedApi_CanNotReachOtherVersionOperationsWithParameters_OnTheSameController_WithNoVersionSpecified(string method) + { + // Arrange + var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Tickets/5"); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + var body = await response.Content.ReadAsByteArrayAsync(); + Assert.Empty(body); + } + + [Theory] + [InlineData("3")] + [InlineData("4")] + [InlineData("5")] + public async Task VersionedApi_CanUseOrderToDisambiguate_OverlappingVersionRanges(string version) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Books?version=" + version); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Books", result.Controller); + Assert.Equal("GetBreakingChange", result.Action); + } + + [Theory] + [InlineData("1")] + [InlineData("2")] + [InlineData("6")] + public async Task VersionedApi_OverlappingVersionRanges_FallsBackToLowerOrderAction(string version) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Books?version=" + version); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Books", result.Controller); + Assert.Equal("Get", result.Action); + } + + + [Theory] + [InlineData("GET", "Get")] + [InlineData("POST", "Post")] + public async Task VersionedApi_CanReachV1Operations_OnTheOriginalController_WithNoVersionSpecified(string method, string action) + { + // Arrange + var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Movies"); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Movies", result.Controller); + Assert.Equal(action, result.Action); + } + + [Theory] + [InlineData("GET", "Get")] + [InlineData("POST", "Post")] + public async Task VersionedApi_CanReachV1Operations_OnTheOriginalController_WithVersionSpecified(string method, string action) + { + // Arrange + var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Movies?version=2"); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Movies", result.Controller); + Assert.Equal(action, result.Action); + } + + [Theory] + [InlineData("GET", "GetById")] + [InlineData("PUT", "Put")] + [InlineData("DELETE", "Delete")] + public async Task VersionedApi_CanReachV1OperationsWithParameters_OnTheOriginalController(string method, string action) + { + // Arrange + var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Movies/5"); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Movies", result.Controller); + Assert.Equal(action, result.Action); + } + + [Theory] + [InlineData("GET", "GetById")] + [InlineData("DELETE", "Delete")] + public async Task VersionedApi_CanReachV1OperationsWithParameters_OnTheOriginalController_WithVersionSpecified(string method, string action) + { + // Arrange + var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Movies/5?version=2"); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Movies", result.Controller); + Assert.Equal(action, result.Action); + } + + [Fact] + public async Task VersionedApi_CanReachOtherVersionOperationsWithParameters_OnTheV2Controller() + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Put, "http://localhost/Movies/5?version=2"); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("MoviesV2", result.Controller); + Assert.Equal("Put", result.Action); + Assert.NotEmpty(result.RouteValues); + } + + [Theory] + [InlineData("v1/Pets")] + [InlineData("v2/Pets")] + public async Task VersionedApi_CanHaveTwoRoutesWithVersionOnTheUrl_OnTheSameAction(string url) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/" + url); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Pets", result.Controller); + Assert.Equal("Get", result.Action); + } + + [Theory] + [InlineData("v1/Pets/5", "V1")] + [InlineData("v2/Pets/5", "V2")] + public async Task VersionedApi_CanHaveTwoRoutesWithVersionOnTheUrl_OnDifferentActions(string url, string version) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/" + url); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Pets", result.Controller); + Assert.Equal("Get" + version, result.Action); + } + + [Theory] + [InlineData("v1/Pets", "V1")] + [InlineData("v2/Pets", "V2")] + public async Task VersionedApi_CanHaveTwoRoutesWithVersionOnTheUrl_OnDifferentActions_WithInlineConstraint(string url, string version) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Post, "http://localhost/" + url); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Pets", result.Controller); + Assert.Equal("Post" + version, result.Action); + } + + [Theory] + [InlineData("Customers/5", "?version=1", "Get")] + [InlineData("Customers/5", "?version=2", "Get")] + [InlineData("Customers/5", "?version=3", "GetV3ToV5")] + [InlineData("Customers/5", "?version=4", "GetV3ToV5")] + [InlineData("Customers/5", "?version=5", "GetV3ToV5")] + public async Task VersionedApi_CanProvideVersioningInformation_UsingPlainActionConstraint(string url, string query, string actionName) + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/" + url + query); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Customers", result.Controller); + Assert.Equal(actionName, result.Action); + } + + [Fact] + public async Task VersionedApi_ConstraintOrder_IsRespected() + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Post, "http://localhost/" + "Customers?version=2"); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Customers", result.Controller); + Assert.Equal("AnyV2OrHigher", result.Action); + } + + [Fact] + public async Task VersionedApi_CanUseConstraintOrder_ToChangeSelectedAction() + { + // Arrange + var message = new HttpRequestMessage(HttpMethod.Delete, "http://localhost/" + "Customers/5?version=2"); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Customers", result.Controller); + Assert.Equal("Delete", result.Action); + } + + [Theory] + [InlineData("1")] + [InlineData("2")] + public async Task VersionedApi_MultipleVersionsUsingAttributeRouting_OnTheSameMethod(string version) + { + // Arrange + var path = "/" + version + "/Vouchers?version=" + version; + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost" + path); + + // Act + var response = await Client.SendAsync(message); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(body); + + Assert.Equal("Vouchers", result.Controller); + Assert.Equal("GetVouchersMultipleVersions", result.Action); + + var actualUrl = Assert.Single(result.ExpectedUrls); + Assert.Equal(path, actualUrl); + } + + private class RoutingResult + { + public string[] ExpectedUrls { get; set; } + + public string ActualUrl { get; set; } + + public Dictionary RouteValues { get; set; } + + public string RouteName { get; set; } + + public string Action { get; set; } + + public string Controller { get; set; } + + public string Link { get; set; } + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/Program.cs b/test/WebSites/VersioningWebSite/Program.cs new file mode 100644 index 0000000000..7dce10d19d --- /dev/null +++ b/test/WebSites/VersioningWebSite/Program.cs @@ -0,0 +1,26 @@ +// 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.IO; +using Microsoft.AspNetCore.Hosting; + +namespace VersioningWebSite +{ + public class Program + { + public static void Main(string[] args) + { + var host = CreateWebHostBuilder(args) + .Build(); + + host.Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + new WebHostBuilder() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup() + .UseKestrel() + .UseIISIntegration(); + } +} diff --git a/test/WebSites/VersioningWebSite/Startup.cs b/test/WebSites/VersioningWebSite/Startup.cs index cdf090a0a6..5373436c06 100644 --- a/test/WebSites/VersioningWebSite/Startup.cs +++ b/test/WebSites/VersioningWebSite/Startup.cs @@ -24,21 +24,5 @@ namespace VersioningWebSite { app.UseMvcWithDefaultRoute(); } - - public static void Main(string[] args) - { - var host = CreateWebHostBuilder(args) - .Build(); - - host.Run(); - } - - public static IWebHostBuilder CreateWebHostBuilder(string[] args) => - new WebHostBuilder() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseStartup() - .UseKestrel() - .UseIISIntegration(); } -} - +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/StartupWithDispatching.cs b/test/WebSites/VersioningWebSite/StartupWithDispatching.cs new file mode 100644 index 0000000000..164f86550b --- /dev/null +++ b/test/WebSites/VersioningWebSite/StartupWithDispatching.cs @@ -0,0 +1,37 @@ +// 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.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.DependencyInjection; + +namespace VersioningWebSite +{ + public class StartupWithDispatching + { + public void ConfigureServices(IServiceCollection services) + { + services.AddDispatcher(); + + // Add MVC services to the services container + services.AddMvc(); + + services.AddScoped(); + services.AddSingleton(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseDispatcher(); + + app.UseMvcWithEndpoint(endpoints => + { + endpoints.MapEndpoint( + name: "default", + template: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/VersionAttribute.cs b/test/WebSites/VersioningWebSite/VersionAttribute.cs index e1658fd8b6..827aaef23c 100644 --- a/test/WebSites/VersioningWebSite/VersionAttribute.cs +++ b/test/WebSites/VersioningWebSite/VersionAttribute.cs @@ -3,10 +3,11 @@ using System; using Microsoft.AspNetCore.Mvc.ActionConstraints; +using Microsoft.AspNetCore.Routing.EndpointConstraints; namespace VersioningWebSite { - public class VersionAttribute : Attribute, IActionConstraintFactory + public class VersionAttribute : Attribute, IActionConstraintFactory, IEndpointConstraintFactory { private int? _maxVersion; private int? _minVersion; @@ -32,7 +33,12 @@ namespace VersioningWebSite public bool IsReusable => true; - public IActionConstraint CreateInstance(IServiceProvider services) + IActionConstraint IActionConstraintFactory.CreateInstance(IServiceProvider services) + { + return new VersionRangeValidator(_minVersion, _maxVersion) { Order = _order ?? 0 }; + } + + IEndpointConstraint IEndpointConstraintFactory.CreateInstance(IServiceProvider services) { return new VersionRangeValidator(_minVersion, _maxVersion) { Order = _order ?? 0 }; } diff --git a/test/WebSites/VersioningWebSite/VersionDeleteAttribute.cs b/test/WebSites/VersioningWebSite/VersionDeleteAttribute.cs index 822e281547..9354c56b23 100644 --- a/test/WebSites/VersioningWebSite/VersionDeleteAttribute.cs +++ b/test/WebSites/VersioningWebSite/VersionDeleteAttribute.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.Routing; namespace VersioningWebSite { - public class VersionDeleteAttribute : VersionRoute, IActionHttpMethodProvider + public class VersionDeleteAttribute : VersionRouteAttribute, IActionHttpMethodProvider { public VersionDeleteAttribute(string template) : base(template) diff --git a/test/WebSites/VersioningWebSite/VersionGetAttribute.cs b/test/WebSites/VersioningWebSite/VersionGetAttribute.cs index 4e53537130..b809c585f9 100644 --- a/test/WebSites/VersioningWebSite/VersionGetAttribute.cs +++ b/test/WebSites/VersioningWebSite/VersionGetAttribute.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.Routing; namespace VersioningWebSite { - public class VersionGetAttribute : VersionRoute, IActionHttpMethodProvider + public class VersionGetAttribute : VersionRouteAttribute, IActionHttpMethodProvider { public VersionGetAttribute(string template) : base(template) diff --git a/test/WebSites/VersioningWebSite/VersionPostAttribute.cs b/test/WebSites/VersioningWebSite/VersionPostAttribute.cs index 18db5eb210..9738cd946d 100644 --- a/test/WebSites/VersioningWebSite/VersionPostAttribute.cs +++ b/test/WebSites/VersioningWebSite/VersionPostAttribute.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.Routing; namespace VersioningWebSite { - public class VersionPostAttribute : VersionRoute, IActionHttpMethodProvider + public class VersionPostAttribute : VersionRouteAttribute, IActionHttpMethodProvider { public VersionPostAttribute(string template) : base(template) diff --git a/test/WebSites/VersioningWebSite/VersionPutAttribute.cs b/test/WebSites/VersioningWebSite/VersionPutAttribute.cs index 49d4dab380..ad1dafc176 100644 --- a/test/WebSites/VersioningWebSite/VersionPutAttribute.cs +++ b/test/WebSites/VersioningWebSite/VersionPutAttribute.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Mvc.Routing; namespace VersioningWebSite { - public class VersionPutAttribute : VersionRoute, IActionHttpMethodProvider + public class VersionPutAttribute : VersionRouteAttribute, IActionHttpMethodProvider { public VersionPutAttribute(string template) : base(template) diff --git a/test/WebSites/VersioningWebSite/VersionRangeValidator.cs b/test/WebSites/VersioningWebSite/VersionRangeValidator.cs index 14c8d37662..ec8d244de3 100644 --- a/test/WebSites/VersioningWebSite/VersionRangeValidator.cs +++ b/test/WebSites/VersioningWebSite/VersionRangeValidator.cs @@ -3,10 +3,11 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ActionConstraints; +using Microsoft.AspNetCore.Routing.EndpointConstraints; namespace VersioningWebSite { - public class VersionRangeValidator : IActionConstraint + public class VersionRangeValidator : IActionConstraint, IEndpointConstraint { private readonly int? _minVersion; private readonly int? _maxVersion; @@ -25,9 +26,19 @@ namespace VersioningWebSite } public bool Accept(ActionConstraintContext context) + { + return ProcessRequest(context.RouteContext.HttpContext.Request); + } + + public bool Accept(EndpointConstraintContext context) + { + return ProcessRequest(context.HttpContext.Request); + } + + private bool ProcessRequest(HttpRequest request) { int version; - if (int.TryParse(GetVersion(context.RouteContext.HttpContext.Request), out version)) + if (int.TryParse(GetVersion(request), out version)) { return (_minVersion == null || _minVersion <= version) && (_maxVersion == null || _maxVersion >= version); diff --git a/test/WebSites/VersioningWebSite/VersionRoute.cs b/test/WebSites/VersioningWebSite/VersionRouteAttribute.cs similarity index 84% rename from test/WebSites/VersioningWebSite/VersionRoute.cs rename to test/WebSites/VersioningWebSite/VersionRouteAttribute.cs index 3b6f61c429..288a488790 100644 --- a/test/WebSites/VersioningWebSite/VersionRoute.cs +++ b/test/WebSites/VersioningWebSite/VersionRouteAttribute.cs @@ -5,12 +5,13 @@ using System; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ActionConstraints; +using Microsoft.AspNetCore.Routing.EndpointConstraints; namespace VersioningWebSite { - public class VersionRoute : RouteAttribute, IActionConstraintFactory + public class VersionRouteAttribute : RouteAttribute, IActionConstraintFactory, IEndpointConstraintFactory { - private readonly IActionConstraint _constraint; + private readonly IActionConstraint _actionConstraint; // 5 // [5] @@ -28,12 +29,12 @@ namespace VersioningWebSite public bool IsReusable => true; - public VersionRoute(string template) + public VersionRouteAttribute(string template) : base(template) { } - public VersionRoute(string template, string versionRange) + public VersionRouteAttribute(string template, string versionRange) : base(template) { var constraint = CreateVersionConstraint(versionRange); @@ -44,7 +45,7 @@ namespace VersioningWebSite throw new ArgumentException(message, "versionRange"); } - _constraint = constraint; + _actionConstraint = constraint; } private static IActionConstraint CreateVersionConstraint(string versionRange) @@ -122,9 +123,14 @@ namespace VersioningWebSite } } - public IActionConstraint CreateInstance(IServiceProvider services) + IActionConstraint IActionConstraintFactory.CreateInstance(IServiceProvider services) { - return _constraint; + return _actionConstraint; + } + + IEndpointConstraint IEndpointConstraintFactory.CreateInstance(IServiceProvider services) + { + return (IEndpointConstraint)_actionConstraint; } } } \ No newline at end of file