🔖 Implement API versioning
continuous-integration/drone/push Build is passing Details

pull/61/head
Alexis Drai 2 years ago
parent 15bb089e55
commit 179292e87f

@ -3,10 +3,28 @@
"BaseUrl": "https://localhost:5003" "BaseUrl": "https://localhost:5003"
}, },
"Routes": [ "Routes": [
{
"UpstreamPathTemplate": "/gateway/v1/cats",
"UpstreamHttpMethod": [ "Get" ],
"DownstreamPathTemplate": "/api/v1/cats",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 7229
}
],
"RateLimitOptions": {
"EnableRateLimiting": true,
"Period": "1s",
"PeriodTimespan": 1,
"Limit": 1
}
},
{ {
"UpstreamPathTemplate": "/gateway/cats", "UpstreamPathTemplate": "/gateway/cats",
"UpstreamHttpMethod": [ "Get" ], "UpstreamHttpMethod": [ "Get" ],
"DownstreamPathTemplate": "/api/cats", "DownstreamPathTemplate": "/api/v2/cats",
"DownstreamScheme": "https", "DownstreamScheme": "https",
"DownstreamHostAndPorts": [ "DownstreamHostAndPorts": [
{ {
@ -24,7 +42,7 @@
{ {
"UpstreamPathTemplate": "/gateway/cats", "UpstreamPathTemplate": "/gateway/cats",
"UpstreamHttpMethod": [ "Post" ], "UpstreamHttpMethod": [ "Post" ],
"DownstreamPathTemplate": "/api/cats", "DownstreamPathTemplate": "/api/v1/cats",
"DownstreamScheme": "https", "DownstreamScheme": "https",
"DownstreamHostAndPorts": [ "DownstreamHostAndPorts": [
{ {
@ -36,7 +54,7 @@
{ {
"UpstreamPathTemplate": "/gateway/cats/{id}", "UpstreamPathTemplate": "/gateway/cats/{id}",
"UpstreamHttpMethod": [ "Get", "Put", "Delete" ], "UpstreamHttpMethod": [ "Get", "Put", "Delete" ],
"DownstreamPathTemplate": "/api/cats/{id}", "DownstreamPathTemplate": "/api/v1/cats/{id}",
"DownstreamScheme": "https", "DownstreamScheme": "https",
"DownstreamHostAndPorts": [ "DownstreamHostAndPorts": [
{ {
@ -49,7 +67,7 @@
{ {
"UpstreamPathTemplate": "/gateway/bars", "UpstreamPathTemplate": "/gateway/bars",
"UpstreamHttpMethod": [ "Get" ], "UpstreamHttpMethod": [ "Get" ],
"DownstreamPathTemplate": "/api/bars", "DownstreamPathTemplate": "/api/v1/bars",
"DownstreamScheme": "https", "DownstreamScheme": "https",
"DownstreamHostAndPorts": [ "DownstreamHostAndPorts": [
{ {
@ -67,7 +85,7 @@
{ {
"UpstreamPathTemplate": "/gateway/bars", "UpstreamPathTemplate": "/gateway/bars",
"UpstreamHttpMethod": [ "Post" ], "UpstreamHttpMethod": [ "Post" ],
"DownstreamPathTemplate": "/api/bars", "DownstreamPathTemplate": "/api/v1/bars",
"DownstreamScheme": "https", "DownstreamScheme": "https",
"DownstreamHostAndPorts": [ "DownstreamHostAndPorts": [
{ {
@ -79,7 +97,7 @@
{ {
"UpstreamPathTemplate": "/gateway/bars/{id}", "UpstreamPathTemplate": "/gateway/bars/{id}",
"UpstreamHttpMethod": [ "Get", "Put", "Delete" ], "UpstreamHttpMethod": [ "Get", "Put", "Delete" ],
"DownstreamPathTemplate": "/api/bars/{id}", "DownstreamPathTemplate": "/api/v1/bars/{id}",
"DownstreamScheme": "https", "DownstreamScheme": "https",
"DownstreamHostAndPorts": [ "DownstreamHostAndPorts": [
{ {
@ -92,7 +110,7 @@
{ {
"UpstreamPathTemplate": "/gateway/customers", "UpstreamPathTemplate": "/gateway/customers",
"UpstreamHttpMethod": [ "Get" ], "UpstreamHttpMethod": [ "Get" ],
"DownstreamPathTemplate": "/api/customers", "DownstreamPathTemplate": "/api/v1/customers",
"DownstreamScheme": "https", "DownstreamScheme": "https",
"DownstreamHostAndPorts": [ "DownstreamHostAndPorts": [
{ {
@ -107,7 +125,7 @@
{ {
"UpstreamPathTemplate": "/gateway/customers", "UpstreamPathTemplate": "/gateway/customers",
"UpstreamHttpMethod": [ "Post" ], "UpstreamHttpMethod": [ "Post" ],
"DownstreamPathTemplate": "/api/customers", "DownstreamPathTemplate": "/api/v1/customers",
"DownstreamScheme": "https", "DownstreamScheme": "https",
"DownstreamHostAndPorts": [ "DownstreamHostAndPorts": [
{ {
@ -119,7 +137,7 @@
{ {
"UpstreamPathTemplate": "/gateway/customers/{id}", "UpstreamPathTemplate": "/gateway/customers/{id}",
"UpstreamHttpMethod": [ "Get", "Put", "Delete" ], "UpstreamHttpMethod": [ "Get", "Put", "Delete" ],
"DownstreamPathTemplate": "/api/customers/{id}", "DownstreamPathTemplate": "/api/v1/customers/{id}",
"DownstreamScheme": "https", "DownstreamScheme": "https",
"DownstreamHostAndPorts": [ "DownstreamHostAndPorts": [
{ {

@ -1,22 +1,12 @@
using AutoMapper; using AutoMapper;
using Castle.Core.Logging;
using cat_cafe.Controllers;
using cat_cafe.Dto; using cat_cafe.Dto;
using cat_cafe.Entities; using cat_cafe.Entities;
using cat_cafe.Mappers; using cat_cafe.Mappers;
using cat_cafe.Repositories; using cat_cafe.Repositories;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FluentAssertions; using FluentAssertions;
using cat_cafe.WeSo; using cat_cafe.WeSo;
@ -85,10 +75,10 @@ namespace cat_cafe.Controllers.Tests
} }
[TestMethod()] [TestMethod()]
public async Task GetCatsTest() public async Task GetCatsV2Test()
{ {
// control response type // control response type
var actual = await controller.GetCats(); var actual = await controller.GetCatsV2();
actual.Result.Should().BeOfType<OkObjectResult>(); actual.Result.Should().BeOfType<OkObjectResult>();
// control response object // control response object

@ -14,8 +14,9 @@ using System.Xml.Linq;
namespace cat_cafe.Controllers namespace cat_cafe.Controllers
{ {
[Route("api/[controller]")] [Route("api/v{version:apiVersion}/[controller]")]
[ApiController] [ApiController]
[ApiVersion("2.0")]
public class BarsController : ControllerBase public class BarsController : ControllerBase
{ {
private readonly CatCafeContext _context; private readonly CatCafeContext _context;
@ -29,8 +30,9 @@ namespace cat_cafe.Controllers
_logger = logger; _logger = logger;
} }
// GET: api/Bars // GET: api/v1/Bars
[HttpGet] [HttpGet]
[MapToApiVersion("1.0")]
public async Task<ActionResult<IEnumerable<BarDto>>> GetBars() public async Task<ActionResult<IEnumerable<BarDto>>> GetBars()
{ {
var bars = _context.Bars var bars = _context.Bars
@ -45,8 +47,9 @@ namespace cat_cafe.Controllers
return _mapper.Map<List<BarDto>>(bars); return _mapper.Map<List<BarDto>>(bars);
} }
// GET: api/Bars/5 // GET: api/v1/Bars/5
[HttpGet("{id}")] [HttpGet("{id}")]
[MapToApiVersion("1.0")]
public async Task<ActionResult<BarDto>> GetBar(long id) public async Task<ActionResult<BarDto>> GetBar(long id)
{ {
var bar = _context.Bars.Include(p => p.cats) var bar = _context.Bars.Include(p => p.cats)
@ -66,9 +69,10 @@ namespace cat_cafe.Controllers
return _mapper.Map<BarDto>(bar.Result); return _mapper.Map<BarDto>(bar.Result);
} }
// PUT: api/Bars/5 // PUT: api/v1/Bars/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPut("{id}")] [HttpPut("{id}")]
[MapToApiVersion("1.0")]
public async Task<IActionResult> PutBar(long id, BarDto barDto) public async Task<IActionResult> PutBar(long id, BarDto barDto)
{ {
if (id != barDto.Id) if (id != barDto.Id)
@ -97,9 +101,10 @@ namespace cat_cafe.Controllers
return NoContent(); return NoContent();
} }
// POST: api/Bars // POST: api/v1/Bars
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost] [HttpPost]
[MapToApiVersion("1.0")]
public async Task<ActionResult<BarDto>> PostBar(BarDto barDto) public async Task<ActionResult<BarDto>> PostBar(BarDto barDto)
{ {
// Bar bar = _mapper.Map<Bar>(barDto); // Bar bar = _mapper.Map<Bar>(barDto);
@ -112,8 +117,9 @@ namespace cat_cafe.Controllers
return CreatedAtAction("GetBar", new { id = barDto.Id }, _mapper.Map<BarDto>(bar)); return CreatedAtAction("GetBar", new { id = barDto.Id }, _mapper.Map<BarDto>(bar));
} }
// DELETE: api/Bars/5 // DELETE: api/v1/Bars/5
[HttpDelete("{id}")] [HttpDelete("{id}")]
[MapToApiVersion("1.0")]
public async Task<IActionResult> DeleteBar(long id) public async Task<IActionResult> DeleteBar(long id)
{ {
var bar = await _context.Bars.FindAsync(id); var bar = await _context.Bars.FindAsync(id);

@ -16,8 +16,10 @@ using cat_cafe.WeSo;
namespace cat_cafe.Controllers namespace cat_cafe.Controllers
{ {
[Route("api/[controller]")] [Route("api/v{version:apiVersion}/[controller]")]
[ApiController] [ApiController]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class CatsController : ControllerBase public class CatsController : ControllerBase
{ {
private readonly CatCafeContext _context; private readonly CatCafeContext _context;
@ -38,17 +40,32 @@ namespace cat_cafe.Controllers
_webSocketHandler = webSocketHandler; _webSocketHandler = webSocketHandler;
} }
// GET: api/Cats // GET: api/v1/Cats
[HttpGet] [HttpGet]
[MapToApiVersion("1.0")]
public async Task<ActionResult<IEnumerable<CatDto>>> GetCats() public async Task<ActionResult<IEnumerable<CatDto>>> GetCats()
{
var cats = await _context.Cats.ToListAsync();
cats.Add(new Cat { Id = -1, Age = 42, Name = "Hi! I'm the secret V1 cat" });
return Ok(_mapper.Map<List<CatDto>>(cats));
}
// GET: api/v2/Cats
[HttpGet]
[MapToApiVersion("2.0")]
public async Task<ActionResult<IEnumerable<CatDto>>> GetCatsV2()
{ {
var cats = await _context.Cats.ToListAsync(); var cats = await _context.Cats.ToListAsync();
return Ok(_mapper.Map<List<CatDto>>(cats)); return Ok(_mapper.Map<List<CatDto>>(cats));
} }
// GET: api/Cats/5 // GET: api/v1/Cats/5
[HttpGet("{id}")] [HttpGet("{id}")]
[MapToApiVersion("1.0")]
public async Task<ActionResult<CatDto>> GetCat(long id) public async Task<ActionResult<CatDto>> GetCat(long id)
{ {
var cat = await _context.Cats.FindAsync(id); var cat = await _context.Cats.FindAsync(id);
@ -61,9 +78,10 @@ namespace cat_cafe.Controllers
return Ok(_mapper.Map<CatDto>(cat)); return Ok(_mapper.Map<CatDto>(cat));
} }
// PUT: api/Cats/5 // PUT: api/v1/Cats/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPut("{id}")] [HttpPut("{id}")]
[MapToApiVersion("1.0")]
public async Task<IActionResult> PutCat(long id, CatDto catDto) public async Task<IActionResult> PutCat(long id, CatDto catDto)
{ {
if (id != catDto.Id) if (id != catDto.Id)
@ -93,9 +111,10 @@ namespace cat_cafe.Controllers
return NoContent(); return NoContent();
} }
// POST: api/Cats // POST: api/v1/Cats
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost] [HttpPost]
[MapToApiVersion("1.0")]
public async Task<ActionResult<CatDto>> PostCat(CatDto catDto) public async Task<ActionResult<CatDto>> PostCat(CatDto catDto)
{ {
Cat cat = _mapper.Map<Cat>(catDto); Cat cat = _mapper.Map<Cat>(catDto);
@ -107,8 +126,9 @@ namespace cat_cafe.Controllers
return CreatedAtAction("GetCat", new { id = catDto.Id }, _mapper.Map<CatDto>(cat)); return CreatedAtAction("GetCat", new { id = catDto.Id }, _mapper.Map<CatDto>(cat));
} }
// DELETE: api/Cats/5 // DELETE: api/v1/Cats/5
[HttpDelete("{id}")] [HttpDelete("{id}")]
[MapToApiVersion("1.0")]
public async Task<IActionResult> DeleteCat(long id) public async Task<IActionResult> DeleteCat(long id)
{ {
var cat = await _context.Cats.FindAsync(id); var cat = await _context.Cats.FindAsync(id);

@ -14,8 +14,9 @@ using Newtonsoft.Json;
namespace cat_cafe.Controllers namespace cat_cafe.Controllers
{ {
[Route("api/[controller]")] [Route("api/v{version:apiVersion}/[controller]")]
[ApiController] [ApiController]
[ApiVersion("2.0")]
public class CustomersController : ControllerBase public class CustomersController : ControllerBase
{ {
private readonly CatCafeContext _context; private readonly CatCafeContext _context;
@ -29,8 +30,9 @@ namespace cat_cafe.Controllers
_logger = logger; _logger = logger;
} }
// GET: api/Customers // GET: api/v1/Customers
[HttpGet] [HttpGet]
[MapToApiVersion("1.0")]
public async Task<ActionResult<IEnumerable<CustomerDto>>> GetCustomers() public async Task<ActionResult<IEnumerable<CustomerDto>>> GetCustomers()
{ {
Log.Information(this.Request.Method + " => get All customers"); Log.Information(this.Request.Method + " => get All customers");
@ -44,8 +46,9 @@ namespace cat_cafe.Controllers
return Ok(_mapper.Map<List<CustomerDto>>(customers)); return Ok(_mapper.Map<List<CustomerDto>>(customers));
} }
// GET: api/Customers/5 // GET: api/v1/Customers/5
[HttpGet("{id}")] [HttpGet("{id}")]
[MapToApiVersion("1.0")]
public async Task<ActionResult<CustomerDto>> GetCustomer(long id) public async Task<ActionResult<CustomerDto>> GetCustomer(long id)
{ {
Log.Information(this.Request.Method + " => get by ID {@id}",id); Log.Information(this.Request.Method + " => get by ID {@id}",id);
@ -64,9 +67,10 @@ namespace cat_cafe.Controllers
return Ok(_mapper.Map<CustomerDto>(customer)); return Ok(_mapper.Map<CustomerDto>(customer));
} }
// PUT: api/Customers/5 // PUT: api/v1/Customers/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPut("{id}")] [HttpPut("{id}")]
[MapToApiVersion("1.0")]
public async Task<IActionResult> PutCustomer(long id, CustomerDto customerDto) public async Task<IActionResult> PutCustomer(long id, CustomerDto customerDto)
{ {
Log.Information(this.Request.Method + " => put by ID {@id}", id); Log.Information(this.Request.Method + " => put by ID {@id}", id);
@ -104,9 +108,10 @@ namespace cat_cafe.Controllers
return Ok(); return Ok();
} }
// POST: api/Customers // POST: api/v1/Customers
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost] [HttpPost]
[MapToApiVersion("1.0")]
public async Task<ActionResult<Customer>> PostCustomer(CustomerDto customerDto) public async Task<ActionResult<Customer>> PostCustomer(CustomerDto customerDto)
{ {
Log.Information(this.Request.Method + " => post customer"); Log.Information(this.Request.Method + " => post customer");
@ -123,8 +128,9 @@ namespace cat_cafe.Controllers
return CreatedAtAction("GetCustomer", new { id = customer.Id }, _mapper.Map<Customer>( customer)); return CreatedAtAction("GetCustomer", new { id = customer.Id }, _mapper.Map<Customer>( customer));
} }
// DELETE: api/Customers/5 // DELETE: api/v1/Customers/5
[HttpDelete("{id}")] [HttpDelete("{id}")]
[MapToApiVersion("1.0")]
public async Task<IActionResult> DeleteCustomer(long id) public async Task<IActionResult> DeleteCustomer(long id)
{ {
Log.Information(this.Request.Method + " => delete by ID {@id}", id); Log.Information(this.Request.Method + " => delete by ID {@id}", id);

@ -1,7 +1,6 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using cat_cafe.Repositories; using cat_cafe.Repositories;
using Serilog; using Serilog;
using Serilog.Sinks.File;
using System.Net.WebSockets; using System.Net.WebSockets;
using cat_cafe.WeSo; using cat_cafe.WeSo;
@ -21,6 +20,14 @@ builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
builder.Services.AddAutoMapper(typeof(Program)); builder.Services.AddAutoMapper(typeof(Program));
builder.Services.AddControllersWithViews(); builder.Services.AddControllersWithViews();
builder.Services.AddApiVersioning(o => { o.ReportApiVersions = true; });
builder.Services.AddVersionedApiExplorer(
options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
}
);
var app = builder.Build(); var app = builder.Build();

@ -7,6 +7,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" /> <PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="2.2.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.12" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.12" />

@ -1,6 +1,6 @@
{ {
"info": { "info": {
"_postman_id": "4448c1b2-03b7-4b34-9d5b-ee364ff17986", "_postman_id": "2347c985-6c2f-4532-abd1-63083abc4efb",
"name": "Cat Café", "name": "Cat Café",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_exporter_id": "25802734" "_exporter_id": "25802734"
@ -94,7 +94,7 @@
"response": [] "response": []
}, },
{ {
"name": "Read all cats", "name": "Read all cats (v2)",
"request": { "request": {
"method": "GET", "method": "GET",
"header": [], "header": [],
@ -165,7 +165,7 @@
"response": [] "response": []
}, },
{ {
"name": "Read all cats", "name": "Read all cats (v2)",
"request": { "request": {
"method": "GET", "method": "GET",
"header": [], "header": [],
@ -206,12 +206,12 @@
"response": [] "response": []
}, },
{ {
"name": "Read all cats", "name": "Read all cats (v1)",
"request": { "request": {
"method": "GET", "method": "GET",
"header": [], "header": [],
"url": { "url": {
"raw": "https://localhost:5003/gateway/cats", "raw": "https://localhost:5003/gateway/v1/cats",
"protocol": "https", "protocol": "https",
"host": [ "host": [
"localhost" "localhost"
@ -219,6 +219,7 @@
"port": "5003", "port": "5003",
"path": [ "path": [
"gateway", "gateway",
"v1",
"cats" "cats"
] ]
} }

Loading…
Cancel
Save