[Fixes #1155] Added functional tests to cover common versioning scenarios.

This commit is contained in:
jacalvar 2014-09-19 15:38:36 -07:00
Родитель 78a4e78358
Коммит 8802c831a0
24 изменённых файлов: 1518 добавлений и 0 удалений

12
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

Просмотреть файл

@ -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<IApplicationBuilder> _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<RoutingResult>(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<RoutingResult>(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<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 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<RoutingResult>(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<RoutingResult>(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<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 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<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 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<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 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<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 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<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 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<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 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<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 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<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 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<RoutingResult>(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<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 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<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 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<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 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<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 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<RoutingResult>(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<RoutingResult>(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<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 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<RoutingResult>(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<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; }
}
}
}

Просмотреть файл

@ -14,6 +14,7 @@
"FiltersWebSite": "",
"FormatterWebSite": "",
"InlineConstraintsWebSite": "",
"VersioningWebSite": "",
"Microsoft.AspNet.TestHost": "1.0.0-*",
"Microsoft.AspNet.PipelineCore": "1.0.0-*",
"Microsoft.AspNet.Mvc.TestConfiguration": "",

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

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

Просмотреть файл

@ -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<TestResponseGenerator>();
});
app.UseMvc();
}
}
}

Просмотреть файл

@ -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<ActionContext> 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<IUrlHelper>();
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<string, object>(_actionContext.RouteData.Values),
action = _actionContext.ActionDescriptor.Name,
controller = ((ReflectedActionDescriptor)_actionContext.ActionDescriptor).ControllerDescriptor.Name,
link,
});
}
}
}

Просмотреть файл

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

Просмотреть файл

@ -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<string> _httpMethods = new[] { "DELETE" };
public IEnumerable<string> HttpMethods
{
get
{
return _httpMethods;
}
}
}
}

Просмотреть файл

@ -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<string> _httpMethods = new[] { "GET" };
public IEnumerable<string> HttpMethods
{
get
{
return _httpMethods;
}
}
}
}

Просмотреть файл

@ -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<string> _httpMethods = new[] { "POST" };
public IEnumerable<string> HttpMethods
{
get
{
return _httpMethods;
}
}
}
}

Просмотреть файл

@ -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<string> _httpMethods = new[] { "PUT" };
public IEnumerable<string> HttpMethods
{
get
{
return _httpMethods;
}
}
}
}

Просмотреть файл

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

Просмотреть файл

@ -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(@"^(?<lb>[\(\[])?(?<range>\d+(-\d+)?)(?<hb>[\)\]])?$");
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;
}
}
}

Просмотреть файл

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">12.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>c6304029-78c8-4604-99be-2078dca1dd36</ProjectGuid>
<OutputType>Web</OutputType>
<RootNamespace>VersioningWebSite</RootNamespace>
</PropertyGroup>
<PropertyGroup Condition="$(OutputType) == 'Console'">
<DebuggerFlavor>ConsoleDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="$(OutputType) == 'Web'">
<DebuggerFlavor>WebDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" Label="Configuration">
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>22807</DevelopmentServerPort>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

Просмотреть файл

@ -0,0 +1,11 @@
{
"dependencies": {
"Microsoft.AspNet.Mvc": "",
"Microsoft.AspNet.Mvc.TestConfiguration": "",
"Microsoft.AspNet.Server.IIS": "1.0.0-*"
},
"frameworks": {
"aspnet50": { },
"aspnetcore50": { }
}
}