mirror of
https://github.com/yawaflua/WebSockets.git
synced 2025-12-08 19:39:30 +02:00
Create project files
This commit is contained in:
25
.dockerignore
Normal file
25
.dockerignore
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/.idea
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/azds.yaml
|
||||||
|
**/bin
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/obj
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
LICENSE
|
||||||
|
README.md
|
||||||
23
.github/workflows/dotnet.yml
vendored
Normal file
23
.github/workflows/dotnet.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: .NET Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "*" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "*" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: [6.0.x, 7.0.x, 8.0.x, 9.0.x]
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build --no-restore /p:TreatWarningsAsErrors=false
|
||||||
|
- name: Test
|
||||||
|
run: dotnet test --no-build --verbosity normal
|
||||||
48
.github/workflows/nuget.yml
vendored
Normal file
48
.github/workflows/nuget.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
name: CI/CD NuGet Package
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
env:
|
||||||
|
NUGET_SOURCE: https://api.nuget.org/v3/index.json
|
||||||
|
GITHUB_SOURCE: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v3
|
||||||
|
with:
|
||||||
|
dotnet-version: 7.0.x
|
||||||
|
|
||||||
|
- name: Extract Package Version
|
||||||
|
id: get_version
|
||||||
|
run: |
|
||||||
|
VERSION=${GITHUB_REF#refs/tags/v}
|
||||||
|
echo "PACKAGE_VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
|
echo "Using version: $VERSION"
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build --no-restore --configuration Release
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: dotnet test --no-build --verbosity normal --configuration Release
|
||||||
|
|
||||||
|
- name: Pack
|
||||||
|
run: dotnet pack --no-build --configuration Release -p:PackageVersion=${{ env.PACKAGE_VERSION }}
|
||||||
|
- name: Add GitHub source
|
||||||
|
run: dotnet nuget add source --username ${{ github.repository_owner }} --password ${{ secrets.GHCR_TOKEN }} --store-password-in-clear-text --name github ${{ env.GITHUB_SOURCE }}
|
||||||
|
|
||||||
|
- name: Publish to GitHub Packages
|
||||||
|
run: dotnet nuget push "**/*.nupkg" --source "github"
|
||||||
|
|
||||||
|
- name: Publish to NuGet
|
||||||
|
run: dotnet nuget push "**/*.nupkg" --source ${{ env.NUGET_SOURCE }} --api-key ${{ secrets.NUGET_API_KEY }}
|
||||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
/packages/
|
||||||
|
riderModule.iml
|
||||||
|
/_ReSharper.Caches/
|
||||||
13
.idea/.idea.yawaflua.WebSockets/.idea/.gitignore
generated
vendored
Normal file
13
.idea/.idea.yawaflua.WebSockets/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Rider ignored files
|
||||||
|
/modules.xml
|
||||||
|
/contentModel.xml
|
||||||
|
/.idea.yawaflua.WebSockets.iml
|
||||||
|
/projectSettingsUpdater.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
4
.idea/.idea.yawaflua.WebSockets/.idea/encodings.xml
generated
Normal file
4
.idea/.idea.yawaflua.WebSockets/.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||||
|
</project>
|
||||||
17
Examples/ChatController.cs
Normal file
17
Examples/ChatController.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using yawaflua.WebSockets.Attributes;
|
||||||
|
using yawaflua.WebSockets.Models.Abstracts;
|
||||||
|
using WebSocket = yawaflua.WebSockets.Core.WebSocket;
|
||||||
|
|
||||||
|
namespace Examples;
|
||||||
|
|
||||||
|
[WebSocket("/chat")]
|
||||||
|
public class ChatController : WebSocketController
|
||||||
|
{
|
||||||
|
|
||||||
|
public override async Task OnMessageAsync(
|
||||||
|
WebSocket webSocket,
|
||||||
|
HttpContext httpContext)
|
||||||
|
{
|
||||||
|
await WebSocketManager.Broadcast(k => k.Path == "/chat", $"{webSocket.Client.Id}: {webSocket.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Examples/Dockerfile
Normal file
21
Examples/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base
|
||||||
|
USER $APP_UID
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["Examples/Examples.csproj", "Examples/"]
|
||||||
|
RUN dotnet restore "Examples/Examples.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/Examples"
|
||||||
|
RUN dotnet build "Examples.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||||
|
|
||||||
|
FROM build AS publish
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
RUN dotnet publish "Examples.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "Examples.dll"]
|
||||||
22
Examples/Examples.csproj
Normal file
22
Examples/Examples.csproj
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="..\.dockerignore">
|
||||||
|
<Link>.dockerignore</Link>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\yawaflua.WebSockets\yawaflua.WebSockets.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
42
Examples/Program.cs
Normal file
42
Examples/Program.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using yawaflua.WebSockets;
|
||||||
|
|
||||||
|
namespace Examples;
|
||||||
|
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static async Task Main(string[] args)
|
||||||
|
{
|
||||||
|
await Host.CreateDefaultBuilder(args)
|
||||||
|
.ConfigureLogging(k => k.AddConsole().AddDebug())
|
||||||
|
.ConfigureWebHost(k =>
|
||||||
|
{
|
||||||
|
k.UseKestrel(l => l.ListenAnyIP(80));
|
||||||
|
k.UseStartup<Startup>();
|
||||||
|
})
|
||||||
|
.RunConsoleAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class Startup
|
||||||
|
{
|
||||||
|
public void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.SettingUpWebSockets();
|
||||||
|
services.AddRouting();
|
||||||
|
services.AddHttpLogging();
|
||||||
|
services.AddSingleton<TestWebSocketServer>();
|
||||||
|
services.AddSingleton<ChatController>();
|
||||||
|
services.AddScoped<IConfiguration>(k => new ConfigurationBuilder()
|
||||||
|
.AddJsonFile("appsettings.json", true)
|
||||||
|
.Build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
app.ConnectWebSockets();
|
||||||
|
app.UseRouting();
|
||||||
|
app.UseHttpLogging();
|
||||||
|
app.UseWelcomePage();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
18
Examples/TestWebSocketServer.cs
Normal file
18
Examples/TestWebSocketServer.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using yawaflua.WebSockets.Attributes;
|
||||||
|
using yawaflua.WebSockets.Core;
|
||||||
|
using yawaflua.WebSockets.Models.Abstracts;
|
||||||
|
|
||||||
|
namespace Examples;
|
||||||
|
|
||||||
|
[WebSocket("/test")]
|
||||||
|
public class TestWebSocketServer : WebSocketController
|
||||||
|
{
|
||||||
|
|
||||||
|
[WebSocket("/sub-test")]
|
||||||
|
public override async Task OnMessageAsync(WebSocket webSocket, HttpContext httpContext)
|
||||||
|
{
|
||||||
|
await webSocket.SendAsync("Test! Now on it endpoint: " + WebSocketManager.GetAllClients().Count(k => k.Path == webSocket.Client.Path));
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Examples/appsettings.json
Normal file
1
Examples/appsettings.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
121
README.md
Normal file
121
README.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# New WebSocket routing system
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- ASP.NET Core-style WebSocket routing 🛣️
|
||||||
|
- Method-based endpoint handlers (Like in ASP!) 🎯
|
||||||
|
- Simple integration with existing applications ⚡
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
Add the NuGet package to your project:
|
||||||
|
```bash
|
||||||
|
dotnet add package yawaflua.WebSockets
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Create WebSocket Controller
|
||||||
|
```csharp
|
||||||
|
public class ChatController : WebSocketController
|
||||||
|
{
|
||||||
|
[WebSocket("/chat")]
|
||||||
|
public override async Task OnMessageAsync(
|
||||||
|
WebSocket webSocket,
|
||||||
|
HttpContext httpContext)
|
||||||
|
{
|
||||||
|
await webSocket.SendAsync("Message!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Configure Services
|
||||||
|
```csharp
|
||||||
|
public void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services
|
||||||
|
.AddControllers()
|
||||||
|
.SettingUpWebSockets(); // ← Add WebSocket routing
|
||||||
|
|
||||||
|
services.AddSingleton<ChatController>();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Enable Middleware
|
||||||
|
```csharp
|
||||||
|
public void Configure(IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
app.ConnectWebSockets(); // ← Add WebSocket handling
|
||||||
|
|
||||||
|
app.UseRouting();
|
||||||
|
app.UseEndpoints(endpoints => endpoints.MapControllers());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
### Parameterized Routes
|
||||||
|
```csharp
|
||||||
|
public class NotificationsController : WebSocketController
|
||||||
|
{
|
||||||
|
[WebSocket("/notifications/{userId}")]
|
||||||
|
public override async Task OnMessageAsync(
|
||||||
|
WebSocket webSocket,
|
||||||
|
HttpContext httpContext)
|
||||||
|
{
|
||||||
|
var userId = httpContext.Request.RouteValues["userId"];
|
||||||
|
// Handle user-specific notifications
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method-Level Routes
|
||||||
|
```csharp
|
||||||
|
[WebSocket("/game")]
|
||||||
|
public class GameController : WebSocketController
|
||||||
|
{
|
||||||
|
[WebSocket("join/{roomId}")]
|
||||||
|
public async Task JoinRoom(WebSocket webSocket, HttpContext context)
|
||||||
|
{
|
||||||
|
// Handle room joining
|
||||||
|
}
|
||||||
|
|
||||||
|
[WebSocket("leave/{roomId}")]
|
||||||
|
public async Task LeaveRoom(WebSocket webSocket, HttpContext context)
|
||||||
|
{
|
||||||
|
// Handle room leaving
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle Management
|
||||||
|
1. **Connection** - Automatically handled by middleware
|
||||||
|
2. **Message Handling** - Implement `OnMessageAsync`
|
||||||
|
3. **Cleanup** - Dispose resources in `IDisposable` interface
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
1. **Keep Controllers Light** - Move business logic to services
|
||||||
|
2. **Use Dependency Injection** - Inject services via constructor
|
||||||
|
3. **Handle Exceptions** - Wrap operations in try/catch blocks
|
||||||
|
4. **Manage State** - Use `HttpContext.Items` for request-scoped data
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
**No Route Handling?**
|
||||||
|
- Verify controller registration in DI:
|
||||||
|
```csharp
|
||||||
|
services.AddSingleton<YourController>();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Connection Issues?**
|
||||||
|
- Ensure middleware order:
|
||||||
|
```csharp
|
||||||
|
app.ConnectWebSockets(); // Must be before UseRouting/UseEndpoints
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters Not Working?**
|
||||||
|
- Check route template syntax:
|
||||||
|
```csharp
|
||||||
|
[WebSocket("/correct/{paramName}")] // ✓
|
||||||
|
[WebSocket("/wrong/{param-name}")] // ✗
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
MIT License - Free for commercial and personal use.
|
||||||
7
global.json
Normal file
7
global.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"sdk": {
|
||||||
|
"version": "9.0.0",
|
||||||
|
"rollForward": "latestMinor",
|
||||||
|
"allowPrerelease": false
|
||||||
|
}
|
||||||
|
}
|
||||||
162
yawaflua.WebSockets.Tests/Core/WebSocketManagerTest.cs
Normal file
162
yawaflua.WebSockets.Tests/Core/WebSocketManagerTest.cs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
using yawaflua.WebSockets.Core;
|
||||||
|
using System;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
using yawaflua.WebSockets.Attributes;
|
||||||
|
using yawaflua.WebSockets.Models.Abstracts;
|
||||||
|
using Assert = Xunit.Assert;
|
||||||
|
using WebSocket = yawaflua.WebSockets.Core.WebSocket;
|
||||||
|
using WebSocketManager = Microsoft.AspNetCore.Http.WebSocketManager;
|
||||||
|
|
||||||
|
namespace yawaflua.WebSockets.Tests.Core;
|
||||||
|
|
||||||
|
|
||||||
|
[TestSubject(typeof(WebSocketRouter))]
|
||||||
|
public class WebSocketRouterTests
|
||||||
|
{
|
||||||
|
private readonly Mock<IServiceProvider> _serviceProviderMock = new();
|
||||||
|
private readonly Mock<ILogger<WebSocketRouter>> _loggerMock = new();
|
||||||
|
private IServiceCollection _services;
|
||||||
|
|
||||||
|
public WebSocketRouterTests()
|
||||||
|
{
|
||||||
|
_services = new ServiceCollection();
|
||||||
|
_serviceProviderMock.Setup(k => k.GetService(typeof(IServiceScopeFactory)))
|
||||||
|
.Returns(_services.BuildServiceProvider().CreateScope());
|
||||||
|
}
|
||||||
|
[yawaflua.WebSockets.Attributes.WebSocket("/test")]
|
||||||
|
public class TestHandler : WebSocketController
|
||||||
|
{
|
||||||
|
[yawaflua.WebSockets.Attributes.WebSocket("/static")]
|
||||||
|
public static Task StaticHandler(WebSocket ws, HttpContext context) => Task.CompletedTask;
|
||||||
|
|
||||||
|
[yawaflua.WebSockets.Attributes.WebSocket("/instance")]
|
||||||
|
public Task InstanceHandler(WebSocket ws, HttpContext context) => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DiscoverHandlers_ShouldRegisterStaticAndInstanceMethods()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
_services.AddTransient<TestHandler>();
|
||||||
|
_serviceProviderMock.Setup(x => x.GetService(typeof(TestHandler)))
|
||||||
|
.Returns(new TestHandler());
|
||||||
|
// Act
|
||||||
|
var router = new WebSocketRouter(_serviceProviderMock.Object, _loggerMock.Object);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(WebSocketRouter.Routes.ContainsKey("/test/static"));
|
||||||
|
Assert.True(WebSocketRouter.Routes.ContainsKey("/test/instance"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleRequest_ShouldAcceptWebSocketAndAddClient()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var webSocketMock = new Mock<System.Net.WebSockets.WebSocket>();
|
||||||
|
var contextMock = new Mock<HttpContext>();
|
||||||
|
var webSocketManagerMock = new Mock<WebSocketManager>() { CallBase = true };
|
||||||
|
|
||||||
|
webSocketManagerMock.Setup(m => m.AcceptWebSocketAsync())
|
||||||
|
.ReturnsAsync(webSocketMock.Object);
|
||||||
|
webSocketManagerMock.Setup(m => m.IsWebSocketRequest)
|
||||||
|
.Returns(true);
|
||||||
|
contextMock.SetupGet(c => c.WebSockets).Returns(webSocketManagerMock.Object);
|
||||||
|
contextMock.SetupGet(c => c.Request.Path).Returns(new PathString("/test/static"));
|
||||||
|
contextMock.Setup(c => c.RequestServices)
|
||||||
|
.Returns(_serviceProviderMock.Object);
|
||||||
|
|
||||||
|
var router = new WebSocketRouter(_services.BuildServiceProvider(), _loggerMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await router.HandleRequest(contextMock.Object);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(WebSocketRouter.Clients);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HandleRequest_ShouldReturn404ForUnknownPath()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var contextMock = new Mock<HttpContext>();
|
||||||
|
var responseMock = new Mock<HttpResponse>();
|
||||||
|
var webSocketManagerMock = new Mock<WebSocketManager>();
|
||||||
|
|
||||||
|
webSocketManagerMock.Setup(m => m.IsWebSocketRequest).Returns(true);
|
||||||
|
contextMock.SetupGet(c => c.WebSockets).Returns(webSocketManagerMock.Object);
|
||||||
|
contextMock.SetupGet(c => c.Request.Path).Returns(new PathString("/unknown"));
|
||||||
|
contextMock.SetupGet(c => c.Response).Returns(responseMock.Object);
|
||||||
|
|
||||||
|
var router = new WebSocketRouter(_services.BuildServiceProvider(), _loggerMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await router.HandleRequest(contextMock.Object);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
responseMock.VerifySet(r => r.StatusCode = 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DiscoverHandlers_ShouldLogErrorOnInvalidHandler()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var invalidHandlerType = typeof(InvalidHandler);
|
||||||
|
_serviceProviderMock.Setup(x => x.GetService(invalidHandlerType))
|
||||||
|
.Throws(new InvalidOperationException());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var router = new WebSocketRouter(_serviceProviderMock.Object, _loggerMock.Object);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
_loggerMock.Verify(
|
||||||
|
x => x.Log(
|
||||||
|
LogLevel.Critical,
|
||||||
|
It.IsAny<EventId>(),
|
||||||
|
It.IsAny<It.IsAnyType>(),
|
||||||
|
It.IsAny<Exception>(),
|
||||||
|
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()),
|
||||||
|
Times.AtLeastOnce);
|
||||||
|
}
|
||||||
|
|
||||||
|
[WebSocket("/invalid")]
|
||||||
|
public class InvalidHandler : WebSocketController
|
||||||
|
{
|
||||||
|
[WebSocket("/handler")]
|
||||||
|
public void InvalidMethod() { } // Invalid signature
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Client_ShouldBeRemovedOnConnectionClose()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var webSocketMock = new Mock<System.Net.WebSockets.WebSocket>();
|
||||||
|
webSocketMock.Setup(ws => ws.State).Returns(WebSocketState.Open);
|
||||||
|
webSocketMock.Setup(ws => ws.ReceiveAsync(It.IsAny<ArraySegment<byte>>(), It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new WebSocketReceiveResult(0, WebSocketMessageType.Close, true));
|
||||||
|
|
||||||
|
var contextMock = new Mock<HttpContext>();
|
||||||
|
var webSocketManagerMock = new Mock<WebSocketManager>();
|
||||||
|
|
||||||
|
webSocketManagerMock.Setup(m => m.AcceptWebSocketAsync()).ReturnsAsync(webSocketMock.Object);
|
||||||
|
contextMock.SetupGet(c => c.WebSockets).Returns(webSocketManagerMock.Object);
|
||||||
|
contextMock.SetupGet(c => c.Request.Path).Returns(new PathString("/test/static"));
|
||||||
|
contextMock.Setup(c => c.RequestServices).Returns(_serviceProviderMock.Object);
|
||||||
|
|
||||||
|
var router = new WebSocketRouter(_serviceProviderMock.Object, _loggerMock.Object);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await router.HandleRequest(contextMock.Object);
|
||||||
|
await Task.Delay(100); // Allow background task to complete
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(WebSocketRouter.Clients);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
yawaflua.WebSockets.Tests/ServiceBindingsTest.cs
Normal file
12
yawaflua.WebSockets.Tests/ServiceBindingsTest.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using yawaflua.WebSockets;
|
||||||
|
|
||||||
|
namespace yawaflua.WebSockets.Tests;
|
||||||
|
|
||||||
|
public class ServiceBindingsTest
|
||||||
|
{
|
||||||
|
|
||||||
|
public void TestAddingInServiceProviderNewHosts()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
23
yawaflua.WebSockets.Tests/yawaflua.WebSockets.Tests.csproj
Normal file
23
yawaflua.WebSockets.Tests/yawaflua.WebSockets.Tests.csproj
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.0" PrivateAssets="all"/>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.3.0" PrivateAssets="all"/>
|
||||||
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\yawaflua.WebSockets\yawaflua.WebSockets.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
28
yawaflua.WebSockets.sln
Normal file
28
yawaflua.WebSockets.sln
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "yawaflua.WebSockets", "yawaflua.WebSockets\yawaflua.WebSockets.csproj", "{D9E47608-7769-45DD-8E35-4DA90127D9E0}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples", "Examples\Examples.csproj", "{D90CE32E-BF1F-4129-8FB1-0DD6BD816830}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "yawaflua.WebSockets.Tests", "yawaflua.WebSockets.Tests\yawaflua.WebSockets.Tests.csproj", "{B4B37A81-4147-4C19-91BC-D897CAEBEE6A}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{D9E47608-7769-45DD-8E35-4DA90127D9E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D9E47608-7769-45DD-8E35-4DA90127D9E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D9E47608-7769-45DD-8E35-4DA90127D9E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D9E47608-7769-45DD-8E35-4DA90127D9E0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D90CE32E-BF1F-4129-8FB1-0DD6BD816830}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D90CE32E-BF1F-4129-8FB1-0DD6BD816830}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D90CE32E-BF1F-4129-8FB1-0DD6BD816830}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D90CE32E-BF1F-4129-8FB1-0DD6BD816830}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B4B37A81-4147-4C19-91BC-D897CAEBEE6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B4B37A81-4147-4C19-91BC-D897CAEBEE6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B4B37A81-4147-4C19-91BC-D897CAEBEE6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B4B37A81-4147-4C19-91BC-D897CAEBEE6A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
40
yawaflua.WebSockets/Attributes/WebSocketAttribute.cs
Normal file
40
yawaflua.WebSockets/Attributes/WebSocketAttribute.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using JetBrains.Annotations;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace yawaflua.WebSockets.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute that marks WebSocket endpoint handlers.
|
||||||
|
/// Applied to classes or methods to define WebSocket route templates.
|
||||||
|
///
|
||||||
|
/// Usage examples:
|
||||||
|
/// [WebSocket("/chat")] - basic route
|
||||||
|
/// [WebSocket("/notifications/{userId}")] - route with parameter
|
||||||
|
/// [WebSocket("/game/{roomId}/players")] - complex route
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Inherits from ASP.NET Core's RouteAttribute to leverage standard routing syntax.
|
||||||
|
/// When applied to a class, defines the base path for all WebSocket endpoints in controller.
|
||||||
|
/// When applied to methods, defines specific sub-routes (requires class-level base path).
|
||||||
|
/// </remarks>
|
||||||
|
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||||
|
public class WebSocketAttribute : RouteAttribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Original route template specified in attribute
|
||||||
|
/// </summary>
|
||||||
|
public string Template { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates WebSocket route definition
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">Route template using ASP.NET Core syntax:
|
||||||
|
/// - Static: "/status"
|
||||||
|
/// - Parameters: "/user/{id}"
|
||||||
|
/// - Constraints: "/file/{name:alpha}"
|
||||||
|
/// - Optional: "/feed/{category?}"</param>
|
||||||
|
public WebSocketAttribute([RouteTemplate]string path) : base(path)
|
||||||
|
{
|
||||||
|
Template = path;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
yawaflua.WebSockets/Core/Middleware/WebSocketMiddleware.cs
Normal file
25
yawaflua.WebSockets/Core/Middleware/WebSocketMiddleware.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace yawaflua.WebSockets.Core.Middleware;
|
||||||
|
|
||||||
|
public class WebSocketMiddleware : IMiddleware
|
||||||
|
{
|
||||||
|
private readonly WebSocketRouter _router;
|
||||||
|
|
||||||
|
public WebSocketMiddleware(WebSocketRouter router)
|
||||||
|
{
|
||||||
|
_router = router;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
||||||
|
{
|
||||||
|
if (context.WebSockets.IsWebSocketRequest)
|
||||||
|
{
|
||||||
|
await _router.HandleRequest(context);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await next(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
yawaflua.WebSockets/Core/WebSocket.cs
Normal file
43
yawaflua.WebSockets/Core/WebSocket.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using yawaflua.WebSockets.Models.Interfaces;
|
||||||
|
|
||||||
|
namespace yawaflua.WebSockets.Core;
|
||||||
|
|
||||||
|
public class WebSocket : IDisposable
|
||||||
|
{
|
||||||
|
private readonly System.Net.WebSockets.WebSocket _webSocket;
|
||||||
|
private readonly WebSocketReceiveResult? _webSocketReceiveResult;
|
||||||
|
private readonly string? _message;
|
||||||
|
|
||||||
|
public WebSocketState State => _webSocket.State;
|
||||||
|
public WebSocketCloseStatus? CloseStatus => _webSocket.CloseStatus;
|
||||||
|
public string? SubProtocol => _webSocket.SubProtocol;
|
||||||
|
public string? CloseStatusDescription => _webSocket.CloseStatusDescription;
|
||||||
|
public string? Message => _message;
|
||||||
|
public WebSocketMessageType? MessageType => _webSocketReceiveResult?.MessageType;
|
||||||
|
public IWebSocketClient Client;
|
||||||
|
internal WebSocket(System.Net.WebSockets.WebSocket webSocket, WebSocketReceiveResult? webSocketReceiveResult, string? message, IWebSocketClient client)
|
||||||
|
{
|
||||||
|
_webSocket = webSocket;
|
||||||
|
_webSocketReceiveResult = webSocketReceiveResult;
|
||||||
|
_message = message;
|
||||||
|
Client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendAsync(string m, WebSocketMessageType messageType = WebSocketMessageType.Text, CancellationToken cts = default)
|
||||||
|
=> await _webSocket.SendAsync(
|
||||||
|
Encoding.UTF8.GetBytes(m),
|
||||||
|
messageType,
|
||||||
|
true,
|
||||||
|
cts);
|
||||||
|
|
||||||
|
public async Task CloseAsync(WebSocketCloseStatus closeStatus = WebSocketCloseStatus.NormalClosure, string? reason = null, CancellationToken cts = default)
|
||||||
|
=> await _webSocket.CloseAsync(closeStatus, reason, cts);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public void Abort() => _webSocket.Abort();
|
||||||
|
public void Dispose() => _webSocket.Dispose();
|
||||||
|
|
||||||
|
}
|
||||||
34
yawaflua.WebSockets/Core/WebSocketManager.cs
Normal file
34
yawaflua.WebSockets/Core/WebSocketManager.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using yawaflua.WebSockets.Models.Interfaces;
|
||||||
|
|
||||||
|
namespace yawaflua.WebSockets.Core;
|
||||||
|
|
||||||
|
internal class WebSocketManager : IWebSocketManager
|
||||||
|
{
|
||||||
|
public async Task Broadcast(Func<IWebSocketClient, bool> selector, string message,
|
||||||
|
WebSocketMessageType messageType = WebSocketMessageType.Text, CancellationToken cts = default)
|
||||||
|
{
|
||||||
|
foreach (var client in WebSocketRouter.Clients.Where(selector))
|
||||||
|
{
|
||||||
|
await client.webSocket.SendAsync(Encoding.UTF8.GetBytes(message),
|
||||||
|
messageType,
|
||||||
|
true,
|
||||||
|
cts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<IWebSocketClient> GetAllClients()
|
||||||
|
{
|
||||||
|
return WebSocketRouter.Clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SendToUser(Guid id, string message, WebSocketMessageType messageType = WebSocketMessageType.Text, CancellationToken cts = default)
|
||||||
|
{
|
||||||
|
await WebSocketRouter.Clients.First(k => k.Id == id).webSocket.SendAsync(Encoding.UTF8.GetBytes(message),
|
||||||
|
messageType,
|
||||||
|
true,
|
||||||
|
cts);
|
||||||
|
}
|
||||||
|
}
|
||||||
182
yawaflua.WebSockets/Core/WebSocketRouter.cs
Normal file
182
yawaflua.WebSockets/Core/WebSocketRouter.cs
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using yawaflua.WebSockets.Attributes;
|
||||||
|
using yawaflua.WebSockets.Models;
|
||||||
|
using yawaflua.WebSockets.Models.Abstracts;
|
||||||
|
using yawaflua.WebSockets.Models.Interfaces;
|
||||||
|
|
||||||
|
namespace yawaflua.WebSockets.Core;
|
||||||
|
|
||||||
|
public class WebSocketRouter
|
||||||
|
{
|
||||||
|
internal static readonly Dictionary<string, Func<WebSocket, HttpContext, Task>> Routes = new();
|
||||||
|
internal static readonly List<IWebSocketClient> Clients = new();
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ILogger<WebSocketRouter> _logger;
|
||||||
|
|
||||||
|
public WebSocketRouter(IServiceProvider serviceProvider, ILogger<WebSocketRouter> logger)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
this._logger = logger;
|
||||||
|
DiscoverHandlers();
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
Clients.ForEach(async l =>
|
||||||
|
{
|
||||||
|
await l.webSocket.SendAsync(ArraySegment<byte>.Empty, WebSocketMessageType.Binary,
|
||||||
|
WebSocketMessageFlags.EndOfMessage, default);
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(10));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void DiscoverHandlers()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var handlerTypes = AppDomain.CurrentDomain.GetAssemblies()
|
||||||
|
.SelectMany(a => a.GetTypes())
|
||||||
|
.Where(t =>
|
||||||
|
t.IsSubclassOf(typeof(IWebSocketController))
|
||||||
|
|| t.IsSubclassOf(typeof(WebSocketController))
|
||||||
|
|| t.IsInstanceOfType(typeof(WebSocketController))
|
||||||
|
|| t.IsInstanceOfType(typeof(IWebSocketController))
|
||||||
|
);
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
foreach (var type in handlerTypes.Where(k => k.GetMethods().Length > 0))
|
||||||
|
{
|
||||||
|
var parentAttributeTemplate =
|
||||||
|
new PathString((type.GetCustomAttribute(typeof(WebSocketAttribute)) as WebSocketAttribute)?.Template ?? "/");
|
||||||
|
var methods = type.GetMethods()
|
||||||
|
.Where(m => m.GetCustomAttributes(typeof(WebSocketAttribute), false).Length > 0).ToList();
|
||||||
|
|
||||||
|
|
||||||
|
if (methods.Count == 0 && type.GetMethods().Any(k => k.Name.StartsWith("OnMessage")))
|
||||||
|
{
|
||||||
|
var func = type.GetMethods()
|
||||||
|
.First(k => k.Name.StartsWith("OnMessage"));
|
||||||
|
|
||||||
|
var parameters = func.GetParameters();
|
||||||
|
if (parameters.Length != 2 ||
|
||||||
|
parameters[0].ParameterType != typeof(WebSocket) ||
|
||||||
|
parameters[1].ParameterType != typeof(HttpContext) ||
|
||||||
|
func.ReturnType != typeof(Task))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Invalid handler signature in {type.Name}.{func.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (func.IsStatic)
|
||||||
|
{
|
||||||
|
var delegateFunc = (Func<WebSocket, HttpContext, Task>)Delegate.CreateDelegate(
|
||||||
|
typeof(Func<WebSocket, HttpContext, Task>),
|
||||||
|
func
|
||||||
|
);
|
||||||
|
Routes.Add(parentAttributeTemplate, delegateFunc);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Routes.Add(parentAttributeTemplate, async (ws, context) =>
|
||||||
|
{
|
||||||
|
var instance = context.RequestServices.GetRequiredService(type);
|
||||||
|
await (Task)func.Invoke(instance, new object[] { ws, context })!;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var method in methods)
|
||||||
|
{
|
||||||
|
var attribute =
|
||||||
|
(WebSocketAttribute)method.GetCustomAttributes(typeof(WebSocketAttribute), false).First();
|
||||||
|
if (method.IsStatic)
|
||||||
|
{
|
||||||
|
var delegateFunc = (Func<WebSocket, HttpContext, Task>)Delegate.CreateDelegate(
|
||||||
|
typeof(Func<WebSocket, HttpContext, Task>),
|
||||||
|
method
|
||||||
|
);
|
||||||
|
Routes.Add(parentAttributeTemplate+attribute.Template, delegateFunc);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Routes.Add(parentAttributeTemplate+attribute.Template, async (ws, context) =>
|
||||||
|
{
|
||||||
|
var instance = context.RequestServices.GetRequiredService(type);
|
||||||
|
await (Task)method.Invoke(instance, new object[] { ws, context })!;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var constructors = type.GetConstructors();
|
||||||
|
if (constructors.Length != 0)
|
||||||
|
{
|
||||||
|
var parameters = constructors[0].GetParameters()
|
||||||
|
.Select(param => scope.ServiceProvider.GetRequiredService(param.ParameterType))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
constructors[0].Invoke(parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogCritical(message:"Error when parsing attributes from assemblies: ", exception:ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task HandleRequest(HttpContext context, CancellationToken cts = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!context.WebSockets.IsWebSocketRequest)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var path = context.Request.Path.Value;
|
||||||
|
|
||||||
|
if (Routes.TryGetValue(path, out var handler))
|
||||||
|
{
|
||||||
|
var webSocket = await context.WebSockets.AcceptWebSocketAsync();
|
||||||
|
await Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = new WebSocketClient(context, webSocket, path);
|
||||||
|
Clients.Add(client);
|
||||||
|
var buffer = new byte[1024 * 4];
|
||||||
|
while (webSocket.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cts);
|
||||||
|
if (result.MessageType != WebSocketMessageType.Close)
|
||||||
|
await handler(
|
||||||
|
new WebSocket(
|
||||||
|
webSocket,
|
||||||
|
result,
|
||||||
|
Encoding.UTF8.GetString(buffer, 0, result.Count),
|
||||||
|
client),
|
||||||
|
context);
|
||||||
|
else
|
||||||
|
Clients.Remove(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(message:"Error with handling request: ",exception: ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, cts);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error when handle request {context.Connection.Id}: ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
yawaflua.WebSockets/Models/Abstracts/WebSocketController.cs
Normal file
16
yawaflua.WebSockets/Models/Abstracts/WebSocketController.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using yawaflua.WebSockets.Core;
|
||||||
|
using yawaflua.WebSockets.Models.Interfaces;
|
||||||
|
using WebSocketManager = yawaflua.WebSockets.Core.WebSocketManager;
|
||||||
|
|
||||||
|
namespace yawaflua.WebSockets.Models.Abstracts;
|
||||||
|
|
||||||
|
public abstract class WebSocketController : IWebSocketController
|
||||||
|
{
|
||||||
|
public IWebSocketManager WebSocketManager => new WebSocketManager();
|
||||||
|
|
||||||
|
public virtual Task OnMessageAsync(WebSocket webSocket, HttpContext httpContext)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
yawaflua.WebSockets/Models/Interfaces/IWebSocketClient.cs
Normal file
15
yawaflua.WebSockets/Models/Interfaces/IWebSocketClient.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Net.WebSockets;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace yawaflua.WebSockets.Models.Interfaces;
|
||||||
|
|
||||||
|
public interface IWebSocketClient
|
||||||
|
{
|
||||||
|
public Guid Id { get; }
|
||||||
|
public string Path { get; }
|
||||||
|
public ConnectionInfo? ConnectionInfo { get; }
|
||||||
|
public IDictionary<object, object>? Items { get; set; }
|
||||||
|
public HttpRequest? HttpRequest { get; }
|
||||||
|
internal WebSocket webSocket { get; }
|
||||||
|
public Task Abort();
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using yawaflua.WebSockets.Core;
|
||||||
|
|
||||||
|
namespace yawaflua.WebSockets.Models.Interfaces;
|
||||||
|
|
||||||
|
internal interface IWebSocketController
|
||||||
|
{
|
||||||
|
Task OnMessageAsync(WebSocket webSocket, HttpContext httpContext);
|
||||||
|
}
|
||||||
11
yawaflua.WebSockets/Models/Interfaces/IWebSocketManager.cs
Normal file
11
yawaflua.WebSockets/Models/Interfaces/IWebSocketManager.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
|
||||||
|
namespace yawaflua.WebSockets.Models.Interfaces;
|
||||||
|
|
||||||
|
public interface IWebSocketManager
|
||||||
|
{
|
||||||
|
public Task Broadcast(Func<IWebSocketClient, bool> selector,string message, WebSocketMessageType messageType = WebSocketMessageType.Text, CancellationToken cts = default);
|
||||||
|
public List<IWebSocketClient> GetAllClients();
|
||||||
|
public Task SendToUser(Guid id, string message, WebSocketMessageType messageType, CancellationToken cts);
|
||||||
|
}
|
||||||
34
yawaflua.WebSockets/Models/WebSocketClient.cs
Normal file
34
yawaflua.WebSockets/Models/WebSocketClient.cs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
using System.Net.WebSockets;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using yawaflua.WebSockets.Models.Interfaces;
|
||||||
|
|
||||||
|
namespace yawaflua.WebSockets.Models;
|
||||||
|
|
||||||
|
internal class WebSocketClient : IWebSocketClient
|
||||||
|
{
|
||||||
|
private HttpContext HttpContext { get; set; }
|
||||||
|
public Guid Id { get; } = Guid.NewGuid();
|
||||||
|
public string Path { get; }
|
||||||
|
public ConnectionInfo ConnectionInfo { get => HttpContext.Connection; }
|
||||||
|
public IDictionary<object, object> Items
|
||||||
|
{
|
||||||
|
get => HttpContext.Items;
|
||||||
|
set => HttpContext.Items = (value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpRequest HttpRequest { get => HttpContext.Request; }
|
||||||
|
public WebSocket webSocket { get; }
|
||||||
|
|
||||||
|
internal WebSocketClient(HttpContext httpContext, WebSocket webSocket, string path)
|
||||||
|
{
|
||||||
|
this.webSocket = webSocket;
|
||||||
|
Path = path;
|
||||||
|
this.HttpContext = httpContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Abort()
|
||||||
|
{
|
||||||
|
webSocket.Abort();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
yawaflua.WebSockets/Properties/AssemblyInfo.cs
Normal file
1
yawaflua.WebSockets/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("yawaflua.WebSockets.Tests")]
|
||||||
25
yawaflua.WebSockets/ServiceBindings.cs
Normal file
25
yawaflua.WebSockets/ServiceBindings.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using yawaflua.WebSockets.Core;
|
||||||
|
using yawaflua.WebSockets.Core.Middleware;
|
||||||
|
using yawaflua.WebSockets.Models.Interfaces;
|
||||||
|
|
||||||
|
namespace yawaflua.WebSockets;
|
||||||
|
|
||||||
|
public static class ServiceBindings
|
||||||
|
{
|
||||||
|
public static IServiceCollection SettingUpWebSockets(this IServiceCollection isc)
|
||||||
|
{
|
||||||
|
isc.AddSingleton<WebSocketRouter>();
|
||||||
|
isc.AddScoped<IWebSocketManager, WebSocketManager>();
|
||||||
|
isc.AddSingleton<WebSocketMiddleware>();
|
||||||
|
return isc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IApplicationBuilder ConnectWebSockets(this IApplicationBuilder iab)
|
||||||
|
{
|
||||||
|
iab.UseWebSockets();
|
||||||
|
iab.UseMiddleware<WebSocketMiddleware>();
|
||||||
|
return iab;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
yawaflua.WebSockets/yawaflua.WebSockets.csproj
Normal file
30
yawaflua.WebSockets/yawaflua.WebSockets.csproj
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<Title>yawaflua.WebSockets</Title>
|
||||||
|
<Description>New AspNet controllers looks like websocket manager </Description>
|
||||||
|
<Copyright>Dmitrii Shimanskii</Copyright>
|
||||||
|
<PackageProjectUrl>https://github.com/yawaflua/WebSockets</PackageProjectUrl>
|
||||||
|
<PackageLicenseUrl>https://github.com/yawaflua/WebSockets/LICENCE</PackageLicenseUrl>
|
||||||
|
<RepositoryUrl>https://github.com/yawaflua/WebSockets</RepositoryUrl>
|
||||||
|
<PackageTags>websocket</PackageTags>
|
||||||
|
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="JetBrains.Annotations" Version="(2023.3.0,)"/>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="(2.1.7,)" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="(6.0.0,)" />
|
||||||
|
<PackageReference Include="System.Net.WebSockets" Version="(4.0.0,)"/>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="(2.2.0,)" PrivateAssets="all">
|
||||||
|
<Private>False</Private>
|
||||||
|
</PackageReference>
|
||||||
|
<None Include="..\README.md" Pack="true" PackagePath="\"/>
|
||||||
|
<None Include="..\LICENSE" Pack="true" PackagePath=""/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user