From 1390102703224251af0e89c58d3e885d32937eb7 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 21 Oct 2026 10:20:07 +0100 Subject: [PATCH 1/2] use api defaults and modern startup --- Directory.Packages.props | 33 +-- .../Mediator/MediatorTests.cs | 25 +- .../EmailAttachment.cs | 9 +- .../FileType.cs | 5 +- ...essagingService.DataTransferObjects.csproj | 5 +- .../ResendEmailRequest.cs | 6 +- .../ResendSMSRequest.cs | 5 +- .../SendEmailRequest.cs | 12 +- .../SendEmailResponse.cs | 4 +- .../SendSMSRequest.cs | 8 +- .../SendSMSResponse.cs | 4 +- .../DomainEventHandlerRegistry.cs | 16 +- .../Bootstrapper/MessagingProxyRegistry.cs | 15 +- .../Bootstrapper/MiddlewareRegistry.cs | 75 ++--- .../Bootstrapper/RepositoryRegistry.cs | 6 +- .../DtoXmlDocumentationSchemaTransformer.cs | 71 +++++ MessagingService/Common/Extensions.cs | 27 +- ...gingRequestExamplesOperationTransformer.cs | 74 +++++ .../Common/RequestExamples/ExampleData.cs | 86 ------ .../SendEmailRequestExample.cs | 69 ----- .../SendEmailResponseExample.cs | 30 -- .../RequestExamples/SendSMSRequestExample.cs | 30 -- .../RequestExamples/SendSMSResponseExample.cs | 30 -- .../SubscriptionPreWarmHostedService.cs | 35 +++ .../Common/SwaggerDefaultValues.cs | 50 ---- .../Common/SwaggerJsonConverter.cs | 80 ------ MessagingService/Endpoints/EmailEndpoints.cs | 2 + MessagingService/Endpoints/SMSEndpoints.cs | 4 +- MessagingService/MessagingService.csproj | 13 +- MessagingService/Program.cs | 260 ++++++++++++------ MessagingService/Startup.cs | 108 -------- 31 files changed, 495 insertions(+), 702 deletions(-) create mode 100644 MessagingService/Common/DtoXmlDocumentationSchemaTransformer.cs create mode 100644 MessagingService/Common/MessagingRequestExamplesOperationTransformer.cs delete mode 100644 MessagingService/Common/RequestExamples/ExampleData.cs delete mode 100644 MessagingService/Common/RequestExamples/SendEmailRequestExample.cs delete mode 100644 MessagingService/Common/RequestExamples/SendEmailResponseExample.cs delete mode 100644 MessagingService/Common/RequestExamples/SendSMSRequestExample.cs delete mode 100644 MessagingService/Common/RequestExamples/SendSMSResponseExample.cs create mode 100644 MessagingService/Common/SubscriptionPreWarmHostedService.cs delete mode 100644 MessagingService/Common/SwaggerDefaultValues.cs delete mode 100644 MessagingService/Common/SwaggerJsonConverter.cs delete mode 100644 MessagingService/Startup.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 26c7696..5f39dc3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,36 +3,37 @@ true - - - + + + - + - + + - - - - - + + + + + - + @@ -48,19 +49,13 @@ - - - - - - - - + + \ No newline at end of file diff --git a/MessagingService.BusinessLogic.Tests/Mediator/MediatorTests.cs b/MessagingService.BusinessLogic.Tests/Mediator/MediatorTests.cs index 557d5de..d952100 100644 --- a/MessagingService.BusinessLogic.Tests/Mediator/MediatorTests.cs +++ b/MessagingService.BusinessLogic.Tests/Mediator/MediatorTests.cs @@ -10,6 +10,8 @@ using System.Text; using System.Threading.Tasks; using Xunit; +using MessagingService.Bootstrapper; +using Shared.General; namespace MessagingService.BusinessLogic.Tests.Mediator { @@ -40,15 +42,21 @@ public async Task Mediator_Send_RequestHandled() { hostingEnvironment.ApplicationName.Getter().Returns("Test Application"); ServiceRegistry services = new(); - Startup s = new(hostingEnvironment.Instance()); - Startup.Configuration = this.SetupMemoryConfiguration(); + IConfigurationRoot configuration = this.SetupMemoryConfiguration(); + ConfigurationReader.Initialise(configuration); - this.AddTestRegistrations(services, hostingEnvironment.Instance()); - s.ConfigureContainer(services); - //Startup.Container.AssertConfigurationIsValid(AssertMode.Full); + this.AddTestRegistrations(services, hostingEnvironment.Instance(), configuration); + services.IncludeRegistry(new MediatorRegistry()); + services.IncludeRegistry(new RepositoryRegistry(configuration)); + services.IncludeRegistry(new MiddlewareRegistry(configuration)); + services.IncludeRegistry(new DomainServiceRegistry()); + services.IncludeRegistry(new DomainEventHandlerRegistry(configuration)); + services.IncludeRegistry(new MessagingProxyRegistry(configuration)); + services.IncludeRegistry(new SerialiserRegistry()); + Container container = new(services); List errors = new(); - IMediator mediator = Startup.Container.GetService(); + IMediator mediator = container.GetService(); foreach (IBaseRequest baseRequest in this.Requests) { try { await mediator.Send(baseRequest); @@ -76,14 +84,15 @@ private IConfigurationRoot SetupMemoryConfiguration() } private void AddTestRegistrations(ServiceRegistry services, - IWebHostEnvironment hostingEnvironment) { + IWebHostEnvironment hostingEnvironment, + IConfiguration configuration) { services.AddLogging(); DiagnosticListener diagnosticSource = new(hostingEnvironment.ApplicationName); services.AddSingleton(diagnosticSource); services.AddSingleton(diagnosticSource); services.AddSingleton(hostingEnvironment); services.AddSingleton(hostingEnvironment); - services.AddSingleton(Startup.Configuration); + services.AddSingleton(configuration); services.OverrideServices(s => { s.AddSingleton(); }); } diff --git a/MessagingService.DataTransferObjects/EmailAttachment.cs b/MessagingService.DataTransferObjects/EmailAttachment.cs index 28052da..0dc9f8c 100644 --- a/MessagingService.DataTransferObjects/EmailAttachment.cs +++ b/MessagingService.DataTransferObjects/EmailAttachment.cs @@ -3,16 +3,17 @@ using System; using System.Diagnostics.CodeAnalysis; - /// - /// - /// + /// Describes a file attached to an email. [ExcludeFromCodeCoverage] public class EmailAttachment { + /// Gets or sets the attachment contents encoded as a Base64 string. public String FileData { get; set; } + /// Gets or sets the attachment filename. public String Filename { get; set; } + /// Gets or sets the attachment file type. public FileType FileType { get; set; } } -} \ No newline at end of file +} diff --git a/MessagingService.DataTransferObjects/FileType.cs b/MessagingService.DataTransferObjects/FileType.cs index 6e58d7d..acf614b 100644 --- a/MessagingService.DataTransferObjects/FileType.cs +++ b/MessagingService.DataTransferObjects/FileType.cs @@ -2,10 +2,13 @@ { using System.Diagnostics.CodeAnalysis; + /// Specifies the supported email attachment file types. public enum FileType { + /// No recognized file type. None = 0, + /// Portable Document Format (PDF). PDF } -} \ No newline at end of file +} diff --git a/MessagingService.DataTransferObjects/MessagingService.DataTransferObjects.csproj b/MessagingService.DataTransferObjects/MessagingService.DataTransferObjects.csproj index e3cccf9..a0b5861 100644 --- a/MessagingService.DataTransferObjects/MessagingService.DataTransferObjects.csproj +++ b/MessagingService.DataTransferObjects/MessagingService.DataTransferObjects.csproj @@ -2,10 +2,7 @@ net10.0 - - - - D:\Projects\StuartFerguson\Messaging\MessagingService.DataTransferObjects\MessagingService.DataTransferObjects.xml + true diff --git a/MessagingService.DataTransferObjects/ResendEmailRequest.cs b/MessagingService.DataTransferObjects/ResendEmailRequest.cs index e99e302..30c0141 100644 --- a/MessagingService.DataTransferObjects/ResendEmailRequest.cs +++ b/MessagingService.DataTransferObjects/ResendEmailRequest.cs @@ -3,10 +3,14 @@ using System; using System.Diagnostics.CodeAnalysis; + /// Identifies an existing email message to resend. [ExcludeFromCodeCoverage] public class ResendEmailRequest { + /// Gets or sets the identifier of the message to resend. public Guid MessageId { get; set; } + + /// Gets or sets the identifier of the messaging connection used for the original message. public Guid ConnectionIdentifier { get; set; } } -} \ No newline at end of file +} diff --git a/MessagingService.DataTransferObjects/ResendSMSRequest.cs b/MessagingService.DataTransferObjects/ResendSMSRequest.cs index 5756e36..52c4d0f 100644 --- a/MessagingService.DataTransferObjects/ResendSMSRequest.cs +++ b/MessagingService.DataTransferObjects/ResendSMSRequest.cs @@ -3,10 +3,13 @@ using System; using System.Diagnostics.CodeAnalysis; + /// Identifies an existing SMS message to resend. [ExcludeFromCodeCoverage] public class ResendSMSRequest{ + /// Gets or sets the identifier of the message to resend. public Guid MessageId{ get; set; } + /// Gets or sets the identifier of the messaging connection used for the original message. public Guid ConnectionIdentifier{ get; set; } } -} \ No newline at end of file +} diff --git a/MessagingService.DataTransferObjects/SendEmailRequest.cs b/MessagingService.DataTransferObjects/SendEmailRequest.cs index b9381c4..1e97388 100644 --- a/MessagingService.DataTransferObjects/SendEmailRequest.cs +++ b/MessagingService.DataTransferObjects/SendEmailRequest.cs @@ -4,28 +4,38 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; + /// Describes an email message to send. [ExcludeFromCodeCoverage] public class SendEmailRequest { + /// Initializes a new request with an empty attachment collection. public SendEmailRequest() { this.EmailAttachments = new List(); } + /// Gets or sets an optional caller-supplied message identifier. The service generates an identifier when this value is omitted or null. public Guid? MessageId { get; set; } + /// Gets or sets the email body. Its interpretation as plain text or HTML is controlled by . public String Body { get; set; } + /// Gets or sets the identifier of the messaging connection to use. public Guid ConnectionIdentifier { get; set; } + /// Gets or sets the sender email address. public String FromAddress { get; set; } + /// Gets or sets a value indicating whether contains HTML. public Boolean IsHtml { get; set; } + /// Gets or sets the email subject. public String Subject { get; set; } + /// Gets or sets the recipient email addresses. public List ToAddresses { get; set; } + /// Gets or sets attachments to include with the email. This collection defaults to empty. public List EmailAttachments { get; set; } } -} \ No newline at end of file +} diff --git a/MessagingService.DataTransferObjects/SendEmailResponse.cs b/MessagingService.DataTransferObjects/SendEmailResponse.cs index 5d4d0bf..8daebbf 100644 --- a/MessagingService.DataTransferObjects/SendEmailResponse.cs +++ b/MessagingService.DataTransferObjects/SendEmailResponse.cs @@ -3,9 +3,11 @@ using System; using System.Diagnostics.CodeAnalysis; + /// Contains the result identifier for a send-email request. [ExcludeFromCodeCoverage] public class SendEmailResponse { + /// Gets or sets the identifier assigned to the message. public Guid MessageId { get; set; } } -} \ No newline at end of file +} diff --git a/MessagingService.DataTransferObjects/SendSMSRequest.cs b/MessagingService.DataTransferObjects/SendSMSRequest.cs index 274b18f..6b610e5 100644 --- a/MessagingService.DataTransferObjects/SendSMSRequest.cs +++ b/MessagingService.DataTransferObjects/SendSMSRequest.cs @@ -3,17 +3,23 @@ using System; using System.Diagnostics.CodeAnalysis; + /// Describes an SMS message to send. [ExcludeFromCodeCoverage] public class SendSMSRequest { + /// Gets or sets the identifier of the messaging connection to use. public Guid ConnectionIdentifier { get; set; } + /// Gets or sets the recipient phone number. public String Destination { get; set; } + /// Gets or sets the text content of the SMS message. public String Message { get; set; } + /// Gets or sets an optional caller-supplied message identifier. The service generates an identifier when this value is omitted or null. public Guid? MessageId { get; set; } + /// Gets or sets the sender name or number shown to the recipient. public String Sender { get; set; } } -} \ No newline at end of file +} diff --git a/MessagingService.DataTransferObjects/SendSMSResponse.cs b/MessagingService.DataTransferObjects/SendSMSResponse.cs index 280c17d..d3ab5b3 100644 --- a/MessagingService.DataTransferObjects/SendSMSResponse.cs +++ b/MessagingService.DataTransferObjects/SendSMSResponse.cs @@ -3,9 +3,11 @@ using System; using System.Diagnostics.CodeAnalysis; + /// Contains the result identifier for a send-SMS request. [ExcludeFromCodeCoverage] public class SendSMSResponse { + /// Gets or sets the identifier assigned to the message. public Guid MessageId { get; set; } } -} \ No newline at end of file +} diff --git a/MessagingService/Bootstrapper/DomainEventHandlerRegistry.cs b/MessagingService/Bootstrapper/DomainEventHandlerRegistry.cs index d0ac1d0..9480a47 100644 --- a/MessagingService/Bootstrapper/DomainEventHandlerRegistry.cs +++ b/MessagingService/Bootstrapper/DomainEventHandlerRegistry.cs @@ -14,18 +14,14 @@ [ExcludeFromCodeCoverage] public class DomainEventHandlerRegistry : ServiceRegistry { - public DomainEventHandlerRegistry() + public DomainEventHandlerRegistry(IConfiguration configuration) { Dictionary eventHandlersConfiguration = new Dictionary(); - if (Startup.Configuration != null) + IConfigurationSection section = configuration.GetSection("AppSettings:EventHandlerConfiguration"); + if (section.Exists()) { - IConfigurationSection section = Startup.Configuration.GetSection("AppSettings:EventHandlerConfiguration"); - - if (section != null) - { - Startup.Configuration.GetSection("AppSettings:EventHandlerConfiguration").Bind(eventHandlersConfiguration); - } + section.Bind(eventHandlersConfiguration); } this.AddSingleton(); this.AddSingleton(); @@ -37,8 +33,8 @@ public DomainEventHandlerRegistry() return handler; }); - this.For().Use().Named("Main") + this.For().Use() .Ctor>().Is(eventHandlersConfiguration).Singleton(); this.AddSingleton, DomainEventFactory>(); } -} \ No newline at end of file +} diff --git a/MessagingService/Bootstrapper/MessagingProxyRegistry.cs b/MessagingService/Bootstrapper/MessagingProxyRegistry.cs index 493aa4c..d2e17d9 100644 --- a/MessagingService/Bootstrapper/MessagingProxyRegistry.cs +++ b/MessagingService/Bootstrapper/MessagingProxyRegistry.cs @@ -21,8 +21,11 @@ [ExcludeFromCodeCoverage] public class MessagingProxyRegistry : ServiceRegistry { - public MessagingProxyRegistry() + private readonly IConfiguration configuration; + + public MessagingProxyRegistry(IConfiguration configuration) { + this.configuration = configuration; this.AddHttpContextAccessor(); this.RegisterEmailProxy(); @@ -31,10 +34,10 @@ public MessagingProxyRegistry() private void RegisterEmailProxy() { // read the config setting - String emailProxy = ConfigurationReader.GetValue("AppSettings", "EmailProxy"); + String emailProxy = this.configuration.GetValue("AppSettings:EmailProxy"); if (emailProxy == "Smtp2Go") { - Smtp2GConfig config = Startup.Configuration.GetSection("AppSettings:Smtp2Go").Get() ?? throw new InvalidOperationException("Smtp2Go config missing"); + Smtp2GConfig config = this.configuration.GetSection("AppSettings:Smtp2Go").Get() ?? throw new InvalidOperationException("Smtp2Go config missing"); this.AddSingleton(config); this.RegisterHttpClient(); } @@ -45,10 +48,10 @@ private void RegisterEmailProxy() { private void RegisterSMSProxy() { // read the config setting - String smsProxy = ConfigurationReader.GetValue("AppSettings", "SMSProxy"); + String smsProxy = this.configuration.GetValue("AppSettings:SMSProxy"); if (smsProxy == "TheSMSWorks") { - SmsWorksConfig config = Startup.Configuration.GetSection("AppSettings:TheSmsWorks").Get() ?? throw new InvalidOperationException("SmsWorks config missing"); + SmsWorksConfig config = this.configuration.GetSection("AppSettings:TheSmsWorks").Get() ?? throw new InvalidOperationException("SmsWorks config missing"); this.AddSingleton(config); this.RegisterHttpClient(); } @@ -56,4 +59,4 @@ private void RegisterSMSProxy() { this.AddSingleton(); } } -} \ No newline at end of file +} diff --git a/MessagingService/Bootstrapper/MiddlewareRegistry.cs b/MessagingService/Bootstrapper/MiddlewareRegistry.cs index 6d9c744..fb4601d 100644 --- a/MessagingService/Bootstrapper/MiddlewareRegistry.cs +++ b/MessagingService/Bootstrapper/MiddlewareRegistry.cs @@ -1,11 +1,11 @@ -using KurrentDB.Client; +using HealthMonitoring.Client; +using KurrentDB.Client; using Microsoft.Extensions.Logging; -using Microsoft.OpenApi; +using Microsoft.AspNetCore.OpenApi; using Shared.Authorisation; using Shared.Serialisation; using System; using System.Text.Json; -using HealthMonitoring.Client; namespace MessagingService.Bootstrapper { @@ -16,22 +16,22 @@ namespace MessagingService.Bootstrapper using Microsoft.Extensions.Diagnostics.HealthChecks; using OpenIddict.Validation.AspNetCore; using Shared.EventStore.Extensions; - using Shared.General; using Shared.Middleware; - using Swashbuckle.AspNetCore.Filters; using System; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net.Http; + using TransactionProcessing.ApiDefaults; [ExcludeFromCodeCoverage] public class MiddlewareRegistry : ServiceRegistry { - internal String GetSecurityConfigSetting(String settingName) => ConfigurationReader.GetValue("SecurityConfiguration", settingName); + private readonly IConfiguration configuration; - public MiddlewareRegistry() { - - String connectionString = Startup.Configuration.GetValue("EventStoreSettings:ConnectionString"); + public MiddlewareRegistry(IConfiguration configuration) + { + this.configuration = configuration; + String connectionString = configuration.GetValue("EventStoreSettings:ConnectionString"); KurrentDBClientSettings eventStoreConnectionSettings = KurrentDBClientSettings.Create(connectionString); this.AddHealthChecks() @@ -43,11 +43,10 @@ public MiddlewareRegistry() { this.AddControllers(); this.ConfigureAuthentication(); - this.ConfigureSwagger(); - bool logRequests = ConfigurationReader.GetValueOrDefault("MiddlewareLogging", "LogRequests", true); - bool logResponses = ConfigurationReader.GetValueOrDefault("MiddlewareLogging", "LogResponses", true); - LogLevel middlewareLogLevel = ConfigurationReader.GetValueOrDefault("MiddlewareLogging", "MiddlewareLogLevel", LogLevel.Warning); + bool logRequests = configuration.GetValue("MiddlewareLogging:LogRequests", true); + bool logResponses = configuration.GetValue("MiddlewareLogging:LogResponses", true); + LogLevel middlewareLogLevel = configuration.GetValue("MiddlewareLogging:MiddlewareLogLevel", LogLevel.Warning); RequestResponseMiddlewareLoggingConfig config = new(middlewareLogLevel, logRequests, logResponses); @@ -55,7 +54,19 @@ public MiddlewareRegistry() { this.ConfigureHttpJsonOptions(jsonOptions => JsonSerializerConfiguration.ConfigureMinimalApi(jsonOptions.SerializerOptions)); - this.AddHealthMonitoringRegistration(Startup.Configuration); + this.AddHealthMonitoringRegistration(configuration); + + this.AddApiDefaults(options => + { + options.Title = "Messaging Service"; + options.Description = "API to allow email and sms integrations"; + }); + + this.AddOpenApi(options => + { + options.AddSchemaTransformer(new DtoXmlDocumentationSchemaTransformer()); + options.AddOperationTransformer(new MessagingRequestExamplesOperationTransformer()); + }); } private void ConfigureAuthentication() { @@ -68,7 +79,7 @@ private void ConfigureAuthentication() { .AddValidation(options => { // Same as your Authority - options.SetIssuer(new Uri(GetSecurityConfigSetting("Authority"))); + options.SetIssuer(new Uri(this.configuration.GetValue("SecurityConfiguration:Authority"))); // Enables discovery and HTTP backchannel support options.UseSystemNetHttp() @@ -83,7 +94,7 @@ private void ConfigureAuthentication() { options.UseAspNetCore(); // Optionally set expected audience(s): - options.AddAudiences(GetSecurityConfigSetting("ApiName")); + options.AddAudiences(this.configuration.GetValue("SecurityConfiguration:ApiName")); }); @@ -93,38 +104,6 @@ private void ConfigureAuthentication() { this.AddClientCredentialsOnlyPolicy(); this.AddClientCredentialsHandler(); } - private void ConfigureSwagger() { - - this.AddSwaggerGen(c => - { - c.SwaggerDoc("v1", new OpenApiInfo - { - Title = "Messaging API", - Version = "1.0", - Description = "A REST Api to manage sending of various messages over different formats, currently only Email and SMS are supported.", - Contact = new OpenApiContact - { - Name = "Stuart Ferguson", - Email = "golfhandicapping@btinternet.com" - } - }); - // add a custom operation filter which sets default values - c.OperationFilter(); - c.ExampleFilters(); - - //Locate the XML files being generated by ASP.NET... - var directory = new DirectoryInfo(AppContext.BaseDirectory); - var xmlFiles = directory.GetFiles("*.xml"); - - //... and tell Swagger to use those XML comments. - foreach (FileInfo fileInfo in xmlFiles) - { - c.IncludeXmlComments(fileInfo.FullName); - } - }); - - this.AddSwaggerExamplesFromAssemblyOf(); - } } } diff --git a/MessagingService/Bootstrapper/RepositoryRegistry.cs b/MessagingService/Bootstrapper/RepositoryRegistry.cs index 1368478..500a0ce 100644 --- a/MessagingService/Bootstrapper/RepositoryRegistry.cs +++ b/MessagingService/Bootstrapper/RepositoryRegistry.cs @@ -19,9 +19,9 @@ [ExcludeFromCodeCoverage] public class RepositoryRegistry: ServiceRegistry { - public RepositoryRegistry() + public RepositoryRegistry(IConfiguration configuration) { - String connectionString = Startup.Configuration.GetValue("EventStoreSettings:ConnectionString"); + String connectionString = configuration.GetValue("EventStoreSettings:ConnectionString"); this.AddKurrentDBPersistentSubscriptionsClient(connectionString); this.AddKurrentDBProjectionManagementClient(connectionString); @@ -35,4 +35,4 @@ public RepositoryRegistry() this.AddSingleton>(cont => SubscriptionRepository.Create); } -} \ No newline at end of file +} diff --git a/MessagingService/Common/DtoXmlDocumentationSchemaTransformer.cs b/MessagingService/Common/DtoXmlDocumentationSchemaTransformer.cs new file mode 100644 index 0000000..652b2e3 --- /dev/null +++ b/MessagingService/Common/DtoXmlDocumentationSchemaTransformer.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; +using System; + +namespace MessagingService.Common; + +/// +/// Adds DTO XML documentation comments to generated OpenAPI schemas and properties. +/// +internal sealed class DtoXmlDocumentationSchemaTransformer : IOpenApiSchemaTransformer +{ + private readonly IReadOnlyDictionary summaries; + + public DtoXmlDocumentationSchemaTransformer() + { + string documentationPath = Path.Combine(AppContext.BaseDirectory, "MessagingService.DataTransferObjects.xml"); + this.summaries = LoadSummaries(documentationPath); + } + + public Task TransformAsync(OpenApiSchema schema, + OpenApiSchemaTransformerContext context, + CancellationToken cancellationToken) + { + if (context.JsonPropertyInfo?.AttributeProvider is PropertyInfo property) + { + string propertyMemberName = $"P:{property.DeclaringType?.FullName}.{property.Name}"; + if (this.summaries.TryGetValue(propertyMemberName, out string? propertySummary)) + { + schema.Description = propertySummary; + } + } + + Type? schemaType = context.JsonTypeInfo?.Type; + if (schemaType is not null) + { + string typeMemberName = $"T:{schemaType.FullName}"; + if (this.summaries.TryGetValue(typeMemberName, out string? typeSummary)) + { + schema.Description ??= typeSummary; + } + } + + return Task.CompletedTask; + } + + private static IReadOnlyDictionary LoadSummaries(string documentationPath) + { + if (File.Exists(documentationPath) == false) + { + return new Dictionary(); + } + + XDocument document = XDocument.Load(documentationPath); + return document.Descendants("member") + .Select(member => new + { + Name = (string?)member.Attribute("name"), + Summary = member.Element("summary")?.Value.Trim() + }) + .Where(member => string.IsNullOrWhiteSpace(member.Name) == false + && string.IsNullOrWhiteSpace(member.Summary) == false) + .ToDictionary(member => member.Name!, member => member.Summary!); + } +} diff --git a/MessagingService/Common/Extensions.cs b/MessagingService/Common/Extensions.cs index ce9bd93..562541c 100644 --- a/MessagingService/Common/Extensions.cs +++ b/MessagingService/Common/Extensions.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Threading; +using System.Threading.Tasks; using EventStore.Client; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; @@ -47,29 +48,33 @@ public static class Extensions } }; - public static void PreWarm(this IApplicationBuilder applicationBuilder) + public static async Task PreWarm(IConfiguration configuration, + IServiceProvider services, + CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); TypeProvider.LoadDomainEventsTypeDynamically(); - IConfigurationSection subscriptionConfigSection = Startup.Configuration.GetSection("AppSettings:SubscriptionConfiguration"); + IConfigurationSection subscriptionConfigSection = configuration.GetSection("AppSettings:SubscriptionConfiguration"); SubscriptionWorkersRoot subscriptionWorkersRoot = new(); subscriptionConfigSection.Bind(subscriptionWorkersRoot); - IDomainEventHandlerResolver mainEventHandlerResolver = Startup.Container.GetInstance("Main"); + IDomainEventHandlerResolver mainEventHandlerResolver = services.GetRequiredService(); Dictionary eventHandlerResolvers = new() { {"Main", mainEventHandlerResolver} }; - Func subscriptionRepositoryResolver = Startup.Container.GetInstance>(); + Func subscriptionRepositoryResolver = services.GetRequiredService>(); - String connectionString = Startup.Configuration.GetValue("EventStoreSettings:ConnectionString"); + String connectionString = configuration.GetValue("EventStoreSettings:ConnectionString"); - applicationBuilder.ConfigureSubscriptionService(subscriptionWorkersRoot, - connectionString, - eventHandlerResolvers, - Extensions.log, - subscriptionRepositoryResolver).Wait(CancellationToken.None); + IApplicationBuilder applicationBuilder = new ApplicationBuilder(services); + await applicationBuilder.ConfigureSubscriptionService(subscriptionWorkersRoot, + connectionString, + eventHandlerResolvers, + Extensions.log, + subscriptionRepositoryResolver); } -} \ No newline at end of file +} diff --git a/MessagingService/Common/MessagingRequestExamplesOperationTransformer.cs b/MessagingService/Common/MessagingRequestExamplesOperationTransformer.cs new file mode 100644 index 0000000..2556081 --- /dev/null +++ b/MessagingService/Common/MessagingRequestExamplesOperationTransformer.cs @@ -0,0 +1,74 @@ +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace MessagingService.Common; + +/// +/// Adds representative JSON request examples to the messaging endpoints. +/// +internal sealed class MessagingRequestExamplesOperationTransformer : IOpenApiOperationTransformer +{ + private const string SendEmailExample = """ + { + "messageId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "body": "Your appointment is confirmed for tomorrow at 10:00.", + "connectionIdentifier": "a8098c1a-f86e-11da-bd1a-00112444be1e", + "fromAddress": "notifications@example.test", + "isHtml": false, + "subject": "Appointment confirmed", + "toAddresses": ["recipient@example.test"], + "emailAttachments": [] + } + """; + + private const string ResendEmailExample = """ + { + "messageId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "connectionIdentifier": "a8098c1a-f86e-11da-bd1a-00112444be1e" + } + """; + + private const string SendSmsExample = """ + { + "connectionIdentifier": "a8098c1a-f86e-11da-bd1a-00112444be1e", + "destination": "+447700900123", + "message": "Your appointment is confirmed for tomorrow at 10:00.", + "messageId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "sender": "Example Service" + } + """; + + private const string ResendSmsExample = """ + { + "messageId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "connectionIdentifier": "a8098c1a-f86e-11da-bd1a-00112444be1e" + } + """; + + public Task TransformAsync(OpenApiOperation operation, + OpenApiOperationTransformerContext context, + CancellationToken cancellationToken) + { + string path = context.Description.RelativePath?.Trim('/') ?? string.Empty; + string? example = (context.Description.HttpMethod, path) switch + { + ("POST", "api/email") => SendEmailExample, + ("POST", "api/email/resend") => ResendEmailExample, + ("POST", "api/sms") => SendSmsExample, + ("POST", "api/sms/resend") => ResendSmsExample, + _ => null + }; + + if (example is not null + && operation.RequestBody is OpenApiRequestBody requestBody + && requestBody.Content.TryGetValue("application/json", out OpenApiMediaType? mediaType)) + { + mediaType.Example = JsonNode.Parse(example); + } + + return Task.CompletedTask; + } +} diff --git a/MessagingService/Common/RequestExamples/ExampleData.cs b/MessagingService/Common/RequestExamples/ExampleData.cs deleted file mode 100644 index 2595c2f..0000000 --- a/MessagingService/Common/RequestExamples/ExampleData.cs +++ /dev/null @@ -1,86 +0,0 @@ -namespace MessagingService.Common.RequestExamples -{ - using System; - using System.Diagnostics.CodeAnalysis; - - /// - /// - /// - [ExcludeFromCodeCoverage] - internal static class ExampleData - { - #region Fields - - /// - /// The connection identifier - /// - internal static Guid ConnectionIdentifier = Guid.Parse("A6080219-E243-48F8-A6C3-D79610A74A5B"); - - /// - /// The email message from address - /// - internal static String EmailMessageFromAddress = "fromaddress@exampleemail.com"; - - /// - /// The email message HTML body - /// - internal static String EmailMessageHtmlBody = "

This is a test message body

"; - - /// - /// The email message identifier - /// - internal static Guid EmailMessageId = Guid.Parse("63BDE20F-28E0-4698-AF46-923A08198994"); - - /// - /// The email message is HTML - /// - internal static Boolean EmailMessageIsHtml = true; - - /// - /// The email message plain text body - /// - internal static String EmailMessagePlainTextBody = "This is a test message body"; - - /// - /// The email message plain text is HTML - /// - internal static Boolean EmailMessagePlainTextIsHtml = false; - - /// - /// The email message subject - /// - internal static String EmailMessageSubject = "Email Subject"; - - /// - /// The email message to address1 - /// - internal static String EmailMessageToAddress1 = "toaddress1@exampleemail.com"; - - /// - /// The email message to address2 - /// - internal static String EmailMessageToAddress2 = "toaddress2@exampleemail.com"; - - /// - /// The SMS message destination - /// - internal static String SMSMessageDestination = "07123456789"; - - /// - /// The SMS message identifier - /// - internal static Guid SMSMessageId = Guid.Parse("D38E20B1-64F1-4217-B192-24123862FE10"); - - /// - /// The SMS message message - /// - internal static String SMSMessageMessage = "Test SMS Message"; - - /// - /// The SMS message sender - /// - internal static String SMSMessageSender = "07000000001"; - - #endregion - } -} \ No newline at end of file diff --git a/MessagingService/Common/RequestExamples/SendEmailRequestExample.cs b/MessagingService/Common/RequestExamples/SendEmailRequestExample.cs deleted file mode 100644 index 8d0685f..0000000 --- a/MessagingService/Common/RequestExamples/SendEmailRequestExample.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace MessagingService.Common.RequestExamples -{ - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using DataTransferObjects; - using Swashbuckle.AspNetCore.Filters; - - /// - /// - /// - /// - [ExcludeFromCodeCoverage] - public class SendEmailRequestExample : IMultipleExamplesProvider - { - #region Methods - - /// - /// Gets the examples. - /// - /// - public IEnumerable> GetExamples() - { - SendEmailRequest htmlEmailRequest = new() - { - Body = ExampleData.EmailMessageHtmlBody, - ConnectionIdentifier = ExampleData.ConnectionIdentifier, - FromAddress = ExampleData.EmailMessageFromAddress, - ToAddresses = new List - { - ExampleData.EmailMessageToAddress1, - ExampleData.EmailMessageToAddress2 - }, - IsHtml = ExampleData.EmailMessageIsHtml, - MessageId = ExampleData.EmailMessageId, - Subject = ExampleData.EmailMessageSubject - }; - - SendEmailRequest plainTextEmailRequest = new() - { - Body = ExampleData.EmailMessagePlainTextBody, - ConnectionIdentifier = ExampleData.ConnectionIdentifier, - FromAddress = ExampleData.EmailMessageFromAddress, - ToAddresses = new List - { - ExampleData.EmailMessageToAddress1, - ExampleData.EmailMessageToAddress2 - }, - IsHtml = ExampleData.EmailMessagePlainTextIsHtml, - MessageId = ExampleData.EmailMessageId, - Subject = ExampleData.EmailMessageSubject - }; - List> examples = new(); - examples.Add(new SwaggerExample - { - Name = "Html Email Request", - Value = htmlEmailRequest - }); - examples.Add(new SwaggerExample - { - Name = "Plan Text Email Request", - Value = plainTextEmailRequest - }); - return examples; - } - - #endregion - } -} \ No newline at end of file diff --git a/MessagingService/Common/RequestExamples/SendEmailResponseExample.cs b/MessagingService/Common/RequestExamples/SendEmailResponseExample.cs deleted file mode 100644 index 86f9a81..0000000 --- a/MessagingService/Common/RequestExamples/SendEmailResponseExample.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace MessagingService.Common.RequestExamples -{ - using System.Diagnostics.CodeAnalysis; - using DataTransferObjects; - using Swashbuckle.AspNetCore.Filters; - - /// - /// - /// - /// - [ExcludeFromCodeCoverage] - public class SendEmailResponseExample : IExamplesProvider - { - #region Methods - - /// - /// Gets the examples. - /// - /// - public SendEmailResponse GetExamples() - { - return new SendEmailResponse - { - MessageId = ExampleData.EmailMessageId - }; - } - - #endregion - } -} \ No newline at end of file diff --git a/MessagingService/Common/RequestExamples/SendSMSRequestExample.cs b/MessagingService/Common/RequestExamples/SendSMSRequestExample.cs deleted file mode 100644 index 09623a6..0000000 --- a/MessagingService/Common/RequestExamples/SendSMSRequestExample.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace MessagingService.Common.RequestExamples -{ - using System.Diagnostics.CodeAnalysis; - using DataTransferObjects; - using Swashbuckle.AspNetCore.Filters; - - /// - /// - /// - /// - [ExcludeFromCodeCoverage] - public class SendSMSRequestExample : IExamplesProvider - { - /// - /// Gets the examples. - /// - /// - public SendSMSRequest GetExamples() - { - return new SendSMSRequest - { - ConnectionIdentifier = ExampleData.ConnectionIdentifier, - MessageId = ExampleData.SMSMessageId, - Destination = ExampleData.SMSMessageDestination, - Message = ExampleData.SMSMessageMessage, - Sender = ExampleData.SMSMessageSender - }; - } - } -} \ No newline at end of file diff --git a/MessagingService/Common/RequestExamples/SendSMSResponseExample.cs b/MessagingService/Common/RequestExamples/SendSMSResponseExample.cs deleted file mode 100644 index 3c5a635..0000000 --- a/MessagingService/Common/RequestExamples/SendSMSResponseExample.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace MessagingService.Common.RequestExamples -{ - using System.Diagnostics.CodeAnalysis; - using DataTransferObjects; - using Swashbuckle.AspNetCore.Filters; - - /// - /// - /// - /// - [ExcludeFromCodeCoverage] - public class SendSMSResponseExample : IExamplesProvider - { - #region Methods - - /// - /// Gets the examples. - /// - /// - public SendSMSResponse GetExamples() - { - return new SendSMSResponse - { - MessageId = ExampleData.SMSMessageId - }; - } - - #endregion - } -} \ No newline at end of file diff --git a/MessagingService/Common/SubscriptionPreWarmHostedService.cs b/MessagingService/Common/SubscriptionPreWarmHostedService.cs new file mode 100644 index 0000000..f9099b8 --- /dev/null +++ b/MessagingService/Common/SubscriptionPreWarmHostedService.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace MessagingService.Common; + +public sealed class SubscriptionPreWarmHostedService : IHostedLifecycleService +{ + private readonly IConfiguration configuration; + private readonly IServiceProvider services; + + public SubscriptionPreWarmHostedService(IConfiguration configuration, IServiceProvider services) + { + this.configuration = configuration; + this.services = services; + } + + public async Task StartingAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + await Extensions.PreWarm(this.configuration, this.services, cancellationToken); + } + + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/MessagingService/Common/SwaggerDefaultValues.cs b/MessagingService/Common/SwaggerDefaultValues.cs deleted file mode 100644 index 6e86ce3..0000000 --- a/MessagingService/Common/SwaggerDefaultValues.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.OpenApi; - -namespace MessagingService.Common -{ - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using Microsoft.AspNetCore.Mvc.ApiExplorer; - using Swashbuckle.AspNetCore.SwaggerGen; - - /// - /// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter. - /// - /// This is only required due to bugs in the . - /// Once they are fixed and published, this class can be removed. - [ExcludeFromCodeCoverage] - public class SwaggerDefaultValues : IOperationFilter - { - #region Methods - - /// - /// Applies the filter to the specified operation using the given context. - /// - /// The operation to apply the filter to. - /// The current operation filter context. - public void Apply(OpenApiOperation operation, - OperationFilterContext context) - { - ApiDescription apiDescription = context.ApiDescription; - - if (operation.Parameters == null) - { - return; - } - - foreach (OpenApiParameter parameter in operation.Parameters) - { - ApiParameterDescription description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name); - - if (parameter.Description == null) - { - parameter.Description = description.ModelMetadata?.Description; - } - - parameter.Required |= description.IsRequired; - } - } - - #endregion - } -} \ No newline at end of file diff --git a/MessagingService/Common/SwaggerJsonConverter.cs b/MessagingService/Common/SwaggerJsonConverter.cs deleted file mode 100644 index 03c4fd8..0000000 --- a/MessagingService/Common/SwaggerJsonConverter.cs +++ /dev/null @@ -1,80 +0,0 @@ -namespace MessagingService.Common -{ - using System; - using System.Diagnostics.CodeAnalysis; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; - - /// - /// - /// - /// - [ExcludeFromCodeCoverage] - public class SwaggerJsonConverter : JsonConverter - { - #region Properties - - /// - /// Gets a value indicating whether this can read JSON. - /// - /// - /// true if this can read JSON; otherwise, false. - /// - public override Boolean CanRead => false; - - #endregion - - #region Methods - - /// - /// Determines whether this instance can convert the specified object type. - /// - /// Type of the object. - /// - /// true if this instance can convert the specified object type; otherwise, false. - /// - public override Boolean CanConvert(Type objectType) - { - return true; - } - - /// - /// Reads the JSON representation of the object. - /// - /// The to read from. - /// Type of the object. - /// The existing value of object being read. - /// The calling serializer. - /// - /// The object value. - /// - /// - /// - public override Object ReadJson(JsonReader reader, - Type objectType, - Object existingValue, - JsonSerializer serializer) - { - throw new NotImplementedException(); - } - - /// - /// Writes the JSON representation of the object. - /// - /// The to write to. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, - Object value, - JsonSerializer serializer) - { - // Disable sending the $type in the serialized json - serializer.TypeNameHandling = TypeNameHandling.None; - - JToken t = JToken.FromObject(value); - t.WriteTo(writer); - } - - #endregion - } -} \ No newline at end of file diff --git a/MessagingService/Endpoints/EmailEndpoints.cs b/MessagingService/Endpoints/EmailEndpoints.cs index 94271fa..80f3675 100644 --- a/MessagingService/Endpoints/EmailEndpoints.cs +++ b/MessagingService/Endpoints/EmailEndpoints.cs @@ -21,12 +21,14 @@ public static IEndpointRouteBuilder MapEmailEndpoints(this IEndpointRouteBuilder .RequireAuthorization(AuthorizationExtensions.PolicyNames.ClientCredentialsOnlyPolicy) .WithName("Send Email") .WithSummary("Sends an email") + .WithDescription("Submits an email for delivery. If messageId is omitted or null, the service generates a message identifier. Attachments may be included in the request.") .WithStandardProduces(StatusCodes.Status201Created); emailGroup.MapPost("/resend", EmailHandlers.ResendEmail) .RequireAuthorization(AuthorizationExtensions.PolicyNames.ClientCredentialsOnlyPolicy) .WithName("Resend Email") .WithSummary("Resends an email by id") + .WithDescription("Requests delivery of an existing email message, identified by messageId and connectionIdentifier.") .WithStandardProduces< ErrorResponse>(StatusCodes.Status202Accepted); return endpoints; diff --git a/MessagingService/Endpoints/SMSEndpoints.cs b/MessagingService/Endpoints/SMSEndpoints.cs index 8ee3527..95716a5 100644 --- a/MessagingService/Endpoints/SMSEndpoints.cs +++ b/MessagingService/Endpoints/SMSEndpoints.cs @@ -20,14 +20,16 @@ public static IEndpointRouteBuilder MapSMSEndpoints(this IEndpointRouteBuilder e .RequireAuthorization(AuthorizationExtensions.PolicyNames.ClientCredentialsOnlyPolicy) .WithName("Send SMS") .WithSummary("Sends an SMS message") + .WithDescription("Submits an SMS message for delivery. If messageId is omitted or null, the service generates a message identifier.") .WithStandardProduces(StatusCodes.Status201Created); smsGroup.MapPost("/resend", SMSHandlers.ResendSMS) .RequireAuthorization(AuthorizationExtensions.PolicyNames.ClientCredentialsOnlyPolicy) .WithName("Resend SMS") .WithSummary("Resends an SMS message by id") + .WithDescription("Requests delivery of an existing SMS message, identified by messageId and connectionIdentifier.") .WithStandardProduces< ErrorResponse>(StatusCodes.Status202Accepted); return endpoints; } -} \ No newline at end of file +} diff --git a/MessagingService/MessagingService.csproj b/MessagingService/MessagingService.csproj index 5e58f1b..70616be 100644 --- a/MessagingService/MessagingService.csproj +++ b/MessagingService/MessagingService.csproj @@ -4,10 +4,8 @@ net10.0 Linux true - - - - D:\Projects\StuartFerguson\Messaging\MessagingService\MessagingService.xml + true + $(InterceptorsNamespaces);Microsoft.AspNetCore.OpenApi.Generated @@ -30,15 +28,10 @@ - - - - - - + diff --git a/MessagingService/Program.cs b/MessagingService/Program.cs index 98c2210..a0da357 100644 --- a/MessagingService/Program.cs +++ b/MessagingService/Program.cs @@ -1,117 +1,201 @@ +using Lamar; +using Lamar.Microsoft.DependencyInjection; +using MessagingService.Bootstrapper; +using MessagingService.Common; +using MessagingService.Endpoints; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Metadata; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Json; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using NLog; using NLog.Extensions.Logging; using Sentry.Extensibility; +using Shared.Extensions; +using Shared.General; using Shared.Logger; using Shared.Middleware; +using Shared.Serialisation; using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Reflection; +using TransactionProcessing.ApiDefaults; -namespace MessagingService +namespace MessagingService; + +[ExcludeFromCodeCoverage] +public static class Program { - using Lamar.Microsoft.DependencyInjection; - using NLog; - using Shared.General; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - - [ExcludeFromCodeCoverage] - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) + ConfigureLogging(); + + string assemblyDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + ?? AppContext.BaseDirectory; + WebApplicationBuilder builder = WebApplication.CreateBuilder(new WebApplicationOptions { - Program.CreateHostBuilder(args).Build() .Run(); - } + Args = args, + ContentRootPath = assemblyDirectory + }); + + builder.Host.UseWindowsService(); + builder.Logging.AddConsole(); + builder.Logging.AddNLog(); + + ConfigureConfiguration(builder.Configuration, builder.Environment, assemblyDirectory); + ConfigurationReader.Initialise(builder.Configuration); + ConfigureSentry(builder); + + builder.Services.AddHostedService(); - public static IHostBuilder CreateHostBuilder(string[] args) + builder.Host.UseLamar((_, services) => { - //At this stage, we only need our hosting file for ip and ports - FileInfo fi = new(System.Reflection.Assembly.GetExecutingAssembly().Location); - - ConfigureLogging(); - - IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args); - hostBuilder.UseWindowsService(); - hostBuilder.UseLamar(); - hostBuilder.ConfigureLogging(logging => - { - logging.AddConsole(); - logging.AddNLog(); - - }); - hostBuilder.ConfigureWebHostDefaults(webBuilder => { ConfigureWebHost(webBuilder, fi.Directory.FullName); }); - - - return hostBuilder; - } + IConfiguration configuration = builder.Configuration; + services.IncludeRegistry(new MediatorRegistry()); + services.IncludeRegistry(new RepositoryRegistry(configuration)); + services.IncludeRegistry(new MiddlewareRegistry(configuration)); + services.IncludeRegistry(new DomainServiceRegistry()); + services.IncludeRegistry(new DomainEventHandlerRegistry(configuration)); + services.IncludeRegistry(new MessagingProxyRegistry(configuration)); + services.IncludeRegistry(new SerialiserRegistry()); + }); + + WebApplication app = builder.Build(); + ConfigureApplication(app); + app.Run(); + } - private static void ConfigureLogging() { - String contentRoot = Directory.GetCurrentDirectory(); - String nlogConfigPath = Path.Combine(contentRoot, "nlog.config"); + private static void ConfigureLogging() + { + string nlogConfigPath = Path.Combine(Directory.GetCurrentDirectory(), "nlog.config"); - LogManager.Setup(b => + LogManager.Setup(b => + { + b.SetupLogFactory(setup => { - b.SetupLogFactory(setup => - { - setup.AddCallSiteHiddenAssembly(typeof(NlogLogger).Assembly); - setup.AddCallSiteHiddenAssembly(typeof(Shared.Logger.Logger).Assembly); - setup.AddCallSiteHiddenAssembly(typeof(TenantMiddleware).Assembly); - }); - b.LoadConfigurationFromFile(nlogConfigPath); + setup.AddCallSiteHiddenAssembly(typeof(NlogLogger).Assembly); + setup.AddCallSiteHiddenAssembly(typeof(Shared.Logger.Logger).Assembly); + setup.AddCallSiteHiddenAssembly(typeof(TenantMiddleware).Assembly); }); + b.LoadConfigurationFromFile(nlogConfigPath); + }); + } + + private static void ConfigureConfiguration(IConfigurationManager configuration, + IHostEnvironment environment, + string basePath) + { + RemoveDefaultAppSettingsSources(configuration, environment.EnvironmentName); + + configuration.SetBasePath(basePath) + .AddJsonFile("hosting.json", optional: true) + .AddJsonFile($"hosting.{environment.EnvironmentName}.json", optional: true) + .AddJsonFile("/home/txnproc/config/appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"/home/txnproc/config/appsettings.{environment.EnvironmentName}.json", optional: true, reloadOnChange: true) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{environment.EnvironmentName}.json", optional: true, reloadOnChange: true) + .AddJsonFile("/home/txnproc/config/appsettings.local.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddInMemoryCollection(new Dictionary + { + ["HealthMonitoring:Service:Version"] = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0" + }); + } + + private static void RemoveDefaultAppSettingsSources(IConfigurationManager configuration, + string environmentName) + { + string environmentSettingsPath = $"appsettings.{environmentName}.json"; + + for (int index = configuration.Sources.Count - 1; index >= 0; index--) + { + if (configuration.Sources[index] is JsonConfigurationSource jsonSource + && (jsonSource.Path == "appsettings.json" || jsonSource.Path == environmentSettingsPath)) + { + configuration.Sources.RemoveAt(index); + } } + } + + private static void ConfigureSentry(WebApplicationBuilder builder) + { + IConfiguration configuration = builder.Configuration; + IHostEnvironment environment = builder.Environment; + IConfigurationSection sentrySection = configuration.GetSection("SentryConfiguration"); - private static void ConfigureWebHost(IWebHostBuilder webBuilder, - String basePath) { - webBuilder.ConfigureAppConfiguration((context, configBuilder) => + if (sentrySection.Exists() && environment.IsDevelopment() == false) + { + builder.WebHost.UseSentry(options => { - var env = context.HostingEnvironment; - - configBuilder.SetBasePath(basePath) - .AddJsonFile("hosting.json", optional: true) - .AddJsonFile($"hosting.{env.EnvironmentName}.json", optional: true) - .AddJsonFile("/home/txnproc/config/appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"/home/txnproc/config/appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true) - .AddJsonFile($"/home/txnproc/config/appsettings.local.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables().AddInMemoryCollection(new Dictionary - { - ["HealthMonitoring:Service:Version"] = Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0.0.0.0" - }); - - // Build a snapshot of configuration so we can use it immediately (e.g. for Sentry) - var builtConfig = configBuilder.Build(); - - // Keep existing static usage (if you must), and initialise the ConfigurationReader now. - Startup.Configuration = builtConfig; - ConfigurationReader.Initialise(Startup.Configuration); - - // Configure Sentry on the webBuilder using the config snapshot. - var sentrySection = builtConfig.GetSection("SentryConfiguration"); - if (sentrySection.Exists()) - { - // Replace the condition below if you intended to only enable Sentry in certain environments. - if (env.IsDevelopment() == false) - { - webBuilder.UseSentry(o => - { - o.Dsn = builtConfig["SentryConfiguration:Dsn"]; - o.SendDefaultPii = true; - o.MaxRequestBodySize = RequestSize.Always; - o.CaptureBlockingCalls = ConfigurationReader.GetValueOrDefault("SentryConfiguration", "CaptureBlockingCalls", false); - o.IncludeActivityData = ConfigurationReader.GetValueOrDefault("SentryConfiguration", "IncludeActivityData", false); - o.Release = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; - }); - } - } + options.Dsn = configuration["SentryConfiguration:Dsn"]; + options.SendDefaultPii = true; + options.MaxRequestBodySize = RequestSize.Always; + options.CaptureBlockingCalls = configuration.GetValue("SentryConfiguration:CaptureBlockingCalls", false); + options.IncludeActivityData = configuration.GetValue("SentryConfiguration:IncludeActivityData", false); + options.Release = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown"; }); + } + } + + private static void ConfigureApplication(WebApplication app) + { + if (app.Environment.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + Microsoft.Extensions.Logging.ILogger logger = app.Services.GetRequiredService().CreateLogger("MessagingService"); + Shared.Logger.Logger.Initialise(logger); + ((IConfigurationRoot)app.Configuration).LogConfiguration(Shared.Logger.Logger.LogWarning); - webBuilder.UseStartup(); - webBuilder.UseKestrel(); + IStringSerialiser serialiser = app.Services.GetRequiredService(); + StringSerialiser.Initialise(serialiser); + + foreach (KeyValuePair type in TypeMap.Map) + { + Shared.Logger.Logger.LogInformation($"Type name {type.Value} mapped to {type.Key.Name}"); } + + app.UseMiddleware(); + app.AddRequestResponseLogging(); + app.AddExceptionHandler(); + + app.UseRouting(); + app.UseAuthentication(); + app.UseAuthorization(); + + app.MapEmailEndpoints(); + app.MapSMSEndpoints(); + app.MapDomainEventEndpoint(); + + app.MapHealthChecks("health", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions + { + Predicate = _ => true, + ResponseWriter = Shared.HealthChecks.HealthCheckMiddleware.WriteResponse + }) + .WithName("HealthCheck") + .WithMetadata( + new EndpointSummaryMetadata("Checks the health of the Messaging Service"), + new EndpointDescriptionMetadata("Returns the health status of the service and its configured dependency checks.")); + app.MapHealthChecks("healthui", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions + { + Predicate = _ => true, + ResponseWriter = HealthChecks.UI.Client.UIResponseWriter.WriteHealthCheckUIResponse + }) + .WithName("HealthCheckUi") + .WithMetadata( + new EndpointSummaryMetadata("Returns health details for the health-check UI"), + new EndpointDescriptionMetadata("Returns the health-check data formatted for the health monitoring UI.")); + + app.UseApiDefaults(); } + + private sealed record EndpointSummaryMetadata(string Summary) : IEndpointSummaryMetadata; + + private sealed record EndpointDescriptionMetadata(string Description) : IEndpointDescriptionMetadata; } diff --git a/MessagingService/Startup.cs b/MessagingService/Startup.cs deleted file mode 100644 index 1f19dd0..0000000 --- a/MessagingService/Startup.cs +++ /dev/null @@ -1,108 +0,0 @@ -using MessagingService.Endpoints; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; - -namespace MessagingService -{ - using Bootstrapper; - using Common; - using HealthChecks.UI.Client; - using Lamar; - using Microsoft.AspNetCore.Diagnostics.HealthChecks; - using Shared.Extensions; - using Shared.General; - using Shared.Middleware; - using Shared.Serialisation; - using System.Diagnostics.CodeAnalysis; - - [ExcludeFromCodeCoverage] - public class Startup - { - public Startup(IWebHostEnvironment webHostEnvironment) - { - Startup.WebHostEnvironment = webHostEnvironment; - } - - public static IConfigurationRoot Configuration { get; set; } - - public static IWebHostEnvironment WebHostEnvironment { get; set; } - - public static Container Container; - - public void ConfigureContainer(ServiceRegistry services) - { - ConfigurationReader.Initialise(Startup.Configuration); - - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - services.IncludeRegistry(); - - Startup.Container = new Container(services); - - var serialiser = Container.GetRequiredService(); - StringSerialiser.Initialise(serialiser); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, - IHostApplicationLifetime lifetime, IHost host) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - Microsoft.Extensions.Logging.ILogger logger = loggerFactory.CreateLogger("MessagingService"); - - Shared.Logger.Logger.Initialise(logger); - Startup.Configuration.LogConfiguration(Shared.Logger.Logger.LogWarning); - - foreach (KeyValuePair type in TypeMap.Map) - { - Shared.Logger.Logger.LogInformation($"Type name {type.Value} mapped to {type.Key.Name}"); - } - - ConfigurationReader.Initialise(Startup.Configuration); - app.UseMiddleware(); - app.AddRequestResponseLogging(); - app.AddExceptionHandler(); - - app.UseRouting(); - app.UseAuthentication(); - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapEmailEndpoints(); - endpoints.MapSMSEndpoints(); - endpoints.MapDomainEventEndpoint(); - - endpoints.MapHealthChecks("health", new HealthCheckOptions() - { - Predicate = _ => true, - ResponseWriter = Shared.HealthChecks.HealthCheckMiddleware.WriteResponse - }); - endpoints.MapHealthChecks("healthui", new HealthCheckOptions() - { - Predicate = _ => true, - ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse - }); - }); - app.UseSwagger(); - - app.UseSwaggerUI(); - - app.PreWarm(); - } - } -} From 6f5845a1cf1f1f3de0da426450fba471fdc90f13 Mon Sep 17 00:00:00 2001 From: StuartFerguson Date: Wed, 21 Oct 2026 10:22:51 +0100 Subject: [PATCH 2/2] fix build issue --- MessagingService.IntegrationTests/Common/GenericSteps.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MessagingService.IntegrationTests/Common/GenericSteps.cs b/MessagingService.IntegrationTests/Common/GenericSteps.cs index 86cc8ac..abb003c 100644 --- a/MessagingService.IntegrationTests/Common/GenericSteps.cs +++ b/MessagingService.IntegrationTests/Common/GenericSteps.cs @@ -36,7 +36,7 @@ public async Task StartSystem() logger.Initialise(LogManager.GetLogger(scenarioName), scenarioName); LogManager.AddHiddenAssembly(typeof(NlogLogger).Assembly); - DockerServices dockerServices = DockerServices.SqlServer | DockerServices.MessagingService | DockerServices.EventStore | DockerServices.SecurityService; + DockerServices dockerServices = DockerServices.SqlServer | DockerServices.MessagingService | DockerServices.KurrentDb | DockerServices.SecurityService; this.TestingContext.DockerHelper = new(); this.TestingContext.DockerHelper.Logger = logger;