From 3e00125a2b3434bdfd18364f4f0d9ae41bcb10c8 Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Mon, 3 Nov 2025 15:00:20 +0000 Subject: [PATCH 1/2] change from controllers to miminal api --- .../MessagingService.Client.csproj | 4 +- ...ssagingService.EmailAggregate.Tests.csproj | 2 +- ...ngService.EmailMessage.DomainEvents.csproj | 4 +- ...sagingService.EmailMessageAggregate.csproj | 4 +- ...gService.IntegrationTesting.Helpers.csproj | 2 +- .../MessagingService.IntegrationTests.csproj | 4 +- ...gingService.SMSMessage.DomainEvents.csproj | 2 +- ...essagingService.SMSMessageAggregate.csproj | 2 +- .../Bootstrapper/MiddlewareRegistry.cs | 21 +-- .../Controllers/DomainEventController.cs | 162 ------------------ .../Controllers/EmailController.cs | 159 ----------------- MessagingService/Controllers/SMSController.cs | 126 -------------- .../Endpoints/DomainEventEndpoints.cs | 18 ++ MessagingService/Endpoints/EmailEndpoints.cs | 35 ++++ MessagingService/Endpoints/SMSEndpoints.cs | 33 ++++ .../Handlers/DomainEventHandlers.cs | 111 ++++++++++++ MessagingService/Handlers/EmailHandlers.cs | 76 ++++++++ MessagingService/Handlers/SMSHandlers.cs | 42 +++++ MessagingService/MessagingService.csproj | 8 +- MessagingService/Startup.cs | 6 +- 20 files changed, 345 insertions(+), 476 deletions(-) delete mode 100644 MessagingService/Controllers/DomainEventController.cs delete mode 100644 MessagingService/Controllers/EmailController.cs delete mode 100644 MessagingService/Controllers/SMSController.cs create mode 100644 MessagingService/Endpoints/DomainEventEndpoints.cs create mode 100644 MessagingService/Endpoints/EmailEndpoints.cs create mode 100644 MessagingService/Endpoints/SMSEndpoints.cs create mode 100644 MessagingService/Handlers/DomainEventHandlers.cs create mode 100644 MessagingService/Handlers/EmailHandlers.cs create mode 100644 MessagingService/Handlers/SMSHandlers.cs diff --git a/MessagingService.Client/MessagingService.Client.csproj b/MessagingService.Client/MessagingService.Client.csproj index 2649e26..38943a0 100644 --- a/MessagingService.Client/MessagingService.Client.csproj +++ b/MessagingService.Client/MessagingService.Client.csproj @@ -6,9 +6,9 @@ - + - + diff --git a/MessagingService.EmailAggregate.Tests/MessagingService.EmailAggregate.Tests.csproj b/MessagingService.EmailAggregate.Tests/MessagingService.EmailAggregate.Tests.csproj index 0085f15..e1d967a 100644 --- a/MessagingService.EmailAggregate.Tests/MessagingService.EmailAggregate.Tests.csproj +++ b/MessagingService.EmailAggregate.Tests/MessagingService.EmailAggregate.Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/MessagingService.EmailMessage.DomainEvents/MessagingService.EmailMessage.DomainEvents.csproj b/MessagingService.EmailMessage.DomainEvents/MessagingService.EmailMessage.DomainEvents.csproj index 8aa9ea1..36ed7e0 100644 --- a/MessagingService.EmailMessage.DomainEvents/MessagingService.EmailMessage.DomainEvents.csproj +++ b/MessagingService.EmailMessage.DomainEvents/MessagingService.EmailMessage.DomainEvents.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/MessagingService.EmailMessageAggregate/MessagingService.EmailMessageAggregate.csproj b/MessagingService.EmailMessageAggregate/MessagingService.EmailMessageAggregate.csproj index 5a1c187..d93bdd6 100644 --- a/MessagingService.EmailMessageAggregate/MessagingService.EmailMessageAggregate.csproj +++ b/MessagingService.EmailMessageAggregate/MessagingService.EmailMessageAggregate.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/MessagingService.IntegrationTesting.Helpers/MessagingService.IntegrationTesting.Helpers.csproj b/MessagingService.IntegrationTesting.Helpers/MessagingService.IntegrationTesting.Helpers.csproj index 6c160a3..b3ac028 100644 --- a/MessagingService.IntegrationTesting.Helpers/MessagingService.IntegrationTesting.Helpers.csproj +++ b/MessagingService.IntegrationTesting.Helpers/MessagingService.IntegrationTesting.Helpers.csproj @@ -7,7 +7,7 @@ - + diff --git a/MessagingService.IntegrationTests/MessagingService.IntegrationTests.csproj b/MessagingService.IntegrationTests/MessagingService.IntegrationTests.csproj index 7403b5c..14ba96f 100644 --- a/MessagingService.IntegrationTests/MessagingService.IntegrationTests.csproj +++ b/MessagingService.IntegrationTests/MessagingService.IntegrationTests.csproj @@ -6,7 +6,7 @@ - + @@ -16,7 +16,7 @@ - + all diff --git a/MessagingService.SMSMessage.DomainEvents/MessagingService.SMSMessage.DomainEvents.csproj b/MessagingService.SMSMessage.DomainEvents/MessagingService.SMSMessage.DomainEvents.csproj index efebbd1..5637812 100644 --- a/MessagingService.SMSMessage.DomainEvents/MessagingService.SMSMessage.DomainEvents.csproj +++ b/MessagingService.SMSMessage.DomainEvents/MessagingService.SMSMessage.DomainEvents.csproj @@ -6,7 +6,7 @@ - + diff --git a/MessagingService.SMSMessageAggregate/MessagingService.SMSMessageAggregate.csproj b/MessagingService.SMSMessageAggregate/MessagingService.SMSMessageAggregate.csproj index 72a9731..13cc55c 100644 --- a/MessagingService.SMSMessageAggregate/MessagingService.SMSMessageAggregate.csproj +++ b/MessagingService.SMSMessageAggregate/MessagingService.SMSMessageAggregate.csproj @@ -6,7 +6,7 @@ - + diff --git a/MessagingService/Bootstrapper/MiddlewareRegistry.cs b/MessagingService/Bootstrapper/MiddlewareRegistry.cs index f27927c..ed3e23e 100644 --- a/MessagingService/Bootstrapper/MiddlewareRegistry.cs +++ b/MessagingService/Bootstrapper/MiddlewareRegistry.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Logging; +using Shared.Authorisation; namespace MessagingService.Bootstrapper { @@ -45,18 +46,6 @@ public MiddlewareRegistry() { this.ConfigureAuthentication(); this.ConfigureSwagger(); - - this.AddControllers().AddNewtonsoftJson(options => - { - options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; - options.SerializerSettings.TypeNameHandling = TypeNameHandling.Auto; - options.SerializerSettings.Formatting = Formatting.Indented; - options.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc; - options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); - }); - - Assembly assembly = this.GetType().GetTypeInfo().Assembly; - this.AddMvcCore().AddApplicationPart(assembly).AddControllersAsServices(); bool logRequests = ConfigurationReader.GetValueOrDefault("MiddlewareLogging", "LogRequests", true); bool logResponses = ConfigurationReader.GetValueOrDefault("MiddlewareLogging", "LogResponses", true); @@ -92,6 +81,14 @@ private void ConfigureAuthentication() { }; options.IncludeErrorDetails = true; }); + this.AddClientCredentialsOnlyPolicy(); + this.AddClientCredentialsHandler(); + + this.ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.PropertyNamingPolicy = new SnakeCaseNamingPolicy(); + options.SerializerOptions.PropertyNameCaseInsensitive = true; // optional, but safer + }); } private void ConfigureSwagger() { this.AddSwaggerGen(c => diff --git a/MessagingService/Controllers/DomainEventController.cs b/MessagingService/Controllers/DomainEventController.cs deleted file mode 100644 index 95deb4d..0000000 --- a/MessagingService/Controllers/DomainEventController.cs +++ /dev/null @@ -1,162 +0,0 @@ -using Shared.Exceptions; - -namespace MessagingService.Controllers -{ - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Mvc; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; - using Shared.DomainDrivenDesign.EventSourcing; - using Shared.EventStore.Aggregate; - using Shared.EventStore.EventHandling; - using Shared.General; - using Shared.Logger; - using Shared.Serialisation; - - /// - /// - /// - /// - [Route(DomainEventController.ControllerRoute)] - [ApiController] - [ExcludeFromCodeCoverage] - [ApiExplorerSettings(IgnoreApi = true)] - public class DomainEventController : ControllerBase - { - #region Fields - - /// - /// The domain event handler resolver - /// - private readonly IDomainEventHandlerResolver DomainEventHandlerResolver; - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - /// The domain event handler resolver. - public DomainEventController(IDomainEventHandlerResolver domainEventHandlerResolver) { - this.DomainEventHandlerResolver = domainEventHandlerResolver; - } - - #endregion - - #region Methods - - /// - /// Posts the event asynchronous. - /// - /// The domain event. - /// The cancellation token. - /// - [HttpPost] - public async Task PostEventAsync([FromBody] Object request, - CancellationToken cancellationToken) { - IDomainEvent domainEvent = await this.GetDomainEvent(request); - - cancellationToken.Register(() => this.Callback(cancellationToken, domainEvent.EventId)); - - try { - Logger.LogInformation($"Processing event - ID [{domainEvent.EventId}], Type[{domainEvent.GetType().Name}]"); - - var eventHandlersResult = this.DomainEventHandlerResolver.GetDomainEventHandlers(domainEvent); - - if (eventHandlersResult.IsFailed) { - // Log a warning out - Logger.LogWarning($"No event handlers configured for Event Type [{domainEvent.GetType().Name}]"); - return this.Ok(); - } - - var eventHandlers = eventHandlersResult.Data; - - List tasks = new List(); - foreach (IDomainEventHandler domainEventHandler in eventHandlers) { - tasks.Add(domainEventHandler.Handle(domainEvent, cancellationToken)); - } - - Task.WaitAll(tasks.ToArray()); - - Logger.LogInformation($"Finished processing event - ID [{domainEvent.EventId}]"); - - return this.Ok(); - } - catch(Exception ex) { - String domainEventData = JsonConvert.SerializeObject(domainEvent); - Logger.LogError(new Exception($" Failed to Process Event, Event Data received [{domainEventData}]", ex)); - - throw; - } - } - - private void Callback(CancellationToken cancellationToken, - Guid eventId) { - if (cancellationToken.IsCancellationRequested) //I think this would always be true anyway - { - Logger.LogInformation($"Cancel request for EventId {eventId}"); - cancellationToken.ThrowIfCancellationRequested(); - } - } - - private async Task GetDomainEvent(Object domainEvent) { - String eventType = this.Request.Headers["eventType"].ToString(); - - Type type = TypeMap.GetType(eventType); - - if (type == null) - throw new NotFoundException($"Failed to find a domain event with type {eventType}"); - - JsonIgnoreAttributeIgnorerContractResolver jsonIgnoreAttributeIgnorerContractResolver = new(); - JsonSerializerSettings jsonSerialiserSettings = new() { - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - TypeNameHandling = TypeNameHandling.All, - Formatting = Formatting.Indented, - DateTimeZoneHandling = DateTimeZoneHandling.Utc, - ContractResolver = jsonIgnoreAttributeIgnorerContractResolver - }; - - if (type.IsSubclassOf(typeof(DomainEvent))) { - String json = JsonConvert.SerializeObject(domainEvent, jsonSerialiserSettings); - - DomainEventFactory domainEventFactory = new(); - String validatedJson = this.ValidateEvent(json); - return domainEventFactory.CreateDomainEvent(validatedJson, type); - } - - return null; - } - - private String ValidateEvent(String domainEventJson) { - JObject domainEvent = JObject.Parse(domainEventJson); - - if (domainEvent.ContainsKey("eventId") == false || domainEvent["eventId"].ToObject() == Guid.Empty) { - throw new ArgumentException("Domain Event must contain an Event Id"); - } - - return domainEventJson; - } - - #endregion - - #region Others - - /// - /// The controller name - /// - private const String ControllerName = "domainevents"; - - /// - /// The controller route - /// - private const String ControllerRoute = "api/" + DomainEventController.ControllerName; - - #endregion - } -} \ No newline at end of file diff --git a/MessagingService/Controllers/EmailController.cs b/MessagingService/Controllers/EmailController.cs deleted file mode 100644 index 7ce902e..0000000 --- a/MessagingService/Controllers/EmailController.cs +++ /dev/null @@ -1,159 +0,0 @@ -using Shared.General; -using Shared.Results; -using Shared.Results.Web; -using SimpleResults; -using SendEmailRequestDTO = MessagingService.DataTransferObjects.SendEmailRequest; -using SendEmailResponseDTO = MessagingService.DataTransferObjects.SendEmailResponse; -using ResendEmailRequestDTO = MessagingService.DataTransferObjects.ResendEmailRequest; - -namespace MessagingService.Controllers -{ - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Threading; - using System.Threading.Tasks; - using BusinessLogic.Requests; - using Common; - using Common.RequestExamples; - using MediatR; - using Microsoft.AspNetCore.Authorization; - using Microsoft.AspNetCore.Mvc; - using Swashbuckle.AspNetCore.Annotations; - using Swashbuckle.AspNetCore.Filters; - - /// - /// - /// - /// - [ExcludeFromCodeCoverage] - [Route(EmailController.ControllerRoute)] - [ApiController] - [Authorize] - public class EmailController : ControllerBase - { - #region Fields - - /// - /// The mediator - /// - private readonly IMediator Mediator; - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - /// The mediator. - public EmailController(IMediator mediator) - { - this.Mediator = mediator; - } - - #endregion - - #region Methods - - /// - /// Sends the email. - /// - /// The send email request. - /// The cancellation token. - /// - [HttpPost] - [Route("")] - [SwaggerResponse(201, "Created", typeof(SendEmailResponseDTO))] - [SwaggerResponseExample(201, typeof(SendEmailResponseExample))] - public async Task SendEmail([FromBody] SendEmailRequestDTO sendEmailRequest, - CancellationToken cancellationToken) - { - // Reject password tokens - if (ClaimsHelper.IsPasswordToken(this.User)) - { - return this.Forbid(); - } - - Guid messageId = sendEmailRequest.MessageId.HasValue ? sendEmailRequest.MessageId.Value : Guid.NewGuid(); - - List emailAttachments = new List(); - foreach (DataTransferObjects.EmailAttachment emailAttachment in sendEmailRequest.EmailAttachments) - { - emailAttachments.Add(new EmailAttachment - { - FileData = emailAttachment.FileData, - FileType = this.ConvertFileType(emailAttachment.FileType), - Filename = emailAttachment.Filename - }); - } - - // Create the command - EmailCommands.SendEmailCommand command= new(sendEmailRequest.ConnectionIdentifier, - messageId, - sendEmailRequest.FromAddress, - sendEmailRequest.ToAddresses, - sendEmailRequest.Subject, - sendEmailRequest.Body, - sendEmailRequest.IsHtml, - emailAttachments); - - // Route the command - Result result = await this.Mediator.Send(command, cancellationToken); - - // return the result - return result.ToActionResultX(); - } - - [ValidateAntiForgeryToken] - [HttpPost] - [Route("resend")] - [SwaggerResponse(202, "Accepted", typeof(SendEmailResponseDTO))] - [SwaggerResponseExample(202, typeof(SendEmailResponseExample))] - public async Task ResendEmail([FromBody] ResendEmailRequestDTO resendEmailRequest, - CancellationToken cancellationToken) - { - // Reject password tokens - if (ClaimsHelper.IsPasswordToken(this.User)) - { - return this.Forbid(); - } - - // Create the command - EmailCommands.ResendEmailCommand command = new(resendEmailRequest.ConnectionIdentifier, resendEmailRequest.MessageId); - - // Route the command - Result result = await this.Mediator.Send(command, cancellationToken); - - // return the result - return result.ToActionResultX(); - } - - private FileType ConvertFileType(DataTransferObjects.FileType emailAttachmentFileType) - { - switch(emailAttachmentFileType) - { - case DataTransferObjects.FileType.PDF: - return MessagingService.BusinessLogic.Requests.FileType.PDF; - default: - return MessagingService.BusinessLogic.Requests.FileType.None; - } - } - - #endregion - - #region Others - - /// - /// The controller name - /// - private const String ControllerName = "email"; - - /// - /// The controller route - /// - private const String ControllerRoute = "api/" + EmailController.ControllerName; - - #endregion - } -} \ No newline at end of file diff --git a/MessagingService/Controllers/SMSController.cs b/MessagingService/Controllers/SMSController.cs deleted file mode 100644 index 5b89539..0000000 --- a/MessagingService/Controllers/SMSController.cs +++ /dev/null @@ -1,126 +0,0 @@ -using MessagingService.BusinessLogic.Requests; -using Shared.General; -using Shared.Results; -using Shared.Results.Web; -using SimpleResults; - -namespace MessagingService.Controllers -{ - using System; - using System.Diagnostics.CodeAnalysis; - using System.Threading; - using System.Threading.Tasks; - using Common; - using Common.RequestExamples; - using DataTransferObjects; - using MediatR; - using Microsoft.AspNetCore.Authorization; - using Microsoft.AspNetCore.Mvc; - using Swashbuckle.AspNetCore.Annotations; - using Swashbuckle.AspNetCore.Filters; - - [ExcludeFromCodeCoverage] - [Route(SMSController.ControllerRoute)] - [ApiController] - [Authorize] - public class SMSController : ControllerBase - { - #region Fields - - /// - /// The mediator - /// - private readonly IMediator Mediator; - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the class. - /// - /// The mediator. - public SMSController(IMediator mediator) - { - this.Mediator = mediator; - } - - #endregion - - #region Methods - - /// - /// Sends the SMS. - /// - /// The send SMS request. - /// The cancellation token. - /// - [HttpPost] - [Route("")] - [SwaggerResponse(201, "Created", typeof(SendSMSResponse))] - [SwaggerResponseExample(201, typeof(SendSMSResponseExample))] - public async Task SendSMS([FromBody] SendSMSRequest sendSMSRequest, - CancellationToken cancellationToken) - { - // Reject password tokens - if (ClaimsHelper.IsPasswordToken(this.User)) { - return Result.Forbidden().ToActionResultX(); - } - - Guid messageId = sendSMSRequest.MessageId.HasValue ? sendSMSRequest.MessageId.Value : Guid.NewGuid(); - - // Create the command - SMSCommands.SendSMSCommand command = new(sendSMSRequest.ConnectionIdentifier, - messageId, - sendSMSRequest.Sender, - sendSMSRequest.Destination, - sendSMSRequest.Message); - - // Route the command - Result result = await this.Mediator.Send(command, cancellationToken); - - // return the result - return result.ToActionResultX(); - } - - [ValidateAntiForgeryToken] - [HttpPost] - [Route("resend")] - [SwaggerResponse(202, "Accepted", typeof(SendSMSResponse))] - [SwaggerResponseExample(202, typeof(SendSMSResponseExample))] - public async Task ResendSMS([FromBody] ResendSMSRequest resendSMSRequest, - CancellationToken cancellationToken) - { - // Reject password tokens - if (ClaimsHelper.IsPasswordToken(this.User)) - { - return Result.Forbidden().ToActionResultX(); - } - - // Create the command - SMSCommands.ResendSMSCommand command = new(resendSMSRequest.ConnectionIdentifier, resendSMSRequest.MessageId); - - // Route the command - Result result = await this.Mediator.Send(command, cancellationToken); - - // return the result - return result.ToActionResultX(); - } - - #endregion - - #region Others - - /// - /// The controller name - /// - private const String ControllerName = "sms"; - - /// - /// The controller route - /// - private const String ControllerRoute = "api/" + SMSController.ControllerName; - - #endregion - } -} \ No newline at end of file diff --git a/MessagingService/Endpoints/DomainEventEndpoints.cs b/MessagingService/Endpoints/DomainEventEndpoints.cs new file mode 100644 index 0000000..c6e2e16 --- /dev/null +++ b/MessagingService/Endpoints/DomainEventEndpoints.cs @@ -0,0 +1,18 @@ +using MessagingService.Handlers; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace MessagingService.Endpoints; + +public static class DomainEventEndpoints +{ + private const string Route = "api/domainevents"; + + public static void MapDomainEventEndpoint(this IEndpointRouteBuilder app) + { + app.MapPost(Route, DomainEventHandlers.HandleDomainEvent) + .ExcludeFromDescription() // hides from Swagger + .WithName("DomainEvent"); + } +} \ No newline at end of file diff --git a/MessagingService/Endpoints/EmailEndpoints.cs b/MessagingService/Endpoints/EmailEndpoints.cs new file mode 100644 index 0000000..94271fa --- /dev/null +++ b/MessagingService/Endpoints/EmailEndpoints.cs @@ -0,0 +1,35 @@ +using MessagingService.DataTransferObjects; +using MessagingService.Handlers; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Shared.Authorisation; +using Shared.Extensions; +using Shared.Middleware; + +namespace MessagingService.Endpoints +{ + public static class EmailEndpoints + { + private const string BaseRoute = "/api/email"; + + public static IEndpointRouteBuilder MapEmailEndpoints(this IEndpointRouteBuilder endpoints) + { + RouteGroupBuilder emailGroup = endpoints.MapGroup(BaseRoute).WithTags("Email"); + + emailGroup.MapPost("/", EmailHandlers.SendEmail) + .RequireAuthorization(AuthorizationExtensions.PolicyNames.ClientCredentialsOnlyPolicy) + .WithName("Send Email") + .WithSummary("Sends an email") + .WithStandardProduces(StatusCodes.Status201Created); + + emailGroup.MapPost("/resend", EmailHandlers.ResendEmail) + .RequireAuthorization(AuthorizationExtensions.PolicyNames.ClientCredentialsOnlyPolicy) + .WithName("Resend Email") + .WithSummary("Resends an email by id") + .WithStandardProduces< ErrorResponse>(StatusCodes.Status202Accepted); + + return endpoints; + } + } +} diff --git a/MessagingService/Endpoints/SMSEndpoints.cs b/MessagingService/Endpoints/SMSEndpoints.cs new file mode 100644 index 0000000..8ee3527 --- /dev/null +++ b/MessagingService/Endpoints/SMSEndpoints.cs @@ -0,0 +1,33 @@ +using MessagingService.DataTransferObjects; +using MessagingService.Handlers; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Shared.Authorisation; +using Shared.Extensions; +using Shared.Middleware; + +namespace MessagingService.Endpoints; + +public static class SMSEndpoints +{ + private const string BaseRoute = "/api/sms"; + public static IEndpointRouteBuilder MapSMSEndpoints(this IEndpointRouteBuilder endpoints) + { + RouteGroupBuilder smsGroup = endpoints.MapGroup(BaseRoute).WithTags("SMS"); + + smsGroup.MapPost("/", SMSHandlers.SendSMS) + .RequireAuthorization(AuthorizationExtensions.PolicyNames.ClientCredentialsOnlyPolicy) + .WithName("Send SMS") + .WithSummary("Sends an SMS message") + .WithStandardProduces(StatusCodes.Status201Created); + + smsGroup.MapPost("/resend", SMSHandlers.ResendSMS) + .RequireAuthorization(AuthorizationExtensions.PolicyNames.ClientCredentialsOnlyPolicy) + .WithName("Resend SMS") + .WithSummary("Resends an SMS message by id") + .WithStandardProduces< ErrorResponse>(StatusCodes.Status202Accepted); + + return endpoints; + } +} \ No newline at end of file diff --git a/MessagingService/Handlers/DomainEventHandlers.cs b/MessagingService/Handlers/DomainEventHandlers.cs new file mode 100644 index 0000000..5393ab0 --- /dev/null +++ b/MessagingService/Handlers/DomainEventHandlers.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Shared.DomainDrivenDesign.EventSourcing; +using Shared.EventStore.Aggregate; +using Shared.EventStore.EventHandling; +using Shared.Exceptions; +using Shared.General; +using Shared.Logger; +using Shared.Serialisation; +using SimpleResults; + +namespace MessagingService.Handlers; + +public static class DomainEventHandlers +{ + public static async Task HandleDomainEvent(HttpRequest request, + object body, + IDomainEventHandlerResolver resolver, + CancellationToken cancellationToken) + { + IDomainEvent domainEvent = await GetDomainEvent(request, body); + + cancellationToken.Register(() => Callback(cancellationToken, domainEvent.EventId)); + + try + { + Logger.LogInformation($"Processing event - ID [{domainEvent.EventId}], Type[{domainEvent.GetType().Name}]"); + + Result> eventHandlersResult = resolver.GetDomainEventHandlers(domainEvent); + + if (eventHandlersResult.IsFailed) + { + Logger.LogWarning($"No event handlers configured for Event Type [{domainEvent.GetType().Name}]"); + return Results.Ok(); + } + + var eventHandlers = eventHandlersResult.Data; + var tasks = eventHandlers.Select(h => h.Handle(domainEvent, cancellationToken)); + await Task.WhenAll(tasks); + + Logger.LogInformation("Finished processing event - ID [{domainEvent.EventId}]"); + + return Results.Ok(); + } + catch (Exception ex) + { + string domainEventData = JsonConvert.SerializeObject(domainEvent); + Logger.LogError($"Failed to process event. Data received [{domainEventData}]", ex); + throw; + } + } + + private static void Callback(CancellationToken cancellationToken, Guid eventId) + { + if (cancellationToken.IsCancellationRequested) + { + Logger.LogInformation($"Cancel request for EventId {eventId}"); + cancellationToken.ThrowIfCancellationRequested(); + } + } + + private static async Task GetDomainEvent(HttpRequest request, object domainEvent) + { + string eventType = request.Headers["eventType"].ToString(); + + Type type = TypeMap.GetType(eventType); + + if (type == null) + throw new NotFoundException($"Failed to find a domain event with type {eventType}"); + + var resolver = new JsonIgnoreAttributeIgnorerContractResolver(); + var settings = new JsonSerializerSettings + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + TypeNameHandling = TypeNameHandling.All, + Formatting = Formatting.Indented, + DateTimeZoneHandling = DateTimeZoneHandling.Utc, + ContractResolver = resolver + }; + + if (type.IsSubclassOf(typeof(DomainEvent))) + { + string json = JsonConvert.SerializeObject(domainEvent, settings); + + var factory = new DomainEventFactory(); + string validatedJson = ValidateEvent(json); + return factory.CreateDomainEvent(validatedJson, type); + } + + return null; + } + + private static string ValidateEvent(string domainEventJson) + { + var domainEvent = JObject.Parse(domainEventJson); + + if (!domainEvent.ContainsKey("eventId") || + domainEvent["eventId"]!.ToObject() == Guid.Empty) + { + throw new ArgumentException("Domain Event must contain an Event Id"); + } + + return domainEventJson; + } +} \ No newline at end of file diff --git a/MessagingService/Handlers/EmailHandlers.cs b/MessagingService/Handlers/EmailHandlers.cs new file mode 100644 index 0000000..8e37157 --- /dev/null +++ b/MessagingService/Handlers/EmailHandlers.cs @@ -0,0 +1,76 @@ +using MediatR; +using MessagingService.BusinessLogic.Requests; +using MessagingService.DataTransferObjects; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Shared.Results.Web; +using SimpleResults; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EmailAttachment = MessagingService.BusinessLogic.Requests.EmailAttachment; +using FileType = MessagingService.BusinessLogic.Requests.FileType; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace MessagingService.Handlers +{ + public static class EmailHandlers + { + public static async Task SendEmail(SendEmailRequest sendEmailRequest, + IMediator mediator, + CancellationToken cancellationToken) { + Guid messageId = sendEmailRequest.MessageId.HasValue ? sendEmailRequest.MessageId.Value : Guid.NewGuid(); + + List emailAttachments = new(); + foreach (DataTransferObjects.EmailAttachment emailAttachment in sendEmailRequest.EmailAttachments) + { + emailAttachments.Add(new EmailAttachment + { + FileData = emailAttachment.FileData, + FileType = ConvertFileType(emailAttachment.FileType), + Filename = emailAttachment.Filename + }); + } + + // Create the command + EmailCommands.SendEmailCommand command = new(sendEmailRequest.ConnectionIdentifier, + messageId, + sendEmailRequest.FromAddress, + sendEmailRequest.ToAddresses, + sendEmailRequest.Subject, + sendEmailRequest.Body, + sendEmailRequest.IsHtml, + emailAttachments); + + // Route the command + Result result = await mediator.Send(command, cancellationToken); + + return ResponseFactory.FromResult(result, (r) => new SendEmailResponse + { + MessageId = r + }); + } + + public static async Task ResendEmail(ResendEmailRequest resendEmailRequest, + IMediator mediator, + CancellationToken cancellationToken) { + // Create the command + EmailCommands.ResendEmailCommand command = new(resendEmailRequest.ConnectionIdentifier, + resendEmailRequest.MessageId); + // Route the command + Result result = await mediator.Send(command, cancellationToken); + + return ResponseFactory.FromResult(result); + } + + internal static FileType ConvertFileType(DataTransferObjects.FileType emailAttachmentFileType) { + switch (emailAttachmentFileType) { + case DataTransferObjects.FileType.PDF: + return FileType.PDF; + default: + return FileType.None; + } + } + } +} diff --git a/MessagingService/Handlers/SMSHandlers.cs b/MessagingService/Handlers/SMSHandlers.cs new file mode 100644 index 0000000..d32d82c --- /dev/null +++ b/MessagingService/Handlers/SMSHandlers.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using MessagingService.BusinessLogic.Requests; +using MessagingService.DataTransferObjects; +using Microsoft.AspNetCore.Http; +using Shared.Results.Web; +using SimpleResults; + +namespace MessagingService.Handlers; + +public static class SMSHandlers { + public static async Task SendSMS(SendSMSRequest sendSMSRequest, + IMediator mediator, + CancellationToken cancellationToken) { + Guid messageId = sendSMSRequest.MessageId.HasValue ? sendSMSRequest.MessageId.Value : Guid.NewGuid(); + // Create the command + SMSCommands.SendSMSCommand command = new(sendSMSRequest.ConnectionIdentifier, + messageId, + sendSMSRequest.Sender, + sendSMSRequest.Destination, + sendSMSRequest.Message); + // Route the command + Result result = await mediator.Send(command, cancellationToken); + return ResponseFactory.FromResult(result, (r) => new SendSMSResponse + { + MessageId = r + }); + } + + public static async Task ResendSMS(ResendSMSRequest resendSMSRequest, + IMediator mediator, + CancellationToken cancellationToken) { + // Create the command + SMSCommands.ResendSMSCommand command = new(resendSMSRequest.ConnectionIdentifier, + resendSMSRequest.MessageId); + // Route the command + Result result = await mediator.Send(command, cancellationToken); + return ResponseFactory.FromResult(result); + } +} \ No newline at end of file diff --git a/MessagingService/MessagingService.csproj b/MessagingService/MessagingService.csproj index 63561f8..0fb2325 100644 --- a/MessagingService/MessagingService.csproj +++ b/MessagingService/MessagingService.csproj @@ -18,13 +18,13 @@ - - + + - - + + diff --git a/MessagingService/Startup.cs b/MessagingService/Startup.cs index b97e1a1..b7a18fc 100644 --- a/MessagingService/Startup.cs +++ b/MessagingService/Startup.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using MessagingService.Endpoints; namespace MessagingService { @@ -98,7 +99,10 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF app.UseEndpoints(endpoints => { - endpoints.MapControllers(); + endpoints.MapEmailEndpoints(); + endpoints.MapSMSEndpoints(); + endpoints.MapDomainEventEndpoint(); + endpoints.MapHealthChecks("health", new HealthCheckOptions() { Predicate = _ => true, From 64671b4aaffde4d95006a616d0f5e6d8b4535884 Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Mon, 3 Nov 2025 19:29:46 +0000 Subject: [PATCH 2/2] fix test failures --- MessagingService.Tests/General/BootstrapperTests.cs | 13 ++++++++++--- .../MessagingService.Tests.csproj | 1 + MessagingService/Bootstrapper/MiddlewareRegistry.cs | 3 +++ MessagingService/Startup.cs | 1 - 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/MessagingService.Tests/General/BootstrapperTests.cs b/MessagingService.Tests/General/BootstrapperTests.cs index 77ad2ca..03846a0 100644 --- a/MessagingService.Tests/General/BootstrapperTests.cs +++ b/MessagingService.Tests/General/BootstrapperTests.cs @@ -1,17 +1,24 @@ -using System; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using System; using System.Collections.Generic; +using System.Reflection; using System.Text; +using Microsoft.AspNetCore.Routing.Patterns; namespace MessagingService.Tests.General { - using System.Diagnostics; - using System.Linq; using Lamar; using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Mvc.Abstractions; + using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Options; using Moq; + using System.Diagnostics; + using System.Linq; using Testing; using Xunit; diff --git a/MessagingService.Tests/MessagingService.Tests.csproj b/MessagingService.Tests/MessagingService.Tests.csproj index 8d1c9a0..8944057 100644 --- a/MessagingService.Tests/MessagingService.Tests.csproj +++ b/MessagingService.Tests/MessagingService.Tests.csproj @@ -7,6 +7,7 @@ + diff --git a/MessagingService/Bootstrapper/MiddlewareRegistry.cs b/MessagingService/Bootstrapper/MiddlewareRegistry.cs index ed3e23e..295e79b 100644 --- a/MessagingService/Bootstrapper/MiddlewareRegistry.cs +++ b/MessagingService/Bootstrapper/MiddlewareRegistry.cs @@ -44,6 +44,8 @@ public MiddlewareRegistry() { failureStatus: HealthStatus.Unhealthy, tags: new string[] { "db", "eventstore" }); + + this.AddControllers(); this.ConfigureAuthentication(); this.ConfigureSwagger(); @@ -91,6 +93,7 @@ private void ConfigureAuthentication() { }); } private void ConfigureSwagger() { + this.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo diff --git a/MessagingService/Startup.cs b/MessagingService/Startup.cs index b7a18fc..a2249e6 100644 --- a/MessagingService/Startup.cs +++ b/MessagingService/Startup.cs @@ -93,7 +93,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF app.AddExceptionHandler(); app.UseRouting(); - app.UseAuthentication(); app.UseAuthorization();