diff --git a/src/Infrastructure.Common/Recording/ForwardToAncillaryApiMetrics.cs b/src/Infrastructure.Common/Recording/ForwardToAncillaryApiMetrics.cs deleted file mode 100644 index 40a9fb40..00000000 --- a/src/Infrastructure.Common/Recording/ForwardToAncillaryApiMetrics.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Application.Common; -using Application.Interfaces.Services; -using Common.Recording; -using Domain.Interfaces.Services; -using Infrastructure.Web.Api.Common.Extensions; -using Infrastructure.Web.Api.Operations.Shared.Ancillary; -using Infrastructure.Web.Common.Clients; -using Infrastructure.Web.Interfaces.Clients; - -namespace Infrastructure.Common.Recording; - -/// -/// Provides a that forwards the measurement to the backend Ancillary API -/// -public class ForwardToAncillaryApiMetrics : IMetricReporter -{ - private readonly string _hmacSecret; - private readonly IServiceClient _serviceClient; - - public ForwardToAncillaryApiMetrics(IDependencyContainer container) : this( - new InterHostServiceClient(container.Resolve(), - container.Resolve().GetAncillaryApiHostBaseUrl()), - container.Resolve().GetAncillaryApiHostHmacAuthSecret()) - { - } - - private ForwardToAncillaryApiMetrics(IServiceClient serviceClient, string hmacSecret) - { - _serviceClient = serviceClient; - _hmacSecret = hmacSecret; - } - - public void Measure(string eventName, Dictionary? additional = null) - { - // TODO: If we are running on a BackEndForFrontEndWebHost we need to copy the bearer token from the cookie into the caller.Authorization - var caller = Caller.CreateAsAnonymous(); - var request = new RecordMeasureRequest - { - EventName = eventName, - Additional = additional! - }; - _serviceClient.PostAsync(caller, request, req => - { - req.SetHmacAuth(request, _hmacSecret); - req.SetRequestId(caller.ToCall()); - }).GetAwaiter().GetResult(); - } -} \ No newline at end of file diff --git a/src/Infrastructure.Common/Recording/ForwardToAncillaryApiUsages.cs b/src/Infrastructure.Common/Recording/ForwardToAncillaryApiUsages.cs deleted file mode 100644 index ef368c03..00000000 --- a/src/Infrastructure.Common/Recording/ForwardToAncillaryApiUsages.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Application.Common; -using Application.Interfaces.Services; -using Common; -using Common.Recording; -using Domain.Interfaces.Services; -using Infrastructure.Web.Api.Common.Extensions; -using Infrastructure.Web.Api.Operations.Shared.Ancillary; -using Infrastructure.Web.Common.Clients; -using Infrastructure.Web.Interfaces.Clients; - -namespace Infrastructure.Common.Recording; - -/// -/// Provides a that forwards the usage to the backend Ancillary API -/// -public class ForwardToAncillaryApiUsages : IUsageReporter -{ - private readonly string _hmacSecret; - private readonly IServiceClient _serviceClient; - - public ForwardToAncillaryApiUsages(IDependencyContainer container) : this( - new InterHostServiceClient(container.Resolve(), - container.Resolve().GetAncillaryApiHostBaseUrl()), - container.Resolve().GetAncillaryApiHostHmacAuthSecret()) - { - } - - private ForwardToAncillaryApiUsages(IServiceClient serviceClient, string hmacSecret) - { - _serviceClient = serviceClient; - _hmacSecret = hmacSecret; - } - - public void Track(ICallContext? call, string forId, string eventName, Dictionary? additional = null) - { - // TODO: If we are running on a BackEndForFrontEndWebHost we need to copy the bearer token from the cookie into the caller.Authorization - var caller = Caller.CreateAsCallerFromCall(call ?? CallContext.CreateUnknown()); - var request = new RecordUseRequest - { - EventName = eventName, - Additional = additional! - }; - _serviceClient.PostAsync(caller, request, req => - { - req.SetHmacAuth(request, _hmacSecret); - req.SetRequestId(caller.ToCall()); - }).GetAwaiter().GetResult(); - } -} \ No newline at end of file diff --git a/src/Infrastructure.Common/Recording/RecorderOptions.cs b/src/Infrastructure.Common/Recording/RecorderOptions.cs index 9690f040..1c6221fc 100644 --- a/src/Infrastructure.Common/Recording/RecorderOptions.cs +++ b/src/Infrastructure.Common/Recording/RecorderOptions.cs @@ -150,7 +150,6 @@ public enum MetricReporterOption { None = 0, Cloud = 1, - ForwardToAncillaryApi = 3 } /// @@ -160,5 +159,4 @@ public enum UsageReporterOption { None = 0, ReliableQueue = 1, - ForwardToAncillaryApi = 2 } \ No newline at end of file diff --git a/src/Infrastructure.Hosting.Common/Recording/HostRecorder.cs b/src/Infrastructure.Hosting.Common/Recording/HostRecorder.cs index daea7b79..13def9a2 100644 --- a/src/Infrastructure.Hosting.Common/Recording/HostRecorder.cs +++ b/src/Infrastructure.Hosting.Common/Recording/HostRecorder.cs @@ -67,9 +67,23 @@ private void Dispose(bool disposing) (_crashReporter as IDisposable)?.Dispose(); // ReSharper disable once SuspiciousTypeConversion.Global (_metricsReporter as IDisposable)?.Dispose(); + // ReSharper disable once SuspiciousTypeConversion.Global + (_usageReporter as IDisposable)?.Dispose(); } } + public override string ToString() + { + var builder = new StringBuilder(); + builder.AppendFormat("{0}: ", GetType().Name); + builder.AppendFormat("Crashes-> {0}, ", _crashReporter.GetType().Name); + builder.AppendFormat("Audits -> {0}, ", _auditReporter.GetType().Name); + builder.AppendFormat("Usages -> {0}, ", _usageReporter.GetType().Name); + builder.AppendFormat("Metrics -> {0}", _metricsReporter.GetType().Name); + + return builder.ToString(); + } + public void TraceDebug(ICallContext? context, string messageTemplate, params object[] templateArgs) { var (augmentedMessageTemplate, augmentedArguments) = @@ -243,7 +257,6 @@ private static IMetricReporter GetMetricReporter(IDependencyContainer container, #elif HOSTEDONAWS new NullMetricReporter(), #endif - MetricReporterOption.ForwardToAncillaryApi => new ForwardToAncillaryApiMetrics(container), _ => throw new ArgumentOutOfRangeException(nameof(options.MetricReporting)) }; } @@ -256,7 +269,6 @@ private static IUsageReporter GetUsageReporter(IDependencyContainer container, UsageReporterOption.None => new NullUsageReporter(), UsageReporterOption.ReliableQueue => new QueuedUsageReporter(container, container.Resolve().Platform), - UsageReporterOption.ForwardToAncillaryApi => new ForwardToAncillaryApiUsages(container), _ => throw new ArgumentOutOfRangeException(nameof(options.MetricReporting)) }; } diff --git a/src/Infrastructure.Persistence.Common/ApplicationServices/InProcessInMemStore.cs b/src/Infrastructure.Persistence.Common/ApplicationServices/InProcessInMemStore.cs index 812c5cf0..5f4ce8c6 100644 --- a/src/Infrastructure.Persistence.Common/ApplicationServices/InProcessInMemStore.cs +++ b/src/Infrastructure.Persistence.Common/ApplicationServices/InProcessInMemStore.cs @@ -1,6 +1,6 @@ #if TESTINGONLY using System.Diagnostics.CodeAnalysis; -using Common; +using Common.Extensions; using Domain.Interfaces; using Infrastructure.Persistence.Interfaces; using Infrastructure.Persistence.Interfaces.ApplicationServices; @@ -13,13 +13,18 @@ namespace Infrastructure.Persistence.Common.ApplicationServices; [ExcludeFromCodeCoverage] public sealed partial class InProcessInMemStore { - public InProcessInMemStore(Optional handler = default) + public static InProcessInMemStore Create(IQueueStoreNotificationHandler? handler = default) { - if (handler.HasValue) + return new InProcessInMemStore(handler); + } + + private InProcessInMemStore(IQueueStoreNotificationHandler? handler = default) + { + if (handler.Exists()) { FireMessageQueueUpdated += (_, args) => { - handler.Value.HandleMessagesQueueUpdated(args.QueueName, args.MessageCount); + handler.HandleMessagesQueueUpdated(args.QueueName, args.MessageCount); }; NotifyAllQueuedMessages(); } diff --git a/src/Infrastructure.Persistence.Common/ApplicationServices/LocalMachineJsonFileStore.cs b/src/Infrastructure.Persistence.Common/ApplicationServices/LocalMachineJsonFileStore.cs index af985448..40f157d7 100644 --- a/src/Infrastructure.Persistence.Common/ApplicationServices/LocalMachineJsonFileStore.cs +++ b/src/Infrastructure.Persistence.Common/ApplicationServices/LocalMachineJsonFileStore.cs @@ -22,7 +22,7 @@ public partial class LocalMachineJsonFileStore private readonly string _rootPath; public static LocalMachineJsonFileStore Create(ISettings settings, - Optional handler = default) + IQueueStoreNotificationHandler? handler = default) { var configPath = settings.GetString(PathSettingName); var basePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); @@ -32,7 +32,7 @@ public static LocalMachineJsonFileStore Create(ISettings settings, return new LocalMachineJsonFileStore(path, handler); } - private LocalMachineJsonFileStore(string rootPath, Optional handler = default) + private LocalMachineJsonFileStore(string rootPath, IQueueStoreNotificationHandler? handler = default) { rootPath.ThrowIfNotValuedParameter(nameof(rootPath)); if (rootPath.IsInvalidParameter(ValidateRootPath, nameof(rootPath), @@ -43,11 +43,11 @@ private LocalMachineJsonFileStore(string rootPath, Optional { - handler.Value.HandleMessagesQueueUpdated(args.QueueName, args.MessageCount); + handler.HandleMessagesQueueUpdated(args.QueueName, args.MessageCount); }; NotifyAllQueuedMessages(); } diff --git a/src/Infrastructure.Persistence.Shared.IntegrationTests/InProcessInMemStoreSpec.cs b/src/Infrastructure.Persistence.Shared.IntegrationTests/InProcessInMemStoreSpec.cs index b5fcd578..f855a5a7 100644 --- a/src/Infrastructure.Persistence.Shared.IntegrationTests/InProcessInMemStoreSpec.cs +++ b/src/Infrastructure.Persistence.Shared.IntegrationTests/InProcessInMemStoreSpec.cs @@ -14,13 +14,15 @@ public class AllInProcessInMemStoreSpecs : ICollectionFixture _store; - public IEventStore EventStore { get; } = new InProcessInMemStore(); + public IDataStore DataStore => _store; - public IQueueStore QueueStore { get; } = new InProcessInMemStore(); + public IEventStore EventStore => _store; + + public IQueueStore QueueStore => _store; } [Trait("Category", "Integration.Storage")] diff --git a/src/Infrastructure.Persistence.Shared.IntegrationTests/LocalMachineJsonFileStoreSpec.cs b/src/Infrastructure.Persistence.Shared.IntegrationTests/LocalMachineJsonFileStoreSpec.cs index 9f12ee28..3415bc15 100644 --- a/src/Infrastructure.Persistence.Shared.IntegrationTests/LocalMachineJsonFileStoreSpec.cs +++ b/src/Infrastructure.Persistence.Shared.IntegrationTests/LocalMachineJsonFileStoreSpec.cs @@ -14,21 +14,20 @@ public class AllLocalMachineJsonFileStoreSpecs : ICollectionFixture _store; - public IDataStore DataStore { get; } + public IDataStore DataStore => _store; - public IEventStore EventStore { get; } + public IEventStore EventStore => _store; - public IQueueStore QueueStore { get; } + public IQueueStore QueueStore => _store; } [Trait("Category", "Integration.Storage")] diff --git a/src/Infrastructure.Web.Api.Common/HttpConstants.cs b/src/Infrastructure.Web.Api.Common/HttpConstants.cs index 60c5ed4f..87d85477 100644 --- a/src/Infrastructure.Web.Api.Common/HttpConstants.cs +++ b/src/Infrastructure.Web.Api.Common/HttpConstants.cs @@ -24,6 +24,7 @@ public static class HttpHeaders { public const string Accept = "Accept"; public const string Authorization = "Authorization"; + public const string ContentType = "Content-Type"; public const string HmacSignature = "X-Hub-Signature"; public const string RequestId = "Request-ID"; } diff --git a/src/Infrastructure.Web.Api.Common/Resources.Designer.cs b/src/Infrastructure.Web.Api.Common/Resources.Designer.cs index 830f3544..b23d74e8 100644 --- a/src/Infrastructure.Web.Api.Common/Resources.Designer.cs +++ b/src/Infrastructure.Web.Api.Common/Resources.Designer.cs @@ -67,14 +67,5 @@ internal static string RequestExtensions_MissingRouteAttribute { return ResourceManager.GetString("RequestExtensions_MissingRouteAttribute", resourceCulture); } } - - /// - /// Looks up a localized string similar to An unexpected error occurred. - /// - internal static string WebApplicationExtensions_AddExceptionShielding_UnexpectedExceptionMessage { - get { - return ResourceManager.GetString("WebApplicationExtensions_AddExceptionShielding_UnexpectedExceptionMessage", resourceCulture); - } - } } } diff --git a/src/Infrastructure.Web.Api.Common/Resources.resx b/src/Infrastructure.Web.Api.Common/Resources.resx index 50f2dbef..a359cde0 100644 --- a/src/Infrastructure.Web.Api.Common/Resources.resx +++ b/src/Infrastructure.Web.Api.Common/Resources.resx @@ -27,7 +27,4 @@ The request DTO type '{0}' is missing a '{1}' declared on the class - - An unexpected error occurred - \ No newline at end of file diff --git a/src/Infrastructure.Web.Hosting.Common/ApplicationServices/StubQueueDrainingService.cs b/src/Infrastructure.Web.Hosting.Common/ApplicationServices/StubQueueDrainingService.cs index 7f5d5871..0bcf235b 100644 --- a/src/Infrastructure.Web.Hosting.Common/ApplicationServices/StubQueueDrainingService.cs +++ b/src/Infrastructure.Web.Hosting.Common/ApplicationServices/StubQueueDrainingService.cs @@ -56,6 +56,8 @@ public override void Dispose() GC.SuppressFinalize(this); } + public IEnumerable MonitoredQueues => _monitorQueueMappings.Select(mqm => mqm.Key); + protected override async Task ExecuteAsync(CancellationToken cancellationToken) { await Task.Delay(StartInterval, cancellationToken); diff --git a/src/Infrastructure.Web.Hosting.Common/Extensions/HostExtensions.cs b/src/Infrastructure.Web.Hosting.Common/Extensions/HostExtensions.cs index 5b0a1683..b3743476 100644 --- a/src/Infrastructure.Web.Hosting.Common/Extensions/HostExtensions.cs +++ b/src/Infrastructure.Web.Hosting.Common/Extensions/HostExtensions.cs @@ -4,6 +4,7 @@ using Application.Interfaces.Services; using Common; using Common.Configuration; +using Common.Extensions; using Domain.Common; using Domain.Common.Identity; using Domain.Interfaces; @@ -30,6 +31,7 @@ using Infrastructure.Persistence.Interfaces.ApplicationServices; using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Api.Operations.Shared.Ancillary; + #else #if HOSTEDONAZURE using Microsoft.ApplicationInsights.Extensibility; @@ -43,31 +45,44 @@ namespace Infrastructure.Web.Hosting.Common.Extensions; public static class HostExtensions { + private const string AllowedCORSOriginsSettingName = "Hosts:AllowedCORSOrigins"; + private const string CheckPointAggregatePrefix = "check"; + private const string LoggingSettingName = "Logging"; + private static readonly char[] AllowedCORSOriginsDelimiters = { ',', ';', ' ' }; + private static readonly Dictionary StubQueueDrainingServiceQueuedApiMappings = new() + { + { "audits", new DrainAllAuditsRequest() }, + { "usages", new DrainAllUsagesRequest() } + // { "emails", new DrainAllEmailsRequest() }, + // { "events", new DrainAllEventsRequest() }, + }; + /// /// Configures a WebHost /// public static WebApplication ConfigureApiHost(this WebApplicationBuilder appBuilder, SubDomainModules modules, - WebHostOptions options) + WebHostOptions hostOptions) { ConfigureSharedServices(); - ConfigureConfiguration(options.IsMultiTenanted); + ConfigureConfiguration(hostOptions.IsMultiTenanted); ConfigureRecording(); - ConfigureMultiTenancy(options.IsMultiTenanted); + ConfigureMultiTenancy(hostOptions.IsMultiTenanted); ConfigureAuthenticationAuthorization(); ConfigureWireFormats(); ConfigureApiRequests(); ConfigureApplicationServices(); - ConfigurePersistence(options.Persistence.UsesQueues); + ConfigurePersistence(hostOptions.Persistence.UsesQueues); + ConfigureCors(hostOptions.UsesCORS); var app = appBuilder.Build(); + app.EnableOtherOptions(hostOptions); app.EnableRequestRewind(); app.AddExceptionShielding(); //TODO: app.AddMultiTenancyDetection(); we need a TenantDetective - app.AddEventingListeners(options.Persistence.UsesEventing); - app.EnableApiUsageTracking(options.TrackApiUsage); - //TODO: add the HealthCheck endpoint - //TODO: enable CORS + app.EnableEventingListeners(hostOptions.Persistence.UsesEventing); + app.EnableApiUsageTracking(hostOptions.TrackApiUsage); + app.EnableCORS(hostOptions.UsesCORS); modules.ConfigureHost(app); @@ -80,13 +95,11 @@ void ConfigureSharedServices() void ConfigureConfiguration(bool isMultiTenanted) { -#if !TESTINGONLY #if HOSTEDONAZURE appBuilder.Configuration.AddJsonFile("appsettings.Azure.json", true); #endif #if HOSTEDONAWS appBuilder.Configuration.AddJsonFile("appsettings.AWS.json", true); -#endif #endif if (isMultiTenanted) @@ -123,13 +136,13 @@ void ConfigureRecording() appBuilder.Services.AddLogging(loggingBuilder => { loggingBuilder.ClearProviders(); - loggingBuilder.AddConfiguration(appBuilder.Configuration.GetSection("Logging")); + loggingBuilder.AddConfiguration(appBuilder.Configuration.GetSection(LoggingSettingName)); #if TESTINGONLY - loggingBuilder.AddSimpleConsole(opts => + loggingBuilder.AddSimpleConsole(options => { - opts.TimestampFormat = "hh:mm:ss "; - opts.SingleLine = true; - opts.IncludeScopes = false; + options.TimestampFormat = "hh:mm:ss "; + options.SingleLine = true; + options.IncludeScopes = false; }); loggingBuilder.AddDebug(); #else @@ -146,7 +159,7 @@ void ConfigureRecording() appBuilder.Services.RegisterUnshared(c => new HostRecorder(c.ResolveForUnshared(), c.ResolveForUnshared(), - options)); + hostOptions)); } void ConfigureMultiTenancy(bool isMultiTenanted) @@ -178,25 +191,28 @@ void ConfigureApiRequests() void ConfigureWireFormats() { - appBuilder.Services.ConfigureHttpJsonOptions(opts => + appBuilder.Services.ConfigureHttpJsonOptions(options => { - opts.SerializerOptions.PropertyNameCaseInsensitive = true; - opts.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - opts.SerializerOptions.WriteIndented = false; - opts.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault; - opts.SerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, + options.SerializerOptions.PropertyNameCaseInsensitive = true; + options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.SerializerOptions.WriteIndented = false; + options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault; + options.SerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, false)); - opts.SerializerOptions.Converters.Add(new JsonDateTimeConverter(DateFormat.Iso8601)); + options.SerializerOptions.Converters.Add(new JsonDateTimeConverter(DateFormat.Iso8601)); }); - appBuilder.Services.ConfigureHttpXmlOptions(opts => { opts.SerializerOptions.WriteIndented = false; }); + appBuilder.Services.ConfigureHttpXmlOptions(options => + { + options.SerializerOptions.WriteIndented = false; + }); } void ConfigureApplicationServices() { appBuilder.Services.AddHttpClient(); var prefixes = modules.AggregatePrefixes; - prefixes.Add(typeof(Checkpoint), "check"); + prefixes.Add(typeof(Checkpoint), CheckPointAggregatePrefix); appBuilder.Services.RegisterUnshared(_ => new HostIdentifierFactory(prefixes)); appBuilder.Services.RegisterTenanted(); } @@ -222,22 +238,61 @@ void ConfigurePersistence(bool usesQueues) #endif } + void ConfigureCors(bool usesCORS) + { + if (!usesCORS) + { + return; + } + + appBuilder.Services.AddCors(options => + { + var allowedOrigins = appBuilder.Configuration.GetValue(AllowedCORSOriginsSettingName) + ?? string.Empty; + if (allowedOrigins.HasValue()) + { + var origins = allowedOrigins.Split(AllowedCORSOriginsDelimiters); + options.AddDefaultPolicy(corsBuilder => + { + corsBuilder.WithOrigins(origins); + corsBuilder.AllowAnyMethod(); + corsBuilder.WithHeaders(HttpHeaders.ContentType, HttpHeaders.Authorization); + corsBuilder.DisallowCredentials(); + corsBuilder.SetPreflightMaxAge(TimeSpan.FromSeconds(600)); + }); + } + else + { + options.AddDefaultPolicy(corsBuilder => + { + corsBuilder.AllowAnyOrigin(); + corsBuilder.AllowAnyMethod(); + corsBuilder.WithHeaders(HttpHeaders.ContentType, HttpHeaders.Authorization); + corsBuilder.DisallowCredentials(); + corsBuilder.SetPreflightMaxAge(TimeSpan.FromSeconds(600)); + }); + } + }); + } + #if TESTINGONLY static void RegisterStoreForTestingOnly(WebApplicationBuilder appBuilder, bool usesQueues) { appBuilder.Services .RegisterPlatform(c => LocalMachineJsonFileStore.Create(c.ResolveForUnshared().Platform, - c.ResolveForUnshared() - .ToOptional())); + usesQueues + ? c.ResolveForUnshared() + : null)); //HACK: In TESTINGONLY there won't be any physical partitioning of data for different tenants, // even if the host is multi-tenanted. So we can register a singleton for this specific store, // as we only ever want to resolve one instance for this store for all its uses (tenanted or unshared, except for platform use) appBuilder.Services .RegisterUnshared(c => LocalMachineJsonFileStore.Create(c.ResolveForUnshared().Platform, - c.ResolveForUnshared() - .ToOptional())); + usesQueues + ? c.ResolveForUnshared() + : null)); if (usesQueues) { RegisterStubMessageQueueDrainingService(appBuilder); @@ -248,18 +303,11 @@ static void RegisterStubMessageQueueDrainingService(WebApplicationBuilder appBui { appBuilder.Services.RegisterUnshared(); appBuilder.Services.RegisterUnshared(); - var drainApiMappings = new Dictionary - { - { "audits", new DrainAllAuditsRequest() }, - { "usages", new DrainAllUsagesRequest() } - // { "emails", new DrainAllEmailsRequest() }, - // { "events", new DrainAllEventsRequest() }, - }; appBuilder.Services.AddHostedService(services => new StubQueueDrainingService(services.GetRequiredService(), services.ResolveForUnshared(), services.GetRequiredService>(), - services.ResolveForUnshared(), drainApiMappings)); + services.ResolveForUnshared(), StubQueueDrainingServiceQueuedApiMappings)); } #endif } diff --git a/src/Infrastructure.Web.Api.Common/Extensions/WebApplicationExtensions.cs b/src/Infrastructure.Web.Hosting.Common/Extensions/WebApplicationExtensions.cs similarity index 58% rename from src/Infrastructure.Web.Api.Common/Extensions/WebApplicationExtensions.cs rename to src/Infrastructure.Web.Hosting.Common/Extensions/WebApplicationExtensions.cs index e7cd4cc9..71263aa8 100644 --- a/src/Infrastructure.Web.Api.Common/Extensions/WebApplicationExtensions.cs +++ b/src/Infrastructure.Web.Hosting.Common/Extensions/WebApplicationExtensions.cs @@ -6,15 +6,26 @@ using Common.Extensions; using Infrastructure.Eventing.Interfaces.Notifications; using Infrastructure.Eventing.Interfaces.Projections; +using Infrastructure.Hosting.Common.Extensions; +using Infrastructure.Persistence.Interfaces; +using Infrastructure.Web.Api.Common; +using Infrastructure.Web.Api.Common.Extensions; using Infrastructure.Web.Api.Operations.Shared.Ancillary; +using Infrastructure.Web.Hosting.Common.ApplicationServices; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Json; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; -namespace Infrastructure.Web.Api.Common.Extensions; +namespace Infrastructure.Web.Hosting.Common.Extensions; public static class WebApplicationExtensions { @@ -35,34 +46,6 @@ public static class WebApplicationExtensions typeof(RecordMeasureRequest) }; - /// - /// Starts the relays for eventing projections and notifications - /// - public static IApplicationBuilder AddEventingListeners(this WebApplication app, bool usesEventing) - { - if (!usesEventing) - { - return app; - } - - return app.Use(async (context, next) => - { - var readModelRelay = context.RequestServices.GetRequiredService(); - if (!readModelRelay.IsStarted) - { - readModelRelay.Start(); - } - - var notificationRelay = context.RequestServices.GetRequiredService(); - if (!notificationRelay.IsStarted) - { - notificationRelay.Start(); - } - - await next(); - }); - } - /// /// Provides a global handler when an exception is encountered, and converts the exception /// to an RFC7807 error. @@ -70,6 +53,7 @@ public static IApplicationBuilder AddEventingListeners(this WebApplication app, /// public static IApplicationBuilder AddExceptionShielding(this WebApplication app) { + app.Logger.LogInformation("Exception Shielding is enabled"); return app.UseExceptionHandler(configure => configure.Run(async context => { var exceptionMessage = string.Empty; @@ -107,14 +91,15 @@ await Results.Problem(details) /// Enables the tracking of all inbound API calls /// /// - public static WebApplication EnableApiUsageTracking(this WebApplication app, bool tracksUsage) + public static IApplicationBuilder EnableApiUsageTracking(this WebApplication app, bool tracksUsage) { if (!tracksUsage) { return app; } - app.Use(async (context, next) => + app.Logger.LogInformation("API Usage Tracking is enabled"); + return app.Use(async (context, next) => { var recorder = context.RequestServices.GetRequiredService(); var caller = context.RequestServices.GetRequiredService(); @@ -122,6 +107,92 @@ public static WebApplication EnableApiUsageTracking(this WebApplication app, boo TrackUsage(context, recorder, caller); await next(); }); + } + + /// + /// Enables CORS for the host + /// + public static IApplicationBuilder EnableCORS(this WebApplication app, bool usesCORS) + { + if (!usesCORS) + { + return app; + } + + var httpContext = app.Services.GetRequiredService().Create(new FeatureCollection()); + var policy = app.Services.GetRequiredService() + .GetPolicyAsync(httpContext, WebHostingConstants.DefaultCORSPolicyName).GetAwaiter().GetResult(); + app.Logger.LogInformation("CORS is enabled: Policy -> {Policy}", policy!.ToString()); + return app.UseCors(); + } + + /// + /// Starts the relays for eventing projections and notifications + /// + public static IApplicationBuilder EnableEventingListeners(this WebApplication app, bool usesEventing) + { + if (!usesEventing) + { + return app; + } + + app.Logger.LogInformation("Eventing Projections/Notifications is enabled"); + return app.Use(async (context, next) => + { + var readModelRelay = context.RequestServices.GetRequiredService(); + if (!readModelRelay.IsStarted) + { + readModelRelay.Start(); + } + + var notificationRelay = context.RequestServices.GetRequiredService(); + if (!notificationRelay.IsStarted) + { + notificationRelay.Start(); + } + + await next(); + }); + } + + /// + /// Enables other options + /// + public static IApplicationBuilder EnableOtherOptions(this WebApplication app, WebHostOptions hostOptions) + { + var loggers = app.Services.GetServices() + .Select(logger => logger.GetType().Name).Join(", "); + app.Logger.LogInformation("Logging to -> {Providers}", loggers); + + var appSettings = ((ConfigurationManager)app.Configuration).Sources + .OfType() + .Select(jsonSource => jsonSource.Path) + .Join(", "); + app.Logger.LogInformation("Configuration loaded from -> {Sources}", appSettings); + + var recorder = app.Services.GetRequiredService(); + app.Logger.LogInformation("Recording with -> {Recorder}", recorder.ToString()); + + app.Logger.LogInformation("Multi-Tenancy request detection is {Status}", hostOptions.IsMultiTenanted + ? "disabled" + : "enabled"); + + var dataStore = app.Services.ResolveForPlatform().GetType().Name; + var eventStore = app.Services.ResolveForPlatform().GetType().Name; + var queueStore = app.Services.ResolveForPlatform().GetType().Name; + var blobStore = app.Services.ResolveForPlatform().GetType().Name; + app.Logger.LogInformation( + "Platform Persistence stores: DataStore -> {DataStore} EventStore -> {EventStore} QueueStore -> {QueueStore} BlobStore -> {BlobStore}", + dataStore, eventStore, queueStore, blobStore); + var stubDrainingServices = app.Services.GetServices() + .OfType() + .ToList(); + if (stubDrainingServices.HasAny()) + { + var stubDrainingService = stubDrainingServices[0]; + var queues = stubDrainingService.MonitoredQueues.Join(", "); + app.Logger.LogInformation("Background queue draining on queues -> {Queues}", queues); + } return app; } @@ -129,9 +200,9 @@ public static WebApplication EnableApiUsageTracking(this WebApplication app, boo /// /// Enables request buffering, so that request bodies can be read in filters /// - public static void EnableRequestRewind(this WebApplication app) + public static IApplicationBuilder EnableRequestRewind(this WebApplication app) { - app.Use(async (context, next) => + return app.Use(async (context, next) => { context.Request.EnableBuffering(); await next(); diff --git a/src/Infrastructure.Web.Hosting.Common/Resources.Designer.cs b/src/Infrastructure.Web.Hosting.Common/Resources.Designer.cs index 2ea49476..5d311c31 100644 --- a/src/Infrastructure.Web.Hosting.Common/Resources.Designer.cs +++ b/src/Infrastructure.Web.Hosting.Common/Resources.Designer.cs @@ -58,5 +58,14 @@ internal Resources() { resourceCulture = value; } } + + /// + /// Looks up a localized string similar to An unexpected error occurred. + /// + internal static string WebApplicationExtensions_AddExceptionShielding_UnexpectedExceptionMessage { + get { + return ResourceManager.GetString("WebApplicationExtensions_AddExceptionShielding_UnexpectedExceptionMessage", resourceCulture); + } + } } } diff --git a/src/Infrastructure.Web.Hosting.Common/Resources.resx b/src/Infrastructure.Web.Hosting.Common/Resources.resx index 755958fe..819bcf7d 100644 --- a/src/Infrastructure.Web.Hosting.Common/Resources.resx +++ b/src/Infrastructure.Web.Hosting.Common/Resources.resx @@ -24,4 +24,7 @@ PublicKeyToken=b77a5c561934e089 + + An unexpected error occurred + \ No newline at end of file diff --git a/src/Infrastructure.Web.Hosting.Common/WebHostOptions.cs b/src/Infrastructure.Web.Hosting.Common/WebHostOptions.cs index 85644597..6cdbe3e3 100644 --- a/src/Infrastructure.Web.Hosting.Common/WebHostOptions.cs +++ b/src/Infrastructure.Web.Hosting.Common/WebHostOptions.cs @@ -9,41 +9,34 @@ public class WebHostOptions : HostOptions { public new static readonly WebHostOptions BackEndAncillaryApiHost = new(HostOptions.BackEndAncillaryApiHost) { - DefaultApiPath = string.Empty, - AllowCors = true, + UsesCORS = true, TrackApiUsage = true, }; public new static readonly WebHostOptions BackEndApiHost = new(HostOptions.BackEndApiHost) { - DefaultApiPath = string.Empty, - AllowCors = true, + UsesCORS = true, TrackApiUsage = true }; public new static readonly WebHostOptions BackEndForFrontEndWebHost = new(HostOptions.BackEndForFrontEndWebHost) { - DefaultApiPath = "api", - AllowCors = true, + UsesCORS = true, TrackApiUsage = false }; public new static readonly WebHostOptions TestingStubsHost = new(HostOptions.TestingStubsHost) { - DefaultApiPath = string.Empty, - AllowCors = true, + UsesCORS = true, TrackApiUsage = false }; private WebHostOptions(HostOptions options) : base(options) { - DefaultApiPath = string.Empty; - AllowCors = true; + UsesCORS = true; TrackApiUsage = false; } public bool TrackApiUsage { get; private set; } - public bool AllowCors { get; private init; } - - public string DefaultApiPath { get; private init; } + public bool UsesCORS { get; private init; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Hosting.Common/WebHostingConstants.cs b/src/Infrastructure.Web.Hosting.Common/WebHostingConstants.cs new file mode 100644 index 00000000..d0b08594 --- /dev/null +++ b/src/Infrastructure.Web.Hosting.Common/WebHostingConstants.cs @@ -0,0 +1,6 @@ +namespace Infrastructure.Web.Hosting.Common; + +public static class WebHostingConstants +{ + public const string DefaultCORSPolicyName = "__DefaultCorsPolicy"; +} \ No newline at end of file diff --git a/src/SaaStack.sln.DotSettings b/src/SaaStack.sln.DotSettings index 1b757b32..f8df7233 100644 --- a/src/SaaStack.sln.DotSettings +++ b/src/SaaStack.sln.DotSettings @@ -306,6 +306,7 @@ </Entry> </TypePattern> </Patterns> + CORS <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="Configure" Suffix="" Style="AaBb" /></Policy> <Policy Inspect="True" Prefix="When" Suffix="" Style="AaBb_AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /><ExtraRule Prefix="Setup" Suffix="" Style="AaBb" /><ExtraRule Prefix="Configure" Suffix="" Style="AaBb" /></Policy> True diff --git a/src/Tools.Generators.WebApi/MinimalApiMediatRGenerator.cs b/src/Tools.Generators.WebApi/MinimalApiMediatRGenerator.cs index 72f0ee77..8867737c 100644 --- a/src/Tools.Generators.WebApi/MinimalApiMediatRGenerator.cs +++ b/src/Tools.Generators.WebApi/MinimalApiMediatRGenerator.cs @@ -1,5 +1,6 @@ using System.Text; using Infrastructure.Web.Api.Interfaces; +using Infrastructure.Web.Hosting.Common; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using Tools.Generators.WebApi.Extensions; @@ -96,8 +97,10 @@ private static void BuildEndpointRegistrations( { var serviceClassName = serviceRegistrations.Key.Name; var groupName = $"{serviceClassName.ToLowerInvariant()}Group"; + var corsPolicyName = WebHostingConstants.DefaultCORSPolicyName; endpointRegistrations.AppendLine($@" var {groupName} = app.MapGroup(string.Empty) .WithGroupName(""{serviceClassName}"") + .RequireCors(""{corsPolicyName}"") .AddEndpointFilter() .AddEndpointFilter();"); diff --git a/src/Tools.Generators.WebApi/Tools.Generators.WebApi.csproj b/src/Tools.Generators.WebApi/Tools.Generators.WebApi.csproj index 3851cad4..4a40a5a1 100644 --- a/src/Tools.Generators.WebApi/Tools.Generators.WebApi.csproj +++ b/src/Tools.Generators.WebApi/Tools.Generators.WebApi.csproj @@ -34,6 +34,9 @@ Reference\Infrastructure.Web.Api.Interfaces\ServiceOperation.cs + + Reference\Infrastructure.Web.Hosting.Common\WebHostingConstants.cs + diff --git a/src/WebsiteHost/appsettings.json b/src/WebsiteHost/appsettings.json index 36dfad9c..81c25abd 100644 --- a/src/WebsiteHost/appsettings.json +++ b/src/WebsiteHost/appsettings.json @@ -22,6 +22,7 @@ }, "WebsiteHost": { "BaseUrl": "https://localhost:5101" - } + }, + "AllowedCORSOrigins": "https://localhost:5101" } }