diff --git a/Mvc.sln b/Mvc.sln index 13b6839de7..cf638fb0a9 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -81,6 +81,7 @@ EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "UrlHelperWebSite", "test\WebSites\UrlHelperWebSite\UrlHelperWebSite.kproj", "{A192E504-2881-41DC-90D1-B7F1DD1134E8}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ApiExplorerWebSite", "test\WebSites\ApiExplorerWebSite\ApiExplorerWebSite.kproj", "{61061528-071E-424E-965A-07BCC2F02672}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "VersioningWebSite", "test\WebSites\VersioningWebSite\VersioningWebSite.kproj", "{C6304029-78C8-4604-99BE-2078DCA1DD36}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ReflectedModelWebSite", "test\WebSites\ReflectedModelWebSite\ReflectedModelWebSite.kproj", "{C2EF54F8-8886-4260-A322-44F76245F95D}" EndProject @@ -438,6 +439,16 @@ Global {61061528-071E-424E-965A-07BCC2F02672}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {61061528-071E-424E-965A-07BCC2F02672}.Release|Mixed Platforms.Build.0 = Release|Any CPU {61061528-071E-424E-965A-07BCC2F02672}.Release|x86.ActiveCfg = Release|Any CPU + {C6304029-78C8-4604-99BE-2078DCA1DD36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6304029-78C8-4604-99BE-2078DCA1DD36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6304029-78C8-4604-99BE-2078DCA1DD36}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {C6304029-78C8-4604-99BE-2078DCA1DD36}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {C6304029-78C8-4604-99BE-2078DCA1DD36}.Debug|x86.ActiveCfg = Debug|Any CPU + {C6304029-78C8-4604-99BE-2078DCA1DD36}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6304029-78C8-4604-99BE-2078DCA1DD36}.Release|Any CPU.Build.0 = Release|Any CPU + {C6304029-78C8-4604-99BE-2078DCA1DD36}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {C6304029-78C8-4604-99BE-2078DCA1DD36}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {C6304029-78C8-4604-99BE-2078DCA1DD36}.Release|x86.ActiveCfg = Release|Any CPU {C2EF54F8-8886-4260-A322-44F76245F95D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C2EF54F8-8886-4260-A322-44F76245F95D}.Debug|Any CPU.Build.0 = Debug|Any CPU {C2EF54F8-8886-4260-A322-44F76245F95D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -498,6 +509,7 @@ Global {A192E504-2881-41DC-90D1-B7F1DD1134E8} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {0EF9860B-10D7-452F-B0F4-A405B88BEBB3} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {61061528-071E-424E-965A-07BCC2F02672} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} + {C6304029-78C8-4604-99BE-2078DCA1DD36} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {C2EF54F8-8886-4260-A322-44F76245F95D} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} {2B2B9876-903C-4065-8D62-2EE832BBA106} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} EndGlobalSection diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/VersioningTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/VersioningTests.cs new file mode 100644 index 0000000000..8e844de594 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/VersioningTests.cs @@ -0,0 +1,613 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.TestHost; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class VersioningTests + { + private readonly IServiceProvider _services = TestHelper.CreateServices("VersioningWebSite"); + private readonly Action _app = new VersioningWebSite.Startup().Configure; + + [Theory] + [InlineData("1")] + [InlineData("2")] + public async Task AttributeRoutedAction_WithVersionedRoutes_IsNotAmbiguous(string version) + { + // Arrange + var server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/api/Addresses?version=" + version); + 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 + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + 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 + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Tickets"); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Tickets?version=2"); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Tickets/5"); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Tickets/5?version=2"); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Tickets?version=" + version); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Tickets"); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Tickets/5?version=" + version); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Tickets/5"); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Books?version=" + version); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Books?version=" + version); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Movies"); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Movies?version=2"); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Movies/5"); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Movies/5?version=2"); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(HttpMethod.Put, "http://localhost/Movies/5?version=2"); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/" + url); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/" + url); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(HttpMethod.Post, "http://localhost/" + url); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/" + url + query); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(HttpMethod.Post, "http://localhost/" + "Customers?version=2"); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + + // Act + var message = new HttpRequestMessage(HttpMethod.Delete, "http://localhost/" + "Customers/5?version=2"); + 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 server = TestServer.Create(_services, _app); + var client = server.CreateClient(); + var path = "/" + version + "/Vouchers?version=" + version; + + // Act + var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost" + path); + 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); + } + + // See TestResponseGenerator in RoutingWebSite for the code that generates this data. + 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.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index 7d1ad1ecef..ff9918b64e 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -14,6 +14,7 @@ "FiltersWebSite": "", "FormatterWebSite": "", "InlineConstraintsWebSite": "", + "VersioningWebSite": "", "Microsoft.AspNet.TestHost": "1.0.0-*", "Microsoft.AspNet.PipelineCore": "1.0.0-*", "Microsoft.AspNet.Mvc.TestConfiguration": "", diff --git a/test/WebSites/VersioningWebSite/Controllers/AddressController.cs b/test/WebSites/VersioningWebSite/Controllers/AddressController.cs new file mode 100644 index 0000000000..fe94515af8 --- /dev/null +++ b/test/WebSites/VersioningWebSite/Controllers/AddressController.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace VersioningWebSite +{ + // Scenario + // Same template disjoint version sets. + // Same template overlapping version sets disambiguated by order. + public class AddressController : Controller + { + private TestResponseGenerator _generator; + + public AddressController(TestResponseGenerator generator) + { + _generator = generator; + } + + [VersionRoute("api/Addresses", versionRange: "[1]")] + public IActionResult GetV1() + { + return _generator.Generate("api/addresses"); + } + + [VersionRoute("api/Addresses", versionRange: "[2]")] + public IActionResult GetV2() + { + return _generator.Generate("api/addresses"); + } + + [VersionRoute("api/addresses/all", versionRange: "[1]")] + public IActionResult GetAllV1(string version) + { + return _generator.Generate( + Url.Action("GetAllV1", + new { version = version }), Url.RouteUrl(new { version = version })); + } + + [VersionRoute("api/addresses/all", versionRange: "[1-2]", Order = 1)] + public IActionResult GetAllV2(string version) + { + return _generator.Generate( + Url.Action("GetAllV2", new { version = version }), + Url.RouteUrl(new { version = version })); + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/Controllers/BooksController.cs b/test/WebSites/VersioningWebSite/Controllers/BooksController.cs new file mode 100644 index 0000000000..293aebc62e --- /dev/null +++ b/test/WebSites/VersioningWebSite/Controllers/BooksController.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace VersioningWebSite +{ + // Scenario + // Actions define version ranges and some + // versions overlap. + public class BooksController : Controller + { + private readonly TestResponseGenerator _generator; + + public BooksController(TestResponseGenerator generator) + { + _generator = generator; + } + + [VersionGet("Books", versionRange: "[1-6]", Order = 1)] + public IActionResult Get() + { + return _generator.Generate(); + } + + [VersionGet("Books", versionRange: "[3-5]", Order = 0)] + public IActionResult GetBreakingChange() + { + return _generator.Generate(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/Controllers/CustomersController.cs b/test/WebSites/VersioningWebSite/Controllers/CustomersController.cs new file mode 100644 index 0000000000..28279bba42 --- /dev/null +++ b/test/WebSites/VersioningWebSite/Controllers/CustomersController.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Mvc; + +namespace VersioningWebSite +{ + // Scenario: + // Version constraint provided separately from the attribute route. + [Route("Customers")] + public class CustomersController + { + private readonly TestResponseGenerator _generator; + + public CustomersController(TestResponseGenerator generator) + { + _generator = generator; + } + + [HttpGet("{id}")] + [Version(MaxVersion = 2)] + public IActionResult Get(int id) + { + return _generator.Generate(); + } + + [HttpGet("{id}")] + [Version(MinVersion = 3, MaxVersion = 5)] + public IActionResult GetV3ToV5(int id) + { + return _generator.Generate(); + } + + [Version(MinVersion = 2)] + public IActionResult AnyV2OrHigher() + { + return _generator.Generate(); + } + + [HttpPost] + public IActionResult Post() + { + return _generator.Generate(); + } + + [Version(MinVersion = 2, Order = int.MaxValue)] + [Route("{id}")] + public IActionResult AnyV2OrHigherWithId() + { + return _generator.Generate(); + } + + [HttpDelete("{id}")] + public IActionResult Delete() + { + return _generator.Generate(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/Controllers/ItemsController.cs b/test/WebSites/VersioningWebSite/Controllers/ItemsController.cs new file mode 100644 index 0000000000..de3405de54 --- /dev/null +++ b/test/WebSites/VersioningWebSite/Controllers/ItemsController.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace VersioningWebSite +{ + // Scenario: + // Controller without any kind of specific version handling. + // New versions of the API will be exposed in a different controller. + [Route("Items/{id}")] + public class ItemsController : Controller + { + private readonly TestResponseGenerator _generator; + + public ItemsController(TestResponseGenerator generator) + { + _generator = generator; + } + + [HttpGet("/Items")] + public IActionResult Get() + { + return _generator.Generate(); + } + + [HttpGet] + public IActionResult Get(int id) + { + return _generator.Generate(); + } + + [HttpPost("/Items")] + public IActionResult Post() + { + return _generator.Generate(); + } + + [HttpPut] + public IActionResult Put(int id) + { + return _generator.Generate(); + } + + [HttpDelete] + public IActionResult Delete(int id) + { + return _generator.Generate(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/Controllers/ItemsV2Controller.cs b/test/WebSites/VersioningWebSite/Controllers/ItemsV2Controller.cs new file mode 100644 index 0000000000..96889efc39 --- /dev/null +++ b/test/WebSites/VersioningWebSite/Controllers/ItemsV2Controller.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace VersioningWebSite +{ + // This is the version 2 for an API. The version 1 is unconstrained + [VersionRoute("Items/{id}", versionRange: "2")] + public class ItemsV2Controller : Controller + { + private readonly TestResponseGenerator _generator; + + public ItemsV2Controller(TestResponseGenerator generator) + { + _generator = generator; + } + + [VersionGet("/Items", versionRange: "2")] + public IActionResult Get() + { + return _generator.Generate(); + } + + [HttpGet] + public IActionResult Get(int id) + { + return _generator.Generate(); + } + + [VersionPost("/Items", versionRange: "2")] + public IActionResult Post() + { + return _generator.Generate(); + } + + [HttpPut] + public IActionResult Put(int id) + { + return _generator.Generate(); + } + + [HttpDelete] + public IActionResult Delete(int id) + { + return _generator.Generate(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/Controllers/MoviesController.cs b/test/WebSites/VersioningWebSite/Controllers/MoviesController.cs new file mode 100644 index 0000000000..009128cc7b --- /dev/null +++ b/test/WebSites/VersioningWebSite/Controllers/MoviesController.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace VersioningWebSite +{ + // Scenario: + // This is a controller for the V1 (unconstrained) version + // of the service. The v2 version will be in a controller + // that contains only actions for which the api surface + // changes. Actions for which V1 and V2 have the same + // API surface. + public class MoviesController : Controller + { + private readonly TestResponseGenerator _generator; + + public MoviesController(TestResponseGenerator generator) + { + _generator = generator; + } + + [HttpGet("Movies")] + public IActionResult Get() + { + return _generator.Generate(); + } + + [HttpGet("Movies/{id}")] + public IActionResult GetById(int id) + { + return _generator.Generate(); + } + + [HttpPost("/Movies")] + public IActionResult Post() + { + return _generator.Generate(); + } + + [HttpPut("Movies/{id}")] + public IActionResult Put(int id) + { + return _generator.Generate(); + } + + [HttpDelete("Movies/{id}")] + public IActionResult Delete(int id) + { + return _generator.Generate(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/Controllers/MoviesV2Controller.cs b/test/WebSites/VersioningWebSite/Controllers/MoviesV2Controller.cs new file mode 100644 index 0000000000..72dbe16dbf --- /dev/null +++ b/test/WebSites/VersioningWebSite/Controllers/MoviesV2Controller.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace VersioningWebSite +{ + public class MoviesV2Controller : Controller + { + private readonly TestResponseGenerator _generator; + + public MoviesV2Controller(TestResponseGenerator generator) + { + _generator = generator; + } + + [VersionPut("Movies/{id}", versionRange: "2")] + public IActionResult Put(int id) + { + return _generator.Generate(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/Controllers/PetsController.cs b/test/WebSites/VersioningWebSite/Controllers/PetsController.cs new file mode 100644 index 0000000000..9267a78460 --- /dev/null +++ b/test/WebSites/VersioningWebSite/Controllers/PetsController.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace VersioningWebSite +{ + // Scenario + // The version is in the path of the URL + public class PetsController : Controller + { + private readonly TestResponseGenerator _generator; + + public PetsController(TestResponseGenerator generator) + { + _generator = generator; + } + + [HttpGet("v1/Pets")] + [HttpGet("v2/Pets")] + public IActionResult Get() + { + return _generator.Generate(); + } + + [HttpGet("v1/Pets/{id}")] + public IActionResult GetV1(int id) + { + return _generator.Generate(); + } + + [HttpGet("v2/Pets/{id}")] + public IActionResult GetV2(int id) + { + return _generator.Generate(); + } + + [HttpPost("v1/Pets")] + public IActionResult PostV1() + { + return _generator.Generate(); + } + + [HttpPost("v{version:Min(2)}/Pets")] + public IActionResult PostV2() + { + return _generator.Generate(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/Controllers/TicketsController.cs b/test/WebSites/VersioningWebSite/Controllers/TicketsController.cs new file mode 100644 index 0000000000..9128cf3472 --- /dev/null +++ b/test/WebSites/VersioningWebSite/Controllers/TicketsController.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc; + +namespace VersioningWebSite +{ + // Scenario + // V1 of the API is read-only and unconstrained + // V2 of the API is constrained + public class TicketsController : Controller + { + private readonly TestResponseGenerator _generator; + + public TicketsController(TestResponseGenerator generator) + { + _generator = generator; + } + + [HttpGet("/Tickets")] + public IActionResult Get() + { + return _generator.Generate(); + } + + [HttpGet("/Tickets/{id}")] + public IActionResult GetById(int id) + { + return _generator.Generate(); + } + + [VersionPost("/Tickets", versionRange: "2")] + public IActionResult Post() + { + return _generator.Generate(); + } + + [VersionPut("/Tickets/{id}", versionRange: "2")] + public IActionResult Put(int id) + { + return _generator.Generate(); + } + + [VersionDelete("/Tickets/{id}", versionRange: "2")] + public IActionResult Delete(int id) + { + return _generator.Generate(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/Controllers/VouchersController.cs b/test/WebSites/VersioningWebSite/Controllers/VouchersController.cs new file mode 100644 index 0000000000..73b2c75d0d --- /dev/null +++ b/test/WebSites/VersioningWebSite/Controllers/VouchersController.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNet.Mvc; +using System; + +namespace VersioningWebSite +{ + public class VouchersController : Controller + { + private readonly TestResponseGenerator _generator; + + public VouchersController(TestResponseGenerator generator) + { + _generator = generator; + } + + // We are verifying that the right constraint gets applied along the route. + [VersionGet("1/Vouchers", versionRange: "[1]", Name = "V1")] + [VersionGet("2/Vouchers", versionRange: "[2]", Name = "V2")] + public IActionResult GetVouchersMultipleVersions(string version) + { + return _generator.Generate(Url.RouteUrl("V" + version, new { version = version })); + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/Startup.cs b/test/WebSites/VersioningWebSite/Startup.cs new file mode 100644 index 0000000000..a19a7ee669 --- /dev/null +++ b/test/WebSites/VersioningWebSite/Startup.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Builder; +using Microsoft.Framework.DependencyInjection; + +namespace VersioningWebSite +{ + public class Startup + { + public void Configure(IApplicationBuilder app) + { + var configuration = app.GetTestConfiguration(); + + app.UseServices(services => + { + services.AddMvc(configuration); + + services.AddScoped(); + }); + + app.UseMvc(); + } + } +} diff --git a/test/WebSites/VersioningWebSite/TestResponseGenerator.cs b/test/WebSites/VersioningWebSite/TestResponseGenerator.cs new file mode 100644 index 0000000000..4821b71ee1 --- /dev/null +++ b/test/WebSites/VersioningWebSite/TestResponseGenerator.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Mvc; +using Microsoft.Framework.DependencyInjection; + +namespace VersioningWebSite +{ + // Generates a response based on the expected URL and action context + public class TestResponseGenerator + { + private readonly ActionContext _actionContext; + + public TestResponseGenerator(IContextAccessor contextAccessor) + { + _actionContext = contextAccessor.Value; + if (_actionContext == null) + { + throw new InvalidOperationException("ActionContext should not be null here."); + } + } + + public JsonResult Generate(params string[] expectedUrls) + { + var link = (string)null; + var query = _actionContext.HttpContext.Request.Query; + if (query.ContainsKey("link")) + { + var values = query + .Where(kvp => kvp.Key != "link" && kvp.Key != "link_action" && kvp.Key != "link_controller") + .ToDictionary(kvp => kvp.Key.Substring("link_".Length), kvp => (object)kvp.Value[0]); + + var urlHelper = _actionContext.HttpContext.RequestServices.GetService(); + link = urlHelper.Action(query["link_action"], query["link_controller"], values); + } + + var attributeRoutingInfo = _actionContext.ActionDescriptor.AttributeRouteInfo; + + return new JsonResult(new + { + expectedUrls = expectedUrls, + actualUrl = _actionContext.HttpContext.Request.Path.Value, + routeName = attributeRoutingInfo == null ? null : attributeRoutingInfo.Name, + routeValues = new Dictionary(_actionContext.RouteData.Values), + + action = _actionContext.ActionDescriptor.Name, + controller = ((ReflectedActionDescriptor)_actionContext.ActionDescriptor).ControllerDescriptor.Name, + + link, + }); + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/VersionAttribute.cs b/test/WebSites/VersioningWebSite/VersionAttribute.cs new file mode 100644 index 0000000000..0fb413db11 --- /dev/null +++ b/test/WebSites/VersioningWebSite/VersionAttribute.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Mvc; + +namespace VersioningWebSite +{ + public class VersionAttribute : Attribute, IActionConstraintFactory + { + private int? _maxVersion; + private int? _minVersion; + private int? _order; + + public int MinVersion + { + get { return _minVersion ?? -1; } + set { _minVersion = value; } + } + + public int MaxVersion + { + get { return _maxVersion ?? -1; } + set { _maxVersion = value; } + } + + public int Order + { + get { return _order ?? -1; } + set { _order = value; } + } + + public IActionConstraint CreateInstance(IServiceProvider services) + { + return new VersionRangeValidator(_minVersion, _maxVersion) { Order = _order ?? 0 }; + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/VersionDeleteAttribute.cs b/test/WebSites/VersioningWebSite/VersionDeleteAttribute.cs new file mode 100644 index 0000000000..61d8343c66 --- /dev/null +++ b/test/WebSites/VersioningWebSite/VersionDeleteAttribute.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.Mvc; + +namespace VersioningWebSite +{ + public class VersionDeleteAttribute : VersionRoute, IActionHttpMethodProvider + { + public VersionDeleteAttribute(string template) + : base(template) + { + } + + public VersionDeleteAttribute(string template, string versionRange) + : base(template, versionRange) + { + } + + private readonly IEnumerable _httpMethods = new[] { "DELETE" }; + + public IEnumerable HttpMethods + { + get + { + return _httpMethods; + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/VersionGetAttribute.cs b/test/WebSites/VersioningWebSite/VersionGetAttribute.cs new file mode 100644 index 0000000000..91aa8cf400 --- /dev/null +++ b/test/WebSites/VersioningWebSite/VersionGetAttribute.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.Mvc; + +namespace VersioningWebSite +{ + public class VersionGetAttribute : VersionRoute, IActionHttpMethodProvider + { + public VersionGetAttribute(string template) + : base(template) + { + } + + public VersionGetAttribute(string template, string versionRange) + : base(template, versionRange) + { + } + + private readonly IEnumerable _httpMethods = new[] { "GET" }; + + public IEnumerable HttpMethods + { + get + { + return _httpMethods; + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/VersionPostAttribute.cs b/test/WebSites/VersioningWebSite/VersionPostAttribute.cs new file mode 100644 index 0000000000..324c18b075 --- /dev/null +++ b/test/WebSites/VersioningWebSite/VersionPostAttribute.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.Mvc; + +namespace VersioningWebSite +{ + public class VersionPostAttribute : VersionRoute, IActionHttpMethodProvider + { + public VersionPostAttribute(string template) + : base(template) + { + } + + public VersionPostAttribute(string template, string versionRange) + : base(template, versionRange) + { + } + + private readonly IEnumerable _httpMethods = new[] { "POST" }; + + public IEnumerable HttpMethods + { + get + { + return _httpMethods; + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/VersionPutAttribute.cs b/test/WebSites/VersioningWebSite/VersionPutAttribute.cs new file mode 100644 index 0000000000..eff95eea94 --- /dev/null +++ b/test/WebSites/VersioningWebSite/VersionPutAttribute.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.Mvc; + +namespace VersioningWebSite +{ + public class VersionPutAttribute : VersionRoute, IActionHttpMethodProvider + { + public VersionPutAttribute(string template) + : base(template) + { + } + + public VersionPutAttribute(string template, string versionRange) + : base(template, versionRange) + { + } + + private readonly IEnumerable _httpMethods = new[] { "PUT" }; + + public IEnumerable HttpMethods + { + get + { + return _httpMethods; + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/VersionRangeValidator.cs b/test/WebSites/VersioningWebSite/VersionRangeValidator.cs new file mode 100644 index 0000000000..954dd9ab5c --- /dev/null +++ b/test/WebSites/VersioningWebSite/VersionRangeValidator.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Routing; + +namespace VersioningWebSite +{ + public class VersionRangeValidator : IActionConstraint + { + private readonly int? _minVersion; + private readonly int? _maxVersion; + + public int Order { get; set; } + + public VersionRangeValidator(int? minVersion, int? maxVersion) + { + _minVersion = minVersion; + _maxVersion = maxVersion; + } + + public static string GetVersion(HttpRequest request) + { + return request.Query.Get("version"); + } + + public bool Accept(ActionConstraintContext context) + { + int version; + if (int.TryParse(GetVersion(context.RouteContext.HttpContext.Request), out version)) + { + return (_minVersion == null || _minVersion <= version) && + (_maxVersion == null || _maxVersion >= version); + } + else + { + return false; + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/VersionRoute.cs b/test/WebSites/VersioningWebSite/VersionRoute.cs new file mode 100644 index 0000000000..959d3e7589 --- /dev/null +++ b/test/WebSites/VersioningWebSite/VersionRoute.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.Mvc; +using System.Text.RegularExpressions; + +namespace VersioningWebSite +{ + public class VersionRoute : RouteAttribute, IActionConstraintFactory + { + private readonly IActionConstraint _constraint; + + // 5 + // [5] + // (5) + // (5] + // [5) + // (3-5) + // (3-5] + // [3-5) + // [3-5] + // [35-56] + // Parses the above version formats and captures lb (lower bound), range, and hb (higher bound) + // We filter out (5), (5], [5) manually after we do the parsing. + private static readonly Regex _versionParser = new Regex(@"^(?[\(\[])?(?\d+(-\d+)?)(?[\)\]])?$"); + + public VersionRoute(string template) + : base(template) + { + } + + public VersionRoute(string template, string versionRange) + : base(template) + { + var constraint = CreateVersionConstraint(versionRange); + + if (constraint == null) + { + var message = string.Format("Invalid version format: {0}", versionRange); + throw new ArgumentException(message, "versionRange"); + } + + _constraint = constraint; + } + + private static IActionConstraint CreateVersionConstraint(string versionRange) + { + var match = _versionParser.Match(versionRange); + + if (!match.Success) + { + return null; + } + + var lowerBound = match.Groups["lb"].Value; + var higherBound = match.Groups["hb"].Value; + var range = match.Groups["range"].Value; + + var rangeValues = range.Split('-'); + if (rangeValues.Length == 1) + { + return GetSingleVersionOrUnboundedHigherVersionConstraint(lowerBound, higherBound, rangeValues); + } + else + { + return GetBoundedRangeVersionConstraint(lowerBound, higherBound, rangeValues); + } + } + + private static IActionConstraint GetBoundedRangeVersionConstraint( + string lowerBound, + string higherBound, + string[] rangeValues) + { + // [3-5, (3-5, 3-5], 3-5), 3-5 are not valid + if (string.IsNullOrEmpty(lowerBound) || string.IsNullOrEmpty(higherBound)) + { + return null; + } + + var minVersion = int.Parse(rangeValues[0]); + var maxVersion = int.Parse(rangeValues[1]); + + // Adjust min version and max version if the limit is exclusive. + minVersion = lowerBound == "(" ? minVersion + 1 : minVersion; + maxVersion = higherBound == ")" ? maxVersion - 1 : maxVersion; + + if (minVersion > maxVersion) + { + return null; + } + + return new VersionRangeValidator(minVersion, maxVersion); + } + + private static IActionConstraint GetSingleVersionOrUnboundedHigherVersionConstraint( + string lowerBound, + string higherBound, + string[] rangeValues) + { + // (5], [5), (5), [5, (5, 5], 5) are not valid + if (lowerBound == "(" || higherBound == ")" || + (string.IsNullOrEmpty(lowerBound) ^ string.IsNullOrEmpty(higherBound))) + { + return null; + } + + var version = int.Parse(rangeValues[0]); + if (!string.IsNullOrEmpty(lowerBound)) + { + // [5] + return new VersionRangeValidator(version, version); + } + else + { + // 5 + return new VersionRangeValidator(version, maxVersion: null); + } + } + + public IActionConstraint CreateInstance(IServiceProvider services) + { + return _constraint; + } + } +} \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/VersioningWebSite.kproj b/test/WebSites/VersioningWebSite/VersioningWebSite.kproj new file mode 100644 index 0000000000..77e72ea748 --- /dev/null +++ b/test/WebSites/VersioningWebSite/VersioningWebSite.kproj @@ -0,0 +1,30 @@ + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + Debug + AnyCPU + + + + c6304029-78c8-4604-99be-2078dca1dd36 + Web + VersioningWebSite + + + ConsoleDebugger + + + WebDebugger + + + + + 2.0 + 22807 + + + \ No newline at end of file diff --git a/test/WebSites/VersioningWebSite/project.json b/test/WebSites/VersioningWebSite/project.json new file mode 100644 index 0000000000..1d2b4c1309 --- /dev/null +++ b/test/WebSites/VersioningWebSite/project.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "Microsoft.AspNet.Mvc": "", + "Microsoft.AspNet.Mvc.TestConfiguration": "", + "Microsoft.AspNet.Server.IIS": "1.0.0-*" + }, + "frameworks": { + "aspnet50": { }, + "aspnetcore50": { } + } +}