diff --git a/src/WART-Core/Controllers/WartBaseController.cs b/src/WART-Core/Controllers/WartBaseController.cs new file mode 100644 index 0000000..c45b999 --- /dev/null +++ b/src/WART-Core/Controllers/WartBaseController.cs @@ -0,0 +1,96 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using Microsoft.AspNetCore.Mvc.Filters; +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; + +namespace WART_Core.Controllers +{ + public abstract class WartBaseController : Controller where THub : Hub + { + private readonly ILogger _logger; + private readonly IHubContext _hubContext; + private const string RouteDataKey = "REQUEST"; + + protected WartBaseController(IHubContext hubContext, ILogger logger) + { + _hubContext = hubContext; + _logger = logger; + } + + /// + /// Adds the request objects to RouteData. + /// + /// The action executing context. + public override void OnActionExecuting(ActionExecutingContext context) + { + context?.RouteData.Values.Add(RouteDataKey, context.ActionArguments); + base.OnActionExecuting(context); + } + + /// + /// Processes the executed action and sends the event to the SignalR hub if applicable. + /// + /// The action executed context. + public override async void OnActionExecuted(ActionExecutedContext context) + { + if (context?.Result is ObjectResult objectResult) + { + var exclusion = context.Filters.Any(f => f.GetType().Name == nameof(ExcludeWartAttribute)); + if (!exclusion && context.RouteData.Values.TryGetValue(RouteDataKey, out var request)) + { + var httpMethod = context.HttpContext?.Request.Method; + var httpPath = context.HttpContext?.Request.Path; + var remoteAddress = context.HttpContext?.Connection.RemoteIpAddress?.ToString(); + var response = objectResult.Value; + + var wartEvent = new WartEvent(request, response, httpMethod, httpPath, remoteAddress); + await SendToHub(wartEvent, [.. context.Filters]); + } + } + + base.OnActionExecuted(context); + } + + /// + /// Sends the current event to the SignalR hub. + /// + /// The current WartEvent. + /// The list of filters applied to the request. + private async Task SendToHub(WartEvent wartEvent, List filters) + { + try + { + if (filters.Any(f => f.GetType().Name == nameof(GroupWartAttribute))) + { + var wartGroup = filters.OfType().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"); + } + } + } +} diff --git a/src/WART-Core/Controllers/WartController.cs b/src/WART-Core/Controllers/WartController.cs index 3b5a660..d052e4f 100755 --- a/src/WART-Core/Controllers/WartController.cs +++ b/src/WART-Core/Controllers/WartController.cs @@ -1,15 +1,7 @@ // (c) 2019 Francesco Del Re // This code is licensed under MIT license (see LICENSE.txt for details) -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; 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 WART_Core.Hubs; namespace WART_Core.Controllers @@ -17,105 +9,11 @@ namespace WART_Core.Controllers /// /// The WART Controller /// - public class WartController : Controller + public class WartController : WartBaseController { - private readonly ILogger _logger; - private readonly IHubContext _hubContext; - private const string RouteDataKey = "REQUEST"; - public WartController(IHubContext hubContext, ILogger logger) + : base(hubContext, logger) { - _hubContext = hubContext; - _logger = logger; - } - - public WartController(IHubContext hubContext) - { - _hubContext = hubContext; - } - - /// - /// WART OnActionExecuting override - /// - /// ActionExecutedContext context - public override void OnActionExecuting(ActionExecutingContext context) - { - // add the request objects to RouteData - context?.RouteData.Values.Add(RouteDataKey, context.ActionArguments); - - base.OnActionExecuting(context); - } - - /// - /// WART OnActionExecuted override - /// - /// ActionExecutedContext context - public override async void OnActionExecuted(ActionExecutedContext context) - { - if (context?.Result is ObjectResult objectResult) - { - // check for wart exclusion - var exclusion = context.Filters.Any(f => f.GetType().Name == nameof(ExcludeWartAttribute)); - if (!exclusion) - { - // check for RouteData key existence - if (context.RouteData.Values.TryGetValue(RouteDataKey, out var request)) - { - // get the request objects from RouteData - var httpMethod = context.HttpContext?.Request.Method; - var httpPath = context.HttpContext?.Request.Path; - var remoteAddress = context.HttpContext?.Connection.RemoteIpAddress?.ToString(); - // get the object response - var response = objectResult.Value; - // create the new WartEvent and broadcast to all clients - var wartEvent = new WartEvent(request, response, httpMethod, httpPath, remoteAddress); - await SendToHub(wartEvent, [.. context.Filters]); - } - } - } - - base.OnActionExecuted(context); - } - - /// - /// Send the current event to the SignalR hub. - /// - /// The current WartEvent - /// The current Filters - /// - private async Task SendToHub(WartEvent wartEvent, List filters) - { - try - { - // check for specific groups - if (filters.Any(f => f.GetType().Name == nameof(GroupWartAttribute))) - { - // get the list of filters of type WartGroupAttribute - var wartGroup = filters.FirstOrDefault(f => f.GetType() == typeof(GroupWartAttribute)) as GroupWartAttribute; - var groups = wartGroup?.GroupNames; - foreach (var group in groups) - { - // send to the specific group - await _hubContext?.Clients - .Group(group) - .SendAsync("Send", wartEvent.ToString()); - - _logger?.LogInformation($"Group: {group}, WartEvent: {wartEvent}"); - } - } - else - { - // send to all clients - 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"); - } } } } diff --git a/src/WART-Core/Controllers/WartControllerJwt.cs b/src/WART-Core/Controllers/WartControllerJwt.cs index d416d3b..b8a085c 100755 --- a/src/WART-Core/Controllers/WartControllerJwt.cs +++ b/src/WART-Core/Controllers/WartControllerJwt.cs @@ -1,120 +1,19 @@ // (c) 2021 Francesco Del Re // This code is licensed under MIT license (see LICENSE.txt for details) -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; -using System.Threading.Tasks; -using System; -using WART_Core.Entity; using WART_Core.Hubs; -using System.Linq; -using WART_Core.Filters; -using System.Collections.Generic; namespace WART_Core.Controllers { /// - /// The WART Controller + /// The WART Controller with JWT authentication /// - public class WartControllerJwt : Controller + public class WartControllerJwt : WartBaseController { - private readonly ILogger _logger; - private readonly IHubContext _hubContext; - private const string RouteDataKey = "REQUEST"; - public WartControllerJwt(IHubContext hubContext, ILogger logger) + : base(hubContext, logger) { - _hubContext = hubContext; - _logger = logger; - } - - public WartControllerJwt(IHubContext hubContext) - { - _hubContext = hubContext; - } - - /// - /// WART OnActionExecuting override - /// - /// ActionExecutedContext context - public override void OnActionExecuting(ActionExecutingContext context) - { - // add the request objects to RouteData - context?.RouteData.Values.Add(RouteDataKey, context.ActionArguments); - - base.OnActionExecuting(context); - } - - /// - /// WART OnActionExecuted override - /// - /// ActionExecutedContext context - public override async void OnActionExecuted(ActionExecutedContext context) - { - if (context?.Result is ObjectResult objectResult) - { - // check for wart exclusion - var exclusion = context.Filters.Any(f => f.GetType().Name == nameof(ExcludeWartAttribute)); - if (!exclusion) - { - // check for RouteData key existence - if (context.RouteData.Values.TryGetValue(RouteDataKey, out var request)) - { - // get the request objects from RouteData - var httpMethod = context.HttpContext?.Request.Method; - var httpPath = context.HttpContext?.Request.Path; - var remoteAddress = context.HttpContext?.Connection.RemoteIpAddress?.ToString(); - // get the object response - var response = objectResult.Value; - // create the new WartEvent and broadcast to all clients - var wartEvent = new WartEvent(request, response, httpMethod, httpPath, remoteAddress); - await SendToHub(wartEvent, [.. context.Filters]); - } - } - } - - base.OnActionExecuted(context); - } - - /// - /// Send the current event to the SignalR hub. - /// - /// The current WartEvent - /// The current Filters - /// - private async Task SendToHub(WartEvent wartEvent, List filters) - { - try - { - // check for specific groups - if (filters.Any(f => f.GetType().Name == nameof(GroupWartAttribute))) - { - // get the list of filters of type WartGroupAttribute - var wartGroup = filters.FirstOrDefault(f => f.GetType() == typeof(GroupWartAttribute)) as GroupWartAttribute; - var groups = wartGroup?.GroupNames; - foreach (var group in groups) - { - await _hubContext?.Clients - .Group(group) - .SendAsync("Send", wartEvent.ToString()); - - _logger?.LogInformation($"Group: {group}, WartEvent: {wartEvent}"); - } - } - else - { - // send to all clients - 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"); - } } } } diff --git a/src/WART-Core/Entity/WartEvent.cs b/src/WART-Core/Entity/WartEvent.cs index ab5ebd0..5f83199 100755 --- a/src/WART-Core/Entity/WartEvent.cs +++ b/src/WART-Core/Entity/WartEvent.cs @@ -1,6 +1,7 @@ // (c) 2019 Francesco Del Re // This code is licensed under MIT license (see LICENSE.txt for details) using System; +using System.Collections.Generic; using System.Text.Json.Serialization; using WART_Core.Helpers; using WART_Core.Serialization; @@ -101,5 +102,25 @@ public T GetResponseObject() where T : class { return SerializationHelper.Deserialize(JsonResponsePayload); } - } + + /// + /// Converts the WartEvent instance into a dictionary for flexible logging or data analysis. + /// + /// A dictionary representation of the event. + public Dictionary ToDictionary() + { + return new Dictionary + { + { "EventId", EventId }, + { "TimeStamp", TimeStamp }, + { "UtcTimeStamp", UtcTimeStamp }, + { "HttpMethod", HttpMethod }, + { "HttpPath", HttpPath }, + { "RemoteAddress", RemoteAddress }, + { "JsonRequestPayload", JsonRequestPayload }, + { "JsonResponsePayload", JsonResponsePayload }, + { "ExtraInfo", ExtraInfo } + }; + } + } } diff --git a/src/WART-Core/Hubs/WartHub.cs b/src/WART-Core/Hubs/WartHub.cs index d8bcacd..4e785ae 100755 --- a/src/WART-Core/Hubs/WartHub.cs +++ b/src/WART-Core/Hubs/WartHub.cs @@ -1,9 +1,5 @@ // (c) 2019 Francesco Del Re // This code is licensed under MIT license (see LICENSE.txt for details) -using System; -using System.Collections.Concurrent; -using System.Threading.Tasks; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; namespace WART_Core.Hubs @@ -11,94 +7,8 @@ namespace WART_Core.Hubs /// /// The WART SignalR hub. /// - public class WartHub : Hub + public class WartHub : WartHubBase { - private static readonly ConcurrentDictionary _connections = new ConcurrentDictionary(); - - private readonly ILogger _logger; - - public WartHub(ILogger logger) - { - _logger = logger; - } - - public override async Task OnConnectedAsync() - { - var httpContext = Context.GetHttpContext(); - var wartGroup = httpContext.Request.Query["WartGroup"].ToString(); - var userName = Context.User?.Identity?.Name ?? "Anonymous"; - - _connections.TryAdd(Context.ConnectionId, Context.User.Identity.Name); - - if (!string.IsNullOrEmpty(wartGroup)) - { - await AddToGroup(wartGroup); - } - - _logger?.LogInformation($"OnConnect: ConnectionId={Context.ConnectionId}, User={userName}"); - - await base.OnConnectedAsync(); - } - - public override Task OnDisconnectedAsync(Exception exception) - { - _connections.TryRemove(Context.ConnectionId, out _); - - if (exception != null) - { - _logger?.LogWarning(exception, $"OnDisconnect with error: ConnectionId={Context.ConnectionId}"); - } - else - { - _logger?.LogInformation($"OnDisconnect: ConnectionId={Context.ConnectionId}"); - } - - return base.OnDisconnectedAsync(exception); - } - - /// - /// Adds a connection to a group. - /// - /// The group name to add the connection to. - /// - public async Task AddToGroup(string groupName) - { - if (string.IsNullOrWhiteSpace(groupName)) - { - _logger?.LogWarning("Attempted to add to a null or empty group."); - return; - } - - await Groups.AddToGroupAsync(Context.ConnectionId, groupName); - - _logger?.LogInformation($"Connection {Context.ConnectionId} added to group {groupName}"); - } - - /// - /// Removes a connection from a group. - /// - /// The group name to remove the connection from. - /// - public async Task RemoveFromGroup(string groupName) - { - if (string.IsNullOrWhiteSpace(groupName)) - { - _logger?.LogWarning("Attempted to remove from a null or empty group."); - return; - } - - await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName); - - _logger?.LogInformation($"Connection {Context.ConnectionId} removed from group {groupName}"); - } - - /// - /// Get the current number of active connection - /// - /// - public static int GetConnectionsCount() - { - return _connections.Count; - } + public WartHub(ILogger logger) : base(logger) { } } } diff --git a/src/WART-Core/Hubs/WartHubBase.cs b/src/WART-Core/Hubs/WartHubBase.cs new file mode 100644 index 0000000..0a55722 --- /dev/null +++ b/src/WART-Core/Hubs/WartHubBase.cs @@ -0,0 +1,126 @@ +// (c) 2024 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging; + +namespace WART_Core.Hubs +{ + /// + /// Base class for WART SignalR hubs. + /// Provides shared logic for managing connections, groups, and logging. + /// + public abstract class WartHubBase : Hub + { + /// + /// Stores active connections with their respective identifiers. + /// + private static readonly ConcurrentDictionary _connections = new ConcurrentDictionary(); + + /// + /// Logger instance for logging hub activities. + /// + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// Logger instance for the hub. + protected WartHubBase(ILogger logger) + { + _logger = logger; + } + + /// + /// Called when a new connection is established. + /// Adds the connection to the dictionary and optionally assigns it to a group. + /// + /// A task that represents the asynchronous operation. + public override async Task OnConnectedAsync() + { + var httpContext = Context.GetHttpContext(); + var wartGroup = httpContext.Request.Query["WartGroup"].ToString(); + var userName = Context.User?.Identity?.Name ?? "Anonymous"; + + _connections.TryAdd(Context.ConnectionId, userName); + + if (!string.IsNullOrEmpty(wartGroup)) + { + await AddToGroup(wartGroup); + } + + _logger?.LogInformation($"OnConnect: ConnectionId={Context.ConnectionId}, User={userName}"); + + await base.OnConnectedAsync(); + } + + /// + /// Called when a connection is disconnected. + /// Removes the connection from the dictionary and logs the event. + /// + /// The exception that occurred, if any. + /// A task that represents the asynchronous operation. + public override Task OnDisconnectedAsync(Exception exception) + { + _connections.TryRemove(Context.ConnectionId, out _); + + if (exception != null) + { + _logger?.LogWarning(exception, $"OnDisconnect with error: ConnectionId={Context.ConnectionId}"); + } + else + { + _logger?.LogInformation($"OnDisconnect: ConnectionId={Context.ConnectionId}"); + } + + return base.OnDisconnectedAsync(exception); + } + + /// + /// Adds the current connection to a specified SignalR group. + /// + /// The name of the SignalR group to add the connection to. + /// A task that represents the asynchronous operation. + public async Task AddToGroup(string groupName) + { + if (string.IsNullOrWhiteSpace(groupName)) + { + _logger?.LogWarning("Attempted to add to a null or empty group."); + return; + } + + await Groups.AddToGroupAsync(Context.ConnectionId, groupName); + + _logger?.LogInformation($"Connection {Context.ConnectionId} added to group {groupName}"); + } + + /// + /// Removes the current connection from a specified SignalR group. + /// + /// The name of the SignalR group to remove the connection from. + /// A task that represents the asynchronous operation. + public async Task RemoveFromGroup(string groupName) + { + if (string.IsNullOrWhiteSpace(groupName)) + { + _logger?.LogWarning("Attempted to remove from a null or empty group."); + return; + } + + await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName); + + _logger?.LogInformation($"Connection {Context.ConnectionId} removed from group {groupName}"); + } + + /// + /// Gets the current number of active connections. + /// + /// The count of active connections. + public static int GetConnectionsCount() + { + return _connections.Count; + } + } +} \ No newline at end of file diff --git a/src/WART-Core/Hubs/WartHubJwt.cs b/src/WART-Core/Hubs/WartHubJwt.cs index 89b8650..d118ee5 100755 --- a/src/WART-Core/Hubs/WartHubJwt.cs +++ b/src/WART-Core/Hubs/WartHubJwt.cs @@ -2,11 +2,7 @@ // This code is licensed under MIT license (see LICENSE.txt for details) using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; -using System; -using System.Collections.Concurrent; -using System.Threading.Tasks; namespace WART_Core.Hubs { @@ -14,94 +10,8 @@ namespace WART_Core.Hubs /// The WART SignalR hub with JWT authentication. /// [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] - public class WartHubJwt : Hub + public class WartHubJwt : WartHubBase { - private static readonly ConcurrentDictionary _connections = new ConcurrentDictionary(); - - private readonly ILogger _logger; - - public WartHubJwt(ILogger logger) - { - _logger = logger; - } - - public override async Task OnConnectedAsync() - { - var httpContext = Context.GetHttpContext(); - var wartGroup = httpContext.Request.Query["WartGroup"].ToString(); - var userName = Context.User?.Identity?.Name ?? "Anonymous"; - - _connections.TryAdd(Context.ConnectionId, httpContext.User.Identity.Name); - - if(!string.IsNullOrEmpty(wartGroup)) - { - await AddToGroup(wartGroup); - } - - _logger?.LogInformation($"OnConnect: ConnectionId={Context.ConnectionId}, User={userName}"); - - await base.OnConnectedAsync(); - } - - public override Task OnDisconnectedAsync(Exception exception) - { - _connections.TryRemove(Context.ConnectionId, out _); - - if (exception != null) - { - _logger?.LogWarning(exception, $"OnDisconnect with error: ConnectionId={Context.ConnectionId}"); - } - else - { - _logger?.LogInformation($"OnDisconnect: ConnectionId={Context.ConnectionId}"); - } - - return base.OnDisconnectedAsync(exception); - } - - /// - /// Adds a connection to a group. - /// - /// The group name to add the connection to. - /// - public async Task AddToGroup(string groupName) - { - if (string.IsNullOrWhiteSpace(groupName)) - { - _logger?.LogWarning("Attempted to add to a null or empty group."); - return; - } - - await Groups.AddToGroupAsync(Context.ConnectionId, groupName); - - _logger?.LogInformation($"Connection {Context.ConnectionId} added to group {groupName}"); - } - - /// - /// Removes a connection from a group. - /// - /// The group name to remove the connection from. - /// - public async Task RemoveFromGroup(string groupName) - { - if (string.IsNullOrWhiteSpace(groupName)) - { - _logger?.LogWarning("Attempted to remove from a null or empty group."); - return; - } - - await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName); - - _logger?.LogInformation($"Connection {Context.ConnectionId} removed from group {groupName}"); - } - - /// - /// Get the current number of active connection - /// - /// - public static int GetConnectionsCount() - { - return _connections.Count; - } + public WartHubJwt(ILogger logger) : base(logger) { } } } diff --git a/src/WART-Core/WART-Core.csproj b/src/WART-Core/WART-Core.csproj index e713e19..ce614c7 100644 --- a/src/WART-Core/WART-Core.csproj +++ b/src/WART-Core/WART-Core.csproj @@ -6,7 +6,7 @@ true Francesco Del Re Francesco Del Re - WART is a C# .NET Core library that allows extending any WebApi controller and forwarding any calls received by the controller to a SignalR hub + Transforms REST APIs into SignalR events, bringing real-time interaction to your applications MIT https://github.com/engineering87/WART https://github.com/engineering87/WART @@ -14,7 +14,7 @@ 4.0.0.0 4.0.0.0 - 5.3.3 + 5.3.4 icon.png README.md