Versioning with endpoint constraint (#8098)

This commit is contained in:
James Newton-King 2018-07-17 16:37:45 +12:00 committed by GitHub
parent b62499e02c
commit 42218d5fb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 715 additions and 597 deletions

View File

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

View File

@ -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<VersioningWebSite.StartupWithDispatching>
{
public VersioningDispatchingTests(MvcTestFixture<VersioningWebSite.StartupWithDispatching> fixture)
: base(fixture)
{
}
}
}

View File

@ -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<MvcTestFixture<VersioningWebSite.Startup>>
public class VersioningTests : VersioningTestsBase<VersioningWebSite.Startup>
{
public VersioningTests(MvcTestFixture<VersioningWebSite.Startup> 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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(body);
Assert.Equal("Tickets", result.Controller);
Assert.Equal("GetById", result.Action);
Assert.NotEmpty(result.RouteValues);
Assert.Contains(
new KeyValuePair<string, object>("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<RoutingResult>(body);
Assert.Equal("Tickets", result.Controller);
Assert.Equal("Post", result.Action);
Assert.NotEmpty(result.RouteValues);
Assert.DoesNotContain(
new KeyValuePair<string, object>("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<RoutingResult>(body);
Assert.Equal("Tickets", result.Controller);
Assert.Equal(action, result.Action);
Assert.NotEmpty(result.RouteValues);
Assert.Contains(
new KeyValuePair<string, object>("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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<string, object> RouteValues { get; set; }
public string RouteName { get; set; }
public string Action { get; set; }
public string Controller { get; set; }
public string Link { get; set; }
}
}
}

View File

@ -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<TStartup> : IClassFixture<MvcTestFixture<TStartup>> where TStartup : class
{
protected VersioningTestsBase(MvcTestFixture<TStartup> fixture)
{
var factory = fixture.Factories.FirstOrDefault() ?? fixture.WithWebHostBuilder(ConfigureWebHostBuilder);
Client = factory.CreateDefaultClient();
}
private static void ConfigureWebHostBuilder(IWebHostBuilder builder) =>
builder.UseStartup<TStartup>();
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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(body);
Assert.Equal("Tickets", result.Controller);
Assert.Equal("GetById", result.Action);
Assert.NotEmpty(result.RouteValues);
Assert.Contains(
new KeyValuePair<string, object>("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<RoutingResult>(body);
Assert.Equal("Tickets", result.Controller);
Assert.Equal("Post", result.Action);
Assert.NotEmpty(result.RouteValues);
Assert.DoesNotContain(
new KeyValuePair<string, object>("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<RoutingResult>(body);
Assert.Equal("Tickets", result.Controller);
Assert.Equal(action, result.Action);
Assert.NotEmpty(result.RouteValues);
Assert.Contains(
new KeyValuePair<string, object>("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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<RoutingResult>(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<string, object> RouteValues { get; set; }
public string RouteName { get; set; }
public string Action { get; set; }
public string Controller { get; set; }
public string Link { get; set; }
}
}
}

View File

@ -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<Startup>()
.UseKestrel()
.UseIISIntegration();
}
}

View File

@ -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<Startup>()
.UseKestrel()
.UseIISIntegration();
}
}
}

View File

@ -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<TestResponseGenerator>();
services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();
}
public void Configure(IApplicationBuilder app)
{
app.UseDispatcher();
app.UseMvcWithEndpoint(endpoints =>
{
endpoints.MapEndpoint(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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