Add Canonical JSON serialization library with tests and documentation

- Implemented CanonJson class for deterministic JSON serialization and hashing.
- Added unit tests for CanonJson functionality, covering various scenarios including key sorting, handling of nested objects, arrays, and special characters.
- Created project files for the Canonical JSON library and its tests, including necessary package references.
- Added README.md for library usage and API reference.
- Introduced RabbitMqIntegrationFactAttribute for conditional RabbitMQ integration tests.
This commit is contained in:
master
2025-12-19 15:35:00 +02:00
parent 43882078a4
commit 951a38d561
192 changed files with 27550 additions and 2611 deletions

View File

@@ -1,8 +1,10 @@
using System.Collections.Concurrent;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Router.Common.Abstractions;
using StellaOps.Router.Common.Enums;
using StellaOps.Router.Common.Frames;
using StellaOps.Router.Common.Models;
namespace StellaOps.Microservice;
@@ -14,6 +16,7 @@ public sealed class RouterConnectionManager : IRouterConnectionManager, IDisposa
{
private readonly StellaMicroserviceOptions _options;
private readonly IEndpointDiscoveryProvider _endpointDiscovery;
private readonly RequestDispatcher _requestDispatcher;
private readonly IMicroserviceTransport? _microserviceTransport;
private readonly IGeneratedEndpointProvider? _generatedProvider;
private readonly ILogger<RouterConnectionManager> _logger;
@@ -37,12 +40,14 @@ public sealed class RouterConnectionManager : IRouterConnectionManager, IDisposa
public RouterConnectionManager(
IOptions<StellaMicroserviceOptions> options,
IEndpointDiscoveryProvider endpointDiscovery,
RequestDispatcher requestDispatcher,
IMicroserviceTransport? microserviceTransport,
IGeneratedEndpointProvider? generatedProvider,
ILogger<RouterConnectionManager> logger)
ILogger<RouterConnectionManager> logger,
IGeneratedEndpointProvider? generatedProvider = null)
{
_options = options.Value;
_endpointDiscovery = endpointDiscovery;
_requestDispatcher = requestDispatcher;
_microserviceTransport = microserviceTransport;
_generatedProvider = generatedProvider;
_logger = logger;
@@ -91,6 +96,12 @@ public sealed class RouterConnectionManager : IRouterConnectionManager, IDisposa
_endpoints = _endpointDiscovery.DiscoverEndpoints();
_logger.LogInformation("Discovered {EndpointCount} endpoints", _endpoints.Count);
// Wire request handling before transport connect to avoid a race after HELLO.
if (_microserviceTransport is not null)
{
_microserviceTransport.OnRequestReceived += HandleRequestReceivedAsync;
}
// Get schema definitions from generated provider
_schemas = _generatedProvider?.GetSchemaDefinitions()
?? new Dictionary<string, SchemaDefinition>();
@@ -110,6 +121,24 @@ public sealed class RouterConnectionManager : IRouterConnectionManager, IDisposa
await ConnectToRouterAsync(router, cancellationToken);
}
// Establish transport connection to the gateway (InMemory/TCP/RabbitMQ/etc).
if (_microserviceTransport is not null)
{
var instance = new InstanceDescriptor
{
InstanceId = _options.InstanceId,
ServiceName = _options.ServiceName,
Version = _options.Version,
Region = _options.Region
};
await _microserviceTransport.ConnectAsync(instance, _endpoints, cancellationToken);
}
else
{
_logger.LogWarning("No microservice transport configured; skipping transport connection.");
}
// Start heartbeat task
_heartbeatTask = Task.Run(() => HeartbeatLoopAsync(_cts.Token), CancellationToken.None);
}
@@ -121,6 +150,22 @@ public sealed class RouterConnectionManager : IRouterConnectionManager, IDisposa
await _cts.CancelAsync();
if (_microserviceTransport is not null)
{
try
{
await _microserviceTransport.DisconnectAsync();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to disconnect transport");
}
finally
{
_microserviceTransport.OnRequestReceived -= HandleRequestReceivedAsync;
}
}
if (_heartbeatTask is not null)
{
try
@@ -136,6 +181,42 @@ public sealed class RouterConnectionManager : IRouterConnectionManager, IDisposa
_connections.Clear();
}
private async Task<Frame> HandleRequestReceivedAsync(Frame frame, CancellationToken cancellationToken)
{
var request = FrameConverter.ToRequestFrame(frame);
if (request is null)
{
_logger.LogWarning(
"Received invalid request frame: type={FrameType}, correlationId={CorrelationId}",
frame.Type,
frame.CorrelationId ?? "(null)");
var error = new ResponseFrame
{
RequestId = frame.CorrelationId ?? Guid.NewGuid().ToString("N"),
StatusCode = 400,
Headers = new Dictionary<string, string>
{
["Content-Type"] = "text/plain; charset=utf-8"
},
Payload = Encoding.UTF8.GetBytes("Invalid request frame")
};
var errorFrame = FrameConverter.ToFrame(error);
return frame.CorrelationId is null
? errorFrame
: errorFrame with { CorrelationId = frame.CorrelationId };
}
var response = await _requestDispatcher.DispatchAsync(request, cancellationToken);
var responseFrame = FrameConverter.ToFrame(response);
// Ensure correlation ID matches the incoming request for transport-level matching.
return frame.CorrelationId is null
? responseFrame
: responseFrame with { CorrelationId = frame.CorrelationId };
}
private async Task ConnectToRouterAsync(RouterEndpointConfig router, CancellationToken cancellationToken)
{
var connectionId = $"{router.Host}:{router.Port}";