🧦 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.UseWebSockets();
await app.UseOcelot();
app.Run();

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

@ -8,7 +8,7 @@
### 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
@ -18,8 +18,21 @@ We used an ASP .NET Web API, with a Swagger configuration to visualize the inter
### 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
An [Ocelot](https://ocelot.readthedocs.io/en/latest/) API Gateway manages the whole system.
@ -56,10 +69,19 @@ The Gateway routes offer access to the REST API in a similar way as the REST API
| REST(old) | Gateway(current) |
|--|--|
| `.../7229/api/...` | `.../5003/gateway/...` |
| `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}` |
...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
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.Threading.Tasks;
using FluentAssertions;
using cat_cafe.WeSo;
namespace cat_cafe.Controllers.Tests
{
@ -64,7 +64,7 @@ namespace cat_cafe.Controllers.Tests
{
mapper = mapperConf.CreateMapper();
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);
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 Newtonsoft.Json;
using Microsoft.Extensions.Logging.Abstractions;
using cat_cafe.WeSo;
namespace cat_cafe.Controllers
{
@ -22,12 +23,19 @@ namespace cat_cafe.Controllers
private readonly CatCafeContext _context;
private readonly IMapper _mapper;
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;
_context = context;
_logger = logger;
_webSocketHandler = webSocketHandler;
}
// GET: api/Cats
@ -94,6 +102,8 @@ namespace cat_cafe.Controllers
_context.Cats.Add(cat);
await _context.SaveChangesAsync();
await _webSocketHandler.BroadcastMessageAsync("entity-created");
return CreatedAtAction("GetCat", new { id = catDto.Id }, _mapper.Map<CatDto>(cat));
}

@ -2,13 +2,19 @@ using Microsoft.EntityFrameworkCore;
using cat_cafe.Repositories;
using Serilog;
using Serilog.Sinks.File;
using System.Net.WebSockets;
using cat_cafe.WeSo;
var builder = WebApplication.CreateBuilder(args);
Log.Logger = new LoggerConfiguration().MinimumLevel.Information().WriteTo.File("log.txt").CreateLogger();
List<WebSocket> _sockets = new();
// Add services to the container.
builder.Services.AddSingleton<List<WebSocket>>(x => _sockets);
builder.Services.AddSingleton<WebSocketHandler>();
builder.Services.AddControllers();
builder.Services.AddDbContext<CatCafeContext>(opt => opt.UseInMemoryDatabase("CatCafe"));
builder.Services.AddEndpointsApiExplorer();
@ -33,6 +39,40 @@ app.UseAuthorization();
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");
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>
<ItemGroup>
<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" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.12" />

Loading…
Cancel
Save