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"
}
}