From df9332123f38a779fd115269555ae05f1ee03ee3 Mon Sep 17 00:00:00 2001 From: Wes Shaddix Date: Tue, 19 Nov 2024 15:29:00 -0500 Subject: [PATCH 1/4] updated to .net 9 and updated NuGet packages --- CHANGELOG.md | 12 ++- src/Directory.Packages.props | 20 +++++ src/Dockerfile | 4 +- src/ExampleHandlers/ExampleHandlers.csproj | 14 ++-- src/ExampleHandlers/GetCustomer.cs | 8 +- src/ExampleHandlers/Healthcheck.cs | 8 +- src/ExampleHandlers/Logging.cs | 3 +- src/ExampleHandlers/Metrics.cs | 8 +- src/ExampleHandlers/Program.cs | 16 ++-- src/ExampleHandlers/Tracing.cs | 9 ++- src/HttpNatsProxy.sln | 1 + src/Proxy.Shared/CallTiming.cs | 12 +-- src/Proxy.Shared/Guard.cs | 16 ++++ src/Proxy.Shared/IMessageHandler.cs | 6 +- src/Proxy.Shared/IMessageObserver.cs | 6 +- src/Proxy.Shared/MicroserviceMessage.cs | 61 ++++++--------- .../MicroserviceMessageExtensions.cs | 2 +- src/Proxy.Shared/NatsConfiguration.cs | 22 ++---- src/Proxy.Shared/NatsHelper.cs | 70 ++++++++++-------- src/Proxy.Shared/NatsSubscription.cs | 1 - src/Proxy.Shared/NatsSubscriptionHandler.cs | 55 ++++++++------ src/Proxy.Shared/Proxy.Shared.csproj | 17 +++-- src/Proxy/HttpRequestParser.cs | 17 ++--- src/Proxy/HttpResponseFactory.cs | 3 +- src/Proxy/NatsSubjectParser.cs | 2 +- src/Proxy/Observer.cs | 2 +- src/Proxy/Pipeline.cs | 4 +- src/Proxy/PipelineExecutor.cs | 13 ++-- src/Proxy/Program.cs | 38 +++++----- src/Proxy/Proxy.csproj | 26 ++++--- src/Proxy/ProxyConfiguration.cs | 74 ++++++++----------- src/Proxy/RequestHandler.cs | 31 +++++--- src/Proxy/Step.cs | 6 +- src/Proxy/StepException.cs | 12 ++- src/docker-compose.yml | 2 +- src/global.json | 12 +-- 36 files changed, 330 insertions(+), 283 deletions(-) create mode 100644 src/Directory.Packages.props create mode 100644 src/Proxy.Shared/Guard.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a5831b..2ed0e98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,4 +65,14 @@ * updated NATS server version to 1.4.1 * updated projects target framework to .Net Core 2.2 * merged in PR to fix parsing of extended properties during pipeline execution (thanks to https://github.com/timsmid) -* updated all nuget dependencies \ No newline at end of file +* updated all nuget dependencies + +## 1.2.0 + +* Updated to .Net 9 +* Migrated to centralized package management +* Updated log messages to follow best practices (no trailing period) +* Fixed typo in code comment +* Added additional error handling and null checks +* Updated any outdated or vulnerable NuGet packages +* Updated Dockerfile to latest versions and addressed scout vulnerabilities \ No newline at end of file diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props new file mode 100644 index 0000000..8b35270 --- /dev/null +++ b/src/Directory.Packages.props @@ -0,0 +1,20 @@ + + + true + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Dockerfile b/src/Dockerfile index 11bcf4d..8276b3d 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -1,7 +1,7 @@ # STAGE 1 - COMPILE AND PUBLISH # use the image that has the build tools for the "build stage" -FROM mcr.microsoft.com/dotnet/core/sdk:2.2.301-alpine3.9 AS build-env +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build-env # set the working directory in the image as "app" WORKDIR /app @@ -22,7 +22,7 @@ RUN dotnet publish ./Proxy/Proxy.csproj -c Release -o /publish # STAGE 2 - BUILD RUNTIME OPTIMIZED IMAGE # use the runtime optimized image that does not have any build tools, only the .net core runtime -FROM mcr.microsoft.com/dotnet/core/runtime:2.2.6-alpine3.9 +FROM mcr.microsoft.com/dotnet/runtime:9.0-alpine # set the working directory in the image as "app" WORKDIR /app diff --git a/src/ExampleHandlers/ExampleHandlers.csproj b/src/ExampleHandlers/ExampleHandlers.csproj index 46ce32f..931d98b 100644 --- a/src/ExampleHandlers/ExampleHandlers.csproj +++ b/src/ExampleHandlers/ExampleHandlers.csproj @@ -2,15 +2,17 @@ Exe - netcoreapp2.2 + net9.0 + enable + enable - - - - - + + + + + diff --git a/src/ExampleHandlers/GetCustomer.cs b/src/ExampleHandlers/GetCustomer.cs index b0d85fb..8efef80 100644 --- a/src/ExampleHandlers/GetCustomer.cs +++ b/src/ExampleHandlers/GetCustomer.cs @@ -1,12 +1,16 @@ using Proxy.Shared; -using System.Threading.Tasks; namespace ExampleHandlers { public class GetCustomer : IMessageHandler { - public async Task HandleAsync(MicroserviceMessage msg) + public async Task HandleAsync(MicroserviceMessage? msg) { + if (msg is null) + { + throw new Exception("msg is null"); + } + // grab the customer id that is being fetched if (msg.TryGetParam("id", out var customerId)) { diff --git a/src/ExampleHandlers/Healthcheck.cs b/src/ExampleHandlers/Healthcheck.cs index acc69e2..3504aa2 100644 --- a/src/ExampleHandlers/Healthcheck.cs +++ b/src/ExampleHandlers/Healthcheck.cs @@ -1,12 +1,16 @@ using Proxy.Shared; -using System.Threading.Tasks; namespace ExampleHandlers { public class Healthcheck : IMessageHandler { - async Task IMessageHandler.HandleAsync(MicroserviceMessage msg) + async Task IMessageHandler.HandleAsync(MicroserviceMessage? msg) { + if (msg is null) + { + throw new Exception("msg is null"); + } + // simulate code to go check connections to infrastructure dependencies like a database, redis cache, 3rd party api, etc await Task.Delay(1000); diff --git a/src/ExampleHandlers/Logging.cs b/src/ExampleHandlers/Logging.cs index 7bb8606..a0fd35e 100644 --- a/src/ExampleHandlers/Logging.cs +++ b/src/ExampleHandlers/Logging.cs @@ -1,13 +1,12 @@ using Newtonsoft.Json; using Proxy.Shared; using Serilog; -using System.Threading.Tasks; namespace ExampleHandlers { public class Logging : IMessageObserver { - public Task ObserveAsync(MicroserviceMessage msg) + public Task ObserveAsync(MicroserviceMessage? msg) { Log.Information(JsonConvert.SerializeObject(msg)); diff --git a/src/ExampleHandlers/Metrics.cs b/src/ExampleHandlers/Metrics.cs index e8034f4..3e3ddab 100644 --- a/src/ExampleHandlers/Metrics.cs +++ b/src/ExampleHandlers/Metrics.cs @@ -1,13 +1,17 @@ using Proxy.Shared; using Serilog; -using System.Threading.Tasks; namespace ExampleHandlers { public class Metrics : IMessageObserver { - public Task ObserveAsync(MicroserviceMessage msg) + public Task ObserveAsync(MicroserviceMessage? msg) { + if (msg is null) + { + throw new Exception("msg is null"); + } + // grab all the metrics from the message and "record" them foreach (var callTiming in msg.CallTimings) { diff --git a/src/ExampleHandlers/Program.cs b/src/ExampleHandlers/Program.cs index d9b4c07..c94bc49 100644 --- a/src/ExampleHandlers/Program.cs +++ b/src/ExampleHandlers/Program.cs @@ -1,28 +1,30 @@ using Microsoft.Extensions.DependencyInjection; using Proxy.Shared; using Serilog; -using System; -using System.Threading; namespace ExampleHandlers { internal class Program { private static readonly ManualResetEvent _mre = new ManualResetEvent(false); - private static IServiceProvider _container; + private static IServiceProvider? _container; private static void ConfigureNatsSubscriptions() { Log.Information("Configuring NATS Subscriptions"); // create the nats subscription handler + if (_container is null) + { + throw new Exception("container is null"); + } var subscriptionHandler = new NatsSubscriptionHandler(_container); const string queueGroup = "Example.Queue.Group"; NatsHelper.Configure(cfg => { cfg.ClientName = "Example Message Handlers"; - cfg.NatsServerUrls = new[] { "nats://localhost:4222" }; + cfg.NatsServerUrls = ["nats://localhost:4222"]; cfg.PingInterval = 2000; cfg.MaxPingsOut = 2; @@ -53,7 +55,7 @@ private static void Main(string[] args) // configure our ioc container SetupDependencies(); - // setup an event handler that will run when our application process shuts down + // set up an event handler that will run when our application process shuts down SetupShutdownHandler(); // configure our NATS subscriptions that we are going to listen to @@ -90,10 +92,10 @@ private static void SetupLogger() private static void SetupShutdownHandler() { - Console.CancelKeyPress += (s, e) => + Console.CancelKeyPress += (_, _) => { // log that we are shutting down - Log.Information("Example Handlers are shutting down."); + Log.Information("Example Handlers are shutting down"); // shutdown the logger Log.CloseAndFlush(); diff --git a/src/ExampleHandlers/Tracing.cs b/src/ExampleHandlers/Tracing.cs index 665bed7..3f6c395 100644 --- a/src/ExampleHandlers/Tracing.cs +++ b/src/ExampleHandlers/Tracing.cs @@ -1,14 +1,17 @@ using Proxy.Shared; using Serilog; -using System; -using System.Threading.Tasks; namespace ExampleHandlers { public class Tracing : IMessageHandler { - public Task HandleAsync(MicroserviceMessage msg) + public Task HandleAsync(MicroserviceMessage? msg) { + if (msg is null) + { + throw new Exception("msg is null"); + } + const string headerName = "x-trace-id"; // see if a trace header is already on the msg diff --git a/src/HttpNatsProxy.sln b/src/HttpNatsProxy.sln index eed361c..ef1de4e 100644 --- a/src/HttpNatsProxy.sln +++ b/src/HttpNatsProxy.sln @@ -13,6 +13,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Dockerfile = Dockerfile global.json = global.json ..\README.md = ..\README.md + Directory.Packages.props = Directory.Packages.props EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Proxy.Shared", "Proxy.Shared\Proxy.Shared.csproj", "{16ADB926-350D-4A8A-AFED-C51E2321FFA0}" diff --git a/src/Proxy.Shared/CallTiming.cs b/src/Proxy.Shared/CallTiming.cs index 0d4d23a..f559d77 100644 --- a/src/Proxy.Shared/CallTiming.cs +++ b/src/Proxy.Shared/CallTiming.cs @@ -1,15 +1,9 @@ namespace Proxy.Shared { - public class CallTiming + public class CallTiming(string? subject, long ellapsedMs) { - public long EllapsedMs { get; set; } + public long EllapsedMs { get; set; } = ellapsedMs; - public string Subject { get; set; } - - public CallTiming(string subject, long ellapsedMs) - { - Subject = subject; - EllapsedMs = ellapsedMs; - } + public string? Subject { get; set; } = subject; } } \ No newline at end of file diff --git a/src/Proxy.Shared/Guard.cs b/src/Proxy.Shared/Guard.cs new file mode 100644 index 0000000..732a929 --- /dev/null +++ b/src/Proxy.Shared/Guard.cs @@ -0,0 +1,16 @@ +using System.Runtime.CompilerServices; + +namespace Proxy.Shared; + +public static class Guard +{ + public static T AgainstNull(T value, [CallerArgumentExpression("value")] string? name = null) where T : class? + { + if (null == value) + { + throw new ArgumentException($"{name} cannot be null"); + } + + return value; + } +} \ No newline at end of file diff --git a/src/Proxy.Shared/IMessageHandler.cs b/src/Proxy.Shared/IMessageHandler.cs index 07e87b9..0c54d18 100644 --- a/src/Proxy.Shared/IMessageHandler.cs +++ b/src/Proxy.Shared/IMessageHandler.cs @@ -1,9 +1,7 @@ -using System.Threading.Tasks; - -namespace Proxy.Shared +namespace Proxy.Shared { public interface IMessageHandler { - Task HandleAsync(MicroserviceMessage msg); + Task HandleAsync(MicroserviceMessage? msg); } } \ No newline at end of file diff --git a/src/Proxy.Shared/IMessageObserver.cs b/src/Proxy.Shared/IMessageObserver.cs index 276f001..2d92195 100644 --- a/src/Proxy.Shared/IMessageObserver.cs +++ b/src/Proxy.Shared/IMessageObserver.cs @@ -1,9 +1,7 @@ -using System.Threading.Tasks; - -namespace Proxy.Shared +namespace Proxy.Shared { public interface IMessageObserver { - Task ObserveAsync(MicroserviceMessage msg); + Task ObserveAsync(MicroserviceMessage? msg); } } \ No newline at end of file diff --git a/src/Proxy.Shared/MicroserviceMessage.cs b/src/Proxy.Shared/MicroserviceMessage.cs index 7493ab0..deb1932 100644 --- a/src/Proxy.Shared/MicroserviceMessage.cs +++ b/src/Proxy.Shared/MicroserviceMessage.cs @@ -1,51 +1,32 @@ using Newtonsoft.Json; -using System; -using System.Collections.Generic; namespace Proxy.Shared { - public sealed class MicroserviceMessage + public sealed class MicroserviceMessage(string host, string contentType) { - public List CallTimings { get; set; } + public List CallTimings { get; set; } = new(); public long CompletedOnUtc { get; set; } - public Dictionary Cookies { get; set; } - public string ErrorMessage { get; set; } + public Dictionary Cookies { get; set; } = new(); + public string? ErrorMessage { get; set; } public long ExecutionTimeMs => CompletedOnUtc - StartedOnUtc; - public Dictionary ExtendedProperties { get; set; } - public string Host { get; set; } - public Dictionary QueryParams { get; set; } - public string RequestBody { get; set; } - public Dictionary RequestHeaders { get; set; } - public string ResponseBody { get; set; } - public string ResponseContentType { get; set; } - public Dictionary ResponseHeaders { get; set; } - public int ResponseStatusCode { get; set; } + public Dictionary ExtendedProperties { get; set; } = new(); + public string Host { get; set; } = host; + public Dictionary QueryParams { get; set; } = new(); + public string? RequestBody { get; set; } + public Dictionary RequestHeaders { get; set; } = new(); + public string? ResponseBody { get; set; } + public string ResponseContentType { get; set; } = contentType; + public Dictionary ResponseHeaders { get; set; } = new(); + public int ResponseStatusCode { get; set; } = -1; public bool ShouldTerminateRequest { get; set; } - public long StartedOnUtc { get; set; } - public string Subject { get; set; } + public long StartedOnUtc { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + public string? Subject { get; set; } - public MicroserviceMessage(string host, string contentType) - { - // capture the time in epoch utc that this message was started - StartedOnUtc = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - - // default the response status code to an invalid value for comparison later on when the response is being processed by the RequestHandler - ResponseStatusCode = -1; - - // capture the host machine that we're executing on - Host = host; - - // capture the content type for the http response that we're configured for - ResponseContentType = contentType; - - // initialize the default properties - Cookies = new Dictionary(); - ExtendedProperties = new Dictionary(); - QueryParams = new Dictionary(); - RequestHeaders = new Dictionary(); - ResponseHeaders = new Dictionary(); - CallTimings = new List(); - } + // capture the time in epoch utc that this message was started + // default the response status code to an invalid value for comparison later on when the response is being processed by the RequestHandler + // capture the host machine that we're executing on + // capture the content type for the http response that we're configured for + // initialize the default properties public void MarkComplete() { @@ -57,7 +38,7 @@ public void SetResponse(object response) ResponseBody = JsonConvert.SerializeObject(response); } - public bool TryGetParam(string key, out T value) + public bool TryGetParam(string key, out T? value) { // try to find a parameter with matching name across the cookies, query parameters, request headers and extended properties if (QueryParams.TryGetValue(key, out var queryParamValue)) diff --git a/src/Proxy.Shared/MicroserviceMessageExtensions.cs b/src/Proxy.Shared/MicroserviceMessageExtensions.cs index c934f0d..19d0061 100644 --- a/src/Proxy.Shared/MicroserviceMessageExtensions.cs +++ b/src/Proxy.Shared/MicroserviceMessageExtensions.cs @@ -11,7 +11,7 @@ public static byte[] ToBytes(this MicroserviceMessage msg, JsonSerializerSetting return Encoding.UTF8.GetBytes(serializedMessage); } - public static MicroserviceMessage ToMicroserviceMessage(this byte[] data) + public static MicroserviceMessage? ToMicroserviceMessage(this byte[] data) { return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); } diff --git a/src/Proxy.Shared/NatsConfiguration.cs b/src/Proxy.Shared/NatsConfiguration.cs index fbff616..6b768bc 100644 --- a/src/Proxy.Shared/NatsConfiguration.cs +++ b/src/Proxy.Shared/NatsConfiguration.cs @@ -1,36 +1,28 @@ -using System; -using System.Collections.Generic; - -namespace Proxy.Shared +namespace Proxy.Shared { public class NatsConfiguration { - public string ClientName { get; set; } + public string ClientName { get; set; } = "N/A"; public int MaxPingsOut { get; set; } = 2; - public string[] NatsServerUrls { get; set; } - public List NatsSubscriptions { get; set; } + public required string[] NatsServerUrls { get; set; } = ["nats://localhost:4222"]; + public List NatsSubscriptions { get; set; } = []; public int PingInterval { get; set; } = 2000; - public NatsConfiguration() - { - NatsSubscriptions = new List(); - } - internal void Validate() { - // if the nats server url isn't specified throw throw an exception + // if the nats server url isn't specified throw an exception if (null == NatsServerUrls || NatsServerUrls.Length == 0) { throw new ArgumentNullException(nameof(NatsServerUrls)); } - // if the client name isn't specified throw throw an exception + // if the client name isn't specified throw an exception if (string.IsNullOrWhiteSpace(ClientName)) { throw new ArgumentNullException(nameof(ClientName)); } - // if there are no subscriptions specified throw throw an exception + // if there are no subscriptions specified throw an exception if (NatsSubscriptions.Count == 0) { throw new ArgumentException("You must have at least one subscription specified."); diff --git a/src/Proxy.Shared/NatsHelper.cs b/src/Proxy.Shared/NatsHelper.cs index a30d356..31624fc 100644 --- a/src/Proxy.Shared/NatsHelper.cs +++ b/src/Proxy.Shared/NatsHelper.cs @@ -1,22 +1,23 @@ using NATS.Client; using Newtonsoft.Json; using Serilog; -using System; using System.Text; -using System.Threading.Tasks; namespace Proxy.Shared { public static class NatsHelper { - private static IConnection _connection; + private static IConnection? _connection; public static void Configure(Action configAction) { // create a new instance of a configuration - var config = new NatsConfiguration(); + var config = new NatsConfiguration + { + NatsServerUrls = [] + }; - // allow the client to setup the subscriptions, connection name and nats server url + // allow the client to set up the subscriptions, connection name and nats server url configAction(config); // validate the configuration in case there are any problems @@ -26,15 +27,22 @@ public static void Configure(Action configAction) Connect(config.ClientName, config.NatsServerUrls, config.PingInterval, config.MaxPingsOut); // start the subscriptions + if (_connection is null) + { + throw new InvalidOperationException("The NATS connection is null"); + } + config.NatsSubscriptions.ForEach(s => _connection.SubscribeAsync(s.Subject, s.QueueGroup, s.Handler).Start()); } - public static void Publish(MicroserviceMessage message) + public static void Publish(MicroserviceMessage? message) { + ArgumentNullException.ThrowIfNull(message); + Publish(message.Subject, message); } - public static void Publish(string subject, MicroserviceMessage message) + public static void Publish(string? subject, MicroserviceMessage? message) { // validate params if (string.IsNullOrWhiteSpace(subject)) @@ -42,24 +50,26 @@ public static void Publish(string subject, MicroserviceMessage message) throw new ArgumentNullException(nameof(subject)); } - if (message == null) - { - throw new ArgumentNullException(nameof(message)); - } + ArgumentNullException.ThrowIfNull(message); // serialize the message var serializedMessage = JsonConvert.SerializeObject(message); // send the message + if (_connection is null) + { + throw new InvalidOperationException("The NATS connection is null"); + } + _connection.Publish(subject, Encoding.UTF8.GetBytes(serializedMessage)); } - public static Task RequestAsync(MicroserviceMessage message) + public static Task RequestAsync(MicroserviceMessage message) { return RequestAsync(message.Subject, message); } - public static async Task RequestAsync(string subject, MicroserviceMessage message) + private static async Task RequestAsync(string? subject, MicroserviceMessage message) { // validate params if (string.IsNullOrWhiteSpace(subject)) @@ -67,15 +77,17 @@ public static async Task RequestAsync(string subject, Micro throw new ArgumentNullException(nameof(subject)); } - if (message == null) - { - throw new ArgumentNullException(nameof(message)); - } + ArgumentNullException.ThrowIfNull(message); // serialize the message var serializedMessage = JsonConvert.SerializeObject(message); // send the message + if (_connection is null) + { + throw new InvalidOperationException("The NATS connection is null"); + } + var response = await _connection.RequestAsync(subject, Encoding.UTF8.GetBytes(serializedMessage)).ConfigureAwait(false); // return the response as a Microservice message @@ -87,7 +99,7 @@ private static void Connect(string clientName, string[] natsServerUrls, int ping // the params are validated in the Configure method so we don't need to revalidate here // if we're already connected to the nats server then do nothing - if (null != _connection && _connection.State == ConnState.CONNECTED) + if (_connection is { State: ConnState.CONNECTED }) { return; } @@ -101,31 +113,31 @@ private static void Connect(string clientName, string[] natsServerUrls, int ping options.PingInterval = pingInterval; options.MaxPingsOut = maxPingsOut; - options.AsyncErrorEventHandler += (sender, args) => + options.AsyncErrorEventHandler += (_, _) => { - Log.Information("The AsyncErrorEvent was just handled."); + Log.Information("The AsyncErrorEvent was just handled"); }; - options.ClosedEventHandler += (sender, args) => + options.ClosedEventHandler += (_, _) => { - Log.Information("The ClosedEvent was just handled."); + Log.Information("The ClosedEvent was just handled"); }; - options.DisconnectedEventHandler += (sender, args) => + options.DisconnectedEventHandler += (_, _) => { - Log.Information("The DisconnectedEvent was just handled."); + Log.Information("The DisconnectedEvent was just handled"); }; - options.ReconnectedEventHandler += (sender, args) => + options.ReconnectedEventHandler += (_, _) => { - Log.Information("The ReconnectedEvent was just handled."); + Log.Information("The ReconnectedEvent was just handled"); }; - options.ServerDiscoveredEventHandler += (sender, args) => + options.ServerDiscoveredEventHandler += (_, _) => { - Log.Information("The ServerDiscoveredEvent was just handled."); + Log.Information("The ServerDiscoveredEvent was just handled"); }; // create a connection to the NATS server _connection = connectionFactory.CreateConnection(options); - Log.Information("Connected to NATS server."); + Log.Information("Connected to NATS server"); } } } \ No newline at end of file diff --git a/src/Proxy.Shared/NatsSubscription.cs b/src/Proxy.Shared/NatsSubscription.cs index 05bcaa9..ceb1bdb 100644 --- a/src/Proxy.Shared/NatsSubscription.cs +++ b/src/Proxy.Shared/NatsSubscription.cs @@ -1,5 +1,4 @@ using NATS.Client; -using System; namespace Proxy.Shared { diff --git a/src/Proxy.Shared/NatsSubscriptionHandler.cs b/src/Proxy.Shared/NatsSubscriptionHandler.cs index 84220a4..40b508b 100644 --- a/src/Proxy.Shared/NatsSubscriptionHandler.cs +++ b/src/Proxy.Shared/NatsSubscriptionHandler.cs @@ -1,12 +1,11 @@ using Microsoft.Extensions.DependencyInjection; using NATS.Client; using Serilog; -using System; namespace Proxy.Shared { /// - /// This is a NATS subscription handler that abstracts all of the NATS related logic away from the use case classes. The goal of this class is to + /// This is a NATS subscription handler that abstracts all the NATS related logic away from the use case classes. The goal of this class is to /// provide the NATS specific code required so that the use case classes can be tested without having to deal with NATS related concerns. /// public class NatsSubscriptionHandler @@ -20,32 +19,42 @@ public NatsSubscriptionHandler(IServiceProvider serviceProvider) _serviceScopeFactory = serviceProvider.GetRequiredService(); } - public async void HandleMsgWith(object sender, MsgHandlerEventArgs e) where T : IMessageHandler + public async void HandleMsgWith(object? sender, MsgHandlerEventArgs e) where T : IMessageHandler { // store the replyTo subject var replyTo = e.Message.Reply; // deserialize the microservice msg that was embedded in the nats msg var microserviceMsg = e.Message.Data.ToMicroserviceMessage(); - MicroserviceMessage responseMsg = null; + MicroserviceMessage? responseMsg = null; try { // create a new scope to handle the message in. this will create a new instance of the handler class per message - using (var scope = _serviceScopeFactory.CreateScope()) - { - // create a new instance of the message handler - var msgHandler = scope.ServiceProvider.GetService(); + using var scope = _serviceScopeFactory.CreateScope(); + + // create a new instance of the message handler + var msgHandler = scope.ServiceProvider.GetService(); - // handle the message - responseMsg = await msgHandler.HandleAsync(microserviceMsg).ConfigureAwait(false); + if (msgHandler is null) + { + throw new InvalidOperationException($"Unable to resolve {typeof(T).Name} from the service provider."); } + + // handle the message + responseMsg = await msgHandler.HandleAsync(microserviceMsg).ConfigureAwait(false); } catch (Exception ex) { - microserviceMsg.ErrorMessage = ex.GetBaseException().Message; - microserviceMsg.ResponseStatusCode = 500; - Log.Error(microserviceMsg.ErrorMessage); + var msg = ex.GetBaseException().Message; + + if (microserviceMsg is not null) + { + microserviceMsg.ErrorMessage = msg; + microserviceMsg.ResponseStatusCode = 500; + } + + Log.Error(ex, "Error: {Error}", msg); } finally { @@ -57,7 +66,7 @@ public async void HandleMsgWith(object sender, MsgHandlerEventArgs e) where T } } - public async void ObserveMsgWith(object sender, MsgHandlerEventArgs e) where T : IMessageObserver + public async void ObserveMsgWith(object? sender, MsgHandlerEventArgs e) where T : IMessageObserver { // deserialize the microservice msg that was embedded in the nats msg var microserviceMsg = e.Message.Data.ToMicroserviceMessage(); @@ -65,18 +74,22 @@ public async void ObserveMsgWith(object sender, MsgHandlerEventArgs e) where try { // create a new scope to handle the message in. this will create a new instance of the observer class per message - using (var scope = _serviceScopeFactory.CreateScope()) - { - // create a new instance of the message handler - var msgObserver = scope.ServiceProvider.GetService(); + using var scope = _serviceScopeFactory.CreateScope(); - // observe the message - await msgObserver.ObserveAsync(microserviceMsg).ConfigureAwait(false); + // create a new instance of the message handler + var msgObserver = scope.ServiceProvider.GetService(); + + // observe the message + if (msgObserver is null) + { + throw new InvalidOperationException($"Unable to resolve {typeof(T).Name} from the service provider"); } + + await msgObserver.ObserveAsync(microserviceMsg).ConfigureAwait(false); } catch (Exception ex) { - Log.Error(ex.GetBaseException().Message); + Log.Error(ex, "Error: {Error}", ex.Message); } } } diff --git a/src/Proxy.Shared/Proxy.Shared.csproj b/src/Proxy.Shared/Proxy.Shared.csproj index 1f2ec36..b3dfda6 100644 --- a/src/Proxy.Shared/Proxy.Shared.csproj +++ b/src/Proxy.Shared/Proxy.Shared.csproj @@ -1,22 +1,23 @@  - netstandard2.0 + net9.0 + enable + enable HttpNatsProxy.Shared Wes Shaddix A shared library with types and helper classes for interacting with the http-nats-proxy - https://github.com/wshaddix/http-nats-proxy - 1.1.2 + 2.0.0 - - - - - + + + + + diff --git a/src/Proxy/HttpRequestParser.cs b/src/Proxy/HttpRequestParser.cs index 8a7eacf..d51b637 100644 --- a/src/Proxy/HttpRequestParser.cs +++ b/src/Proxy/HttpRequestParser.cs @@ -1,19 +1,14 @@ -using Microsoft.AspNetCore.Http; -using System.Collections.Generic; -using System.IO; -using System.Text; +using System.Text; namespace Proxy { internal class HttpRequestParser { - public static string ParseBody(Stream requestBody) + public static async Task ParseBodyAsync(Stream requestBody) { // if there is a body with the request then read it - using (var reader = new StreamReader(requestBody, Encoding.UTF8)) - { - return reader.ReadToEnd(); - } + using var reader = new StreamReader(requestBody, Encoding.UTF8); + return await reader.ReadToEndAsync(); } public static Dictionary ParseCookies(IRequestCookieCollection requestCookies) @@ -34,7 +29,7 @@ public static Dictionary ParseHeaders(IHeaderDictionary requestH foreach (var header in requestHeaders) { - headers.Add(header.Key, string.Join(',', header.Value)); + headers.Add(header.Key, string.Join(',', header.Value.ToString())); } return headers; @@ -46,7 +41,7 @@ public static Dictionary ParseQueryParams(IQueryCollection reque foreach (var param in requestQuery) { - queryParams.Add(param.Key, string.Join(',', param.Value)); + queryParams.Add(param.Key, string.Join(',', param.Value.ToString())); } return queryParams; diff --git a/src/Proxy/HttpResponseFactory.cs b/src/Proxy/HttpResponseFactory.cs index f429384..39b2c5d 100644 --- a/src/Proxy/HttpResponseFactory.cs +++ b/src/Proxy/HttpResponseFactory.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Http; -using Newtonsoft.Json; +using Newtonsoft.Json; using Proxy.Shared; namespace Proxy diff --git a/src/Proxy/NatsSubjectParser.cs b/src/Proxy/NatsSubjectParser.cs index 857f40a..e6c33fd 100644 --- a/src/Proxy/NatsSubjectParser.cs +++ b/src/Proxy/NatsSubjectParser.cs @@ -4,7 +4,7 @@ namespace Proxy { internal static class NatsSubjectParser { - internal static string Parse(string httpMethod, string urlPath) + internal static string? Parse(string httpMethod, string urlPath) { // replace all forward slashes with periods in the http request path var subjectPath = urlPath.Replace('/', '.').TrimEnd('.'); diff --git a/src/Proxy/Observer.cs b/src/Proxy/Observer.cs index dfad55a..926f24e 100644 --- a/src/Proxy/Observer.cs +++ b/src/Proxy/Observer.cs @@ -2,6 +2,6 @@ { public class Observer { - public string Subject { get; set; } + public required string Subject { get; set; } } } \ No newline at end of file diff --git a/src/Proxy/Pipeline.cs b/src/Proxy/Pipeline.cs index 61fac00..fd8291a 100644 --- a/src/Proxy/Pipeline.cs +++ b/src/Proxy/Pipeline.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Proxy +namespace Proxy { public class Pipeline { diff --git a/src/Proxy/PipelineExecutor.cs b/src/Proxy/PipelineExecutor.cs index c9cff3d..b6b1768 100644 --- a/src/Proxy/PipelineExecutor.cs +++ b/src/Proxy/PipelineExecutor.cs @@ -1,12 +1,8 @@ using NATS.Client; using Newtonsoft.Json; using Proxy.Shared; -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Text; -using System.Threading.Tasks; namespace Proxy { @@ -60,7 +56,14 @@ private static MicroserviceMessage ExtractMessageFromReply(Msg reply) { // the NATS msg.Data property is a json encoded instance of our MicroserviceMessage so we convert it from a byte[] to a string and then // deserialize it from json - return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(reply.Data)); + var message = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(reply.Data)); + + if (message is null) + { + throw new Exception("The NATS reply message is not a valid MicroserviceMessage"); + } + + return message; } private static void MergeMessageProperties(MicroserviceMessage message, MicroserviceMessage responseMessage) diff --git a/src/Proxy/Program.cs b/src/Proxy/Program.cs index 684c4c8..37bfd84 100644 --- a/src/Proxy/Program.cs +++ b/src/Proxy/Program.cs @@ -1,17 +1,13 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using NATS.Client; +using NATS.Client; using Serilog; -using System; using System.Net; namespace Proxy { public class Program { - private static ProxyConfiguration _config; - private static IWebHost _host; + private static ProxyConfiguration? _config; + private static IWebHost? _host; public static void Main(string[] args) { @@ -28,7 +24,7 @@ public static void Main(string[] args) ConfigureWebHost(); // run the host - _host.Run(); + _host?.Run(); } private static void ConfigureEnvironment() @@ -76,14 +72,16 @@ private static void ConfigureLogging() private static void ConfigureWebHost() { + Proxy.Shared.Guard.AgainstNull(_config); + // create the request handler - var requestHandler = new RequestHandler(_config); + var requestHandler = new RequestHandler(_config!); _host = new WebHostBuilder() .UseKestrel(options => { - // tell Kestrel to listen on all ip addresses at the specififed port - options.Listen(IPAddress.Any, int.Parse(_config.Port)); + // tell Kestrel to listen on all ip addresses at the specified port + options.Listen(IPAddress.Any, int.Parse(_config!.Port)); }) .ConfigureServices(services => { @@ -95,8 +93,8 @@ private static void ConfigureWebHost() builder .AllowAnyHeader() .AllowAnyMethod() - .AllowAnyOrigin() - .AllowCredentials(); + .AllowAnyOrigin(); + //.AllowCredentials(); }); }); }) @@ -113,7 +111,9 @@ private static void ConfigureWebHost() private static void ConnectToNats() { - Log.Information("Attempting to connect to NATS server at: {NatsUrl}", _config.NatsUrl); + Shared.Guard.AgainstNull(_config); + + Log.Information("Attempting to connect to NATS server at: {NatsUrl}", _config!.NatsUrl); // create a connection to the NATS server var connectionFactory = new ConnectionFactory(); @@ -124,23 +124,23 @@ private static void ConnectToNats() options.MaxPingsOut = 2; options.AsyncErrorEventHandler += (sender, args) => { - Log.Information("The AsyncErrorEvent was just handled."); + Log.Information("The AsyncErrorEvent was just handled"); }; options.ClosedEventHandler += (sender, args) => { - Log.Information("The ClosedEvent was just handled."); + Log.Information("The ClosedEvent was just handled"); }; options.DisconnectedEventHandler += (sender, args) => { - Log.Information("The DisconnectedEvent was just handled."); + Log.Information("The DisconnectedEvent was just handled"); }; options.ReconnectedEventHandler += (sender, args) => { - Log.Information("The ReconnectedEvent was just handled."); + Log.Information("The ReconnectedEvent was just handled"); }; options.ServerDiscoveredEventHandler += (sender, args) => { - Log.Information("The ServerDiscoveredEvent was just handled."); + Log.Information("The ServerDiscoveredEvent was just handled"); }; options.Name = "http-nats-proxy"; _config.NatsConnection = connectionFactory.CreateConnection(options); diff --git a/src/Proxy/Proxy.csproj b/src/Proxy/Proxy.csproj index e831cbf..4b2d81b 100644 --- a/src/Proxy/Proxy.csproj +++ b/src/Proxy/Proxy.csproj @@ -1,20 +1,24 @@ - netcoreapp2.2 + net9.0 + enable + enable - - - - - - - - - - + + + + + + + + + + + + diff --git a/src/Proxy/ProxyConfiguration.cs b/src/Proxy/ProxyConfiguration.cs index 2e75371..c4a2f72 100644 --- a/src/Proxy/ProxyConfiguration.cs +++ b/src/Proxy/ProxyConfiguration.cs @@ -1,10 +1,6 @@ using NATS.Client; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -12,34 +8,26 @@ namespace Proxy { public class ProxyConfiguration { - public JsonSerializerSettings JsonSerializerSettings; - public string ContentType { get; set; } - public int DeleteStatusCode { get; set; } - public int GetStatusCode { get; set; } - public int HeadStatusCode { get; set; } - public string Host { get; set; } - public Pipeline IncomingPipeline { get; private set; } - public IConnection NatsConnection { get; set; } - public string NatsUrl { get; set; } - public IList Observers { get; set; } - public Pipeline OutgoingPipeline { get; private set; } - public int PatchStatusCode { get; set; } - public string PipelineConfigFile { get; set; } - public string Port { get; set; } - public int PostStatusCode { get; set; } - public int PutStatusCode { get; set; } - public int Timeout { get; set; } - - public ProxyConfiguration() + public readonly JsonSerializerSettings JsonSerializerSettings = new() { - Host = Environment.MachineName; - - // configure the json serializer settings to use - JsonSerializerSettings = new JsonSerializerSettings - { - ContractResolver = new CamelCasePropertyNamesContractResolver() - }; - } + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + public required string ContentType { get; init; } + public int DeleteStatusCode { get; init; } + public int GetStatusCode { get; init; } + public int HeadStatusCode { get; init; } + public string Host { get; set; } = Environment.MachineName; + public Pipeline? IncomingPipeline { get; private set; } + public IConnection? NatsConnection { get; set; } + public required string NatsUrl { get; init; } + public IList Observers { get; set; } = new List(); + public Pipeline? OutgoingPipeline { get; private set; } + public int PatchStatusCode { get; init; } + public required string PipelineConfigFile { get; init; } + public required string Port { get; init; } + public int PostStatusCode { get; init; } + public int PutStatusCode { get; init; } + public int Timeout { get; init; } public void Build() { @@ -69,7 +57,7 @@ private Pipeline BuildRequestPipeline() } var deserializer = new DeserializerBuilder() - .WithNamingConvention(new CamelCaseNamingConvention()) + .WithNamingConvention(CamelCaseNamingConvention.Instance) .Build(); pipeline = deserializer.Deserialize(File.ReadAllText(PipelineConfigFile)); @@ -79,16 +67,16 @@ private Pipeline BuildRequestPipeline() // if the pipeline config file has not been set then just send all requests directly to the microservice pipeline = new Pipeline { - Steps = new List - { - new Step - { - Subject = "*", - Direction = "incoming", - Order = 1, - Pattern = "request" - } - } + Steps = + [ + new Step + { + Subject = "*", + Direction = "incoming", + Order = 1, + Pattern = "request" + } + ] }; } @@ -99,7 +87,7 @@ private void ConfigureIncomingPipeline(Pipeline pipeline) { IncomingPipeline = new Pipeline(); - // return just those steps that are to be ran for the directions of "incoming" or "both" + // return just those steps that are to be run for the directions of "incoming" or "both" var directions = new List { "incoming", "both" }; foreach (var step in pipeline.Steps.OrderBy(s => s.Order)) diff --git a/src/Proxy/RequestHandler.cs b/src/Proxy/RequestHandler.cs index d810acb..552926e 100644 --- a/src/Proxy/RequestHandler.cs +++ b/src/Proxy/RequestHandler.cs @@ -1,8 +1,5 @@ -using Microsoft.AspNetCore.Http; -using Newtonsoft.Json; +using Newtonsoft.Json; using Proxy.Shared; -using System; -using System.Threading.Tasks; namespace Proxy { @@ -14,11 +11,16 @@ public class RequestHandler public RequestHandler(ProxyConfiguration config) { _config = config; - _pipelineExecutor = new PipelineExecutor(natsConnection: _config.NatsConnection, + + Guard.AgainstNull(_config.NatsConnection); + Guard.AgainstNull(_config.IncomingPipeline); + Guard.AgainstNull(_config.OutgoingPipeline); + + _pipelineExecutor = new PipelineExecutor(natsConnection: _config.NatsConnection!, jsonSerializerSettings: _config.JsonSerializerSettings, timeout: _config.Timeout, - incomingPipeline: _config.IncomingPipeline, - outgoingPipeline: _config.OutgoingPipeline, + incomingPipeline: _config.IncomingPipeline!, + outgoingPipeline: _config.OutgoingPipeline!, observers: _config.Observers); } @@ -29,7 +31,7 @@ public async Task HandleAsync(HttpContext context) try { // create a nats message from the http request - CreateNatsMsgFromHttpRequest(context.Request, message); + await CreateNatsMsgFromHttpRequestAsync(context.Request, message); // execute the request pipeline await _pipelineExecutor.ExecutePipelineAsync(message).ConfigureAwait(false); @@ -74,19 +76,24 @@ public async Task HandleAsync(HttpContext context) } } - private static void CreateNatsMsgFromHttpRequest(HttpRequest httpRequest, MicroserviceMessage message) + private static async Task CreateNatsMsgFromHttpRequestAsync(HttpRequest httpRequest, MicroserviceMessage message) { // create a NATS subject from the request method and path + if (httpRequest.Path.Value is null) + { + throw new Exception("httpRequest.Path.Value is null"); + } + message.Subject = NatsSubjectParser.Parse(httpRequest.Method, httpRequest.Path.Value); // parse the request headers, cookies, query params and body and put them on the message - ParseHttpRequest(httpRequest, message); + await ParseHttpRequestAsync(httpRequest, message); } - private static void ParseHttpRequest(HttpRequest request, MicroserviceMessage message) + private static async Task ParseHttpRequestAsync(HttpRequest request, MicroserviceMessage message) { // parse the http request body - message.RequestBody = HttpRequestParser.ParseBody(request.Body); + message.RequestBody = await HttpRequestParser.ParseBodyAsync(request.Body); // parse the request headers message.RequestHeaders = HttpRequestParser.ParseHeaders(request.Headers); diff --git a/src/Proxy/Step.cs b/src/Proxy/Step.cs index 8862984..97b1251 100644 --- a/src/Proxy/Step.cs +++ b/src/Proxy/Step.cs @@ -2,9 +2,9 @@ { public class Step { - public string Direction { get; set; } + public required string Direction { get; set; } public int Order { get; set; } - public string Pattern { get; set; } - public string Subject { get; set; } + public required string Pattern { get; set; } + public required string Subject { get; set; } } } \ No newline at end of file diff --git a/src/Proxy/StepException.cs b/src/Proxy/StepException.cs index 558e7c2..8e69945 100644 --- a/src/Proxy/StepException.cs +++ b/src/Proxy/StepException.cs @@ -1,18 +1,16 @@ -using System; - -namespace Proxy +namespace Proxy { public class StepException : Exception { - public string Msg { get; private set; } - public string Pattern { get; private set; } - public string Subject { get; private set; } + public string? Msg { get; private set; } + public string? Pattern { get; private set; } + public string? Subject { get; private set; } public StepException(string message) : base(message) { } - public StepException(string subject, string pattern, string msg) + public StepException(string? subject, string pattern, string msg) { Subject = subject; Pattern = pattern; diff --git a/src/docker-compose.yml b/src/docker-compose.yml index 771fadd..070f703 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -1,5 +1,5 @@ version: '3.6' - +name: http-nats-proxy services: nats: image: nats:1.4.1 diff --git a/src/global.json b/src/global.json index da9051d..6a56c88 100644 --- a/src/global.json +++ b/src/global.json @@ -1,5 +1,7 @@ -{ - "sdk": { - "version": "2.2.301" - } -} +{ + "sdk": { + "version": "9.0.100", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file From b3129a1aa88c95ec60f468f903968388634dea67 Mon Sep 17 00:00:00 2001 From: Wes Shaddix Date: Tue, 19 Nov 2024 15:29:30 -0500 Subject: [PATCH 2/4] adding bruno api call collection for testing --- bruno-collection/NATS Proxy/Get Customer.bru | 15 +++++++++++++++ bruno-collection/NATS Proxy/Health Check.bru | 11 +++++++++++ bruno-collection/NATS Proxy/bruno.json | 9 +++++++++ 3 files changed, 35 insertions(+) create mode 100644 bruno-collection/NATS Proxy/Get Customer.bru create mode 100644 bruno-collection/NATS Proxy/Health Check.bru create mode 100644 bruno-collection/NATS Proxy/bruno.json diff --git a/bruno-collection/NATS Proxy/Get Customer.bru b/bruno-collection/NATS Proxy/Get Customer.bru new file mode 100644 index 0000000..f965de6 --- /dev/null +++ b/bruno-collection/NATS Proxy/Get Customer.bru @@ -0,0 +1,15 @@ +meta { + name: Get Customer + type: http + seq: 3 +} + +get { + url: http://localhost:5000/customers?id=324 + body: none + auth: none +} + +params:query { + id: 324 +} diff --git a/bruno-collection/NATS Proxy/Health Check.bru b/bruno-collection/NATS Proxy/Health Check.bru new file mode 100644 index 0000000..4614ef7 --- /dev/null +++ b/bruno-collection/NATS Proxy/Health Check.bru @@ -0,0 +1,11 @@ +meta { + name: Health Check + type: http + seq: 2 +} + +get { + url: http://localhost:5000/healthcheck + body: none + auth: none +} diff --git a/bruno-collection/NATS Proxy/bruno.json b/bruno-collection/NATS Proxy/bruno.json new file mode 100644 index 0000000..5134456 --- /dev/null +++ b/bruno-collection/NATS Proxy/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "NATS Proxy", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file From d893ff4e2bf54e19b684dc491330257a13e3b19b Mon Sep 17 00:00:00 2001 From: Wes Shaddix Date: Thu, 21 Nov 2024 08:58:48 -0500 Subject: [PATCH 3/4] chore(update to .net 9) - updating to .Net 9 and various code fixes --- CHANGELOG.md | 33 ++- README.md | 94 +++++-- src/Directory.Packages.props | 36 +-- src/Dockerfile | 1 - src/ExampleHandlers/ExampleHandlers.csproj | 32 +-- src/ExampleHandlers/GetCustomer.cs | 58 ++-- src/ExampleHandlers/Healthcheck.cs | 36 ++- src/ExampleHandlers/Logging.cs | 13 +- src/ExampleHandlers/Metrics.cs | 26 +- src/ExampleHandlers/Program.cs | 154 +++++----- src/ExampleHandlers/Tracing.cs | 44 ++- src/Proxy.Shared/CallTiming.cs | 11 +- src/Proxy.Shared/Guard.cs | 5 +- src/Proxy.Shared/IMessageHandler.cs | 9 +- src/Proxy.Shared/IMessageObserver.cs | 9 +- src/Proxy.Shared/MicroserviceMessage.cs | 111 ++++---- .../MicroserviceMessageExtensions.cs | 25 +- src/Proxy.Shared/NatsConfiguration.cs | 42 ++- src/Proxy.Shared/NatsHelper.cs | 208 ++++++-------- src/Proxy.Shared/NatsSubscription.cs | 23 +- src/Proxy.Shared/NatsSubscriptionHandler.cs | 134 +++++---- src/Proxy.Shared/Proxy.Shared.csproj | 36 +-- src/Proxy/HttpRequestParser.cs | 78 +++-- src/Proxy/HttpResponseFactory.cs | 49 ++-- src/Proxy/NatsMessageFactory.cs | 11 +- src/Proxy/NatsSubjectParser.cs | 25 +- src/Proxy/Observer.cs | 9 +- src/Proxy/Pipeline.cs | 19 +- src/Proxy/PipelineExecutor.cs | 266 ++++++++---------- src/Proxy/Program.cs | 251 +++++++++-------- src/Proxy/Proxy.csproj | 50 ++-- src/Proxy/ProxyConfiguration.cs | 194 ++++++------- src/Proxy/RequestHandler.cs | 234 ++++++++------- src/Proxy/Step.cs | 15 +- src/Proxy/StepException.cs | 29 +- 35 files changed, 1144 insertions(+), 1226 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ed0e98..0169bdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,15 @@ * now honoring the `NatsMessage.responseStatusCode` if it has been set by the microservice handling the request. -* if the `NatsMessage.errorMessage` property is set then the http-nats-proxy will return a status code 500 with a formatted error message to the api client. +* if the `NatsMessage.errorMessage` property is set then the http-nats-proxy will return a status code 500 with a + formatted error message to the api client. * now returning the `NatsMessage.response` as the http response. ## 1.0.0 -* added pipeline feature and refactored the solution to have working examples of logging, metrics, authentiation and trace header injection. +* added pipeline feature and refactored the solution to have working examples of logging, metrics, authentiation and + trace header injection. ## 1.0.1 @@ -24,11 +26,14 @@ ## 1.0.3 -* changing the format of the message that the proxy sends to microservices and the pipeline steps to make them more intuitive. Specifically changed `Cookies, ExtendedProperties, QueryParams, RequestHeaders` and `ResponseHeaders` from name/value collections to `Dictionary` so they serialize into a more intuitive json string. +* changing the format of the message that the proxy sends to microservices and the pipeline steps to make them more + intuitive. Specifically changed `Cookies, ExtendedProperties, QueryParams, RequestHeaders` and `ResponseHeaders` from + name/value collections to `Dictionary` so they serialize into a more intuitive json string. ## 1.0.4 -* changed the call timings from a tuple to a custom class because tuples do not serialize to json with readable property names (see https://github.com/JamesNK/Newtonsoft.Json/issues/1230) +* changed the call timings from a tuple to a custom class because tuples do not serialize to json with readable property + names (see https://github.com/JamesNK/Newtonsoft.Json/issues/1230) * renamed property `Body` to `RequestBody` on the `NatsMessage` * renamed property `Response` to `ResponseBody` on the `NatsMessage` * introduced the concept of Observers to the request pipeline (see README for details) @@ -39,11 +44,13 @@ ## 1.0.6 -* bugfix - when CORS was enabled in release 1.0.5 I didn't fully enable it b/c i didn't include the .AllowCredentials() method on the CORS policy builder +* bugfix - when CORS was enabled in release 1.0.5 I didn't fully enable it b/c i didn't include the .AllowCredentials() + method on the CORS policy builder ## 1.1.0 -* extracted some types and helper classes into Proxy.Shared and made into a nuget package for other c# based microservices to leverage +* extracted some types and helper classes into Proxy.Shared and made into a nuget package for other c# based + microservices to leverage * refactored the example handlers (handlers and observers) to use the Proxy.Shared library * updated the docker image to use dotnet core 2.1 alpine * updated logging to use serilog instead of console.writeline @@ -51,17 +58,19 @@ * refactored the request handler to be less bloated and more focused * updated nats to version 1.2.0 * now compiling against .net core 2.1.300 -* added a .TryGetParam() method to the MicroserviceMessage to make it easier to get at headers, cookies and query string params +* added a .TryGetParam() method to the MicroserviceMessage to make it easier to get at headers, cookies and query string + params ## 1.1.1 -* fixing issue where response type was not getting set +* fixing issue where response type was not getting set ## 1.1.2 * code cleanup * updated .net core version to 2.2.301 -* updated docker images to mcr.microsoft.com/dotnet/core/sdk:2.2.301-alpine3.9 and mcr.microsoft.com/dotnet/core/runtime:2.2.6-alpine3.9 +* updated docker images to mcr.microsoft.com/dotnet/core/sdk:2.2.301-alpine3.9 and + mcr.microsoft.com/dotnet/core/runtime:2.2.6-alpine3.9 * updated NATS server version to 1.4.1 * updated projects target framework to .Net Core 2.2 * merged in PR to fix parsing of extended properties during pipeline execution (thanks to https://github.com/timsmid) @@ -72,7 +81,9 @@ * Updated to .Net 9 * Migrated to centralized package management * Updated log messages to follow best practices (no trailing period) -* Fixed typo in code comment +* Fixed typos in code comments * Added additional error handling and null checks * Updated any outdated or vulnerable NuGet packages -* Updated Dockerfile to latest versions and addressed scout vulnerabilities \ No newline at end of file +* Updated Dockerfile to latest versions and addressed scout vulnerabilities +* Added new env vars to enhance logging (see README) +* Added Bruno (https://www.usebruno.com/) collection of api tests diff --git a/README.md b/README.md index 36e349a..9c05767 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,30 @@ # http-nats-proxy + A proxy service that converts http requests to [NATS](https://nats.io) messages. ## Purpose -This proxy service is designed to be ran in a docker container and will translate all of your http based api calls (REST endpoints). It will translate the http route to a [NATS](https://nats.io) message and will send the message as a request into the NATS messaging system. Once a reply has been returned it is written back to the http response. The primary use case for this service is when you have an ecosystem of microservices that communicate via NATS but you want http REST api clients to be able to communicate to those services as a typical http REST api client. + +This proxy service is designed to be ran in a docker container and will translate all of your http based api calls (REST +endpoints). It will translate the http route to a [NATS](https://nats.io) message and will send the message as a request +into the NATS messaging system. Once a reply has been returned it is written back to the http response. The primary use +case for this service is when you have an ecosystem of microservices that communicate via NATS but you want http REST +api clients to be able to communicate to those services as a typical http REST api client. ## Features ### Routing -When an http request is received the http-nats-proxy will take the request path and replace every forward slash ('/') with a period ('.'). It will then prefix that with the http verb that was used (get, put, post, delete, head, etc.). Finally it will convert the entire string to lowercase. This becomes your NATS subject where the message will be sent. -For example, the http request of `GET http://localhost:5000/test/v1/customer?id=1234&show-history=true` will result in a NATS message subject of `get.test.v1.customer` +When an http request is received the http-nats-proxy will take the request path and replace every forward slash ('/') +with a period ('.'). It will then prefix that with the http verb that was used (get, put, post, delete, head, etc.). +Finally it will convert the entire string to lowercase. This becomes your NATS subject where the message will be sent. +For example, the http request of `GET http://localhost:5000/test/v1/customer?id=1234&show-history=true` will result in a +NATS message subject of `get.test.v1.customer` ### HTTP Request Format Translation -When an http request is received, the http-nats-proxy will extract the headers, cookies, query string parameters and the body of the request and create a NATS message. The NATS message that goes into the messaging system will be a json serialized object that has the following structure: +When an http request is received, the http-nats-proxy will extract the headers, cookies, query string parameters and the +body of the request and create a NATS message. The NATS message that goes into the messaging system will be a json +serialized object that has the following structure: ``` { @@ -47,21 +58,37 @@ When an http request is received, the http-nats-proxy will extract the headers, ### Pipelines -Every http(s) request that comes into the http-nats-proxy can go through a series of steps before being delivered to the final destination (your microservice). Those steps are defined in a yaml based configuration file and the location of the file is passed to the https-nats-proxy via the `HTTP_NATS_PROXY_REQUEST_PIPELINE_CONFIG_FILE` environment variable. Each step defined in the pipeline configuration file contains the following properties: +Every http(s) request that comes into the http-nats-proxy can go through a series of steps before being delivered to the +final destination (your microservice). Those steps are defined in a yaml based configuration file and the location of +the file is passed to the https-nats-proxy via the `HTTP_NATS_PROXY_REQUEST_PIPELINE_CONFIG_FILE` environment variable. +Each step defined in the pipeline configuration file contains the following properties: -**subject:** The NATS subject name where the request will be delivered for this particular step in the pipeline. In order to notify the http-nats-proxy to send the message to your microservice use `*` for the subject name. +**subject:** The NATS subject name where the request will be delivered for this particular step in the pipeline. In +order to notify the http-nats-proxy to send the message to your microservice use `*` for the subject name. -**pattern:** Either `publish` or `request`, this tells the http-nats-proxy whether it should do a fire and forget message exchange pattern or if it should use the request/reply pattern. If you are not going to change or cancel the http request then you should use `publish`. If your pipeline step will potentially modify the request or cancel the request you should use `request` +**pattern:** Either `publish` or `request`, this tells the http-nats-proxy whether it should do a fire and forget +message exchange pattern or if it should use the request/reply pattern. If you are not going to change or cancel the +http request then you should use `publish`. If your pipeline step will potentially modify the request or cancel the +request you should use `request` -**direction:** Either `incoming, outgoing` or `both`. This tells the http-nats-proxy when to call your pipeline step. `incoming` is when the message is inbound, `outgoing` is after the message has reached the end of the pipeline and the response is being returned. `both` will call you pipeline step twice, once when the request is inbound and a second time when the response is outbound. +**direction:** Either `incoming, outgoing` or `both`. This tells the http-nats-proxy when to call your pipeline step. +`incoming` is when the message is inbound, `outgoing` is after the message has reached the end of the pipeline and the +response is being returned. `both` will call you pipeline step twice, once when the request is inbound and a second time +when the response is outbound. **order:** A numeric value that is the order that your step should be called in relation to other steps in the pipeline ### Observers -There are times when you want to get a copy of the request/response message after it has completed running through all of the pipeline steps. Examples of this would be when you wanted to log the request/response or capture metrics about how long each request took to process, etc. In these cases, the metadata about the request is not available during the execution of the pipeline steps. For these scenarios you can use Observers. Observers are notified via a NATS publish message after all of the pipeline steps have executed and metadata has been stored for the request/response pair. It is a "copy" of the final state of the http request. +There are times when you want to get a copy of the request/response message after it has completed running through all +of the pipeline steps. Examples of this would be when you wanted to log the request/response or capture metrics about +how long each request took to process, etc. In these cases, the metadata about the request is not available during the +execution of the pipeline steps. For these scenarios you can use Observers. Observers are notified via a NATS publish +message after all of the pipeline steps have executed and metadata has been stored for the request/response pair. It is +a "copy" of the final state of the http request. #### Example pipeline-config.yaml file + ``` steps: - subject: trace.header @@ -84,26 +111,27 @@ observers: ``` ## Configuration -All configuration of the http-nats-proxy is done via environment variables. - -| Environment Variable | Default Value | Description | -|------------------------------------------|---------------------------------|------------------------------------------| -| HTTP_NATS_PROXY_HOST_PORT | 5000 | The port that the http-nats-proxy will listen for incoming http requests on | -| HTTP_NATS_PROXY_NAT_URL | nats://localhost:4222 | The NATS url where the http-nats-proxy will send the NATS message to | -| HTTP_NATS_PROXY_WAIT_TIMEOUT_SECONDS | 10 | The number of seconds that the http-nats-proxy will wait for a response from the microservice backend before it returns a Timeout Error to the http client | -| HTTP_NATS_PROXY_HEAD_STATUS_CODE | 200 | The http status code that will be used for a successful HEAD request | -| HTTP_NATS_PROXY_PUT_STATUS_CODE | 201 | The http status code that will be used for a successful PUT request | -| HTTP_NATS_PROXY_GET_STATUS_CODE | 200 | The http status code that will be used for a successful GET request | -| HTTP_NATS_PROXY_PATCH_STATUS_CODE | 201 | The http status code that will be used for a successful PATCH request | -| HTTP_NATS_PROXY_POST_STATUS_CODE | 201 | The http status code that will be used for a successful POST request | -| HTTP_NATS_PROXY_DELETE_STATUS_CODE | 204 | The http status code that will be used for a successful DELETE request | -| HTTP_NATS_PROXY_CONTENT_TYPE | application/json; charset=utf-8 | The http response Content-Type header value. This should be set to whatever messaging format your microservice api supports (xml, json, etc) | -| HTTP_NATS_PROXY_REQUEST_PIPELINE_CONFIG_FILE | | The full file path and name of the configuration file that specifies your request pipeline | +All configuration of the http-nats-proxy is done via environment variables. +| Environment Variable | Default Value | Description | +|----------------------------------------------|---------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------| +| HTTP_NATS_PROXY_HOST_PORT | 5000 | The port that the http-nats-proxy will listen for incoming http requests on | +| HTTP_NATS_PROXY_NAT_URL | nats://localhost:4222 | The NATS url where the http-nats-proxy will send the NATS message to | +| HTTP_NATS_PROXY_WAIT_TIMEOUT_SECONDS | 10 | The number of seconds that the http-nats-proxy will wait for a response from the microservice backend before it returns a Timeout Error to the http client | +| HTTP_NATS_PROXY_HEAD_STATUS_CODE | 200 | The http status code that will be used for a successful HEAD request | +| HTTP_NATS_PROXY_PUT_STATUS_CODE | 201 | The http status code that will be used for a successful PUT request | +| HTTP_NATS_PROXY_GET_STATUS_CODE | 200 | The http status code that will be used for a successful GET request | +| HTTP_NATS_PROXY_PATCH_STATUS_CODE | 201 | The http status code that will be used for a successful PATCH request | +| HTTP_NATS_PROXY_POST_STATUS_CODE | 201 | The http status code that will be used for a successful POST request | +| HTTP_NATS_PROXY_DELETE_STATUS_CODE | 204 | The http status code that will be used for a successful DELETE request | +| HTTP_NATS_PROXY_CONTENT_TYPE | application/json; charset=utf-8 | The http response Content-Type header value. This should be set to whatever messaging format your microservice api supports (xml, json, etc) | +| HTTP_NATS_PROXY_REQUEST_PIPELINE_CONFIG_FILE | | The full file path and name of the configuration file that specifies your request pipeline | ## Running the Demo -In order to see the http-nats-proxy in action along with a test microservice, logging and metrics you can run the docker compose file in your environment. + +In order to see the http-nats-proxy in action along with a test microservice, logging and metrics you can run the docker +compose file in your environment. ``` cd src @@ -112,16 +140,22 @@ docker-compose up This will run the NATS server in a container. -Next start the project from Visual Studio and make sure that each project in the solution is set to run on startup (multi-project start-up configuration). The http-nats-proxy will listen for http requests on port 5000 of your host machine. You can then send http requests to the proxy and have them processed by the test microservice. The test microservice that comes with this repo will respond to the following http routes: +Next start the project from Visual Studio and make sure that each project in the solution is set to run on startup ( +multi-project start-up configuration). The http-nats-proxy will listen for http requests on port 5000 of your host +machine. You can then send http requests to the proxy and have them processed by the test microservice. The test +microservice that comes with this repo will respond to the following http routes: ``` GET http://localhost:5000/healthcheck GET http://localhost:5000/customers?id= ``` -Additionally, if configured for metrics, logging and tracing you will see the output of those microservices as well in your terminal. This is for demo purposes only. There is a 1/2 second delay in the logging and metrics microservices just to simulate work. +Additionally, if configured for metrics, logging and tracing you will see the output of those microservices as well in +your terminal. This is for demo purposes only. There is a 1/2 second delay in the logging and metrics microservices just +to simulate work. ## Creating your own docker image + The http-nats-proxy has a `Dockerfile` where you can build your own docker images by running: ``` @@ -132,4 +166,8 @@ docker build -t http-nats-proxy . ## Responsibilities of your microservices -In order to control what gets returned from the http-nats-proxy, your microservice has to set the `response` property of the NATS message that you receive when you subscribe to a NATS subject. You should return the *entire* NATS message. You may optionally set the `responseStatusCode` and the `errorMessage` properties if an error occurs while you are processing the message. The nats-http-proxy will honor the `responseStatusCode` if it is set and will also format and return an error response if the `errorMessage` property has been set. \ No newline at end of file +In order to control what gets returned from the http-nats-proxy, your microservice has to set the `response` property of +the NATS message that you receive when you subscribe to a NATS subject. You should return the *entire* NATS message. You +may optionally set the `responseStatusCode` and the `errorMessage` properties if an error occurs while you are +processing the message. The nats-http-proxy will honor the `responseStatusCode` if it is set and will also format and +return an error response if the `errorMessage` property has been set. \ No newline at end of file diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 8b35270..70b22e3 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -1,20 +1,20 @@ - - true - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Dockerfile b/src/Dockerfile index 8276b3d..1bf9c21 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -11,7 +11,6 @@ WORKDIR /app COPY ./Proxy/Proxy.csproj ./Proxy/Proxy.csproj COPY ./Proxy.Shared/Proxy.Shared.csproj ./Proxy.Shared/Proxy.Shared.csproj RUN dotnet restore ./Proxy/Proxy.csproj -# RUN dotnet restore ./Proxy.Shared/Proxy.Shared.csproj # copy the rest of the code COPY . ./ diff --git a/src/ExampleHandlers/ExampleHandlers.csproj b/src/ExampleHandlers/ExampleHandlers.csproj index 931d98b..225ea40 100644 --- a/src/ExampleHandlers/ExampleHandlers.csproj +++ b/src/ExampleHandlers/ExampleHandlers.csproj @@ -1,22 +1,22 @@ - - Exe - net9.0 - enable - enable - + + Exe + net9.0 + enable + enable + - - - - - - - + + + + + + + - - - + + + diff --git a/src/ExampleHandlers/GetCustomer.cs b/src/ExampleHandlers/GetCustomer.cs index 8efef80..a259499 100644 --- a/src/ExampleHandlers/GetCustomer.cs +++ b/src/ExampleHandlers/GetCustomer.cs @@ -1,42 +1,38 @@ using Proxy.Shared; -namespace ExampleHandlers +namespace ExampleHandlers; + +public class GetCustomer : IMessageHandler { - public class GetCustomer : IMessageHandler + public async Task HandleAsync(MicroserviceMessage? msg) { - public async Task HandleAsync(MicroserviceMessage? msg) - { - if (msg is null) - { - throw new Exception("msg is null"); - } - - // grab the customer id that is being fetched - if (msg.TryGetParam("id", out var customerId)) - { - // simulate code to go query the database for the customer - await Task.Delay(1000); + if (msg is null) throw new Exception("msg is null"); - // create an anonymous type to represent our response - var reply = new - { - id = customerId, - firstName = "John", - lastName = "Smith" - }; + // grab the customer id that is being fetched + if (msg.TryGetParam("id", out var customerId)) + { + // simulate code to go query the database for the customer + await Task.Delay(1000); - // set the response on the message - msg.SetResponse(reply); - } - else + // create an anonymous type to represent our response + var reply = new { - // the id parameter was missing - msg.ErrorMessage = "You must specify the id parameter"; - msg.ResponseStatusCode = 400; - } + id = customerId, + firstName = "John", + lastName = "Smith" + }; - // return the updated message - return msg; + // set the response on the message + msg.SetResponse(reply); + } + else + { + // the id parameter was missing + msg.ErrorMessage = "You must specify the id parameter"; + msg.ResponseStatusCode = 400; } + + // return the updated message + return msg; } } \ No newline at end of file diff --git a/src/ExampleHandlers/Healthcheck.cs b/src/ExampleHandlers/Healthcheck.cs index 3504aa2..2021da0 100644 --- a/src/ExampleHandlers/Healthcheck.cs +++ b/src/ExampleHandlers/Healthcheck.cs @@ -1,30 +1,26 @@ using Proxy.Shared; -namespace ExampleHandlers +namespace ExampleHandlers; + +public class Healthcheck : IMessageHandler { - public class Healthcheck : IMessageHandler + async Task IMessageHandler.HandleAsync(MicroserviceMessage? msg) { - async Task IMessageHandler.HandleAsync(MicroserviceMessage? msg) - { - if (msg is null) - { - throw new Exception("msg is null"); - } + if (msg is null) throw new Exception("msg is null"); - // simulate code to go check connections to infrastructure dependencies like a database, redis cache, 3rd party api, etc - await Task.Delay(1000); + // simulate code to go check connections to infrastructure dependencies like a database, redis cache, 3rd party api, etc + await Task.Delay(1000); - // create an anonymous type to represent our response - var reply = new - { - status = "ok" - }; + // create an anonymous type to represent our response + var reply = new + { + status = "ok" + }; - // set the response on the message - msg.SetResponse(reply); + // set the response on the message + msg.SetResponse(reply); - // return the updated message - return msg; - } + // return the updated message + return msg; } } \ No newline at end of file diff --git a/src/ExampleHandlers/Logging.cs b/src/ExampleHandlers/Logging.cs index a0fd35e..3202a03 100644 --- a/src/ExampleHandlers/Logging.cs +++ b/src/ExampleHandlers/Logging.cs @@ -2,15 +2,14 @@ using Proxy.Shared; using Serilog; -namespace ExampleHandlers +namespace ExampleHandlers; + +public class Logging : IMessageObserver { - public class Logging : IMessageObserver + public Task ObserveAsync(MicroserviceMessage? msg) { - public Task ObserveAsync(MicroserviceMessage? msg) - { - Log.Information(JsonConvert.SerializeObject(msg)); + Log.Information("{Message}", JsonConvert.SerializeObject(msg)); - return Task.FromResult(true); - } + return Task.FromResult(true); } } \ No newline at end of file diff --git a/src/ExampleHandlers/Metrics.cs b/src/ExampleHandlers/Metrics.cs index 3e3ddab..16ba561 100644 --- a/src/ExampleHandlers/Metrics.cs +++ b/src/ExampleHandlers/Metrics.cs @@ -1,25 +1,19 @@ using Proxy.Shared; using Serilog; -namespace ExampleHandlers +namespace ExampleHandlers; + +public class Metrics : IMessageObserver { - public class Metrics : IMessageObserver + public Task ObserveAsync(MicroserviceMessage? msg) { - public Task ObserveAsync(MicroserviceMessage? msg) - { - if (msg is null) - { - throw new Exception("msg is null"); - } + if (msg is null) throw new Exception("msg is null"); - // grab all the metrics from the message and "record" them - foreach (var callTiming in msg.CallTimings) - { - Log.Information("Subject: {Subject} - Execution Time (ms): {EllapsedMs}", - callTiming.Subject, callTiming.EllapsedMs); - } + // grab all the metrics from the message and "record" them + foreach (var callTiming in msg.CallTimings) + Log.Information("Subject: {Subject} - Execution Time (ms): {EllapsedMs}", + callTiming.Subject, callTiming.EllapsedMs); - return Task.FromResult(true); - } + return Task.FromResult(true); } } \ No newline at end of file diff --git a/src/ExampleHandlers/Program.cs b/src/ExampleHandlers/Program.cs index c94bc49..42028dc 100644 --- a/src/ExampleHandlers/Program.cs +++ b/src/ExampleHandlers/Program.cs @@ -2,104 +2,100 @@ using Proxy.Shared; using Serilog; -namespace ExampleHandlers +namespace ExampleHandlers; + +internal class Program { - internal class Program + private static readonly ManualResetEvent _mre = new(false); + private static IServiceProvider? _container; + + private static void ConfigureNatsSubscriptions() { - private static readonly ManualResetEvent _mre = new ManualResetEvent(false); - private static IServiceProvider? _container; + Log.Information("Configuring NATS Subscriptions"); - private static void ConfigureNatsSubscriptions() + // create the nats subscription handler + if (_container is null) throw new Exception("container is null"); + var subscriptionHandler = new NatsSubscriptionHandler(_container); + const string queueGroup = "Example.Queue.Group"; + + NatsHelper.Configure(cfg => { - Log.Information("Configuring NATS Subscriptions"); + cfg.ClientName = "Example Message Handlers"; + cfg.NatsServerUrls = ["nats://localhost:4222"]; + cfg.PingInterval = 2000; + cfg.MaxPingsOut = 2; - // create the nats subscription handler - if (_container is null) - { - throw new Exception("container is null"); - } - var subscriptionHandler = new NatsSubscriptionHandler(_container); - const string queueGroup = "Example.Queue.Group"; + // healthcheck (handler of HTTP - GET /healthcheck) + cfg.NatsSubscriptions.Add(new NatsSubscription("get.healthcheck", queueGroup, subscriptionHandler.HandleMsgWith)); - NatsHelper.Configure(cfg => - { - cfg.ClientName = "Example Message Handlers"; - cfg.NatsServerUrls = ["nats://localhost:4222"]; - cfg.PingInterval = 2000; - cfg.MaxPingsOut = 2; + // tracing (handler as a pipeline step) + cfg.NatsSubscriptions.Add(new NatsSubscription("add.tracing", queueGroup, subscriptionHandler.HandleMsgWith)); - // healthcheck (handler of HTTP - GET /healthcheck) - cfg.NatsSubscriptions.Add(new NatsSubscription("get.healthcheck", queueGroup, subscriptionHandler.HandleMsgWith)); + // metrics (observer as a pipeline step) + cfg.NatsSubscriptions.Add(new NatsSubscription("record.metrics", queueGroup, subscriptionHandler.ObserveMsgWith)); - // tracing (handler as a pipeline step) - cfg.NatsSubscriptions.Add(new NatsSubscription("add.tracing", queueGroup, subscriptionHandler.HandleMsgWith)); + // logging (observer as a pipeline step) + cfg.NatsSubscriptions.Add(new NatsSubscription("logging", queueGroup, subscriptionHandler.ObserveMsgWith)); - // metrics (observer as a pipeline step) - cfg.NatsSubscriptions.Add(new NatsSubscription("record.metrics", queueGroup, subscriptionHandler.ObserveMsgWith)); + // customers (handler of HTTP - Get /customers?id=41) + cfg.NatsSubscriptions.Add(new NatsSubscription("get.customers", queueGroup, subscriptionHandler.HandleMsgWith)); + }); - // logging (observer as a pipeline step) - cfg.NatsSubscriptions.Add(new NatsSubscription("logging", queueGroup, subscriptionHandler.ObserveMsgWith)); + Log.Information("NATS Subscriptions Configured"); + } - // customers (handler of HTTP - Get /customers?id=41) - cfg.NatsSubscriptions.Add(new NatsSubscription("get.customers", queueGroup, subscriptionHandler.HandleMsgWith)); - }); + private static void Main(string[] args) + { + // setup our logger + SetupLogger(); - Log.Information("NATS Subscriptions Configured"); - } + // configure our ioc container + SetupDependencies(); - private static void Main(string[] args) - { - // setup our logger - SetupLogger(); + // set up an event handler that will run when our application process shuts down + SetupShutdownHandler(); - // configure our ioc container - SetupDependencies(); + // configure our NATS subscriptions that we are going to listen to + ConfigureNatsSubscriptions(); - // set up an event handler that will run when our application process shuts down - SetupShutdownHandler(); + // run until we're shutdown + _mre.WaitOne(); + } - // configure our NATS subscriptions that we are going to listen to - ConfigureNatsSubscriptions(); + private static void SetupDependencies() + { + // create a new service collection + var serviceCollection = new ServiceCollection(); + + // scan our assembly for all classes that implement IUseCase and register them with a scoped lifetime + serviceCollection.Scan(scan => scan + .FromAssembliesOf(typeof(Healthcheck)) + .AddClasses() + .AsSelf() + //.AsImplementedInterfaces() + .WithScopedLifetime()); + + // build the IServiceProvider + _container = serviceCollection.BuildServiceProvider(); + } - // run until we're shutdown - _mre.WaitOne(); - } + private static void SetupLogger() + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .WriteTo.Console() + .CreateLogger(); + } - private static void SetupDependencies() - { - // create a new service collection - var serviceCollection = new ServiceCollection(); - - // scan our assembly for all classes that implement IUseCase and register them with a scoped lifetime - serviceCollection.Scan(scan => scan - .FromAssembliesOf(typeof(Healthcheck)) - .AddClasses() - .AsSelf() - //.AsImplementedInterfaces() - .WithScopedLifetime()); - - // build the IServiceProvider - _container = serviceCollection.BuildServiceProvider(); - } - - private static void SetupLogger() + private static void SetupShutdownHandler() + { + Console.CancelKeyPress += (_, _) => { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Information() - .WriteTo.Console() - .CreateLogger(); - } + // log that we are shutting down + Log.Information("Example Handlers are shutting down"); - private static void SetupShutdownHandler() - { - Console.CancelKeyPress += (_, _) => - { - // log that we are shutting down - Log.Information("Example Handlers are shutting down"); - - // shutdown the logger - Log.CloseAndFlush(); - }; - } + // shutdown the logger + Log.CloseAndFlush(); + }; } } \ No newline at end of file diff --git a/src/ExampleHandlers/Tracing.cs b/src/ExampleHandlers/Tracing.cs index 3f6c395..8fcdea4 100644 --- a/src/ExampleHandlers/Tracing.cs +++ b/src/ExampleHandlers/Tracing.cs @@ -1,35 +1,31 @@ using Proxy.Shared; using Serilog; -namespace ExampleHandlers +namespace ExampleHandlers; + +public class Tracing : IMessageHandler { - public class Tracing : IMessageHandler + public Task HandleAsync(MicroserviceMessage? msg) { - public Task HandleAsync(MicroserviceMessage? msg) - { - if (msg is null) - { - throw new Exception("msg is null"); - } + if (msg is null) throw new Exception("msg is null"); - const string headerName = "x-trace-id"; + const string headerName = "x-trace-id"; - // see if a trace header is already on the msg - if (msg.TryGetParam(headerName, out var traceHeader)) - { - Log.Information("Message already had trace id of {TraceId} present", traceHeader); - } - else - { - // add a trace header to the msg - var traceId = Guid.NewGuid().ToString("N"); - msg.RequestHeaders.Add(headerName, traceId); - msg.ResponseHeaders.Add(headerName, traceId); - - Log.Information("Added trace id of {TraceId} to the message", traceId); - } + // see if a trace header is already on the msg + if (msg.TryGetParam(headerName, out var traceHeader)) + { + Log.Information("Message already had trace id of {TraceId} present", traceHeader); + } + else + { + // add a trace header to the msg + var traceId = Guid.NewGuid().ToString("N"); + msg.RequestHeaders.Add(headerName, traceId); + msg.ResponseHeaders.Add(headerName, traceId); - return Task.FromResult(msg); + Log.Information("Added trace id of {TraceId} to the message", traceId); } + + return Task.FromResult(msg); } } \ No newline at end of file diff --git a/src/Proxy.Shared/CallTiming.cs b/src/Proxy.Shared/CallTiming.cs index f559d77..e9c53ec 100644 --- a/src/Proxy.Shared/CallTiming.cs +++ b/src/Proxy.Shared/CallTiming.cs @@ -1,9 +1,8 @@ -namespace Proxy.Shared +namespace Proxy.Shared; + +public class CallTiming(string? subject, long ellapsedMs) { - public class CallTiming(string? subject, long ellapsedMs) - { - public long EllapsedMs { get; set; } = ellapsedMs; + public long EllapsedMs { get; set; } = ellapsedMs; - public string? Subject { get; set; } = subject; - } + public string? Subject { get; set; } = subject; } \ No newline at end of file diff --git a/src/Proxy.Shared/Guard.cs b/src/Proxy.Shared/Guard.cs index 732a929..5f69d89 100644 --- a/src/Proxy.Shared/Guard.cs +++ b/src/Proxy.Shared/Guard.cs @@ -6,10 +6,7 @@ public static class Guard { public static T AgainstNull(T value, [CallerArgumentExpression("value")] string? name = null) where T : class? { - if (null == value) - { - throw new ArgumentException($"{name} cannot be null"); - } + if (null == value) throw new ArgumentException($"{name} cannot be null"); return value; } diff --git a/src/Proxy.Shared/IMessageHandler.cs b/src/Proxy.Shared/IMessageHandler.cs index 0c54d18..77bd82e 100644 --- a/src/Proxy.Shared/IMessageHandler.cs +++ b/src/Proxy.Shared/IMessageHandler.cs @@ -1,7 +1,6 @@ -namespace Proxy.Shared +namespace Proxy.Shared; + +public interface IMessageHandler { - public interface IMessageHandler - { - Task HandleAsync(MicroserviceMessage? msg); - } + Task HandleAsync(MicroserviceMessage? msg); } \ No newline at end of file diff --git a/src/Proxy.Shared/IMessageObserver.cs b/src/Proxy.Shared/IMessageObserver.cs index 2d92195..9b180bb 100644 --- a/src/Proxy.Shared/IMessageObserver.cs +++ b/src/Proxy.Shared/IMessageObserver.cs @@ -1,7 +1,6 @@ -namespace Proxy.Shared +namespace Proxy.Shared; + +public interface IMessageObserver { - public interface IMessageObserver - { - Task ObserveAsync(MicroserviceMessage? msg); - } + Task ObserveAsync(MicroserviceMessage? msg); } \ No newline at end of file diff --git a/src/Proxy.Shared/MicroserviceMessage.cs b/src/Proxy.Shared/MicroserviceMessage.cs index deb1932..804c1f3 100644 --- a/src/Proxy.Shared/MicroserviceMessage.cs +++ b/src/Proxy.Shared/MicroserviceMessage.cs @@ -1,73 +1,72 @@ using Newtonsoft.Json; -namespace Proxy.Shared +namespace Proxy.Shared; + +public sealed class MicroserviceMessage(string host, string contentType) { - public sealed class MicroserviceMessage(string host, string contentType) + public List CallTimings { get; set; } = new(); + public long CompletedOnUtc { get; set; } + public Dictionary Cookies { get; set; } = new(); + public string? ErrorMessage { get; set; } + public long ExecutionTimeMs => CompletedOnUtc - StartedOnUtc; + public Dictionary ExtendedProperties { get; set; } = new(); + public string Host { get; set; } = host; + public Dictionary QueryParams { get; set; } = new(); + public string? RequestBody { get; set; } + public Dictionary RequestHeaders { get; set; } = new(); + public string? ResponseBody { get; set; } + public string ResponseContentType { get; set; } = contentType; + public Dictionary ResponseHeaders { get; set; } = new(); + public int ResponseStatusCode { get; set; } = -1; + public bool ShouldTerminateRequest { get; set; } + public long StartedOnUtc { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + public string? Subject { get; set; } + + // capture the time in epoch utc that this message was started + // default the response status code to an invalid value for comparison later on when the response is being processed by the RequestHandler + // capture the host machine that we're executing on + // capture the content type for the http response that we're configured for + // initialize the default properties + + public void MarkComplete() { - public List CallTimings { get; set; } = new(); - public long CompletedOnUtc { get; set; } - public Dictionary Cookies { get; set; } = new(); - public string? ErrorMessage { get; set; } - public long ExecutionTimeMs => CompletedOnUtc - StartedOnUtc; - public Dictionary ExtendedProperties { get; set; } = new(); - public string Host { get; set; } = host; - public Dictionary QueryParams { get; set; } = new(); - public string? RequestBody { get; set; } - public Dictionary RequestHeaders { get; set; } = new(); - public string? ResponseBody { get; set; } - public string ResponseContentType { get; set; } = contentType; - public Dictionary ResponseHeaders { get; set; } = new(); - public int ResponseStatusCode { get; set; } = -1; - public bool ShouldTerminateRequest { get; set; } - public long StartedOnUtc { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - public string? Subject { get; set; } + CompletedOnUtc = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + } - // capture the time in epoch utc that this message was started - // default the response status code to an invalid value for comparison later on when the response is being processed by the RequestHandler - // capture the host machine that we're executing on - // capture the content type for the http response that we're configured for - // initialize the default properties + public void SetResponse(object response) + { + ResponseBody = JsonConvert.SerializeObject(response); + } - public void MarkComplete() + public bool TryGetParam(string key, out T? value) + { + // try to find a parameter with matching name across the cookies, query parameters, request headers and extended properties + if (QueryParams.TryGetValue(key, out var queryParamValue)) { - CompletedOnUtc = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + value = (T)queryParamValue; + return true; } - public void SetResponse(object response) + if (RequestHeaders.TryGetValue(key, out var headerValue)) { - ResponseBody = JsonConvert.SerializeObject(response); + value = (T)headerValue; + return true; } - public bool TryGetParam(string key, out T? value) + if (Cookies.TryGetValue(key, out var cookieValue)) { - // try to find a parameter with matching name across the cookies, query parameters, request headers and extended properties - if (QueryParams.TryGetValue(key, out var queryParamValue)) - { - value = (T)queryParamValue; - return true; - } - - if (RequestHeaders.TryGetValue(key, out var headerValue)) - { - value = (T)headerValue; - return true; - } - - if (Cookies.TryGetValue(key, out var cookieValue)) - { - value = (T)cookieValue; - return true; - } - - if (ExtendedProperties.TryGetValue(key, out var extendedValue)) - { - value = (T)extendedValue; - return true; - } + value = (T)cookieValue; + return true; + } - // the parameter doesn't exist so return null/false - value = default(T); - return false; + if (ExtendedProperties.TryGetValue(key, out var extendedValue)) + { + value = (T)extendedValue; + return true; } + + // the parameter doesn't exist so return null/false + value = default; + return false; } } \ No newline at end of file diff --git a/src/Proxy.Shared/MicroserviceMessageExtensions.cs b/src/Proxy.Shared/MicroserviceMessageExtensions.cs index 19d0061..2e75a5b 100644 --- a/src/Proxy.Shared/MicroserviceMessageExtensions.cs +++ b/src/Proxy.Shared/MicroserviceMessageExtensions.cs @@ -1,19 +1,18 @@ -using Newtonsoft.Json; -using System.Text; +using System.Text; +using Newtonsoft.Json; -namespace Proxy.Shared +namespace Proxy.Shared; + +public static class MicroserviceMessageExtensions { - public static class MicroserviceMessageExtensions + public static byte[] ToBytes(this MicroserviceMessage msg, JsonSerializerSettings serializerSettings) { - public static byte[] ToBytes(this MicroserviceMessage msg, JsonSerializerSettings serializerSettings) - { - var serializedMessage = JsonConvert.SerializeObject(msg, serializerSettings); - return Encoding.UTF8.GetBytes(serializedMessage); - } + var serializedMessage = JsonConvert.SerializeObject(msg, serializerSettings); + return Encoding.UTF8.GetBytes(serializedMessage); + } - public static MicroserviceMessage? ToMicroserviceMessage(this byte[] data) - { - return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); - } + public static MicroserviceMessage? ToMicroserviceMessage(this byte[] data) + { + return JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data)); } } \ No newline at end of file diff --git a/src/Proxy.Shared/NatsConfiguration.cs b/src/Proxy.Shared/NatsConfiguration.cs index 6b768bc..2263064 100644 --- a/src/Proxy.Shared/NatsConfiguration.cs +++ b/src/Proxy.Shared/NatsConfiguration.cs @@ -1,32 +1,22 @@ -namespace Proxy.Shared +namespace Proxy.Shared; + +public class NatsConfiguration { - public class NatsConfiguration - { - public string ClientName { get; set; } = "N/A"; - public int MaxPingsOut { get; set; } = 2; - public required string[] NatsServerUrls { get; set; } = ["nats://localhost:4222"]; - public List NatsSubscriptions { get; set; } = []; - public int PingInterval { get; set; } = 2000; + public string ClientName { get; set; } = "N/A"; + public int MaxPingsOut { get; set; } = 2; + public required string[] NatsServerUrls { get; set; } = ["nats://localhost:4222"]; + public List NatsSubscriptions { get; set; } = []; + public int PingInterval { get; set; } = 2000; - internal void Validate() - { - // if the nats server url isn't specified throw an exception - if (null == NatsServerUrls || NatsServerUrls.Length == 0) - { - throw new ArgumentNullException(nameof(NatsServerUrls)); - } + internal void Validate() + { + // if the nats server url isn't specified throw an exception + if (null == NatsServerUrls || NatsServerUrls.Length == 0) throw new ArgumentNullException(nameof(NatsServerUrls)); - // if the client name isn't specified throw an exception - if (string.IsNullOrWhiteSpace(ClientName)) - { - throw new ArgumentNullException(nameof(ClientName)); - } + // if the client name isn't specified throw an exception + if (string.IsNullOrWhiteSpace(ClientName)) throw new ArgumentNullException(nameof(ClientName)); - // if there are no subscriptions specified throw an exception - if (NatsSubscriptions.Count == 0) - { - throw new ArgumentException("You must have at least one subscription specified."); - } - } + // if there are no subscriptions specified throw an exception + if (NatsSubscriptions.Count == 0) throw new ArgumentException("You must have at least one subscription specified."); } } \ No newline at end of file diff --git a/src/Proxy.Shared/NatsHelper.cs b/src/Proxy.Shared/NatsHelper.cs index 31624fc..e47a587 100644 --- a/src/Proxy.Shared/NatsHelper.cs +++ b/src/Proxy.Shared/NatsHelper.cs @@ -1,143 +1,109 @@ -using NATS.Client; +using System.Text; +using NATS.Client; using Newtonsoft.Json; using Serilog; -using System.Text; -namespace Proxy.Shared +namespace Proxy.Shared; + +public static class NatsHelper { - public static class NatsHelper - { - private static IConnection? _connection; + private static IConnection? _connection; - public static void Configure(Action configAction) - { - // create a new instance of a configuration - var config = new NatsConfiguration - { - NatsServerUrls = [] - }; - - // allow the client to set up the subscriptions, connection name and nats server url - configAction(config); - - // validate the configuration in case there are any problems - config.Validate(); - - // ensure we are connected to the nats server - Connect(config.ClientName, config.NatsServerUrls, config.PingInterval, config.MaxPingsOut); - - // start the subscriptions - if (_connection is null) - { - throw new InvalidOperationException("The NATS connection is null"); - } - - config.NatsSubscriptions.ForEach(s => _connection.SubscribeAsync(s.Subject, s.QueueGroup, s.Handler).Start()); - } - - public static void Publish(MicroserviceMessage? message) + public static void Configure(Action configAction) + { + // create a new instance of a configuration + var config = new NatsConfiguration { - ArgumentNullException.ThrowIfNull(message); + NatsServerUrls = [] + }; - Publish(message.Subject, message); - } + // allow the client to set up the subscriptions, connection name and nats server url + configAction(config); - public static void Publish(string? subject, MicroserviceMessage? message) - { - // validate params - if (string.IsNullOrWhiteSpace(subject)) - { - throw new ArgumentNullException(nameof(subject)); - } + // validate the configuration in case there are any problems + config.Validate(); + + // ensure we are connected to the nats server + Connect(config.ClientName, config.NatsServerUrls, config.PingInterval, config.MaxPingsOut); - ArgumentNullException.ThrowIfNull(message); + // start the subscriptions + if (_connection is null) throw new InvalidOperationException("The NATS connection is null"); - // serialize the message - var serializedMessage = JsonConvert.SerializeObject(message); + config.NatsSubscriptions.ForEach(s => _connection.SubscribeAsync(s.Subject, s.QueueGroup, s.Handler).Start()); + } - // send the message - if (_connection is null) - { - throw new InvalidOperationException("The NATS connection is null"); - } + public static void Publish(MicroserviceMessage? message) + { + ArgumentNullException.ThrowIfNull(message); - _connection.Publish(subject, Encoding.UTF8.GetBytes(serializedMessage)); - } + Publish(message.Subject, message); + } - public static Task RequestAsync(MicroserviceMessage message) - { - return RequestAsync(message.Subject, message); - } + public static void Publish(string? subject, MicroserviceMessage? message) + { + // validate params + if (string.IsNullOrWhiteSpace(subject)) throw new ArgumentNullException(nameof(subject)); - private static async Task RequestAsync(string? subject, MicroserviceMessage message) - { - // validate params - if (string.IsNullOrWhiteSpace(subject)) - { - throw new ArgumentNullException(nameof(subject)); - } + ArgumentNullException.ThrowIfNull(message); - ArgumentNullException.ThrowIfNull(message); + // serialize the message + var serializedMessage = JsonConvert.SerializeObject(message); - // serialize the message - var serializedMessage = JsonConvert.SerializeObject(message); + // send the message + if (_connection is null) throw new InvalidOperationException("The NATS connection is null"); - // send the message - if (_connection is null) - { - throw new InvalidOperationException("The NATS connection is null"); - } + _connection.Publish(subject, Encoding.UTF8.GetBytes(serializedMessage)); + } + + public static Task RequestAsync(MicroserviceMessage message) + { + return RequestAsync(message.Subject, message); + } - var response = await _connection.RequestAsync(subject, Encoding.UTF8.GetBytes(serializedMessage)).ConfigureAwait(false); + private static async Task RequestAsync(string? subject, MicroserviceMessage message) + { + // validate params + if (string.IsNullOrWhiteSpace(subject)) throw new ArgumentNullException(nameof(subject)); - // return the response as a Microservice message - return response.Data.ToMicroserviceMessage(); - } + ArgumentNullException.ThrowIfNull(message); - private static void Connect(string clientName, string[] natsServerUrls, int pingInterval, int maxPingsOut) - { - // the params are validated in the Configure method so we don't need to revalidate here - - // if we're already connected to the nats server then do nothing - if (_connection is { State: ConnState.CONNECTED }) - { - return; - } - - // configure the options for this connection - var connectionFactory = new ConnectionFactory(); - var options = ConnectionFactory.GetDefaultOptions(); - options.Name = clientName; - options.AllowReconnect = true; - options.Servers = natsServerUrls; - options.PingInterval = pingInterval; - options.MaxPingsOut = maxPingsOut; - - options.AsyncErrorEventHandler += (_, _) => - { - Log.Information("The AsyncErrorEvent was just handled"); - }; - options.ClosedEventHandler += (_, _) => - { - Log.Information("The ClosedEvent was just handled"); - }; - options.DisconnectedEventHandler += (_, _) => - { - Log.Information("The DisconnectedEvent was just handled"); - }; - options.ReconnectedEventHandler += (_, _) => - { - Log.Information("The ReconnectedEvent was just handled"); - }; - options.ServerDiscoveredEventHandler += (_, _) => - { - Log.Information("The ServerDiscoveredEvent was just handled"); - }; - - // create a connection to the NATS server - _connection = connectionFactory.CreateConnection(options); - - Log.Information("Connected to NATS server"); - } + // serialize the message + var serializedMessage = JsonConvert.SerializeObject(message); + + // send the message + if (_connection is null) throw new InvalidOperationException("The NATS connection is null"); + + var response = await _connection.RequestAsync(subject, Encoding.UTF8.GetBytes(serializedMessage)).ConfigureAwait(false); + + // return the response as a Microservice message + return response.Data.ToMicroserviceMessage(); + } + + private static void Connect(string clientName, string[] natsServerUrls, int pingInterval, int maxPingsOut) + { + // the params are validated in the Configure method so we don't need to revalidate here + + // if we're already connected to the nats server then do nothing + if (_connection is { State: ConnState.CONNECTED }) return; + + // configure the options for this connection + var connectionFactory = new ConnectionFactory(); + var options = ConnectionFactory.GetDefaultOptions(); + options.Name = clientName; + options.AllowReconnect = true; + options.Servers = natsServerUrls; + options.PingInterval = pingInterval; + options.MaxPingsOut = maxPingsOut; + + options.AsyncErrorEventHandler += (_, _) => { Log.Information("The AsyncErrorEvent was just handled"); }; + options.ClosedEventHandler += (_, _) => { Log.Information("The ClosedEvent was just handled"); }; + options.DisconnectedEventHandler += (_, _) => { Log.Information("The DisconnectedEvent was just handled"); }; + options.ReconnectedEventHandler += (_, _) => { Log.Information("The ReconnectedEvent was just handled"); }; + options.ServerDiscoveredEventHandler += (_, _) => { Log.Information("The ServerDiscoveredEvent was just handled"); }; + + // create a connection to the NATS server + _connection = connectionFactory.CreateConnection(options); + + Log.Information("Connected to NATS server"); } } \ No newline at end of file diff --git a/src/Proxy.Shared/NatsSubscription.cs b/src/Proxy.Shared/NatsSubscription.cs index ceb1bdb..b6eef6b 100644 --- a/src/Proxy.Shared/NatsSubscription.cs +++ b/src/Proxy.Shared/NatsSubscription.cs @@ -1,18 +1,17 @@ using NATS.Client; -namespace Proxy.Shared +namespace Proxy.Shared; + +public class NatsSubscription { - public class NatsSubscription + public NatsSubscription(string subject, string queueGroup, EventHandler handler) { - public EventHandler Handler { get; private set; } - public string QueueGroup { get; private set; } - public string Subject { get; private set; } - - public NatsSubscription(string subject, string queueGroup, EventHandler handler) - { - Subject = subject ?? throw new ArgumentNullException(nameof(subject)); - QueueGroup = queueGroup ?? throw new ArgumentNullException(nameof(queueGroup)); - Handler = handler ?? throw new ArgumentNullException(nameof(handler)); - } + Subject = subject ?? throw new ArgumentNullException(nameof(subject)); + QueueGroup = queueGroup ?? throw new ArgumentNullException(nameof(queueGroup)); + Handler = handler ?? throw new ArgumentNullException(nameof(handler)); } + + public EventHandler Handler { get; private set; } + public string QueueGroup { get; private set; } + public string Subject { get; private set; } } \ No newline at end of file diff --git a/src/Proxy.Shared/NatsSubscriptionHandler.cs b/src/Proxy.Shared/NatsSubscriptionHandler.cs index 40b508b..937c2cf 100644 --- a/src/Proxy.Shared/NatsSubscriptionHandler.cs +++ b/src/Proxy.Shared/NatsSubscriptionHandler.cs @@ -2,95 +2,87 @@ using NATS.Client; using Serilog; -namespace Proxy.Shared +namespace Proxy.Shared; + +/// +/// This is a NATS subscription handler that abstracts all the NATS related logic away from the use case classes. The +/// goal of this class is to +/// provide the NATS specific code required so that the use case classes can be tested without having to deal with NATS +/// related concerns. +/// +public class NatsSubscriptionHandler { - /// - /// This is a NATS subscription handler that abstracts all the NATS related logic away from the use case classes. The goal of this class is to - /// provide the NATS specific code required so that the use case classes can be tested without having to deal with NATS related concerns. - /// - public class NatsSubscriptionHandler - { - private readonly IServiceScopeFactory _serviceScopeFactory; - - public NatsSubscriptionHandler(IServiceProvider serviceProvider) - { - if (null == serviceProvider) throw new ArgumentNullException(nameof(serviceProvider)); + private readonly IServiceScopeFactory _serviceScopeFactory; - _serviceScopeFactory = serviceProvider.GetRequiredService(); - } + public NatsSubscriptionHandler(IServiceProvider serviceProvider) + { + if (null == serviceProvider) throw new ArgumentNullException(nameof(serviceProvider)); - public async void HandleMsgWith(object? sender, MsgHandlerEventArgs e) where T : IMessageHandler - { - // store the replyTo subject - var replyTo = e.Message.Reply; + _serviceScopeFactory = serviceProvider.GetRequiredService(); + } - // deserialize the microservice msg that was embedded in the nats msg - var microserviceMsg = e.Message.Data.ToMicroserviceMessage(); - MicroserviceMessage? responseMsg = null; + public async void HandleMsgWith(object? sender, MsgHandlerEventArgs e) where T : IMessageHandler + { + // store the replyTo subject + var replyTo = e.Message.Reply; - try - { - // create a new scope to handle the message in. this will create a new instance of the handler class per message - using var scope = _serviceScopeFactory.CreateScope(); + // deserialize the microservice msg that was embedded in the nats msg + var microserviceMsg = e.Message.Data.ToMicroserviceMessage(); + MicroserviceMessage? responseMsg = null; - // create a new instance of the message handler - var msgHandler = scope.ServiceProvider.GetService(); + try + { + // create a new scope to handle the message in. this will create a new instance of the handler class per message + using var scope = _serviceScopeFactory.CreateScope(); - if (msgHandler is null) - { - throw new InvalidOperationException($"Unable to resolve {typeof(T).Name} from the service provider."); - } + // create a new instance of the message handler + var msgHandler = scope.ServiceProvider.GetService(); - // handle the message - responseMsg = await msgHandler.HandleAsync(microserviceMsg).ConfigureAwait(false); - } - catch (Exception ex) - { - var msg = ex.GetBaseException().Message; + if (msgHandler is null) throw new InvalidOperationException($"Unable to resolve {typeof(T).Name} from the service provider."); - if (microserviceMsg is not null) - { - microserviceMsg.ErrorMessage = msg; - microserviceMsg.ResponseStatusCode = 500; - } + // handle the message + responseMsg = await msgHandler.HandleAsync(microserviceMsg).ConfigureAwait(false); + } + catch (Exception ex) + { + var msg = ex.GetBaseException().Message; - Log.Error(ex, "Error: {Error}", msg); - } - finally + if (microserviceMsg is not null) { - // send the NATS message (with the response) back to the calling client - if (null != responseMsg && !string.IsNullOrWhiteSpace(replyTo)) - { - NatsHelper.Publish(replyTo, responseMsg); - } + microserviceMsg.ErrorMessage = msg; + microserviceMsg.ResponseStatusCode = 500; } - } - public async void ObserveMsgWith(object? sender, MsgHandlerEventArgs e) where T : IMessageObserver + Log.Error(ex, "Error: {Error}", msg); + } + finally { - // deserialize the microservice msg that was embedded in the nats msg - var microserviceMsg = e.Message.Data.ToMicroserviceMessage(); + // send the NATS message (with the response) back to the calling client + if (null != responseMsg && !string.IsNullOrWhiteSpace(replyTo)) NatsHelper.Publish(replyTo, responseMsg); + } + } - try - { - // create a new scope to handle the message in. this will create a new instance of the observer class per message - using var scope = _serviceScopeFactory.CreateScope(); + public async void ObserveMsgWith(object? sender, MsgHandlerEventArgs e) where T : IMessageObserver + { + // deserialize the microservice msg that was embedded in the nats msg + var microserviceMsg = e.Message.Data.ToMicroserviceMessage(); - // create a new instance of the message handler - var msgObserver = scope.ServiceProvider.GetService(); + try + { + // create a new scope to handle the message in. this will create a new instance of the observer class per message + using var scope = _serviceScopeFactory.CreateScope(); - // observe the message - if (msgObserver is null) - { - throw new InvalidOperationException($"Unable to resolve {typeof(T).Name} from the service provider"); - } + // create a new instance of the message handler + var msgObserver = scope.ServiceProvider.GetService(); - await msgObserver.ObserveAsync(microserviceMsg).ConfigureAwait(false); - } - catch (Exception ex) - { - Log.Error(ex, "Error: {Error}", ex.Message); - } + // observe the message + if (msgObserver is null) throw new InvalidOperationException($"Unable to resolve {typeof(T).Name} from the service provider"); + + await msgObserver.ObserveAsync(microserviceMsg).ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Error(ex, "Error: {Error}", ex.Message); } } } \ No newline at end of file diff --git a/src/Proxy.Shared/Proxy.Shared.csproj b/src/Proxy.Shared/Proxy.Shared.csproj index b3dfda6..abb09a5 100644 --- a/src/Proxy.Shared/Proxy.Shared.csproj +++ b/src/Proxy.Shared/Proxy.Shared.csproj @@ -1,23 +1,23 @@  - - net9.0 - enable - enable - HttpNatsProxy.Shared - Wes Shaddix - - A shared library with types and helper classes for interacting with the http-nats-proxy - https://github.com/wshaddix/http-nats-proxy - 2.0.0 - + + net9.0 + enable + enable + HttpNatsProxy.Shared + Wes Shaddix + + A shared library with types and helper classes for interacting with the http-nats-proxy + https://github.com/wshaddix/http-nats-proxy + 2.0.0 + - - - - - - - + + + + + + + diff --git a/src/Proxy/HttpRequestParser.cs b/src/Proxy/HttpRequestParser.cs index d51b637..679e69e 100644 --- a/src/Proxy/HttpRequestParser.cs +++ b/src/Proxy/HttpRequestParser.cs @@ -1,50 +1,40 @@ using System.Text; -namespace Proxy +namespace Proxy; + +internal class HttpRequestParser { - internal class HttpRequestParser + public static async Task ParseBodyAsync(Stream requestBody) + { + // if there is a body with the request then read it + using var reader = new StreamReader(requestBody, Encoding.UTF8); + return await reader.ReadToEndAsync(); + } + + public static Dictionary ParseCookies(IRequestCookieCollection requestCookies) + { + var cookies = new Dictionary(); + + foreach (var cookie in requestCookies) cookies.Add(cookie.Key, string.Join(',', cookie.Value)); + + return cookies; + } + + public static Dictionary ParseHeaders(IHeaderDictionary requestHeaders) + { + var headers = new Dictionary(); + + foreach (var header in requestHeaders) headers.Add(header.Key, string.Join(',', header.Value.ToString())); + + return headers; + } + + public static Dictionary ParseQueryParams(IQueryCollection requestQuery) { - public static async Task ParseBodyAsync(Stream requestBody) - { - // if there is a body with the request then read it - using var reader = new StreamReader(requestBody, Encoding.UTF8); - return await reader.ReadToEndAsync(); - } - - public static Dictionary ParseCookies(IRequestCookieCollection requestCookies) - { - var cookies = new Dictionary(); - - foreach (var cookie in requestCookies) - { - cookies.Add(cookie.Key, string.Join(',', cookie.Value)); - } - - return cookies; - } - - public static Dictionary ParseHeaders(IHeaderDictionary requestHeaders) - { - var headers = new Dictionary(); - - foreach (var header in requestHeaders) - { - headers.Add(header.Key, string.Join(',', header.Value.ToString())); - } - - return headers; - } - - public static Dictionary ParseQueryParams(IQueryCollection requestQuery) - { - var queryParams = new Dictionary(); - - foreach (var param in requestQuery) - { - queryParams.Add(param.Key, string.Join(',', param.Value.ToString())); - } - - return queryParams; - } + var queryParams = new Dictionary(); + + foreach (var param in requestQuery) queryParams.Add(param.Key, string.Join(',', param.Value.ToString())); + + return queryParams; } } \ No newline at end of file diff --git a/src/Proxy/HttpResponseFactory.cs b/src/Proxy/HttpResponseFactory.cs index 39b2c5d..26b5e81 100644 --- a/src/Proxy/HttpResponseFactory.cs +++ b/src/Proxy/HttpResponseFactory.cs @@ -1,41 +1,34 @@ using Newtonsoft.Json; using Proxy.Shared; -namespace Proxy +namespace Proxy; + +internal class HttpResponseFactory { - internal class HttpResponseFactory + internal static async void PrepareResponseAsync(HttpResponse httpResponse, + MicroserviceMessage message, JsonSerializerSettings jsonSerializerSettings) { - internal static async void PrepareResponseAsync(HttpResponse httpResponse, - MicroserviceMessage message, JsonSerializerSettings jsonSerializerSettings) - { - // set the status code - httpResponse.StatusCode = message.ResponseStatusCode; + // set the status code + httpResponse.StatusCode = message.ResponseStatusCode; - // set the response type - httpResponse.ContentType = message.ResponseContentType; + // set the response type + httpResponse.ContentType = message.ResponseContentType; - // set any response headers - foreach (var header in message.ResponseHeaders) - { - httpResponse.GetTypedHeaders().Append(header.Key, header.Value); - } + // set any response headers + foreach (var header in message.ResponseHeaders) httpResponse.GetTypedHeaders().Append(header.Key, header.Value); - // if the message includes an error add it - if (!string.IsNullOrWhiteSpace(message.ErrorMessage)) + // if the message includes an error add it + if (!string.IsNullOrWhiteSpace(message.ErrorMessage)) + { + var response = new { - var response = new - { - message.ErrorMessage - }; + message.ErrorMessage + }; - await httpResponse.WriteAsync(JsonConvert.SerializeObject(response, jsonSerializerSettings)).ConfigureAwait(false); - } - - // if the message includes a response body add it - if (!string.IsNullOrWhiteSpace(message.ResponseBody)) - { - await httpResponse.WriteAsync(message.ResponseBody).ConfigureAwait(false); - } + await httpResponse.WriteAsync(JsonConvert.SerializeObject(response, jsonSerializerSettings)).ConfigureAwait(false); } + + // if the message includes a response body add it + if (!string.IsNullOrWhiteSpace(message.ResponseBody)) await httpResponse.WriteAsync(message.ResponseBody).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/Proxy/NatsMessageFactory.cs b/src/Proxy/NatsMessageFactory.cs index f419e74..c48480c 100644 --- a/src/Proxy/NatsMessageFactory.cs +++ b/src/Proxy/NatsMessageFactory.cs @@ -1,12 +1,11 @@ using Proxy.Shared; -namespace Proxy +namespace Proxy; + +internal static class NatsMessageFactory { - internal static class NatsMessageFactory + internal static MicroserviceMessage InitializeMessage(ProxyConfiguration config) { - internal static MicroserviceMessage InitializeMessage(ProxyConfiguration config) - { - return new MicroserviceMessage(config.Host, config.ContentType); - } + return new MicroserviceMessage(config.Host, config.ContentType); } } \ No newline at end of file diff --git a/src/Proxy/NatsSubjectParser.cs b/src/Proxy/NatsSubjectParser.cs index e6c33fd..6661cd6 100644 --- a/src/Proxy/NatsSubjectParser.cs +++ b/src/Proxy/NatsSubjectParser.cs @@ -1,22 +1,21 @@ using Serilog; -namespace Proxy +namespace Proxy; + +internal static class NatsSubjectParser { - internal static class NatsSubjectParser + internal static string? Parse(string httpMethod, string urlPath) { - internal static string? Parse(string httpMethod, string urlPath) - { - // replace all forward slashes with periods in the http request path - var subjectPath = urlPath.Replace('/', '.').TrimEnd('.'); + // replace all forward slashes with periods in the http request path + var subjectPath = urlPath.Replace('/', '.').TrimEnd('.'); - // the subject is the http method followed by the path all lowercased - var subject = string.Concat(httpMethod, subjectPath).ToLower(); + // the subject is the http method followed by the path all lowercased + var subject = string.Concat(httpMethod, subjectPath).ToLower(); - // emit a log message - Log.Information("Parsed the http request {HttpMethod} {UrlPath} to {Subject}", - httpMethod, urlPath, subject); + // emit a log message + Log.Information("Parsed the http request {HttpMethod} {UrlPath} to {Subject}", + httpMethod, urlPath, subject); - return subject; - } + return subject; } } \ No newline at end of file diff --git a/src/Proxy/Observer.cs b/src/Proxy/Observer.cs index 926f24e..a0eca57 100644 --- a/src/Proxy/Observer.cs +++ b/src/Proxy/Observer.cs @@ -1,7 +1,6 @@ -namespace Proxy +namespace Proxy; + +public class Observer { - public class Observer - { - public required string Subject { get; set; } - } + public required string Subject { get; set; } } \ No newline at end of file diff --git a/src/Proxy/Pipeline.cs b/src/Proxy/Pipeline.cs index fd8291a..290fdb3 100644 --- a/src/Proxy/Pipeline.cs +++ b/src/Proxy/Pipeline.cs @@ -1,14 +1,13 @@ -namespace Proxy +namespace Proxy; + +public class Pipeline { - public class Pipeline + public Pipeline() { - public List Observers { get; set; } - public List Steps { get; set; } - - public Pipeline() - { - Steps = new List(); - Observers = new List(); - } + Steps = new List(); + Observers = new List(); } + + public List Observers { get; set; } + public List Steps { get; set; } } \ No newline at end of file diff --git a/src/Proxy/PipelineExecutor.cs b/src/Proxy/PipelineExecutor.cs index b6b1768..2cd2d47 100644 --- a/src/Proxy/PipelineExecutor.cs +++ b/src/Proxy/PipelineExecutor.cs @@ -1,177 +1,159 @@ -using NATS.Client; +using System.Diagnostics; +using System.Text; +using NATS.Client; using Newtonsoft.Json; using Proxy.Shared; -using System.Diagnostics; -using System.Text; -namespace Proxy +namespace Proxy; + +internal class PipelineExecutor { - internal class PipelineExecutor + private readonly Pipeline _incomingPipeline; + private readonly JsonSerializerSettings _jsonSerializerSettings; + private readonly IConnection _natsConnection; + private readonly IEnumerable _observers; + private readonly Pipeline _outgoingPipeline; + private readonly int _timeout; + + public PipelineExecutor(IConnection natsConnection, + JsonSerializerSettings jsonSerializerSettings, int timeout, + Pipeline incomingPipeline, Pipeline outgoingPipeline, + IEnumerable observers) { - private readonly Pipeline _incomingPipeline; - private readonly JsonSerializerSettings _jsonSerializerSettings; - private readonly IConnection _natsConnection; - private readonly IEnumerable _observers; - private readonly Pipeline _outgoingPipeline; - private readonly int _timeout; - - public PipelineExecutor(IConnection natsConnection, - JsonSerializerSettings jsonSerializerSettings, int timeout, - Pipeline incomingPipeline, Pipeline outgoingPipeline, - IEnumerable observers) - { - _natsConnection = natsConnection; - _jsonSerializerSettings = jsonSerializerSettings; - _timeout = timeout; - _incomingPipeline = incomingPipeline; - _outgoingPipeline = outgoingPipeline; - _observers = observers; - } + _natsConnection = natsConnection; + _jsonSerializerSettings = jsonSerializerSettings; + _timeout = timeout; + _incomingPipeline = incomingPipeline; + _outgoingPipeline = outgoingPipeline; + _observers = observers; + } - internal async Task ExecutePipelineAsync(MicroserviceMessage message) - { - // execute the incoming pipeline steps allowing for request termination - await ExecutePipelineInternalAsync(message, _incomingPipeline).ConfigureAwait(false); + internal async Task ExecutePipelineAsync(MicroserviceMessage message) + { + // execute the incoming pipeline steps allowing for request termination + await ExecutePipelineInternalAsync(message, _incomingPipeline).ConfigureAwait(false); - // execute the outgoing pipeline steps (no request termination) - await ExecutePipelineInternalAsync(message, _outgoingPipeline, false).ConfigureAwait(false); + // execute the outgoing pipeline steps (no request termination) + await ExecutePipelineInternalAsync(message, _outgoingPipeline, false).ConfigureAwait(false); - // capture the execution time that it took to process the message - message.MarkComplete(); - } + // capture the execution time that it took to process the message + message.MarkComplete(); + } - internal void NotifyObservers(MicroserviceMessage message) + internal void NotifyObservers(MicroserviceMessage message) + { + foreach (var observer in _observers) { - foreach (var observer in _observers) - { - // ensure the nats connection is still in a CONNECTED state - VerifyNatsConnection(); + // ensure the nats connection is still in a CONNECTED state + VerifyNatsConnection(); - // send the message to the nats server - _natsConnection.Publish(observer, message.ToBytes(_jsonSerializerSettings)); - } + // send the message to the nats server + _natsConnection.Publish(observer, message.ToBytes(_jsonSerializerSettings)); } + } - private static MicroserviceMessage ExtractMessageFromReply(Msg reply) - { - // the NATS msg.Data property is a json encoded instance of our MicroserviceMessage so we convert it from a byte[] to a string and then - // deserialize it from json - var message = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(reply.Data)); + private static MicroserviceMessage ExtractMessageFromReply(Msg reply) + { + // the NATS msg.Data property is a json encoded instance of our MicroserviceMessage so we convert it from a byte[] to a string and then + // deserialize it from json + var message = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(reply.Data)); - if (message is null) - { - throw new Exception("The NATS reply message is not a valid MicroserviceMessage"); - } + if (message is null) throw new Exception("The NATS reply message is not a valid MicroserviceMessage"); - return message; - } + return message; + } - private static void MergeMessageProperties(MicroserviceMessage message, MicroserviceMessage responseMessage) + private static void MergeMessageProperties(MicroserviceMessage message, MicroserviceMessage responseMessage) + { + // we don't want to lose data on the original message if a microservice fails to return all of the data so we're going to just copy + // non-null properties from the responseMessage onto the message + message.ShouldTerminateRequest = responseMessage.ShouldTerminateRequest; + message.ResponseStatusCode = responseMessage.ResponseStatusCode; + message.ResponseBody = responseMessage.ResponseBody; + message.ErrorMessage = responseMessage.ErrorMessage ?? message.ErrorMessage; + + // we want to concatenate the extended properties as each step in the pipeline may be adding information + responseMessage.ExtendedProperties.ToList().ForEach(h => { - // we don't want to lose data on the original message if a microservice fails to return all of the data so we're going to just copy - // non-null properties from the responseMessage onto the message - message.ShouldTerminateRequest = responseMessage.ShouldTerminateRequest; - message.ResponseStatusCode = responseMessage.ResponseStatusCode; - message.ResponseBody = responseMessage.ResponseBody; - message.ErrorMessage = responseMessage.ErrorMessage ?? message.ErrorMessage; - - // we want to concatenate the extended properties as each step in the pipeline may be adding information - responseMessage.ExtendedProperties.ToList().ForEach(h => - { - if (!message.ExtendedProperties.ContainsKey(h.Key)) - { - message.ExtendedProperties.Add(h.Key, h.Value); - } - }); - - // we want to add any request headers that the pipeline step could have added that are not already in the RequestHeaders dictionary - responseMessage.RequestHeaders.ToList().ForEach(h => - { - if (!message.RequestHeaders.ContainsKey(h.Key)) - { - message.RequestHeaders.Add(h.Key, h.Value); - } - }); - - // we want to add any response headers that the pipeline step could have added that are not already in the ResponseHeaders dictionary - responseMessage.ResponseHeaders.ToList().ForEach(h => - { - if (!message.ResponseHeaders.ContainsKey(h.Key)) - { - message.ResponseHeaders.Add(h.Key, h.Value); - } - }); - } + if (!message.ExtendedProperties.ContainsKey(h.Key)) message.ExtendedProperties.Add(h.Key, h.Value); + }); - private async Task ExecutePipelineInternalAsync(MicroserviceMessage message, Pipeline pipeline, bool allowTermination = true) + // we want to add any request headers that the pipeline step could have added that are not already in the RequestHeaders dictionary + responseMessage.RequestHeaders.ToList().ForEach(h => { - var sw = Stopwatch.StartNew(); - foreach (var step in pipeline.Steps) - { - // start a timer - sw.Restart(); + if (!message.RequestHeaders.ContainsKey(h.Key)) message.RequestHeaders.Add(h.Key, h.Value); + }); - // execute the step - await ExecuteStepAsync(message, step).ConfigureAwait(false); + // we want to add any response headers that the pipeline step could have added that are not already in the ResponseHeaders dictionary + responseMessage.ResponseHeaders.ToList().ForEach(h => + { + if (!message.ResponseHeaders.ContainsKey(h.Key)) message.ResponseHeaders.Add(h.Key, h.Value); + }); + } - // stop the timer - sw.Stop(); + private async Task ExecutePipelineInternalAsync(MicroserviceMessage message, Pipeline pipeline, bool allowTermination = true) + { + var sw = Stopwatch.StartNew(); + foreach (var step in pipeline.Steps) + { + // start a timer + sw.Restart(); - // record how long the step took to execute - message.CallTimings.Add(new CallTiming(step.Subject, sw.ElapsedMilliseconds)); + // execute the step + await ExecuteStepAsync(message, step).ConfigureAwait(false); - // if the step requested termination we should stop processing steps - if (allowTermination && message.ShouldTerminateRequest) - { - break; - } - } + // stop the timer + sw.Stop(); + + // record how long the step took to execute + message.CallTimings.Add(new CallTiming(step.Subject, sw.ElapsedMilliseconds)); + + // if the step requested termination we should stop processing steps + if (allowTermination && message.ShouldTerminateRequest) break; } + } - private async Task ExecuteStepAsync(MicroserviceMessage message, Step step) - { - // the subject is the step's configured subject unless it is an '*' in which case it's the microservice itself - var subject = step.Subject.Equals("*") ? message.Subject : step.Subject; + private async Task ExecuteStepAsync(MicroserviceMessage message, Step step) + { + // the subject is the step's configured subject unless it is an '*' in which case it's the microservice itself + var subject = step.Subject.Equals("*") ? message.Subject : step.Subject; - try + try + { + // if the step pattern is "publish" then do a fire-and-forget NATS call, otherwise to a request/response + if (step.Pattern.Equals("publish", StringComparison.OrdinalIgnoreCase)) { - // if the step pattern is "publish" then do a fire-and-forget NATS call, otherwise to a request/response - if (step.Pattern.Equals("publish", StringComparison.OrdinalIgnoreCase)) - { - // ensure the nats connection is still in a CONNECTED state - VerifyNatsConnection(); - - // send the message to the nats server - _natsConnection.Publish(subject, message.ToBytes(_jsonSerializerSettings)); - } - else - { - // ensure the nats connection is still in a CONNECTED state - VerifyNatsConnection(); - - // call the step and wait for the response - var response = await _natsConnection.RequestAsync(subject, message.ToBytes(_jsonSerializerSettings), _timeout).ConfigureAwait(false); - - // extract the response message - var responseMessage = ExtractMessageFromReply(response); - - // merge the response into our original nats message - MergeMessageProperties(message, responseMessage); - } + // ensure the nats connection is still in a CONNECTED state + VerifyNatsConnection(); + + // send the message to the nats server + _natsConnection.Publish(subject, message.ToBytes(_jsonSerializerSettings)); } - catch (Exception ex) + else { - throw new StepException(subject, step.Pattern, ex.GetBaseException().Message); + // ensure the nats connection is still in a CONNECTED state + VerifyNatsConnection(); + + // call the step and wait for the response + var response = await _natsConnection.RequestAsync(subject, message.ToBytes(_jsonSerializerSettings), _timeout).ConfigureAwait(false); + + // extract the response message + var responseMessage = ExtractMessageFromReply(response); + + // merge the response into our original nats message + MergeMessageProperties(message, responseMessage); } } - - private void VerifyNatsConnection() + catch (Exception ex) { - if (_natsConnection.State != ConnState.CONNECTED) - { - throw new Exception( - $"Cannot send message to the NATS server because the connection is in a {_natsConnection.State} state"); - } + throw new StepException(subject, step.Pattern, ex.GetBaseException().Message); } } + + private void VerifyNatsConnection() + { + if (_natsConnection.State != ConnState.CONNECTED) + throw new Exception( + $"Cannot send message to the NATS server because the connection is in a {_natsConnection.State} state"); + } } \ No newline at end of file diff --git a/src/Proxy/Program.cs b/src/Proxy/Program.cs index 37bfd84..ab0ac33 100644 --- a/src/Proxy/Program.cs +++ b/src/Proxy/Program.cs @@ -1,152 +1,163 @@ -using NATS.Client; +using System.Net; +using NATS.Client; +using Proxy.Shared; using Serilog; -using System.Net; +using Serilog.Events; -namespace Proxy +namespace Proxy; + +public class Program { - public class Program + private static ProxyConfiguration? _config; + private static IWebHost? _host; + + public static void Main(string[] args) { - private static ProxyConfiguration? _config; - private static IWebHost? _host; + // configure our logger + ConfigureLogging(); - public static void Main(string[] args) - { - // configure our logger - ConfigureLogging(); + // capture the runtime configuration settings + ConfigureEnvironment(); - // capture the runtime configuration settings - ConfigureEnvironment(); + // create a connection to the NATS server + ConnectToNats(); - // create a connection to the NATS server - ConnectToNats(); + // configure the host + ConfigureWebHost(); - // configure the host - ConfigureWebHost(); + // run the host + _host?.Run(); + } - // run the host - _host?.Run(); - } + private static void ConfigureEnvironment() + { + Log.Information("Reading configuration values"); - private static void ConfigureEnvironment() + _config = new ProxyConfiguration { - Log.Information("Reading configuration values"); + // configure which port for Kestrel to listen on + Port = Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_HOST_PORT") ?? "5000", - _config = new ProxyConfiguration - { - // configure which port for Kestrel to listen on - Port = Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_HOST_PORT") ?? "5000", + // configure the url to the NATS server + NatsUrl = Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_NAT_URL") ?? "nats://localhost:4222", - // configure the url to the NATS server - NatsUrl = Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_NAT_URL") ?? "nats://localhost:4222", + // configure how long we are willing to wait for a reply after sending the message to the NATS server + Timeout = 1000 * int.Parse(Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_WAIT_TIMEOUT_SECONDS") ?? "10"), - // configure how long we are willing to wait for a reply after sending the message to the NATS server - Timeout = 1000 * int.Parse(Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_WAIT_TIMEOUT_SECONDS") ?? "10"), + // configure the http response status codes + HeadStatusCode = int.Parse(Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_HEAD_STATUS_CODE") ?? "200"), + PutStatusCode = int.Parse(Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_PUT_STATUS_CODE") ?? "201"), + GetStatusCode = int.Parse(Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_GET_STATUS_CODE") ?? "200"), + PatchStatusCode = int.Parse(Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_PATCH_STATUS_CODE") ?? "201"), + PostStatusCode = int.Parse(Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_POST_STATUS_CODE") ?? "201"), + DeleteStatusCode = int.Parse(Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_DELETE_STATUS_CODE") ?? "204"), - // configure the http response status codes - HeadStatusCode = int.Parse(Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_HEAD_STATUS_CODE") ?? "200"), - PutStatusCode = int.Parse(Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_PUT_STATUS_CODE") ?? "201"), - GetStatusCode = int.Parse(Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_GET_STATUS_CODE") ?? "200"), - PatchStatusCode = int.Parse(Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_PATCH_STATUS_CODE") ?? "201"), - PostStatusCode = int.Parse(Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_POST_STATUS_CODE") ?? "201"), - DeleteStatusCode = int.Parse(Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_DELETE_STATUS_CODE") ?? "204"), + // configure the content type of the http response to be used + ContentType = Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_CONTENT_TYPE") ?? "application/json; charset=utf-8", - // configure the content type of the http response to be used - ContentType = Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_CONTENT_TYPE") ?? "application/json; charset=utf-8", + // capture the request pipeline config file + PipelineConfigFile = Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_REQUEST_PIPELINE_CONFIG_FILE") ?? string.Empty + }; - // capture the request pipeline config file - PipelineConfigFile = Environment.GetEnvironmentVariable("HTTP_NATS_PROXY_REQUEST_PIPELINE_CONFIG_FILE") ?? string.Empty - }; + _config.Build(); - _config.Build(); + Log.Information("Configured"); + } - Log.Information("Configured"); - } + private static void ConfigureLogging() + { + var logProperties = Environment.GetEnvironmentVariable("LOGGING_PROPERTIES") ?? string.Empty; + var logLevelSetting = Environment.GetEnvironmentVariable("LOGGING_LEVEL") ?? "Verbose"; - private static void ConfigureLogging() - { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .WriteTo.Console() - .CreateLogger(); - } + // set the configured log level + if (!Enum.TryParse(logLevelSetting, true, out var logLevel)) logLevel = LogEventLevel.Verbose; + + // create the logging configuration + var loggerConfiguration = new LoggerConfiguration() + .MinimumLevel.Is(logLevel) + .WriteTo.Console(); - private static void ConfigureWebHost() + // set any log properties that are configured + // these should be in the format of property=value,property=value,property=value + if (!string.IsNullOrWhiteSpace(logProperties)) { - Proxy.Shared.Guard.AgainstNull(_config); + // split the properties by comma + var properties = logProperties.Split(','); - // create the request handler - var requestHandler = new RequestHandler(_config!); + foreach (var property in properties) + { + // add the property and value to the logging configuration + var key = property.Split('=')[0]; + var value = property.Split('=')[1]; + loggerConfiguration.Enrich.WithProperty(key, value); + } + } - _host = new WebHostBuilder() - .UseKestrel(options => - { - // tell Kestrel to listen on all ip addresses at the specified port - options.Listen(IPAddress.Any, int.Parse(_config!.Port)); - }) - .ConfigureServices(services => - { - // enable cors - services.AddCors(options => - { - options.AddPolicy("DefaultCORS", builder => - { - builder - .AllowAnyHeader() - .AllowAnyMethod() - .AllowAnyOrigin(); - //.AllowCredentials(); - }); - }); - }) - .Configure(app => - { - // configure cors to allow any origin - app.UseCors("DefaultCORS"); + // create the logger + Log.Logger = loggerConfiguration.CreateLogger(); + } - // every http request will be handled by our request handler - app.Run(requestHandler.HandleAsync); - }) - .Build(); - } + private static void ConfigureWebHost() + { + Guard.AgainstNull(_config); - private static void ConnectToNats() - { - Shared.Guard.AgainstNull(_config); - - Log.Information("Attempting to connect to NATS server at: {NatsUrl}", _config!.NatsUrl); - - // create a connection to the NATS server - var connectionFactory = new ConnectionFactory(); - var options = ConnectionFactory.GetDefaultOptions(); - options.AllowReconnect = true; - options.Url = _config.NatsUrl; - options.PingInterval = 1000; - options.MaxPingsOut = 2; - options.AsyncErrorEventHandler += (sender, args) => - { - Log.Information("The AsyncErrorEvent was just handled"); - }; - options.ClosedEventHandler += (sender, args) => - { - Log.Information("The ClosedEvent was just handled"); - }; - options.DisconnectedEventHandler += (sender, args) => + // create the request handler + var requestHandler = new RequestHandler(_config!); + + _host = new WebHostBuilder() + .UseKestrel(options => { - Log.Information("The DisconnectedEvent was just handled"); - }; - options.ReconnectedEventHandler += (sender, args) => + // tell Kestrel to listen on all ip addresses at the specified port + options.Listen(IPAddress.Any, int.Parse(_config!.Port)); + }) + .ConfigureServices(services => { - Log.Information("The ReconnectedEvent was just handled"); - }; - options.ServerDiscoveredEventHandler += (sender, args) => + // enable cors + services.AddCors(options => + { + options.AddPolicy("DefaultCORS", builder => + { + builder + .AllowAnyHeader() + .AllowAnyMethod() + .AllowAnyOrigin(); + //.AllowCredentials(); + }); + }); + }) + .Configure(app => { - Log.Information("The ServerDiscoveredEvent was just handled"); - }; - options.Name = "http-nats-proxy"; - _config.NatsConnection = connectionFactory.CreateConnection(options); + // configure cors to allow any origin + app.UseCors("DefaultCORS"); - Log.Information("HttpNatsProxy connected to NATS at {Url}", options.Url); - Log.Information("Waiting for messages"); - } + // every http request will be handled by our request handler + app.Run(requestHandler.HandleAsync); + }) + .Build(); + } + + private static void ConnectToNats() + { + Guard.AgainstNull(_config); + + Log.Information("Attempting to connect to NATS server at: {NatsUrl}", _config!.NatsUrl); + + // create a connection to the NATS server + var connectionFactory = new ConnectionFactory(); + var options = ConnectionFactory.GetDefaultOptions(); + options.AllowReconnect = true; + options.Url = _config.NatsUrl; + options.PingInterval = 1000; + options.MaxPingsOut = 2; + options.AsyncErrorEventHandler += (sender, args) => { Log.Information("The AsyncErrorEvent was just handled"); }; + options.ClosedEventHandler += (sender, args) => { Log.Information("The ClosedEvent was just handled"); }; + options.DisconnectedEventHandler += (sender, args) => { Log.Information("The DisconnectedEvent was just handled"); }; + options.ReconnectedEventHandler += (sender, args) => { Log.Information("The ReconnectedEvent was just handled"); }; + options.ServerDiscoveredEventHandler += (sender, args) => { Log.Information("The ServerDiscoveredEvent was just handled"); }; + options.Name = "http-nats-proxy"; + _config.NatsConnection = connectionFactory.CreateConnection(options); + + Log.Information("HttpNatsProxy connected to NATS at {Url}", options.Url); + Log.Information("Waiting for messages"); } } \ No newline at end of file diff --git a/src/Proxy/Proxy.csproj b/src/Proxy/Proxy.csproj index 4b2d81b..13cffe1 100644 --- a/src/Proxy/Proxy.csproj +++ b/src/Proxy/Proxy.csproj @@ -1,28 +1,34 @@ - - net9.0 - enable - enable - + + net9.0 + enable + enable + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - + + + + + + + PreserveNewest + + diff --git a/src/Proxy/ProxyConfiguration.cs b/src/Proxy/ProxyConfiguration.cs index c4a2f72..3482278 100644 --- a/src/Proxy/ProxyConfiguration.cs +++ b/src/Proxy/ProxyConfiguration.cs @@ -4,127 +4,113 @@ using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; -namespace Proxy +namespace Proxy; + +public class ProxyConfiguration { - public class ProxyConfiguration + public readonly JsonSerializerSettings JsonSerializerSettings = new() { - public readonly JsonSerializerSettings JsonSerializerSettings = new() - { - ContractResolver = new CamelCasePropertyNamesContractResolver() - }; - public required string ContentType { get; init; } - public int DeleteStatusCode { get; init; } - public int GetStatusCode { get; init; } - public int HeadStatusCode { get; init; } - public string Host { get; set; } = Environment.MachineName; - public Pipeline? IncomingPipeline { get; private set; } - public IConnection? NatsConnection { get; set; } - public required string NatsUrl { get; init; } - public IList Observers { get; set; } = new List(); - public Pipeline? OutgoingPipeline { get; private set; } - public int PatchStatusCode { get; init; } - public required string PipelineConfigFile { get; init; } - public required string Port { get; init; } - public int PostStatusCode { get; init; } - public int PutStatusCode { get; init; } - public int Timeout { get; init; } - - public void Build() - { - // build the entire request pipeline - var pipeline = BuildRequestPipeline(); + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + + public required string ContentType { get; init; } + public int DeleteStatusCode { get; init; } + public int GetStatusCode { get; init; } + public int HeadStatusCode { get; init; } + public string Host { get; set; } = Environment.MachineName; + public Pipeline? IncomingPipeline { get; private set; } + public IConnection? NatsConnection { get; set; } + public required string NatsUrl { get; init; } + public IList Observers { get; set; } = new List(); + public Pipeline? OutgoingPipeline { get; private set; } + public int PatchStatusCode { get; init; } + public required string PipelineConfigFile { get; init; } + public required string Port { get; init; } + public int PostStatusCode { get; init; } + public int PutStatusCode { get; init; } + public int Timeout { get; init; } + + public void Build() + { + // build the entire request pipeline + var pipeline = BuildRequestPipeline(); - // configure the incoming pipeline - ConfigureIncomingPipeline(pipeline); + // configure the incoming pipeline + ConfigureIncomingPipeline(pipeline); - // configure the outgoing pipeline - ConfigureOutgoingPipeline(pipeline); + // configure the outgoing pipeline + ConfigureOutgoingPipeline(pipeline); - // configure the observers - ConfigureObservers(pipeline); - } + // configure the observers + ConfigureObservers(pipeline); + } + + private Pipeline BuildRequestPipeline() + { + Pipeline pipeline; - private Pipeline BuildRequestPipeline() + // if the pipeline config file has been set, read in the configuration. + if (!string.IsNullOrWhiteSpace(PipelineConfigFile)) { - Pipeline pipeline; + if (!File.Exists(PipelineConfigFile)) throw new Exception($"The pipeline config file {PipelineConfigFile} does not exist."); - // if the pipeline config file has been set, read in the configuration. - if (!string.IsNullOrWhiteSpace(PipelineConfigFile)) - { - if (!File.Exists(PipelineConfigFile)) - { - throw new Exception($"The pipeline config file {PipelineConfigFile} does not exist."); - } - - var deserializer = new DeserializerBuilder() - .WithNamingConvention(CamelCaseNamingConvention.Instance) - .Build(); - - pipeline = deserializer.Deserialize(File.ReadAllText(PipelineConfigFile)); - } - else + var deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + pipeline = deserializer.Deserialize(File.ReadAllText(PipelineConfigFile)); + } + else + { + // if the pipeline config file has not been set then just send all requests directly to the microservice + pipeline = new Pipeline { - // if the pipeline config file has not been set then just send all requests directly to the microservice - pipeline = new Pipeline - { - Steps = - [ - new Step - { - Subject = "*", - Direction = "incoming", - Order = 1, - Pattern = "request" - } - ] - }; - } - - return pipeline; + Steps = + [ + new Step + { + Subject = "*", + Direction = "incoming", + Order = 1, + Pattern = "request" + } + ] + }; } - private void ConfigureIncomingPipeline(Pipeline pipeline) - { - IncomingPipeline = new Pipeline(); + return pipeline; + } - // return just those steps that are to be run for the directions of "incoming" or "both" - var directions = new List { "incoming", "both" }; + private void ConfigureIncomingPipeline(Pipeline pipeline) + { + IncomingPipeline = new Pipeline(); - foreach (var step in pipeline.Steps.OrderBy(s => s.Order)) - { - if (directions.Contains(step.Direction.ToLower())) - { - // add the step to the incoming pipeline - IncomingPipeline.Steps.Add(step); - } - } - } + // return just those steps that are to be run for the directions of "incoming" or "both" + var directions = new List { "incoming", "both" }; - private void ConfigureObservers(Pipeline pipeline) - { - Observers = new List(); + foreach (var step in pipeline.Steps.OrderBy(s => s.Order)) + if (directions.Contains(step.Direction.ToLower())) + // add the step to the incoming pipeline + IncomingPipeline.Steps.Add(step); + } - foreach (var observer in pipeline.Observers) - { - Observers.Add(observer.Subject); - } - } + private void ConfigureObservers(Pipeline pipeline) + { + Observers = new List(); - private void ConfigureOutgoingPipeline(Pipeline pipeline) - { - OutgoingPipeline = new Pipeline(); + foreach (var observer in pipeline.Observers) Observers.Add(observer.Subject); + } - // return just those steps that are to be ran for the directions of "outgoing" or "both" - var directions = new List { "outgoing", "both" }; + private void ConfigureOutgoingPipeline(Pipeline pipeline) + { + OutgoingPipeline = new Pipeline(); - foreach (var step in pipeline.Steps.OrderByDescending(s => s.Order)) - { - if (directions.Contains(step.Direction.ToLower())) - { - // add the step to the incoming pipeline - OutgoingPipeline.Steps.Add(step); - } - } - } + // return just those steps that are to be ran for the directions of "outgoing" or "both" + var directions = new List { "outgoing", "both" }; + + foreach (var step in pipeline.Steps.OrderByDescending(s => s.Order)) + if (directions.Contains(step.Direction.ToLower())) + // add the step to the incoming pipeline + OutgoingPipeline.Steps.Add(step); } } \ No newline at end of file diff --git a/src/Proxy/RequestHandler.cs b/src/Proxy/RequestHandler.cs index 552926e..ad66f5c 100644 --- a/src/Proxy/RequestHandler.cs +++ b/src/Proxy/RequestHandler.cs @@ -1,151 +1,143 @@ using Newtonsoft.Json; using Proxy.Shared; -namespace Proxy +namespace Proxy; + +public class RequestHandler { - public class RequestHandler + private readonly ProxyConfiguration _config; + private readonly PipelineExecutor _pipelineExecutor; + + public RequestHandler(ProxyConfiguration config) { - private readonly ProxyConfiguration _config; - private readonly PipelineExecutor _pipelineExecutor; + _config = config; + + Guard.AgainstNull(_config.NatsConnection); + Guard.AgainstNull(_config.IncomingPipeline); + Guard.AgainstNull(_config.OutgoingPipeline); + + _pipelineExecutor = new PipelineExecutor(_config.NatsConnection!, + _config.JsonSerializerSettings, + _config.Timeout, + _config.IncomingPipeline!, + _config.OutgoingPipeline!, + _config.Observers); + } - public RequestHandler(ProxyConfiguration config) - { - _config = config; - - Guard.AgainstNull(_config.NatsConnection); - Guard.AgainstNull(_config.IncomingPipeline); - Guard.AgainstNull(_config.OutgoingPipeline); - - _pipelineExecutor = new PipelineExecutor(natsConnection: _config.NatsConnection!, - jsonSerializerSettings: _config.JsonSerializerSettings, - timeout: _config.Timeout, - incomingPipeline: _config.IncomingPipeline!, - outgoingPipeline: _config.OutgoingPipeline!, - observers: _config.Observers); - } + public async Task HandleAsync(HttpContext context) + { + var message = NatsMessageFactory.InitializeMessage(_config); - public async Task HandleAsync(HttpContext context) + try { - var message = NatsMessageFactory.InitializeMessage(_config); + // create a nats message from the http request + await CreateNatsMsgFromHttpRequestAsync(context.Request, message); - try - { - // create a nats message from the http request - await CreateNatsMsgFromHttpRequestAsync(context.Request, message); + // execute the request pipeline + await _pipelineExecutor.ExecutePipelineAsync(message).ConfigureAwait(false); - // execute the request pipeline - await _pipelineExecutor.ExecutePipelineAsync(message).ConfigureAwait(false); + // create the http response from the processed nats message + CreateHttpResponseFromNatsMsg(context, message); - // create the http response from the processed nats message - CreateHttpResponseFromNatsMsg(context, message); - - // notify any observers that want a copy of the completed request/response - _pipelineExecutor.NotifyObservers(message); - } - catch (Exception ex) - { - // set the status code to 500 (internal server error) - context.Response.StatusCode = 500; - message.ResponseStatusCode = 500; - message.ErrorMessage = ex.GetBaseException().Message; + // notify any observers that want a copy of the completed request/response + _pipelineExecutor.NotifyObservers(message); + } + catch (Exception ex) + { + // set the status code to 500 (internal server error) + context.Response.StatusCode = 500; + message.ResponseStatusCode = 500; + message.ErrorMessage = ex.GetBaseException().Message; - object response; + object response; - if (ex is StepException stepException) + if (ex is StepException stepException) + response = new { - response = new - { - stepException.Subject, - stepException.Pattern, - Message = stepException.Msg - }; - } - else + stepException.Subject, + stepException.Pattern, + Message = stepException.Msg + }; + else + response = new { - response = new - { - message.ErrorMessage - }; - } - - // capture the execution time that it took to process the message - message.MarkComplete(); - - // write the response as a json formatted response - await context.Response.WriteAsync(JsonConvert.SerializeObject(response, _config.JsonSerializerSettings)).ConfigureAwait(false); - } + message.ErrorMessage + }; + + // capture the execution time that it took to process the message + message.MarkComplete(); + + // write the response as a json formatted response + await context.Response.WriteAsync(JsonConvert.SerializeObject(response, _config.JsonSerializerSettings)).ConfigureAwait(false); } + } - private static async Task CreateNatsMsgFromHttpRequestAsync(HttpRequest httpRequest, MicroserviceMessage message) - { - // create a NATS subject from the request method and path - if (httpRequest.Path.Value is null) - { - throw new Exception("httpRequest.Path.Value is null"); - } + private static async Task CreateNatsMsgFromHttpRequestAsync(HttpRequest httpRequest, MicroserviceMessage message) + { + // create a NATS subject from the request method and path + if (httpRequest.Path.Value is null) throw new Exception("httpRequest.Path.Value is null"); - message.Subject = NatsSubjectParser.Parse(httpRequest.Method, httpRequest.Path.Value); + message.Subject = NatsSubjectParser.Parse(httpRequest.Method, httpRequest.Path.Value); - // parse the request headers, cookies, query params and body and put them on the message - await ParseHttpRequestAsync(httpRequest, message); - } + // parse the request headers, cookies, query params and body and put them on the message + await ParseHttpRequestAsync(httpRequest, message); + } - private static async Task ParseHttpRequestAsync(HttpRequest request, MicroserviceMessage message) - { - // parse the http request body - message.RequestBody = await HttpRequestParser.ParseBodyAsync(request.Body); + private static async Task ParseHttpRequestAsync(HttpRequest request, MicroserviceMessage message) + { + // parse the http request body + message.RequestBody = await HttpRequestParser.ParseBodyAsync(request.Body); - // parse the request headers - message.RequestHeaders = HttpRequestParser.ParseHeaders(request.Headers); + // parse the request headers + message.RequestHeaders = HttpRequestParser.ParseHeaders(request.Headers); - // parse the cookies - message.Cookies = HttpRequestParser.ParseCookies(request.Cookies); + // parse the cookies + message.Cookies = HttpRequestParser.ParseCookies(request.Cookies); - // parse the query string parameters - message.QueryParams = HttpRequestParser.ParseQueryParams(request.Query); - } + // parse the query string parameters + message.QueryParams = HttpRequestParser.ParseQueryParams(request.Query); + } - private void CreateHttpResponseFromNatsMsg(HttpContext context, MicroserviceMessage message) - { - // set the response status code - message.ResponseStatusCode = - message.ResponseStatusCode == -1 ? DetermineStatusCode(context) : message.ResponseStatusCode; + private void CreateHttpResponseFromNatsMsg(HttpContext context, MicroserviceMessage message) + { + // set the response status code + message.ResponseStatusCode = + message.ResponseStatusCode == -1 ? DetermineStatusCode(context) : message.ResponseStatusCode; - // build up the http response - HttpResponseFactory.PrepareResponseAsync(context.Response, message, _config.JsonSerializerSettings); - } + // build up the http response + HttpResponseFactory.PrepareResponseAsync(context.Response, message, _config.JsonSerializerSettings); + } - private int DetermineStatusCode(HttpContext context) + private int DetermineStatusCode(HttpContext context) + { + var statusCode = 200; + switch (context.Request.Method.ToLower()) { - var statusCode = 200; - switch (context.Request.Method.ToLower()) - { - case "head": - statusCode = _config.HeadStatusCode; - break; - - case "get": - statusCode = _config.GetStatusCode; - break; - - case "put": - statusCode = _config.PutStatusCode; - break; - - case "patch": - statusCode = _config.PatchStatusCode; - break; - - case "post": - statusCode = _config.PostStatusCode; - break; - - case "delete": - statusCode = _config.DeleteStatusCode; - break; - } - - return statusCode; + case "head": + statusCode = _config.HeadStatusCode; + break; + + case "get": + statusCode = _config.GetStatusCode; + break; + + case "put": + statusCode = _config.PutStatusCode; + break; + + case "patch": + statusCode = _config.PatchStatusCode; + break; + + case "post": + statusCode = _config.PostStatusCode; + break; + + case "delete": + statusCode = _config.DeleteStatusCode; + break; } + + return statusCode; } } \ No newline at end of file diff --git a/src/Proxy/Step.cs b/src/Proxy/Step.cs index 97b1251..5c4b54e 100644 --- a/src/Proxy/Step.cs +++ b/src/Proxy/Step.cs @@ -1,10 +1,9 @@ -namespace Proxy +namespace Proxy; + +public class Step { - public class Step - { - public required string Direction { get; set; } - public int Order { get; set; } - public required string Pattern { get; set; } - public required string Subject { get; set; } - } + public required string Direction { get; set; } + public int Order { get; set; } + public required string Pattern { get; set; } + public required string Subject { get; set; } } \ No newline at end of file diff --git a/src/Proxy/StepException.cs b/src/Proxy/StepException.cs index 8e69945..f468c4e 100644 --- a/src/Proxy/StepException.cs +++ b/src/Proxy/StepException.cs @@ -1,20 +1,19 @@ -namespace Proxy +namespace Proxy; + +public class StepException : Exception { - public class StepException : Exception + public StepException(string message) : base(message) { - public string? Msg { get; private set; } - public string? Pattern { get; private set; } - public string? Subject { get; private set; } - - public StepException(string message) : base(message) - { - } + } - public StepException(string? subject, string pattern, string msg) - { - Subject = subject; - Pattern = pattern; - Msg = msg; - } + public StepException(string? subject, string pattern, string msg) + { + Subject = subject; + Pattern = pattern; + Msg = msg; } + + public string? Msg { get; private set; } + public string? Pattern { get; private set; } + public string? Subject { get; private set; } } \ No newline at end of file From 7baf1b25916555fe568ad73fd85add55c600499d Mon Sep 17 00:00:00 2001 From: Wes Shaddix Date: Thu, 21 Nov 2024 09:21:10 -0500 Subject: [PATCH 4/4] adding github action to build and publish docker image --- .github/workflows/publish-docker-image.yaml | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/publish-docker-image.yaml diff --git a/.github/workflows/publish-docker-image.yaml b/.github/workflows/publish-docker-image.yaml new file mode 100644 index 0000000..cddad07 --- /dev/null +++ b/.github/workflows/publish-docker-image.yaml @@ -0,0 +1,25 @@ +name: ci + +on: + push: + tags: + - '*' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Set up Docker Buildx + - uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + - uses: docker/login-action@v3 + - with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + - uses: docker/build-push-action@v6 + - with: + push: true + tags: wshaddix/http-nats-proxy:${{github.ref_name}} \ No newline at end of file