Skip to content

Commit

Permalink
Merge pull request #55 from engineering87/develop
Browse files Browse the repository at this point in the history
Decouple API and SignalR with an In-Memory Queue
  • Loading branch information
engineering87 authored Dec 11, 2024
2 parents 947b713 + 253aaba commit 5f476ae
Show file tree
Hide file tree
Showing 11 changed files with 278 additions and 43 deletions.
2 changes: 1 addition & 1 deletion src/WART-Client/WART-Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.3.0" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/WART-Client/WartTestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public static async Task ConnectAsync(string wartHubUrl)
}
catch (Exception e)
{
Console.WriteLine(e);
Console.WriteLine(e.Message);
}

await Task.CompletedTask;
Expand Down
2 changes: 1 addition & 1 deletion src/WART-Client/WartTestClientJwt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public static async Task ConnectAsync(string wartHubUrl, string key)
}
catch (Exception e)
{
Console.WriteLine(e);
Console.WriteLine(e.Message);
}

await Task.CompletedTask;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using System.Linq;
using WART_Core.Hubs;
using WART_Core.Services;

namespace WART_Core.Authentication.JWT
{
Expand Down Expand Up @@ -79,6 +81,12 @@ public static IServiceCollection AddJwtMiddleware(this IServiceCollection servic
};
});

// Register WART event queue as a singleton service.
services.AddSingleton<WartEventQueueService>();

// Register the WART event worker as a hosted service.
services.AddHostedService<WartEventWorker<WartHubJwt>>();

// Configure SignalR options, including error handling and timeouts
services.AddSignalR(options =>
{
Expand Down
47 changes: 8 additions & 39 deletions src/WART-Core/Controllers/WartBaseController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WART_Core.Entity;
using WART_Core.Filters;
using Microsoft.Extensions.DependencyInjection;
using WART_Core.Services;

namespace WART_Core.Controllers
{
Expand All @@ -19,6 +18,8 @@ public abstract class WartBaseController<THub> : Controller where THub : Hub
private readonly IHubContext<THub> _hubContext;
private const string RouteDataKey = "REQUEST";

private WartEventQueueService _eventQueue;

protected WartBaseController(IHubContext<THub> hubContext, ILogger logger)
{
_hubContext = hubContext;
Expand All @@ -39,7 +40,7 @@ public override void OnActionExecuting(ActionExecutingContext context)
/// Processes the executed action and sends the event to the SignalR hub if applicable.
/// </summary>
/// <param name="context">The action executed context.</param>
public override async void OnActionExecuted(ActionExecutedContext context)
public override void OnActionExecuted(ActionExecutedContext context)
{
if (context?.Result is ObjectResult objectResult)
{
Expand All @@ -52,45 +53,13 @@ public override async void OnActionExecuted(ActionExecutedContext context)
var response = objectResult.Value;

var wartEvent = new WartEvent(request, response, httpMethod, httpPath, remoteAddress);
await SendToHub(wartEvent, [.. context.Filters]);

_eventQueue = context.HttpContext?.RequestServices.GetService<WartEventQueueService>();
_eventQueue?.Enqueue(new WartEventWithFilters(wartEvent, [.. context.Filters]));
}
}

base.OnActionExecuted(context);
}

/// <summary>
/// Sends the current event to the SignalR hub.
/// </summary>
/// <param name="wartEvent">The current WartEvent.</param>
/// <param name="filters">The list of filters applied to the request.</param>
private async Task SendToHub(WartEvent wartEvent, List<IFilterMetadata> filters)
{
try
{
if (filters.Any(f => f.GetType().Name == nameof(GroupWartAttribute)))
{
var wartGroup = filters.OfType<GroupWartAttribute>().FirstOrDefault();
var groups = wartGroup?.GroupNames;
if (groups != null)
{
foreach (var group in groups)
{
await _hubContext.Clients.Group(group).SendAsync("Send", wartEvent.ToString());
_logger?.LogInformation($"Group: {group}, WartEvent: {wartEvent}");
}
}
}
else
{
await _hubContext.Clients.All.SendAsync("Send", wartEvent.ToString());
_logger?.LogInformation("Event: {EventName}, Details: {EventDetails}", nameof(WartEvent), wartEvent.ToString());
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "Error sending WartEvent to clients");
}
}
}
}
35 changes: 35 additions & 0 deletions src/WART-Core/Entity/WartEventWithFilters.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// (c) 2024 Francesco Del Re <[email protected]>
// This code is licensed under MIT license (see LICENSE.txt for details)
using Microsoft.AspNetCore.Mvc.Filters;
using System.Collections.Generic;

namespace WART_Core.Entity
{
/// <summary>
/// Represents an event that contains additional filter metadata.
/// </summary>
public class WartEventWithFilters
{
/// <summary>
/// The main WartEvent object.
/// </summary>
public WartEvent WartEvent { get; set; }

/// <summary>
/// A list of filters applied to the event.
/// </summary>
public List<IFilterMetadata> Filters { get; set; }

/// <summary>
/// Initializes a new instance of the WartEventWithFilters class.
/// </summary>
/// <param name="wartEvent">The WartEvent to associate with the filters.</param>
/// <param name="filters">The list of filters applied to the event.</param>
public WartEventWithFilters(WartEvent wartEvent, List<IFilterMetadata> filters)
{
// Initialize the WartEvent and Filters properties
WartEvent = wartEvent;
Filters = filters;
}
}
}
5 changes: 5 additions & 0 deletions src/WART-Core/Hubs/WartHubBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,5 +122,10 @@ public static int GetConnectionsCount()
{
return _connections.Count;
}

/// <summary>
/// Returns a value indicating whether there are connected clients.
/// </summary>
public static bool HasConnectedClients => !_connections.IsEmpty;
}
}
8 changes: 8 additions & 0 deletions src/WART-Core/Middleware/WartServiceCollectionExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using System.Linq;
using WART_Core.Authentication.JWT;
using WART_Core.Enum;
using WART_Core.Hubs;
using WART_Core.Services;

namespace WART_Core.Middleware
{
Expand All @@ -32,6 +34,12 @@ public static IServiceCollection AddWartMiddleware(this IServiceCollection servi
// Add console logging.
services.AddLogging(configure => configure.AddConsole());

// Register WART event queue as a singleton service.
services.AddSingleton<WartEventQueueService>();

// Register the WART event worker as a hosted service.
services.AddHostedService<WartEventWorker<WartHub>>();

// Configure SignalR with custom options.
services.AddSignalR(options =>
{
Expand Down
43 changes: 43 additions & 0 deletions src/WART-Core/Services/WartEventQueueService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// (c) 2024 Francesco Del Re <[email protected]>
// This code is licensed under MIT license (see LICENSE.txt for details)
using System.Collections.Concurrent;
using WART_Core.Entity;

namespace WART_Core.Services
{
/// <summary>
/// A service that manages a concurrent queue for WartEvent objects with filters.
/// This class provides methods for enqueuing and dequeuing events.
/// </summary>
public class WartEventQueueService
{
// A thread-safe queue to hold WartEvent objects along with their associated filters.
private readonly ConcurrentQueue<WartEventWithFilters> _queue = new ConcurrentQueue<WartEventWithFilters>();

/// <summary>
/// Enqueues a WartEventWithFilters object to the queue.
/// </summary>
/// <param name="wartEventWithFilters">The WartEventWithFilters object to enqueue.</param>
public void Enqueue(WartEventWithFilters wartEventWithFilters)
{
// Adds the event with filters to the concurrent queue.
_queue.Enqueue(wartEventWithFilters);
}

/// <summary>
/// Attempts to dequeue a WartEventWithFilters object from the queue.
/// </summary>
/// <param name="wartEventWithFilters">The dequeued WartEventWithFilters object.</param>
/// <returns>True if an event was dequeued; otherwise, false.</returns>
public bool TryDequeue(out WartEventWithFilters wartEventWithFilters)
{
// Attempts to remove and return the event with filters from the queue.
return _queue.TryDequeue(out wartEventWithFilters);
}

/// <summary>
/// Gets the current count of events in the queue.
/// </summary>
public int Count => _queue.Count;
}
}
Loading

0 comments on commit 5f476ae

Please sign in to comment.