🧦 Implement WebSocket #58

Merged
alexis.drai merged 1 commits from implement-websocket into master 2 years ago

@ -32,6 +32,8 @@ app.UseAuthorization();
app.MapControllers(); app.MapControllers();
app.UseWebSockets();
await app.UseOcelot(); await app.UseOcelot();
app.Run(); app.Run();

@ -127,6 +127,20 @@
"Port": 7229 "Port": 7229
} }
] ]
},
{
"UpstreamPathTemplate": "/gateway/ws",
"UpstreamHttpMethod": [ "Get", "Post" ],
"DownstreamPathTemplate": "/ws",
"DownstreamScheme": "wss",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 7229
}
],
"UseWebSockets": true
} }
] ]
} }

@ -8,7 +8,7 @@
### Concept ### Concept
This application attempts to modelize a cat café, with cafés (called "bars" here), cats, and customers. The cats can go "meow". This application attempts to modelize a cat café, with cafés (called "bars" here), cats, and customers.
### API REST ### API REST
@ -18,8 +18,21 @@ We used an ASP .NET Web API, with a Swagger configuration to visualize the inter
### WebSocket ### WebSocket
... A websocket was set up to notify clients (who subscribe to it) whenever a Cat is `POST`ed.
Clients need to subscribe by typing the following code in the console of their browser, in developer mode :
```js
new WebSocket("wss://localhost:5003/gateway/ws").onmessage = function (event) {
if (event.data === "entity-created") {
alert("A new entity was created!");
}
};
```
*Note*:
- while the app uses port `7229` in our default config, **you should use port `5003` anyway** to subscribe to our WebSocket through our *API Gateway*
- `"entity-created"` is a hard-coded event ID and should not be changed.
- you are free to change the content of the `Alert` itself, of course
### API Gateway ### API Gateway
An [Ocelot](https://ocelot.readthedocs.io/en/latest/) API Gateway manages the whole system. An [Ocelot](https://ocelot.readthedocs.io/en/latest/) API Gateway manages the whole system.
@ -54,12 +67,21 @@ Overall, the architecture may be summed up like so:
#### Routes #### Routes
The Gateway routes offer access to the REST API in a similar way as the REST API itself, with a small transformation: there is a new port, and the word "gateway" replaces "api". The REST API's Swagger UI will give you all the information required about those routes. The Gateway routes offer access to the REST API in a similar way as the REST API itself, with a small transformation: there is a new port, and the word "gateway" replaces "api". The REST API's Swagger UI will give you all the information required about those routes.
| REST(old) | Gateway(current) | | REST(old) | Gateway(current) |
|--|--| |--|--|
| `.../7229/api/...` | `.../5003/gateway/...` |
| `GET` on `https://localhost/7229/api/cats` | `GET` on `https://localhost/5003/gateway/cats` | | `GET` on `https://localhost/7229/api/cats` | `GET` on `https://localhost/5003/gateway/cats` |
| `POST` on `https://localhost/7229/api/bars/{id}` | `GET` on `https://localhost/5003/gateway/bars/{id}` | | `POST` on `https://localhost/7229/api/bars/{id}` | `GET` on `https://localhost/5003/gateway/bars/{id}` |
...and for the websocket:
- old :
```js
new WebSocket("wss://localhost:7229/ws").onmessage = function (event) {...};
```
- new :
```js
new WebSocket("wss://localhost:5003/gateway/ws").onmessage = function (event) {...};
```
#### Caching #### Caching
The gateway uses caching to ensure that the entire list of customers is only queried from the database once every 10 seconds. The rest of the time, clients sending `GET`-all requests get served the contents of a cache. The gateway uses caching to ensure that the entire list of customers is only queried from the database once every 10 seconds. The rest of the time, clients sending `GET`-all requests get served the contents of a cache.

@ -18,7 +18,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using FluentAssertions; using FluentAssertions;
using cat_cafe.WeSo;
namespace cat_cafe.Controllers.Tests namespace cat_cafe.Controllers.Tests
{ {
@ -64,7 +64,7 @@ namespace cat_cafe.Controllers.Tests
{ {
mapper = mapperConf.CreateMapper(); mapper = mapperConf.CreateMapper();
context = new CatCafeContext(options); context = new CatCafeContext(options);
controller = new CatsController(context, mapper, logger); controller = new CatsController(context, mapper, logger, new WebSocketHandler(new List<System.Net.WebSockets.WebSocket>()));
aliceDto = mapper.Map<CatDto>(alice); aliceDto = mapper.Map<CatDto>(alice);
bobDto = mapper.Map<CatDto>(bob); bobDto = mapper.Map<CatDto>(bob);
} }

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Ocelot" Version="18.0.0" />
<PackageReference Include="Ocelot.Cache.CacheManager" Version="18.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
</Project>

@ -12,6 +12,7 @@ using cat_cafe.Dto;
using Serilog; using Serilog;
using Newtonsoft.Json; using Newtonsoft.Json;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using cat_cafe.WeSo;
namespace cat_cafe.Controllers namespace cat_cafe.Controllers
{ {
@ -22,12 +23,19 @@ namespace cat_cafe.Controllers
private readonly CatCafeContext _context; private readonly CatCafeContext _context;
private readonly IMapper _mapper; private readonly IMapper _mapper;
private readonly ILogger<CatsController> _logger; private readonly ILogger<CatsController> _logger;
private readonly WebSocketHandler _webSocketHandler;
public CatsController(CatCafeContext context, IMapper mapper, ILogger<CatsController> logger)
public CatsController(
CatCafeContext context,
IMapper mapper,
ILogger<CatsController> logger,
WebSocketHandler webSocketHandler
)
{ {
_mapper = mapper; _mapper = mapper;
_context = context; _context = context;
_logger = logger; _logger = logger;
_webSocketHandler = webSocketHandler;
} }
// GET: api/Cats // GET: api/Cats
@ -94,6 +102,8 @@ namespace cat_cafe.Controllers
_context.Cats.Add(cat); _context.Cats.Add(cat);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
await _webSocketHandler.BroadcastMessageAsync("entity-created");
return CreatedAtAction("GetCat", new { id = catDto.Id }, _mapper.Map<CatDto>(cat)); return CreatedAtAction("GetCat", new { id = catDto.Id }, _mapper.Map<CatDto>(cat));
} }

@ -2,13 +2,19 @@ using Microsoft.EntityFrameworkCore;
using cat_cafe.Repositories; using cat_cafe.Repositories;
using Serilog; using Serilog;
using Serilog.Sinks.File; using Serilog.Sinks.File;
using System.Net.WebSockets;
using cat_cafe.WeSo;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
Log.Logger = new LoggerConfiguration().MinimumLevel.Information().WriteTo.File("log.txt").CreateLogger(); Log.Logger = new LoggerConfiguration().MinimumLevel.Information().WriteTo.File("log.txt").CreateLogger();
List<WebSocket> _sockets = new();
// Add services to the container. // Add services to the container.
builder.Services.AddSingleton<List<WebSocket>>(x => _sockets);
builder.Services.AddSingleton<WebSocketHandler>();
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddDbContext<CatCafeContext>(opt => opt.UseInMemoryDatabase("CatCafe")); builder.Services.AddDbContext<CatCafeContext>(opt => opt.UseInMemoryDatabase("CatCafe"));
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
@ -33,6 +39,40 @@ app.UseAuthorization();
app.MapControllers(); app.MapControllers();
app.UseWebSockets();
app.Use(async (context, next) =>
{
if (context.Request.Path == "/ws")
{
if (context.WebSockets.IsWebSocketRequest)
{
WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
_sockets.Add(webSocket);
var buffer = new byte[1024 * 4];
WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
while (!result.CloseStatus.HasValue)
{
await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);
result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
}
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}
else
{
context.Response.StatusCode = 400;
}
}
else
{
await next();
}
});
Log.Information("program start"); Log.Information("program start");
app.Run(); app.Run();

@ -0,0 +1,28 @@
using System.Net.WebSockets;
using System.Text;
namespace cat_cafe.WeSo
{
public class WebSocketHandler
{
private readonly List<WebSocket> _sockets;
public WebSocketHandler(List<WebSocket> sockets)
{
_sockets = sockets;
}
public async Task BroadcastMessageAsync(string message)
{
var buffer = Encoding.UTF8.GetBytes(message);
foreach (var socket in _sockets)
{
if (socket.State == WebSocketState.Open)
{
await socket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
}
}
}
}
}

@ -7,6 +7,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<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" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.12" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.12" />

Loading…
Cancel
Save