Merge pull request 'implement-api-versioning' (#61) from implement-api-versioning into master
continuous-integration/drone/push Build is passing Details

Reviewed-on: #61
pull/62/head
Alexis Drai 2 years ago
commit 47f960d0c5

@ -4,9 +4,9 @@
},
"Routes": [
{
"UpstreamPathTemplate": "/gateway/cats",
"UpstreamPathTemplate": "/gateway/v1/cats",
"UpstreamHttpMethod": [ "Get" ],
"DownstreamPathTemplate": "/api/cats",
"DownstreamPathTemplate": "/api/v1/cats",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
@ -21,10 +21,22 @@
"Limit": 1
}
},
{
"UpstreamPathTemplate": "/gateway/cats",
"UpstreamHttpMethod": [ "Get" ],
"DownstreamPathTemplate": "/api/v2/cats",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 7229
}
]
},
{
"UpstreamPathTemplate": "/gateway/cats",
"UpstreamHttpMethod": [ "Post" ],
"DownstreamPathTemplate": "/api/cats",
"DownstreamPathTemplate": "/api/v1/cats",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
@ -36,7 +48,7 @@
{
"UpstreamPathTemplate": "/gateway/cats/{id}",
"UpstreamHttpMethod": [ "Get", "Put", "Delete" ],
"DownstreamPathTemplate": "/api/cats/{id}",
"DownstreamPathTemplate": "/api/v1/cats/{id}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
@ -49,7 +61,7 @@
{
"UpstreamPathTemplate": "/gateway/bars",
"UpstreamHttpMethod": [ "Get" ],
"DownstreamPathTemplate": "/api/bars",
"DownstreamPathTemplate": "/api/v1/bars",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
@ -67,7 +79,7 @@
{
"UpstreamPathTemplate": "/gateway/bars",
"UpstreamHttpMethod": [ "Post" ],
"DownstreamPathTemplate": "/api/bars",
"DownstreamPathTemplate": "/api/v1/bars",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
@ -79,7 +91,7 @@
{
"UpstreamPathTemplate": "/gateway/bars/{id}",
"UpstreamHttpMethod": [ "Get", "Put", "Delete" ],
"DownstreamPathTemplate": "/api/bars/{id}",
"DownstreamPathTemplate": "/api/v1/bars/{id}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
@ -92,7 +104,7 @@
{
"UpstreamPathTemplate": "/gateway/customers",
"UpstreamHttpMethod": [ "Get" ],
"DownstreamPathTemplate": "/api/customers",
"DownstreamPathTemplate": "/api/v1/customers",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
@ -107,7 +119,7 @@
{
"UpstreamPathTemplate": "/gateway/customers",
"UpstreamHttpMethod": [ "Post" ],
"DownstreamPathTemplate": "/api/customers",
"DownstreamPathTemplate": "/api/v1/customers",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{
@ -119,7 +131,7 @@
{
"UpstreamPathTemplate": "/gateway/customers/{id}",
"UpstreamHttpMethod": [ "Get", "Put", "Delete" ],
"DownstreamPathTemplate": "/api/customers/{id}",
"DownstreamPathTemplate": "/api/v1/customers/{id}",
"DownstreamScheme": "https",
"DownstreamHostAndPorts": [
{

@ -9,6 +9,7 @@
* [WebSocket](#websocket)
* [API type choices](#api-type-choices)
* [API Gateway](#api-gateway)
* [Versioning](#versioning)
* [Testing the app](#testing-the-app)
## Global architecture
@ -163,14 +164,39 @@ The gateway uses rate limiting to make sure that clients cannot send an all-incl
}
} ...
```
---
### Versioning
This API is versioned, but our clients do not need to know about it.
We use URIs to implement our versioning, but our clients do not need to rewrite their requests to stay up-to-date.
That is all thanks to our API Gateway, which masks these details:
```json
...
{
"UpstreamPathTemplate": "/gateway/cats",
"UpstreamHttpMethod": [ "Get" ],
"DownstreamPathTemplate": "/api/v2/cats",
...
},
{
"UpstreamPathTemplate": "/gateway/cats",
"UpstreamHttpMethod": [ "Post" ],
"DownstreamPathTemplate": "/api/v1/cats",
...
},
...
```
---
## Testing the app
0. Prepare to use Postman
1. Start both the `cat_cafe` and `ApiGateway` projects (you may refer to [this procedure](#how-to-launch-both-projects)). Two browser windows will open, which you will not need to use much.
2. Use whichever browser window to open a WebSocket, so that you will get notifications (you may refer to [that procedure](#websocket))
3. In Postman, inside a workspace, click the `Import` button in the top left corner. Then click `Choose files` and find the collection to import in `docs/Cat Café.postman_collection.json`, in this repo. Follow the procedure to save it.
4. Keep an eye on the browser window you used to open a WebSocket, and execute the requests in this imported collection. When using `POST`, you should get an alert in the browser window.
1. Start both the `cat_cafe` and `ApiGateway` projects (you may refer to [this procedure](#how-to-launch-both-projects)). Two browser windows will open, but *you can ignore SwaggerUI*.
2. Use a browser window to open a WebSocket, so that you will get notifications (you may refer to [that procedure](#websocket))
3. In Postman, inside a workspace, click the `Import` button in the top left corner. Then click `Choose files` and find the collection to import in `docs/cat_cafe.postman_collection.json`, in this repo. Follow the procedure to save it.
4. Keep an eye on the browser window that you used to open a WebSocket, and execute the requests in this imported collection. When using `POST`, you should get an alert in the browser window.
Notice that the REST API and the WebSocket are accessed through our API Gateway.

@ -1,22 +1,12 @@
using AutoMapper;
using Castle.Core.Logging;
using cat_cafe.Controllers;
using cat_cafe.Dto;
using cat_cafe.Entities;
using cat_cafe.Mappers;
using cat_cafe.Repositories;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
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 cat_cafe.WeSo;
@ -85,10 +75,10 @@ namespace cat_cafe.Controllers.Tests
}
[TestMethod()]
public async Task GetCatsTest()
public async Task GetCatsV2Test()
{
// control response type
var actual = await controller.GetCats();
var actual = await controller.GetCatsV2();
actual.Result.Should().BeOfType<OkObjectResult>();
// control response object

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

@ -16,8 +16,10 @@ using cat_cafe.WeSo;
namespace cat_cafe.Controllers
{
[Route("api/[controller]")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class CatsController : ControllerBase
{
private readonly CatCafeContext _context;
@ -38,17 +40,32 @@ namespace cat_cafe.Controllers
_webSocketHandler = webSocketHandler;
}
// GET: api/Cats
// GET: api/v1/Cats
[HttpGet]
[MapToApiVersion("1.0")]
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();
return Ok(_mapper.Map<List<CatDto>>(cats));
}
// GET: api/Cats/5
// GET: api/v1/Cats/5
[HttpGet("{id}")]
[MapToApiVersion("1.0")]
public async Task<ActionResult<CatDto>> GetCat(long id)
{
var cat = await _context.Cats.FindAsync(id);
@ -61,9 +78,10 @@ namespace cat_cafe.Controllers
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
[HttpPut("{id}")]
[MapToApiVersion("1.0")]
public async Task<IActionResult> PutCat(long id, CatDto catDto)
{
if (id != catDto.Id)
@ -93,9 +111,10 @@ namespace cat_cafe.Controllers
return NoContent();
}
// POST: api/Cats
// POST: api/v1/Cats
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost]
[MapToApiVersion("1.0")]
public async Task<ActionResult<CatDto>> PostCat(CatDto 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));
}
// DELETE: api/Cats/5
// DELETE: api/v1/Cats/5
[HttpDelete("{id}")]
[MapToApiVersion("1.0")]
public async Task<IActionResult> DeleteCat(long id)
{
var cat = await _context.Cats.FindAsync(id);

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

@ -1,7 +1,6 @@
using Microsoft.EntityFrameworkCore;
using cat_cafe.Repositories;
using Serilog;
using Serilog.Sinks.File;
using System.Net.WebSockets;
using cat_cafe.WeSo;
@ -21,6 +20,14 @@ builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddAutoMapper(typeof(Program));
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();

@ -7,6 +7,8 @@
</PropertyGroup>
<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.EntityFrameworkCore.InMemory" Version="7.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.12" />

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