diff --git a/ApiGateway/ocelot.json b/ApiGateway/ocelot.json index d1b8387..79b6dba 100644 --- a/ApiGateway/ocelot.json +++ b/ApiGateway/ocelot.json @@ -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": [ { diff --git a/README.md b/README.md index f46c985..b680da4 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/Tests/Controllers/CatsControllerTest.cs b/Tests/Controllers/CatsControllerTest.cs index 66f207d..c42f046 100644 --- a/Tests/Controllers/CatsControllerTest.cs +++ b/Tests/Controllers/CatsControllerTest.cs @@ -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(); // control response object diff --git a/cat_cafe/Controllers/BarsController.cs b/cat_cafe/Controllers/BarsController.cs index ec750c4..263b8fb 100644 --- a/cat_cafe/Controllers/BarsController.cs +++ b/cat_cafe/Controllers/BarsController.cs @@ -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>> GetBars() { var bars = _context.Bars @@ -45,8 +47,9 @@ namespace cat_cafe.Controllers return _mapper.Map>(bars); } - // GET: api/Bars/5 + // GET: api/v1/Bars/5 [HttpGet("{id}")] + [MapToApiVersion("1.0")] public async Task> GetBar(long id) { var bar = _context.Bars.Include(p => p.cats) @@ -66,9 +69,10 @@ namespace cat_cafe.Controllers return _mapper.Map(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 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> PostBar(BarDto barDto) { // Bar bar = _mapper.Map(barDto); @@ -112,8 +117,9 @@ namespace cat_cafe.Controllers return CreatedAtAction("GetBar", new { id = barDto.Id }, _mapper.Map(bar)); } - // DELETE: api/Bars/5 + // DELETE: api/v1/Bars/5 [HttpDelete("{id}")] + [MapToApiVersion("1.0")] public async Task DeleteBar(long id) { var bar = await _context.Bars.FindAsync(id); diff --git a/cat_cafe/Controllers/CatsController.cs b/cat_cafe/Controllers/CatsController.cs index 033ab58..178270b 100644 --- a/cat_cafe/Controllers/CatsController.cs +++ b/cat_cafe/Controllers/CatsController.cs @@ -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>> 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>(cats)); + } + + + // GET: api/v2/Cats + [HttpGet] + [MapToApiVersion("2.0")] + + public async Task>> GetCatsV2() { var cats = await _context.Cats.ToListAsync(); return Ok(_mapper.Map>(cats)); } - // GET: api/Cats/5 + // GET: api/v1/Cats/5 [HttpGet("{id}")] + [MapToApiVersion("1.0")] public async Task> GetCat(long id) { var cat = await _context.Cats.FindAsync(id); @@ -61,9 +78,10 @@ namespace cat_cafe.Controllers return Ok(_mapper.Map(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 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> PostCat(CatDto catDto) { Cat cat = _mapper.Map(catDto); @@ -107,8 +126,9 @@ namespace cat_cafe.Controllers return CreatedAtAction("GetCat", new { id = catDto.Id }, _mapper.Map(cat)); } - // DELETE: api/Cats/5 + // DELETE: api/v1/Cats/5 [HttpDelete("{id}")] + [MapToApiVersion("1.0")] public async Task DeleteCat(long id) { var cat = await _context.Cats.FindAsync(id); diff --git a/cat_cafe/Controllers/CustomersController.cs b/cat_cafe/Controllers/CustomersController.cs index 3055732..d695dd6 100644 --- a/cat_cafe/Controllers/CustomersController.cs +++ b/cat_cafe/Controllers/CustomersController.cs @@ -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>> GetCustomers() { Log.Information(this.Request.Method + " => get All customers"); @@ -44,8 +46,9 @@ namespace cat_cafe.Controllers return Ok(_mapper.Map>(customers)); } - // GET: api/Customers/5 + // GET: api/v1/Customers/5 [HttpGet("{id}")] + [MapToApiVersion("1.0")] public async Task> 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(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 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> 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)); } - // DELETE: api/Customers/5 + // DELETE: api/v1/Customers/5 [HttpDelete("{id}")] + [MapToApiVersion("1.0")] public async Task DeleteCustomer(long id) { Log.Information(this.Request.Method + " => delete by ID {@id}", id); diff --git a/cat_cafe/Program.cs b/cat_cafe/Program.cs index e53f6f9..927f514 100644 --- a/cat_cafe/Program.cs +++ b/cat_cafe/Program.cs @@ -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(); diff --git a/cat_cafe/cat_cafe.csproj b/cat_cafe/cat_cafe.csproj index 4aff7dd..17549b0 100644 --- a/cat_cafe/cat_cafe.csproj +++ b/cat_cafe/cat_cafe.csproj @@ -7,6 +7,8 @@ + + diff --git a/docs/Cat Café.postman_collection.json b/docs/cat_cafe.postman_collection.json similarity index 94% rename from docs/Cat Café.postman_collection.json rename to docs/cat_cafe.postman_collection.json index ab88daa..96989d7 100644 --- a/docs/Cat Café.postman_collection.json +++ b/docs/cat_cafe.postman_collection.json @@ -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" ] }