From ec3aa8e5c34210449fb306e07bb56e1215ed5ef5 Mon Sep 17 00:00:00 2001 From: Ivan Ball-llovera Date: Tue, 18 Aug 2026 10:11:16 -0400 Subject: [PATCH 1/4] feat: broker second-level redelivery, fault observability, outbox publish circuit breaker, inbox loudly-off (A1/A5/A9) - UseDelayedRedelivery: opt-in for RabbitMq (delayed-message-exchange plugin required, absent in the Aspire dev container), always-on for Azure Service Bus; intervals via MessageBus:RedeliveryIntervalsSeconds (default 60/600/3600). - FaultIntegrationEventConsumer auto-registered by RegisterIntegrationEventConsumer (opt-out flag): one Error log + a new MMCA.Common.Broker meter (broker.fault.count). - Polly circuit breaker around the outbox broker publish only (BrokerResilienceDefaults: 0.5 ratio / 10 min-throughput / 30s sampling / 15s break); BrokenCircuitException follows the normal re-lease path with a distinct once-per-batch log + broker.circuit.open.count. - InboxDisabledWarningService: startup Warning when a broker-connected host runs NoOpInboxStore; EnableInbox docs expanded. - Polly.Core 8.7.0 pin + Infrastructure reference; lock regen also corrected pre-existing stale entries (Scalar 2.16.20, xunit.v3.extensibility.core 4.0.0). - 17 new tests (3395 total green). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RDQ4jE9QP6pC1ShZVG8ov5 --- Directory.Packages.props | 6 + .../DependencyInjection.cs | 59 ++++++- .../MMCA.Common.Infrastructure.csproj | 5 + .../Messaging/BrokerMetrics.cs | 46 ++++++ .../Inbox/InboxDisabledWarningService.cs | 35 +++++ .../Persistence/Outbox/OutboxProcessor.cs | 82 +++++++++- .../Services/FaultIntegrationEventConsumer.cs | 61 ++++++++ .../IntegrationEventConsumerExtensions.cs | 26 +++- .../Settings/MessageBusSettings.cs | 64 ++++++++ .../packages.lock.json | 11 +- .../Resilience/BrokerResilienceDefaults.cs | 56 +++++++ .../packages.lock.json | 11 +- .../Hosting/MMCA.Common.Aspire/Extensions.cs | 9 +- .../MMCA.Common.Aspire/packages.lock.json | 11 +- .../MMCA.Common.Testing.UI/packages.lock.json | 11 +- .../MMCA.Common.API/packages.lock.json | 12 +- .../MMCA.Common.Grpc/packages.lock.json | 11 +- .../MMCA.Common.UI.Web/packages.lock.json | 20 +-- .../MMCA.Common.UI/packages.lock.json | 11 +- .../packages.lock.json | 24 +-- .../packages.lock.json | 2 +- .../packages.lock.json | 2 +- ...DependencyInjectionBrokerMessagingTests.cs | 66 ++++++++ .../Inbox/InboxDisabledWarningServiceTests.cs | 64 ++++++++ .../Persistence/OutboxProcessorTests.cs | 107 +++++++++++++ .../FaultIntegrationEventConsumerTests.cs | 146 ++++++++++++++++++ .../Settings/SettingsTests.cs | 50 ++++++ .../packages.lock.json | 14 +- .../packages.lock.json | 2 +- .../packages.lock.json | 13 +- .../packages.lock.json | 4 +- .../MMCA.Common.API.Tests/packages.lock.json | 22 +-- .../MMCA.Common.Grpc.Tests/packages.lock.json | 13 +- .../MMCA.Common.UI.Tests/packages.lock.json | 13 +- .../packages.lock.json | 22 +-- 35 files changed, 997 insertions(+), 114 deletions(-) create mode 100644 Source/Core/MMCA.Common.Infrastructure/Messaging/BrokerMetrics.cs create mode 100644 Source/Core/MMCA.Common.Infrastructure/Persistence/Inbox/InboxDisabledWarningService.cs create mode 100644 Source/Core/MMCA.Common.Infrastructure/Services/FaultIntegrationEventConsumer.cs create mode 100644 Source/Core/MMCA.Common.Shared/Resilience/BrokerResilienceDefaults.cs create mode 100644 Tests/Core/MMCA.Common.Infrastructure.Tests/DependencyInjectionBrokerMessagingTests.cs create mode 100644 Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/Inbox/InboxDisabledWarningServiceTests.cs create mode 100644 Tests/Core/MMCA.Common.Infrastructure.Tests/Services/FaultIntegrationEventConsumerTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index fd0630cc..23ef5469 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -122,6 +122,12 @@ + + diff --git a/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs b/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs index 60804754..ce37a556 100644 --- a/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs +++ b/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs @@ -690,6 +690,11 @@ public IServiceCollection AddBrokerMessaging( else { services.TryAddSingleton(); + + // Loudly off: a disabled dedup store looks exactly like an enabled one until a + // duplicate side effect reaches a customer. One startup Warning makes the posture + // visible for the cost of a single log line. + services.AddHostedService(); } return services; @@ -767,10 +772,20 @@ public IHttpClientBuilder AddTypedServiceClient(str /// below the analyzer threshold. /// /// - /// Only in-process retry (UseMessageRetry) is configured — not UseDelayedRedelivery, - /// which on RabbitMQ requires the delayed-message-exchange plugin that the Aspire RabbitMQ - /// container does not ship. A consumer that needs broker-level delayed redelivery can layer it - /// on per-endpoint after installing the plugin (or on Azure Service Bus, which supports it natively). + /// Second-level redelivery (UseDelayedRedelivery) sits ABOVE the in-process retry policy: + /// in-process retry absorbs a blip measured in seconds, delayed redelivery reschedules the + /// message through the broker over + /// (one minute, ten minutes, one hour by default) so an outage measured in minutes or hours + /// does not dead-letter the event. It is registered before UseMessageRetry so the retry + /// filter runs innermost: every immediate attempt is exhausted before a redelivery is scheduled. + /// + /// The two transports differ in posture. Azure Service Bus schedules messages natively, so + /// redelivery is applied UNCONDITIONALLY there. RabbitMQ needs the + /// rabbitmq_delayed_message_exchange plugin, which the Aspire development container does + /// not ship, so it is gated behind + /// (default ) and must only be turned on against a broker that has the + /// plugin installed. + /// /// [SuppressMessage( "Style", @@ -791,6 +806,19 @@ private static void ConfigureBrokerTransport( cfg.Host(new Uri(connectionString)); } + // Opt-in: needs the rabbitmq_delayed_message_exchange plugin, which the Aspire + // development container does not ship. Registered before UseMessageRetry so the + // retry filter stays innermost (all immediate attempts first, then a scheduled + // redelivery). + if (settings.EnableDelayedRedelivery) + { + TimeSpan[] intervals = BuildRedeliveryIntervals(settings); + if (intervals.Length > 0) + { + cfg.UseDelayedRedelivery(r => r.Intervals(intervals)); + } + } + cfg.UseMessageRetry(r => r.Exponential( settings.RetryLimit, TimeSpan.FromSeconds(settings.RetryMinIntervalSeconds), @@ -808,6 +836,16 @@ private static void ConfigureBrokerTransport( cfg.Host(connectionString); } + // Unconditional: Azure Service Bus schedules messages natively, so there is no + // plugin to install and no configuration in which this can fail at bus start. + // The EnableDelayedRedelivery flag is deliberately not consulted on this + // transport, because it exists only to gate the RabbitMQ plugin requirement. + TimeSpan[] intervals = BuildRedeliveryIntervals(settings); + if (intervals.Length > 0) + { + cfg.UseDelayedRedelivery(r => r.Intervals(intervals)); + } + cfg.UseMessageRetry(r => r.Exponential( settings.RetryLimit, TimeSpan.FromSeconds(settings.RetryMinIntervalSeconds), @@ -823,4 +861,17 @@ private static void ConfigureBrokerTransport( break; } } + + /// + /// Maps to the + /// array MassTransit's redelivery configurator expects. Non-positive + /// entries are dropped: a zero or negative interval schedules an immediate redelivery, which + /// is what UseMessageRetry already does and would turn the second level into a hot loop. + /// Returns an empty array when nothing survives, and the caller then skips the filter entirely + /// rather than registering a redelivery policy with no attempts. + /// + private static TimeSpan[] BuildRedeliveryIntervals(MessageBusSettings settings) => + [.. (settings.RedeliveryIntervalsSeconds ?? []) + .Where(seconds => seconds > 0) + .Select(seconds => TimeSpan.FromSeconds(seconds))]; } diff --git a/Source/Core/MMCA.Common.Infrastructure/MMCA.Common.Infrastructure.csproj b/Source/Core/MMCA.Common.Infrastructure/MMCA.Common.Infrastructure.csproj index 5bc54e6c..172fd1ed 100644 --- a/Source/Core/MMCA.Common.Infrastructure/MMCA.Common.Infrastructure.csproj +++ b/Source/Core/MMCA.Common.Infrastructure/MMCA.Common.Infrastructure.csproj @@ -54,6 +54,11 @@ Referenced unconditionally so the types are available to hosts that call AddScheduledJobs; a host that does not call it registers no runner and maps no table. --> + + diff --git a/Source/Core/MMCA.Common.Infrastructure/Messaging/BrokerMetrics.cs b/Source/Core/MMCA.Common.Infrastructure/Messaging/BrokerMetrics.cs new file mode 100644 index 00000000..033aeb6a --- /dev/null +++ b/Source/Core/MMCA.Common.Infrastructure/Messaging/BrokerMetrics.cs @@ -0,0 +1,46 @@ +using System.Diagnostics.Metrics; + +namespace MMCA.Common.Infrastructure.Messaging; + +/// +/// OpenTelemetry instruments for the broker transport: consumer faults observed by +/// FaultIntegrationEventConsumer<TEvent> and circuit-breaker openings observed by +/// the outbox publish path. A host exports them by registering the meter: +/// the Aspire service defaults (ConfigureOpenTelemetry) already do. The meter name is +/// duplicated as a literal in MMCA.Common.Aspire because that package has no reference to +/// Infrastructure. +/// +/// One meter serves every broker instrument. Never create a second with this +/// name: a duplicate instance publishes a parallel set of instruments under the same meter name, +/// and a listener enabling one of them silently misses the measurements recorded on the other. +/// +/// +internal static class BrokerMetrics +{ + /// OpenTelemetry meter name for broker transport metrics. + internal const string MeterName = "MMCA.Common.Broker"; + + private static readonly Meter Meter = new(MeterName); + + /// + /// Messages that exhausted their retry policy and were published as a MassTransit + /// Fault<TEvent>, tagged by event_type. A non-zero rate here means events + /// are being lost to the error queue, so it is the natural alert target for consumer health. + /// + internal static readonly Counter FaultCounter = Meter.CreateCounter( + "broker.fault.count", + unit: "messages", + description: "Number of integration events that exhausted retries and faulted, tagged by event type."); + + /// + /// Times the outbox broker-publish circuit breaker rejected a publish because the circuit was + /// open, tagged by event_type. Distinct from a publish failure: these attempts never + /// reached the broker at all, which is the whole point of the breaker (fail fast rather than + /// stack publish timeouts against a dead broker). The affected rows stay leased and are + /// retried on a later cycle. + /// + internal static readonly Counter CircuitOpenCounter = Meter.CreateCounter( + "broker.circuit.open.count", + unit: "messages", + description: "Number of outbox publishes short-circuited by the open broker circuit breaker, tagged by event type."); +} diff --git a/Source/Core/MMCA.Common.Infrastructure/Persistence/Inbox/InboxDisabledWarningService.cs b/Source/Core/MMCA.Common.Infrastructure/Persistence/Inbox/InboxDisabledWarningService.cs new file mode 100644 index 00000000..999abb86 --- /dev/null +++ b/Source/Core/MMCA.Common.Infrastructure/Persistence/Inbox/InboxDisabledWarningService.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace MMCA.Common.Infrastructure.Persistence.Inbox; + +/// +/// Emits a single startup Warning when a host enables broker messaging but leaves consumer-side +/// inbox deduplication off (the registration). A silently disabled +/// safety feature is indistinguishable from an enabled one until the first duplicate side effect +/// reaches a customer, so the off state is made loud exactly once, at startup, where it costs one +/// log line and nothing per message. +/// +/// Registered by AddBrokerMessaging only on the disabled branch, so a host that turns the +/// inbox on never sees this service at all. +/// +/// +/// Logger for the startup warning. +internal sealed partial class InboxDisabledWarningService(ILogger logger) + : IHostedService +{ + /// + public Task StartAsync(CancellationToken cancellationToken) + { + LogInboxDisabled(logger); + return Task.CompletedTask; + } + + /// + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + [LoggerMessage( + Level = LogLevel.Warning, + Message = "Broker messaging is enabled but consumer-side inbox deduplication is OFF (NoOpInboxStore). Broker delivery is at-least-once, so a redelivered message will run its handlers again and duplicate their side effects. Enable it with MessageBus:EnableInbox=true; the InboxMessages table is already part of the model.")] + private static partial void LogInboxDisabled(ILogger logger); +} diff --git a/Source/Core/MMCA.Common.Infrastructure/Persistence/Outbox/OutboxProcessor.cs b/Source/Core/MMCA.Common.Infrastructure/Persistence/Outbox/OutboxProcessor.cs index 98edce06..19039436 100644 --- a/Source/Core/MMCA.Common.Infrastructure/Persistence/Outbox/OutboxProcessor.cs +++ b/Source/Core/MMCA.Common.Infrastructure/Persistence/Outbox/OutboxProcessor.cs @@ -8,9 +8,13 @@ using MMCA.Common.Application.Interfaces.Infrastructure; using MMCA.Common.Application.Messaging; using MMCA.Common.Domain.Interfaces; +using MMCA.Common.Infrastructure.Messaging; using MMCA.Common.Infrastructure.Persistence.DataSources; using MMCA.Common.Infrastructure.Persistence.DbContexts; using MMCA.Common.Infrastructure.Settings; +using MMCA.Common.Shared.Resilience; +using Polly; +using Polly.CircuitBreaker; namespace MMCA.Common.Infrastructure.Persistence.Outbox; @@ -80,6 +84,20 @@ public sealed partial class OutboxProcessor( private static readonly ActivitySource OutboxActivitySource = new("MMCA.Common.Outbox"); + /// + /// Circuit breaker guarding the broker-publish call only (never the database calls: a breaker + /// on those would open exactly when the processor most needs to persist retry state). Tuned by + /// and carrying NO retry strategy, because the outbox + /// already owns retry via RetryCount and . + /// + /// Per instance rather than per process. A host runs one processor, so the practical scope is + /// the same, while an instance field keeps the breaker state from leaking across the many + /// processors a test assembly constructs in parallel: one test deliberately failing publishes + /// would otherwise open a shared circuit under another test's feet. + /// + /// + private readonly ResiliencePipeline _brokerPublishPipeline = BuildBrokerPublishPipeline(); + /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { @@ -461,6 +479,12 @@ private async Task DispatchMessagesAsync( CancellationToken cancellationToken) { var processedAny = false; + + // Log-once latch for this batch: an open circuit rejects every remaining row in the same + // instant, and 50 identical Warning lines per cycle is noise an operator learns to filter. + // The per-row signal stays on the metric (BrokerMetrics.CircuitOpenCounter). + var circuitOpenLogged = false; + foreach (var message in messages) { using var activity = StartOutboxActivity(message, source); @@ -485,7 +509,15 @@ private async Task DispatchMessagesAsync( // determines delivery. Pure domain events keep the legacy in-process dispatch. if (domainEvent is IIntegrationEvent integrationEvent) { - await messageBus.PublishAsync(integrationEvent, cancellationToken).ConfigureAwait(false); + // Only the broker hop is wrapped. The in-process dispatcher branch below is a + // direct method call into this same process: it has no transport to be dead, + // so a breaker there would only add a way to reject work that would have + // succeeded. + await _brokerPublishPipeline.ExecuteAsync( + static async (state, ct) => + await state.Bus.PublishAsync(state.Event, ct).ConfigureAwait(false), + (Bus: messageBus, Event: integrationEvent), + cancellationToken).ConfigureAwait(false); } else { @@ -532,6 +564,26 @@ private async Task DispatchMessagesAsync( activity?.SetStatus(ActivityStatusCode.Error, ex.Message); + // An open circuit is a rejection, not a delivery attempt: the publish never left + // the process. It still follows the normal failure path above (retry increment and + // re-lease) so the row is retried on a later cycle exactly like any other failure, + // but it gets its own counter and its own log line, because "the broker refused + // 50 messages" and "we did not try, the broker is known-dead" are different + // operational facts. + var circuitOpen = ex is BrokenCircuitException; + if (circuitOpen) + { + BrokerMetrics.CircuitOpenCounter.Add( + 1, + new KeyValuePair("event_type", message.EventType)); + } + + if (circuitOpen && !circuitOpenLogged) + { + circuitOpenLogged = true; + LogBrokerCircuitOpen(logger, source.ToString()); + } + if (message.RetryCount >= _settings.MaxRetries) { // The moment of exhaustion is the operator's last loud signal: from here the @@ -543,8 +595,9 @@ private async Task DispatchMessagesAsync( new KeyValuePair("reason", "retries_exhausted")); LogRetriesExhausted(logger, message.Id, message.EventType, message.RetryCount, ex); } - else + else if (!circuitOpen) { + // Circuit-open rejections already reported themselves above, once per batch. LogMessageRetry(logger, message.Id, message.RetryCount, ex); } } @@ -576,6 +629,26 @@ internal double ComputeRetryBackoffSeconds(int retryCount) return Math.Min(backoff * jitter, _settings.LeaseSeconds); } + /// + /// Builds the broker-publish circuit breaker from . + /// is excluded from the handled set: a host shutdown + /// cancelling a batch mid-flight is not evidence that the broker is unhealthy, and letting it + /// count toward the failure ratio would leave the circuit open against a perfectly good broker + /// on the next start. + /// + private static ResiliencePipeline BuildBrokerPublishPipeline() => + new ResiliencePipelineBuilder() + .AddCircuitBreaker(new CircuitBreakerStrategyOptions + { + FailureRatio = BrokerResilienceDefaults.FailureRatio, + MinimumThroughput = BrokerResilienceDefaults.MinimumThroughput, + SamplingDuration = BrokerResilienceDefaults.SamplingDuration, + BreakDuration = BrokerResilienceDefaults.BreakDuration, + ShouldHandle = new PredicateBuilder() + .Handle(ex => ex is not OperationCanceledException), + }) + .Build(); + /// /// Starts a new linked to the original request's trace context /// stored in the outbox message. Returns when no trace context @@ -634,4 +707,9 @@ internal double ComputeRetryBackoffSeconds(int retryCount) [LoggerMessage(Level = LogLevel.Error, Message = "Outbox message {MessageId} ({EventType}) dead-lettered: retries exhausted after {RetryCount} attempts — the event was never delivered")] private static partial void LogRetriesExhausted(ILogger logger, Guid messageId, string eventType, int retryCount, Exception exception); + + // Logged once per batch, not once per message: an open circuit rejects every remaining row in + // the same instant. Warning rather than Error because nothing is lost, only deferred. + [LoggerMessage(Level = LogLevel.Warning, Message = "Broker circuit is open for data source {DataSourceName}: skipping outbox publishes this cycle and retrying the affected messages on a later one")] + private static partial void LogBrokerCircuitOpen(ILogger logger, string dataSourceName); } diff --git a/Source/Core/MMCA.Common.Infrastructure/Services/FaultIntegrationEventConsumer.cs b/Source/Core/MMCA.Common.Infrastructure/Services/FaultIntegrationEventConsumer.cs new file mode 100644 index 00000000..ca760512 --- /dev/null +++ b/Source/Core/MMCA.Common.Infrastructure/Services/FaultIntegrationEventConsumer.cs @@ -0,0 +1,61 @@ +using MassTransit; +using Microsoft.Extensions.Logging; +using MMCA.Common.Domain.Interfaces; +using MMCA.Common.Infrastructure.Messaging; + +namespace MMCA.Common.Infrastructure.Services; + +/// +/// Consumes the message MassTransit publishes when a consumer for +/// exhausts its retry policy, turning what would otherwise be a +/// silent row in the broker's _error queue into one structured Error log plus a +/// broker.fault.count metric tagged by event type. +/// +/// Registered automatically alongside every consumer wired through +/// RegisterIntegrationEventConsumer<TEvent> (opt out per event with its +/// registerFaultConsumer parameter, or globally with +/// MessageBus:RegisterFaultConsumers=false). +/// +/// +/// This consumer never throws. A fault consumer that faults would publish +/// Fault<Fault<TEvent>> and, on a broker with second-level redelivery enabled, +/// keep re-entering itself: observability code must not be able to create its own incident. The +/// original message is already in the error queue and is not replayed from here. +/// +/// +/// The integration event type whose faults are observed. +/// Logger for the fault diagnostic. +public sealed partial class FaultIntegrationEventConsumer( + ILogger> logger) : IConsumer> + where TEvent : class, IIntegrationEvent +{ + /// + public Task Consume(ConsumeContext> context) + { + ArgumentNullException.ThrowIfNull(context); + + Fault fault = context.Message; + + // The broker's own message id when MassTransit captured one, otherwise the fault id: both + // are what an operator pastes into a queue browser, and one of them is always present. + var messageId = fault.FaultedMessageId ?? fault.FaultId; + + // Every exception in the chain, innermost cause included, on one line: the stack traces + // stay in the error queue's message headers, and the message text is what identifies the + // failure at a glance. + var reasons = fault.Exceptions is { Length: > 0 } exceptions + ? string.Join(" | ", exceptions.Select(e => e.Message)) + : ""; + + LogFault(logger, messageId, typeof(TEvent).Name, reasons); + + BrokerMetrics.FaultCounter.Add( + 1, + new KeyValuePair("event_type", typeof(TEvent).Name)); + + return Task.CompletedTask; + } + + [LoggerMessage(Level = LogLevel.Error, Message = "Integration event {EventType} (message {MessageId}) faulted after exhausting its retry policy and was moved to the error queue: {Reasons}")] + private static partial void LogFault(ILogger logger, Guid messageId, string eventType, string reasons); +} diff --git a/Source/Core/MMCA.Common.Infrastructure/Services/IntegrationEventConsumerExtensions.cs b/Source/Core/MMCA.Common.Infrastructure/Services/IntegrationEventConsumerExtensions.cs index dfd15567..fa51b176 100644 --- a/Source/Core/MMCA.Common.Infrastructure/Services/IntegrationEventConsumerExtensions.cs +++ b/Source/Core/MMCA.Common.Infrastructure/Services/IntegrationEventConsumerExtensions.cs @@ -17,12 +17,36 @@ public static class IntegrationEventConsumerExtensions /// to all /// implementations resolved from DI. Use one call per integration event type the /// service consumes. + /// + /// A is registered alongside it by + /// default. MassTransit publishes a Fault<TEvent> message whenever a consumer + /// exhausts its retry policy; with nothing subscribed to that topic the only trace of an + /// undelivered event is a row in the broker's _error queue that no dashboard is + /// watching. The fault consumer subscribes to it and emits one Error log plus a + /// broker.fault.count metric. + /// /// /// The integration event type. - public IBusRegistrationConfigurator RegisterIntegrationEventConsumer() + /// + /// Whether to also register for this + /// event. Defaults to . Pass for an event + /// whose faults a host routes itself (a dedicated fault service, or a custom + /// IConsumer<Fault<TEvent>>), so two consumers do not compete for the + /// same fault topic. This parameter is the per-event switch; the host-wide default is + /// MessageBus:RegisterFaultConsumers, which callers read themselves because this + /// extension has no access to configuration. + /// + public IBusRegistrationConfigurator RegisterIntegrationEventConsumer( + bool registerFaultConsumer = true) where TEvent : class, IIntegrationEvent { x.AddConsumer>(); + + if (registerFaultConsumer) + { + x.AddConsumer>(); + } + return x; } } diff --git a/Source/Core/MMCA.Common.Infrastructure/Settings/MessageBusSettings.cs b/Source/Core/MMCA.Common.Infrastructure/Settings/MessageBusSettings.cs index 656f7ecf..ee4a3c98 100644 --- a/Source/Core/MMCA.Common.Infrastructure/Settings/MessageBusSettings.cs +++ b/Source/Core/MMCA.Common.Infrastructure/Settings/MessageBusSettings.cs @@ -60,8 +60,72 @@ public sealed class MessageBusSettings /// , IntegrationEventConsumer dedups already-processed messages via /// an InboxMessages table in the consumer's database — which requires that table to exist /// (apply the AddInboxMessages migration). Defaults to . + /// + /// RECOMMENDED for any broker-connected host. Broker delivery is + /// at-least-once by contract: a consumer that acks after a network blip, a redelivered message + /// after a lease expiry, or an outbox row republished after a crash all hand the same event to + /// the same handlers twice. With the inbox off, every one of those becomes a duplicate side + /// effect (a second email, a second charge attempt, a double decrement) unless every handler + /// happens to be idempotent on its own. The default stays only so an + /// existing host does not start querying a table it has not migrated yet; a host that enables + /// broker messaging and leaves this off gets a startup warning + /// (InboxDisabledWarningService) rather than silence. + /// /// public bool EnableInbox { get; init; } + + /// + /// Gets a value indicating whether second-level (broker-scheduled) redelivery is applied on + /// top of the in-process UseMessageRetry policy. When , a message + /// that exhausts its immediate retries is scheduled back onto the queue after each interval in + /// instead of dead-lettering right away, which is what + /// carries a consumer through an outage measured in minutes or hours rather than seconds. + /// + /// Defaults to because on RabbitMQ this requires the + /// rabbitmq_delayed_message_exchange plugin, and the Aspire development RabbitMQ + /// container does not ship it: enabling it against a plugin-less broker fails at bus start. + /// Set it to only on a broker where the plugin is installed. + /// + /// + /// This flag is IGNORED by , which supports + /// scheduled redelivery natively and therefore always applies + /// . + /// + /// + public bool EnableDelayedRedelivery { get; init; } + + /// + /// Gets the second-level redelivery intervals, in seconds. Each entry is one scheduled + /// redelivery attempt after the in-process retry policy is exhausted; the message + /// dead-letters only after the last interval also fails. Defaults to + /// [60, 600, 3600] (one minute, ten minutes, one hour), a spread wide enough to ride + /// out a dependency restart, a failover and a short incident without an operator replaying + /// the error queue by hand. + /// + /// Applied unconditionally on (native + /// scheduled delivery) and only when is + /// on (needs the + /// delayed-message-exchange plugin). + /// + /// + public IReadOnlyList RedeliveryIntervalsSeconds { get; init; } = [60, 600, 3600]; + + /// + /// Gets a value indicating whether a FaultIntegrationEventConsumer<TEvent> is + /// registered alongside each integration-event consumer. MassTransit publishes a + /// Fault<TEvent> message when a consumer exhausts its retries, and with nothing + /// subscribed to that topic the only trace of an undelivered event is a row in the broker's + /// _error queue that no dashboard is watching. The fault consumer turns that into one + /// structured Error log plus a broker.fault.count metric. Defaults to + /// . + /// + /// Hosts that route faults themselves (a dedicated fault service, or a per-event opt-out via + /// the registerFaultConsumer parameter on + /// RegisterIntegrationEventConsumer<TEvent>) can set this to + /// to document the intent. + /// + /// + public bool RegisterFaultConsumers { get; init; } = true; } /// Available message bus transports. diff --git a/Source/Core/MMCA.Common.Infrastructure/packages.lock.json b/Source/Core/MMCA.Common.Infrastructure/packages.lock.json index 3714290c..ae44678a 100644 --- a/Source/Core/MMCA.Common.Infrastructure/packages.lock.json +++ b/Source/Core/MMCA.Common.Infrastructure/packages.lock.json @@ -189,6 +189,12 @@ "resolved": "7.0.0", "contentHash": "2lMTCQl5bGP4iv0JNkockPnyllC6eHLz+CoK2ICvalvHod+exXSxueu9hq+zNkU7bZBJf8wMfeRC/Edn8AGmEg==" }, + "Polly.Core": { + "type": "Direct", + "requested": "[8.7.0, )", + "resolved": "8.7.0", + "contentHash": "BS2t+nsBer16PIebCEPNBK5fgMisADQyRCd7K+BgkMWpFmSaiYE+rVVNpFhGRqUkJmNSLmw0uCNzHHWfgml28Q==" + }, "Roslynator.Analyzers": { "type": "Direct", "requested": "[4.16.1, )", @@ -628,11 +634,6 @@ "resolved": "2.2.16", "contentHash": "nonu9l0YrZH6krVYbskBjE7uG0VMAnvOQ9PU+RmzUsy+++vHkqzzCkHRoP877OiA3iRTxJyNDLfpcU8hrJ3/YQ==" }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" - }, "Polly.Extensions": { "type": "Transitive", "resolved": "8.4.2", diff --git a/Source/Core/MMCA.Common.Shared/Resilience/BrokerResilienceDefaults.cs b/Source/Core/MMCA.Common.Shared/Resilience/BrokerResilienceDefaults.cs new file mode 100644 index 00000000..c598c8c2 --- /dev/null +++ b/Source/Core/MMCA.Common.Shared/Resilience/BrokerResilienceDefaults.cs @@ -0,0 +1,56 @@ +namespace MMCA.Common.Shared.Resilience; + +/// +/// Single source of truth for the circuit-breaker values guarding the outbox's broker-publish +/// path (OutboxProcessor in MMCA.Common.Infrastructure). Lives in Shared alongside +/// so the numbers are reviewable in one place rather than +/// buried as literals inside a background service. +/// +/// Why the outbox needs a breaker at all: when the broker is down, every publish in a 50-row +/// batch waits out its own transport timeout before failing, so one cycle can spend minutes doing +/// nothing but timing out, and the retry loop then queues the same wait again on the next cycle. +/// The breaker turns the second and later attempts into an immediate rejection. Nothing is lost by +/// failing fast: an outbox row that is not published stays unprocessed, keeps its lease-based +/// backoff and is retried on a later cycle exactly as a normal publish failure would be. The +/// breaker only changes how long the processor spends discovering that the broker is still down. +/// +/// +/// Deliberately NOT paired with a retry strategy: the outbox already owns retry (RetryCount, +/// exponential backoff with jitter, MaxRetries then dead-letter). A Polly retry inside a publish +/// would multiply against that budget and make the row's effective attempt count an accident of +/// two independent policies. +/// +/// +public static class BrokerResilienceDefaults +{ + /// + /// Fraction of failed publishes within that opens the circuit. + /// Half, rather than a lower bar: a partially degraded broker still delivering half its + /// messages is worth continuing to drain, and only a clearly one-sided failure rate is + /// evidence that further attempts this cycle are wasted. + /// + public static double FailureRatio => 0.5; + + /// + /// Minimum publish attempts within before the failure ratio is + /// evaluated at all. Ten keeps a quiet host, where two attempts an hour is normal traffic, + /// from opening the circuit on a single unlucky pair; the breaker should react to a broker + /// outage, not to sparse traffic. + /// + public static int MinimumThroughput => 10; + + /// + /// Rolling window over which the failure ratio is measured. Thirty seconds is a few outbox + /// cycles at the default polling interval: long enough that a batch's worth of attempts lands + /// inside one window, short enough that a resolved outage ages out of the statistics quickly. + /// + public static TimeSpan SamplingDuration => TimeSpan.FromSeconds(30); + + /// + /// How long the circuit stays open before a single trial publish is allowed through. Fifteen + /// seconds is deliberately short: a rejected publish costs one outbox row a retry increment, + /// not a customer-facing error, so recovery latency matters more here than protecting the + /// broker from one probe. + /// + public static TimeSpan BreakDuration => TimeSpan.FromSeconds(15); +} diff --git a/Source/Hosting/MMCA.Common.Aspire.Hosting/packages.lock.json b/Source/Hosting/MMCA.Common.Aspire.Hosting/packages.lock.json index a0303d69..7b93d085 100644 --- a/Source/Hosting/MMCA.Common.Aspire.Hosting/packages.lock.json +++ b/Source/Hosting/MMCA.Common.Aspire.Hosting/packages.lock.json @@ -972,11 +972,6 @@ "OpenTelemetry.Api": "1.15.3" } }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.6.6", - "contentHash": "lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==" - }, "PolyType": { "type": "Transitive", "resolved": "1.3.1", @@ -1211,6 +1206,12 @@ "OpenTelemetry": "1.15.3" } }, + "Polly.Core": { + "type": "CentralTransitive", + "requested": "[8.7.0, )", + "resolved": "8.6.6", + "contentHash": "lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==" + }, "System.IdentityModel.Tokens.Jwt": { "type": "CentralTransitive", "requested": "[8.22.0, )", diff --git a/Source/Hosting/MMCA.Common.Aspire/Extensions.cs b/Source/Hosting/MMCA.Common.Aspire/Extensions.cs index 59f537e7..9cadf60f 100644 --- a/Source/Hosting/MMCA.Common.Aspire/Extensions.cs +++ b/Source/Hosting/MMCA.Common.Aspire/Extensions.cs @@ -155,12 +155,15 @@ public TBuilder ConfigureOpenTelemetry() // MMCA.Common meters (literal names, because Aspire has no reference to the // defining assemblies): outbox counters and dispatch lag, CQRS RED histograms // plus query cache hit/miss, the idempotency filter's replay, conflict and - // degraded counters, and the recurring scheduler's run outcomes, duration and - // schedule lag (inert in a host that never enables Scheduler:Enabled). + // degraded counters, the recurring scheduler's run outcomes, duration and + // schedule lag (inert in a host that never enables Scheduler:Enabled), and the + // broker transport's consumer faults plus outbox circuit-breaker openings + // (inert in a host that stays on the in-process bus). metrics.AddMeter("MMCA.Common.Outbox") .AddMeter("MMCA.Common.Cqrs") .AddMeter("MMCA.Common.Idempotency") - .AddMeter("MMCA.Common.Scheduler"); + .AddMeter("MMCA.Common.Scheduler") + .AddMeter("MMCA.Common.Broker"); }) .WithTracing(tracing => { diff --git a/Source/Hosting/MMCA.Common.Aspire/packages.lock.json b/Source/Hosting/MMCA.Common.Aspire/packages.lock.json index 6f0036c8..4f619967 100644 --- a/Source/Hosting/MMCA.Common.Aspire/packages.lock.json +++ b/Source/Hosting/MMCA.Common.Aspire/packages.lock.json @@ -682,11 +682,6 @@ "System.IO.Pipelines": "5.0.1" } }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" - }, "Polly.Extensions": { "type": "Transitive", "resolved": "8.4.2", @@ -882,6 +877,12 @@ "Microsoft.Extensions.Options": "10.0.11" } }, + "Polly.Core": { + "type": "CentralTransitive", + "requested": "[8.7.0, )", + "resolved": "8.4.2", + "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" + }, "StackExchange.Redis": { "type": "CentralTransitive", "requested": "[2.13.17, )", diff --git a/Source/Hosting/MMCA.Common.Testing.UI/packages.lock.json b/Source/Hosting/MMCA.Common.Testing.UI/packages.lock.json index aa2dd8e1..49256b76 100644 --- a/Source/Hosting/MMCA.Common.Testing.UI/packages.lock.json +++ b/Source/Hosting/MMCA.Common.Testing.UI/packages.lock.json @@ -161,11 +161,6 @@ "resolved": "6.0.0", "contentHash": "hqTM5628jSsQiv+HGpiq3WKBl2c8v1KZfby2J6Pr7pEPlK9waPdgEO6b8A/+/xn/yZ9ulv8HuqK71ONy2tg67A==" }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.7.0", - "contentHash": "BS2t+nsBer16PIebCEPNBK5fgMisADQyRCd7K+BgkMWpFmSaiYE+rVVNpFhGRqUkJmNSLmw0uCNzHHWfgml28Q==" - }, "StyleCop.Analyzers.Unstable": { "type": "Transitive", "resolved": "1.2.0.556", @@ -223,6 +218,12 @@ "Polly.Core": "8.7.0" } }, + "Polly.Core": { + "type": "CentralTransitive", + "requested": "[8.7.0, )", + "resolved": "8.7.0", + "contentHash": "BS2t+nsBer16PIebCEPNBK5fgMisADQyRCd7K+BgkMWpFmSaiYE+rVVNpFhGRqUkJmNSLmw0uCNzHHWfgml28Q==" + }, "QRCoder": { "type": "CentralTransitive", "requested": "[1.8.0, )", diff --git a/Source/Presentation/MMCA.Common.API/packages.lock.json b/Source/Presentation/MMCA.Common.API/packages.lock.json index 7a508ce3..85803f17 100644 --- a/Source/Presentation/MMCA.Common.API/packages.lock.json +++ b/Source/Presentation/MMCA.Common.API/packages.lock.json @@ -659,11 +659,6 @@ "resolved": "2.2.16", "contentHash": "nonu9l0YrZH6krVYbskBjE7uG0VMAnvOQ9PU+RmzUsy+++vHkqzzCkHRoP877OiA3iRTxJyNDLfpcU8hrJ3/YQ==" }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" - }, "Polly.Extensions": { "type": "Transitive", "resolved": "8.4.2", @@ -808,6 +803,7 @@ "Microsoft.Extensions.Caching.Hybrid": "[10.9.0, )", "Microsoft.Extensions.Http.Resilience": "[10.9.0, )", "MiniProfiler.EntityFrameworkCore": "[4.5.4, )", + "Polly.Core": "[8.7.0, )", "SQLitePCLRaw.bundle_e_sqlite3": "[3.0.5, )", "SixLabors.ImageSharp": "[3.1.12, )", "StackExchange.Redis": "[2.13.17, )" @@ -1056,6 +1052,12 @@ "MiniProfiler.Shared": "4.5.4" } }, + "Polly.Core": { + "type": "CentralTransitive", + "requested": "[8.7.0, )", + "resolved": "8.7.0", + "contentHash": "BS2t+nsBer16PIebCEPNBK5fgMisADQyRCd7K+BgkMWpFmSaiYE+rVVNpFhGRqUkJmNSLmw0uCNzHHWfgml28Q==" + }, "Scrutor": { "type": "CentralTransitive", "requested": "[7.0.0, )", diff --git a/Source/Presentation/MMCA.Common.Grpc/packages.lock.json b/Source/Presentation/MMCA.Common.Grpc/packages.lock.json index fdc4da44..1b354465 100644 --- a/Source/Presentation/MMCA.Common.Grpc/packages.lock.json +++ b/Source/Presentation/MMCA.Common.Grpc/packages.lock.json @@ -200,11 +200,6 @@ "Microsoft.Extensions.Compliance.Abstractions": "10.9.0" } }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" - }, "Polly.Extensions": { "type": "Transitive", "resolved": "8.4.2", @@ -240,6 +235,12 @@ "requested": "[2.82.0, )", "resolved": "2.80.0", "contentHash": "NS1AxwVZnrdbBoMf5L5ruGjBjoLBej9avhqxWNsgfu2GU6FhpxEVJOwLJsdljlDCjvquJiAH2zf/WodIWMvf2w==" + }, + "Polly.Core": { + "type": "CentralTransitive", + "requested": "[8.7.0, )", + "resolved": "8.4.2", + "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" } } } diff --git a/Source/Presentation/MMCA.Common.UI.Web/packages.lock.json b/Source/Presentation/MMCA.Common.UI.Web/packages.lock.json index 7aaa1373..46455471 100644 --- a/Source/Presentation/MMCA.Common.UI.Web/packages.lock.json +++ b/Source/Presentation/MMCA.Common.UI.Web/packages.lock.json @@ -444,11 +444,6 @@ "resolved": "2.2.16", "contentHash": "nonu9l0YrZH6krVYbskBjE7uG0VMAnvOQ9PU+RmzUsy+++vHkqzzCkHRoP877OiA3iRTxJyNDLfpcU8hrJ3/YQ==" }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.7.0", - "contentHash": "BS2t+nsBer16PIebCEPNBK5fgMisADQyRCd7K+BgkMWpFmSaiYE+rVVNpFhGRqUkJmNSLmw0uCNzHHWfgml28Q==" - }, "Polly.Extensions": { "type": "Transitive", "resolved": "8.4.2", @@ -559,7 +554,7 @@ "Microsoft.AspNetCore.OpenApi": "[10.0.11, )", "Microsoft.FeatureManagement.AspNetCore": "[4.6.0, )", "Microsoft.OpenApi": "[2.12.0, )", - "Scalar.AspNetCore": "[2.16.18, )" + "Scalar.AspNetCore": "[2.16.20, )" } }, "mmca.common.application": { @@ -620,6 +615,7 @@ "Microsoft.Extensions.Caching.Hybrid": "[10.9.0, )", "Microsoft.Extensions.Http.Resilience": "[10.9.0, )", "MiniProfiler.EntityFrameworkCore": "[4.5.4, )", + "Polly.Core": "[8.7.0, )", "SQLitePCLRaw.bundle_e_sqlite3": "[3.0.5, )", "SixLabors.ImageSharp": "[3.1.12, )", "StackExchange.Redis": "[2.13.17, )" @@ -1059,6 +1055,12 @@ "Polly.Core": "8.7.0" } }, + "Polly.Core": { + "type": "CentralTransitive", + "requested": "[8.7.0, )", + "resolved": "8.7.0", + "contentHash": "BS2t+nsBer16PIebCEPNBK5fgMisADQyRCd7K+BgkMWpFmSaiYE+rVVNpFhGRqUkJmNSLmw0uCNzHHWfgml28Q==" + }, "QRCoder": { "type": "CentralTransitive", "requested": "[1.8.0, )", @@ -1070,9 +1072,9 @@ }, "Scalar.AspNetCore": { "type": "CentralTransitive", - "requested": "[2.16.18, )", - "resolved": "2.16.18", - "contentHash": "R/S9FSpMLjdd75uOe22YPNnfDtPYygK31feG6XJ9BLjX/y4WM8cGRzwT49iEBKhnJB2CnWUnfLlWGV7TsVDk+Q==" + "requested": "[2.16.20, )", + "resolved": "2.16.20", + "contentHash": "pSmyME4FCnYocbLmn9lmAUTVVSEPb5E6noqRcmJYpO65eaum68gw17yMORbeckW+gMksiL04m+zXoTxOKv0+kQ==" }, "Scrutor": { "type": "CentralTransitive", diff --git a/Source/Presentation/MMCA.Common.UI/packages.lock.json b/Source/Presentation/MMCA.Common.UI/packages.lock.json index 628398dc..262169cd 100644 --- a/Source/Presentation/MMCA.Common.UI/packages.lock.json +++ b/Source/Presentation/MMCA.Common.UI/packages.lock.json @@ -647,11 +647,6 @@ "resolved": "6.0.0", "contentHash": "hqTM5628jSsQiv+HGpiq3WKBl2c8v1KZfby2J6Pr7pEPlK9waPdgEO6b8A/+/xn/yZ9ulv8HuqK71ONy2tg67A==" }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.7.0", - "contentHash": "BS2t+nsBer16PIebCEPNBK5fgMisADQyRCd7K+BgkMWpFmSaiYE+rVVNpFhGRqUkJmNSLmw0uCNzHHWfgml28Q==" - }, "StyleCop.Analyzers.Unstable": { "type": "Transitive", "resolved": "1.2.0.556", @@ -667,6 +662,12 @@ }, "mmca.common.shared": { "type": "Project" + }, + "Polly.Core": { + "type": "CentralTransitive", + "requested": "[8.7.0, )", + "resolved": "8.7.0", + "contentHash": "BS2t+nsBer16PIebCEPNBK5fgMisADQyRCd7K+BgkMWpFmSaiYE+rVVNpFhGRqUkJmNSLmw0uCNzHHWfgml28Q==" } } } diff --git a/Tests/Architecture/MMCA.Common.Architecture.Tests/packages.lock.json b/Tests/Architecture/MMCA.Common.Architecture.Tests/packages.lock.json index e1e1fa85..81376c56 100644 --- a/Tests/Architecture/MMCA.Common.Architecture.Tests/packages.lock.json +++ b/Tests/Architecture/MMCA.Common.Architecture.Tests/packages.lock.json @@ -971,11 +971,6 @@ "resolved": "2.2.16", "contentHash": "nonu9l0YrZH6krVYbskBjE7uG0VMAnvOQ9PU+RmzUsy+++vHkqzzCkHRoP877OiA3iRTxJyNDLfpcU8hrJ3/YQ==" }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.7.0", - "contentHash": "BS2t+nsBer16PIebCEPNBK5fgMisADQyRCd7K+BgkMWpFmSaiYE+rVVNpFhGRqUkJmNSLmw0uCNzHHWfgml28Q==" - }, "Polly.Extensions": { "type": "Transitive", "resolved": "8.4.2", @@ -1171,7 +1166,7 @@ "Microsoft.AspNetCore.OpenApi": "[10.0.11, )", "Microsoft.FeatureManagement.AspNetCore": "[4.6.0, )", "Microsoft.OpenApi": "[2.12.0, )", - "Scalar.AspNetCore": "[2.16.18, )" + "Scalar.AspNetCore": "[2.16.20, )" } }, "mmca.common.application": { @@ -1221,6 +1216,7 @@ "Microsoft.Extensions.Caching.Hybrid": "[10.9.0, )", "Microsoft.Extensions.Http.Resilience": "[10.9.0, )", "MiniProfiler.EntityFrameworkCore": "[4.5.4, )", + "Polly.Core": "[8.7.0, )", "SQLitePCLRaw.bundle_e_sqlite3": "[3.0.5, )", "SixLabors.ImageSharp": "[3.1.12, )", "StackExchange.Redis": "[2.13.17, )" @@ -1234,7 +1230,7 @@ "dependencies": { "AwesomeAssertions": "[9.5.0, )", "NetArchTest.eNhancedEdition": "[1.4.5, )", - "xunit.v3.extensibility.core": "[3.2.2, )" + "xunit.v3.extensibility.core": "[4.0.0, )" } }, "mmca.common.ui": { @@ -1720,6 +1716,12 @@ "Polly.Core": "8.7.0" } }, + "Polly.Core": { + "type": "CentralTransitive", + "requested": "[8.7.0, )", + "resolved": "8.7.0", + "contentHash": "BS2t+nsBer16PIebCEPNBK5fgMisADQyRCd7K+BgkMWpFmSaiYE+rVVNpFhGRqUkJmNSLmw0uCNzHHWfgml28Q==" + }, "QRCoder": { "type": "CentralTransitive", "requested": "[1.8.0, )", @@ -1731,9 +1733,9 @@ }, "Scalar.AspNetCore": { "type": "CentralTransitive", - "requested": "[2.16.18, )", - "resolved": "2.16.18", - "contentHash": "R/S9FSpMLjdd75uOe22YPNnfDtPYygK31feG6XJ9BLjX/y4WM8cGRzwT49iEBKhnJB2CnWUnfLlWGV7TsVDk+Q==" + "requested": "[2.16.20, )", + "resolved": "2.16.20", + "contentHash": "pSmyME4FCnYocbLmn9lmAUTVVSEPb5E6noqRcmJYpO65eaum68gw17yMORbeckW+gMksiL04m+zXoTxOKv0+kQ==" }, "Scrutor": { "type": "CentralTransitive", @@ -1790,7 +1792,7 @@ }, "xunit.v3.extensibility.core": { "type": "CentralTransitive", - "requested": "[3.2.2, )", + "requested": "[4.0.0, )", "resolved": "4.0.0", "contentHash": "+tTe9VX2vwrUHGI48FE4ly956Ry084wPH4I/7g5D36AkAMGjQRZZDVz8eH2GwX/CLS9PDQRSca534WyrAYsyzw==", "dependencies": { diff --git a/Tests/Core/MMCA.Common.Application.Tests/packages.lock.json b/Tests/Core/MMCA.Common.Application.Tests/packages.lock.json index 4827a8c1..c5f442fe 100644 --- a/Tests/Core/MMCA.Common.Application.Tests/packages.lock.json +++ b/Tests/Core/MMCA.Common.Application.Tests/packages.lock.json @@ -391,7 +391,7 @@ }, "xunit.v3.extensibility.core": { "type": "CentralTransitive", - "requested": "[3.2.2, )", + "requested": "[4.0.0, )", "resolved": "4.0.0", "contentHash": "+tTe9VX2vwrUHGI48FE4ly956Ry084wPH4I/7g5D36AkAMGjQRZZDVz8eH2GwX/CLS9PDQRSca534WyrAYsyzw==", "dependencies": { diff --git a/Tests/Core/MMCA.Common.Domain.Tests/packages.lock.json b/Tests/Core/MMCA.Common.Domain.Tests/packages.lock.json index d3f13d7f..d7c10f57 100644 --- a/Tests/Core/MMCA.Common.Domain.Tests/packages.lock.json +++ b/Tests/Core/MMCA.Common.Domain.Tests/packages.lock.json @@ -182,7 +182,7 @@ }, "xunit.v3.extensibility.core": { "type": "CentralTransitive", - "requested": "[3.2.2, )", + "requested": "[4.0.0, )", "resolved": "4.0.0", "contentHash": "+tTe9VX2vwrUHGI48FE4ly956Ry084wPH4I/7g5D36AkAMGjQRZZDVz8eH2GwX/CLS9PDQRSca534WyrAYsyzw==", "dependencies": { diff --git a/Tests/Core/MMCA.Common.Infrastructure.Tests/DependencyInjectionBrokerMessagingTests.cs b/Tests/Core/MMCA.Common.Infrastructure.Tests/DependencyInjectionBrokerMessagingTests.cs new file mode 100644 index 00000000..0ba24011 --- /dev/null +++ b/Tests/Core/MMCA.Common.Infrastructure.Tests/DependencyInjectionBrokerMessagingTests.cs @@ -0,0 +1,66 @@ +using AwesomeAssertions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using MMCA.Common.Infrastructure.Persistence.Inbox; + +namespace MMCA.Common.Infrastructure.Tests; + +/// +/// Registration-level tests for AddBrokerMessaging. They inspect the +/// rather than building a provider: the broker branch registers +/// MassTransit and an EF-backed inbox store whose dependencies a unit test has no business +/// standing up, and what is under test here is which descriptors land, not what they resolve to. +/// +public sealed class DependencyInjectionBrokerMessagingTests +{ + private static IConfiguration ConfigurationFor(string provider, bool enableInbox) => + new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["MessageBus:Provider"] = provider, + ["MessageBus:EnableInbox"] = enableInbox ? "true" : "false", + ["MessageBus:ConnectionString"] = "amqp://guest:guest@localhost:5672", + }) + .Build(); + + private static bool HasHostedService(IServiceCollection services) => + services.Any(d => d.ServiceType == typeof(IHostedService) && d.ImplementationType == typeof(T)); + + private static Type? InboxStoreImplementation(IServiceCollection services) => + services.FirstOrDefault(d => d.ServiceType == typeof(IInboxStore))?.ImplementationType; + + [Fact] + public void AddBrokerMessaging_InboxDisabled_RegistersNoOpStoreAndTheLoudWarningService() + { + var services = new ServiceCollection(); + + services.AddBrokerMessaging(ConfigurationFor("RabbitMq", enableInbox: false)); + + InboxStoreImplementation(services).Should().Be(); + HasHostedService(services).Should() + .BeTrue("a silently disabled dedup store is indistinguishable from an enabled one until a duplicate reaches a customer"); + } + + [Fact] + public void AddBrokerMessaging_InboxEnabled_RegistersEfStoreAndNoWarningService() + { + var services = new ServiceCollection(); + + services.AddBrokerMessaging(ConfigurationFor("RabbitMq", enableInbox: true)); + + InboxStoreImplementation(services).Should().Be(); + HasHostedService(services).Should() + .BeFalse("nothing is off, so there is nothing to warn about"); + } + + [Fact] + public void AddBrokerMessaging_InProcessProvider_RegistersNothing() + { + var services = new ServiceCollection(); + + services.AddBrokerMessaging(ConfigurationFor("InProcess", enableInbox: false)); + + services.Should().BeEmpty("the in-process provider short-circuits before touching the container"); + } +} diff --git a/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/Inbox/InboxDisabledWarningServiceTests.cs b/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/Inbox/InboxDisabledWarningServiceTests.cs new file mode 100644 index 00000000..15d73dad --- /dev/null +++ b/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/Inbox/InboxDisabledWarningServiceTests.cs @@ -0,0 +1,64 @@ +using AwesomeAssertions; +using Microsoft.Extensions.Logging; +using MMCA.Common.Infrastructure.Persistence.Inbox; + +namespace MMCA.Common.Infrastructure.Tests.Persistence.Inbox; + +/// +/// Unit tests for : the one startup line that keeps a +/// disabled dedup store from looking exactly like an enabled one. +/// +public sealed class InboxDisabledWarningServiceTests +{ + /// + /// Hand-rolled instead of a Mock<ILogger<T>>: the service under test is + /// internal, and Castle DynamicProxy cannot build a proxy for a closed generic over an + /// internal type when the generic definition lives in the strong-named + /// Microsoft.Extensions.Logging.Abstractions assembly. + /// + private sealed class RecordingLogger : ILogger + { + public List<(LogLevel Level, string Message)> Entries { get; } = []; + + public IDisposable? BeginScope(TState state) + where TState : notnull => null; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter) + { + ArgumentNullException.ThrowIfNull(formatter); + Entries.Add((logLevel, formatter(state, exception))); + } + } + + [Fact] + public async Task StartAsync_LogsExactlyOneWarningNamingTheSettingThatTurnsTheInboxOn() + { + var logger = new RecordingLogger(); + var sut = new InboxDisabledWarningService(logger); + + await sut.StartAsync(CancellationToken.None); + + logger.Entries.Should().ContainSingle("the warning is a startup posture statement, not a per-message log"); + logger.Entries[0].Level.Should().Be(LogLevel.Warning); + logger.Entries[0].Message.Should().Contain("MessageBus:EnableInbox=true", "the log must carry its own remedy"); + logger.Entries[0].Message.Should().Contain("at-least-once"); + } + + [Fact] + public async Task StopAsync_CompletesWithoutLogging() + { + var logger = new RecordingLogger(); + var sut = new InboxDisabledWarningService(logger); + + await sut.StopAsync(CancellationToken.None); + + logger.Entries.Should().BeEmpty(); + } +} diff --git a/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/OutboxProcessorTests.cs b/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/OutboxProcessorTests.cs index e6d6cb50..567bebc2 100644 --- a/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/OutboxProcessorTests.cs +++ b/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/OutboxProcessorTests.cs @@ -18,6 +18,7 @@ using MMCA.Common.Infrastructure.Persistence.Outbox; using MMCA.Common.Infrastructure.Settings; using MMCA.Common.Infrastructure.Tests.TestDoubles; +using MMCA.Common.Shared.Resilience; using Moq; using IDbContextFactory = MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IDbContextFactory; @@ -494,6 +495,112 @@ public async Task IntegrationEventPublishFailure_DegradesGracefully_BuffersForRe retried.LastError.Should().Be("Broker unreachable"); } + [Fact] + public async Task SustainedPublishFailures_OpenTheBrokerCircuit_RowsStillRetryAndTheOpeningIsReportedOnce() + { + // A dead broker makes every publish wait out its own transport timeout, so one batch can + // spend minutes discovering the same fact 50 times. The breaker turns the later attempts + // into an immediate rejection; nothing is lost, because a rejected row follows the normal + // failure path (retry increment, re-lease) and is retried on a later cycle. + _messageBusMock + .Setup(b => b.PublishAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(new InvalidOperationException("Broker unreachable")); + + // The breaker only evaluates its failure ratio once it has MinimumThroughput samples in the + // window, so the batch has to be larger than that threshold to open mid-cycle. + var messageCount = BrokerResilienceDefaults.MinimumThroughput + 5; + OutboxMessage[] messages = [.. Enumerable.Range(0, messageCount).Select(i => + CreateEligibleMessage( + eventType: typeof(TestIntegrationEvent).AssemblyQualifiedName, + occurredOn: DateTime.UtcNow.AddMinutes(-10).AddSeconds(i)))]; + await _dbContext.Set().AddRangeAsync(messages); + await _dbContext.SaveChangesAsync(); + + var logged = new List<(LogLevel Level, string Message)>(); + var mockLogger = new Mock>(); + mockLogger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + mockLogger + .Setup(l => l.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Callback(new InvocationAction(invocation => + { + var formatter = (Delegate)invocation.Arguments[4]; + logged.Add(( + (LogLevel)invocation.Arguments[0], + (string)formatter.DynamicInvoke(invocation.Arguments[2], invocation.Arguments[3])!)); + })); + + var gate = new System.Threading.Lock(); + var circuitOpenMeasurements = new List<(long Value, string? EventType)>(); + using MeterListener listener = StartBrokerListener(gate, circuitOpenMeasurements); + + using OutboxProcessor processor = CreateProcessor(new OutboxSettings(), logger: mockLogger.Object); + await processor.ProcessPendingMessagesAsync(CancellationToken.None); + + // Every row took the normal failure path: nothing is marked delivered, nothing is lost. + List updated = await _dbContext.Set().ToListAsync(); + updated.Should().HaveCount(messageCount); + updated.Should().AllSatisfy(m => + { + m.ProcessedOn.Should().BeNull("a broker failure must not mark the event delivered"); + m.RetryCount.Should().Be(1); + }); + + // Some rows never reached the broker at all: that is the breaker doing its job. + circuitOpenMeasurements.Should().NotBeEmpty("the circuit must open once the failure ratio is provable"); + updated.Should().Contain( + m => m.LastError != null && m.LastError.Contains("circuit", StringComparison.OrdinalIgnoreCase), + "a short-circuited publish records the rejection, not a transport error it never saw"); + + // Once per batch, not once per row: an open circuit rejects the whole remainder in the + // same instant, and 50 identical warnings is noise an operator learns to filter. + logged.Where(e => e.Message.Contains("circuit is open", StringComparison.Ordinal)) + .Should().ContainSingle(); + } + + /// + /// Listener for the broker meter's circuit-open counter, mirroring + /// for the outbox meter. + /// + private static MeterListener StartBrokerListener( + System.Threading.Lock gate, + List<(long Value, string? EventType)> sink) + { + var listener = new MeterListener + { + InstrumentPublished = (instrument, l) => + { + if (string.Equals(instrument.Meter.Name, "MMCA.Common.Broker", StringComparison.Ordinal) + && string.Equals(instrument.Name, "broker.circuit.open.count", StringComparison.Ordinal)) + { + l.EnableMeasurementEvents(instrument); + } + }, + }; + listener.SetMeasurementEventCallback((_, value, tags, _) => + { + string? eventType = null; + foreach (KeyValuePair tag in tags) + { + if (string.Equals(tag.Key, "event_type", StringComparison.Ordinal)) + { + eventType = tag.Value as string; + } + } + + lock (gate) + { + sink.Add((value, eventType)); + } + }); + listener.Start(); + return listener; + } + // ── Lease: rows under an unexpired lock are skipped; expired locks are claimable ── [Fact] public async Task LockedRow_SkippedWhileLeaseUnexpired_ClaimedAndProcessedAfterExpiry() diff --git a/Tests/Core/MMCA.Common.Infrastructure.Tests/Services/FaultIntegrationEventConsumerTests.cs b/Tests/Core/MMCA.Common.Infrastructure.Tests/Services/FaultIntegrationEventConsumerTests.cs new file mode 100644 index 00000000..bec83db3 --- /dev/null +++ b/Tests/Core/MMCA.Common.Infrastructure.Tests/Services/FaultIntegrationEventConsumerTests.cs @@ -0,0 +1,146 @@ +using System.Diagnostics.Metrics; +using AwesomeAssertions; +using MassTransit; +using Microsoft.Extensions.Logging; +using MMCA.Common.Domain.DomainEvents; +using MMCA.Common.Infrastructure.Services; +using Moq; + +namespace MMCA.Common.Infrastructure.Tests.Services; + +/// +/// Unit tests for : the observability path a +/// faulted integration event takes once MassTransit has given up retrying it. +/// +public sealed class FaultIntegrationEventConsumerTests +{ + public sealed record class TestFaultedEvent : BaseIntegrationEvent; + + private static Mock>> ContextFor(Fault fault) + { + var context = new Mock>>(); + context.SetupGet(c => c.Message).Returns(fault); + return context; + } + + private static Fault FaultWith(Guid? faultedMessageId, params string[] exceptionMessages) + { + var fault = new Mock>(); + fault.SetupGet(f => f.FaultId).Returns(Guid.NewGuid()); + fault.SetupGet(f => f.FaultedMessageId).Returns(faultedMessageId); + fault.SetupGet(f => f.Exceptions).Returns( + [.. exceptionMessages.Select(m => Mock.Of(e => e.Message == m))]); + return fault.Object; + } + + /// + /// Captures the rendered text and level of every log call so the test asserts on what an + /// operator actually reads, not on an EventId. Same shape as the OutboxProcessor log tests. + /// + private static Mock>> CapturingLogger( + List<(LogLevel Level, string Message)> sink) + { + var logger = new Mock>>(); + logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + logger + .Setup(l => l.Log( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny>())) + .Callback(new InvocationAction(invocation => + { + var formatter = (Delegate)invocation.Arguments[4]; + sink.Add(( + (LogLevel)invocation.Arguments[0], + (string)formatter.DynamicInvoke(invocation.Arguments[2], invocation.Arguments[3])!)); + })); + return logger; + } + + [Fact] + public async Task Consume_LogsOneErrorNamingTheEventAndEveryExceptionMessage() + { + var faultedMessageId = Guid.NewGuid(); + var logged = new List<(LogLevel Level, string Message)>(); + var sut = new FaultIntegrationEventConsumer(CapturingLogger(logged).Object); + + await sut.Consume(ContextFor(FaultWith(faultedMessageId, "outer boom", "inner boom")).Object); + + logged.Should().ContainSingle(); + (LogLevel Level, string Message) entry = logged[0]; + entry.Level.Should().Be(LogLevel.Error, "a lost integration event is an operator-actionable failure"); + entry.Message.Should().Contain(nameof(TestFaultedEvent)); + entry.Message.Should().Contain(faultedMessageId.ToString()); + entry.Message.Should().Contain("outer boom"); + entry.Message.Should().Contain("inner boom", "the whole exception chain identifies the cause"); + } + + [Fact] + public async Task Consume_IncrementsFaultCounter_TaggedByEventType() + { + var gate = new System.Threading.Lock(); + var measurements = new List<(long Value, string? EventType)>(); + using var listener = new MeterListener + { + InstrumentPublished = (instrument, l) => + { + if (string.Equals(instrument.Meter.Name, "MMCA.Common.Broker", StringComparison.Ordinal) + && string.Equals(instrument.Name, "broker.fault.count", StringComparison.Ordinal)) + { + l.EnableMeasurementEvents(instrument); + } + }, + }; + listener.SetMeasurementEventCallback((_, value, tags, _) => + { + string? eventType = null; + foreach (KeyValuePair tag in tags) + { + if (string.Equals(tag.Key, "event_type", StringComparison.Ordinal)) + { + eventType = tag.Value as string; + } + } + + lock (gate) + { + measurements.Add((value, eventType)); + } + }); + listener.Start(); + + var sut = new FaultIntegrationEventConsumer( + Mock.Of>>()); + + await sut.Consume(ContextFor(FaultWith(Guid.NewGuid(), "boom")).Object); + + measurements.Should().Contain((1L, nameof(TestFaultedEvent))); + } + + [Fact] + public async Task Consume_FallsBackToFaultId_WhenNoFaultedMessageIdWasCaptured() + { + var logged = new List<(LogLevel Level, string Message)>(); + var sut = new FaultIntegrationEventConsumer(CapturingLogger(logged).Object); + + await sut.Consume(ContextFor(FaultWith(faultedMessageId: null, "boom")).Object); + + logged.Should().ContainSingle(); + logged[0].Message.Should().NotContain(Guid.Empty.ToString(), "the fault id stands in when no message id was captured"); + } + + [Fact] + public async Task Consume_DoesNotThrow_WhenTheFaultCarriesNoExceptionDetail() + { + // A fault consumer that faults would publish Fault> and, with second-level + // redelivery on, keep re-entering itself. Observability code must not create incidents. + var sut = new FaultIntegrationEventConsumer( + Mock.Of>>()); + + var act = async () => await sut.Consume(ContextFor(FaultWith(Guid.NewGuid())).Object); + + await act.Should().NotThrowAsync(); + } +} diff --git a/Tests/Core/MMCA.Common.Infrastructure.Tests/Settings/SettingsTests.cs b/Tests/Core/MMCA.Common.Infrastructure.Tests/Settings/SettingsTests.cs index 4cea2743..684893f0 100644 --- a/Tests/Core/MMCA.Common.Infrastructure.Tests/Settings/SettingsTests.cs +++ b/Tests/Core/MMCA.Common.Infrastructure.Tests/Settings/SettingsTests.cs @@ -223,6 +223,56 @@ public void Properties_RoundTrip() } } +// ── MessageBusSettings ── +public class MessageBusSettingsTests +{ + [Fact] + public void SectionName_IsMessageBus() => + MessageBusSettings.SectionName.Should().Be("MessageBus"); + + [Fact] + public void Default_Provider_IsInProcess() => + new MessageBusSettings().Provider.Should().Be(MessageBusProvider.InProcess); + + [Fact] + public void Default_EnableInbox_IsFalse() => + new MessageBusSettings().EnableInbox.Should().BeFalse(); + + // Default-off is load-bearing, not incidental: RabbitMQ needs the delayed-message-exchange + // plugin, which the Aspire development container does not ship, so a default-on flag would + // fail bus start on every local run. + [Fact] + public void Default_EnableDelayedRedelivery_IsFalse() => + new MessageBusSettings().EnableDelayedRedelivery.Should().BeFalse(); + + [Fact] + public void Default_RedeliveryIntervalsSeconds_IsOneMinuteTenMinutesOneHour() => + new MessageBusSettings().RedeliveryIntervalsSeconds.Should().Equal(60, 600, 3600); + + // Default-ON: a faulted event with no fault consumer leaves no trace outside the broker's + // error queue, which is the failure mode this consumer exists to close. + [Fact] + public void Default_RegisterFaultConsumers_IsTrue() => + new MessageBusSettings().RegisterFaultConsumers.Should().BeTrue(); + + [Fact] + public void ResilienceProperties_RoundTrip() + { + var sut = new MessageBusSettings + { + EnableInbox = true, + EnableDelayedRedelivery = true, + RedeliveryIntervalsSeconds = [5, 15], + RegisterFaultConsumers = false, + }; + + sut.EnableInbox.Should().BeTrue(); + sut.EnableDelayedRedelivery.Should().BeTrue(); + sut.RedeliveryIntervalsSeconds.Should().Equal(5, 15); + sut.RegisterFaultConsumers.Should().BeFalse(); + } +} + // ── PushNotificationSettings ── public class PushNotificationSettingsTests { diff --git a/Tests/Core/MMCA.Common.Infrastructure.Tests/packages.lock.json b/Tests/Core/MMCA.Common.Infrastructure.Tests/packages.lock.json index 963b9573..2ff0c222 100644 --- a/Tests/Core/MMCA.Common.Infrastructure.Tests/packages.lock.json +++ b/Tests/Core/MMCA.Common.Infrastructure.Tests/packages.lock.json @@ -697,11 +697,6 @@ "resolved": "2.2.16", "contentHash": "nonu9l0YrZH6krVYbskBjE7uG0VMAnvOQ9PU+RmzUsy+++vHkqzzCkHRoP877OiA3iRTxJyNDLfpcU8hrJ3/YQ==" }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" - }, "Polly.Extensions": { "type": "Transitive", "resolved": "8.4.2", @@ -911,6 +906,7 @@ "Microsoft.Extensions.Caching.Hybrid": "[10.9.0, )", "Microsoft.Extensions.Http.Resilience": "[10.9.0, )", "MiniProfiler.EntityFrameworkCore": "[4.5.4, )", + "Polly.Core": "[8.7.0, )", "SQLitePCLRaw.bundle_e_sqlite3": "[3.0.5, )", "SixLabors.ImageSharp": "[3.1.12, )", "StackExchange.Redis": "[2.13.17, )" @@ -1135,6 +1131,12 @@ "MiniProfiler.Shared": "4.5.4" } }, + "Polly.Core": { + "type": "CentralTransitive", + "requested": "[8.7.0, )", + "resolved": "8.7.0", + "contentHash": "BS2t+nsBer16PIebCEPNBK5fgMisADQyRCd7K+BgkMWpFmSaiYE+rVVNpFhGRqUkJmNSLmw0uCNzHHWfgml28Q==" + }, "Scrutor": { "type": "CentralTransitive", "requested": "[7.0.0, )", @@ -1190,7 +1192,7 @@ }, "xunit.v3.extensibility.core": { "type": "CentralTransitive", - "requested": "[3.2.2, )", + "requested": "[4.0.0, )", "resolved": "4.0.0", "contentHash": "+tTe9VX2vwrUHGI48FE4ly956Ry084wPH4I/7g5D36AkAMGjQRZZDVz8eH2GwX/CLS9PDQRSca534WyrAYsyzw==", "dependencies": { diff --git a/Tests/Core/MMCA.Common.Shared.Tests/packages.lock.json b/Tests/Core/MMCA.Common.Shared.Tests/packages.lock.json index c75c2515..0d0d2680 100644 --- a/Tests/Core/MMCA.Common.Shared.Tests/packages.lock.json +++ b/Tests/Core/MMCA.Common.Shared.Tests/packages.lock.json @@ -176,7 +176,7 @@ }, "xunit.v3.extensibility.core": { "type": "CentralTransitive", - "requested": "[3.2.2, )", + "requested": "[4.0.0, )", "resolved": "4.0.0", "contentHash": "+tTe9VX2vwrUHGI48FE4ly956Ry084wPH4I/7g5D36AkAMGjQRZZDVz8eH2GwX/CLS9PDQRSca534WyrAYsyzw==", "dependencies": { diff --git a/Tests/Hosting/MMCA.Common.Aspire.Tests/packages.lock.json b/Tests/Hosting/MMCA.Common.Aspire.Tests/packages.lock.json index 99e9a3df..7edaec3a 100644 --- a/Tests/Hosting/MMCA.Common.Aspire.Tests/packages.lock.json +++ b/Tests/Hosting/MMCA.Common.Aspire.Tests/packages.lock.json @@ -318,11 +318,6 @@ "resolved": "2.2.8", "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==" }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" - }, "Polly.Extensions": { "type": "Transitive", "resolved": "8.4.2", @@ -653,6 +648,12 @@ "OpenTelemetry.Api": "[1.17.0, 2.0.0)" } }, + "Polly.Core": { + "type": "CentralTransitive", + "requested": "[8.7.0, )", + "resolved": "8.4.2", + "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" + }, "StackExchange.Redis": { "type": "CentralTransitive", "requested": "[2.13.17, )", @@ -674,7 +675,7 @@ }, "xunit.v3.extensibility.core": { "type": "CentralTransitive", - "requested": "[3.2.2, )", + "requested": "[4.0.0, )", "resolved": "4.0.0", "contentHash": "+tTe9VX2vwrUHGI48FE4ly956Ry084wPH4I/7g5D36AkAMGjQRZZDVz8eH2GwX/CLS9PDQRSca534WyrAYsyzw==", "dependencies": { diff --git a/Tests/Hosting/MMCA.Common.Testing.Tests/packages.lock.json b/Tests/Hosting/MMCA.Common.Testing.Tests/packages.lock.json index 054c6939..3e28635a 100644 --- a/Tests/Hosting/MMCA.Common.Testing.Tests/packages.lock.json +++ b/Tests/Hosting/MMCA.Common.Testing.Tests/packages.lock.json @@ -764,7 +764,7 @@ "System.IdentityModel.Tokens.Jwt": "[8.22.0, )", "Testcontainers.MsSql": "[4.14.0, )", "Testcontainers.RabbitMq": "[4.14.0, )", - "xunit.v3.extensibility.core": "[3.2.2, )" + "xunit.v3.extensibility.core": "[4.0.0, )" } }, "Azure.Identity": { @@ -933,7 +933,7 @@ }, "xunit.v3.extensibility.core": { "type": "CentralTransitive", - "requested": "[3.2.2, )", + "requested": "[4.0.0, )", "resolved": "4.0.0", "contentHash": "+tTe9VX2vwrUHGI48FE4ly956Ry084wPH4I/7g5D36AkAMGjQRZZDVz8eH2GwX/CLS9PDQRSca534WyrAYsyzw==", "dependencies": { diff --git a/Tests/Presentation/MMCA.Common.API.Tests/packages.lock.json b/Tests/Presentation/MMCA.Common.API.Tests/packages.lock.json index 2bb81e8b..afafa790 100644 --- a/Tests/Presentation/MMCA.Common.API.Tests/packages.lock.json +++ b/Tests/Presentation/MMCA.Common.API.Tests/packages.lock.json @@ -804,11 +804,6 @@ "resolved": "2.2.16", "contentHash": "nonu9l0YrZH6krVYbskBjE7uG0VMAnvOQ9PU+RmzUsy+++vHkqzzCkHRoP877OiA3iRTxJyNDLfpcU8hrJ3/YQ==" }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" - }, "Polly.Extensions": { "type": "Transitive", "resolved": "8.4.2", @@ -996,7 +991,7 @@ "Microsoft.AspNetCore.OpenApi": "[10.0.11, )", "Microsoft.FeatureManagement.AspNetCore": "[4.6.0, )", "Microsoft.OpenApi": "[2.12.0, )", - "Scalar.AspNetCore": "[2.16.18, )" + "Scalar.AspNetCore": "[2.16.20, )" } }, "mmca.common.application": { @@ -1035,6 +1030,7 @@ "Microsoft.Extensions.Caching.Hybrid": "[10.9.0, )", "Microsoft.Extensions.Http.Resilience": "[10.9.0, )", "MiniProfiler.EntityFrameworkCore": "[4.5.4, )", + "Polly.Core": "[8.7.0, )", "SQLitePCLRaw.bundle_e_sqlite3": "[3.0.5, )", "SixLabors.ImageSharp": "[3.1.12, )", "StackExchange.Redis": "[2.13.17, )" @@ -1369,11 +1365,17 @@ "MiniProfiler.Shared": "4.5.4" } }, + "Polly.Core": { + "type": "CentralTransitive", + "requested": "[8.7.0, )", + "resolved": "8.7.0", + "contentHash": "BS2t+nsBer16PIebCEPNBK5fgMisADQyRCd7K+BgkMWpFmSaiYE+rVVNpFhGRqUkJmNSLmw0uCNzHHWfgml28Q==" + }, "Scalar.AspNetCore": { "type": "CentralTransitive", - "requested": "[2.16.18, )", - "resolved": "2.16.18", - "contentHash": "R/S9FSpMLjdd75uOe22YPNnfDtPYygK31feG6XJ9BLjX/y4WM8cGRzwT49iEBKhnJB2CnWUnfLlWGV7TsVDk+Q==" + "requested": "[2.16.20, )", + "resolved": "2.16.20", + "contentHash": "pSmyME4FCnYocbLmn9lmAUTVVSEPb5E6noqRcmJYpO65eaum68gw17yMORbeckW+gMksiL04m+zXoTxOKv0+kQ==" }, "Scrutor": { "type": "CentralTransitive", @@ -1430,7 +1432,7 @@ }, "xunit.v3.extensibility.core": { "type": "CentralTransitive", - "requested": "[3.2.2, )", + "requested": "[4.0.0, )", "resolved": "4.0.0", "contentHash": "+tTe9VX2vwrUHGI48FE4ly956Ry084wPH4I/7g5D36AkAMGjQRZZDVz8eH2GwX/CLS9PDQRSca534WyrAYsyzw==", "dependencies": { diff --git a/Tests/Presentation/MMCA.Common.Grpc.Tests/packages.lock.json b/Tests/Presentation/MMCA.Common.Grpc.Tests/packages.lock.json index 036dc3c2..b8b6e5d0 100644 --- a/Tests/Presentation/MMCA.Common.Grpc.Tests/packages.lock.json +++ b/Tests/Presentation/MMCA.Common.Grpc.Tests/packages.lock.json @@ -399,11 +399,6 @@ "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" - }, "Polly.Extensions": { "type": "Transitive", "resolved": "8.4.2", @@ -605,9 +600,15 @@ "Microsoft.Extensions.ServiceDiscovery.Abstractions": "10.9.0" } }, + "Polly.Core": { + "type": "CentralTransitive", + "requested": "[8.7.0, )", + "resolved": "8.4.2", + "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" + }, "xunit.v3.extensibility.core": { "type": "CentralTransitive", - "requested": "[3.2.2, )", + "requested": "[4.0.0, )", "resolved": "4.0.0", "contentHash": "+tTe9VX2vwrUHGI48FE4ly956Ry084wPH4I/7g5D36AkAMGjQRZZDVz8eH2GwX/CLS9PDQRSca534WyrAYsyzw==", "dependencies": { diff --git a/Tests/Presentation/MMCA.Common.UI.Tests/packages.lock.json b/Tests/Presentation/MMCA.Common.UI.Tests/packages.lock.json index a9392799..cb82c6da 100644 --- a/Tests/Presentation/MMCA.Common.UI.Tests/packages.lock.json +++ b/Tests/Presentation/MMCA.Common.UI.Tests/packages.lock.json @@ -663,11 +663,6 @@ "resolved": "6.0.0", "contentHash": "hqTM5628jSsQiv+HGpiq3WKBl2c8v1KZfby2J6Pr7pEPlK9waPdgEO6b8A/+/xn/yZ9ulv8HuqK71ONy2tg67A==" }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.7.0", - "contentHash": "BS2t+nsBer16PIebCEPNBK5fgMisADQyRCd7K+BgkMWpFmSaiYE+rVVNpFhGRqUkJmNSLmw0uCNzHHWfgml28Q==" - }, "StyleCop.Analyzers.Unstable": { "type": "Transitive", "resolved": "1.2.0.556", @@ -926,6 +921,12 @@ "Polly.Core": "8.7.0" } }, + "Polly.Core": { + "type": "CentralTransitive", + "requested": "[8.7.0, )", + "resolved": "8.7.0", + "contentHash": "BS2t+nsBer16PIebCEPNBK5fgMisADQyRCd7K+BgkMWpFmSaiYE+rVVNpFhGRqUkJmNSLmw0uCNzHHWfgml28Q==" + }, "QRCoder": { "type": "CentralTransitive", "requested": "[1.8.0, )", @@ -957,7 +958,7 @@ }, "xunit.v3.extensibility.core": { "type": "CentralTransitive", - "requested": "[3.2.2, )", + "requested": "[4.0.0, )", "resolved": "4.0.0", "contentHash": "+tTe9VX2vwrUHGI48FE4ly956Ry084wPH4I/7g5D36AkAMGjQRZZDVz8eH2GwX/CLS9PDQRSca534WyrAYsyzw==", "dependencies": { diff --git a/Tests/Presentation/MMCA.Common.UI.Web.Tests/packages.lock.json b/Tests/Presentation/MMCA.Common.UI.Web.Tests/packages.lock.json index 6acb258d..fa9d54e4 100644 --- a/Tests/Presentation/MMCA.Common.UI.Web.Tests/packages.lock.json +++ b/Tests/Presentation/MMCA.Common.UI.Web.Tests/packages.lock.json @@ -513,11 +513,6 @@ "resolved": "2.2.16", "contentHash": "nonu9l0YrZH6krVYbskBjE7uG0VMAnvOQ9PU+RmzUsy+++vHkqzzCkHRoP877OiA3iRTxJyNDLfpcU8hrJ3/YQ==" }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.7.0", - "contentHash": "BS2t+nsBer16PIebCEPNBK5fgMisADQyRCd7K+BgkMWpFmSaiYE+rVVNpFhGRqUkJmNSLmw0uCNzHHWfgml28Q==" - }, "Polly.Extensions": { "type": "Transitive", "resolved": "8.4.2", @@ -693,7 +688,7 @@ "Microsoft.AspNetCore.OpenApi": "[10.0.11, )", "Microsoft.FeatureManagement.AspNetCore": "[4.6.0, )", "Microsoft.OpenApi": "[2.12.0, )", - "Scalar.AspNetCore": "[2.16.18, )" + "Scalar.AspNetCore": "[2.16.20, )" } }, "mmca.common.application": { @@ -754,6 +749,7 @@ "Microsoft.Extensions.Caching.Hybrid": "[10.9.0, )", "Microsoft.Extensions.Http.Resilience": "[10.9.0, )", "MiniProfiler.EntityFrameworkCore": "[4.5.4, )", + "Polly.Core": "[8.7.0, )", "SQLitePCLRaw.bundle_e_sqlite3": "[3.0.5, )", "SixLabors.ImageSharp": "[3.1.12, )", "StackExchange.Redis": "[2.13.17, )" @@ -1201,6 +1197,12 @@ "Polly.Core": "8.7.0" } }, + "Polly.Core": { + "type": "CentralTransitive", + "requested": "[8.7.0, )", + "resolved": "8.7.0", + "contentHash": "BS2t+nsBer16PIebCEPNBK5fgMisADQyRCd7K+BgkMWpFmSaiYE+rVVNpFhGRqUkJmNSLmw0uCNzHHWfgml28Q==" + }, "QRCoder": { "type": "CentralTransitive", "requested": "[1.8.0, )", @@ -1212,9 +1214,9 @@ }, "Scalar.AspNetCore": { "type": "CentralTransitive", - "requested": "[2.16.18, )", - "resolved": "2.16.18", - "contentHash": "R/S9FSpMLjdd75uOe22YPNnfDtPYygK31feG6XJ9BLjX/y4WM8cGRzwT49iEBKhnJB2CnWUnfLlWGV7TsVDk+Q==" + "requested": "[2.16.20, )", + "resolved": "2.16.20", + "contentHash": "pSmyME4FCnYocbLmn9lmAUTVVSEPb5E6noqRcmJYpO65eaum68gw17yMORbeckW+gMksiL04m+zXoTxOKv0+kQ==" }, "Scrutor": { "type": "CentralTransitive", @@ -1269,7 +1271,7 @@ }, "xunit.v3.extensibility.core": { "type": "CentralTransitive", - "requested": "[3.2.2, )", + "requested": "[4.0.0, )", "resolved": "4.0.0", "contentHash": "+tTe9VX2vwrUHGI48FE4ly956Ry084wPH4I/7g5D36AkAMGjQRZZDVz8eH2GwX/CLS9PDQRSca534WyrAYsyzw==", "dependencies": { From 03e7b5feed6f4fcc4980cca91a338071d1fe04e6 Mon Sep 17 00:00:00 2001 From: Ivan Ball-llovera Date: Tue, 18 Aug 2026 10:29:56 -0400 Subject: [PATCH 2/4] feat: authorization + timeout CQRS decorators, rate-limiting settings with sliding-window and Redis option, feature-flag targeting (A4/A6/A7) - Pipeline order now FeatureGate -> Authorization -> Logging -> Caching -> Validating -> Timeout -> Transactional (queries analogous); enforced list updated in DecoratorPipelineOrderTestsBase; ADR-014 docs updated. - Authorization: IRequiresPermission checked via IPermissionRegistry + ICurrentUserService.Roles; denial = Forbidden error + cqrs.authorization.denied.count. Outside caching by design. - Timeout: IHasTimeout linked-token budget; expiry = Request.TimedOut failure + cqrs.timeout.count; caller cancellation still propagates. - RateLimitingSettings (section RateLimiting): algorithm FixedWindow or SlidingWindow, SegmentsPerWindow, Distributed; IConfiguration overload; Redis-backed fixed-window limiter (INCR+EXPIRE, fail-open) for the global and UserPolicy partitions; auth-ip stays in-memory. - CurrentUserTargetingContextAccessor + WithTargeting: percentage/targeting rollouts now work through the existing IFeatureGated pipeline unchanged. - 56 new tests (3451 total green). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RDQ4jE9QP6pC1ShZVG8ov5 --- CLAUDE.md | 6 +- .../DependencyInjection.cs | 43 +++- .../AuthorizationCommandDecorator.cs | 73 ++++++ .../Decorators/AuthorizationQueryDecorator.cs | 68 ++++++ .../UseCases/Decorators/CqrsMetrics.cs | 27 ++ .../Decorators/TimeoutCommandDecorator.cs | 87 +++++++ .../Decorators/TimeoutQueryDecorator.cs | 87 +++++++ .../UseCases/IHasTimeout.cs | 22 ++ .../UseCases/IRequiresPermission.cs | 24 ++ .../DecoratorPipelineOrderTestsBase.cs | 18 +- .../MMCA.Common.API/DependencyInjection.cs | 11 +- .../CurrentUserTargetingContextAccessor.cs | 90 +++++++ .../RateLimiting/RateLimitAlgorithm.cs | 23 ++ .../RateLimiting/RateLimitingSettings.cs | 73 ++++++ .../RedisFixedWindowRateLimiter.cs | 191 +++++++++++++++ .../WebApplicationBuilderExtensions.cs | 230 +++++++++++++++--- .../AuthorizationCommandDecoratorTests.cs | 155 ++++++++++++ .../AuthorizationQueryDecoratorTests.cs | 103 ++++++++ .../TimeoutCommandDecoratorTests.cs | 152 ++++++++++++ .../Decorators/TimeoutQueryDecoratorTests.cs | 130 ++++++++++ .../DecoratorPipelineOrderTests.cs | 3 + ...urrentUserTargetingContextAccessorTests.cs | 94 +++++++ .../RateLimiting/RateLimitingSettingsTests.cs | 94 +++++++ .../RedisFixedWindowRateLimiterTests.cs | 220 +++++++++++++++++ .../RateLimitAlgorithmSelectionTests.cs | 158 ++++++++++++ 25 files changed, 2123 insertions(+), 59 deletions(-) create mode 100644 Source/Core/MMCA.Common.Application/UseCases/Decorators/AuthorizationCommandDecorator.cs create mode 100644 Source/Core/MMCA.Common.Application/UseCases/Decorators/AuthorizationQueryDecorator.cs create mode 100644 Source/Core/MMCA.Common.Application/UseCases/Decorators/TimeoutCommandDecorator.cs create mode 100644 Source/Core/MMCA.Common.Application/UseCases/Decorators/TimeoutQueryDecorator.cs create mode 100644 Source/Core/MMCA.Common.Application/UseCases/IHasTimeout.cs create mode 100644 Source/Core/MMCA.Common.Application/UseCases/IRequiresPermission.cs create mode 100644 Source/Presentation/MMCA.Common.API/FeatureManagement/CurrentUserTargetingContextAccessor.cs create mode 100644 Source/Presentation/MMCA.Common.API/RateLimiting/RateLimitAlgorithm.cs create mode 100644 Source/Presentation/MMCA.Common.API/RateLimiting/RateLimitingSettings.cs create mode 100644 Source/Presentation/MMCA.Common.API/RateLimiting/RedisFixedWindowRateLimiter.cs create mode 100644 Tests/Core/MMCA.Common.Application.Tests/Decorators/AuthorizationCommandDecoratorTests.cs create mode 100644 Tests/Core/MMCA.Common.Application.Tests/Decorators/AuthorizationQueryDecoratorTests.cs create mode 100644 Tests/Core/MMCA.Common.Application.Tests/Decorators/TimeoutCommandDecoratorTests.cs create mode 100644 Tests/Core/MMCA.Common.Application.Tests/Decorators/TimeoutQueryDecoratorTests.cs create mode 100644 Tests/Presentation/MMCA.Common.API.Tests/FeatureManagement/CurrentUserTargetingContextAccessorTests.cs create mode 100644 Tests/Presentation/MMCA.Common.API.Tests/RateLimiting/RateLimitingSettingsTests.cs create mode 100644 Tests/Presentation/MMCA.Common.API.Tests/RateLimiting/RedisFixedWindowRateLimiterTests.cs create mode 100644 Tests/Presentation/MMCA.Common.API.Tests/Startup/RateLimitAlgorithmSelectionTests.cs diff --git a/CLAUDE.md b/CLAUDE.md index ae902a97..4184cb0d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -75,14 +75,16 @@ Downstream apps register `AddApplicationDecorators()` **last**: Scrutor `TryDeco `ICommandHandler` / `IQueryHandler` with decorators registered in `AddApplicationDecorators()` and applied by Scrutor `TryDecorate` in reverse registration order (last registered = outermost). Execution order, outermost to innermost (ADR-014): ``` -Commands: FeatureGate -> Logging -> Caching -> Validating -> Transactional -> Handler -Queries: FeatureGate -> Logging -> Caching -> Handler +Commands: FeatureGate -> Authorization -> Logging -> Caching -> Validating -> Timeout -> Transactional -> Handler +Queries: FeatureGate -> Authorization -> Logging -> Caching -> Timeout -> Handler ``` - **FeatureGate**: short-circuits when the command/query's feature flag is off. +- **Authorization**: `IRequiresPermission` commands/queries are checked against `IPermissionRegistry.HasPermission(ICurrentUserService.Roles, ...)`; a denial short-circuits with a `Forbidden` error and increments `cqrs.authorization.denied.count`. Sits outside caching on purpose, so a denied query never reads or populates the cache. - **Logging**: full pipeline duration via `ICorrelationContext`. - **Caching**: `ICacheInvalidating` commands invalidate on success (outside the transaction); `IQueryCacheable` queries (with `CacheKey` + `CacheDuration`) cache results. - **Validating**: FluentValidation before the transaction opens; queries have no Validating or Transactional decorator. +- **Timeout**: `IHasTimeout` commands/queries run under a linked token cancelled after their own budget; expiry returns a `Request.TimedOut` failure and increments `cqrs.timeout.count`, while caller cancellation still propagates as an exception. A budget of zero or less passes through. - **Transactional**: `ITransactional` commands get a DB transaction; exceptions AND business failures (`Result.Failure`) roll back (atomicity over partial persistence). In-process domain event dispatch is deferred until after a successful commit (`DbContextFactory.ExecuteInTransactionAsync` flushes it post-commit and drops it on rollback), so handlers never act on state that could still roll back; cache invalidation still runs only on success, outside the transaction. An optional `Profiling` decorator pair is registered by a separate opt-in `AddApplicationProfiling()` call and is not wired by any host today. diff --git a/Source/Core/MMCA.Common.Application/DependencyInjection.cs b/Source/Core/MMCA.Common.Application/DependencyInjection.cs index 1daa6a2d..c9776022 100644 --- a/Source/Core/MMCA.Common.Application/DependencyInjection.cs +++ b/Source/Core/MMCA.Common.Application/DependencyInjection.cs @@ -52,33 +52,46 @@ public IServiceCollection AddApplication() /// /// Command pipeline (nesting from outermost to innermost): /// - /// FeatureGateCommandDecorator ← outermost: short-circuits if feature flag disabled - /// → LoggingCommandDecorator ← logs start/end, captures full pipeline duration - /// → CachingCommandDecorator ← invalidates cache AFTER transaction commits - /// → ValidatingCommandDecorator ← short-circuits with Result.Failure on validation errors - /// → TransactionalCommandDecorator ← wraps handler in DB transaction (if ITransactional) - /// → ConcreteHandler ← the actual business logic + /// FeatureGateCommandDecorator ← outermost: short-circuits if feature flag disabled + /// → AuthorizationCommandDecorator ← short-circuits with Forbidden (if IRequiresPermission) + /// → LoggingCommandDecorator ← logs start/end, captures full pipeline duration + /// → CachingCommandDecorator ← invalidates cache AFTER transaction commits + /// → ValidatingCommandDecorator ← short-circuits with Result.Failure on validation errors + /// → TimeoutCommandDecorator ← applies the command's own budget (if IHasTimeout) + /// → TransactionalCommandDecorator ← wraps handler in DB transaction (if ITransactional) + /// → ConcreteHandler ← the actual business logic /// /// /// /// Query pipeline (nesting from outermost to innermost): /// - /// FeatureGateQueryDecorator ← outermost: short-circuits if feature flag disabled - /// → LoggingQueryDecorator ← logs start/end, captures full pipeline duration - /// → CachingQueryDecorator ← innermost: caches results (if IQueryCacheable) - /// → ConcreteHandler ← the actual query logic + /// FeatureGateQueryDecorator ← outermost: short-circuits if feature flag disabled + /// → AuthorizationQueryDecorator ← short-circuits with Forbidden (if IRequiresPermission) + /// → LoggingQueryDecorator ← logs start/end, captures full pipeline duration + /// → CachingQueryDecorator ← caches results (if IQueryCacheable) + /// → TimeoutQueryDecorator ← innermost: applies the query's own budget (if IHasTimeout) + /// → ConcreteHandler ← the actual query logic /// /// /// /// Design rationale: /// /// Feature gating is outermost so disabled features are rejected immediately with zero - /// overhead — no logging, caching, validation, or transaction work. + /// overhead: no authorization, logging, caching, validation, or transaction work. It also + /// sits outside authorization deliberately: a feature that is off must answer the same way + /// for every caller rather than leaking which permission guards it. + /// Authorization sits directly inside feature gating and outside caching, so a denied + /// request neither reads nor populates the cache: a cache lookup ahead of the permission + /// check would serve another caller's rows to a principal not allowed to run the query. /// Logging sits inside feature gating so it only measures enabled feature executions. /// Validation sits outside the transaction boundary so invalid commands never start /// a database transaction — saving resources on malformed requests. /// Cache invalidation sits outside validation so cache is only cleared after a valid, /// committed mutation — a rollback or validation failure leaves cache intact. + /// The timeout budget sits inside validation and outside the transaction, so it covers + /// the database work that actually hangs, does not charge the caller for validation, and + /// cancels the transaction instead of leaving it open. On the query side it is innermost, so + /// a cache hit is served without starting a budget at all. /// On business failure (.IsFailure), the transaction is rolled /// back (atomicity over partial persistence) and cache invalidation is skipped. /// On exception, the transaction rolls back and the exception propagates through all decorators. @@ -92,14 +105,18 @@ public IServiceCollection AddApplicationDecorators() // Registered first = innermost (wraps the concrete handler directly). // Registered last = outermost (wraps all other decorators). services.TryDecorate(typeof(ICommandHandler<,>), typeof(TransactionalCommandDecorator<,>)); // innermost - services.TryDecorate(typeof(ICommandHandler<,>), typeof(ValidatingCommandDecorator<,>)); // validates before transaction + services.TryDecorate(typeof(ICommandHandler<,>), typeof(TimeoutCommandDecorator<,>)); // per-command execution budget + services.TryDecorate(typeof(ICommandHandler<,>), typeof(ValidatingCommandDecorator<,>)); // validates before the budget and transaction services.TryDecorate(typeof(ICommandHandler<,>), typeof(CachingCommandDecorator<,>)); // cache invalidation services.TryDecorate(typeof(ICommandHandler<,>), typeof(LoggingCommandDecorator<,>)); // logging + services.TryDecorate(typeof(ICommandHandler<,>), typeof(AuthorizationCommandDecorator<,>)); // permission check services.TryDecorate(typeof(ICommandHandler<,>), typeof(FeatureGateCommandDecorator<,>)); // outermost — feature flag check // ── Query decorators ──────────────────────────────────────── - services.TryDecorate(typeof(IQueryHandler<,>), typeof(CachingQueryDecorator<,>)); // innermost + services.TryDecorate(typeof(IQueryHandler<,>), typeof(TimeoutQueryDecorator<,>)); // innermost: per-query execution budget + services.TryDecorate(typeof(IQueryHandler<,>), typeof(CachingQueryDecorator<,>)); // caching services.TryDecorate(typeof(IQueryHandler<,>), typeof(LoggingQueryDecorator<,>)); // logging + services.TryDecorate(typeof(IQueryHandler<,>), typeof(AuthorizationQueryDecorator<,>)); // permission check services.TryDecorate(typeof(IQueryHandler<,>), typeof(FeatureGateQueryDecorator<,>)); // outermost — feature flag check return services; diff --git a/Source/Core/MMCA.Common.Application/UseCases/Decorators/AuthorizationCommandDecorator.cs b/Source/Core/MMCA.Common.Application/UseCases/Decorators/AuthorizationCommandDecorator.cs new file mode 100644 index 00000000..e81155c8 --- /dev/null +++ b/Source/Core/MMCA.Common.Application/UseCases/Decorators/AuthorizationCommandDecorator.cs @@ -0,0 +1,73 @@ +using MMCA.Common.Application.Interfaces.Infrastructure; +using MMCA.Common.Shared.Abstractions; +using MMCA.Common.Shared.Auth; + +namespace MMCA.Common.Application.UseCases.Decorators; + +/// +/// Decorator that checks the command's required permission before executing the inner handler. +/// Commands that do not implement pass through unchanged. When +/// none of the caller's roles grants the permission, returns a failure result with +/// without invoking the handler. +/// +/// Registered directly inside the feature gate and outside logging, so a denied command never +/// starts a transaction, never touches the cache, and never runs validation, while a disabled +/// feature is still rejected first (a feature that is off must not leak the existence of the +/// permission that guards it). +/// +/// +/// This is a defense in depth layer, not a replacement for the endpoint's [Authorize] +/// policy: it moves the capability check next to the use case, so a command reached through a new +/// transport (gRPC, a scheduled job, another module) is checked the same way it is over HTTP. +/// +/// +/// The command type. +/// The result type (typically or ). +public sealed class AuthorizationCommandDecorator( + ICommandHandler inner, + ICurrentUserService currentUser, + IPermissionRegistry permissionRegistry) : ICommandHandler +{ + /// + /// Cached delegate that creates a failure from a collection of + /// instances. Built once per generic type instantiation via reflection + /// to avoid per-call reflection overhead. + /// + /// + /// Built on the first short-circuit rather than in the static constructor, for the same reason + /// as : + /// supports only and + /// , and an eager static initializer would turn an unsupported + /// into a at RESOLVE + /// time (Scrutor's TryDecorate is unconditional) for a handler that never short-circuits. One + /// assignment per closed generic type; a benign duplicate build under a race produces an + /// equivalent delegate. The happy path never touches it. + /// + private static Func, TResult>? _createFailure; + + /// + /// Returns the failure factory, building it on first use. Kept static so the lazy assignment is + /// never a write to a static field from an instance member. + /// + private static Func, TResult> CreateFailure() + => _createFailure ??= ResultFailureFactory.Build(); + + /// + public async Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) + { + if (command is not IRequiresPermission requiresPermission) + return await inner.HandleAsync(command, cancellationToken).ConfigureAwait(false); + + if (permissionRegistry.HasPermission(currentUser.Roles, requiresPermission.Permission)) + return await inner.HandleAsync(command, cancellationToken).ConfigureAwait(false); + + var commandName = typeof(TCommand).Name; + CqrsMetrics.RecordAuthorizationDenied(commandName); + + var createFailure = CreateFailure(); + return createFailure([Error.Forbidden( + "Authorization.PermissionDenied", + $"The current user does not hold the '{requiresPermission.Permission}' permission.", + source: commandName)]); + } +} diff --git a/Source/Core/MMCA.Common.Application/UseCases/Decorators/AuthorizationQueryDecorator.cs b/Source/Core/MMCA.Common.Application/UseCases/Decorators/AuthorizationQueryDecorator.cs new file mode 100644 index 00000000..411d4fb0 --- /dev/null +++ b/Source/Core/MMCA.Common.Application/UseCases/Decorators/AuthorizationQueryDecorator.cs @@ -0,0 +1,68 @@ +using MMCA.Common.Application.Interfaces.Infrastructure; +using MMCA.Common.Shared.Abstractions; +using MMCA.Common.Shared.Auth; + +namespace MMCA.Common.Application.UseCases.Decorators; + +/// +/// Decorator that checks the query's required permission before executing the inner handler. +/// Queries that do not implement pass through unchanged. When +/// none of the caller's roles grants the permission, returns a failure result with +/// without invoking the handler. +/// +/// Registered directly inside the feature gate and outside logging and caching, so a denied query +/// neither reads nor populates the cache. That ordering is load-bearing: a cache lookup placed +/// before the permission check would serve another caller's cached rows to a principal who is not +/// allowed to run the query at all. +/// +/// +/// The query type. +/// The result type (typically or ). +public sealed class AuthorizationQueryDecorator( + IQueryHandler inner, + ICurrentUserService currentUser, + IPermissionRegistry permissionRegistry) : IQueryHandler +{ + /// + /// Cached delegate that creates a failure from a collection of + /// instances. Built once per generic type instantiation via reflection + /// to avoid per-call reflection overhead. + /// + /// + /// Built on the first short-circuit rather than in the static constructor, for the same reason + /// as : + /// supports only and + /// , and an eager static initializer would turn an unsupported + /// into a at RESOLVE + /// time (Scrutor's TryDecorate is unconditional) for a handler that never short-circuits. One + /// assignment per closed generic type; a benign duplicate build under a race produces an + /// equivalent delegate. The happy path never touches it. + /// + private static Func, TResult>? _createFailure; + + /// + /// Returns the failure factory, building it on first use. Kept static so the lazy assignment is + /// never a write to a static field from an instance member. + /// + private static Func, TResult> CreateFailure() + => _createFailure ??= ResultFailureFactory.Build(); + + /// + public async Task HandleAsync(TQuery query, CancellationToken cancellationToken = default) + { + if (query is not IRequiresPermission requiresPermission) + return await inner.HandleAsync(query, cancellationToken).ConfigureAwait(false); + + if (permissionRegistry.HasPermission(currentUser.Roles, requiresPermission.Permission)) + return await inner.HandleAsync(query, cancellationToken).ConfigureAwait(false); + + var queryName = typeof(TQuery).Name; + CqrsMetrics.RecordAuthorizationDenied(queryName); + + var createFailure = CreateFailure(); + return createFailure([Error.Forbidden( + "Authorization.PermissionDenied", + $"The current user does not hold the '{requiresPermission.Permission}' permission.", + source: queryName)]); + } +} diff --git a/Source/Core/MMCA.Common.Application/UseCases/Decorators/CqrsMetrics.cs b/Source/Core/MMCA.Common.Application/UseCases/Decorators/CqrsMetrics.cs index 93784261..c0aebe83 100644 --- a/Source/Core/MMCA.Common.Application/UseCases/Decorators/CqrsMetrics.cs +++ b/Source/Core/MMCA.Common.Application/UseCases/Decorators/CqrsMetrics.cs @@ -12,6 +12,11 @@ namespace MMCA.Common.Application.UseCases.Decorators; /// The caching query decorator adds the cache hit/miss counters below, so a host can chart the /// hit ratio per query and spot a cache that has stopped serving reads. /// +/// +/// The authorization and timeout decorators add two short-circuit counters, so a permission that +/// is denying far more traffic than expected, or a handler that keeps exhausting its execution +/// budget, is visible as a metric rather than only as a client-side error rate. +/// /// internal static class CqrsMetrics { @@ -44,6 +49,18 @@ internal static class CqrsMetrics unit: "{query}", description: "Count of cacheable queries that executed the handler because the cache did not serve them, tagged by query name."); + /// Requests short-circuited by the authorization decorators, tagged by request_type. + internal static readonly Counter AuthorizationDenied = Meter.CreateCounter( + "cqrs.authorization.denied.count", + unit: "{request}", + description: "Count of commands and queries denied by the authorization decorators, tagged by request type name."); + + /// Requests abandoned because their own execution budget expired, tagged by request_type. + internal static readonly Counter TimeoutExpired = Meter.CreateCounter( + "cqrs.timeout.count", + unit: "{request}", + description: "Count of commands and queries whose IHasTimeout budget expired before the handler completed, tagged by request type name."); + /// Records one cache hit for the named query. /// The query type name. internal static void RecordCacheHit(string queryName) => @@ -53,4 +70,14 @@ internal static void RecordCacheHit(string queryName) => /// The query type name. internal static void RecordCacheMiss(string queryName) => QueryCacheMisses.Add(1, new KeyValuePair("query", queryName)); + + /// Records one authorization denial for the named command or query. + /// The command or query type name. + internal static void RecordAuthorizationDenied(string requestTypeName) => + AuthorizationDenied.Add(1, new KeyValuePair("request_type", requestTypeName)); + + /// Records one expired execution budget for the named command or query. + /// The command or query type name. + internal static void RecordTimeout(string requestTypeName) => + TimeoutExpired.Add(1, new KeyValuePair("request_type", requestTypeName)); } diff --git a/Source/Core/MMCA.Common.Application/UseCases/Decorators/TimeoutCommandDecorator.cs b/Source/Core/MMCA.Common.Application/UseCases/Decorators/TimeoutCommandDecorator.cs new file mode 100644 index 00000000..e1f91fa2 --- /dev/null +++ b/Source/Core/MMCA.Common.Application/UseCases/Decorators/TimeoutCommandDecorator.cs @@ -0,0 +1,87 @@ +using System.Globalization; +using MMCA.Common.Shared.Abstractions; + +namespace MMCA.Common.Application.UseCases.Decorators; + +/// +/// Decorator that enforces a per-command execution budget. Commands that do not implement +/// pass through unchanged. When a command does, the decorator links a +/// cancellation source to the caller's token, cancels it after , +/// and converts the resulting cancellation into a failure result with +/// and the code Request.TimedOut. +/// +/// The framework's taxonomy has no timeout member (it maps to HTTP status +/// codes and nothing in it corresponds to 408 or 504), so an expired budget is reported as the +/// general classification. The machine-readable +/// Request.TimedOut code, not the type, is what callers branch on. +/// +/// +/// Registered inside validation and outside the transaction, so the budget covers the database +/// work (the part that actually hangs) without charging the caller for validation, and an expired +/// budget cancels the transaction rather than leaving it open. +/// +/// +/// A budget of or less is treated as "no budget" and passes through +/// with the caller's token untouched: a misconfigured value must not fail every request instantly. +/// Cancellation raised by the CALLER's token is rethrown unchanged, so a genuinely aborted request +/// still surfaces exactly as the inner handler would; only the decorator's own budget becomes a +/// failure result. +/// +/// +/// The command type. +/// The result type (typically or ). +public sealed class TimeoutCommandDecorator( + ICommandHandler inner) : ICommandHandler +{ + /// + /// Cached delegate that creates a failure from a collection of + /// instances. Built once per generic type instantiation via reflection + /// to avoid per-call reflection overhead. + /// + /// + /// Built on the first short-circuit rather than in the static constructor, for the same reason + /// as : + /// supports only and + /// , and an eager static initializer would turn an unsupported + /// into a at RESOLVE + /// time (Scrutor's TryDecorate is unconditional) for a handler that never times out. One + /// assignment per closed generic type; a benign duplicate build under a race produces an + /// equivalent delegate. The happy path never touches it. + /// + private static Func, TResult>? _createFailure; + + /// + /// Returns the failure factory, building it on first use. Kept static so the lazy assignment is + /// never a write to a static field from an instance member. + /// + private static Func, TResult> CreateFailure() + => _createFailure ??= ResultFailureFactory.Build(); + + /// + public async Task HandleAsync(TCommand command, CancellationToken cancellationToken = default) + { + if (command is not IHasTimeout hasTimeout || hasTimeout.Timeout <= TimeSpan.Zero) + return await inner.HandleAsync(command, cancellationToken).ConfigureAwait(false); + + using var budget = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + budget.CancelAfter(hasTimeout.Timeout); + + try + { + return await inner.HandleAsync(command, budget.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (budget.IsCancellationRequested && !cancellationToken.IsCancellationRequested) + { + var commandName = typeof(TCommand).Name; + CqrsMetrics.RecordTimeout(commandName); + + var createFailure = CreateFailure(); + return createFailure([Error.Failure( + "Request.TimedOut", + string.Create( + CultureInfo.InvariantCulture, + $"The command did not complete within its {hasTimeout.Timeout.TotalSeconds:0.###}s budget."), + source: commandName)]); + } + } +} diff --git a/Source/Core/MMCA.Common.Application/UseCases/Decorators/TimeoutQueryDecorator.cs b/Source/Core/MMCA.Common.Application/UseCases/Decorators/TimeoutQueryDecorator.cs new file mode 100644 index 00000000..f14233a0 --- /dev/null +++ b/Source/Core/MMCA.Common.Application/UseCases/Decorators/TimeoutQueryDecorator.cs @@ -0,0 +1,87 @@ +using System.Globalization; +using MMCA.Common.Shared.Abstractions; + +namespace MMCA.Common.Application.UseCases.Decorators; + +/// +/// Decorator that enforces a per-query execution budget. Queries that do not implement +/// pass through unchanged. When a query does, the decorator links a +/// cancellation source to the caller's token, cancels it after , +/// and converts the resulting cancellation into a failure result with +/// and the code Request.TimedOut. +/// +/// The framework's taxonomy has no timeout member (it maps to HTTP status +/// codes and nothing in it corresponds to 408 or 504), so an expired budget is reported as the +/// general classification. The machine-readable +/// Request.TimedOut code, not the type, is what callers branch on. +/// +/// +/// Registered as the innermost query decorator, so the budget covers the handler's own work only: +/// a cache hit is served before the budget is even started, and a timed-out execution returns a +/// failure that the caching decorator refuses to cache. +/// +/// +/// A budget of or less is treated as "no budget" and passes through +/// with the caller's token untouched: a misconfigured value must not fail every request instantly. +/// Cancellation raised by the CALLER's token is rethrown unchanged, so a genuinely aborted request +/// still surfaces exactly as the inner handler would; only the decorator's own budget becomes a +/// failure result. +/// +/// +/// The query type. +/// The result type (typically or ). +public sealed class TimeoutQueryDecorator( + IQueryHandler inner) : IQueryHandler +{ + /// + /// Cached delegate that creates a failure from a collection of + /// instances. Built once per generic type instantiation via reflection + /// to avoid per-call reflection overhead. + /// + /// + /// Built on the first short-circuit rather than in the static constructor, for the same reason + /// as : + /// supports only and + /// , and an eager static initializer would turn an unsupported + /// into a at RESOLVE + /// time (Scrutor's TryDecorate is unconditional) for a handler that never times out. One + /// assignment per closed generic type; a benign duplicate build under a race produces an + /// equivalent delegate. The happy path never touches it. + /// + private static Func, TResult>? _createFailure; + + /// + /// Returns the failure factory, building it on first use. Kept static so the lazy assignment is + /// never a write to a static field from an instance member. + /// + private static Func, TResult> CreateFailure() + => _createFailure ??= ResultFailureFactory.Build(); + + /// + public async Task HandleAsync(TQuery query, CancellationToken cancellationToken = default) + { + if (query is not IHasTimeout hasTimeout || hasTimeout.Timeout <= TimeSpan.Zero) + return await inner.HandleAsync(query, cancellationToken).ConfigureAwait(false); + + using var budget = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + budget.CancelAfter(hasTimeout.Timeout); + + try + { + return await inner.HandleAsync(query, budget.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (budget.IsCancellationRequested && !cancellationToken.IsCancellationRequested) + { + var queryName = typeof(TQuery).Name; + CqrsMetrics.RecordTimeout(queryName); + + var createFailure = CreateFailure(); + return createFailure([Error.Failure( + "Request.TimedOut", + string.Create( + CultureInfo.InvariantCulture, + $"The query did not complete within its {hasTimeout.Timeout.TotalSeconds:0.###}s budget."), + source: queryName)]); + } + } +} diff --git a/Source/Core/MMCA.Common.Application/UseCases/IHasTimeout.cs b/Source/Core/MMCA.Common.Application/UseCases/IHasTimeout.cs new file mode 100644 index 00000000..4dcd5e4d --- /dev/null +++ b/Source/Core/MMCA.Common.Application/UseCases/IHasTimeout.cs @@ -0,0 +1,22 @@ +namespace MMCA.Common.Application.UseCases; + +/// +/// Marker interface for commands and queries that carry their own execution budget. +/// The and +/// link a cancellation source to the +/// caller's token, cancel it after , and convert the resulting cancellation +/// into a failure result rather than letting it surface as an exception. +/// +/// Opting in is per request type: a command or query that does not implement this interface passes +/// through the decorator untouched and keeps the caller's token unchanged. +/// +/// +public interface IHasTimeout +{ + /// + /// The execution budget for this request. A value less than or equal to + /// disables the budget and passes the request through with the + /// caller's token, which keeps a misconfigured value from failing every request instantly. + /// + TimeSpan Timeout { get; } +} diff --git a/Source/Core/MMCA.Common.Application/UseCases/IRequiresPermission.cs b/Source/Core/MMCA.Common.Application/UseCases/IRequiresPermission.cs new file mode 100644 index 00000000..0b886927 --- /dev/null +++ b/Source/Core/MMCA.Common.Application/UseCases/IRequiresPermission.cs @@ -0,0 +1,24 @@ +namespace MMCA.Common.Application.UseCases; + +/// +/// Marker interface for commands and queries that require a fine-grained permission. +/// The and +/// resolve the caller's roles +/// from ICurrentUserService, ask IPermissionRegistry whether any of them grants +/// , and short-circuit with a +/// failure when none does. +/// +/// Opting in is per request type: a command or query that does not implement this interface passes +/// through the decorator untouched, so endpoint-level [Authorize] policies remain the only +/// gate for everything that has not opted in. +/// +/// +public interface IRequiresPermission +{ + /// + /// The permission the caller must hold (e.g. "catalog.products.write"). + /// Must match a value the host's IPermissionRegistry knows about; an unknown permission + /// is granted by no role and therefore denies every caller. + /// + string Permission { get; } +} diff --git a/Source/Hosting/MMCA.Common.Testing/DecoratorPipelineOrderTestsBase.cs b/Source/Hosting/MMCA.Common.Testing/DecoratorPipelineOrderTestsBase.cs index cae39c02..4e4e27aa 100644 --- a/Source/Hosting/MMCA.Common.Testing/DecoratorPipelineOrderTestsBase.cs +++ b/Source/Hosting/MMCA.Common.Testing/DecoratorPipelineOrderTestsBase.cs @@ -11,15 +11,17 @@ namespace MMCA.Common.Testing; /// through the repo's own registration sequence, resolves the /// decorated command/query handlers from the built provider, and asserts the runtime nesting order /// is exactly the documented pipeline — -/// commands: FeatureGate → Logging → Caching → Validating → Transactional → Handler; -/// queries: FeatureGate → Logging → Caching → Handler. Because Scrutor's TryDecorate applies -/// decorators in reverse registration order, an innocent-looking reorder of the -/// AddApplicationDecorators() lines (or a module scan registered after it) silently changes -/// runtime behavior; this base turns that into a test failure. +/// commands: FeatureGate → Authorization → Logging → Caching → Validating → Timeout → Transactional +/// → Handler; +/// queries: FeatureGate → Authorization → Logging → Caching → Timeout → Handler. Because Scrutor's +/// TryDecorate applies decorators in reverse registration order, an innocent-looking reorder +/// of the AddApplicationDecorators() lines (or a module scan registered after it) silently +/// changes runtime behavior; this base turns that into a test failure. /// /// Subclass with one representative command/query pair and implement /// to (1) register test doubles for the decorator dependencies -/// (IFeatureManager, ICorrelationContext, ICacheService, IUnitOfWork, +/// (IFeatureManager, ICurrentUserService, IPermissionRegistry, +/// ICorrelationContext, ICacheService, IUnitOfWork, /// ILogger<>), then (2) run the repo's real registration sequence, e.g. /// services.AddApplication().ScanModuleApplicationServices<MyMarker>().AddApplicationDecorators(), /// where the scanned assembly contains a concrete handler for each of the four type parameters. @@ -47,9 +49,11 @@ public abstract class DecoratorPipelineOrderTestsBase ExpectedCommandDecorators => [ "FeatureGateCommandDecorator", + "AuthorizationCommandDecorator", "LoggingCommandDecorator", "CachingCommandDecorator", "ValidatingCommandDecorator", + "TimeoutCommandDecorator", "TransactionalCommandDecorator", ]; @@ -57,8 +61,10 @@ public abstract class DecoratorPipelineOrderTestsBase ExpectedQueryDecorators => [ "FeatureGateQueryDecorator", + "AuthorizationQueryDecorator", "LoggingQueryDecorator", "CachingQueryDecorator", + "TimeoutQueryDecorator", ]; [Fact] diff --git a/Source/Presentation/MMCA.Common.API/DependencyInjection.cs b/Source/Presentation/MMCA.Common.API/DependencyInjection.cs index 5cc92b90..e5a5f059 100644 --- a/Source/Presentation/MMCA.Common.API/DependencyInjection.cs +++ b/Source/Presentation/MMCA.Common.API/DependencyInjection.cs @@ -80,7 +80,16 @@ public IServiceCollection AddAPI(ModulesSettings? modulesSettings = null, IConfi // Feature Management — registers IFeatureManager, IFeatureManagerSnapshot, // and built-in filters (Percentage, TimeWindow, Targeting). // Feature flags are read from the "FeatureManagement" configuration section. - services.AddFeatureManagement(); + // + // WithTargeting supplies the Targeting filter's audience from the current request's + // principal (see CurrentUserTargetingContextAccessor), which is what makes a percentage + // rollout sticky per user instead of random per request. The accessor is registered as + // a singleton by WithTargeting, so it reads IHttpContextAccessor rather than the scoped + // ICurrentUserService; AddHttpContextAccessor is TryAdd-based and therefore safe to + // call here as well as in AddServerAuthSessionCookie. + services.AddHttpContextAccessor(); + services.AddFeatureManagement() + .WithTargeting(); services.AddSingleton(); // Server-side error-message localization at the HTTP edge, keyed by Error.Code (ADR-027). diff --git a/Source/Presentation/MMCA.Common.API/FeatureManagement/CurrentUserTargetingContextAccessor.cs b/Source/Presentation/MMCA.Common.API/FeatureManagement/CurrentUserTargetingContextAccessor.cs new file mode 100644 index 00000000..17039e58 --- /dev/null +++ b/Source/Presentation/MMCA.Common.API/FeatureManagement/CurrentUserTargetingContextAccessor.cs @@ -0,0 +1,90 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.FeatureManagement.FeatureFilters; + +namespace MMCA.Common.API.FeatureManagement; + +/// +/// Supplies the targeting context (user id plus group membership) that the feature-management +/// Targeting filter evaluates, read from the current HTTP request's principal. Registered by +/// AddAPI via AddFeatureManagement().WithTargeting<CurrentUserTargetingContextAccessor>(), +/// which turns a feature flag into a percentage rollout that is sticky per user rather than +/// random per request: the filter hashes the user id, so the same caller keeps the same answer +/// across requests and instances. +/// +/// The user id is the user_id claim emitted by TokenService (the same claim +/// CurrentUserService and IdempotencyFilter read), falling back to the principal's +/// name when a token predates it. Groups are the caller's role claims, accepting each claim type +/// the JWT middleware may produce: the standard URI when inbound +/// claim mapping is on, or the raw role / roles claim when it is off. That mirrors +/// ICurrentUserService.Roles, which cannot be used directly here because the accessor is +/// registered as a singleton while ICurrentUserService is scoped. +/// +/// +/// An anonymous request yields an empty context (no user id, no groups), so a targeted feature is +/// simply off for anonymous callers unless the audience opts everyone in. +/// +/// +/// A rollout that always includes the Organizer role, includes 25 percent of everyone else, and +/// pins two named users: +/// +/// "FeatureManagement": { +/// "Conference.NewAgenda": { +/// "EnabledFor": [ +/// { +/// "Name": "Targeting", +/// "Parameters": { +/// "Audience": { +/// "Users": [ "42", "1337" ], +/// "Groups": [ { "Name": "Organizer", "RolloutPercentage": 100 } ], +/// "DefaultRolloutPercentage": 25 +/// } +/// } +/// } +/// ] +/// } +/// } +/// +/// +/// +/// Accessor for the current request, or none outside a request. +public sealed class CurrentUserTargetingContextAccessor(IHttpContextAccessor httpContextAccessor) + : ITargetingContextAccessor +{ + /// The claim type carrying the user identifier, matching TokenService. + private const string UserIdClaimType = "user_id"; + + /// + /// Builds the targeting context for the current request. Never returns : + /// a request without a principal (background work, an anonymous call) produces an empty context + /// rather than an error, because a feature filter must not be able to fail a request. + /// + /// The targeting context for the current caller. + public ValueTask GetContextAsync() + { + var user = httpContextAccessor.HttpContext?.User; + + if (user?.Identity?.IsAuthenticated != true) + { + return ValueTask.FromResult(new TargetingContext + { + UserId = null, + Groups = [], + }); + } + + var groups = user.Claims + .Where(claim => + string.Equals(claim.Type, ClaimTypes.Role, StringComparison.Ordinal) + || string.Equals(claim.Type, "role", StringComparison.Ordinal) + || string.Equals(claim.Type, "roles", StringComparison.Ordinal)) + .Select(claim => claim.Value) + .ToArray(); + + return ValueTask.FromResult(new TargetingContext + { + UserId = user.FindFirst(UserIdClaimType)?.Value ?? user.Identity.Name, + Groups = groups, + }); + } +} diff --git a/Source/Presentation/MMCA.Common.API/RateLimiting/RateLimitAlgorithm.cs b/Source/Presentation/MMCA.Common.API/RateLimiting/RateLimitAlgorithm.cs new file mode 100644 index 00000000..083b0ffa --- /dev/null +++ b/Source/Presentation/MMCA.Common.API/RateLimiting/RateLimitAlgorithm.cs @@ -0,0 +1,23 @@ +namespace MMCA.Common.API.RateLimiting; + +/// +/// Selects the limiting algorithm used by the in-memory rate-limit partitions registered by +/// AddCommonRateLimiting. Both algorithms use the same one-minute window and the same +/// partition keys, so switching between them changes only how permits are replenished. +/// +public enum RateLimitAlgorithm +{ + /// + /// A fixed one-minute window: the whole allowance becomes available again at the window + /// boundary. Cheapest to run, but a caller can spend the allowance twice across a boundary + /// (once at the end of one window, once at the start of the next). + /// + FixedWindow, + + /// + /// A sliding one-minute window divided into SegmentsPerWindow segments: each segment's + /// permits are returned as it ages out, so the boundary burst of + /// is smoothed away at the cost of tracking one counter per segment. + /// + SlidingWindow +} diff --git a/Source/Presentation/MMCA.Common.API/RateLimiting/RateLimitingSettings.cs b/Source/Presentation/MMCA.Common.API/RateLimiting/RateLimitingSettings.cs new file mode 100644 index 00000000..0cdee7ad --- /dev/null +++ b/Source/Presentation/MMCA.Common.API/RateLimiting/RateLimitingSettings.cs @@ -0,0 +1,73 @@ +using System.ComponentModel.DataAnnotations; + +namespace MMCA.Common.API.RateLimiting; + +/// +/// Configuration for AddCommonRateLimiting, bound from the RateLimiting section. +/// Every property has the same default the parameterized overload of +/// AddCommonRateLimiting has always used, so the section is optional in +/// appsettings.json and a host that omits it keeps the previous behaviour exactly. +/// +/// +/// +/// "RateLimiting": { +/// "GlobalPermitLimit": 600, +/// "Algorithm": "SlidingWindow", +/// "SegmentsPerWindow": 6, +/// "Distributed": true +/// } +/// +/// +public sealed class RateLimitingSettings +{ + /// Configuration section name used for options binding. + public static readonly string SectionName = "RateLimiting"; + + /// Requests per minute for the opt-in "FixedPolicy" limiter. + [Range(1, 1_000_000)] + public int PermitLimit { get; init; } = 100; + + /// Queued requests allowed once "FixedPolicy" or "UserPolicy" are saturated. + [Range(0, 10_000)] + public int QueueLimit { get; init; } = 2; + + /// Requests per minute per user for the opt-in "UserPolicy" limiter. + [Range(1, 1_000_000)] + public int PerUserPermitLimit { get; init; } = 30; + + /// Requests per minute per authenticated user for the always-on global limiter. + [Range(1, 1_000_000)] + public int GlobalPermitLimit { get; init; } = 300; + + /// + /// Requests per minute per client IP for the auth-ip policy that throttles anonymous + /// authentication attempts. + /// + [Range(1, 1_000_000)] + public int AuthIpPermitLimit { get; init; } = 30; + + /// + /// The limiting algorithm for the in-memory partitions. Defaults to + /// , which is what the framework has always used. + /// + public RateLimitAlgorithm Algorithm { get; init; } = RateLimitAlgorithm.FixedWindow; + + /// + /// Number of segments the one-minute window is divided into when + /// is . Ignored for + /// . Higher values smooth the window further and + /// cost one more counter per partition. + /// + [Range(1, 60)] + public int SegmentsPerWindow { get; init; } = 4; + + /// + /// Whether the global limiter and the "UserPolicy" limiter should count against a shared Redis + /// counter instead of per-instance memory, so a limit means the same thing behind a load + /// balancer as it does on one node. Requires an IConnectionMultiplexer in the container; + /// when none is registered the limiters silently fall back to the in-memory (per-instance) + /// behaviour rather than failing startup. The auth-ip policy stays in memory either way, + /// because per-account login protection already backs it. + /// + public bool Distributed { get; init; } +} diff --git a/Source/Presentation/MMCA.Common.API/RateLimiting/RedisFixedWindowRateLimiter.cs b/Source/Presentation/MMCA.Common.API/RateLimiting/RedisFixedWindowRateLimiter.cs new file mode 100644 index 00000000..cc9a0f61 --- /dev/null +++ b/Source/Presentation/MMCA.Common.API/RateLimiting/RedisFixedWindowRateLimiter.cs @@ -0,0 +1,191 @@ +using System.Diagnostics; +using System.Globalization; +using System.Threading.RateLimiting; +using Microsoft.Extensions.Logging; +using StackExchange.Redis; + +namespace MMCA.Common.API.RateLimiting; + +/// +/// A that counts a fixed one-minute window in Redis, so every instance +/// behind a load balancer shares one allowance per partition instead of each getting its own. +/// Selected by RateLimitingSettings.Distributed for the global limiter and the "UserPolicy" +/// limiter; when no is registered the caller falls back to the +/// in-memory limiters instead. +/// +/// Layering: this lives in MMCA.Common.API rather than behind an abstraction implemented in +/// Infrastructure because API already references Infrastructure (which owns the +/// StackExchange.Redis dependency), so using the client here adds no new dependency edge and +/// breaks no rule in MMCA.Common.LayerEnforcement.targets. An extra interface plus an +/// Infrastructure implementation would buy nothing but indirection: the limiter is a presentation +/// concern that only the rate-limiting middleware constructs. +/// +/// +/// Storage: one key per partition per window, rl:{partitionKey}:{unixMinute}, +/// incremented with INCR and given a TTL slightly longer than the window on the increment +/// that creates it. Keys expire on their own, so nothing has to sweep them, and a window rollover +/// is a new key rather than a reset. The counter is not transactional with the permit decision +/// (INCR then compare), which can let a burst arriving in the same instant overshoot the limit +/// slightly; that is the accepted trade for one round trip per request. +/// +/// +/// Fail open: any Redis fault permits the request and logs at warning level, at most once +/// per window across the process. Rate limiting protects capacity; it must never become the reason +/// an otherwise healthy request is rejected. +/// +/// +public sealed partial class RedisFixedWindowRateLimiter : RateLimiter +{ + /// + /// The window this process last logged a Redis fault for, so a dead Redis produces one warning + /// a minute rather than one per request. Static (rather than per instance) because a fault is a + /// property of the connection, which every partition shares. + /// + private static long _lastLoggedFailureWindow = -1; + + private readonly IConnectionMultiplexer _connection; + private readonly string _partitionKey; + private readonly int _permitLimit; + private readonly ILogger _logger; + private readonly TimeProvider _timeProvider; + + private long _lastUsedTimestamp = Stopwatch.GetTimestamp(); + + /// + /// Initializes a new instance of the class. + /// + /// The shared Redis connection multiplexer. + /// + /// The partition this limiter counts, already scoped by the caller (e.g. global:alice) + /// so two policies limiting the same user never share one counter. + /// + /// Permits allowed per one-minute window. + /// Logger for the fail-open warning. + /// + /// Clock used to compute the window stamp. Defaults to ; + /// tests substitute it to pin a window. + /// + public RedisFixedWindowRateLimiter( + IConnectionMultiplexer connection, + string partitionKey, + int permitLimit, + ILogger logger, + TimeProvider? timeProvider = null) + { + ArgumentNullException.ThrowIfNull(connection); + ArgumentException.ThrowIfNullOrWhiteSpace(partitionKey); + ArgumentOutOfRangeException.ThrowIfLessThan(permitLimit, 1); + ArgumentNullException.ThrowIfNull(logger); + + _connection = connection; + _partitionKey = partitionKey; + _permitLimit = permitLimit; + _logger = logger; + _timeProvider = timeProvider ?? TimeProvider.System; + } + + /// + /// How long this partition has gone unused, so the owning + /// can evict it. Reported rather than left + /// because partition keys embed user identity: never reporting idleness + /// would grow the partition table with one entry per user seen since start-up. + /// + public override TimeSpan? IdleDuration => + Stopwatch.GetElapsedTime(Volatile.Read(ref _lastUsedTimestamp)); + + /// + /// Returns : the counter lives in Redis, and reporting it would cost a + /// round trip per call for a diagnostic the middleware does not require. + /// + /// Always . + public override RateLimiterStatistics? GetStatistics() => null; + + /// + /// Synchronous acquisition always permits. The ASP.NET Core rate-limiting middleware uses the + /// asynchronous path exclusively, so this exists only to satisfy the base contract, and + /// blocking a request thread on a Redis round trip to serve it would be strictly worse than + /// the fail-open posture the whole limiter already takes on a Redis fault. + /// + /// The permits requested. + /// An acquired lease. + protected override RateLimitLease AttemptAcquireCore(int permitCount) + { + Volatile.Write(ref _lastUsedTimestamp, Stopwatch.GetTimestamp()); + return RedisRateLimitLease.Acquired; + } + + /// + protected override async ValueTask AcquireAsyncCore( + int permitCount, + CancellationToken cancellationToken) + { + Volatile.Write(ref _lastUsedTimestamp, Stopwatch.GetTimestamp()); + + if (permitCount > _permitLimit) + { + return RedisRateLimitLease.Rejected; + } + + var window = _timeProvider.GetUtcNow().ToUnixTimeSeconds() / 60; + var key = string.Create(CultureInfo.InvariantCulture, $"rl:{_partitionKey}:{window}"); + + try + { + var database = _connection.GetDatabase(); + var count = await database.StringIncrementAsync(key, permitCount).ConfigureAwait(false); + + if (count <= permitCount) + { + // First increment of this window created the key, so give it a TTL. The skew past + // the window length covers clock drift between instances: an early-expiring key + // would hand the partition a fresh allowance inside the same window. + await database.KeyExpireAsync(key, TimeSpan.FromSeconds(65)).ConfigureAwait(false); + } + + return count <= _permitLimit ? RedisRateLimitLease.Acquired : RedisRateLimitLease.Rejected; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + if (Interlocked.Exchange(ref _lastLoggedFailureWindow, window) != window) + { + LogRedisUnavailable(_logger, _partitionKey, ex); + } + + return RedisRateLimitLease.Acquired; + } + } + + [LoggerMessage( + Level = LogLevel.Warning, + Message = "Distributed rate limiting is failing open for partition '{PartitionKey}': the Redis counter is unreachable, so requests are permitted uncounted until it recovers")] + private static partial void LogRedisUnavailable(ILogger logger, string partitionKey, Exception exception); +} + +/// +/// The two leases hands out. Both are stateless and +/// carry no metadata, so one shared instance of each serves every request rather than allocating +/// a lease per call. +/// +internal sealed class RedisRateLimitLease : RateLimitLease +{ + /// The lease returned when the request is permitted. + internal static readonly RedisRateLimitLease Acquired = new(isAcquired: true); + + /// The lease returned when the window's allowance is exhausted. + internal static readonly RedisRateLimitLease Rejected = new(isAcquired: false); + + private RedisRateLimitLease(bool isAcquired) => IsAcquired = isAcquired; + + /// + public override bool IsAcquired { get; } + + /// + public override IEnumerable MetadataNames => []; + + /// + public override bool TryGetMetadata(string metadataName, out object? metadata) + { + metadata = null; + return false; + } +} diff --git a/Source/Presentation/MMCA.Common.API/Startup/WebApplicationBuilderExtensions.cs b/Source/Presentation/MMCA.Common.API/Startup/WebApplicationBuilderExtensions.cs index ddb28a10..e505dd48 100644 --- a/Source/Presentation/MMCA.Common.API/Startup/WebApplicationBuilderExtensions.cs +++ b/Source/Presentation/MMCA.Common.API/Startup/WebApplicationBuilderExtensions.cs @@ -7,15 +7,18 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.IdentityModel.Tokens; using MMCA.Common.API.Authorization; using MMCA.Common.API.OpenApi; +using MMCA.Common.API.RateLimiting; using MMCA.Common.Infrastructure.Settings; +using StackExchange.Redis; namespace MMCA.Common.API.Startup; @@ -54,7 +57,15 @@ internal static bool IsRateLimitBypassed(HttpContext httpContext) => /// and limits authenticated callers per user (name → user_id → IP). /// Internal (not private) so the partition-key selection is unit-testable via /// InternalsVisibleTo. - internal static RateLimitPartition GlobalRateLimitPartition(HttpContext httpContext, int globalPermitLimit) + internal static RateLimitPartition GlobalRateLimitPartition(HttpContext httpContext, int globalPermitLimit) => + GlobalRateLimitPartition(httpContext, new RateLimitingSettings { GlobalPermitLimit = globalPermitLimit }); + + /// Global rate-limit partition, configured from : + /// same exemptions and same partition keys as the permit-count overload, with the algorithm and + /// the distributed-counter choice taken from settings. + /// Internal (not private) so the partition-key selection is unit-testable via + /// InternalsVisibleTo. + internal static RateLimitPartition GlobalRateLimitPartition(HttpContext httpContext, RateLimitingSettings settings) { if (IsRateLimitBypassed(httpContext)) { @@ -71,11 +82,107 @@ internal static RateLimitPartition GlobalRateLimitPartition(HttpContext ?? httpContext.Connection.RemoteIpAddress?.ToString() ?? "authenticated"; + return CreateLimitedPartition( + httpContext, + partitionKey, + redisScope: "global", + permitLimit: settings.GlobalPermitLimit, + queueLimit: 0, + settings, + allowDistributed: true); + } + + /// + /// Partition selector for the opt-in "UserPolicy" limiter: one bucket per authenticated user, + /// falling back to the client IP and then to a shared anonymous bucket. + /// + /// + /// Extracted from the inline lambda it used to be so the key selection is unit-testable via + /// InternalsVisibleTo, exactly like . + /// + internal static RateLimitPartition UserPolicyRateLimitPartition(HttpContext httpContext, RateLimitingSettings settings) + { + var partitionKey = httpContext.User?.Identity?.Name + ?? httpContext.Connection.RemoteIpAddress?.ToString() + ?? "anonymous"; + + return CreateLimitedPartition( + httpContext, + partitionKey, + redisScope: "user", + permitLimit: settings.PerUserPermitLimit, + queueLimit: settings.QueueLimit, + settings, + allowDistributed: true); + } + + /// + /// Builds one limited partition for , choosing between the + /// shared Redis counter and the in-memory fixed or sliding window. + /// + /// The request, used only to resolve services. + /// The partition key, unchanged from what the caller computed. + /// + /// Prefix applied to the Redis key only, so the global limiter and "UserPolicy" never share a + /// counter for the same user while both keep their original partition keys. + /// + /// Permits per one-minute window for this partition. + /// Queued requests allowed once the partition is saturated. + /// The bound rate-limiting settings. + /// + /// Whether this partition may use the Redis counter. False for the auth-ip policy, which + /// stays per-instance: per-account login protection already backs it, and a login throttle that + /// fails open on a Redis outage is a worse trade than one that stays local. + /// + private static RateLimitPartition CreateLimitedPartition( + HttpContext httpContext, + string partitionKey, + string redisScope, + int permitLimit, + int queueLimit, + RateLimitingSettings settings, + bool allowDistributed) + { + if (allowDistributed && settings.Distributed) + { + // RequestServices is declared non-nullable but is genuinely null outside a request + // pipeline (a bare DefaultHttpContext in a unit test), so it is read through a nullable + // local rather than dereferenced. + IServiceProvider? requestServices = httpContext.RequestServices; + var connection = requestServices?.GetService(); + + if (connection is not null) + { + var logger = (ILogger?)requestServices?.GetService>() + ?? NullLogger.Instance; + + return RateLimitPartition.Get( + partitionKey, + key => new RedisFixedWindowRateLimiter(connection, $"{redisScope}:{key}", permitLimit, logger)); + } + + // No multiplexer registered: fall through to the in-memory limiters rather than failing + // startup, so a host that turns the flag on before wiring Redis degrades to + // per-instance limits instead of losing rate limiting altogether. + } + + if (settings.Algorithm == RateLimitAlgorithm.SlidingWindow) + { + return RateLimitPartition.GetSlidingWindowLimiter(partitionKey, _ => new SlidingWindowRateLimiterOptions + { + Window = TimeSpan.FromMinutes(1), + SegmentsPerWindow = settings.SegmentsPerWindow, + PermitLimit = permitLimit, + QueueLimit = queueLimit, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + }); + } + return RateLimitPartition.GetFixedWindowLimiter(partitionKey, _ => new FixedWindowRateLimiterOptions { Window = TimeSpan.FromMinutes(1), - PermitLimit = globalPermitLimit, - QueueLimit = 0, + PermitLimit = permitLimit, + QueueLimit = queueLimit, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, }); } @@ -85,24 +192,35 @@ internal static RateLimitPartition GlobalRateLimitPartition(HttpContext /// client IP, or no limiter at all when the IP is unattributable. /// /// - /// Internal (not private) for the same reason as : the + /// Internal (not private) for the same reason as + /// : the /// load-bearing decision (fail open on a null IP rather than collapsing every such request into /// one shared bucket, which would throttle the in-process TestServer and the integration tier to /// a standstill) is worth asserting directly rather than only through a request flood. /// - internal static RateLimitPartition AuthIpRateLimitPartition(HttpContext httpContext, int authIpPermitLimit) + internal static RateLimitPartition AuthIpRateLimitPartition(HttpContext httpContext, int authIpPermitLimit) => + AuthIpRateLimitPartition(httpContext, new RateLimitingSettings { AuthIpPermitLimit = authIpPermitLimit }); + + /// + /// Partition selector for , configured from + /// . Honors the configured algorithm but never the + /// distributed counter: see the allowDistributed note on the partition factory. + /// + /// Internal (not private) for the same reason as the permit-count overload. + internal static RateLimitPartition AuthIpRateLimitPartition(HttpContext httpContext, RateLimitingSettings settings) { var clientIp = httpContext.Connection.RemoteIpAddress?.ToString(); return clientIp is null ? RateLimitPartition.GetNoLimiter("__unknown-ip") - : RateLimitPartition.GetFixedWindowLimiter(clientIp, _ => new FixedWindowRateLimiterOptions - { - Window = TimeSpan.FromMinutes(1), - PermitLimit = authIpPermitLimit, - QueueLimit = 0, - QueueProcessingOrder = QueueProcessingOrder.OldestFirst, - }); + : CreateLimitedPartition( + httpContext, + clientIp, + redisScope: "auth-ip", + permitLimit: settings.AuthIpPermitLimit, + queueLimit: 0, + settings, + allowDistributed: false); } extension(IServiceCollection services) @@ -146,6 +264,11 @@ public IServiceCollection AddCommonApiVersioning() /// at high frequency. The named "FixedPolicy"/"UserPolicy" limiters remain for opt-in /// [EnableRateLimiting] use, as does , the per-IP /// anonymous-authentication throttle described on . + /// + /// This overload keeps every other knob at its default. To reach the sliding-window + /// algorithm or the shared Redis counter, use the overload and + /// a RateLimiting configuration section (). + /// /// /// Requests per minute for the opt-in "FixedPolicy" limiter. /// Queued requests allowed once "FixedPolicy"/"UserPolicy" are saturated. @@ -160,33 +283,65 @@ public IServiceCollection AddCommonApiVersioning() /// top. Tighten toward 10 only once real client IPs are forwarded end to end. /// public IServiceCollection AddCommonRateLimiting(int permitLimit = 100, int queueLimit = 2, int perUserPermitLimit = 30, int globalPermitLimit = 300, int authIpPermitLimit = 30) => - services.AddRateLimiter(options => + services.AddCommonRateLimiting(new RateLimitingSettings { - options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; + PermitLimit = permitLimit, + QueueLimit = queueLimit, + PerUserPermitLimit = perUserPermitLimit, + GlobalPermitLimit = globalPermitLimit, + AuthIpPermitLimit = authIpPermitLimit, + }); - options.GlobalLimiter = PartitionedRateLimiter.Create( - httpContext => GlobalRateLimitPartition(httpContext, globalPermitLimit)); + /// + /// Registers rate limiting from the RateLimiting configuration section. Equivalent to + /// the permit-count overload when the section is absent, and the only way to reach the + /// sliding-window algorithm and the shared Redis counter + /// (, ). + /// + /// The application configuration. + /// The service collection for chaining. + public IServiceCollection AddCommonRateLimiting(IConfiguration configuration) + { + ArgumentNullException.ThrowIfNull(configuration); - options.AddFixedWindowLimiter("FixedPolicy", limiterOptions => - { - limiterOptions.Window = TimeSpan.FromMinutes(1); - limiterOptions.PermitLimit = permitLimit; - limiterOptions.QueueLimit = queueLimit; - limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; - }); + var settings = configuration.GetSection(RateLimitingSettings.SectionName).Get() + ?? new RateLimitingSettings(); - options.AddPolicy("UserPolicy", httpContext => - RateLimitPartition.GetFixedWindowLimiter( - partitionKey: httpContext.User?.Identity?.Name - ?? httpContext.Connection.RemoteIpAddress?.ToString() - ?? "anonymous", - factory: _ => new FixedWindowRateLimiterOptions - { - Window = TimeSpan.FromMinutes(1), - PermitLimit = perUserPermitLimit, - QueueLimit = queueLimit, - QueueProcessingOrder = QueueProcessingOrder.OldestFirst - })); + return services.AddCommonRateLimiting(settings); + } + + /// + /// Registers rate limiting from an already-built . The + /// policy names ("FixedPolicy", "UserPolicy", ), the + /// bypass list and every partition key are identical whatever the settings say; only the + /// permit counts, the algorithm and the counter's location change. + /// + /// The rate-limiting settings. + /// The service collection for chaining. + public IServiceCollection AddCommonRateLimiting(RateLimitingSettings settings) + { + ArgumentNullException.ThrowIfNull(settings); + + return services.AddRateLimiter(options => + { + options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; + + options.GlobalLimiter = PartitionedRateLimiter.Create( + httpContext => GlobalRateLimitPartition(httpContext, settings)); + + // "FixedPolicy" keeps its name whichever algorithm is configured: it is an opt-in + // policy referenced by name from [EnableRateLimiting] attributes in three repos, so + // renaming it on a settings change would silently unlimit every endpoint using it. + options.AddPolicy("FixedPolicy", httpContext => CreateLimitedPartition( + httpContext, + partitionKey: "__fixed", + redisScope: "fixed", + permitLimit: settings.PermitLimit, + queueLimit: settings.QueueLimit, + settings, + allowDistributed: false)); + + options.AddPolicy("UserPolicy", httpContext => UserPolicyRateLimitPartition(httpContext, settings)); // Per-IP anonymous authentication throttle. Client IP is taken from // Connection.RemoteIpAddress, which the shared pipeline has already resolved from @@ -198,8 +353,9 @@ public IServiceCollection AddCommonRateLimiting(int permitLimit = 100, int queue // untouched. options.AddPolicy( RateLimitPolicyAuthIp, - httpContext => AuthIpRateLimitPartition(httpContext, authIpPermitLimit)); + httpContext => AuthIpRateLimitPartition(httpContext, settings)); }); + } /// /// Registers Brotli + Gzip response compression for HTTPS responses. diff --git a/Tests/Core/MMCA.Common.Application.Tests/Decorators/AuthorizationCommandDecoratorTests.cs b/Tests/Core/MMCA.Common.Application.Tests/Decorators/AuthorizationCommandDecoratorTests.cs new file mode 100644 index 00000000..e40d8f1a --- /dev/null +++ b/Tests/Core/MMCA.Common.Application.Tests/Decorators/AuthorizationCommandDecoratorTests.cs @@ -0,0 +1,155 @@ +using AwesomeAssertions; +using MMCA.Common.Application.Interfaces.Infrastructure; +using MMCA.Common.Application.UseCases; +using MMCA.Common.Application.UseCases.Decorators; +using MMCA.Common.Shared.Abstractions; +using MMCA.Common.Shared.Auth; +using Moq; + +namespace MMCA.Common.Application.Tests.Decorators; + +public sealed class AuthorizationCommandDecoratorTests +{ + private static readonly string[] MultipleRoles = ["Organizer", "Attendee"]; + + private readonly Mock _currentUser = new(); + private readonly Mock _permissionRegistry = new(); + + public AuthorizationCommandDecoratorTests() => + _currentUser.Setup(x => x.Roles).Returns(["Attendee"]); + + // ── A command without the marker is never checked at all ── + [Fact] + public async Task HandleAsync_CommandWithoutPermission_DelegatesToInner() + { + var inner = new Mock>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success()); + + var sut = new AuthorizationCommandDecorator( + inner.Object, _currentUser.Object, _permissionRegistry.Object); + + var result = await sut.HandleAsync(new UnguardedCommand()); + + result.IsSuccess.Should().BeTrue(); + inner.Verify(x => x.HandleAsync(It.IsAny(), It.IsAny()), Times.Once); + _permissionRegistry.Verify( + x => x.HasPermission(It.IsAny>(), It.IsAny()), + Times.Never); + } + + // ── Granted permission passes through ── + [Fact] + public async Task HandleAsync_WhenPermissionGranted_DelegatesToInner() + { + var inner = new Mock>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success()); + _permissionRegistry.Setup(x => x.HasPermission(It.IsAny>(), "catalog.products.write")) + .Returns(true); + + var sut = new AuthorizationCommandDecorator( + inner.Object, _currentUser.Object, _permissionRegistry.Object); + + var result = await sut.HandleAsync(new GuardedCommand()); + + result.IsSuccess.Should().BeTrue(); + inner.Verify(x => x.HandleAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + // ── Denied permission short-circuits with Forbidden and never reaches the handler ── + [Fact] + public async Task HandleAsync_WhenPermissionDenied_ReturnsForbiddenWithoutCallingInner() + { + var inner = new Mock>(); + _permissionRegistry.Setup(x => x.HasPermission(It.IsAny>(), It.IsAny())) + .Returns(false); + + var sut = new AuthorizationCommandDecorator( + inner.Object, _currentUser.Object, _permissionRegistry.Object); + + var result = await sut.HandleAsync(new GuardedCommand()); + + result.IsFailure.Should().BeTrue(); + var error = result.Errors.Should().ContainSingle().Subject; + error.Code.Should().Be("Authorization.PermissionDenied"); + error.Type.Should().Be(ErrorType.Forbidden); + error.Source.Should().Be(nameof(GuardedCommand)); + error.Message.Should().Contain("catalog.products.write"); + inner.Verify(x => x.HandleAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + // ── The caller's roles, not a hard-coded set, are what the registry is asked about ── + [Fact] + public async Task HandleAsync_ChecksThePermissionAgainstTheCurrentUsersRoles() + { + var inner = new Mock>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success()); + _currentUser.Setup(x => x.Roles).Returns(MultipleRoles); + _permissionRegistry.Setup(x => x.HasPermission(It.IsAny>(), It.IsAny())) + .Returns(true); + + var sut = new AuthorizationCommandDecorator( + inner.Object, _currentUser.Object, _permissionRegistry.Object); + + await sut.HandleAsync(new GuardedCommand()); + + _permissionRegistry.Verify( + x => x.HasPermission( + It.Is>(roles => roles.SequenceEqual(MultipleRoles)), + "catalog.products.write"), + Times.Once); + } + + // ── The failure factory also serves Result ── + [Fact] + public async Task HandleAsync_WhenPermissionDenied_WithGenericResult_ReturnsFailure() + { + var inner = new Mock>>(); + _permissionRegistry.Setup(x => x.HasPermission(It.IsAny>(), It.IsAny())) + .Returns(false); + + var sut = new AuthorizationCommandDecorator>( + inner.Object, _currentUser.Object, _permissionRegistry.Object); + + var result = await sut.HandleAsync(new GuardedCommandWithValue()); + + result.IsFailure.Should().BeTrue(); + result.Errors.Should().ContainSingle().Which.Type.Should().Be(ErrorType.Forbidden); + inner.Verify( + x => x.HandleAsync(It.IsAny(), It.IsAny()), + Times.Never); + } + + // Scrutor's TryDecorate is unconditional, so a handler whose TResult is neither Result nor + // Result gets decorated too. Building the failure delegate eagerly would turn that into a + // TypeInitializationException at RESOLVE time, even for a command that is never denied. + [Fact] + public async Task HandleAsync_NonResultTResult_UnguardedCommand_PassesThrough() + { + var inner = new Mock>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync("handled"); + + var sut = new AuthorizationCommandDecorator( + inner.Object, _currentUser.Object, _permissionRegistry.Object); + + var result = await sut.HandleAsync(new UnguardedCommand()); + + result.Should().Be("handled"); + } +} + +// ── Test types ── +public sealed record UnguardedCommand; + +public sealed record GuardedCommand : IRequiresPermission +{ + public string Permission => "catalog.products.write"; +} + +public sealed record GuardedCommandWithValue : IRequiresPermission +{ + public string Permission => "catalog.products.write"; +} diff --git a/Tests/Core/MMCA.Common.Application.Tests/Decorators/AuthorizationQueryDecoratorTests.cs b/Tests/Core/MMCA.Common.Application.Tests/Decorators/AuthorizationQueryDecoratorTests.cs new file mode 100644 index 00000000..0dac9f70 --- /dev/null +++ b/Tests/Core/MMCA.Common.Application.Tests/Decorators/AuthorizationQueryDecoratorTests.cs @@ -0,0 +1,103 @@ +using AwesomeAssertions; +using MMCA.Common.Application.Interfaces.Infrastructure; +using MMCA.Common.Application.UseCases; +using MMCA.Common.Application.UseCases.Decorators; +using MMCA.Common.Shared.Abstractions; +using MMCA.Common.Shared.Auth; +using Moq; + +namespace MMCA.Common.Application.Tests.Decorators; + +public sealed class AuthorizationQueryDecoratorTests +{ + private readonly Mock _currentUser = new(); + private readonly Mock _permissionRegistry = new(); + + public AuthorizationQueryDecoratorTests() => + _currentUser.Setup(x => x.Roles).Returns(["Attendee"]); + + // ── A query without the marker is never checked at all ── + [Fact] + public async Task HandleAsync_QueryWithoutPermission_DelegatesToInner() + { + var inner = new Mock>>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success("ok")); + + var sut = new AuthorizationQueryDecorator>( + inner.Object, _currentUser.Object, _permissionRegistry.Object); + + var result = await sut.HandleAsync(new UnguardedQuery()); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Be("ok"); + _permissionRegistry.Verify( + x => x.HasPermission(It.IsAny>(), It.IsAny()), + Times.Never); + } + + // ── Granted permission passes through ── + [Fact] + public async Task HandleAsync_WhenPermissionGranted_DelegatesToInner() + { + var inner = new Mock>>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success("ok")); + _permissionRegistry.Setup(x => x.HasPermission(It.IsAny>(), "catalog.products.read")) + .Returns(true); + + var sut = new AuthorizationQueryDecorator>( + inner.Object, _currentUser.Object, _permissionRegistry.Object); + + var result = await sut.HandleAsync(new GuardedQuery()); + + result.IsSuccess.Should().BeTrue(); + inner.Verify(x => x.HandleAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + // ── Denied permission short-circuits with Forbidden and never reaches the handler ── + [Fact] + public async Task HandleAsync_WhenPermissionDenied_ReturnsForbiddenWithoutCallingInner() + { + var inner = new Mock>>(); + _permissionRegistry.Setup(x => x.HasPermission(It.IsAny>(), It.IsAny())) + .Returns(false); + + var sut = new AuthorizationQueryDecorator>( + inner.Object, _currentUser.Object, _permissionRegistry.Object); + + var result = await sut.HandleAsync(new GuardedQuery()); + + result.IsFailure.Should().BeTrue(); + var error = result.Errors.Should().ContainSingle().Subject; + error.Code.Should().Be("Authorization.PermissionDenied"); + error.Type.Should().Be(ErrorType.Forbidden); + error.Source.Should().Be(nameof(GuardedQuery)); + inner.Verify(x => x.HandleAsync(It.IsAny(), It.IsAny()), Times.Never); + } + + // A handler whose TResult is neither Result nor Result is decorated too and must not fail on + // resolve; it only fails if it ever needs to fabricate a failure. + [Fact] + public async Task HandleAsync_NonResultTResult_UnguardedQuery_PassesThrough() + { + var inner = new Mock>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync("handled"); + + var sut = new AuthorizationQueryDecorator( + inner.Object, _currentUser.Object, _permissionRegistry.Object); + + var result = await sut.HandleAsync(new UnguardedQuery()); + + result.Should().Be("handled"); + } +} + +// ── Test types ── +public sealed record UnguardedQuery; + +public sealed record GuardedQuery : IRequiresPermission +{ + public string Permission => "catalog.products.read"; +} diff --git a/Tests/Core/MMCA.Common.Application.Tests/Decorators/TimeoutCommandDecoratorTests.cs b/Tests/Core/MMCA.Common.Application.Tests/Decorators/TimeoutCommandDecoratorTests.cs new file mode 100644 index 00000000..474ff6f0 --- /dev/null +++ b/Tests/Core/MMCA.Common.Application.Tests/Decorators/TimeoutCommandDecoratorTests.cs @@ -0,0 +1,152 @@ +using AwesomeAssertions; +using MMCA.Common.Application.UseCases; +using MMCA.Common.Application.UseCases.Decorators; +using MMCA.Common.Shared.Abstractions; +using Moq; + +namespace MMCA.Common.Application.Tests.Decorators; + +public sealed class TimeoutCommandDecoratorTests +{ + // ── A command without a budget keeps the caller's token untouched ── + [Fact] + public async Task HandleAsync_CommandWithoutTimeout_DelegatesWithCallerToken() + { + using var cts = new CancellationTokenSource(); + CancellationToken token = cts.Token; + + var inner = new Mock>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), token)) + .ReturnsAsync(Result.Success()); + + var sut = new TimeoutCommandDecorator(inner.Object); + + var result = await sut.HandleAsync(new UnbudgetedCommand(), token); + + result.IsSuccess.Should().BeTrue(); + inner.Verify(x => x.HandleAsync(It.IsAny(), token), Times.Once); + } + + // ── A budget that is never exhausted is invisible ── + [Fact] + public async Task HandleAsync_WhenHandlerCompletesInsideBudget_ReturnsHandlerResult() + { + var inner = new Mock>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success()); + + var sut = new TimeoutCommandDecorator(inner.Object); + + var result = await sut.HandleAsync(new BudgetedCommand(TimeSpan.FromSeconds(30))); + + result.IsSuccess.Should().BeTrue(); + } + + // ── An expired budget becomes a failure result, not an exception ── + [Fact] + public async Task HandleAsync_WhenBudgetExpires_ReturnsTimedOutFailure() + { + var inner = new Mock>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), It.IsAny())) + .Returns(async (_, ct) => + { + await Task.Delay(TimeSpan.FromMinutes(1), ct); + return Result.Success(); + }); + + var sut = new TimeoutCommandDecorator(inner.Object); + + var result = await sut.HandleAsync(new BudgetedCommand(TimeSpan.FromMilliseconds(30))); + + result.IsFailure.Should().BeTrue(); + var error = result.Errors.Should().ContainSingle().Subject; + error.Code.Should().Be("Request.TimedOut"); + error.Source.Should().Be(nameof(BudgetedCommand)); + } + + // ── Cancellation raised by the CALLER stays an exception ── + // Turning it into a failure result would report an aborted request as a server-side timeout and + // hide the abort from every caller of the pipeline. + [Fact] + public async Task HandleAsync_WhenCallerCancels_RethrowsInsteadOfReturningFailure() + { + using var cts = new CancellationTokenSource(); + + var inner = new Mock>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), It.IsAny())) + .Returns(async (_, ct) => + { + await cts.CancelAsync(); + await Task.Delay(TimeSpan.FromMinutes(1), ct); + return Result.Success(); + }); + + var sut = new TimeoutCommandDecorator(inner.Object); + + Func act = () => sut.HandleAsync(new BudgetedCommand(TimeSpan.FromMinutes(5)), cts.Token); + + await act.Should().ThrowAsync(); + } + + // ── A non-positive budget is treated as "no budget" ── + [Theory] + [InlineData(0)] + [InlineData(-1)] + public async Task HandleAsync_WhenBudgetIsNotPositive_PassesThroughWithCallerToken(int seconds) + { + using var cts = new CancellationTokenSource(); + CancellationToken token = cts.Token; + + var inner = new Mock>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), token)) + .ReturnsAsync(Result.Success()); + + var sut = new TimeoutCommandDecorator(inner.Object); + + var result = await sut.HandleAsync(new BudgetedCommand(TimeSpan.FromSeconds(seconds)), token); + + result.IsSuccess.Should().BeTrue(); + inner.Verify(x => x.HandleAsync(It.IsAny(), token), Times.Once); + } + + // ── The failure factory also serves Result ── + [Fact] + public async Task HandleAsync_WhenBudgetExpires_WithGenericResult_ReturnsFailure() + { + var inner = new Mock>>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), It.IsAny())) + .Returns(async (_, ct) => + { + await Task.Delay(TimeSpan.FromMinutes(1), ct); + return Result.Success(1); + }); + + var sut = new TimeoutCommandDecorator>(inner.Object); + + var result = await sut.HandleAsync(new BudgetedCommand(TimeSpan.FromMilliseconds(30))); + + result.IsFailure.Should().BeTrue(); + result.Errors.Should().ContainSingle().Which.Code.Should().Be("Request.TimedOut"); + } + + // Scrutor decorates handlers whose TResult is neither Result nor Result too; the decorator + // must resolve and pass through rather than fail at type-initialization time. + [Fact] + public async Task HandleAsync_NonResultTResult_UnbudgetedCommand_PassesThrough() + { + var inner = new Mock>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync("handled"); + + var sut = new TimeoutCommandDecorator(inner.Object); + + var result = await sut.HandleAsync(new UnbudgetedCommand()); + + result.Should().Be("handled"); + } +} + +// ── Test types ── +public sealed record UnbudgetedCommand; + +public sealed record BudgetedCommand(TimeSpan Timeout) : IHasTimeout; diff --git a/Tests/Core/MMCA.Common.Application.Tests/Decorators/TimeoutQueryDecoratorTests.cs b/Tests/Core/MMCA.Common.Application.Tests/Decorators/TimeoutQueryDecoratorTests.cs new file mode 100644 index 00000000..aa907167 --- /dev/null +++ b/Tests/Core/MMCA.Common.Application.Tests/Decorators/TimeoutQueryDecoratorTests.cs @@ -0,0 +1,130 @@ +using AwesomeAssertions; +using MMCA.Common.Application.UseCases; +using MMCA.Common.Application.UseCases.Decorators; +using MMCA.Common.Shared.Abstractions; +using Moq; + +namespace MMCA.Common.Application.Tests.Decorators; + +public sealed class TimeoutQueryDecoratorTests +{ + // ── A query without a budget keeps the caller's token untouched ── + [Fact] + public async Task HandleAsync_QueryWithoutTimeout_DelegatesWithCallerToken() + { + using var cts = new CancellationTokenSource(); + CancellationToken token = cts.Token; + + var inner = new Mock>>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), token)) + .ReturnsAsync(Result.Success("ok")); + + var sut = new TimeoutQueryDecorator>(inner.Object); + + var result = await sut.HandleAsync(new UnbudgetedQuery(), token); + + result.IsSuccess.Should().BeTrue(); + inner.Verify(x => x.HandleAsync(It.IsAny(), token), Times.Once); + } + + // ── A budget that is never exhausted is invisible ── + [Fact] + public async Task HandleAsync_WhenHandlerCompletesInsideBudget_ReturnsHandlerResult() + { + var inner = new Mock>>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(Result.Success("ok")); + + var sut = new TimeoutQueryDecorator>(inner.Object); + + var result = await sut.HandleAsync(new BudgetedQuery(TimeSpan.FromSeconds(30))); + + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Be("ok"); + } + + // ── An expired budget becomes a failure result, not an exception ── + [Fact] + public async Task HandleAsync_WhenBudgetExpires_ReturnsTimedOutFailure() + { + var inner = new Mock>>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), It.IsAny())) + .Returns(async (_, ct) => + { + await Task.Delay(TimeSpan.FromMinutes(1), ct); + return Result.Success("ok"); + }); + + var sut = new TimeoutQueryDecorator>(inner.Object); + + var result = await sut.HandleAsync(new BudgetedQuery(TimeSpan.FromMilliseconds(30))); + + result.IsFailure.Should().BeTrue(); + var error = result.Errors.Should().ContainSingle().Subject; + error.Code.Should().Be("Request.TimedOut"); + error.Source.Should().Be(nameof(BudgetedQuery)); + } + + // ── Cancellation raised by the CALLER stays an exception ── + [Fact] + public async Task HandleAsync_WhenCallerCancels_RethrowsInsteadOfReturningFailure() + { + using var cts = new CancellationTokenSource(); + + var inner = new Mock>>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), It.IsAny())) + .Returns(async (_, ct) => + { + await cts.CancelAsync(); + await Task.Delay(TimeSpan.FromMinutes(1), ct); + return Result.Success("ok"); + }); + + var sut = new TimeoutQueryDecorator>(inner.Object); + + Func act = () => sut.HandleAsync(new BudgetedQuery(TimeSpan.FromMinutes(5)), cts.Token); + + await act.Should().ThrowAsync(); + } + + // ── A non-positive budget is treated as "no budget" ── + [Theory] + [InlineData(0)] + [InlineData(-1)] + public async Task HandleAsync_WhenBudgetIsNotPositive_PassesThroughWithCallerToken(int seconds) + { + using var cts = new CancellationTokenSource(); + CancellationToken token = cts.Token; + + var inner = new Mock>>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), token)) + .ReturnsAsync(Result.Success("ok")); + + var sut = new TimeoutQueryDecorator>(inner.Object); + + var result = await sut.HandleAsync(new BudgetedQuery(TimeSpan.FromSeconds(seconds)), token); + + result.IsSuccess.Should().BeTrue(); + inner.Verify(x => x.HandleAsync(It.IsAny(), token), Times.Once); + } + + // Scrutor decorates handlers whose TResult is neither Result nor Result too. + [Fact] + public async Task HandleAsync_NonResultTResult_UnbudgetedQuery_PassesThrough() + { + var inner = new Mock>(); + inner.Setup(x => x.HandleAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync("handled"); + + var sut = new TimeoutQueryDecorator(inner.Object); + + var result = await sut.HandleAsync(new UnbudgetedQuery()); + + result.Should().Be("handled"); + } +} + +// ── Test types ── +public sealed record UnbudgetedQuery; + +public sealed record BudgetedQuery(TimeSpan Timeout) : IHasTimeout; diff --git a/Tests/Hosting/MMCA.Common.Testing.Tests/DecoratorPipelineOrderTests.cs b/Tests/Hosting/MMCA.Common.Testing.Tests/DecoratorPipelineOrderTests.cs index d790cbd2..c2ad81ef 100644 --- a/Tests/Hosting/MMCA.Common.Testing.Tests/DecoratorPipelineOrderTests.cs +++ b/Tests/Hosting/MMCA.Common.Testing.Tests/DecoratorPipelineOrderTests.cs @@ -7,6 +7,7 @@ using MMCA.Common.Application.Interfaces.Infrastructure; using MMCA.Common.Application.UseCases; using MMCA.Common.Shared.Abstractions; +using MMCA.Common.Shared.Auth; using Moq; namespace MMCA.Common.Testing.Tests; @@ -24,6 +25,8 @@ protected override void ConfigureServices(IServiceCollection services) { // Test doubles for the decorator constructor dependencies. services.AddSingleton(Mock.Of()); + services.AddScoped(_ => Mock.Of()); + services.AddSingleton(Mock.Of()); services.AddSingleton(Mock.Of()); services.AddSingleton(Mock.Of()); services.AddScoped(_ => Mock.Of()); diff --git a/Tests/Presentation/MMCA.Common.API.Tests/FeatureManagement/CurrentUserTargetingContextAccessorTests.cs b/Tests/Presentation/MMCA.Common.API.Tests/FeatureManagement/CurrentUserTargetingContextAccessorTests.cs new file mode 100644 index 00000000..c95f7a74 --- /dev/null +++ b/Tests/Presentation/MMCA.Common.API.Tests/FeatureManagement/CurrentUserTargetingContextAccessorTests.cs @@ -0,0 +1,94 @@ +using System.Security.Claims; +using AwesomeAssertions; +using Microsoft.AspNetCore.Http; +using MMCA.Common.API.FeatureManagement; + +namespace MMCA.Common.API.Tests.FeatureManagement; + +/// +/// Tests for the feature-flag targeting context. The load-bearing decisions are which claim carries +/// the user id (the Targeting filter hashes it, so a rollout is only sticky per user if the id is +/// stable) and that an anonymous request produces an empty context rather than an error, because a +/// feature filter must never be able to fail a request. +/// +public sealed class CurrentUserTargetingContextAccessorTests +{ + [Fact] + public async Task GetContextAsync_ForAuthenticatedUser_ReturnsUserIdAndRoleGroups() + { + var sut = CreateAccessor(new ClaimsPrincipal(new ClaimsIdentity( + [ + new Claim("user_id", "42"), + new Claim(ClaimTypes.Role, "Organizer"), + new Claim(ClaimTypes.Role, "Attendee"), + ], + authenticationType: "TestAuth"))); + + var context = await sut.GetContextAsync(); + + context.UserId.Should().Be("42"); + context.Groups.Should().BeEquivalentTo("Organizer", "Attendee"); + } + + // Inbound claim mapping can be off, in which case the middleware leaves the raw JWT claim names + // in place. Reading only ClaimTypes.Role would report no groups and quietly exclude every user + // from a group-targeted rollout. + [Theory] + [InlineData("role")] + [InlineData("roles")] + public async Task GetContextAsync_ReadsUnmappedRoleClaimTypesToo(string roleClaimType) + { + var sut = CreateAccessor(new ClaimsPrincipal(new ClaimsIdentity( + [new Claim("user_id", "42"), new Claim(roleClaimType, "Organizer")], + authenticationType: "TestAuth"))); + + var context = await sut.GetContextAsync(); + + context.Groups.Should().BeEquivalentTo("Organizer"); + } + + // A token predating the user_id claim still has to target something stable, or every such + // caller would collapse into one bucket. + [Fact] + public async Task GetContextAsync_WhenUserIdClaimMissing_FallsBackToTheIdentityName() + { + var sut = CreateAccessor(new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(ClaimTypes.Name, "alice")], + authenticationType: "TestAuth"))); + + var context = await sut.GetContextAsync(); + + context.UserId.Should().Be("alice"); + context.Groups.Should().BeEmpty(); + } + + [Fact] + public async Task GetContextAsync_ForAnonymousRequest_ReturnsAnEmptyContext() + { + var sut = CreateAccessor(new ClaimsPrincipal(new ClaimsIdentity())); + + var context = await sut.GetContextAsync(); + + context.UserId.Should().BeNull(); + context.Groups.Should().BeEmpty(); + } + + // Background work (a hosted service, an outbox drain) has no HttpContext at all; the accessor + // is a singleton, so it must answer there rather than throw. + [Fact] + public async Task GetContextAsync_OutsideAnyRequest_ReturnsAnEmptyContext() + { + var sut = new CurrentUserTargetingContextAccessor(new HttpContextAccessor()); + + var context = await sut.GetContextAsync(); + + context.UserId.Should().BeNull(); + context.Groups.Should().BeEmpty(); + } + + private static CurrentUserTargetingContextAccessor CreateAccessor(ClaimsPrincipal user) + { + var httpContext = new DefaultHttpContext { User = user }; + return new CurrentUserTargetingContextAccessor(new HttpContextAccessor { HttpContext = httpContext }); + } +} diff --git a/Tests/Presentation/MMCA.Common.API.Tests/RateLimiting/RateLimitingSettingsTests.cs b/Tests/Presentation/MMCA.Common.API.Tests/RateLimiting/RateLimitingSettingsTests.cs new file mode 100644 index 00000000..28119e4b --- /dev/null +++ b/Tests/Presentation/MMCA.Common.API.Tests/RateLimiting/RateLimitingSettingsTests.cs @@ -0,0 +1,94 @@ +using AwesomeAssertions; +using Microsoft.Extensions.Configuration; +using MMCA.Common.API.RateLimiting; + +namespace MMCA.Common.API.Tests.RateLimiting; + +/// +/// Guards the defaults and the binding of . The defaults are +/// load-bearing: they are what the long-standing permit-count overload of +/// AddCommonRateLimiting delegates to, so drifting one of them silently re-tunes every host +/// that never wrote a RateLimiting section. +/// +public sealed class RateLimitingSettingsTests +{ + [Fact] + public void Defaults_MatchThePermitCountOverloadsDefaults() + { + var settings = new RateLimitingSettings(); + + settings.PermitLimit.Should().Be(100); + settings.QueueLimit.Should().Be(2); + settings.PerUserPermitLimit.Should().Be(30); + settings.GlobalPermitLimit.Should().Be(300); + settings.AuthIpPermitLimit.Should().Be(30); + settings.Algorithm.Should().Be(RateLimitAlgorithm.FixedWindow); + settings.SegmentsPerWindow.Should().Be(4); + settings.Distributed.Should().BeFalse(); + } + + [Fact] + public void SectionName_IsRateLimiting() => + RateLimitingSettings.SectionName.Should().Be("RateLimiting"); + + [Fact] + public void Bind_FromConfiguration_ReadsEveryProperty() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["RateLimiting:PermitLimit"] = "50", + ["RateLimiting:QueueLimit"] = "7", + ["RateLimiting:PerUserPermitLimit"] = "11", + ["RateLimiting:GlobalPermitLimit"] = "600", + ["RateLimiting:AuthIpPermitLimit"] = "13", + ["RateLimiting:Algorithm"] = "SlidingWindow", + ["RateLimiting:SegmentsPerWindow"] = "6", + ["RateLimiting:Distributed"] = "true", + }) + .Build(); + + var settings = configuration.GetSection(RateLimitingSettings.SectionName).Get(); + + settings.Should().NotBeNull(); + settings.PermitLimit.Should().Be(50); + settings.QueueLimit.Should().Be(7); + settings.PerUserPermitLimit.Should().Be(11); + settings.GlobalPermitLimit.Should().Be(600); + settings.AuthIpPermitLimit.Should().Be(13); + settings.Algorithm.Should().Be(RateLimitAlgorithm.SlidingWindow); + settings.SegmentsPerWindow.Should().Be(6); + settings.Distributed.Should().BeTrue(); + } + + // A partially specified section must leave the untouched knobs at their defaults, or adding one + // line of configuration would silently reset the rest of the limiter. + [Fact] + public void Bind_FromPartialConfiguration_LeavesOtherPropertiesAtDefaults() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["RateLimiting:GlobalPermitLimit"] = "900", + }) + .Build(); + + var settings = configuration.GetSection(RateLimitingSettings.SectionName).Get(); + + settings.Should().NotBeNull(); + settings.GlobalPermitLimit.Should().Be(900); + settings.PermitLimit.Should().Be(100); + settings.Algorithm.Should().Be(RateLimitAlgorithm.FixedWindow); + settings.Distributed.Should().BeFalse(); + } + + // An absent section binds to null; AddCommonRateLimiting(IConfiguration) is what turns that + // into the default settings instance. + [Fact] + public void Bind_WhenSectionAbsent_ReturnsNull() + { + var configuration = new ConfigurationBuilder().Build(); + + configuration.GetSection(RateLimitingSettings.SectionName).Get().Should().BeNull(); + } +} diff --git a/Tests/Presentation/MMCA.Common.API.Tests/RateLimiting/RedisFixedWindowRateLimiterTests.cs b/Tests/Presentation/MMCA.Common.API.Tests/RateLimiting/RedisFixedWindowRateLimiterTests.cs new file mode 100644 index 00000000..3d353675 --- /dev/null +++ b/Tests/Presentation/MMCA.Common.API.Tests/RateLimiting/RedisFixedWindowRateLimiterTests.cs @@ -0,0 +1,220 @@ +using System.Globalization; +using AwesomeAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Time.Testing; +using MMCA.Common.API.RateLimiting; +using Moq; +using StackExchange.Redis; + +namespace MMCA.Common.API.Tests.RateLimiting; + +/// +/// Unit tests for the shared-counter rate limiter. The load-bearing decisions are the permit +/// comparison, the one-shot TTL on the key that opens a window, and the fail-open posture on a +/// Redis fault: a limiter that failed closed would turn a cache outage into a site-wide 429 storm. +/// +public sealed class RedisFixedWindowRateLimiterTests +{ + private static readonly DateTimeOffset FixedInstant = new(2026, 1, 1, 12, 34, 56, TimeSpan.Zero); + + [Fact] + public async Task AcquireAsync_WhenCountIsInsideTheLimit_Permits() + { + var (connection, database) = CreateConnection(incrementResult: 1); + await using var sut = CreateLimiter(connection.Object, permitLimit: 5); + + using var lease = await sut.AcquireAsync(); + + lease.IsAcquired.Should().BeTrue(); + database.Verify( + d => d.StringIncrementAsync(It.IsAny(), 1L, It.IsAny()), + Times.Once); + } + + [Fact] + public async Task AcquireAsync_AtExactlyTheLimit_StillPermits() + { + var (connection, _) = CreateConnection(incrementResult: 5); + await using var sut = CreateLimiter(connection.Object, permitLimit: 5); + + using var lease = await sut.AcquireAsync(); + + lease.IsAcquired.Should().BeTrue(); + } + + [Fact] + public async Task AcquireAsync_PastTheLimit_Rejects() + { + var (connection, _) = CreateConnection(incrementResult: 6); + await using var sut = CreateLimiter(connection.Object, permitLimit: 5); + + using var lease = await sut.AcquireAsync(); + + lease.IsAcquired.Should().BeFalse(); + } + + // Only the increment that CREATES the window's key sets its TTL. Re-stamping the expiry on + // every request would slide the window forward for a caller who keeps hitting it, so the key + // would never expire and the allowance would never reset. + [Fact] + public async Task AcquireAsync_OnTheFirstRequestOfAWindow_SetsTheKeyExpiry() + { + var (connection, database) = CreateConnection(incrementResult: 1); + await using var sut = CreateLimiter(connection.Object, permitLimit: 5); + + using var lease = await sut.AcquireAsync(); + + database.Verify( + d => d.KeyExpireAsync( + It.IsAny(), + It.Is(expiry => expiry > TimeSpan.FromSeconds(60)), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task AcquireAsync_OnALaterRequestOfTheSameWindow_DoesNotResetTheExpiry() + { + var (connection, database) = CreateConnection(incrementResult: 2); + await using var sut = CreateLimiter(connection.Object, permitLimit: 5); + + using var lease = await sut.AcquireAsync(); + + database.Verify( + d => d.KeyExpireAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } + + // The key must carry both the partition and the window, or two partitions would share an + // allowance and a window rollover would never reset one. + [Fact] + public async Task AcquireAsync_KeysTheCounterByPartitionAndWindow() + { + var (connection, database) = CreateConnection(incrementResult: 1); + RedisKey capturedKey = default; + database.Setup(d => d.StringIncrementAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((key, _, _) => capturedKey = key) + .ReturnsAsync(1L); + + var timeProvider = new FakeTimeProvider(FixedInstant); + await using var sut = new RedisFixedWindowRateLimiter( + connection.Object, "global:alice", 5, NullLogger.Instance, timeProvider); + + using var lease = await sut.AcquireAsync(); + + var expectedWindow = FixedInstant.ToUnixTimeSeconds() / 60; + capturedKey.ToString().Should().Be( + string.Create(CultureInfo.InvariantCulture, $"rl:global:alice:{expectedWindow}")); + } + + // Rate limiting protects capacity; it must never become the reason a healthy request is + // rejected, so a dead Redis permits rather than denies. + [Fact] + public async Task AcquireAsync_WhenRedisFaults_FailsOpen() + { + var database = new Mock(); + database.Setup(d => d.StringIncrementAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new RedisConnectionException(ConnectionFailureType.UnableToConnect, "down")); + + var connection = new Mock(); + connection.Setup(c => c.GetDatabase(It.IsAny(), It.IsAny())).Returns(database.Object); + + await using var sut = CreateLimiter(connection.Object, permitLimit: 1); + + using var lease = await sut.AcquireAsync(); + + lease.IsAcquired.Should().BeTrue(); + } + + // A request asking for more permits than the window can ever hold is rejected without a round + // trip: incrementing for it would burn the whole window's allowance on a request that can + // never be satisfied. + [Fact] + public async Task AcquireAsync_WhenPermitCountExceedsTheLimit_RejectsWithoutTouchingRedis() + { + var (connection, database) = CreateConnection(incrementResult: 1); + await using var sut = CreateLimiter(connection.Object, permitLimit: 5); + + using var lease = await sut.AcquireAsync(permitCount: 6); + + lease.IsAcquired.Should().BeFalse(); + database.Verify( + d => d.StringIncrementAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + // The synchronous path exists only to satisfy the base contract: the ASP.NET Core middleware + // uses AcquireAsync, and blocking a request thread on a Redis round trip would be worse than + // the fail-open posture the limiter already takes. + [Fact] + public async Task AttemptAcquire_AlwaysPermits() + { + var (connection, database) = CreateConnection(incrementResult: 99); + await using var sut = CreateLimiter(connection.Object, permitLimit: 1); + + using var lease = sut.AttemptAcquire(); + + lease.IsAcquired.Should().BeTrue(); + database.Verify( + d => d.StringIncrementAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + // Reported rather than null so the owning PartitionedRateLimiter can evict partitions: keys + // embed user identity, so a never-idle limiter would grow the partition table without bound. + [Fact] + public async Task IdleDuration_IsReportedSoUnusedPartitionsCanBeEvicted() + { + var (connection, _) = CreateConnection(incrementResult: 1); + await using var sut = CreateLimiter(connection.Object, permitLimit: 5); + + sut.IdleDuration.Should().NotBeNull(); + sut.IdleDuration!.Value.Should().BeGreaterThanOrEqualTo(TimeSpan.Zero); + } + + [Fact] + public async Task GetStatistics_ReturnsNullBecauseTheCounterLivesInRedis() + { + var (connection, _) = CreateConnection(incrementResult: 1); + await using var sut = CreateLimiter(connection.Object, permitLimit: 5); + + sut.GetStatistics().Should().BeNull(); + } + + [Fact] + public void Constructor_RejectsANonPositivePermitLimit() + { + var (connection, _) = CreateConnection(incrementResult: 1); + + Action act = () => _ = new RedisFixedWindowRateLimiter( + connection.Object, "global:alice", 0, NullLogger.Instance); + + act.Should().Throw(); + } + + private static RedisFixedWindowRateLimiter CreateLimiter(IConnectionMultiplexer connection, int permitLimit) => + new(connection, "global:alice", permitLimit, NullLogger.Instance, new FakeTimeProvider(FixedInstant)); + + private static (Mock Connection, Mock Database) CreateConnection(long incrementResult) + { + var database = new Mock(); + database.Setup(d => d.StringIncrementAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(incrementResult); + database.Setup(d => d.KeyExpireAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(true); + + var connection = new Mock(); + connection.Setup(c => c.GetDatabase(It.IsAny(), It.IsAny())).Returns(database.Object); + + return (connection, database); + } +} diff --git a/Tests/Presentation/MMCA.Common.API.Tests/Startup/RateLimitAlgorithmSelectionTests.cs b/Tests/Presentation/MMCA.Common.API.Tests/Startup/RateLimitAlgorithmSelectionTests.cs new file mode 100644 index 00000000..d0bcec76 --- /dev/null +++ b/Tests/Presentation/MMCA.Common.API.Tests/Startup/RateLimitAlgorithmSelectionTests.cs @@ -0,0 +1,158 @@ +using System.Net; +using System.Reflection; +using System.Security.Claims; +using System.Threading.RateLimiting; +using AwesomeAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using MMCA.Common.API.RateLimiting; +using MMCA.Common.API.Startup; +using Moq; +using StackExchange.Redis; + +namespace MMCA.Common.API.Tests.Startup; + +/// +/// Asserts which limiter each partition factory actually builds. The partition key alone does not +/// say whether a request is counted in memory or against the shared Redis counter, and the +/// difference is the whole point of , so these tests +/// resolve the partition's factory and inspect the limiter it produces. +/// +public sealed class RateLimitAlgorithmSelectionTests +{ + [Fact] + public void GlobalPartition_WithFixedWindowSettings_BuildsAFixedWindowLimiter() + { + var partition = WebApplicationBuilderExtensions.GlobalRateLimitPartition( + AuthenticatedContext(), new RateLimitingSettings()); + + using var limiter = ResolveLimiter(partition); + + limiter.Should().BeOfType(); + } + + [Fact] + public void GlobalPartition_WithSlidingWindowSettings_BuildsASlidingWindowLimiter() + { + var partition = WebApplicationBuilderExtensions.GlobalRateLimitPartition( + AuthenticatedContext(), + new RateLimitingSettings { Algorithm = RateLimitAlgorithm.SlidingWindow, SegmentsPerWindow = 6 }); + + using var limiter = ResolveLimiter(partition); + + limiter.Should().BeOfType(); + } + + [Fact] + public void GlobalPartition_WhenDistributedAndRedisIsPresent_BuildsTheSharedCounterLimiter() + { + var partition = WebApplicationBuilderExtensions.GlobalRateLimitPartition( + AuthenticatedContext(withRedis: true), + new RateLimitingSettings { Distributed = true }); + + using var limiter = ResolveLimiter(partition); + + limiter.Should().BeOfType(); + } + + // Turning the flag on before wiring Redis must degrade to per-instance limits rather than + // failing startup or silently removing the limit altogether. + [Fact] + public void GlobalPartition_WhenDistributedButRedisIsAbsent_FallsBackToTheInMemoryLimiter() + { + var partition = WebApplicationBuilderExtensions.GlobalRateLimitPartition( + AuthenticatedContext(), + new RateLimitingSettings { Distributed = true }); + + using var limiter = ResolveLimiter(partition); + + limiter.Should().BeOfType(); + } + + [Fact] + public void UserPolicyPartition_PartitionsByNameAndHonorsTheAlgorithm() + { + var partition = WebApplicationBuilderExtensions.UserPolicyRateLimitPartition( + AuthenticatedContext(), + new RateLimitingSettings { Algorithm = RateLimitAlgorithm.SlidingWindow }); + + partition.PartitionKey.Should().Be("alice"); + + using var limiter = ResolveLimiter(partition); + + limiter.Should().BeOfType(); + } + + [Fact] + public void UserPolicyPartition_WhenNoIdentity_FallsBackToTheAnonymousBucket() => + WebApplicationBuilderExtensions.UserPolicyRateLimitPartition( + new DefaultHttpContext(), new RateLimitingSettings()) + .PartitionKey.Should().Be("anonymous"); + + // The login throttle stays per-instance even with the distributed flag on: per-account lockout + // already backs it, and a login throttle that fails open on a Redis outage is a worse trade + // than one that stays local. + [Fact] + public void AuthIpPartition_WhenDistributed_StaysInMemory() + { + var partition = WebApplicationBuilderExtensions.AuthIpRateLimitPartition( + AuthenticatedContext(withRedis: true), + new RateLimitingSettings { Distributed = true }); + + using var limiter = ResolveLimiter(partition); + + limiter.Should().BeOfType(); + } + + [Fact] + public void AuthIpPartition_WithSlidingWindowSettings_BuildsASlidingWindowLimiter() + { + var partition = WebApplicationBuilderExtensions.AuthIpRateLimitPartition( + AuthenticatedContext(), + new RateLimitingSettings { Algorithm = RateLimitAlgorithm.SlidingWindow }); + + using var limiter = ResolveLimiter(partition); + + limiter.Should().BeOfType(); + } + + /// + /// Invokes the partition's limiter factory. RateLimitPartition<TKey>.Factory is + /// internal to the BCL, so it is reached by looking for the only member of the struct whose + /// value is a limiter factory rather than by hard-coding its name. + /// + private static RateLimiter ResolveLimiter(RateLimitPartition partition) + { + const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + + var factory = typeof(RateLimitPartition).GetProperties(flags) + .Select(property => property.GetValue(partition)) + .Concat(typeof(RateLimitPartition).GetFields(flags) + .Select(field => field.GetValue(partition))) + .OfType>() + .FirstOrDefault(); + + factory.Should().NotBeNull( + because: "RateLimitPartition must expose the limiter factory this test resolves the built limiter through"); + + return factory(partition.PartitionKey); + } + + private static DefaultHttpContext AuthenticatedContext(bool withRedis = false) + { + var context = new DefaultHttpContext(); + context.Request.Path = "/api/events"; + context.Connection.RemoteIpAddress = IPAddress.Parse("203.0.113.7"); + context.User = new ClaimsPrincipal( + new ClaimsIdentity([new Claim(ClaimTypes.Name, "alice")], authenticationType: "TestAuth")); + + var services = new ServiceCollection(); + if (withRedis) + { + services.AddSingleton(Mock.Of()); + } + + context.RequestServices = services.BuildServiceProvider(); + return context; + } +} From 51b3865703e2917f3683a891a6aa2e13758977f2 Mon Sep 17 00:00:00 2001 From: Ivan Ball-llovera Date: Tue, 18 Aug 2026 11:04:53 -0400 Subject: [PATCH 3/4] feat: specification-driven repository with projection pushdown, keyset pagination, deterministic paged ordering (A2/A3) - QuerySpecification (ordering/includes/paging/tracking) over the existing Specification base; And/Or/Not now compose by parameter substitution with per-instance caching (no Expression.Invoke); fluent And/Or/Not extensions. - IEntityQuerier: ListAsync (+projection overload), CountAsync/AnyAsync by specification, GetPageByCursorAsync (keyset, versioned base64url cursor, Result-based validation failures); SpecificationEvaluator owns the include split-query switch once. - IEntityQueryService widened to ISpecification; optional IEntityDTOProjector (second ctor) drives a server-side projection path in the pipeline (ExecuteProjectedAsync) when no cross-source includes and not tracking; PushNotification projector ships as the reference implementation with a value-equivalence test against the instance mapper. - Paged reads are now deterministic: default Id ordering when unsorted and an Id tie-break appended to caller sorts; unpaginated reads unchanged. - Known consumer-facing nuance: a literal CountAsync(null) call is now ambiguous (cast required); canary verifies Helpdesk. - 160 new tests (3611 total green); FACTS regenerated (79 executed fitness methods); perf baseline untouched, dry benchmarks clean. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RDQ4jE9QP6pC1ShZVG8ov5 --- FACTS.md | 4 +- .../DependencyInjection.cs | 9 + .../Interfaces/IEntityDTOProjector.cs | 63 ++ .../Interfaces/IEntityQueryService.cs | 10 +- .../Interfaces/Infrastructure/IRepository.cs | 87 +++ .../Notifications/DependencyInjection.cs | 7 + .../DTOs/PushNotificationDTOProjector.cs | 45 ++ .../Services/EntityQueryService.cs | 139 +++- .../Services/Query/EntityQueryPipeline.cs | 121 +++- .../Services/Query/IEntityQueryPipeline.cs | 34 + .../Services/QueryFieldService.cs | 86 ++- .../CrossSourceSpecification.cs | 12 +- .../MMCA.Common.Domain.csproj | 8 + .../Specifications/ParameterReplacer.cs | 46 ++ .../Specifications/QuerySpecification.cs | 150 +++++ .../Specifications/Specification.cs | 132 +++- .../Specifications/SpecificationExtensions.cs | 92 +++ .../Repositories/EFReadRepository.cs | 199 +++++- .../Repositories/EFReadRepositoryDecorator.cs | 34 + .../Repositories/KeysetQueryBuilder.cs | 275 ++++++++ .../Repositories/SpecificationEvaluator.cs | 198 ++++++ .../Abstractions/KeysetPagination.cs | 215 +++++++ .../SpecificationFitnessTests.cs | 38 ++ .../PushNotificationDTOProjectorTests.cs | 124 ++++ .../EntityQueryPipelineOrderingTests.cs | 140 ++++ .../EntityQueryServiceProjectionTests.cs | 259 ++++++++ .../EntityQueryServiceResolutionTests.cs | 100 +++ .../QueryFieldServiceTieBreakTests.cs | 101 +++ .../Specifications/QuerySpecificationTests.cs | 145 +++++ .../SpecificationCompositionTests.cs | 314 +++++++++ .../packages.lock.json | 603 +++++++++--------- .../EFReadRepositoryKeysetPagingTests.cs | 270 ++++++++ .../EFReadRepositorySpecificationTests.cs | 264 ++++++++ .../EFRepositoryIntegrationTests.cs | 2 +- ...hNotificationProjectionTranslationTests.cs | 125 ++++ .../SpecificationEvaluatorTests.cs | 238 +++++++ .../Persistence/SpecificationTestContext.cs | 75 +++ .../Abstractions/KeysetPaginationTests.cs | 167 +++++ .../EntityControllerBaseExportTests.cs | 11 +- 39 files changed, 4515 insertions(+), 427 deletions(-) create mode 100644 Source/Core/MMCA.Common.Application/Interfaces/IEntityDTOProjector.cs create mode 100644 Source/Core/MMCA.Common.Application/Notifications/PushNotifications/DTOs/PushNotificationDTOProjector.cs create mode 100644 Source/Core/MMCA.Common.Domain/Specifications/ParameterReplacer.cs create mode 100644 Source/Core/MMCA.Common.Domain/Specifications/QuerySpecification.cs create mode 100644 Source/Core/MMCA.Common.Domain/Specifications/SpecificationExtensions.cs create mode 100644 Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/KeysetQueryBuilder.cs create mode 100644 Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/SpecificationEvaluator.cs create mode 100644 Source/Core/MMCA.Common.Shared/Abstractions/KeysetPagination.cs create mode 100644 Tests/Core/MMCA.Common.Application.Tests/Notifications/PushNotificationDTOProjectorTests.cs create mode 100644 Tests/Core/MMCA.Common.Application.Tests/Services/EntityQueryPipelineOrderingTests.cs create mode 100644 Tests/Core/MMCA.Common.Application.Tests/Services/EntityQueryServiceProjectionTests.cs create mode 100644 Tests/Core/MMCA.Common.Application.Tests/Services/EntityQueryServiceResolutionTests.cs create mode 100644 Tests/Core/MMCA.Common.Application.Tests/Services/QueryFieldServiceTieBreakTests.cs create mode 100644 Tests/Core/MMCA.Common.Domain.Tests/Specifications/QuerySpecificationTests.cs create mode 100644 Tests/Core/MMCA.Common.Domain.Tests/Specifications/SpecificationCompositionTests.cs create mode 100644 Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/EFReadRepositoryKeysetPagingTests.cs create mode 100644 Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/EFReadRepositorySpecificationTests.cs create mode 100644 Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/PushNotificationProjectionTranslationTests.cs create mode 100644 Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/SpecificationEvaluatorTests.cs create mode 100644 Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/SpecificationTestContext.cs create mode 100644 Tests/Core/MMCA.Common.Shared.Tests/Abstractions/KeysetPaginationTests.cs diff --git a/FACTS.md b/FACTS.md index ac09ce17..0721f6c0 100644 --- a/FACTS.md +++ b/FACTS.md @@ -1,7 +1,7 @@ # MMCA.Common — Canonical Facts **Single source of truth for the framework-wide facts that otherwise drift across dozens of docs.** -_As of: 2026-08-14 (framework v1.152.0) — **generated from source by `build/facts`; do not hand-edit the numbers below.**_ +_As of: 2026-08-18 (framework v1.152.0) — **generated from source by `build/facts`; do not hand-edit the numbers below.**_ > **Rule: link here, don't restate.** Other docs (scorecards, CLAUDE.md files, READMEs, the LinkedIn/Medium > campaigns) must **reference** these facts rather than copy the numbers inline. A "thirteen packages" @@ -44,7 +44,7 @@ it owns the range/count and the one-line summaries. Do not restate the `(001-NNN - **100 test methods across 32 abstract `*TestsBase` classes**, shipped once in the `MMCA.Common.Testing.Architecture` package (ADR-015) and re-run as thin subclasses across all consuming repos (Common, ADC, Store). -- MMCA.Common's own build executes **78** of them (the methods of the bases its arch-tests +- MMCA.Common's own build executes **79** of them (the methods of the bases its arch-tests subclass, plus its Common-only direct tests, e.g. `FrameworkSanityTests`/`SpecificationFitnessTests`). ## Governance rubric diff --git a/Source/Core/MMCA.Common.Application/DependencyInjection.cs b/Source/Core/MMCA.Common.Application/DependencyInjection.cs index c9776022..a1dd2d62 100644 --- a/Source/Core/MMCA.Common.Application/DependencyInjection.cs +++ b/Source/Core/MMCA.Common.Application/DependencyInjection.cs @@ -152,6 +152,15 @@ public IServiceCollection ScanModuleApplicationServices() .AsSelfWithInterfaces() .WithScopedLifetime()); + // DTO projectors are optional and opt-in: an entity that has one gets server-side + // projection on its list reads, an entity that has none keeps materialize-then-map. They + // are scanned beside the mappers so a module only has to write the projector class. + services.Scan(scan => scan + .FromAssemblyOf() + .AddClasses(classes => classes.AssignableTo(typeof(IEntityDTOProjector<,,>))) + .AsSelfWithInterfaces() + .WithScopedLifetime()); + services.Scan(scan => scan .FromAssemblyOf() .AddClasses(classes => classes.AssignableTo(typeof(IEntityRequestMapper<,,>))) diff --git a/Source/Core/MMCA.Common.Application/Interfaces/IEntityDTOProjector.cs b/Source/Core/MMCA.Common.Application/Interfaces/IEntityDTOProjector.cs new file mode 100644 index 00000000..1a933b4f --- /dev/null +++ b/Source/Core/MMCA.Common.Application/Interfaces/IEntityDTOProjector.cs @@ -0,0 +1,63 @@ +using MMCA.Common.Domain.Entities; +using MMCA.Common.Shared.DTOs; + +namespace MMCA.Common.Application.Interfaces; + +/// +/// Opt-in server-side projection from an entity queryable straight to a DTO queryable, so the +/// database returns only the columns the DTO actually has. +/// +/// It is the pushdown counterpart of : +/// the mapper maps rows AFTER they are materialized, so the query must select whole entities (every +/// column, plus a JOIN per include the DTO happens to flatten); a projector rewrites the query so the +/// provider selects the DTO's columns directly. Registering one for an entity is what switches the +/// query service's list reads onto the projected path. Nothing breaks when none is registered: the +/// service falls back to materialize-then-map. +/// +/// +/// +/// +/// Implementations are typically a Mapperly [Mapper]-generated static projection wrapped in a +/// small class, for example: +/// +/// +/// [Mapper] +/// internal static partial class OrderDTOProjection +/// { +/// internal static partial IQueryable<OrderDTO> ProjectToDTO(IQueryable<Order> source); +/// } +/// +/// public sealed class OrderDTOProjector : IEntityDTOProjector<Order, OrderDTO, int> +/// { +/// public IQueryable<OrderDTO> ProjectTo(IQueryable<Order> source) => +/// OrderDTOProjection.ProjectToDTO(source); +/// } +/// +/// +/// A projection is an expression tree the provider must translate, which constrains what it can +/// express: no instance sub-mappers, no custom mapping methods (Use = nameof(...)), no +/// after-map hooks, nothing that would have to run in .NET on a materialized object. A DTO whose +/// shape needs any of those simply does not get a projector, and its reads keep using the mapper. +/// +/// +/// A projector MUST produce the same values as the entity's mapper for the same row. The two paths +/// are chosen by configuration, so a divergence would make a response depend on whether a projector +/// happened to be registered. Pin the equivalence with a test. +/// +/// +/// The domain entity type. +/// The DTO type. +/// The entity's primary key type. +public interface IEntityDTOProjector + where TEntity : AuditableBaseEntity + where TEntityDTO : IBaseDTO + where TIdentifierType : notnull +{ + /// + /// Rewrites an entity queryable into a DTO queryable. The result must still be a translatable + /// queryable: do not materialize inside the implementation. + /// + /// The entity queryable, already filtered, sorted, and paged. + /// The projected DTO queryable. + IQueryable ProjectTo(IQueryable source); +} diff --git a/Source/Core/MMCA.Common.Application/Interfaces/IEntityQueryService.cs b/Source/Core/MMCA.Common.Application/Interfaces/IEntityQueryService.cs index 7f9f5977..19f3cf90 100644 --- a/Source/Core/MMCA.Common.Application/Interfaces/IEntityQueryService.cs +++ b/Source/Core/MMCA.Common.Application/Interfaces/IEntityQueryService.cs @@ -1,6 +1,6 @@ using System.Linq.Expressions; using MMCA.Common.Domain.Entities; -using MMCA.Common.Domain.Specifications; +using MMCA.Common.Domain.Interfaces; using MMCA.Common.Shared.Abstractions; using MMCA.Common.Shared.DTOs; @@ -37,7 +37,7 @@ public interface IEntityQueryService Task>> GetAllAsync( bool includeFKs = false, bool includeChildren = false, - Specification? specification = null, + ISpecification? specification = null, string? fields = null, bool asTracking = false, CancellationToken cancellationToken = default); @@ -60,7 +60,7 @@ Task>> GetAllAsync( Task>> GetAllAsync( bool includeFKs = false, bool includeChildren = false, - Specification? specification = null, + ISpecification? specification = null, Dictionary? filters = null, string? sortColumn = null, string? sortDirection = null, @@ -107,7 +107,7 @@ Task> GetEntityByIdAsync( string? idField = null, bool includeFKs = false, bool includeChildren = false, - Specification? specification = null, + ISpecification? specification = null, string? fields = null, bool asTracking = false, CancellationToken cancellationToken = default); @@ -128,7 +128,7 @@ Task> GetByIdAsync( TIdentifierType id, bool includeFKs = false, bool includeChildren = false, - Specification? specification = null, + ISpecification? specification = null, string? fields = null, bool asTracking = false, CancellationToken cancellationToken = default); diff --git a/Source/Core/MMCA.Common.Application/Interfaces/Infrastructure/IRepository.cs b/Source/Core/MMCA.Common.Application/Interfaces/Infrastructure/IRepository.cs index 6713e975..bd1ecbe7 100644 --- a/Source/Core/MMCA.Common.Application/Interfaces/Infrastructure/IRepository.cs +++ b/Source/Core/MMCA.Common.Application/Interfaces/Infrastructure/IRepository.cs @@ -1,5 +1,7 @@ using System.Linq.Expressions; using MMCA.Common.Domain.Entities; +using MMCA.Common.Domain.Interfaces; +using MMCA.Common.Shared.Abstractions; using MMCA.Common.Shared.DTOs; namespace MMCA.Common.Application.Interfaces.Infrastructure; @@ -121,6 +123,91 @@ Task>> GetAllForLookupAsync( Task CountAsync( Expression> where, CancellationToken cancellationToken = default); + + /// + /// Counts the rows a specification matches. Ordering and paging on the specification are ignored + /// deliberately: a count of "page 3 of the matches" is never what a caller means. + /// + /// The specification describing the read. + /// Cancellation token. + /// The number of matching rows. + Task CountAsync( + ISpecification specification, + CancellationToken cancellationToken = default); + + /// + /// Executes a specification and returns the matching entities. + /// + /// + /// A plain contributes its + /// Criteria only. A + /// also + /// contributes its includes, ordering, paging, tracking, and soft-delete scope, so the whole + /// read is described by one object instead of five loose arguments. + /// + /// The specification describing the read. + /// Cancellation token. + /// The matching entities. + Task> ListAsync( + ISpecification specification, + CancellationToken cancellationToken = default); + + /// + /// Executes a specification and projects the matching entities server-side, so only the selected + /// columns leave the database (projection pushdown). + /// + /// + /// The projection is applied AFTER the specification's ordering and paging, so a paged + /// specification still pages over entity rows and projects only that page. Includes on the + /// specification are redundant on this overload (the projection decides what is loaded) but not + /// harmful. + /// + /// The projected result type. + /// The specification describing the read. + /// The projection expression (must be translatable by the provider). + /// Cancellation token. + /// The projected results. + Task> ListAsync( + ISpecification specification, + Expression> select, + CancellationToken cancellationToken = default); + + /// + /// Checks whether a specification matches any row. Ordering and paging are ignored, as for + /// . + /// + /// The specification describing the read. + /// Cancellation token. + /// when at least one row matches. + Task AnyAsync( + ISpecification specification, + CancellationToken cancellationToken = default); + + /// + /// Reads one keyset ("seek") page: the rows strictly after the request's cursor, ordered by the + /// requested sort column with Id as tie-break, plus the cursor for the next page. + /// + /// + /// + /// Unlike offset paging this costs one index seek regardless of how deep the caller has scrolled, + /// and it never skips or repeats a row when the underlying set changes between pages. The trade + /// is no random page access and no total count. + /// + /// + /// Exactly one sort key is supported. A null keys the + /// page on Id alone. The sort column must name a real public property of the entity: an + /// unknown name and a malformed cursor both come back as a validation failure, never as a silent + /// first page. + /// + /// + /// The page size, sort key, direction, and cursor. + /// Optional specification whose Criteria scopes the page. + /// Cancellation token. + /// The page and its next cursor, or a validation failure. + Task>> GetPageByCursorAsync( + KeysetPageRequest request, + ISpecification? specification = null, + CancellationToken cancellationToken = default); } /// diff --git a/Source/Core/MMCA.Common.Application/Notifications/DependencyInjection.cs b/Source/Core/MMCA.Common.Application/Notifications/DependencyInjection.cs index e6a07600..4bd240b0 100644 --- a/Source/Core/MMCA.Common.Application/Notifications/DependencyInjection.cs +++ b/Source/Core/MMCA.Common.Application/Notifications/DependencyInjection.cs @@ -44,6 +44,13 @@ public IServiceCollection AddNotificationApplicationServices() services.TryAddScoped, PushNotificationDTOMapper>(); + // DTO projector: registering it is what switches notification list reads onto the + // server-side projection path (the query service resolves it through its longer + // constructor). The projected values are pinned equal to the mapper's by test. + services.TryAddScoped(); + services.TryAddScoped, + PushNotificationDTOProjector>(); + // Command handlers services.TryAddScoped>, SendPushNotificationHandler>(); diff --git a/Source/Core/MMCA.Common.Application/Notifications/PushNotifications/DTOs/PushNotificationDTOProjector.cs b/Source/Core/MMCA.Common.Application/Notifications/PushNotifications/DTOs/PushNotificationDTOProjector.cs new file mode 100644 index 00000000..9c606efb --- /dev/null +++ b/Source/Core/MMCA.Common.Application/Notifications/PushNotifications/DTOs/PushNotificationDTOProjector.cs @@ -0,0 +1,45 @@ +using MMCA.Common.Application.Interfaces; +using MMCA.Common.Domain.Notifications.PushNotifications; +using MMCA.Common.Shared.Notifications.PushNotifications; +using Riok.Mapperly.Abstractions; + +namespace MMCA.Common.Application.Notifications.PushNotifications.DTOs; + +/// +/// Mapperly-generated server-side projection from to +/// . It is the framework's worked example of a projection: the +/// query returns the DTO's columns instead of whole notification rows that are mapped afterwards. +/// +/// +/// The entity's Status is an enum and the DTO's is a string. The instance mapper renders it +/// with a custom method (Use = nameof(MapStatusToString)), which a projection cannot call: a +/// projection is an expression tree the database has to translate. Mapperly's enum-to-string mapping +/// is expressible, so it is inlined here as a conditional over the known members (a SQL +/// CASE), producing the same strings as PushNotificationStatus.ToString(). The +/// equivalence with the instance mapper is pinned by a test. +/// +[Mapper] +internal static partial class PushNotificationDTOProjection +{ + /// Projects a notification queryable to a DTO queryable, server-side. + /// The entity queryable. + /// The projected DTO queryable. + internal static partial IQueryable ProjectToDTO(IQueryable source); +} + +/// +/// The wrapper around +/// , registered by +/// AddNotificationApplicationServices so notification list reads use projection pushdown. +/// +public sealed class PushNotificationDTOProjector + : IEntityDTOProjector +{ + /// + public IQueryable ProjectTo(IQueryable source) + { + ArgumentNullException.ThrowIfNull(source); + + return PushNotificationDTOProjection.ProjectToDTO(source); + } +} diff --git a/Source/Core/MMCA.Common.Application/Services/EntityQueryService.cs b/Source/Core/MMCA.Common.Application/Services/EntityQueryService.cs index b8162eb6..835ffcd1 100644 --- a/Source/Core/MMCA.Common.Application/Services/EntityQueryService.cs +++ b/Source/Core/MMCA.Common.Application/Services/EntityQueryService.cs @@ -7,7 +7,7 @@ using MMCA.Common.Application.Services.Filtering; using MMCA.Common.Application.Services.Query; using MMCA.Common.Domain.Entities; -using MMCA.Common.Domain.Specifications; +using MMCA.Common.Domain.Interfaces; using MMCA.Common.Shared.Abstractions; using MMCA.Common.Shared.DTOs; @@ -44,6 +44,45 @@ public class EntityQueryService( private INavigationMetadataProvider NavigationMetadataProvider { get; } = navigationMetadataProvider ?? throw new ArgumentNullException(nameof(navigationMetadataProvider)); private IEntityQueryPipeline QueryPipeline { get; } = queryPipeline ?? throw new ArgumentNullException(nameof(queryPipeline)); + /// + /// Initializes the service with a DTO projector, enabling the server-side projection path for + /// list reads. + /// + /// + /// + /// It is a second constructor rather than an optional parameter on the first because + /// Microsoft.Extensions.DependencyInjection has no notion of an optional dependency: a + /// single constructor naming a service nobody registered fails to resolve, default value or not. + /// With two constructors the container picks the longer one when an + /// is registered and the + /// shorter one when it is not, and there is no ambiguity because one parameter set is a strict + /// superset of the other. Existing subclasses that chain to the five-argument constructor keep + /// compiling untouched. + /// + /// + /// The unit of work. + /// The navigation metadata provider. + /// The query pipeline. + /// The DTO mapper (still used for by-id reads and the fallback path). + /// The navigation populator. + /// The DTO projector enabling projection pushdown on list reads. + public EntityQueryService( + IUnitOfWork unitOfWork, + INavigationMetadataProvider navigationMetadataProvider, + IEntityQueryPipeline queryPipeline, + IEntityDTOMapper dtoMapper, + INavigationPopulator navigationPopulator, + IEntityDTOProjector dtoProjector) + : this(unitOfWork, navigationMetadataProvider, queryPipeline, dtoMapper, navigationPopulator) + => DTOProjector = dtoProjector ?? throw new ArgumentNullException(nameof(dtoProjector)); + + /// + /// Gets the optional DTO projector. When present, list reads that qualify are served by + /// server-side projection instead of materialize-then-map; when null every read uses + /// . + /// + protected IEntityDTOProjector? DTOProjector { get; } + /// Gets the read repository. Override to provide a custom repository (e.g. with query filters). protected virtual IReadRepository Repository { get; } = unitOfWork.GetReadRepository(); @@ -82,7 +121,7 @@ public class EntityQueryService( string? idField, bool includeFKs, bool includeChildren, - Specification? specification, + ISpecification? specification, string? fields, bool asTracking, CancellationToken cancellationToken) @@ -123,7 +162,7 @@ private bool TryGetFastPathIncludes( string? idField, bool includeFKs, bool includeChildren, - Specification? specification, + ISpecification? specification, string? fields, out IReadOnlyList includes) { @@ -188,7 +227,7 @@ private static bool TryConvertId(string idValue, out TIdentifierType id) public virtual async Task>> GetAllAsync( bool includeFKs = false, bool includeChildren = false, - Specification? specification = null, + ISpecification? specification = null, string? fields = null, bool asTracking = false, CancellationToken cancellationToken = default) @@ -209,7 +248,7 @@ public virtual async Task>> GetAllAsync( public virtual async Task>> GetAllAsync( bool includeFKs = false, bool includeChildren = false, - Specification? specification = null, + ISpecification? specification = null, Dictionary? filters = null, string? sortColumn = null, string? sortDirection = null, @@ -240,27 +279,58 @@ public virtual async Task>> GetAllAsync( } // Step 2: Execute the query pipeline (includes, criteria, filters, sort, pagination, field selection) - var (entities, totalItemCount) = await BuildQueryAsync( - includeFKs: includeFKs, - includeChildren: includeChildren, - specification: specification, - filters: filters, - sortColumn: sortColumn, - sortDirection: sortDirection, - fields: fields, - pageNumber: pageNumber, - pageSize: pageSize, - asTracking: asTracking, - cancellationToken: cancellationToken).ConfigureAwait(false); + var navigationMetadata = NavigationMetadataProvider.BuildIncludes(includeFKs, includeChildren); - // Step 3: Map entities to DTOs and shape output to requested fields. + var parameters = new EntityQueryParameters + { + Criteria = specification?.Criteria, + Filters = filters, + SortColumn = sortColumn, + SortDirection = sortDirection, + Fields = fields, + PageNumber = pageNumber, + PageSize = pageSize, + IncludeFKs = includeFKs, + IncludeChildren = includeChildren, + DTOToEntityPropertyMap = DTOToEntityPropertyMap + }; + + var baseQuery = asTracking ? Repository.Table : Repository.TableNoTracking; + + IReadOnlyCollection pagedDTOs; + int totalItemCount; + + if (CanProject(navigationMetadata, asTracking)) + { + // Projection pushdown: the provider selects the DTO's columns directly, so nothing is + // materialized as an entity and DTOMapper is never involved. + (pagedDTOs, totalItemCount) = await QueryPipeline + .ExecuteProjectedAsync( + baseQuery, + parameters, + DTOProjector!.ProjectTo, + cancellationToken).ConfigureAwait(false); + } + else + { + var (entities, entityTotal) = await QueryPipeline.ExecuteAsync( + baseQuery, + navigationMetadata, + parameters, + NavigationPopulator.PopulateAsync, + cancellationToken).ConfigureAwait(false); + + totalItemCount = entityTotal; + pagedDTOs = DTOMapper.MapToDTOs(entities); + } + + // Step 3: Shape output to requested fields. // Shaping into dynamic objects only pays off when a field subset was requested; // otherwise the typed DTOs are returned as-is and serialize to the same camelCase - // JSON without the per-row ExpandoObject allocation and boxing. + // JSON without the per-row ExpandoObject allocation and boxing. It reflects over the runtime + // object, so it works identically on a mapped DTO and a projected one. var paginationMetadata = BuildPaginationMetadata(totalItemCount, pageNumber, pageSize); - var pagedDTOs = DTOMapper.MapToDTOs(entities); - ICollection items = string.IsNullOrWhiteSpace(fields) ? [.. pagedDTOs.Cast()] : [.. QueryFieldService.ShapeCollectionData(pagedDTOs, fields)]; @@ -308,7 +378,7 @@ public virtual async Task> GetEntityByIdAsync( string? idField = null, bool includeFKs = false, bool includeChildren = false, - Specification? specification = null, + ISpecification? specification = null, string? fields = null, bool asTracking = false, CancellationToken cancellationToken = default) @@ -368,7 +438,7 @@ public virtual async Task> GetByIdAsync( TIdentifierType id, bool includeFKs = false, bool includeChildren = false, - Specification? specification = null, + ISpecification? specification = null, string? fields = null, bool asTracking = false, CancellationToken cancellationToken = default) @@ -400,13 +470,34 @@ public Task ExistsAsync( CancellationToken cancellationToken = default) => Repository.ExistsAsync(where, ignoreQueryFilters, cancellationToken); + /// + /// Whether this read can be served by the server-side projection path. + /// + /// + /// + /// Three conditions. A projector must be registered at all. There must be no unsupported + /// (cross-source) includes: those are loaded row by row after materialization by the navigation + /// populator, which a projection has no rows to hand it. And the caller must not have asked for + /// tracking: a projection produces DTOs, which the change tracker has nothing to do with, so + /// honoring asTracking means staying on the entity path. + /// + /// + /// Field shaping deliberately does NOT disqualify: shaping runs after materialization, over + /// whatever object the pipeline produced, so it behaves the same on a projected DTO. + /// + /// + private bool CanProject(NavigationMetadata navigationMetadata, bool asTracking) + => DTOProjector is not null + && !asTracking + && navigationMetadata.UnsupportedIncludes.Count == 0; + /// /// Assembles the query parameters and delegates execution to the . /// private async Task<(IReadOnlyCollection Items, int TotalCount)> BuildQueryAsync( bool includeFKs, bool includeChildren, - Specification? specification, + ISpecification? specification, Dictionary? filters, string? sortColumn, string? sortDirection, diff --git a/Source/Core/MMCA.Common.Application/Services/Query/EntityQueryPipeline.cs b/Source/Core/MMCA.Common.Application/Services/Query/EntityQueryPipeline.cs index 136366b0..798a275b 100644 --- a/Source/Core/MMCA.Common.Application/Services/Query/EntityQueryPipeline.cs +++ b/Source/Core/MMCA.Common.Application/Services/Query/EntityQueryPipeline.cs @@ -22,6 +22,19 @@ public sealed class EntityQueryPipeline(IQueryableExecutor queryableExecutor) : /// public const int MaxUnboundedResultLimit = 1000; + /// + /// The key property appended as a final ascending sort key on every paginated read, making the + /// order total. Every entity has it (IBaseEntity.Id), it is unique, and it is + /// server-supplied rather than client input. + /// + /// + /// Only paginated reads get it. An unpaginated read materializes one capped set in one + /// statement, so it cannot suffer the split-across-pages incoherence the tie-break exists to + /// prevent, and adding an ORDER BY there would charge every unsorted list read for a sort the + /// caller never asked for. + /// + private const string PaginationTieBreakProperty = "Id"; + /// public async Task<(IReadOnlyCollection Items, int TotalCount)> ExecuteAsync( IQueryable baseQuery, @@ -31,6 +44,84 @@ public sealed class EntityQueryPipeline(IQueryableExecutor queryableExecutor) : CancellationToken cancellationToken) where TEntity : AuditableBaseEntity where TIdentifierType : notnull + { + var query = ApplyIncludesCriteriaAndFilters(baseQuery, navigationMetadata, parameters); + + // PATH 2 - Unsupported includes: When the data source does not support JOINs + // (e.g. Cosmos DB where entities may live in different containers), we must + // materialize the query, then manually load related data via the NavigationPopulator. + if (navigationMetadata.UnsupportedIncludes.Count != 0) + return await ExecuteWithManualNavigationAsync(query, navigationMetadata, parameters, navigationPopulator, cancellationToken).ConfigureAwait(false); + + return await ExecuteWithServerSideIncludesAsync(query, parameters, cancellationToken).ConfigureAwait(false); + } + + /// + public async Task<(IReadOnlyCollection Items, int TotalCount)> ExecuteProjectedAsync( + IQueryable baseQuery, + EntityQueryParameters parameters, + Func, IQueryable> project, + CancellationToken cancellationToken) + where TEntity : AuditableBaseEntity + where TIdentifierType : notnull + { + ArgumentNullException.ThrowIfNull(parameters); + ArgumentNullException.ThrowIfNull(project); + + var query = baseQuery; + + if (parameters.Criteria is not null) + query = query.Where(parameters.Criteria); + + if (parameters.Filters is not null && parameters.Filters.Count != 0) + query = QueryFilterService.ApplyFilters(query, parameters.Filters, parameters.DTOToEntityPropertyMap); + + bool isPaginated = parameters.PageNumber.HasValue && parameters.PageSize.HasValue; + + query = QueryFieldService.ApplySorting( + query, + parameters.SortColumn, + parameters.SortDirection, + parameters.DTOToEntityPropertyMap, + tieBreakProperty: isPaginated ? PaginationTieBreakProperty : null); + + int totalCount = 0; + var unpagedQuery = query; + + if (isPaginated) + { + totalCount = await queryableExecutor.CountAsync(query, cancellationToken).ConfigureAwait(false); + query = ApplyPaging(query, parameters); + } + else + { + query = query.Take(MaxUnboundedResultLimit); + } + + // Project LAST: filtering, sorting and paging all run over entity rows, so the provider pages + // exactly the rows it means to and only that page's columns are selected. Navigation includes + // are deliberately not applied on this path: the projection itself decides which columns and + // joins the provider emits, and an Include over a non-entity projection is ignored anyway. + var result = await queryableExecutor.ToListAsync(project(query), cancellationToken).ConfigureAwait(false); + + if (!isPaginated) + { + totalCount = await CountUnpaginatedAsync(unpagedQuery, result.Count, cancellationToken).ConfigureAwait(false); + } + + return (result, totalCount); + } + + /// + /// Shared front half of the entity path: server-side includes (with the child-collection + /// split-query switch), specification criteria, and dynamic filters. + /// + private IQueryable ApplyIncludesCriteriaAndFilters( + IQueryable baseQuery, + NavigationMetadata navigationMetadata, + EntityQueryParameters parameters) + where TEntity : AuditableBaseEntity + where TIdentifierType : notnull { var query = baseQuery; @@ -59,13 +150,7 @@ public sealed class EntityQueryPipeline(IQueryableExecutor queryableExecutor) : if (parameters.Filters is not null && parameters.Filters.Count != 0) query = QueryFilterService.ApplyFilters(query, parameters.Filters, parameters.DTOToEntityPropertyMap); - // PATH 2 - Unsupported includes: When the data source does not support JOINs - // (e.g. Cosmos DB where entities may live in different containers), we must - // materialize the query, then manually load related data via the NavigationPopulator. - if (navigationMetadata.UnsupportedIncludes.Count != 0) - return await ExecuteWithManualNavigationAsync(query, navigationMetadata, parameters, navigationPopulator, cancellationToken).ConfigureAwait(false); - - return await ExecuteWithServerSideIncludesAsync(query, parameters, cancellationToken).ConfigureAwait(false); + return query; } /// @@ -85,8 +170,14 @@ public sealed class EntityQueryPipeline(IQueryableExecutor queryableExecutor) : bool isPaginated = parameters.PageNumber.HasValue && parameters.PageSize.HasValue; int totalCount = 0; - // Sort at the DB level before materialization - query = QueryFieldService.ApplySorting(query, parameters.SortColumn, parameters.SortDirection, parameters.DTOToEntityPropertyMap); + // Sort at the DB level before materialization. A paginated read also gets the key tie-break, + // so Skip/Take runs over a total order (see PaginationTieBreakProperty). + query = QueryFieldService.ApplySorting( + query, + parameters.SortColumn, + parameters.SortDirection, + parameters.DTOToEntityPropertyMap, + tieBreakProperty: isPaginated ? PaginationTieBreakProperty : null); var unpagedQuery = query; @@ -129,9 +220,17 @@ public sealed class EntityQueryPipeline(IQueryableExecutor queryableExecutor) : where TEntity : AuditableBaseEntity where TIdentifierType : notnull { - query = QueryFieldService.ApplySorting(query, parameters.SortColumn, parameters.SortDirection, parameters.DTOToEntityPropertyMap); - bool isPaginated = parameters.PageNumber.HasValue && parameters.PageSize.HasValue; + + // A paginated read also gets the key tie-break, so Skip/Take runs over a total order + // (see PaginationTieBreakProperty). + query = QueryFieldService.ApplySorting( + query, + parameters.SortColumn, + parameters.SortDirection, + parameters.DTOToEntityPropertyMap, + tieBreakProperty: isPaginated ? PaginationTieBreakProperty : null); + int totalCount = 0; var unpagedQuery = query; diff --git a/Source/Core/MMCA.Common.Application/Services/Query/IEntityQueryPipeline.cs b/Source/Core/MMCA.Common.Application/Services/Query/IEntityQueryPipeline.cs index bbf292e6..50931093 100644 --- a/Source/Core/MMCA.Common.Application/Services/Query/IEntityQueryPipeline.cs +++ b/Source/Core/MMCA.Common.Application/Services/Query/IEntityQueryPipeline.cs @@ -28,4 +28,38 @@ public interface IEntityQueryPipeline CancellationToken cancellationToken) where TEntity : AuditableBaseEntity where TIdentifierType : notnull; + + /// + /// Executes the query pipeline with server-side projection: criteria, dynamic filters, sorting + /// and pagination all run over entity rows, then rewrites the query so + /// the provider returns the projected shape directly. Nothing is materialized as an entity. + /// + /// + /// + /// This path exists for reads whose result type has a registered + /// IEntityDTOProjector. It skips two costs of the entity path: selecting every entity + /// column, and mapping each materialized row afterwards. + /// + /// + /// It handles server-side navigations only. There is no navigation-populator hook, because a + /// projection cannot be post-processed row by row, so a query with cross-source + /// (unsupported) includes must use instead. Navigation includes are + /// not applied here either: the projection decides what the provider joins and selects. + /// + /// + /// The entity type. + /// The projected result type. + /// The entity's primary key type. + /// The starting queryable (tracked or untracked). + /// All query parameters (criteria, filters, sort, pagination). + /// Rewrites the entity queryable into the projected queryable. + /// Cancellation token. + /// The materialized projections and total count for pagination. + Task<(IReadOnlyCollection Items, int TotalCount)> ExecuteProjectedAsync( + IQueryable baseQuery, + EntityQueryParameters parameters, + Func, IQueryable> project, + CancellationToken cancellationToken) + where TEntity : AuditableBaseEntity + where TIdentifierType : notnull; } diff --git a/Source/Core/MMCA.Common.Application/Services/QueryFieldService.cs b/Source/Core/MMCA.Common.Application/Services/QueryFieldService.cs index 636c5eec..4b807897 100644 --- a/Source/Core/MMCA.Common.Application/Services/QueryFieldService.cs +++ b/Source/Core/MMCA.Common.Application/Services/QueryFieldService.cs @@ -131,30 +131,90 @@ public static List ShapeCollectionData( /// "asc" or "desc". /// DTO-to-entity property name mapping (server-authored; entries may be navigation paths or expressions). /// Fallback sort expression when no valid sort column is specified. + /// + /// Optional server-supplied property name appended as a final ascending key, making the order + /// total. Pass it whenever the caller pages: see the remarks. + /// /// The sorted queryable. + /// + /// + /// Why the tie-break matters. Skip/Take over a query whose ORDER BY is not + /// total is undefined: rows sharing the sort value may come back in any order, and the database + /// is free to choose a different one per statement. The same row could then appear on two + /// consecutive pages while another appeared on neither, from data that never changed. Sorting on + /// a non-unique column (a status, a name) makes that the common case rather than the corner + /// case, and paging with NO sort column at all leaves the entire order undefined. + /// + /// + /// Passing the entity's key as fixes both shapes: it is + /// appended after the requested sort, or used alone when no valid sort column was given. The + /// value is server-supplied (the pipeline passes "Id"), never client input, so it does not + /// widen what a caller can order by. + /// + /// public static IQueryable ApplySorting( IQueryable query, string? sortColumn, string? sortDirection, IReadOnlyDictionary dtoToEntityPropertyMap, - Expression>? defaultSort = null) + Expression>? defaultSort = null, + string? tieBreakProperty = null) { - if (!string.IsNullOrWhiteSpace(sortColumn)) + var sortExpr = ResolveSortExpression(sortColumn, dtoToEntityPropertyMap); + + if (sortExpr is not null) { - var sortExpr = dtoToEntityPropertyMap.TryGetValue(sortColumn, out var mapped) - ? mapped - : typeof(TEntity).GetProperty( - sortColumn, - BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase)?.Name; + return query.OrderBy( + Filtering.DynamicQueryConfig.Parameterized, + BuildOrdering(sortExpr, sortDirection, tieBreakProperty)); + } - if (sortExpr is not null) - { - var descending = string.Equals(sortDirection, "desc", StringComparison.OrdinalIgnoreCase); - return query.OrderBy(Filtering.DynamicQueryConfig.Parameterized, $"{sortExpr} {(descending ? "descending" : "ascending")}"); - } + if (defaultSort is not null) + { + var ordered = query.OrderBy(defaultSort); + return string.IsNullOrWhiteSpace(tieBreakProperty) + ? ordered + : ordered.ThenBy(Filtering.DynamicQueryConfig.Parameterized, $"{tieBreakProperty} ascending"); } - return defaultSort is not null ? query.OrderBy(defaultSort) : query; + return string.IsNullOrWhiteSpace(tieBreakProperty) + ? query + : query.OrderBy(Filtering.DynamicQueryConfig.Parameterized, $"{tieBreakProperty} ascending"); + } + + /// + /// Resolves a client-supplied sort column to the expression Dynamic LINQ will parse: the + /// server-authored map entry when there is one, otherwise the name of a real public property of + /// the entity, otherwise (the caller falls back). + /// + private static string? ResolveSortExpression( + string? sortColumn, + IReadOnlyDictionary dtoToEntityPropertyMap) + { + if (string.IsNullOrWhiteSpace(sortColumn)) + return null; + + return dtoToEntityPropertyMap.TryGetValue(sortColumn, out var mapped) + ? mapped + : typeof(TEntity).GetProperty( + sortColumn, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase)?.Name; + } + + /// + /// Builds the Dynamic LINQ ordering clause, appending the tie-break key unless the caller + /// already sorted by that very column (repeating a key in an ORDER BY is redundant, and some + /// providers reject it outright). + /// + private static string BuildOrdering(string sortExpr, string? sortDirection, string? tieBreakProperty) + { + var descending = string.Equals(sortDirection, "desc", StringComparison.OrdinalIgnoreCase); + var ordering = $"{sortExpr} {(descending ? "descending" : "ascending")}"; + + return string.IsNullOrWhiteSpace(tieBreakProperty) + || string.Equals(sortExpr, tieBreakProperty, StringComparison.OrdinalIgnoreCase) + ? ordering + : $"{ordering}, {tieBreakProperty} ascending"; } /// diff --git a/Source/Core/MMCA.Common.Application/Specifications/CrossSourceSpecification.cs b/Source/Core/MMCA.Common.Application/Specifications/CrossSourceSpecification.cs index 0051096f..2481b04c 100644 --- a/Source/Core/MMCA.Common.Application/Specifications/CrossSourceSpecification.cs +++ b/Source/Core/MMCA.Common.Application/Specifications/CrossSourceSpecification.cs @@ -81,18 +81,12 @@ private static Expression> BuildCriteria>(body, parameter); } - - private sealed class ParameterReplacer(ParameterExpression from, ParameterExpression to) : ExpressionVisitor - { - protected override Expression VisitParameter(ParameterExpression node) => - node == from ? to : base.VisitParameter(node); - } } diff --git a/Source/Core/MMCA.Common.Domain/MMCA.Common.Domain.csproj b/Source/Core/MMCA.Common.Domain/MMCA.Common.Domain.csproj index 3d86ce0a..aebb3cce 100644 --- a/Source/Core/MMCA.Common.Domain/MMCA.Common.Domain.csproj +++ b/Source/Core/MMCA.Common.Domain/MMCA.Common.Domain.csproj @@ -3,6 +3,14 @@ MMCA.Common.Domain MMCA framework: DDD base entities, aggregate roots, domain events, specifications + + + + + diff --git a/Source/Core/MMCA.Common.Domain/Specifications/ParameterReplacer.cs b/Source/Core/MMCA.Common.Domain/Specifications/ParameterReplacer.cs new file mode 100644 index 00000000..5e715c87 --- /dev/null +++ b/Source/Core/MMCA.Common.Domain/Specifications/ParameterReplacer.cs @@ -0,0 +1,46 @@ +using System.Linq.Expressions; + +namespace MMCA.Common.Domain.Specifications; + +/// +/// Expression visitor that rebinds every occurrence of one onto +/// another, so two independently authored lambdas can be merged into a single lambda body. +/// +/// This is the composition primitive behind , +/// and +/// , and behind the cross-source specification +/// builder in the Application layer. Merging by substitution is deliberate: the obvious alternative, +/// Expression.Invoke(spec.Criteria, parameter), leaves an in +/// the tree, and several LINQ providers (Cosmos among them) refuse to translate one. Substitution +/// produces a tree indistinguishable from a hand-written predicate, so it translates everywhere. +/// +/// +/// +/// Internal by design: it is an implementation detail of specification composition, not part of the +/// framework's public surface. MMCA.Common.Application sees it through +/// InternalsVisibleTo so the cross-source builder shares this one visitor instead of carrying +/// a private copy. +/// +internal sealed class ParameterReplacer(ParameterExpression from, ParameterExpression to) : ExpressionVisitor +{ + /// + /// Returns with every reference to replaced by + /// . + /// + /// The expression body to rewrite. + /// The parameter to replace. + /// The parameter to replace it with. + /// The rewritten expression body. + public static Expression Replace(Expression body, ParameterExpression from, ParameterExpression to) + { + ArgumentNullException.ThrowIfNull(body); + ArgumentNullException.ThrowIfNull(from); + ArgumentNullException.ThrowIfNull(to); + + return ReferenceEquals(from, to) ? body : new ParameterReplacer(from, to).Visit(body); + } + + /// + protected override Expression VisitParameter(ParameterExpression node) => + node == from ? to : base.VisitParameter(node); +} diff --git a/Source/Core/MMCA.Common.Domain/Specifications/QuerySpecification.cs b/Source/Core/MMCA.Common.Domain/Specifications/QuerySpecification.cs new file mode 100644 index 00000000..c53673f3 --- /dev/null +++ b/Source/Core/MMCA.Common.Domain/Specifications/QuerySpecification.cs @@ -0,0 +1,150 @@ +using System.Linq.Expressions; +using MMCA.Common.Domain.Interfaces; + +namespace MMCA.Common.Domain.Specifications; + +/// +/// A that carries the rest of a read's shape +/// alongside its predicate: eager-load paths, ordering, paging, tracking, and whether soft-deleted +/// rows are in scope. A repository can then serve the whole query from one object +/// (ListAsync(spec)) instead of the caller threading five loose arguments through every layer. +/// +/// State is exposed read-only and assembled through the protected builder methods below, which +/// derived specifications call from their constructor: +/// +/// +/// public sealed class RecentOpenTicketsSpecification : QuerySpecification<Ticket, int> +/// { +/// public RecentOpenTicketsSpecification(int take) +/// { +/// AddInclude(nameof(Ticket.Comments)); +/// AddOrderBy(t => t.CreatedOn, descending: true); +/// AddOrderBy(t => t.Id); +/// ApplyPaging(skip: 0, take: take); +/// } +/// +/// public override Expression<Func<Ticket, bool>> Criteria => t => !t.IsClosed; +/// } +/// +/// +/// The base chain is deliberately QuerySpecification -> Specification: the +/// SpecificationsDoNotNavigateToOtherEntities fitness rule keys on that base-type prefix and +/// on a property literally named Criteria, so a query specification is analyzed by exactly +/// the same rule as a plain one. +/// +/// +/// The entity type this specification applies to. +/// The entity's identifier type. +public abstract class QuerySpecification + : Specification + where TEntity : IBaseEntity + where TIdentifierType : notnull +{ + private readonly List _orderBy = []; + private readonly List _includePaths = []; + + /// Initializes a new instance with no includes, ordering, paging, or tracking. + protected QuerySpecification() { } + + /// + /// Gets the ordering keys in application order: the first entry becomes OrderBy / + /// OrderByDescending and every later entry a ThenBy / ThenByDescending. + /// Empty when the specification does not order. + /// + public IReadOnlyList OrderBy => _orderBy; + + /// + /// Gets the navigation paths to eager-load, as dot-separated strings (e.g. "Order.Lines"). + /// Empty when the specification loads no navigations. + /// + public IReadOnlyList IncludePaths => _includePaths; + + /// Gets the number of rows to skip, or when the specification does not page. + public int? Skip { get; private set; } + + /// Gets the maximum number of rows to return, or when the specification does not page. + public int? Take { get; private set; } + + /// + /// Gets whether the results should be tracked by the change tracker. Defaults to + /// : a specification-driven read is a read. + /// + public bool AsTracking { get; private set; } + + /// + /// Gets whether soft-deleted rows are in scope. Defaults to . + /// + /// When the repository drops the named SoftDelete global query + /// filter and only that one: the Tenant filter stays in force, so a specification + /// asking for deleted rows can never reach another tenant's data. + /// + /// + public bool IgnoreQueryFilters { get; private set; } + + /// + /// Adds an ordering key. Call once per key, in the order they should be applied. + /// + /// The key type the selector returns. + /// The key selector (e.g. t => t.CreatedOn). + /// Whether this key sorts descending. + protected void AddOrderBy(Expression> keySelector, bool descending = false) + { + ArgumentNullException.ThrowIfNull(keySelector); + + _orderBy.Add(new OrderExpression(keySelector, descending)); + } + + /// + /// Adds a navigation path to eager-load. Blank paths are ignored; a path already added is not + /// added twice. + /// + /// The dot-separated navigation path (e.g. "Order.Lines"). + protected void AddInclude(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return; + + if (!_includePaths.Contains(path, StringComparer.Ordinal)) + _includePaths.Add(path); + } + + /// + /// Sets the paging window. Both values are floored at zero, so a negative offset or size + /// degrades to "from the start" / "no rows" instead of throwing inside the provider. + /// + /// The number of rows to skip. + /// The maximum number of rows to return. + protected void ApplyPaging(int skip, int take) + { + Skip = Math.Max(skip, 0); + Take = Math.Max(take, 0); + } + + /// + /// Opts the results into change tracking. Use only when the caller mutates and saves what it + /// reads; every other read should stay untracked. + /// + protected void WithTracking() => AsTracking = true; + + /// + /// Brings soft-deleted rows into scope (an admin restore screen, a data-subject export). It + /// drops the named SoftDelete filter and only that one: tenant scoping still applies. + /// + protected void WithSoftDeleted() => IgnoreQueryFilters = true; + +} + +/// +/// One ordering key of a . +/// +/// +/// It is a top-level type rather than a member of the generic specification on purpose: a nested +/// type of a generic class is a different type per closed generic, which would stop the repository +/// evaluator from handling an ordering list generically. +/// +/// +/// The key selector, kept as a so keys of different types can share +/// one list; the evaluator binds it back to its concrete key type by reflection. +/// +/// Whether this key sorts descending. +public sealed record OrderExpression(LambdaExpression KeySelector, bool Descending); diff --git a/Source/Core/MMCA.Common.Domain/Specifications/Specification.cs b/Source/Core/MMCA.Common.Domain/Specifications/Specification.cs index aaa968af..22cfd163 100644 --- a/Source/Core/MMCA.Common.Domain/Specifications/Specification.cs +++ b/Source/Core/MMCA.Common.Domain/Specifications/Specification.cs @@ -53,12 +53,31 @@ public sealed class InlineSpecification(Expression -/// Composes two specifications with a logical AND. Uses Expression.Invoke -/// to embed each specification's expression tree into a new lambda, preserving -/// EF Core translatability for LINQ-to-DB queries. +/// Composes two specifications with a logical AND. /// +/// +/// +/// The two criteria are merged by parameter substitution (see ParameterReplacer): +/// the right-hand body is rebound onto the left-hand lambda's parameter and the two bodies are +/// joined with . The result is a tree +/// indistinguishable from a hand-written predicate. +/// +/// +/// It deliberately does not use Expression.Invoke. An +/// survives into the query tree, and while EF Core's relational providers can usually unwrap it, +/// others (Cosmos in particular) throw at translation time, so an ANDed specification failed on +/// exactly the engines the framework is meant to be portable across. +/// +/// +/// The composed expression is built once per instance and cached: the previous implementation +/// rebuilt the whole tree on every read, and the query pipeline reads it at +/// least once per request. +/// +/// /// The entity type this specification applies to. /// The entity's identifier type. +/// The left-hand specification. +/// The right-hand specification. public sealed class AndSpecification( ISpecification spec1, ISpecification spec2) @@ -66,25 +85,23 @@ public sealed class AndSpecification( where TEntity : IBaseEntity where TIdentifierType : notnull { + private Expression>? _criteria; + /// - public override Expression> Criteria - { - get - { - var parameter = Expression.Parameter(typeof(TEntity), "entity"); - var body = Expression.AndAlso( - Expression.Invoke(spec1.Criteria, parameter), - Expression.Invoke(spec2.Criteria, parameter)); - return Expression.Lambda>(body, parameter); - } - } + public override Expression> Criteria => + _criteria ??= SpecificationComposer.Combine( + spec1, spec2, Expression.AndAlso); } /// -/// Composes two specifications with a logical OR using Expression.Invoke composition. +/// Composes two specifications with a logical OR, merging the two criteria by parameter +/// substitution (never Expression.Invoke) and caching the composed expression per instance. +/// See for why. /// /// The entity type this specification applies to. /// The entity's identifier type. +/// The left-hand specification. +/// The right-hand specification. public sealed class OrSpecification( ISpecification spec1, ISpecification spec2) @@ -92,40 +109,85 @@ public sealed class OrSpecification( where TEntity : IBaseEntity where TIdentifierType : notnull { + private Expression>? _criteria; + /// - public override Expression> Criteria - { - get - { - var parameter = Expression.Parameter(typeof(TEntity), "entity"); - var body = Expression.OrElse( - Expression.Invoke(spec1.Criteria, parameter), - Expression.Invoke(spec2.Criteria, parameter)); - return Expression.Lambda>(body, parameter); - } - } + public override Expression> Criteria => + _criteria ??= SpecificationComposer.Combine( + spec1, spec2, Expression.OrElse); } /// -/// Negates a specification using Expression.Not. +/// Negates a specification with , reusing the inner +/// lambda's own parameter (never Expression.Invoke) and caching the composed expression +/// per instance. See for why. /// /// The entity type this specification applies to. /// The entity's identifier type. +/// The specification to negate. public sealed class NotSpecification( ISpecification spec) : Specification where TEntity : IBaseEntity where TIdentifierType : notnull { + private Expression>? _criteria; + /// - public override Expression> Criteria + public override Expression> Criteria => + _criteria ??= SpecificationComposer.Negate(spec); +} + +/// +/// Shared body of the boolean composers: merges two criteria lambdas into one by rebinding the +/// right-hand parameter onto the left-hand one, so the composed tree contains no +/// . +/// +internal static class SpecificationComposer +{ + /// Joins two specifications' criteria with a binary operator. + /// The entity type the specifications apply to. + /// The entity's identifier type. + /// The left-hand specification. + /// The right-hand specification. + /// The binary operator factory (AndAlso or OrElse). + /// The composed criteria expression. + internal static Expression> Combine( + ISpecification spec1, + ISpecification spec2, + Func combine) + where TEntity : IBaseEntity + where TIdentifierType : notnull { - get - { - var parameter = Expression.Parameter(typeof(TEntity), "entity"); - var body = Expression.Not( - Expression.Invoke(spec.Criteria, parameter)); - return Expression.Lambda>(body, parameter); - } + ArgumentNullException.ThrowIfNull(spec1); + ArgumentNullException.ThrowIfNull(spec2); + + var left = spec1.Criteria; + var right = spec2.Criteria; + var parameter = left.Parameters[0]; + + var body = combine( + left.Body, + ParameterReplacer.Replace(right.Body, right.Parameters[0], parameter)); + + return Expression.Lambda>(body, parameter); + } + + /// Negates a specification's criteria in place, keeping its own parameter. + /// The entity type the specification applies to. + /// The entity's identifier type. + /// The specification to negate. + /// The negated criteria expression. + internal static Expression> Negate( + ISpecification spec) + where TEntity : IBaseEntity + where TIdentifierType : notnull + { + ArgumentNullException.ThrowIfNull(spec); + + var criteria = spec.Criteria; + return Expression.Lambda>( + Expression.Not(criteria.Body), + criteria.Parameters[0]); } } diff --git a/Source/Core/MMCA.Common.Domain/Specifications/SpecificationExtensions.cs b/Source/Core/MMCA.Common.Domain/Specifications/SpecificationExtensions.cs new file mode 100644 index 00000000..216f9b1b --- /dev/null +++ b/Source/Core/MMCA.Common.Domain/Specifications/SpecificationExtensions.cs @@ -0,0 +1,92 @@ +using MMCA.Common.Domain.Interfaces; + +namespace MMCA.Common.Domain.Specifications; + +/// +/// Fluent composition members for . +/// +/// They are thin factories over , +/// and +/// , so a composed predicate reads left to +/// right instead of inside out: +/// +/// +/// // Nested constructors: +/// var spec = new AndSpecification<Order, int>( +/// new OwnedByUserSpecification<Order, int>(userId), +/// new NotSpecification<Order, int>(new CancelledOrderSpecification())); +/// +/// // The same thing, fluently: +/// var spec = new OwnedByUserSpecification<Order, int>(userId) +/// .And(new CancelledOrderSpecification().Not()); +/// +/// +/// Every composer merges the underlying criteria by parameter substitution, so the composed +/// Criteria stays translatable on every provider, and caches the composed expression per +/// instance. Hold on to the composed specification (a field, a local) rather than rebuilding it per +/// request if the composition itself is on a hot path. +/// +/// +public static class SpecificationExtensions +{ + extension(ISpecification specification) + where TEntity : IBaseEntity + where TIdentifierType : notnull + { + /// + /// Returns a specification satisfied only when both this specification and + /// are satisfied. + /// + /// The specification to AND with this one. + /// The composed AND specification. + /// + /// + /// var activeAndMine = new ActiveSpecification() + /// .And(new OwnedByUserSpecification<Ticket, int>(currentUserId)); + /// + /// + public AndSpecification And(ISpecification other) + { + ArgumentNullException.ThrowIfNull(specification); + ArgumentNullException.ThrowIfNull(other); + + return new AndSpecification(specification, other); + } + + /// + /// Returns a specification satisfied when either this specification or + /// is satisfied. + /// + /// The specification to OR with this one. + /// The composed OR specification. + /// + /// + /// var visible = new PublishedSpecification() + /// .Or(new OwnedByUserSpecification<Article, int>(currentUserId)); + /// + /// + public OrSpecification Or(ISpecification other) + { + ArgumentNullException.ThrowIfNull(specification); + ArgumentNullException.ThrowIfNull(other); + + return new OrSpecification(specification, other); + } + + /// + /// Returns a specification satisfied exactly when this one is not. + /// + /// The composed NOT specification. + /// + /// + /// var notArchived = new ArchivedSpecification().Not(); + /// + /// + public NotSpecification Not() + { + ArgumentNullException.ThrowIfNull(specification); + + return new NotSpecification(specification); + } + } +} diff --git a/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/EFReadRepository.cs b/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/EFReadRepository.cs index 8b9df68e..605b1960 100644 --- a/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/EFReadRepository.cs +++ b/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/EFReadRepository.cs @@ -1,8 +1,12 @@ using System.Collections.Concurrent; using System.Linq.Expressions; +using System.Reflection; using Microsoft.EntityFrameworkCore; using MMCA.Common.Application.Interfaces.Infrastructure; using MMCA.Common.Domain.Entities; +using MMCA.Common.Domain.Interfaces; +using MMCA.Common.Domain.Specifications; +using MMCA.Common.Shared.Abstractions; using MMCA.Common.Shared.DTOs; namespace MMCA.Common.Infrastructure.Persistence.Repositories; @@ -207,6 +211,19 @@ public virtual async Task CountAsync( return await Entities.CountAsync(where, cancellationToken).ConfigureAwait(false); } + /// + public virtual async Task CountAsync( + ISpecification specification, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(specification); + + return await SpecificationEvaluator + .Apply(BaseQueryFor(specification), specification, applyShape: false) + .CountAsync(cancellationToken) + .ConfigureAwait(false); + } + /// /// Checks whether an entity with the given ID exists. /// @@ -271,53 +288,175 @@ private async Task AnyAsync( public virtual IQueryable TableNoTrackingSplitQuery => TableNoTracking.AsSplitQuery(); /// - /// Applies string-based eager loading includes to the query. Skips empty/whitespace entries. - /// Mirrors the query pipeline's heuristic: when any include targets a collection navigation, - /// the query opts into split-query mode so sibling collections don't multiply rows - /// (cartesian explosion) under EF's default single-query JOIN strategy. + /// Applies string-based eager loading includes to the query, including the collection-navigation + /// split-query auto-switch. The logic itself lives once in + /// , which the specification path uses as well, so the two + /// entry points can never drift apart. /// + /// The queryable to apply the includes to. + /// The dot-separated navigation paths. + /// The queryable with the includes applied. protected static IQueryable ApplyIncludes( IQueryable query, IEnumerable includes) + => SpecificationEvaluator.ApplyIncludes(query, includes); + + // ── Specification-driven reads ─────────────────────────────────────────────────────────── + + /// + /// Chooses the base queryable a specification runs on: tracked or not, with or without the named + /// soft-delete filter dropped. Only a + /// carries those choices; a plain + /// specification gets the untracked, filtered default. + /// + private IQueryable BaseQueryFor(ISpecification specification) { - ArgumentNullException.ThrowIfNull(query); - ArgumentNullException.ThrowIfNull(includes); + var querySpecification = specification as QuerySpecification; + + var query = querySpecification?.AsTracking == true ? Table : TableNoTracking; + + return querySpecification?.IgnoreQueryFilters == true + ? query.IgnoreQueryFilters(SoftDeleteFilterOnly) + : query; + } + + /// + public virtual async Task> ListAsync( + ISpecification specification, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(specification); + + return await SpecificationEvaluator + .Apply(BaseQueryFor(specification), specification) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); + } + + /// + public virtual async Task> ListAsync( + ISpecification specification, + Expression> select, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(specification); + ArgumentNullException.ThrowIfNull(select); + + // Select last: ordering and paging must run over entity rows, so a paged specification pages + // the rows it means to page and only that page is projected. + return await SpecificationEvaluator + .Apply(BaseQueryFor(specification), specification) + .Select(select) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); + } + + /// + public virtual async Task AnyAsync( + ISpecification specification, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(specification); + + // Criteria only, and through the same Cosmos-aware existence check the predicate overloads use. + return await AnyAsync( + BaseQueryFor(specification), + specification.Criteria, + cancellationToken).ConfigureAwait(false); + } + + /// + public virtual async Task>> GetPageByCursorAsync( + KeysetPageRequest request, + ISpecification? specification = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(request); + + if (!KeysetQueryBuilder.TryResolveSortProperty(request.SortColumn, out var sortProperty)) + { + return Result.Failure>( + Error.InvalidEntityField with + { + Message = $"Sort column '{request.SortColumn}' does not exist on type '{typeof(TEntity).Name}'.", + Source = nameof(GetPageByCursorAsync), + Target = typeof(TEntity).Name, + }); + } - var hasCollectionInclude = false; + var query = specification is null + ? TableNoTracking + : SpecificationEvaluator.Apply(BaseQueryFor(specification), specification, applyShape: false); - foreach (string include in includes.Where(i => !string.IsNullOrWhiteSpace(i))) + if (request.Cursor is not null) { - query = query.Include(include); - hasCollectionInclude = hasCollectionInclude || IsCollectionNavigationPath(include); + if (!TryBuildSeekPredicate(request, sortProperty, out var seek)) + { + return Result.Failure>( + Error.Validation( + "Error.InvalidCursor", + "The supplied pagination cursor isn't valid.", + nameof(GetPageByCursorAsync), + typeof(TEntity).Name)); + } + + query = query.Where(seek); } - return hasCollectionInclude ? query.AsSplitQuery() : query; + query = KeysetQueryBuilder.ApplyOrdering(query, sortProperty, request.Descending); + + // One extra row is the next-page probe: it is never returned, it only says whether a next + // page exists, which is cheaper and more honest than a COUNT over the whole set. + var rows = await query.Take(request.PageSize + 1).ToListAsync(cancellationToken).ConfigureAwait(false); + + var hasMore = rows.Count > request.PageSize; + if (hasMore) + rows.RemoveAt(rows.Count - 1); + + string? nextCursor = null; + if (hasMore && rows.Count != 0) + { + var last = rows[^1]; + nextCursor = KeysetCursor.Encode( + KeysetQueryBuilder.ToInvariantString(sortProperty?.GetValue(last)), + KeysetQueryBuilder.ToInvariantString(last.Id) ?? string.Empty); + } + + return Result.Success(new KeysetCollectionResult(rows, nextCursor)); } /// - /// Caches, per include path, whether any segment of the path is a collection navigation. - /// The reflection walk runs once per distinct path; dispatch afterwards is a dictionary hit. + /// Decodes the request's cursor and turns it into the seek predicate, or reports that the cursor + /// is malformed (bad encoding, wrong version, or values that do not parse as this entity's key + /// and sort types). /// - private static readonly System.Collections.Concurrent.ConcurrentDictionary CollectionIncludeCache = - new(StringComparer.Ordinal); + private static bool TryBuildSeekPredicate( + KeysetPageRequest request, + PropertyInfo? sortProperty, + out Expression> seek) + { + seek = null!; + + if (!KeysetCursor.TryDecode(request.Cursor, out var sortText, out var idText)) + return false; - private static bool IsCollectionNavigationPath(string includePath) - => CollectionIncludeCache.GetOrAdd(includePath, static path => + if (!KeysetQueryBuilder.TryFromInvariantString(typeof(TIdentifierType), idText, out var id) + || id is not TIdentifierType typedId) { - var type = typeof(TEntity); - foreach (var segment in path.Split('.')) - { - var property = type.GetProperty(segment, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); - if (property is null) - return false; // unknown segment: leave the split decision to EF's own include validation + return false; + } - var propertyType = property.PropertyType; - if (propertyType != typeof(string) && typeof(System.Collections.IEnumerable).IsAssignableFrom(propertyType)) - return true; + object? sortValue = null; + if (sortProperty is not null + && sortText is not null + && !KeysetQueryBuilder.TryFromInvariantString(sortProperty.PropertyType, sortText, out sortValue)) + { + return false; + } - type = propertyType; - } + seek = KeysetQueryBuilder.BuildSeekPredicate( + sortProperty, sortValue, typedId, request.Descending); - return false; - }); + return true; + } } diff --git a/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/EFReadRepositoryDecorator.cs b/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/EFReadRepositoryDecorator.cs index 87ae9c6b..e8c5991b 100644 --- a/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/EFReadRepositoryDecorator.cs +++ b/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/EFReadRepositoryDecorator.cs @@ -1,6 +1,8 @@ using System.Linq.Expressions; using MMCA.Common.Application.Interfaces.Infrastructure; using MMCA.Common.Domain.Entities; +using MMCA.Common.Domain.Interfaces; +using MMCA.Common.Shared.Abstractions; using MMCA.Common.Shared.DTOs; namespace MMCA.Common.Infrastructure.Persistence.Repositories; @@ -73,6 +75,12 @@ public Task CountAsync(Expression> where, CancellationT ProfilingHelper.ProfileAsync(ClassName, nameof(CountAsync), () => _inner.CountAsync(where, cancellationToken)); + public Task CountAsync( + ISpecification specification, + CancellationToken cancellationToken = default) => + ProfilingHelper.ProfileAsync(ClassName, nameof(CountAsync), + () => _inner.CountAsync(specification, cancellationToken)); + public Task ExistsAsync(TIdentifierType id, bool ignoreQueryFilters = false, CancellationToken cancellationToken = default) => ProfilingHelper.ProfileAsync(ClassName, nameof(ExistsAsync), () => _inner.ExistsAsync(id, ignoreQueryFilters, cancellationToken)); @@ -81,6 +89,32 @@ public Task ExistsAsync(Expression> where, bool ignore ProfilingHelper.ProfileAsync(ClassName, nameof(ExistsAsync), () => _inner.ExistsAsync(where, ignoreQueryFilters, cancellationToken)); + public Task> ListAsync( + ISpecification specification, + CancellationToken cancellationToken = default) => + ProfilingHelper.ProfileAsync(ClassName, nameof(ListAsync), + () => _inner.ListAsync(specification, cancellationToken)); + + public Task> ListAsync( + ISpecification specification, + Expression> select, + CancellationToken cancellationToken = default) => + ProfilingHelper.ProfileAsync(ClassName, nameof(ListAsync), + () => _inner.ListAsync(specification, select, cancellationToken)); + + public Task AnyAsync( + ISpecification specification, + CancellationToken cancellationToken = default) => + ProfilingHelper.ProfileAsync(ClassName, nameof(AnyAsync), + () => _inner.AnyAsync(specification, cancellationToken)); + + public Task>> GetPageByCursorAsync( + KeysetPageRequest request, + ISpecification? specification = null, + CancellationToken cancellationToken = default) => + ProfilingHelper.ProfileAsync(ClassName, nameof(GetPageByCursorAsync), + () => _inner.GetPageByCursorAsync(request, specification, cancellationToken)); + public IQueryable Table => _inner.Table; public IQueryable TableNoTracking => _inner.TableNoTracking; public IQueryable TableNoTrackingSingleQuery => _inner.TableNoTrackingSingleQuery; diff --git a/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/KeysetQueryBuilder.cs b/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/KeysetQueryBuilder.cs new file mode 100644 index 00000000..ffb91958 --- /dev/null +++ b/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/KeysetQueryBuilder.cs @@ -0,0 +1,275 @@ +using System.ComponentModel; +using System.Globalization; +using System.Linq.Expressions; +using System.Reflection; +using MMCA.Common.Domain.Interfaces; + +namespace MMCA.Common.Infrastructure.Persistence.Repositories; + +/// +/// Builds the ordering and the seek predicate of a keyset ("seek") page. +/// +/// A keyset page is ordered by (sortKey, Id) and fetched with a composite comparison against +/// the last row of the previous page, so the database seeks straight to the boundary instead of +/// counting past every skipped row. The tie-break on Id is what makes the order total: without +/// it two rows sharing a sort value can swap places between pages and be returned twice or never. +/// +/// +/// Exactly one sort key is supported, by design. Multi-key keyset paging needs a comparison whose +/// size grows quadratically with the key count, and every provider translates it differently. +/// +/// +internal static class KeysetQueryBuilder +{ + /// + /// Resolves the sort property named by a keyset request, or when the page + /// is keyed by Id alone. + /// + /// The entity type. + /// The requested sort column, or . + /// The resolved property when the method returns . + /// + /// when names something that is not a + /// public instance property of the entity: the caller turns that into a validation failure. + /// + internal static bool TryResolveSortProperty(string? sortColumn, out PropertyInfo? property) + { + property = null; + + if (string.IsNullOrWhiteSpace(sortColumn)) + return true; + + property = typeof(TEntity).GetProperty( + sortColumn, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + + return property is not null; + } + + /// + /// Orders a queryable by (sortKey, Id), or by Id alone when there is no sort key. + /// The identifier tie-break always ascends, which is what the seek predicate assumes. + /// + /// The entity type. + /// The entity's identifier type. + /// The queryable to order. + /// The sort property, or for id-only ordering. + /// Whether the sort key (or the id, when there is no sort key) descends. + /// The ordered queryable. + internal static IQueryable ApplyOrdering( + IQueryable query, + PropertyInfo? sortProperty, + bool descending) + where TEntity : class, IBaseEntity + where TIdentifierType : notnull + { + var parameter = Expression.Parameter(typeof(TEntity), "e"); + var idSelector = Expression.Lambda(Expression.Property(parameter, nameof(IBaseEntity<>.Id)), parameter); + + if (sortProperty is null) + return SpecificationEvaluator.ApplyOrderingStep(query, idSelector, descending, isFirst: true); + + var sortSelector = Expression.Lambda(Expression.Property(parameter, sortProperty), parameter); + query = SpecificationEvaluator.ApplyOrderingStep(query, sortSelector, descending, isFirst: true); + return SpecificationEvaluator.ApplyOrderingStep(query, idSelector, descending: false, isFirst: false); + } + + /// + /// Builds the seek predicate that selects the rows strictly after the cursor's boundary row. + /// + /// + /// + /// With no sort key the predicate is simply Id > lastId (or < when the page + /// descends). With a sort key it is the classic composite comparison + /// sort > v OR (sort == v AND Id > lastId), reversed to < on the sort half + /// for a descending page while the identifier half always ascends. + /// + /// + /// A boundary row whose sort key is is handled explicitly, because SQL + /// comparisons against NULL are unknown and would silently drop rows. Ascending, nulls sort + /// first, so everything after the boundary is either a later null or any non-null value; that is + /// what the predicate says. Descending, nulls sort last, so the remaining rows are the later + /// nulls only. + /// + /// + /// The entity type. + /// The entity's identifier type. + /// The sort property, or for id-only paging. + /// The boundary row's sort value (already converted), or . + /// The boundary row's identifier. + /// Whether the page descends. + /// The seek predicate. + internal static Expression> BuildSeekPredicate( + PropertyInfo? sortProperty, + object? sortValue, + TIdentifierType lastId, + bool descending) + where TEntity : class, IBaseEntity + where TIdentifierType : notnull + { + var parameter = Expression.Parameter(typeof(TEntity), "e"); + var idAccess = Expression.Property(parameter, nameof(IBaseEntity<>.Id)); + var idConstant = Expression.Constant(lastId, typeof(TIdentifierType)); + + if (sortProperty is null) + { + // Id-only paging: the identifier follows the requested direction, since it IS the sort key. + var idOnly = Compare(idAccess, idConstant, greaterThan: !descending); + return Expression.Lambda>(idOnly, parameter); + } + + var sortAccess = Expression.Property(parameter, sortProperty); + var idTieBreak = Compare(idAccess, idConstant, greaterThan: true); + var isNullable = !sortProperty.PropertyType.IsValueType + || Nullable.GetUnderlyingType(sortProperty.PropertyType) is not null; + + if (sortValue is null) + { + if (!isNullable) + { + // A non-nullable key can never have produced a null boundary; degrade to the id seek. + return Expression.Lambda>(idTieBreak, parameter); + } + + var isNull = Expression.Equal(sortAccess, Expression.Constant(null, sortProperty.PropertyType)); + + Expression nullBoundary = descending + ? Expression.AndAlso(isNull, idTieBreak) + : Expression.OrElse( + Expression.NotEqual(sortAccess, Expression.Constant(null, sortProperty.PropertyType)), + Expression.AndAlso(isNull, idTieBreak)); + + return Expression.Lambda>(nullBoundary, parameter); + } + + var sortConstant = Expression.Constant(sortValue, sortProperty.PropertyType); + Expression body = Expression.OrElse( + Compare(sortAccess, sortConstant, greaterThan: !descending), + Expression.AndAlso(Expression.Equal(sortAccess, sortConstant), idTieBreak)); + + if (isNullable && descending) + { + // Descending puts nulls last, and "null < v" is unknown, so they need saying explicitly. + body = Expression.OrElse( + Expression.Equal(sortAccess, Expression.Constant(null, sortProperty.PropertyType)), + body); + } + + return Expression.Lambda>(body, parameter); + } + + /// + /// Renders a value for a cursor with invariant culture, using the round-trip format for the + /// date and time types so a cursor never loses sub-second precision and re-seeks onto the wrong + /// row. + /// + /// The value to render. + /// The rendered value, or when the value is null. + internal static string? ToInvariantString(object? value) => value switch + { + null => null, + DateTime dateTime => dateTime.ToString("O", CultureInfo.InvariantCulture), + DateTimeOffset dateTimeOffset => dateTimeOffset.ToString("O", CultureInfo.InvariantCulture), + DateOnly dateOnly => dateOnly.ToString("O", CultureInfo.InvariantCulture), + TimeOnly timeOnly => timeOnly.ToString("O", CultureInfo.InvariantCulture), + string text => text, + IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture), + _ => value.ToString() ?? string.Empty, + }; + + /// + /// Parses a cursor segment back into the target type with invariant culture. + /// + /// The type to convert to. + /// The rendered value. + /// The converted value when the method returns . + /// when the segment is not a valid value of that type. + internal static bool TryFromInvariantString(Type targetType, string text, out object? value) + { + value = null; + + var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType; + if (underlying == typeof(string)) + { + value = text; + return true; + } + + var converter = TypeDescriptor.GetConverter(underlying); + if (!converter.CanConvertFrom(typeof(string))) + return false; + + try + { + value = converter.ConvertFromInvariantString(text); + return value is not null; + } + catch (Exception ex) when (ex is FormatException or NotSupportedException or ArgumentException) + { + return false; + } + } + + /// + /// Builds a greater-than / less-than comparison that the providers can translate. + /// + /// + /// only exists for types with the + /// operator, which excludes . Strings therefore compare through + /// , which EF Core translates to a native + /// > / <. Anything else with no operator but an + /// implementation (a key, for instance) compares + /// through CompareTo, whose translation is provider-specific. + /// + private static BinaryExpression Compare(Expression left, Expression right, bool greaterThan) + { + var type = Nullable.GetUnderlyingType(left.Type) ?? left.Type; + + if (type == typeof(string)) + { + var compare = Expression.Call(StringCompareMethod, left, right); + return greaterThan + ? Expression.GreaterThan(compare, ZeroConstant) + : Expression.LessThan(compare, ZeroConstant); + } + + if (SupportsRelationalOperator(type)) + { + return greaterThan + ? Expression.GreaterThan(left, right) + : Expression.LessThan(left, right); + } + + var comparableInterface = typeof(IComparable<>).MakeGenericType(type); + if (!comparableInterface.IsAssignableFrom(type)) + { + throw new NotSupportedException( + $"Keyset paging cannot order by '{left.Type.Name}': the type supports neither a relational operator nor IComparable."); + } + + var compareTo = Expression.Call( + Expression.Convert(left, type), + comparableInterface.GetMethod(nameof(IComparable<>.CompareTo))!, + Expression.Convert(right, type)); + + return greaterThan + ? Expression.GreaterThan(compareTo, ZeroConstant) + : Expression.LessThan(compareTo, ZeroConstant); + } + + private static bool SupportsRelationalOperator(Type type) + => type.IsPrimitive + || type.IsEnum + || type == typeof(decimal) + || type == typeof(DateTime) + || type == typeof(DateTimeOffset) + || type == typeof(DateOnly) + || type == typeof(TimeOnly) + || type == typeof(TimeSpan) + || type.GetMethod("op_GreaterThan", BindingFlags.Public | BindingFlags.Static) is not null; + + private static readonly MethodInfo StringCompareMethod = + typeof(string).GetMethod(nameof(string.Compare), [typeof(string), typeof(string)])!; + + private static readonly ConstantExpression ZeroConstant = Expression.Constant(0); +} diff --git a/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/SpecificationEvaluator.cs b/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/SpecificationEvaluator.cs new file mode 100644 index 00000000..7a1e0688 --- /dev/null +++ b/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/SpecificationEvaluator.cs @@ -0,0 +1,198 @@ +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.EntityFrameworkCore; +using MMCA.Common.Domain.Interfaces; +using MMCA.Common.Domain.Specifications; + +namespace MMCA.Common.Infrastructure.Persistence.Repositories; + +/// +/// Turns a specification into an : criteria always, plus the includes, +/// ordering and paging carried by a +/// . +/// +/// Tracking and soft-delete scope are deliberately NOT applied here: those choose the base queryable +/// (Table vs TableNoTracking, with or without the named soft-delete filter dropped), +/// which only the repository can do. The evaluator composes on top of whatever base it is handed. +/// +/// +internal static class SpecificationEvaluator +{ + /// + /// Applies a specification to a base queryable. + /// + /// The entity type. + /// The entity's identifier type. + /// The base queryable (already scoped for tracking and query filters). + /// The specification to apply. + /// + /// Whether to apply the includes, ordering, and paging of a + /// . Aggregate reads (count, exists) + /// pass : joining in includes to count rows costs a join per navigation, + /// and counting "page 3 of the matches" is never what a caller means. + /// + /// The composed queryable. + internal static IQueryable Apply( + IQueryable source, + ISpecification specification, + bool applyShape = true) + where TEntity : class, IBaseEntity + where TIdentifierType : notnull + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(specification); + + var query = source.Where(specification.Criteria); + + if (!applyShape || specification is not QuerySpecification querySpecification) + return query; + + query = ApplyIncludes(query, querySpecification.IncludePaths); + query = ApplyOrdering(query, querySpecification.OrderBy); + + if (querySpecification.Skip is int skip and > 0) + query = query.Skip(skip); + + if (querySpecification.Take is int take) + query = query.Take(take); + + return query; + } + + /// + /// Applies string-based eager loading includes to the query. Skips empty/whitespace entries. + /// When any include targets a collection navigation the query opts into split-query mode so + /// sibling collections do not multiply rows (cartesian explosion) under EF's default + /// single-query JOIN strategy. + /// + /// + /// This is the single home of that heuristic: EFReadRepository.ApplyIncludes delegates + /// here, so the string-include path and the specification path can never drift apart. + /// + /// The entity type. + /// The queryable to apply the includes to. + /// The dot-separated navigation paths. + /// The queryable with the includes (and split-query mode when warranted) applied. + internal static IQueryable ApplyIncludes( + IQueryable query, + IEnumerable includes) + where TEntity : class + { + ArgumentNullException.ThrowIfNull(query); + ArgumentNullException.ThrowIfNull(includes); + + var hasCollectionInclude = false; + + foreach (string include in includes.Where(i => !string.IsNullOrWhiteSpace(i))) + { + query = query.Include(include); + hasCollectionInclude = hasCollectionInclude || IsCollectionNavigationPath(typeof(TEntity), include); + } + + return hasCollectionInclude ? query.AsSplitQuery() : query; + } + + /// + /// Applies an ordering chain: the first key becomes OrderBy/OrderByDescending and + /// every later key a ThenBy/ThenByDescending. A specification with no ordering + /// leaves the query untouched (the caller's own ordering, if any, survives). + /// + /// The entity type. + /// The queryable to order. + /// The ordering keys, in application order. + /// The ordered queryable. + internal static IQueryable ApplyOrdering( + IQueryable query, + IReadOnlyList orderBy) + where TEntity : class + { + ArgumentNullException.ThrowIfNull(query); + ArgumentNullException.ThrowIfNull(orderBy); + + for (var i = 0; i < orderBy.Count; i++) + { + query = ApplyOrderingStep(query, orderBy[i].KeySelector, orderBy[i].Descending, isFirst: i == 0); + } + + return query; + } + + /// + /// Applies one ordering step, binding the untyped key selector back to its concrete key type. + /// + /// The entity type. + /// The queryable to order. + /// The key selector lambda. + /// Whether this key sorts descending. + /// Whether this is the first key (OrderBy) or a later one (ThenBy). + /// The ordered queryable. + internal static IQueryable ApplyOrderingStep( + IQueryable query, + LambdaExpression keySelector, + bool descending, + bool isFirst) + where TEntity : class + { + ArgumentNullException.ThrowIfNull(query); + ArgumentNullException.ThrowIfNull(keySelector); + + var name = (isFirst, descending) switch + { + (true, false) => nameof(Queryable.OrderBy), + (true, true) => nameof(Queryable.OrderByDescending), + (false, false) => nameof(Queryable.ThenBy), + (false, true) => nameof(Queryable.ThenByDescending), + }; + + var method = OrderingMethods[name].MakeGenericMethod(typeof(TEntity), keySelector.ReturnType); + return (IQueryable)method.Invoke(null, [query, keySelector])!; + } + + /// + /// The four two-argument ordering methods, resolved once. Each call site + /// closes one over (entity type, key type); the reflection lookup itself never repeats. + /// + private static readonly Dictionary OrderingMethods = + new[] + { + nameof(Queryable.OrderBy), + nameof(Queryable.OrderByDescending), + nameof(Queryable.ThenBy), + nameof(Queryable.ThenByDescending), + }.ToDictionary( + name => name, + name => typeof(Queryable) + .GetMethods(BindingFlags.Public | BindingFlags.Static) + .Single(m => string.Equals(m.Name, name, StringComparison.Ordinal) + && m.GetGenericArguments().Length == 2 + && m.GetParameters().Length == 2), + StringComparer.Ordinal); + + /// + /// Caches, per (entity type, include path), whether any segment of the path is a collection + /// navigation. The reflection walk runs once per distinct pair; dispatch afterwards is a + /// dictionary hit. + /// + private static readonly ConcurrentDictionary<(Type EntityType, string Path), bool> CollectionIncludeCache = new(); + + private static bool IsCollectionNavigationPath(Type entityType, string includePath) + => CollectionIncludeCache.GetOrAdd((entityType, includePath), static key => + { + var type = key.EntityType; + foreach (var segment in key.Path.Split('.')) + { + var property = type.GetProperty(segment, BindingFlags.Public | BindingFlags.Instance); + if (property is null) + return false; // unknown segment: leave the split decision to EF's own include validation + + var propertyType = property.PropertyType; + if (propertyType != typeof(string) && typeof(System.Collections.IEnumerable).IsAssignableFrom(propertyType)) + return true; + + type = propertyType; + } + + return false; + }); +} diff --git a/Source/Core/MMCA.Common.Shared/Abstractions/KeysetPagination.cs b/Source/Core/MMCA.Common.Shared/Abstractions/KeysetPagination.cs new file mode 100644 index 00000000..6fc9aef0 --- /dev/null +++ b/Source/Core/MMCA.Common.Shared/Abstractions/KeysetPagination.cs @@ -0,0 +1,215 @@ +using System.Buffers.Text; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Serialization; +using System.Text; + +namespace MMCA.Common.Shared.Abstractions; + +/// +/// A request for one keyset ("seek") page: the rows strictly after a cursor, ordered by a single +/// sort key with the entity's Id as tie-break. +/// +/// Keyset paging exists alongside the offset paging carried by , +/// it does not replace it. Offset paging answers "give me page 37" and costs the database a scan of +/// the 36 pages before it; keyset paging answers "give me what comes after this row" and costs one +/// index seek regardless of depth, at the price of losing random page access and a total count. Use +/// it for deep or infinite scrolling over large tables. +/// +/// +[DataContract] +public sealed record KeysetPageRequest +{ + /// + /// Framework ceiling on a keyset page, mirroring the query pipeline's own unbounded-result + /// ceiling so neither entry point can be talked into an unbounded read. + /// + public const int MaxPageSize = 1000; + + /// Initializes an empty request (page size 1, no sort column, no cursor). + public KeysetPageRequest() + : this(pageSize: 1) { } + + /// Initializes a keyset page request. + /// + /// The number of rows to return. Clamped into [1, ] rather than + /// rejected, so a caller that asks for zero or a million gets a sane page instead of an error. + /// + /// + /// The single entity property to order by, or to order by Id alone. + /// + /// Whether the sort key orders descending. + /// + /// The opaque cursor returned as by the + /// previous page, or for the first page. + /// + public KeysetPageRequest( + int pageSize, + string? sortColumn = null, + bool descending = false, + string? cursor = null) + { + PageSize = Math.Clamp(pageSize, 1, MaxPageSize); + SortColumn = sortColumn; + Descending = descending; + Cursor = cursor; + } + + /// Gets the number of rows to return, always within [1, ]. + [DataMember(Order = 1)] + public int PageSize + { + get; + init => field = Math.Clamp(value, 1, MaxPageSize); + } + + /// Gets the entity property to order by, or to order by Id alone. + [DataMember(Order = 2)] + public string? SortColumn { get; init; } + + /// Gets a value indicating whether the sort key orders descending. + [DataMember(Order = 3)] + public bool Descending { get; init; } + + /// Gets the opaque cursor of the previous page, or for the first page. + [DataMember(Order = 4)] + public string? Cursor { get; init; } +} + +/// +/// A for a keyset page: the rows plus the cursor that fetches the +/// next page. There is deliberately no total count and no page number, because a keyset read never +/// pays for either. +/// +/// The type of items in the page. +[DataContract] +public sealed record KeysetCollectionResult : CollectionResult +{ + /// Initializes an empty keyset page with no next cursor. + [SetsRequiredMembers] + public KeysetCollectionResult() + : this(items: [], nextCursor: null) { } + + /// Initializes a keyset page. + /// The rows on this page. + /// + /// The cursor that fetches the following page, or when this page is the + /// last one (there are no more rows). + /// + [SetsRequiredMembers] + public KeysetCollectionResult(IReadOnlyCollection items, string? nextCursor) + : base(items) => NextCursor = nextCursor; + + /// + /// Gets the cursor that fetches the following page, or when there are no + /// more rows. Treat it as opaque: its encoding is an implementation detail and versioned. + /// + [DataMember(Order = 2)] + public string? NextCursor { get; init; } +} + +/// +/// Encodes and decodes the opaque cursor carried by and +/// . +/// +/// A cursor is the base64url form of v1|{hasSortValue}|{sortValue}|{id}, where the two value +/// segments are themselves base64url so a value containing the separator cannot forge one. The +/// v1 prefix is a format version: a future encoding adds v2 and +/// keeps rejecting what it does not understand, instead of silently +/// mis-seeking. +/// +/// +/// It is not signed or encrypted, so it must never carry anything the caller may not see. The +/// values it does carry (a sort key and an id) are already in the rows the caller just received. +/// +/// +public static class KeysetCursor +{ + private const string Version = "v1"; + private const char Separator = '|'; + + /// + /// Encodes a cursor from the last row of a page. + /// + /// + /// The row's sort-key value rendered with invariant culture, or when the + /// page is keyed by id alone (or the sort key was null on that row). + /// + /// The row's identifier rendered with invariant culture. + /// The opaque cursor string. + public static string Encode(string? sortValue, string id) + { + ArgumentNullException.ThrowIfNull(id); + + var payload = string.Concat( + Version, + Separator, + sortValue is null ? "0" : "1", + Separator, + ToBase64Url(sortValue ?? string.Empty), + Separator, + ToBase64Url(id)); + + return ToBase64Url(payload); + } + + /// + /// Decodes a cursor produced by . + /// + /// The cursor string to decode. + /// + /// When this method returns , the decoded sort-key value, or + /// when the cursor carries none. + /// + /// When this method returns , the decoded identifier. + /// + /// when the cursor is a well-formed v1 cursor; + /// for anything else (wrong version, bad base64, wrong segment count). + /// Callers turn into a validation failure rather than a silent first page. + /// + public static bool TryDecode(string? cursor, out string? sortValue, out string id) + { + sortValue = null; + id = string.Empty; + + if (string.IsNullOrWhiteSpace(cursor) || !TryFromBase64Url(cursor, out var payload)) + return false; + + var parts = payload.Split(Separator); + if (parts.Length != 4 || !string.Equals(parts[0], Version, StringComparison.Ordinal)) + return false; + + if (parts[1] is not ("0" or "1")) + return false; + + if (!TryFromBase64Url(parts[2], out var decodedSortValue) || !TryFromBase64Url(parts[3], out var decodedId)) + return false; + + sortValue = parts[1] == "1" ? decodedSortValue : null; + id = decodedId; + return true; + } + + private static string ToBase64Url(string value) => + Base64Url.EncodeToString(Encoding.UTF8.GetBytes(value)); + + private static bool TryFromBase64Url(string value, out string decoded) + { + decoded = string.Empty; + + if (value.Length == 0) + return true; + + // The validity check comes first because TryDecodeFromChars THROWS on an invalid character + // rather than returning false, and a client-supplied cursor is exactly the input that will + // contain one. + if (!Base64Url.IsValid(value)) + return false; + + var buffer = new byte[Base64Url.GetMaxDecodedLength(value.Length)]; + if (!Base64Url.TryDecodeFromChars(value, buffer, out var written)) + return false; + + decoded = Encoding.UTF8.GetString(buffer, 0, written); + return true; + } +} diff --git a/Tests/Architecture/MMCA.Common.Architecture.Tests/SpecificationFitnessTests.cs b/Tests/Architecture/MMCA.Common.Architecture.Tests/SpecificationFitnessTests.cs index 261d823f..eeefa2bb 100644 --- a/Tests/Architecture/MMCA.Common.Architecture.Tests/SpecificationFitnessTests.cs +++ b/Tests/Architecture/MMCA.Common.Architecture.Tests/SpecificationFitnessTests.cs @@ -23,6 +23,20 @@ public void Rule_FlagsNavigatingSpecification_ButNotScalarSpecification() exception.Message.Should().NotContain(nameof(ScalarOnlySpec), "scalar-only specs are safe across data sources"); } + [Fact] + public void Rule_AlsoAnalyzesQuerySpecifications() + { + var act = () => ArchitectureRules.SpecificationsDoNotNavigateToOtherEntities(new SpecTestMap()); + + var exception = act.Should().Throw().Which; + exception.Message.Should().Contain( + nameof(NavigatingQuerySpec), + "a QuerySpecification keeps the Specification base chain the rule keys on, so it stays analyzable"); + exception.Message.Should().NotContain( + nameof(ScalarOnlyQuerySpec), + "a query specification that filters on its own columns is as safe as any other"); + } + private sealed class SpecTestMap : ArchitectureMapBase { public override string RepoToken => "MMCA.Common"; @@ -56,4 +70,28 @@ private sealed class ScalarOnlySpec : Specification { public override Expression> Criteria => d => d.PrincipalId == 1 && d.Flag; } + + // The same two shapes as query specifications: the rule walks the whole base chain, so + // QuerySpecification -> Specification is still analyzed, includes and ordering notwithstanding. + public sealed class NavigatingQuerySpec : QuerySpecification + { + /// Initializes the navigating query specification. + public NavigatingQuerySpec() + { + AddOrderBy(d => d.PrincipalId); + ApplyPaging(skip: 0, take: 10); + } + + /// + public override Expression> Criteria => d => d.Principal!.IsActive; + } + + public sealed class ScalarOnlyQuerySpec : QuerySpecification + { + /// Initializes the scalar-only query specification. + public ScalarOnlyQuerySpec() => AddInclude(nameof(FitnessDependent.Principal)); + + /// + public override Expression> Criteria => d => d.PrincipalId == 1 && d.Flag; + } } diff --git a/Tests/Core/MMCA.Common.Application.Tests/Notifications/PushNotificationDTOProjectorTests.cs b/Tests/Core/MMCA.Common.Application.Tests/Notifications/PushNotificationDTOProjectorTests.cs new file mode 100644 index 00000000..87d96c4c --- /dev/null +++ b/Tests/Core/MMCA.Common.Application.Tests/Notifications/PushNotificationDTOProjectorTests.cs @@ -0,0 +1,124 @@ +using AwesomeAssertions; +using MMCA.Common.Application.Interfaces; +using MMCA.Common.Application.Notifications.PushNotifications.DTOs; +using MMCA.Common.Domain.Notifications.PushNotifications; +using MMCA.Common.Shared.Notifications.PushNotifications; + +namespace MMCA.Common.Application.Tests.Notifications; + +/// +/// Pins the contract that makes projection pushdown safe to switch on: the projector MUST produce +/// exactly what the instance mapper produces for the same row. The two paths are chosen by +/// configuration (whether a projector is registered), so a divergence would make the response depend +/// on a DI detail. The enum-to-string conversion is the one place the two are written differently: +/// the mapper calls a method, the projection inlines a translatable expression. +/// +public sealed class PushNotificationDTOProjectorTests +{ + private readonly PushNotificationDTOMapper _mapper = new(); + private readonly PushNotificationDTOProjector _sut = new(); + + private static PushNotification Create( + PushNotificationStatus status, + string? scopeKey = null, + int id = 7) + { + var notification = PushNotification.Create( + "Title", + "Body", + sentByUserId: 3, + recipientCount: 12, + scopeKey: scopeKey).Value!; + + typeof(PushNotification).GetProperty(nameof(PushNotification.Id))!.SetValue(notification, id); + + switch (status) + { + case PushNotificationStatus.Sent: + notification.MarkAsSent(); + break; + case PushNotificationStatus.Failed: + notification.MarkAsFailed(); + break; + case PushNotificationStatus.Pending: + default: + break; + } + + return notification; + } + + [Theory] + [InlineData(PushNotificationStatus.Pending)] + [InlineData(PushNotificationStatus.Sent)] + [InlineData(PushNotificationStatus.Failed)] + public void ProjectTo_ProducesExactlyWhatTheMapperProduces(PushNotificationStatus status) + { + var entity = Create(status, scopeKey: "event:2"); + + var projected = _sut.ProjectTo(new[] { entity }.AsQueryable()).Single(); + var mapped = _mapper.MapToDTO(entity); + + projected.Should().BeEquivalentTo(mapped); + } + + [Fact] + public void ProjectTo_MatchesTheMapperForAnUnscopedNotification() + { + var entity = Create(PushNotificationStatus.Pending); + + var projected = _sut.ProjectTo(new[] { entity }.AsQueryable()).Single(); + + projected.Should().BeEquivalentTo(_mapper.MapToDTO(entity)); + projected.ScopeKey.Should().BeNull(); + } + + [Fact] + public void ProjectTo_RendersTheStatusAsItsEnumName() + { + var entity = Create(PushNotificationStatus.Sent); + + _sut.ProjectTo(new[] { entity }.AsQueryable()).Single().Status + .Should().Be(nameof(PushNotificationStatus.Sent)); + } + + [Fact] + public void ProjectTo_MatchesTheMapperOverAWholeCollection() + { + PushNotification[] entities = + [ + Create(PushNotificationStatus.Pending, id: 1), + Create(PushNotificationStatus.Sent, "event:1", id: 2), + Create(PushNotificationStatus.Failed, "event:2", id: 3), + ]; + + var projected = _sut.ProjectTo(entities.AsQueryable()).ToList(); + + projected.Should().BeEquivalentTo(_mapper.MapToDTOs(entities)); + } + + [Fact] + public void ProjectTo_ReturnsAQueryable_NotAMaterializedList() + { + var source = new[] { Create(PushNotificationStatus.Pending) }.AsQueryable(); + + var projected = _sut.ProjectTo(source); + + projected.Expression.ToString().Should().Contain( + "Select", + "the projection must stay composable so the provider translates it"); + } + + [Fact] + public void ProjectTo_WithNullSource_Throws() + { + var act = () => _sut.ProjectTo(null!); + + act.Should().Throw(); + } + + [Fact] + public void TheProjector_IsAnEntityDTOProjector() => + _sut.Should().BeAssignableTo< + IEntityDTOProjector>(); +} diff --git a/Tests/Core/MMCA.Common.Application.Tests/Services/EntityQueryPipelineOrderingTests.cs b/Tests/Core/MMCA.Common.Application.Tests/Services/EntityQueryPipelineOrderingTests.cs new file mode 100644 index 00000000..6c719a41 --- /dev/null +++ b/Tests/Core/MMCA.Common.Application.Tests/Services/EntityQueryPipelineOrderingTests.cs @@ -0,0 +1,140 @@ +using AwesomeAssertions; +using MMCA.Common.Application.Interfaces; +using MMCA.Common.Application.Interfaces.Infrastructure; +using MMCA.Common.Application.Services.Query; +using MMCA.Common.Domain.Entities; + +namespace MMCA.Common.Application.Tests.Services; + +/// +/// Pins the deterministic-ordering guarantee: a PAGINATED read always ends up with a total order, +/// because Skip/Take over a partial order is undefined and lets the same row appear on +/// two consecutive pages while another appears on none. An unpaginated read is deliberately left +/// alone, since it materializes one capped set in one statement and would otherwise pay for a sort +/// nobody asked for. +/// +public sealed class EntityQueryPipelineOrderingTests +{ + private readonly EntityQueryPipeline _sut = new(new InMemoryQueryableExecutor()); + + private sealed class OrderingTestEntity : AuditableBaseEntity + { + public string Name { get; init; } = string.Empty; + } + + /// Four rows sharing two names, deliberately seeded out of identifier order. + private static IQueryable Rows => + new List + { + new() { Id = 3, Name = "b" }, + new() { Id = 1, Name = "b" }, + new() { Id = 4, Name = "a" }, + new() { Id = 2, Name = "a" }, + }.AsQueryable(); + + private static EntityQueryParameters Parameters( + string? sortColumn = null, + string? sortDirection = null, + int? pageNumber = null, + int? pageSize = null) + => new() + { + SortColumn = sortColumn, + SortDirection = sortDirection, + PageNumber = pageNumber, + PageSize = pageSize, + DTOToEntityPropertyMap = new Dictionary(StringComparer.OrdinalIgnoreCase), + }; + + private Task<(IReadOnlyCollection Items, int TotalCount)> ExecuteAsync( + EntityQueryParameters parameters) + => _sut.ExecuteAsync( + Rows, + new NavigationMetadata(), + parameters, + (_, _, _, _, _) => Task.CompletedTask, + CancellationToken.None); + + // ── Paginated reads get a total order ── + [Fact] + public async Task PaginatedRead_WithNoSortColumn_OrdersByIdAscending() + { + var (items, _) = await ExecuteAsync(Parameters(pageNumber: 1, pageSize: 4)); + + items.Select(e => e.Id).Should().Equal(1, 2, 3, 4); + } + + [Fact] + public async Task PaginatedRead_WithANonUniqueSort_AppendsTheIdTieBreak() + { + var (items, _) = await ExecuteAsync(Parameters("Name", "asc", pageNumber: 1, pageSize: 4)); + + items.Select(e => e.Id).Should().Equal( + [2, 4, 1, 3], + "rows sharing a sort value must be ordered by the key, otherwise the page boundary is arbitrary"); + } + + [Fact] + public async Task PaginatedRead_WithADescendingSort_StillTieBreaksAscendingById() + { + var (items, _) = await ExecuteAsync(Parameters("Name", "desc", pageNumber: 1, pageSize: 4)); + + items.Select(e => e.Id).Should().Equal(1, 3, 2, 4); + } + + [Fact] + public async Task ConsecutivePages_ReturnEveryRowExactlyOnce() + { + var (first, _) = await ExecuteAsync(Parameters("Name", "asc", pageNumber: 1, pageSize: 2)); + var (second, _) = await ExecuteAsync(Parameters("Name", "asc", pageNumber: 2, pageSize: 2)); + + var ids = first.Concat(second).Select(e => e.Id).ToList(); + + ids.Should().OnlyHaveUniqueItems().And.HaveCount(4); + } + + [Fact] + public async Task PaginatedRead_SortingByIdItself_DoesNotRepeatTheKey() + { + var (items, _) = await ExecuteAsync(Parameters("Id", "desc", pageNumber: 1, pageSize: 4)); + + items.Select(e => e.Id).Should().Equal([4, 3, 2, 1], "the tie-break must not fight the requested sort"); + } + + // ── Unpaginated reads keep their previous behaviour ── + [Fact] + public async Task UnpaginatedRead_WithNoSortColumn_IsLeftUnordered() + { + var (items, _) = await ExecuteAsync(Parameters()); + + items.Select(e => e.Id).Should().Equal([3, 1, 4, 2], "an unpaginated read must not pay for a sort"); + } + + [Fact] + public async Task UnpaginatedRead_WithASortColumn_SortsByItAlone() + { + var (items, _) = await ExecuteAsync(Parameters("Name", "asc")); + + // Stable LINQ-to-Objects ordering keeps the seeded order inside each name group. + items.Select(e => e.Id).Should().Equal(4, 2, 3, 1); + } + + /// + /// Executes the queryable for real (LINQ to Objects) so the tests observe the ORDER the pipeline + /// actually produced, which a Moq executor returning a canned list cannot show. + /// + private sealed class InMemoryQueryableExecutor : IQueryableExecutor + { + public IQueryable Include(IQueryable query, string navigationPropertyPath) + where T : class => query; + + public IQueryable AsSplitQuery(IQueryable query) + where T : class => query; + + public Task> ToListAsync(IQueryable query, CancellationToken cancellationToken = default) + => Task.FromResult(query.ToList()); + + public Task CountAsync(IQueryable query, CancellationToken cancellationToken = default) + => Task.FromResult(query.Count()); + } +} diff --git a/Tests/Core/MMCA.Common.Application.Tests/Services/EntityQueryServiceProjectionTests.cs b/Tests/Core/MMCA.Common.Application.Tests/Services/EntityQueryServiceProjectionTests.cs new file mode 100644 index 00000000..b65d4ef2 --- /dev/null +++ b/Tests/Core/MMCA.Common.Application.Tests/Services/EntityQueryServiceProjectionTests.cs @@ -0,0 +1,259 @@ +using AwesomeAssertions; +using MMCA.Common.Application.Interfaces; +using MMCA.Common.Application.Interfaces.Infrastructure; +using MMCA.Common.Application.Services; +using MMCA.Common.Application.Services.Query; +using MMCA.Common.Domain.Entities; +using MMCA.Common.Domain.Specifications; +using MMCA.Common.Shared.DTOs; +using Moq; + +namespace MMCA.Common.Application.Tests.Services; + +/// +/// Covers the opt-in projection path of : +/// when a projector is registered the query selects DTO columns and the mapper is never called; when +/// the read cannot be projected (cross-source includes, tracking) the service falls back to the +/// unchanged materialize-then-map path. +/// +public sealed class EntityQueryServiceProjectionTests +{ + public sealed class ProjectedEntity : AuditableBaseEntity + { + public string Name { get; set; } = string.Empty; + + public int Rank { get; set; } + } + + public sealed class ProjectedEntityDTO : IBaseDTO + { + public required int Id { get; init; } + + public string Name { get; set; } = string.Empty; + } + + /// A mapper that records every call, so a test can prove it was bypassed. + private sealed class SpyMapper : IEntityDTOMapper + { + public int MapToDTOCallCount { get; private set; } + + public int MapToDTOsCallCount { get; private set; } + + public ProjectedEntityDTO MapToDTO(ProjectedEntity entity) + { + MapToDTOCallCount++; + return new ProjectedEntityDTO { Id = entity.Id, Name = "mapped:" + entity.Name }; + } + + public IReadOnlyCollection MapToDTOs(IReadOnlyCollection entityCollection) + { + MapToDTOsCallCount++; + return [.. entityCollection.Select(MapToDTO)]; + } + } + + /// A projector whose values are deliberately distinguishable from the mapper's. + private sealed class TestProjector : IEntityDTOProjector + { + public IQueryable ProjectTo(IQueryable source) => + source.Select(e => new ProjectedEntityDTO { Id = e.Id, Name = "projected:" + e.Name }); + } + + private static readonly List Rows = + [ + new() { Id = 2, Name = "b", Rank = 2 }, + new() { Id = 1, Name = "a", Rank = 1 }, + new() { Id = 3, Name = "c", Rank = 3 }, + ]; + + private readonly SpyMapper _mapper = new(); + private readonly Mock _navigationMetadataProvider = new(); + private readonly NavigationMetadata _navigationMetadata = new(); + + private EntityQueryService CreateSut(bool withProjector) + { + var repository = new Mock>(); + repository.SetupGet(r => r.Table).Returns(Rows.AsQueryable()); + repository.SetupGet(r => r.TableNoTracking).Returns(Rows.AsQueryable()); + + var unitOfWork = new Mock(); + unitOfWork.Setup(u => u.GetReadRepository()).Returns(repository.Object); + + _navigationMetadataProvider + .Setup(p => p.BuildIncludes(It.IsAny(), It.IsAny())) + .Returns(_navigationMetadata); + + var pipeline = new EntityQueryPipeline(new InMemoryQueryableExecutor()); + var populator = new Mock>(); + + return withProjector + ? new EntityQueryService( + unitOfWork.Object, _navigationMetadataProvider.Object, pipeline, _mapper, populator.Object, new TestProjector()) + : new EntityQueryService( + unitOfWork.Object, _navigationMetadataProvider.Object, pipeline, _mapper, populator.Object); + } + + private void AddCrossSourceInclude() => + _navigationMetadata.AddUnsupported( + new NavigationPropertyInfo("Elsewhere", NavigationType.ForeignKey, typeof(ProjectedEntity), typeof(ProjectedEntity))); + + // ── Projection path ── + [Fact] + public async Task GetAllAsync_WithAProjector_BypassesTheMapper() + { + var sut = CreateSut(withProjector: true); + + var result = await sut.GetAllAsync(filters: null); + + result.IsSuccess.Should().BeTrue(); + _mapper.MapToDTOsCallCount.Should().Be(0, "the projection path never materializes an entity to map"); + _mapper.MapToDTOCallCount.Should().Be(0); + result.Value!.Items.Cast().Select(d => d.Name) + .Should().BeEquivalentTo("projected:a", "projected:b", "projected:c"); + } + + [Fact] + public async Task GetAllAsync_WithoutAProjector_UsesTheMapper() + { + var sut = CreateSut(withProjector: false); + + var result = await sut.GetAllAsync(filters: null); + + result.IsSuccess.Should().BeTrue(); + _mapper.MapToDTOsCallCount.Should().Be(1); + result.Value!.Items.Cast().Select(d => d.Name) + .Should().BeEquivalentTo("mapped:a", "mapped:b", "mapped:c"); + } + + [Fact] + public async Task GetAllAsync_WithAProjector_StillPagesAndCounts() + { + var sut = CreateSut(withProjector: true); + + var result = await sut.GetAllAsync(pageNumber: 1, pageSize: 2); + + result.Value!.Items.Should().HaveCount(2); + result.Value.PaginationMetadata.TotalItemCount.Should().Be(3); + result.Value.Items.Cast().Select(d => d.Id) + .Should().Equal([1, 2], "a paged projection is still ordered by the key tie-break"); + } + + [Fact] + public async Task GetAllAsync_WithAProjector_StillSortsAndFilters() + { + var sut = CreateSut(withProjector: true); + + var result = await sut.GetAllAsync( + filters: new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["Rank"] = ("GREATER THAN", "1"), + }, + sortColumn: "Rank", + sortDirection: "desc", + pageNumber: 1, + pageSize: 10); + + result.Value!.Items.Cast().Select(d => d.Id).Should().Equal(3, 2); + } + + [Fact] + public async Task GetAllAsync_WithAProjectorAndFields_StillShapesTheProjectedDTOs() + { + var sut = CreateSut(withProjector: true); + + var result = await sut.GetAllAsync(filters: null, fields: "Id"); + + _mapper.MapToDTOsCallCount.Should().Be(0); + result.Value!.Items.Should().AllBeOfType( + "shaping reflects over whatever object the pipeline produced, mapped or projected"); + result.Value.Items.Cast>() + .Should().AllSatisfy(shaped => shaped.Keys.Should().Equal("id")); + } + + // ── Fallback conditions ── + [Fact] + public async Task GetAllAsync_WithCrossSourceIncludes_FallsBackToTheMapper() + { + AddCrossSourceInclude(); + var sut = CreateSut(withProjector: true); + + var result = await sut.GetAllAsync(includeFKs: true, filters: null); + + _mapper.MapToDTOsCallCount.Should().Be( + 1, + "a cross-source navigation is batch-loaded after materialization, which a projection has no rows for"); + result.Value!.Items.Cast().Select(d => d.Name) + .Should().AllSatisfy(name => name.Should().StartWith("mapped:")); + } + + [Fact] + public async Task GetAllAsync_WithTracking_FallsBackToTheMapper() + { + var sut = CreateSut(withProjector: true); + + var result = await sut.GetAllAsync(filters: null, asTracking: true); + + _mapper.MapToDTOsCallCount.Should().Be(1, "a projection produces DTOs, which the change tracker has no use for"); + result.IsSuccess.Should().BeTrue(); + } + + // ── Widened specification parameter ── + [Fact] + public async Task GetAllAsync_AcceptsAnInlineSpecification() + { + var sut = CreateSut(withProjector: true); + + var result = await sut.GetAllAsync( + specification: new InlineSpecification(e => e.Rank >= 2), + filters: null); + + result.Value!.Items.Should().HaveCount(2); + } + + [Fact] + public async Task GetAllAsync_AcceptsAComposedSpecification() + { + var sut = CreateSut(withProjector: true); + + var specification = new InlineSpecification(e => e.Rank >= 2) + .And(new InlineSpecification(e => e.Name == "c")); + + var result = await sut.GetAllAsync(specification: specification, filters: null); + + result.Value!.Items.Cast().Should().ContainSingle().Which.Id.Should().Be(3); + } + + [Fact] + public void Constructor_WithANullProjector_Throws() + { + var unitOfWork = new Mock(); + unitOfWork.Setup(u => u.GetReadRepository()) + .Returns(new Mock>().Object); + + var act = () => new EntityQueryService( + unitOfWork.Object, + Mock.Of(), + Mock.Of(), + _mapper, + Mock.Of>(), + null!); + + act.Should().Throw(); + } + + /// Executes the queryable for real (LINQ to Objects), so projections actually run. + private sealed class InMemoryQueryableExecutor : IQueryableExecutor + { + public IQueryable Include(IQueryable query, string navigationPropertyPath) + where T : class => query; + + public IQueryable AsSplitQuery(IQueryable query) + where T : class => query; + + public Task> ToListAsync(IQueryable query, CancellationToken cancellationToken = default) + => Task.FromResult(query.ToList()); + + public Task CountAsync(IQueryable query, CancellationToken cancellationToken = default) + => Task.FromResult(query.Count()); + } +} diff --git a/Tests/Core/MMCA.Common.Application.Tests/Services/EntityQueryServiceResolutionTests.cs b/Tests/Core/MMCA.Common.Application.Tests/Services/EntityQueryServiceResolutionTests.cs new file mode 100644 index 00000000..73e8d8be --- /dev/null +++ b/Tests/Core/MMCA.Common.Application.Tests/Services/EntityQueryServiceResolutionTests.cs @@ -0,0 +1,100 @@ +using AwesomeAssertions; +using Microsoft.Extensions.DependencyInjection; +using MMCA.Common.Application.Interfaces; +using MMCA.Common.Application.Interfaces.Infrastructure; +using MMCA.Common.Application.Services; +using MMCA.Common.Application.Services.Query; +using MMCA.Common.Domain.Entities; +using MMCA.Common.Shared.DTOs; +using Moq; + +namespace MMCA.Common.Application.Tests.Services; + +/// +/// Proves the two-constructor arrangement actually resolves under +/// Microsoft.Extensions.DependencyInjection: the longer constructor when a projector is +/// registered, the shorter one when it is not. It has no notion of an optional dependency, so a +/// single constructor naming an unregistered service would simply fail to resolve. +/// +public sealed class EntityQueryServiceResolutionTests +{ + public sealed class ResolvedEntity : AuditableBaseEntity + { + public string Name { get; set; } = string.Empty; + } + + public sealed class ResolvedEntityDTO : IBaseDTO + { + public required int Id { get; init; } + } + + private sealed class ResolvedProjector : IEntityDTOProjector + { + public IQueryable ProjectTo(IQueryable source) => + source.Select(e => new ResolvedEntityDTO { Id = e.Id }); + } + + private static ServiceCollection BaseServices() + { + var unitOfWork = new Mock(); + unitOfWork.Setup(u => u.GetReadRepository()) + .Returns(new Mock>().Object); + + var services = new ServiceCollection(); + services.AddSingleton(unitOfWork.Object); + services.AddSingleton(Mock.Of()); + services.AddSingleton(Mock.Of()); + services.AddSingleton(Mock.Of>()); + services.AddSingleton(Mock.Of>()); + services.AddScoped, + EntityQueryService>(); + return services; + } + + [Fact] + public void TheQueryService_ResolvesWithoutAProjector() + { + using var provider = BaseServices().BuildServiceProvider(validateScopes: true); + using var scope = provider.CreateScope(); + + var resolved = scope.ServiceProvider.GetRequiredService>(); + + resolved.Should().NotBeNull(); + } + + [Fact] + public void TheQueryService_ResolvesWithAProjector() + { + var services = BaseServices(); + services.AddScoped, ResolvedProjector>(); + + using var provider = services.BuildServiceProvider(validateScopes: true); + using var scope = provider.CreateScope(); + + var resolved = scope.ServiceProvider.GetRequiredService>(); + + resolved.Should().NotBeNull("the longer constructor is a strict superset, so there is no ambiguity"); + } + + [Fact] + public void ScanModuleApplicationServices_RegistersProjectorsBesideMappers() + { + var services = new ServiceCollection(); + + services.ScanModuleApplicationServices(); + + services.Should().Contain(descriptor => + descriptor.ServiceType == typeof(IEntityDTOProjector)); + } + + /// + /// A public projector in this assembly, so the module scan has something to find. It is separate + /// from the private one above because Scrutor only registers public types. + /// + public sealed class ResolvedProjectorMarker : IEntityDTOProjector + { + /// + public IQueryable ProjectTo(IQueryable source) => + source.Select(e => new ResolvedEntityDTO { Id = e.Id }); + } +} diff --git a/Tests/Core/MMCA.Common.Application.Tests/Services/QueryFieldServiceTieBreakTests.cs b/Tests/Core/MMCA.Common.Application.Tests/Services/QueryFieldServiceTieBreakTests.cs new file mode 100644 index 00000000..097d6a3d --- /dev/null +++ b/Tests/Core/MMCA.Common.Application.Tests/Services/QueryFieldServiceTieBreakTests.cs @@ -0,0 +1,101 @@ +using AwesomeAssertions; +using MMCA.Common.Application.Services; + +namespace MMCA.Common.Application.Tests.Services; + +/// +/// Direct coverage of the tie-break parameter added to QueryFieldService.ApplySorting: it +/// turns a partial order into a total one, which is what makes Skip/Take repeatable. Omitting it +/// must leave the previous behaviour exactly as it was. +/// +public sealed class QueryFieldServiceTieBreakTests +{ + private sealed class SortTestEntity + { + public int Id { get; init; } + + public string Name { get; init; } = string.Empty; + } + + private static readonly IReadOnlyDictionary NoMap = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + private static IQueryable Rows => + new List + { + new() { Id = 3, Name = "b" }, + new() { Id = 1, Name = "b" }, + new() { Id = 4, Name = "a" }, + new() { Id = 2, Name = "a" }, + }.AsQueryable(); + + [Fact] + public void ApplySorting_WithATieBreakAndNoSortColumn_OrdersByTheTieBreak() + { + var sorted = QueryFieldService.ApplySorting(Rows, null, null, NoMap, tieBreakProperty: "Id"); + + sorted.Select(e => e.Id).Should().Equal(1, 2, 3, 4); + } + + [Fact] + public void ApplySorting_WithATieBreak_AppendsItAfterTheRequestedSort() + { + var sorted = QueryFieldService.ApplySorting(Rows, "Name", "asc", NoMap, tieBreakProperty: "Id"); + + sorted.Select(e => e.Id).Should().Equal(2, 4, 1, 3); + } + + [Fact] + public void ApplySorting_WithATieBreakAndADescendingSort_KeepsTheTieBreakAscending() + { + var sorted = QueryFieldService.ApplySorting(Rows, "Name", "desc", NoMap, tieBreakProperty: "Id"); + + sorted.Select(e => e.Id).Should().Equal(1, 3, 2, 4); + } + + [Fact] + public void ApplySorting_WhenTheSortColumnIsTheTieBreak_DoesNotRepeatTheKey() + { + var sorted = QueryFieldService.ApplySorting(Rows, "Id", "desc", NoMap, tieBreakProperty: "Id"); + + sorted.Select(e => e.Id).Should().Equal(4, 3, 2, 1); + } + + [Fact] + public void ApplySorting_WithADefaultSortAndATieBreak_AppliesBoth() + { + var sorted = QueryFieldService.ApplySorting( + Rows, + "NotAColumn", + "asc", + NoMap, + defaultSort: e => e.Name, + tieBreakProperty: "Id"); + + sorted.Select(e => e.Id).Should().Equal(2, 4, 1, 3); + } + + [Fact] + public void ApplySorting_WithoutATieBreak_LeavesAnUnsortedQueryUnsorted() + { + var sorted = QueryFieldService.ApplySorting(Rows, null, null, NoMap); + + sorted.Select(e => e.Id).Should().Equal(3, 1, 4, 2); + } + + [Fact] + public void ApplySorting_WithoutATieBreak_SortsByTheRequestedColumnAlone() + { + var sorted = QueryFieldService.ApplySorting(Rows, "Name", "asc", NoMap); + + sorted.Select(e => e.Id).Should().Equal(4, 2, 3, 1); + } + + [Fact] + public void ApplySorting_IgnoresABlankTieBreak() + { + var sorted = QueryFieldService.ApplySorting(Rows, null, null, NoMap, tieBreakProperty: " "); + + sorted.Select(e => e.Id).Should().Equal(3, 1, 4, 2); + } +} diff --git a/Tests/Core/MMCA.Common.Domain.Tests/Specifications/QuerySpecificationTests.cs b/Tests/Core/MMCA.Common.Domain.Tests/Specifications/QuerySpecificationTests.cs new file mode 100644 index 00000000..cac63eca --- /dev/null +++ b/Tests/Core/MMCA.Common.Domain.Tests/Specifications/QuerySpecificationTests.cs @@ -0,0 +1,145 @@ +using System.Linq.Expressions; +using AwesomeAssertions; +using MMCA.Common.Domain.Entities; +using MMCA.Common.Domain.Interfaces; +using MMCA.Common.Domain.Specifications; + +namespace MMCA.Common.Domain.Tests.Specifications; + +/// +/// Covers the builder state a carries +/// beyond its predicate: includes, ordering, paging, tracking, and soft-delete scope. It also pins +/// the base chain, which the SpecificationsDoNotNavigateToOtherEntities fitness rule keys on. +/// +public sealed class QuerySpecificationTests +{ + private sealed class QueryTestEntity : AuditableBaseEntity + { + public string Name { get; set; } = string.Empty; + + public int Age { get; set; } + } + + private sealed class DefaultsSpecification : QuerySpecification + { + public override Expression> Criteria => e => e.Age > 0; + } + + private sealed class FullyConfiguredSpecification : QuerySpecification + { + public FullyConfiguredSpecification() + { + AddInclude("Owner"); + AddInclude("Owner.Address"); + AddInclude(" "); + AddInclude("Owner"); + AddOrderBy(e => e.Name); + AddOrderBy(e => e.Age, descending: true); + ApplyPaging(skip: 10, take: 25); + WithTracking(); + WithSoftDeleted(); + } + + public override Expression> Criteria => e => e.Name != string.Empty; + } + + private sealed class NegativePagingSpecification : QuerySpecification + { + public NegativePagingSpecification() => ApplyPaging(skip: -5, take: -3); + + public override Expression> Criteria => e => true; + } + + // ── Defaults ── + [Fact] + public void Defaults_AreAnUnorderedUnpagedUntrackedFilteredRead() + { + var spec = new DefaultsSpecification(); + + spec.OrderBy.Should().BeEmpty(); + spec.IncludePaths.Should().BeEmpty(); + spec.Skip.Should().BeNull(); + spec.Take.Should().BeNull(); + spec.AsTracking.Should().BeFalse(); + spec.IgnoreQueryFilters.Should().BeFalse(); + } + + [Fact] + public void QuerySpecification_IsStillASpecification() + { + var spec = new DefaultsSpecification(); + + spec.Should().BeAssignableTo>( + "the fitness rule keys on the Specification base-type prefix"); + spec.Should().BeAssignableTo>(); + spec.IsSatisfiedBy(new QueryTestEntity { Id = 1, Age = 3 }).Should().BeTrue(); + spec.IsSatisfiedBy(new QueryTestEntity { Id = 2, Age = 0 }).Should().BeFalse(); + } + + // ── Builders ── + [Fact] + public void AddInclude_KeepsOrder_IgnoresBlanks_AndDoesNotDuplicate() + { + var spec = new FullyConfiguredSpecification(); + + spec.IncludePaths.Should().Equal("Owner", "Owner.Address"); + } + + [Fact] + public void AddOrderBy_RecordsEachKeyWithItsDirection() + { + var spec = new FullyConfiguredSpecification(); + + spec.OrderBy.Should().HaveCount(2); + spec.OrderBy[0].Descending.Should().BeFalse(); + spec.OrderBy[0].KeySelector.ReturnType.Should().Be(); + spec.OrderBy[1].Descending.Should().BeTrue(); + spec.OrderBy[1].KeySelector.ReturnType.Should().Be(); + } + + [Fact] + public void ApplyPaging_RecordsTheWindow() + { + var spec = new FullyConfiguredSpecification(); + + spec.Skip.Should().Be(10); + spec.Take.Should().Be(25); + } + + [Fact] + public void ApplyPaging_FloorsNegativeValuesAtZero() + { + var spec = new NegativePagingSpecification(); + + spec.Skip.Should().Be(0); + spec.Take.Should().Be(0); + } + + [Fact] + public void WithTrackingAndWithSoftDeleted_FlipTheirFlags() + { + var spec = new FullyConfiguredSpecification(); + + spec.AsTracking.Should().BeTrue(); + spec.IgnoreQueryFilters.Should().BeTrue(); + } + + [Fact] + public void OrderByAndIncludePaths_AreReadOnlyToCallers() + { + var spec = new FullyConfiguredSpecification(); + + spec.IncludePaths.Should().BeAssignableTo>(); + spec.OrderBy.Should().BeAssignableTo>(); + } + + // ── Composition still works on a query specification ── + [Fact] + public void AQuerySpecification_ComposesLikeAnyOtherSpecification() + { + var spec = new DefaultsSpecification().And(new DefaultsSpecification().Not()); + + spec.IsSatisfiedBy(new QueryTestEntity { Id = 1, Age = 3 }).Should().BeFalse( + "a predicate ANDed with its own negation matches nothing"); + } +} diff --git a/Tests/Core/MMCA.Common.Domain.Tests/Specifications/SpecificationCompositionTests.cs b/Tests/Core/MMCA.Common.Domain.Tests/Specifications/SpecificationCompositionTests.cs new file mode 100644 index 00000000..482d767e --- /dev/null +++ b/Tests/Core/MMCA.Common.Domain.Tests/Specifications/SpecificationCompositionTests.cs @@ -0,0 +1,314 @@ +using System.Linq.Expressions; +using AwesomeAssertions; +using MMCA.Common.Domain.Entities; +using MMCA.Common.Domain.Specifications; + +namespace MMCA.Common.Domain.Tests.Specifications; + +/// +/// Pins HOW the boolean composers build their criteria, not just what the composed predicate +/// answers. Two properties matter and neither is visible from IsSatisfiedBy: +/// +/// the composed tree contains no , because a provider that +/// cannot unwrap one (Cosmos) throws at translation time on an ANDed specification; +/// the composed tree is built once per instance, because the query pipeline reads +/// Criteria on every request and the old implementation rebuilt it every time. +/// +/// +public sealed class SpecificationCompositionTests +{ + private sealed class CompositionTestEntity : AuditableBaseEntity + { + public string Name { get; set; } = string.Empty; + + public int Age { get; set; } + } + + private sealed class NameStartsWithSpecification(string prefix) : Specification + { + public override Expression> Criteria => + e => e.Name.StartsWith(prefix, StringComparison.Ordinal); + } + + private sealed class AgeGreaterThanSpecification(int threshold) : Specification + { + public override Expression> Criteria => + e => e.Age > threshold; + } + + private static readonly CompositionTestEntity Alice = new() { Id = 1, Name = "Alice", Age = 25 }; + + // ── No Expression.Invoke survives composition ── + [Fact] + public void AndSpecification_Criteria_ContainsNoInvocation() + { + var spec = new AndSpecification( + new NameStartsWithSpecification("A"), + new AgeGreaterThanSpecification(18)); + + InvocationFinder.Count(spec.Criteria).Should().Be( + 0, + "an InvocationExpression in the criteria is what a non-relational provider refuses to translate"); + } + + [Fact] + public void OrSpecification_Criteria_ContainsNoInvocation() + { + var spec = new OrSpecification( + new NameStartsWithSpecification("A"), + new AgeGreaterThanSpecification(18)); + + InvocationFinder.Count(spec.Criteria).Should().Be(0); + } + + [Fact] + public void NotSpecification_Criteria_ContainsNoInvocation() + { + var spec = new NotSpecification(new NameStartsWithSpecification("A")); + + InvocationFinder.Count(spec.Criteria).Should().Be(0); + } + + [Fact] + public void NestedComposition_Criteria_ContainsNoInvocation() + { + var spec = new AndSpecification( + new OrSpecification( + new NameStartsWithSpecification("A"), + new AgeGreaterThanSpecification(30)), + new NotSpecification(new NameStartsWithSpecification("Z"))); + + InvocationFinder.Count(spec.Criteria).Should().Be(0); + } + + // ── One parameter, and it is the lambda's own ── + [Fact] + public void AndSpecification_Criteria_UsesASingleParameterForBothSides() + { + var spec = new AndSpecification( + new NameStartsWithSpecification("A"), + new AgeGreaterThanSpecification(18)); + + var criteria = spec.Criteria; + var parameters = ParameterFinder.Distinct(criteria.Body); + + criteria.Parameters.Should().ContainSingle(); + parameters.Should().ContainSingle("the right-hand body must be rebound onto the left-hand parameter"); + parameters.Should().Contain(criteria.Parameters[0]); + } + + // ── Caching ── + [Fact] + public void AndSpecification_Criteria_IsBuiltOncePerInstance() + { + var spec = new AndSpecification( + new NameStartsWithSpecification("A"), + new AgeGreaterThanSpecification(18)); + + spec.Criteria.Should().BeSameAs(spec.Criteria); + } + + [Fact] + public void OrSpecification_Criteria_IsBuiltOncePerInstance() + { + var spec = new OrSpecification( + new NameStartsWithSpecification("A"), + new AgeGreaterThanSpecification(18)); + + spec.Criteria.Should().BeSameAs(spec.Criteria); + } + + [Fact] + public void NotSpecification_Criteria_IsBuiltOncePerInstance() + { + var spec = new NotSpecification(new NameStartsWithSpecification("A")); + + spec.Criteria.Should().BeSameAs(spec.Criteria); + } + + [Fact] + public void SeparateInstances_DoNotShareTheirComposedCriteria() + { + var first = new AndSpecification( + new NameStartsWithSpecification("A"), + new AgeGreaterThanSpecification(18)); + var second = new AndSpecification( + new NameStartsWithSpecification("A"), + new AgeGreaterThanSpecification(18)); + + first.Criteria.Should().NotBeSameAs(second.Criteria); + } + + // ── Semantics still hold, evaluated through the compiled tree ── + [Theory] + [InlineData("A", 18, true)] + [InlineData("A", 30, false)] + [InlineData("B", 18, false)] + public void AndSpecification_MatchesBothSides(string prefix, int threshold, bool expected) + { + var spec = new AndSpecification( + new NameStartsWithSpecification(prefix), + new AgeGreaterThanSpecification(threshold)); + + spec.Criteria.Compile()(Alice).Should().Be(expected); + spec.IsSatisfiedBy(Alice).Should().Be(expected); + } + + [Theory] + [InlineData("B", 18, true)] + [InlineData("A", 30, true)] + [InlineData("B", 30, false)] + public void OrSpecification_MatchesEitherSide(string prefix, int threshold, bool expected) + { + var spec = new OrSpecification( + new NameStartsWithSpecification(prefix), + new AgeGreaterThanSpecification(threshold)); + + spec.Criteria.Compile()(Alice).Should().Be(expected); + } + + [Theory] + [InlineData("A", false)] + [InlineData("B", true)] + public void NotSpecification_NegatesTheInnerCriteria(string prefix, bool expected) + { + var spec = new NotSpecification(new NameStartsWithSpecification(prefix)); + + spec.Criteria.Compile()(Alice).Should().Be(expected); + } + + [Fact] + public void Composition_AppliesAgainstAQueryable() + { + var rows = new List + { + new() { Id = 1, Name = "Alice", Age = 25 }, + new() { Id = 2, Name = "Bob", Age = 40 }, + new() { Id = 3, Name = "Anna", Age = 12 }, + }.AsQueryable(); + + var spec = new AndSpecification( + new NameStartsWithSpecification("A"), + new AgeGreaterThanSpecification(18)); + + rows.Where(spec.Criteria).Select(e => e.Id).Should().Equal(1); + } + + // ── Null guards ── + [Fact] + public void AndSpecification_WithNullLeftSide_ThrowsWhenComposed() + { + var spec = new AndSpecification(null!, new AgeGreaterThanSpecification(1)); + + var act = () => spec.Criteria; + + act.Should().Throw(); + } + + [Fact] + public void NotSpecification_WithNullInnerSpecification_ThrowsWhenComposed() + { + var spec = new NotSpecification(null!); + + var act = () => spec.Criteria; + + act.Should().Throw(); + } + + // ── Fluent extensions ── + [Fact] + public void And_ReturnsAnAndSpecificationWithTheSameSemantics() + { + var spec = new NameStartsWithSpecification("A").And(new AgeGreaterThanSpecification(18)); + + spec.Should().BeOfType>(); + spec.IsSatisfiedBy(Alice).Should().BeTrue(); + InvocationFinder.Count(spec.Criteria).Should().Be(0); + } + + [Fact] + public void Or_ReturnsAnOrSpecificationWithTheSameSemantics() + { + var spec = new NameStartsWithSpecification("Z").Or(new AgeGreaterThanSpecification(18)); + + spec.Should().BeOfType>(); + spec.IsSatisfiedBy(Alice).Should().BeTrue(); + } + + [Fact] + public void Not_ReturnsANotSpecificationWithTheSameSemantics() + { + var spec = new NameStartsWithSpecification("A").Not(); + + spec.Should().BeOfType>(); + spec.IsSatisfiedBy(Alice).Should().BeFalse(); + } + + [Fact] + public void FluentChain_ComposesLeftToRight() + { + var spec = new NameStartsWithSpecification("A") + .And(new AgeGreaterThanSpecification(30).Not()); + + spec.IsSatisfiedBy(Alice).Should().BeTrue("Alice starts with A and is not over 30"); + InvocationFinder.Count(spec.Criteria).Should().Be(0); + } + + [Fact] + public void And_WithNullOther_Throws() + { + var spec = new NameStartsWithSpecification("A"); + + var act = () => spec.And(null!); + + act.Should().Throw(); + } + + [Fact] + public void Or_WithNullOther_Throws() + { + var spec = new NameStartsWithSpecification("A"); + + var act = () => spec.Or(null!); + + act.Should().Throw(); + } + + /// Counts the nodes in an expression tree. + private sealed class InvocationFinder : ExpressionVisitor + { + private int _count; + + public static int Count(Expression expression) + { + var finder = new InvocationFinder(); + finder.Visit(expression); + return finder._count; + } + + protected override Expression VisitInvocation(InvocationExpression node) + { + _count++; + return base.VisitInvocation(node); + } + } + + /// Collects the distinct parameter instances referenced by an expression tree. + private sealed class ParameterFinder : ExpressionVisitor + { + private readonly HashSet _parameters = []; + + public static HashSet Distinct(Expression expression) + { + var finder = new ParameterFinder(); + finder.Visit(expression); + return finder._parameters; + } + + protected override Expression VisitParameter(ParameterExpression node) + { + _parameters.Add(node); + return base.VisitParameter(node); + } + } +} diff --git a/Tests/Core/MMCA.Common.Infrastructure.Redis.Tests/packages.lock.json b/Tests/Core/MMCA.Common.Infrastructure.Redis.Tests/packages.lock.json index d39e8558..56ab78e8 100644 --- a/Tests/Core/MMCA.Common.Infrastructure.Redis.Tests/packages.lock.json +++ b/Tests/Core/MMCA.Common.Infrastructure.Redis.Tests/packages.lock.json @@ -16,9 +16,9 @@ }, "Meziantou.Analyzer": { "type": "Direct", - "requested": "[3.0.141, )", - "resolved": "3.0.141", - "contentHash": "p0mtUYG/36FEnU6P5GovB82mVH4DoWGdHYjePkLnIeTNI0LQfmvjGj2twQEBTosw6/YTTnk8Fknu2VvaFybECA==" + "requested": "[3.0.163, )", + "resolved": "3.0.163", + "contentHash": "l9iZ+OXBGlAFEKH1y4BSKPXHcmpJmikysSm0brjTau8fR7Qm7IVGLVig+dh0RH9mq4s3neZayuPQrHRweOpxxw==" }, "Microsoft.Extensions.Caching.StackExchangeRedis": { "type": "Direct", @@ -40,9 +40,9 @@ }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.16.0, )", - "resolved": "4.16.0", - "contentHash": "/EZ1HVILd9jPdXquT03vBawvAuGNotmf+r/GBiVfPj9BdUyn/olOdjF9BqqjIK5Cyq3pdBDL/qMuJEGtBLMaZA==" + "requested": "[4.16.1, )", + "resolved": "4.16.1", + "contentHash": "AGzq3UZvIwTGSh9xyIna+Q9F666S8UaLlX/Pk3Iz21qIAWjL8iYporVCcX1k/oi5chrRZ8VJRahup4me4HZptw==" }, "SonarAnalyzer.CSharp": { "type": "Direct", @@ -80,11 +80,11 @@ }, "xunit.v3": { "type": "Direct", - "requested": "[3.2.2, )", - "resolved": "3.2.2", - "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==", + "requested": "[4.0.0, )", + "resolved": "4.0.0", + "contentHash": "czH4MaZ2k2eLjetuN5W1fUEnNVADsTOeYxDvrqIFh04XO6H3Y8Yu1FrSy3psF1q06DZV33wftjqZVv4acwIp9Q==", "dependencies": { - "xunit.v3.mtp-v1": "[3.2.2]" + "xunit.v3.mtp-v2": "[4.0.0]" } }, "Azure.Core": { @@ -177,8 +177,8 @@ }, "Microsoft.Azure.Cosmos": { "type": "Transitive", - "resolved": "3.51.0", - "contentHash": "g3dBhncM1rpEJ2ZVnZ/oHYIg0rrH6RlYa8mmuqk9WFR6dfyWoTF+nwT2SoQUy4df6lB7lW61M41xjdVioIqQ5w==", + "resolved": "3.61.0", + "contentHash": "o0lhN2nrkC2xidy0lhjnojwqgp/N6GOXAr34xQrU5Scs8MmNG9cwG6MtRJSVTWQYW3gm9Ava3pmVbuuTVQyuYg==", "dependencies": { "Azure.Core": "1.44.1", "Microsoft.Bcl.AsyncInterfaces": "6.0.0", @@ -191,11 +191,6 @@ "resolved": "10.0.3", "contentHash": "TV62UsrJZPX6gbt3c4WrtXh7bmaDIcMqf9uft1cc4L6gJXOU07hDGEh+bFQh/L2Az0R1WVOkiT66lFqS6G2NmA==" }, - "Microsoft.Bcl.Cryptography": { - "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "YgZYAWzyNuPVtPq6WNm0bqOWNjYaWgl5mBWTGZyNoXitYBUYSp6iUB9AwK0V1mo793qRJUXz2t6UZrWITZSvuQ==" - }, "Microsoft.Bcl.HashCode": { "type": "Transitive", "resolved": "1.1.0", @@ -213,166 +208,166 @@ }, "Microsoft.Data.Sqlite.Core": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "TPCs0ldm7AWqcKmp6f/Xr+14sat7hx4rHfRlS4RgCURBH2thEWbAKEyX7cCWr63zVJVOJIJZTg2cBiUXa8ys6g==", + "resolved": "10.0.11", + "contentHash": "hubA20AGenQ4Sx0ElWaPpB8DISjXpdx463+1zOGRslsT0e/t/06ITv+pHsop8CcJ0d8PZLfgnT7juCDVD79Dkw==", "dependencies": { - "SQLitePCLRaw.core": "2.1.11" + "SQLitePCLRaw.core": "2.1.12" } }, "Microsoft.EntityFrameworkCore": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "a0V7zj/VbYP6dTdWpUgE/r2PuLKtUGe2aJ0lVKkn/wP9ZhaxUz2kQydVfvOjCv2SKxlrqdBfHhPD4Cvlf+4ffA==", + "resolved": "10.0.11", + "contentHash": "VOSGU8en6HZJs8t7UMFN+9vGcRgVOOn6fA44Ngcg2NyvJ3P1KE94iAb0XzaVaGhXGtt+qaM/VtEn0/hzluQJeg==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "10.0.10", - "Microsoft.EntityFrameworkCore.Analyzers": "10.0.10", - "Microsoft.Extensions.Caching.Memory": "10.0.10", - "Microsoft.Extensions.Logging": "10.0.10" + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.11", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.11", + "Microsoft.Extensions.Caching.Memory": "10.0.11", + "Microsoft.Extensions.Logging": "10.0.11" } }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "bOzrFCl6uZCjaSh2bG1ToRQRdx+iXvxosCg9hFyG9OWeAzOFI4xev9OqKeWfKf/kAHyox2JnbcvLVf2ceA7sqA==" + "resolved": "10.0.11", + "contentHash": "6auJR+9+9VunznKfH7WGrHMrnrmA0F7JZ22EXzwXvVhjfnbu9Xq7NSIWaOf3KJsOanM2qf5ajJ2JR5TlcPZTLA==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "2gLDordUCGf3aNOOuqtTbP5mxhiP9nk6TnvGiE3RnqT891O+Zf/qKu1PIREubs1M16A0SImr4vULBfU5BTDs1Q==" + "resolved": "10.0.11", + "contentHash": "Bv7X4wSSnzCQED9WYXKJ8fwgyvKwf0xZM1GO8xkf6CF9zl+UBnvjxmcPnokJRy0JKjc1SlHSzzhx1HcL4jitTQ==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "wNonj40aZxia+GtuBiiD6ZqVh4h6y5Nje1bGdmzZ8/ui0QRsAN+S0SIrLHFCEGbG9cDbeaE40sh+Lr7o9rRs6g==", + "resolved": "10.0.11", + "contentHash": "grznnTJgEYxaWpdKAsTzg6j+89jHgCXWYp+QGtlX5O92+w/VuhWM6JLPYb+uw8M9VhGUvOTsO76dYOy9vNPd5Q==", "dependencies": { - "Microsoft.EntityFrameworkCore": "10.0.10", - "Microsoft.Extensions.Caching.Memory": "10.0.10", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", - "Microsoft.Extensions.Logging": "10.0.10" + "Microsoft.EntityFrameworkCore": "10.0.11", + "Microsoft.Extensions.Caching.Memory": "10.0.11", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.Logging": "10.0.11" } }, "Microsoft.EntityFrameworkCore.Sqlite.Core": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "YbVWMIouzwTKBiLms8boa7xeRT88wI14R1msv3XExFk9n0/sa8nU7MwDa1CKtfLGMJs7O7QWuS9/xhcQ72AD2A==", + "resolved": "10.0.11", + "contentHash": "ywTQKt32xnVhCzjEQAqFufpEyXkOUfvW/EC/s4xnS8Xaor2xXE+TMUyzhgACqXtZEU5IR95y94RDzHto55Fx7w==", "dependencies": { - "Microsoft.Data.Sqlite.Core": "10.0.10", - "Microsoft.EntityFrameworkCore.Relational": "10.0.10", - "Microsoft.Extensions.Caching.Memory": "10.0.10", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", - "Microsoft.Extensions.DependencyModel": "10.0.10", - "Microsoft.Extensions.Logging": "10.0.10", - "SQLitePCLRaw.core": "2.1.11" + "Microsoft.Data.Sqlite.Core": "10.0.11", + "Microsoft.EntityFrameworkCore.Relational": "10.0.11", + "Microsoft.Extensions.Caching.Memory": "10.0.11", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.DependencyModel": "10.0.11", + "Microsoft.Extensions.Logging": "10.0.11", + "SQLitePCLRaw.core": "2.1.12" } }, "Microsoft.Extensions.AmbientMetadata.Application": { "type": "Transitive", - "resolved": "10.8.0", - "contentHash": "/FYCIyhRLrDwIg29tB/21/Rp7XMaEP/gWR5Cxyi4GgWcN/v/XXUpU52yhQux0s2SQy1Z0C1wnhptCMvLaaT45Q==", + "resolved": "10.9.0", + "contentHash": "BX45SeAjP6sz9Djg6Gm0/IruBuWkyUKxtr4pn9sLAuprnuRn+VBPZVH2XxpzbaT7cVCPim3+Dkijl6yhLHjBOw==", "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.10", - "Microsoft.Extensions.Hosting.Abstractions": "10.0.10", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.11", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.11" } }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "4ZFBNE+jzR+CrWWlhOesnmywCW7pYKT0dxyAQRdL11yJwxe4jvcAu31eorFtEkoFeCDcUTeNssgPv2yaRRptaQ==", + "resolved": "10.0.11", + "contentHash": "vUl798SmruTqqlt/xH2gDk3tJlhk6k3HdOXAHirlRfbNKDym4g/kRpUL9S4sl6F6FsOTOMW+ZsDapqlZMOOiEw==", "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.10" + "Microsoft.Extensions.Primitives": "10.0.11" } }, "Microsoft.Extensions.Caching.Memory": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "N1w5H7uK6gCTnCBZAWzE0/EQYSPysij/uYwDqntqBVvBa6bjMmBKitsnEFd6yh/SX3wLm67nO6+OnZ84K+gZWg==", + "resolved": "10.0.11", + "contentHash": "el1g0mBEbDBGY2bT9mcSfrTWO8QlPdq2nOCnvQugioOFwHV+bVBMeiakoI0dNOdj8d6Hi9K6HY2xzRUWJiDR3w==", "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "10.0.10", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", - "Microsoft.Extensions.Logging.Abstractions": "10.0.10", - "Microsoft.Extensions.Options": "10.0.10", - "Microsoft.Extensions.Primitives": "10.0.10" + "Microsoft.Extensions.Caching.Abstractions": "10.0.11", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" } }, "Microsoft.Extensions.Compliance.Abstractions": { "type": "Transitive", - "resolved": "10.8.0", - "contentHash": "7I0DXMTCqpXpXg+SLLDlfedtv6DN5Lgiv+PR3MclzqKPOHNB17YP42KMNI61ckSrffh0xsuIqQyCIjvWc6vUuw==", + "resolved": "10.9.0", + "contentHash": "tuSqNuiJxlln43sZ8c1EDA4WXit1eX4foGadylXso3DnMVc+DtKfaNEwvHuiFXfPsEUZ6Z3GnF0Bfk9vvOsE4Q==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", - "Microsoft.Extensions.ObjectPool": "10.0.10" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.ObjectPool": "10.0.11" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "plJWK2zpWuuyxI8F8s2scx6Je7N1Ajjs6HvYUGKwRnDMWIVIz9FHwAkiT7ASgrvAOd10T0FPVlh9BzAJJME+jg==", + "resolved": "10.0.11", + "contentHash": "wlhRqZW8LcJPa+vk2oLAc/REXDItHtkFQdf/QcXYGZbZOO13izcsKY1pCvuFQYwUiZD+hwSZwsKASjqT+BNaVg==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", - "Microsoft.Extensions.Primitives": "10.0.10" + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "GqmN2o1CkJvk7uWp+p4CwBYW0w/zfoEbvsiFDbO2G8l1Uz+mrDAbAcZiXhU2lufKPby1cjAUdd5GTWpebYOkOA==", + "resolved": "10.0.11", + "contentHash": "rFn8RuszZn3qquPVkDytMUlPc2+rXl9MCoygwc1XmAgC5vg5/oXJ8hkOosOrLoBLsqdTy4lFwP6iQdPS9uSYOA==", "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.10", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.10" + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "ANyvsgkNBRvcJh2XLgn8veGmajf+8m0AbKK+HPWdRL1yraSNVVSmQhFntLtdz/C795jxqqup+k05cs/3jZQPOA==", + "resolved": "10.0.11", + "contentHash": "PSmotV19c7E3lKed++uYo1kSiXFI+uTl37CBSrhq+CfLC3FCHjG7R91+xPnNehQfHS1b0Tzo/CCLPWH3qaEheg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "z/2xXlFw2aLGjHyEm6E0tQ+In6VfzQzTrtArbQ2c0TQE16ZbyDCMGPvaUT9I0s8rgy9sRWlU2P9waW37qV04qA==" + "resolved": "10.0.11", + "contentHash": "/a1aJz4m7ylhEDf25ugQChLQoN5XwoGjWw/BoR/ZWWKsO1v4DdJElS1uyngahz4B/eOzjFk1KNTkarRLE5wsIg==" }, "Microsoft.Extensions.DependencyInjection.AutoActivation": { "type": "Transitive", - "resolved": "10.8.0", - "contentHash": "LXwOAsnrflu2ln+Bxj7y3Zc/Pn4HquCCRzW81oOIuRsM8F75TxwA/ZpSFZ5E1+YkVpndz7BcHY4ThFSjjA4vbQ==", + "resolved": "10.9.0", + "contentHash": "hO/IpudHBl+EMXm1Ag8+7ersCN7VuiR6ech1+cVk8I+o7ssPRKdTozIAO4HGPt49G/lY6lo7G2kY5Q00biMHew==", "dependencies": { - "Microsoft.Extensions.Hosting.Abstractions": "10.0.10" + "Microsoft.Extensions.Hosting.Abstractions": "10.0.11" } }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "rfZA1RjR021RPqSmIPovfz2aOd79TGqJ9BengbjnzIISOVwjLmuSDnhCMmiY/1c6iYvGolQ1iNGzkav0u11XEA==" + "resolved": "10.0.11", + "contentHash": "PJPtFYsZ+r+uz9qqXWUTEyKeJ1EiBGIJtqavkg9ZXijjGSFAk4Fgi5sqIxj+uAyLZwEKgexDUQXhWhvU6l3+og==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "Kr/e7lUf4+N8tacbqJ2Ctwe/HarKdAc9ZkgKVVqvtJDBKbez+T/KnUwu82KSlnBp/SrpBcxc7u7xkE2oUZT/5Q==", + "resolved": "10.0.11", + "contentHash": "HT70uGPxMLqqnOzKMcnQtDmeV4r0KHr4qVCLhP7SXil9jMEm8sQXwcybxVVFGXZJ1V44xV0mLqQ54aZbcR2OiQ==", "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.10", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.11", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.11" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "9uWiKpeOVac355STyChWR/pliFX/5CeLqChW9kKsaxyDH4EUTZxMkT4Jwp/J/peLm0GBFmSX5c0WCse3yCnq1Q==", + "resolved": "10.0.11", + "contentHash": "se7Kx8QpJEt+nf26L4qIVAofGTDr1wbexxsh/Fm3Xc04xUkqUXK06KUS7FLwSQYSjqb7q9n+T7MEcXYBhI1Y5g==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", - "Microsoft.Extensions.Options": "10.0.10" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11" } }, "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { "type": "Transitive", - "resolved": "10.8.0", - "contentHash": "tXmzwQeWgiycaLOtvSFnz0TPkJxXTyZrkwSVPTr83FgSiMj3BD/8eIbvrpGi5rzc2AgfKyegKo1ntJlJFey1Zg==", + "resolved": "10.9.0", + "contentHash": "Cyp36W6/XHD6sSDnbc6Ss7M+qOcjrer400Dx4Tfkb5OHcKV6bp9uMqZ4Y8PAoSwTfS8a/3lB9xCF+CLf0JGUig==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11" } }, "Microsoft.Extensions.Diagnostics.HealthChecks": { @@ -393,141 +388,150 @@ }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "c5zqFCY9DiIpMovLd7/d/CTiEtrMOuQ639dhv3PABtKQIKNQikSHwQt8+N679uii9q+B55lgK28Uv64FOwEu8w==", + "resolved": "10.0.11", + "contentHash": "JOjac6SQQgZmdmB8WGEw61/7siqMZoWJMkmq2p1goJGxqI59lO6oB4bOl0jNsbaPBdYy5Mlkb+6U7T4+CjnD8Q==", "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.10" + "Microsoft.Extensions.Primitives": "10.0.11" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "5LugpYGHk+mkn0a8IZgcyfBca8PCTAU9RQFoMrTdtOOidq88M2SI5f3px6ugnzgxC+eTkvYYJi8pzlUnG5xdAQ==", + "resolved": "10.0.11", + "contentHash": "pwtpF7iF/NNaOBcX+pvMZ7y2+JAVbH5KkNrH9uMZtuVxVJsFTDiWiCR7Tk3HVptsAijaetipUZVRVK2LLq+nvA==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", - "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.10", - "Microsoft.Extensions.FileProviders.Abstractions": "10.0.10", - "Microsoft.Extensions.Logging.Abstractions": "10.0.10" + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.11", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11" } }, "Microsoft.Extensions.Http.Diagnostics": { "type": "Transitive", - "resolved": "10.8.0", - "contentHash": "2pYnolFkigH7N6A5WHdGx93iZ8dIhrBnu4ZkjvB6RwbHzYmIy/iXVe12pdrViNDdeBSmKZ5FA9YP/xhwLT7q7A==", + "resolved": "10.9.0", + "contentHash": "jxxGX3bUe0ZZFtkMaigIJG44aGK6toE0EFhQwPYfTGLHw4EgXgceO8+RAdXZakK4BezpeDhR//6cxqmtae0I9Q==", "dependencies": { - "Microsoft.Extensions.Http": "10.0.10", - "Microsoft.Extensions.Telemetry": "10.8.0" + "Microsoft.Extensions.Http": "10.0.11", + "Microsoft.Extensions.Telemetry": "10.9.0" } }, "Microsoft.Extensions.Logging": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "Tf6z5HsL0VDYRTfvsoNrTGHGheCwkTsZBA2FFh5ATJUbkAwug+FFNISJK2gjpUNemlAOoWllAK52HOWCjto3EQ==", + "resolved": "10.0.11", + "contentHash": "nUOJwgFkSiLHiVGFpU22pIJtuWYewuSYQ3JVuP/gdK8ASMT807Px+TYQiRWs6uSsOmoyFTaVCwKXTasczV6BpA==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "10.0.10", - "Microsoft.Extensions.Logging.Abstractions": "10.0.10", - "Microsoft.Extensions.Options": "10.0.10" + "Microsoft.Extensions.DependencyInjection": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "zkFxGYUvdxAvIKTyXHrmW+Sux53D4SezD9dMyZ6hrwwzPQJNuwCRy1f5W7AvYTqacEGhWF2XderRQG1OvbV8og==", + "resolved": "10.0.11", + "contentHash": "Ljd0Uxoq5XpScD2Bg0nM/r3mwx7Ao5Uq24eo2ARxbGvqJ7Zht6rt2cJtwVRH4Cv+1ZVMdXz6TB43KbpmsxRrvQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11" } }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "cLrqxkuEfcilZ8SjK+9KAnpLk9lOoMPaOokF+wRUYie+iUEcdX4/p/+gJkt0BYgWLthjpBUCkVTBI6Kxg0nsOw==", + "resolved": "10.0.11", + "contentHash": "S7LvLeVHKNPaY2NMyxW7c2TBGsLgxoSUBCV5Ev5iN8kgC7EPR2UB7eW7vHsElGMcIUDwRmoxLfvGDynCn3q6EA==", "dependencies": { - "Microsoft.Extensions.Configuration": "10.0.10", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", - "Microsoft.Extensions.Configuration.Binder": "10.0.10", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", - "Microsoft.Extensions.Logging": "10.0.10", - "Microsoft.Extensions.Logging.Abstractions": "10.0.10", - "Microsoft.Extensions.Options": "10.0.10", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10" + "Microsoft.Extensions.Configuration": "10.0.11", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.Configuration.Binder": "10.0.11", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Logging": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.11" } }, "Microsoft.Extensions.ObjectPool": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "Tx0R6PY9MNSh0mDKSxbpHLuuec93IxF1cel+1ithQnMNr/vbfLSSHOQHGHIppAJCGfPcEo/wO1Ylj+QpXtzo3A==" + "resolved": "10.0.11", + "contentHash": "p76ztQFROBOlHgdV1vXfmTjRyu073Av7ZlsiLR93ka6+nzkLCeV2ONXq0DO/BGf71REqGW29Uy/20fzQHAjB7Q==" }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "srnhnk7nE8krBiIXp71LvBmKBtraBONWSRzdjJgRv1Ko9Mp8IVNqv4vIS9hGeVteBig8aQkva9ZG+sC+o5sVcA==", + "resolved": "10.0.11", + "contentHash": "eY1GAKcTfD2maP27J84X9IovT3yjHJ2dVDzPmDg6/XqYvt3jMzJhtfQCLjG9pVsZGAd+8DQ2QrjaDcs2+VQLGw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", - "Microsoft.Extensions.Primitives": "10.0.10" + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "tnBmu/LwF25ZQK+HBNCu2xrwnkKoB/XEbJyooGGoYxHrhvxbSKi7eOFiJ4AXBy/QU4vtCvCJfoi8k9Ej72qzOQ==", + "resolved": "10.0.11", + "contentHash": "syEhXQ/sEaSBFaqzlp9gDGHX/nk6gkQkh1sIUpBO1mlBj3Phu1rmb4ML1uCiyPW9N6Kxfxv3y5FGObC+bV01Qw==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", - "Microsoft.Extensions.Configuration.Binder": "10.0.10", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", - "Microsoft.Extensions.Options": "10.0.10", - "Microsoft.Extensions.Primitives": "10.0.10" + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.Configuration.Binder": "10.0.11", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11", + "Microsoft.Extensions.Primitives": "10.0.11" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "10.0.10", - "contentHash": "5wu/GrYVd8mG2DVUw3vFJzF+O336TyTGg/Kmcgw9bfwYhCoFiV5lR5QeEmKecJyrW4W54nMfD3p3589E8a7czQ==" + "resolved": "10.0.11", + "contentHash": "SXcz+kF+4Oo9b1+55zntpJFYfwb1jw66ioxptyNOOTDc8g2FHnBFWjZpsWfCvZIhzr0x+4e2trVTs4OKwQfBtw==" }, "Microsoft.Extensions.Resilience": { "type": "Transitive", - "resolved": "10.8.0", - "contentHash": "RH/gv0ZcCnQ3d30lxb8+T2o661TwZq/K5eMwp488goQLX1QYqWN3i8L8XlN9DhrqP2Zcf0EtsWaKpJO3yHDUTw==", + "resolved": "10.9.0", + "contentHash": "LXMTKZGNzs3fsjKytTVeIEdp+yssJon7PW3VbxERZeqALA2Za/AQzt2h6K64PZ2NzgfFnX4rCJp6aTe+ocX83A==", "dependencies": { - "Microsoft.Extensions.Diagnostics": "10.0.10", - "Microsoft.Extensions.Diagnostics.ExceptionSummarization": "10.8.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.10", - "Microsoft.Extensions.Telemetry.Abstractions": "10.8.0", + "Microsoft.Extensions.Diagnostics": "10.0.11", + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": "10.9.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.11", + "Microsoft.Extensions.Telemetry.Abstractions": "10.9.0", "Polly.Extensions": "8.4.2", "Polly.RateLimiting": "8.4.2" } }, "Microsoft.Extensions.Telemetry": { "type": "Transitive", - "resolved": "10.8.0", - "contentHash": "IIWjDZguUJt6yNld9J4V88xuOUzpC76wBwfnRJLRmacmyTTl9jA8+KP5PuHPi7CKz/Vk/q+lQYmobiopuzapzA==", + "resolved": "10.9.0", + "contentHash": "RpV3VzPa1YoHqvE2Q4uOuVjioVJhLOhaz4FXIE3XAEvla4qujnM+wFB7QnTAmIJxlg7lbBslkIHJQzpI8yHDQQ==", "dependencies": { - "Microsoft.Extensions.AmbientMetadata.Application": "10.8.0", - "Microsoft.Extensions.DependencyInjection.AutoActivation": "10.8.0", - "Microsoft.Extensions.Logging.Configuration": "10.0.10", - "Microsoft.Extensions.ObjectPool": "10.0.10", - "Microsoft.Extensions.Telemetry.Abstractions": "10.8.0" + "Microsoft.Extensions.AmbientMetadata.Application": "10.9.0", + "Microsoft.Extensions.DependencyInjection.AutoActivation": "10.9.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.11", + "Microsoft.Extensions.ObjectPool": "10.0.11", + "Microsoft.Extensions.Telemetry.Abstractions": "10.9.0" } }, "Microsoft.Extensions.Telemetry.Abstractions": { "type": "Transitive", - "resolved": "10.8.0", - "contentHash": "XHlOerNKjVrPJqbuFi196tyFhS7eNMoPUC800xJmCGsSiruE/goGoKyBKzS1HeelAGnoFPFrS+LgNitXNeEYRQ==", + "resolved": "10.9.0", + "contentHash": "rCyM64XvBNi2IY/5noMVsJjt22zVmnI+8tGrb6JnPRW/75Nyv9Hi+7gkxb7zQcR0+2mzS1sAqPXz2PsdHZCqHg==", "dependencies": { - "Microsoft.Extensions.Compliance.Abstractions": "10.8.0", - "Microsoft.Extensions.Logging.Abstractions": "10.0.10", - "Microsoft.Extensions.ObjectPool": "10.0.10", - "Microsoft.Extensions.Options": "10.0.10" + "Microsoft.Extensions.Compliance.Abstractions": "10.9.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11", + "Microsoft.Extensions.ObjectPool": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11" } }, "Microsoft.Identity.Client": { "type": "Transitive", - "resolved": "4.83.1", - "contentHash": "jOLIrZ3cynoqHLLO1cXplFFabrhrMEYs/EuKHvmCyrOm1axqiVFT6nCSnHxk7w5+d2BeQfCdM12Yf/0X7OeS1g==", + "resolved": "4.84.2", + "contentHash": "Va9FjmABSgj/lcUfHH0pBNQkmS7SNyepz2ERV7Yynp6QgEYpFdSqR6Vzy1WWipN8GS5pBHuXoZbqaDWuARCrHQ==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "8.14.0" } }, + "Microsoft.Identity.Client.Broker": { + "type": "Transitive", + "resolved": "4.84.2", + "contentHash": "928ySLeGz1xtvydwq6h9tv7TbxrtA4ZzavLKUqRJh0PW6BGcEJwpI8W8vf2PlQdYemGm/oyGjdzYzdY/I8r/4A==", + "dependencies": { + "Microsoft.Identity.Client": "4.84.2", + "Microsoft.Identity.Client.NativeInterop": "0.20.6" + } + }, "Microsoft.Identity.Client.Extensions.Msal": { "type": "Transitive", "resolved": "4.83.1", @@ -537,6 +541,11 @@ "System.Security.Cryptography.ProtectedData": "4.5.0" } }, + "Microsoft.Identity.Client.NativeInterop": { + "type": "Transitive", + "resolved": "0.20.6", + "contentHash": "noyfdMfVxyWpM6xUDmlW6gUvZ5ci24z7Qoy15JvynTQfZV5vTD2NEdsnT5Gy4ngViwgdIp5iRjRy10jAKXFAgw==" + }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", "resolved": "8.14.0", @@ -595,32 +604,32 @@ }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", - "resolved": "1.9.1", - "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", + "resolved": "2.3.3", + "contentHash": "nY8ceQyPWB9TRE1WE5Oe/sks2e10SxgPv61vBHsFYpgCeDtNwhpMZwmL29GklO3/etTdOsLdrCmNr4zJWaR2fg==", "dependencies": { "Microsoft.ApplicationInsights": "2.23.0", - "Microsoft.Testing.Platform": "1.9.1" + "Microsoft.Testing.Platform": "2.3.3" } }, "Microsoft.Testing.Extensions.TrxReport.Abstractions": { "type": "Transitive", - "resolved": "1.9.1", - "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", + "resolved": "2.3.3", + "contentHash": "dceVNxnfTEjKnrIreLcMfkgrzzBY8kgCCJX5NAv7Lq/6vPlTP4VB5zxKeuYy0yfCoBtWuPkLjUG6qtOBMfpypA==", "dependencies": { - "Microsoft.Testing.Platform": "1.9.1" + "Microsoft.Testing.Platform": "2.3.3" } }, "Microsoft.Testing.Platform": { "type": "Transitive", - "resolved": "1.9.1", - "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" + "resolved": "2.3.3", + "contentHash": "ENbH4BQh9riXtOKc25KKITfiGGWhWMBJA7pZuNaF9zxzzSaVkp5AMeZxGQ6sXYaR6xb724NLz985Is6XIYqLSg==" }, "Microsoft.Testing.Platform.MSBuild": { "type": "Transitive", - "resolved": "1.9.1", - "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", + "resolved": "2.3.3", + "contentHash": "iVAvNbZ5JPDZSrTcIrCDoCsAkzjDxwDEt2mDuMd8ng1P6e2P2NCd0g0BsoBVG+nJLSmK//V+yeEvjCa3E2fxHw==", "dependencies": { - "Microsoft.Testing.Platform": "1.9.1" + "Microsoft.Testing.Platform": "2.3.3" } }, "Microsoft.Win32.Registry": { @@ -646,19 +655,14 @@ }, "Newtonsoft.Json": { "type": "Transitive", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + "resolved": "13.0.4", + "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" }, "Pipelines.Sockets.Unofficial": { "type": "Transitive", "resolved": "2.2.16", "contentHash": "nonu9l0YrZH6krVYbskBjE7uG0VMAnvOQ9PU+RmzUsy+++vHkqzzCkHRoP877OiA3iRTxJyNDLfpcU8hrJ3/YQ==" }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" - }, "Polly.Extensions": { "type": "Transitive", "resolved": "8.4.2", @@ -735,17 +739,17 @@ }, "System.Configuration.ConfigurationManager": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "dvjqKp+2LpGid6phzrdrS/2mmEPxFl3jE1+L7614q4ZChKbLJCpHXg6sBILlCCED1t//EE+un/UdAetzIMpqnw==", + "resolved": "9.0.11", + "contentHash": "vPinosRgEk1CGjohbv0pDuz6gPprkWo5xvBTjOJjks5JOgOEWvPndq5NngGxiTcBcy1+k9GwzZ1c3Bp2fU5ezw==", "dependencies": { - "System.Diagnostics.EventLog": "9.0.4", - "System.Security.Cryptography.ProtectedData": "9.0.4" + "System.Diagnostics.EventLog": "9.0.11", + "System.Security.Cryptography.ProtectedData": "9.0.11" } }, "System.Diagnostics.EventLog": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "getRQEXD8idlpb1KW56XuxImMy0FKp2WJPDf3Qr0kI/QKxxJSftqfDFVo0DZ3HCJRLU73qHSruv5q2l5O47jQQ==" + "resolved": "9.0.11", + "contentHash": "2Us/NchH6SM69NYWzf8NRyeftdv3ILso8LMiMdAjT7ECTiZKzddiXWhAyQj6ZzbddoAOHS9GdlPbAAkwCPID3Q==" }, "System.IO.Hashing": { "type": "Transitive", @@ -757,15 +761,20 @@ "resolved": "10.0.3", "contentHash": "MaGhRfGunmrj/nHjtsi9XkhlYJ/ERGWrbA+BiSKNtGnAjc9XlG5EhAvak6VRcX5LYzPF6pBO8nJ613dTgzabig==" }, + "System.Security.AccessControl": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "IQ4NXP/B3Ayzvw0rDQzVTYsCKyy0Jp9KI6aYcK7UnGVlR9+Awz++TIPCQtPYfLJfOpm8ajowMR09V7quD3sEHw==" + }, "System.Security.Cryptography.Pkcs": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "cUFTcMlz/Qw9s90b2wnWSCvHdjv51Bau9FQqhsr4TlwSe1OX+7SoXUqphis5G74MLOvMOCghxPPlEqOdCrVVGA==" + "resolved": "9.0.11", + "contentHash": "YS2YqtN6fFjlTDIQI+ucjbbrEvwMn796r+VLTQRr/5Oy6g+i+m7nIou83KnJCnAcKna2I+5eVJRks6SoHSpetQ==" }, "System.Security.Cryptography.ProtectedData": { "type": "Transitive", - "resolved": "9.0.4", - "contentHash": "o94k2RKuAce3GeDMlUvIXlhVa1kWpJw95E6C9LwW0KlG0nj5+SgCiIxJ2Eroqb9sLtG1mEMbFttZIBZ13EJPvQ==" + "resolved": "9.0.11", + "contentHash": "s8yUYuYYu+PAwvBdhLG1KyrGrk9gkYeuPxfAsXsTqqWyepwSyEw8hAaflW4nO98NG52YpYI1am2+9o+79h2RtQ==" }, "System.Threading.RateLimiting": { "type": "Transitive", @@ -786,61 +795,62 @@ }, "xunit.analyzers": { "type": "Transitive", - "resolved": "1.27.0", - "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==" + "resolved": "2.0.0", + "contentHash": "2UtauxWDa9C6bT7MvFfZkNoFulfflb00jnDU2xeVO9Y58l4Ah2Mv/HiMs4b0zdpK/SfAxpajkgKNMN8zBKa+7Q==" }, "xunit.v3.assert": { "type": "Transitive", - "resolved": "3.2.2", - "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==" + "resolved": "4.0.0", + "contentHash": "QxYfC+98lCMe7Kl9iWDUeUn+gmiPJ2Sz/r+trSdp1mvKyDMXj8S1kYdfkFNJSHyUSQ6KWomenSDgfWhGpR8zEQ==" }, "xunit.v3.common": { "type": "Transitive", - "resolved": "3.2.2", - "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==", + "resolved": "4.0.0", + "contentHash": "cjaNGmOVA5QJxcp6uuSsDhbt0lNmxezFo226mbYOuXm9E9Owq8bYTKxa9wnAMZRnbvyCmCYhAvc/+UAzf0M2vA==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "6.0.0" } }, - "xunit.v3.core.mtp-v1": { + "xunit.v3.core.mtp-v2": { "type": "Transitive", - "resolved": "3.2.2", - "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==", + "resolved": "4.0.0", + "contentHash": "2I7apws+6HPz5aYi4choAn7c8P4jPAvwbfAtLSy0JPH89oeY/z1Zqz65Cm3HJmput5l56Z1sJOinTxsAQmbyWQ==", "dependencies": { - "Microsoft.Testing.Extensions.Telemetry": "1.9.1", - "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", - "Microsoft.Testing.Platform": "1.9.1", - "Microsoft.Testing.Platform.MSBuild": "1.9.1", - "xunit.v3.extensibility.core": "[3.2.2]", - "xunit.v3.runner.inproc.console": "[3.2.2]" + "Microsoft.Testing.Extensions.Telemetry": "2.3.3", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "2.3.3", + "Microsoft.Testing.Platform": "2.3.3", + "Microsoft.Testing.Platform.MSBuild": "2.3.3", + "xunit.v3.extensibility.core": "[4.0.0]", + "xunit.v3.runner.inproc.console": "[4.0.0]" } }, - "xunit.v3.mtp-v1": { + "xunit.v3.mtp-v2": { "type": "Transitive", - "resolved": "3.2.2", - "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==", + "resolved": "4.0.0", + "contentHash": "svYjct2c3VbLyUSB2r9VWiIkYwemwXHhOgIVAor8RvupcZBFcdiGdmq8G519ZCu30yjPCr0EP3Hy4mvgEXcXpQ==", "dependencies": { - "xunit.analyzers": "1.27.0", - "xunit.v3.assert": "[3.2.2]", - "xunit.v3.core.mtp-v1": "[3.2.2]" + "xunit.analyzers": "2.0.0", + "xunit.v3.assert": "[4.0.0]", + "xunit.v3.core.mtp-v2": "[4.0.0]" } }, "xunit.v3.runner.common": { "type": "Transitive", - "resolved": "3.2.2", - "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==", + "resolved": "4.0.0", + "contentHash": "1IEIAVRgnPo9nihd9D0TvxxsLKVRySa+K0wLy/m0SJ4RMdveRCUj/mICFboruO+ILUJ10fUfCCyp7MC5/y7cGw==", "dependencies": { "Microsoft.Win32.Registry": "[5.0.0]", - "xunit.v3.common": "[3.2.2]" + "System.Security.AccessControl": "[6.0.1]", + "xunit.v3.common": "[4.0.0]" } }, "xunit.v3.runner.inproc.console": { "type": "Transitive", - "resolved": "3.2.2", - "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==", + "resolved": "4.0.0", + "contentHash": "Sp5AALIlZUf2U5pEt2LHs+ebn18eAPSFnXEouJTltLez5p+NQvBm7kzsKSkZOM6iyoS6nuN/wKdsJt2LvixjsQ==", "dependencies": { - "xunit.v3.extensibility.core": "[3.2.2]", - "xunit.v3.runner.common": "[3.2.2]" + "xunit.v3.extensibility.core": "[4.0.0]", + "xunit.v3.runner.common": "[4.0.0]" } }, "mmca.common.application": { @@ -871,14 +881,15 @@ "MassTransit.Azure.ServiceBus.Core": "[8.5.10, )", "MassTransit.RabbitMQ": "[8.5.10, )", "MessagePack": "[2.5.302, )", - "Microsoft.AspNetCore.SignalR.StackExchangeRedis": "[10.0.10, )", + "Microsoft.AspNetCore.SignalR.StackExchangeRedis": "[10.0.11, )", "Microsoft.Azure.NotificationHubs": "[4.2.0, )", - "Microsoft.EntityFrameworkCore.Cosmos": "[10.0.10, )", - "Microsoft.EntityFrameworkCore.SqlServer": "[10.0.10, )", - "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.10, )", - "Microsoft.Extensions.Caching.Hybrid": "[10.8.0, )", - "Microsoft.Extensions.Http.Resilience": "[10.8.0, )", + "Microsoft.EntityFrameworkCore.Cosmos": "[10.0.11, )", + "Microsoft.EntityFrameworkCore.SqlServer": "[10.0.11, )", + "Microsoft.EntityFrameworkCore.Sqlite": "[10.0.11, )", + "Microsoft.Extensions.Caching.Hybrid": "[10.9.0, )", + "Microsoft.Extensions.Http.Resilience": "[10.9.0, )", "MiniProfiler.EntityFrameworkCore": "[4.5.4, )", + "Polly.Core": "[8.7.0, )", "SQLitePCLRaw.bundle_e_sqlite3": "[3.0.5, )", "SixLabors.ImageSharp": "[3.1.12, )", "StackExchange.Redis": "[2.13.17, )" @@ -969,12 +980,12 @@ }, "Microsoft.AspNetCore.SignalR.StackExchangeRedis": { "type": "CentralTransitive", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "mC/dQrt0Etdc9B1hdNy7KDO/E43FfhzHgMajw6Lupryp8XL7B7/3aEN3Zqfagz7LKTbDZadfMvT+XG8flLkKJA==", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "bo7fagrKzIkwBTnzYXwPSqkV/JLALw22QYSwVnizsrHkiV7txWmIrsWxm8Qe3uRTZbxnykBUOnnC3ZavGtlqRg==", "dependencies": { "MessagePack": "2.5.302", - "Microsoft.Extensions.Options": "10.0.10", + "Microsoft.Extensions.Options": "10.0.11", "StackExchange.Redis": "2.7.27" } }, @@ -991,106 +1002,108 @@ "Microsoft.Data.SqlClient": { "type": "CentralTransitive", "requested": "[6.1.6, )", - "resolved": "6.1.1", - "contentHash": "syGQmIUPAYYHAHyTD8FCkTNThpQWvoA7crnIQRMfp8dyB5A2cWU3fQexlRTFkVmV7S0TjVmthi0LJEFVjHo8AQ==", + "resolved": "6.1.6", + "contentHash": "6abRPXrjjEWxcNkTomBIzBdGolYnwD0ykP+6YAynaEEfKlGe94ZSQd58pggucusp28xUA7wLo9XcvzU92ZuF6Q==", "dependencies": { - "Azure.Core": "1.47.1", - "Azure.Identity": "1.14.2", - "Microsoft.Bcl.Cryptography": "9.0.4", + "Azure.Core": "1.50.0", + "Azure.Identity": "1.17.1", "Microsoft.Data.SqlClient.SNI.runtime": "6.0.2", - "Microsoft.Extensions.Caching.Memory": "9.0.4", + "Microsoft.Extensions.Caching.Memory": "9.0.11", + "Microsoft.Identity.Client": "4.84.2", + "Microsoft.Identity.Client.Broker": "4.84.2", "Microsoft.IdentityModel.JsonWebTokens": "7.7.1", "Microsoft.IdentityModel.Protocols.OpenIdConnect": "7.7.1", "Microsoft.SqlServer.Server": "1.0.0", - "System.Configuration.ConfigurationManager": "9.0.4", - "System.Security.Cryptography.Pkcs": "9.0.4" + "System.Configuration.ConfigurationManager": "9.0.11", + "System.IdentityModel.Tokens.Jwt": "7.7.1", + "System.Security.Cryptography.Pkcs": "9.0.11" } }, "Microsoft.EntityFrameworkCore.Cosmos": { "type": "CentralTransitive", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "Phk1cEJb+laEkrttqj+bO0h0O5/IGUB/CZ5gvox+GpJ01F1dW56Z/h0s0MWem/WBvyXAO0uADEaRQknQRyMY2w==", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "Bx8MAu44K23LK7UQvG+iwkYcgHIUnd/G1cFeQX5sk9JzNO9E1fZebGWfLjfk90ii08MUilMmP5wcDOSlkJwFqw==", "dependencies": { - "Microsoft.Azure.Cosmos": "3.51.0", - "Microsoft.EntityFrameworkCore": "10.0.10", - "Microsoft.Extensions.Caching.Memory": "10.0.10", - "Microsoft.Extensions.Logging": "10.0.10", - "Newtonsoft.Json": "13.0.3" + "Microsoft.Azure.Cosmos": "3.61.0", + "Microsoft.EntityFrameworkCore": "10.0.11", + "Microsoft.Extensions.Caching.Memory": "10.0.11", + "Microsoft.Extensions.Logging": "10.0.11", + "Newtonsoft.Json": "13.0.4" } }, "Microsoft.EntityFrameworkCore.Sqlite": { "type": "CentralTransitive", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "kzg9MuQNJvZQxAU+piSkEzc7/1tpW6n1nVSGGMObu2GgxLK8Nf+6fvZundaznTZ+O2KhfPZ8HFNCzMH3PWDUmA==", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "jc7iVrhQyInR3loraMESfEFaFOtQOB1mRKHjX6QYC9o7YDbfMNbAPnIwlpffnFwhXd6/27FKaaV+sWSoLd4F1g==", "dependencies": { - "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.10", - "Microsoft.Extensions.Caching.Memory": "10.0.10", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", - "Microsoft.Extensions.DependencyModel": "10.0.10", - "Microsoft.Extensions.Logging": "10.0.10", - "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", - "SQLitePCLRaw.core": "2.1.11" + "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.11", + "Microsoft.Extensions.Caching.Memory": "10.0.11", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.DependencyModel": "10.0.11", + "Microsoft.Extensions.Logging": "10.0.11", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.12", + "SQLitePCLRaw.core": "2.1.12" } }, "Microsoft.EntityFrameworkCore.SqlServer": { "type": "CentralTransitive", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "xwAOvQ1WCfWyA4sRkoYVHyTm8UJ7NDYpRo++2oWuXGQ9g60Z1yepaQBmoPDT2nX24q4ISWnktAW9zARFTiSVvg==", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "LClSs5cNN6za2Uk7IeH7RAP4Qe5EMXxFsD9i53ncPx21TVg04IEjoFyxvvsSNkq2/Ux9HyJsJ2qEO8Tl7Gcrrg==", "dependencies": { - "Microsoft.Data.SqlClient": "6.1.1", - "Microsoft.EntityFrameworkCore.Relational": "10.0.10", - "Microsoft.Extensions.Caching.Memory": "10.0.10", - "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", - "Microsoft.Extensions.Logging": "10.0.10" + "Microsoft.Data.SqlClient": "6.1.6", + "Microsoft.EntityFrameworkCore.Relational": "10.0.11", + "Microsoft.Extensions.Caching.Memory": "10.0.11", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.Logging": "10.0.11" } }, "Microsoft.Extensions.Caching.Hybrid": { "type": "CentralTransitive", - "requested": "[10.8.0, )", - "resolved": "10.8.0", - "contentHash": "RCtCTK3eqKXx8LXqd7B8GjbTkIp4k3EgKeunaCmBJ+WA55Nva83CI2I+wVyp0S9jTG+aT6AYWQbkKOfvFOlmLA==", + "requested": "[10.9.0, )", + "resolved": "10.9.0", + "contentHash": "D0tHRbUCliInxTfPgN9K6RxrCVaWu9kyIT66vbIJ/YwA6BV2xa8vTqoE3JQXA2Rf2vRdgd29+HR6Ay6KmzDslw==", "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "10.0.10", - "Microsoft.Extensions.Caching.Memory": "10.0.10", - "Microsoft.Extensions.Logging.Abstractions": "10.0.10", - "Microsoft.Extensions.Options": "10.0.10" + "Microsoft.Extensions.Caching.Abstractions": "10.0.11", + "Microsoft.Extensions.Caching.Memory": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "CentralTransitive", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "5Vnd2I75DmZCVEjSynIdJ/0EGafgnLQwgR3t2C2/fkjx/nRG+cLwxLLdInoHeCEpkD5K4Ov/g9ZCRYrl4TRsaA==", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "fVi053xdpda9Em7vSkmgVxO/PtgC2m78ekReKWsgcyskqY0U82Bz/MONwxpGzI0hElYKJfw+fupqMVeKW3fSaA==", "dependencies": { - "Microsoft.Extensions.Primitives": "10.0.10" + "Microsoft.Extensions.Primitives": "10.0.11" } }, "Microsoft.Extensions.Http": { "type": "CentralTransitive", - "requested": "[10.0.10, )", - "resolved": "10.0.10", - "contentHash": "DuEBLw2y7ZBfilnaJtlge8f2M49932v43t7j1InceXVjcZzxNrSEkobYXdgZkXuPFnFbKcbWpAS+fZgYEW1BhA==", + "requested": "[10.0.11, )", + "resolved": "10.0.11", + "contentHash": "ujx8RvcKzkxPFBguwgiygbwWVHVK0P7HFlNJ6I0JBRb28tzwe42jixBoA+dGmqG9IHepPVcm04vv2IDnU93ekA==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "10.0.10", - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.10", - "Microsoft.Extensions.Diagnostics": "10.0.10", - "Microsoft.Extensions.Logging": "10.0.10", - "Microsoft.Extensions.Logging.Abstractions": "10.0.10", - "Microsoft.Extensions.Options": "10.0.10" + "Microsoft.Extensions.Configuration.Abstractions": "10.0.11", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.11", + "Microsoft.Extensions.Diagnostics": "10.0.11", + "Microsoft.Extensions.Logging": "10.0.11", + "Microsoft.Extensions.Logging.Abstractions": "10.0.11", + "Microsoft.Extensions.Options": "10.0.11" } }, "Microsoft.Extensions.Http.Resilience": { "type": "CentralTransitive", - "requested": "[10.8.0, )", - "resolved": "10.8.0", - "contentHash": "XtbZyYVxSNn1Aj2Z0PA8yauS12hPszTWDZ3GtZ5Vww7Ai3d8Pm841jMBnTvoTf1t9mRf3eSB38V5KUWxH6/4oQ==", + "requested": "[10.9.0, )", + "resolved": "10.9.0", + "contentHash": "loWrGc0mZt6N91IV+PZQeNU4uvb2vHdKgU9pUo8i2SGix9edTNCVhwzBp3gWJeaXeoPMph6F9xXcXF4W2ePpRg==", "dependencies": { - "Microsoft.Extensions.Http.Diagnostics": "10.8.0", - "Microsoft.Extensions.ObjectPool": "10.0.10", - "Microsoft.Extensions.Resilience": "10.8.0" + "Microsoft.Extensions.Http.Diagnostics": "10.9.0", + "Microsoft.Extensions.ObjectPool": "10.0.11", + "Microsoft.Extensions.Resilience": "10.9.0" } }, "Microsoft.FeatureManagement": { @@ -1125,6 +1138,12 @@ "MiniProfiler.Shared": "4.5.4" } }, + "Polly.Core": { + "type": "CentralTransitive", + "requested": "[8.7.0, )", + "resolved": "8.7.0", + "contentHash": "BS2t+nsBer16PIebCEPNBK5fgMisADQyRCd7K+BgkMWpFmSaiYE+rVVNpFhGRqUkJmNSLmw0uCNzHHWfgml28Q==" + }, "Scrutor": { "type": "CentralTransitive", "requested": "[7.0.0, )", @@ -1180,11 +1199,11 @@ }, "xunit.v3.extensibility.core": { "type": "CentralTransitive", - "requested": "[3.2.2, )", - "resolved": "3.2.2", - "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==", + "requested": "[4.0.0, )", + "resolved": "4.0.0", + "contentHash": "+tTe9VX2vwrUHGI48FE4ly956Ry084wPH4I/7g5D36AkAMGjQRZZDVz8eH2GwX/CLS9PDQRSca534WyrAYsyzw==", "dependencies": { - "xunit.v3.common": "[3.2.2]" + "xunit.v3.common": "[4.0.0]" } } } diff --git a/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/EFReadRepositoryKeysetPagingTests.cs b/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/EFReadRepositoryKeysetPagingTests.cs new file mode 100644 index 00000000..7464382d --- /dev/null +++ b/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/EFReadRepositoryKeysetPagingTests.cs @@ -0,0 +1,270 @@ +using System.Linq.Expressions; +using AwesomeAssertions; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using MMCA.Common.Domain.Specifications; +using MMCA.Common.Infrastructure.Persistence.Repositories; +using MMCA.Common.Shared.Abstractions; + +namespace MMCA.Common.Infrastructure.Tests.Persistence; + +/// +/// Covers keyset ("seek") paging end to end against a real provider: cursor round-trips, next-page +/// detection, the non-unique sort key that makes the identifier tie-break load-bearing, descending +/// pages, nullable sort keys, and the two rejection paths (unknown sort column, malformed cursor). +/// +public sealed class EFReadRepositoryKeysetPagingTests : IDisposable +{ + private readonly SqliteConnection _connection; + private readonly SpecificationTestDbContext _context; + private readonly EFReadRepository _sut; + + public EFReadRepositoryKeysetPagingTests() + { + _connection = new SqliteConnection("DataSource=:memory:"); + _connection.Open(); + + _context = new SpecificationTestDbContext( + new DbContextOptionsBuilder().UseSqlite(_connection).Options); + _context.Database.EnsureCreated(); + _sut = new EFReadRepository(_context); + + Seed(); + } + + public void Dispose() + { + _context.Dispose(); + _connection.Dispose(); + } + + /// + /// Six rows whose Name repeats deliberately: without the identifier tie-break a page boundary + /// inside a repeated name is exactly where a row gets returned twice or never. + /// + private void Seed() + { + _context.AddRange( + new SpecTestEntity { Id = 1, Name = "aaa", Rank = 10, Category = "x" }, + new SpecTestEntity { Id = 2, Name = "bbb", Rank = 20, Category = null }, + new SpecTestEntity { Id = 3, Name = "bbb", Rank = 30, Category = "y" }, + new SpecTestEntity { Id = 4, Name = "bbb", Rank = 40, Category = null }, + new SpecTestEntity { Id = 5, Name = "ccc", Rank = 50, Category = "z" }, + new SpecTestEntity { Id = 6, Name = "ddd", Rank = 60, Category = "z" }); + _context.SaveChanges(); + _context.ChangeTracker.Clear(); + } + + private async Task> WalkEveryPageAsync(int pageSize, string? sortColumn, bool descending) + { + List ids = []; + string? cursor = null; + + for (var page = 0; page < 20; page++) + { + var result = await _sut.GetPageByCursorAsync( + new KeysetPageRequest(pageSize, sortColumn, descending, cursor)); + + result.IsSuccess.Should().BeTrue(); + ids.AddRange(result.Value!.Items.Select(e => e.Id)); + + cursor = result.Value.NextCursor; + if (cursor is null) + return ids; + } + + throw new InvalidOperationException("The cursor walk did not terminate."); + } + + // ── Id-only paging ── + [Fact] + public async Task GetPageByCursorAsync_WithNoSortColumn_PagesByIdAscending() + { + var ids = await WalkEveryPageAsync(pageSize: 2, sortColumn: null, descending: false); + + ids.Should().Equal(1, 2, 3, 4, 5, 6); + } + + [Fact] + public async Task GetPageByCursorAsync_WithNoSortColumnDescending_PagesByIdDescending() + { + var ids = await WalkEveryPageAsync(pageSize: 4, sortColumn: null, descending: true); + + ids.Should().Equal(6, 5, 4, 3, 2, 1); + } + + [Fact] + public async Task GetPageByCursorAsync_ReturnsTheFirstPageAndACursor() + { + var result = await _sut.GetPageByCursorAsync(new KeysetPageRequest(2)); + + result.IsSuccess.Should().BeTrue(); + result.Value!.Items.Select(e => e.Id).Should().Equal(1, 2); + result.Value.NextCursor.Should().NotBeNullOrEmpty(); + } + + [Fact] + public async Task GetPageByCursorAsync_OnTheLastPage_ReturnsNoCursor() + { + var result = await _sut.GetPageByCursorAsync(new KeysetPageRequest(100)); + + result.Value!.Items.Should().HaveCount(6); + result.Value.NextCursor.Should().BeNull("there is nothing after the last row"); + } + + [Fact] + public async Task GetPageByCursorAsync_WhenThePageSizeExactlyMatchesTheSet_ReturnsNoCursor() + { + var result = await _sut.GetPageByCursorAsync(new KeysetPageRequest(6)); + + result.Value!.Items.Should().HaveCount(6); + result.Value.NextCursor.Should().BeNull("the probe row is what proves a next page exists"); + } + + // ── Non-unique sort key: the tie-break is what makes this correct ── + [Fact] + public async Task GetPageByCursorAsync_WithARepeatedSortKey_ReturnsEveryRowExactlyOnce() + { + var ids = await WalkEveryPageAsync(pageSize: 2, sortColumn: "Name", descending: false); + + ids.Should().Equal(1, 2, 3, 4, 5, 6); + ids.Should().OnlyHaveUniqueItems("a page boundary inside a repeated sort key must not repeat a row"); + } + + [Fact] + public async Task GetPageByCursorAsync_WithARepeatedSortKeyDescending_ReturnsEveryRowExactlyOnce() + { + var ids = await WalkEveryPageAsync(pageSize: 2, sortColumn: "Name", descending: true); + + // Name descending, identifier ascending within each repeated name. + ids.Should().Equal(6, 5, 2, 3, 4, 1); + ids.Should().OnlyHaveUniqueItems(); + } + + [Fact] + public async Task GetPageByCursorAsync_WithANumericSortKey_Pages() + { + var ids = await WalkEveryPageAsync(pageSize: 3, sortColumn: "Rank", descending: false); + + ids.Should().Equal(1, 2, 3, 4, 5, 6); + } + + [Fact] + public async Task GetPageByCursorAsync_ResolvesTheSortColumnCaseInsensitively() + { + var result = await _sut.GetPageByCursorAsync(new KeysetPageRequest(2, "rank", descending: true)); + + result.IsSuccess.Should().BeTrue(); + result.Value!.Items.Select(e => e.Id).Should().Equal(6, 5); + } + + // ── Nullable sort keys ── + [Fact] + public async Task GetPageByCursorAsync_WithANullableSortKey_ReturnsEveryRowExactlyOnce() + { + var ids = await WalkEveryPageAsync(pageSize: 2, sortColumn: "Category", descending: false); + + ids.Should().HaveCount(6).And.OnlyHaveUniqueItems(); + ids.Take(2).Should().BeEquivalentTo([2, 4], "nulls sort first ascending"); + } + + [Fact] + public async Task GetPageByCursorAsync_WithANullableSortKeyDescending_ReturnsEveryRowExactlyOnce() + { + var ids = await WalkEveryPageAsync(pageSize: 2, sortColumn: "Category", descending: true); + + ids.Should().HaveCount(6).And.OnlyHaveUniqueItems(); + } + + // ── Specification scoping ── + [Fact] + public async Task GetPageByCursorAsync_HonorsTheSpecificationCriteria() + { + var result = await _sut.GetPageByCursorAsync(new KeysetPageRequest(100), new BbbSpecification()); + + result.Value!.Items.Select(e => e.Id).Should().Equal(2, 3, 4); + } + + [Fact] + public async Task GetPageByCursorAsync_WithASpecification_KeepsScopingEveryPage() + { + var first = await _sut.GetPageByCursorAsync(new KeysetPageRequest(2), new BbbSpecification()); + var second = await _sut.GetPageByCursorAsync( + new KeysetPageRequest(2, cursor: first.Value!.NextCursor), new BbbSpecification()); + + first.Value.Items.Select(e => e.Id).Should().Equal(2, 3); + second.Value!.Items.Select(e => e.Id).Should().Equal(4); + second.Value.NextCursor.Should().BeNull(); + } + + // ── Rejections ── + [Fact] + public async Task GetPageByCursorAsync_WithAnUnknownSortColumn_FailsValidation() + { + var result = await _sut.GetPageByCursorAsync(new KeysetPageRequest(2, "NotAColumn")); + + result.IsFailure.Should().BeTrue(); + result.Errors.Should().ContainSingle().Which.Type.Should().Be(ErrorType.Validation); + result.Errors[0].Code.Should().Be("Error.InvalidEntityField"); + } + + [Theory] + [InlineData("not-a-cursor!!")] + [InlineData("Zm9v")] + public async Task GetPageByCursorAsync_WithAMalformedCursor_FailsValidation(string cursor) + { + var result = await _sut.GetPageByCursorAsync(new KeysetPageRequest(2, cursor: cursor)); + + result.IsFailure.Should().BeTrue(); + result.Errors.Should().ContainSingle().Which.Code.Should().Be("Error.InvalidCursor"); + } + + [Fact] + public async Task GetPageByCursorAsync_WithACursorWhoseIdIsNotTheKeyType_FailsValidation() + { + var forged = KeysetCursor.Encode(null, "not-an-int"); + + var result = await _sut.GetPageByCursorAsync(new KeysetPageRequest(2, cursor: forged)); + + result.IsFailure.Should().BeTrue(); + result.Errors[0].Code.Should().Be("Error.InvalidCursor"); + } + + [Fact] + public async Task GetPageByCursorAsync_WithACursorWhoseSortValueIsNotTheKeyType_FailsValidation() + { + var forged = KeysetCursor.Encode("not-a-number", "1"); + + var result = await _sut.GetPageByCursorAsync(new KeysetPageRequest(2, "Rank", cursor: forged)); + + result.IsFailure.Should().BeTrue(); + result.Errors[0].Code.Should().Be("Error.InvalidCursor"); + } + + [Fact] + public async Task GetPageByCursorAsync_WithNullRequest_Throws() + { + var act = () => _sut.GetPageByCursorAsync(null!); + + await act.Should().ThrowAsync(); + } + + // ── Soft delete ── + [Fact] + public async Task GetPageByCursorAsync_ExcludesSoftDeletedRows() + { + var deleted = await _context.Entities.SingleAsync(e => e.Id == 3); + deleted.Delete().IsSuccess.Should().BeTrue(); + await _context.SaveChangesAsync(); + _context.ChangeTracker.Clear(); + + var result = await _sut.GetPageByCursorAsync(new KeysetPageRequest(100)); + + result.Value!.Items.Select(e => e.Id).Should().Equal(1, 2, 4, 5, 6); + } + + private sealed class BbbSpecification : Specification + { + public override Expression> Criteria => e => e.Name == "bbb"; + } +} diff --git a/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/EFReadRepositorySpecificationTests.cs b/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/EFReadRepositorySpecificationTests.cs new file mode 100644 index 00000000..a62d5a17 --- /dev/null +++ b/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/EFReadRepositorySpecificationTests.cs @@ -0,0 +1,264 @@ +using System.Linq.Expressions; +using AwesomeAssertions; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using MMCA.Common.Domain.Interfaces; +using MMCA.Common.Domain.Specifications; +using MMCA.Common.Infrastructure.Persistence.Repositories; + +namespace MMCA.Common.Infrastructure.Tests.Persistence; + +/// +/// Covers the specification-driven repository members against a real provider: the list and +/// projected-list reads, the aggregate reads, and the two pieces of state only the repository can +/// apply, tracking and soft-delete scope. +/// +public sealed class EFReadRepositorySpecificationTests : IDisposable +{ + private readonly SqliteConnection _connection; + private readonly SpecificationTestDbContext _context; + private readonly EFReadRepository _sut; + + public EFReadRepositorySpecificationTests() + { + _connection = new SqliteConnection("DataSource=:memory:"); + _connection.Open(); + + _context = new SpecificationTestDbContext( + new DbContextOptionsBuilder().UseSqlite(_connection).Options); + _context.Database.EnsureCreated(); + _sut = new EFReadRepository(_context); + + Seed(); + } + + public void Dispose() + { + _context.Dispose(); + _connection.Dispose(); + } + + private void Seed() + { + var deleted = new SpecTestEntity { Id = 5, Name = "deleted", Rank = 9 }; + + _context.AddRange( + new SpecTestEntity { Id = 1, Name = "beta", Rank = 2, Category = "x" }, + new SpecTestEntity { Id = 2, Name = "alpha", Rank = 3 }, + new SpecTestEntity { Id = 3, Name = "beta", Rank = 1, Category = "y" }, + new SpecTestEntity { Id = 4, Name = "gamma", Rank = 5, Category = "x" }, + deleted); + _context.Add(new SpecTestChild { Id = 10, SpecTestEntityId = 1, Label = "one" }); + _context.SaveChanges(); + + deleted.Delete().IsSuccess.Should().BeTrue(); + _context.SaveChanges(); + _context.ChangeTracker.Clear(); + } + + // ── ListAsync ── + [Fact] + public async Task ListAsync_AppliesCriteriaOrderingAndPaging() + { + var rows = await _sut.ListAsync(new TopTwoByRankSpecification()); + + rows.Select(e => e.Id).Should().Equal(4, 2); + } + + [Fact] + public async Task ListAsync_AppliesIncludes() + { + var rows = await _sut.ListAsync(new IncludingSpecification()); + + rows.Should().ContainSingle(); + rows.Single().Children.Should().ContainSingle().Which.Label.Should().Be("one"); + } + + [Fact] + public async Task ListAsync_IsUntrackedByDefault() + { + await _sut.ListAsync(new AllSpecification()); + + _context.ChangeTracker.Entries().Should().BeEmpty("a specification read is a read"); + } + + [Fact] + public async Task ListAsync_WithTracking_TracksTheResults() + { + await _sut.ListAsync(new TrackedSpecification()); + + _context.ChangeTracker.Entries().Should().NotBeEmpty(); + } + + [Fact] + public async Task ListAsync_ExcludesSoftDeletedRowsByDefault() + { + var rows = await _sut.ListAsync(new AllSpecification()); + + rows.Select(e => e.Id).Should().NotContain(5); + rows.Should().HaveCount(4); + } + + [Fact] + public async Task ListAsync_WithSoftDeleted_IncludesThem() + { + var rows = await _sut.ListAsync(new IncludingSoftDeletedSpecification()); + + rows.Select(e => e.Id).Should().Contain(5); + rows.Should().HaveCount(5); + } + + [Fact] + public async Task ListAsync_WithNullSpecification_Throws() + { + var act = () => _sut.ListAsync(null!); + + await act.Should().ThrowAsync(); + } + + // ── ListAsync with projection ── + [Fact] + public async Task ListAsync_WithSelect_ProjectsServerSide() + { + var names = await _sut.ListAsync(new TopTwoByRankSpecification(), e => e.Name); + + names.Should().Equal("gamma", "alpha"); + } + + [Fact] + public async Task ListAsync_WithSelect_PagesEntityRowsBeforeProjecting() + { + var ranks = await _sut.ListAsync(new TopTwoByRankSpecification(), e => e.Rank); + + ranks.Should().Equal([5, 3], "ordering and paging must run over the entity rows, then project"); + } + + [Fact] + public async Task ListAsync_WithNullSelect_Throws() + { + var act = () => _sut.ListAsync(new AllSpecification(), null!); + + await act.Should().ThrowAsync(); + } + + // ── CountAsync / AnyAsync ── + [Fact] + public async Task CountAsync_CountsEveryMatchingRow_IgnoringPaging() + { + var count = await _sut.CountAsync(new TopTwoByRankSpecification()); + + count.Should().Be(4, "a count of one page of the matches is never what a caller means"); + } + + [Fact] + public async Task CountAsync_HonorsTheCriteria() + { + var count = await _sut.CountAsync(new BetaSpecification()); + + count.Should().Be(2); + } + + [Fact] + public async Task CountAsync_WithSoftDeleted_CountsThem() + { + (await _sut.CountAsync(new AllSpecification())).Should().Be(4); + (await _sut.CountAsync(new IncludingSoftDeletedSpecification())).Should().Be(5); + } + + [Fact] + public async Task AnyAsync_ReturnsTrueWhenAnyRowMatches() => + (await _sut.AnyAsync(new BetaSpecification())).Should().BeTrue(); + + [Fact] + public async Task AnyAsync_ReturnsFalseWhenNoRowMatches() => + (await _sut.AnyAsync(new NoMatchSpecification())).Should().BeFalse(); + + [Fact] + public async Task AnyAsync_DoesNotSeeASoftDeletedRowByDefault() => + (await _sut.AnyAsync(new DeletedByNameSpecification())).Should().BeFalse(); + + [Fact] + public async Task CountAsync_WithNullSpecification_Throws() + { + var act = () => _sut.CountAsync((ISpecification)null!); + + await act.Should().ThrowAsync(); + } + + [Fact] + public async Task AnyAsync_WithNullSpecification_Throws() + { + var act = () => _sut.AnyAsync(null!); + + await act.Should().ThrowAsync(); + } + + // ── Composed specifications reach the database ── + [Fact] + public async Task ListAsync_WithAComposedSpecification_Translates() + { + var composed = new BetaSpecification().And(new HighRankSpecification().Not()); + + var rows = await _sut.ListAsync(composed); + + rows.Select(e => e.Id).Should().BeEquivalentTo([1, 3]); + } + + // ── Test specifications ── + private sealed class AllSpecification : Specification + { + public override Expression> Criteria => e => true; + } + + private sealed class BetaSpecification : Specification + { + public override Expression> Criteria => e => e.Name == "beta"; + } + + private sealed class HighRankSpecification : Specification + { + public override Expression> Criteria => e => e.Rank > 4; + } + + private sealed class NoMatchSpecification : Specification + { + public override Expression> Criteria => e => e.Name == "nothing"; + } + + private sealed class DeletedByNameSpecification : Specification + { + public override Expression> Criteria => e => e.Name == "deleted"; + } + + private sealed class TopTwoByRankSpecification : QuerySpecification + { + public TopTwoByRankSpecification() + { + AddOrderBy(e => e.Rank, descending: true); + ApplyPaging(skip: 0, take: 2); + } + + public override Expression> Criteria => e => true; + } + + private sealed class IncludingSpecification : QuerySpecification + { + public IncludingSpecification() => AddInclude(nameof(SpecTestEntity.Children)); + + public override Expression> Criteria => e => e.Id == 1; + } + + private sealed class TrackedSpecification : QuerySpecification + { + public TrackedSpecification() => WithTracking(); + + public override Expression> Criteria => e => true; + } + + private sealed class IncludingSoftDeletedSpecification : QuerySpecification + { + public IncludingSoftDeletedSpecification() => WithSoftDeleted(); + + public override Expression> Criteria => e => true; + } +} diff --git a/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/EFRepositoryIntegrationTests.cs b/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/EFRepositoryIntegrationTests.cs index 0d62e482..d1c7eb78 100644 --- a/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/EFRepositoryIntegrationTests.cs +++ b/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/EFRepositoryIntegrationTests.cs @@ -268,7 +268,7 @@ public async Task CountAsync_WithPredicate_FiltersCorrectly() [Fact] public async Task CountAsync_WithNullPredicate_Throws() { - var act = () => _sut.CountAsync(null!); + var act = () => _sut.CountAsync((System.Linq.Expressions.Expression>)null!); await act.Should().ThrowAsync(); } diff --git a/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/PushNotificationProjectionTranslationTests.cs b/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/PushNotificationProjectionTranslationTests.cs new file mode 100644 index 00000000..ea70da21 --- /dev/null +++ b/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/PushNotificationProjectionTranslationTests.cs @@ -0,0 +1,125 @@ +using AwesomeAssertions; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using MMCA.Common.Application.Notifications.PushNotifications.DTOs; +using MMCA.Common.Domain.Notifications.PushNotifications; + +namespace MMCA.Common.Infrastructure.Tests.Persistence; + +/// +/// The Application-tier projector test proves the projected VALUES equal the mapper's, but it runs +/// the projection in memory, where anything compiles. This tier proves the other half: that a real +/// provider TRANSLATES the projection into SQL, including the enum-to-string conversion the mapper +/// does with a method call. An untranslatable projection would only surface at runtime in a host. +/// +public sealed class PushNotificationProjectionTranslationTests : IDisposable +{ + private readonly SqliteConnection _connection; + private readonly ProjectionTestDbContext _context; + private readonly PushNotificationDTOProjector _projector = new(); + private readonly PushNotificationDTOMapper _mapper = new(); + + public PushNotificationProjectionTranslationTests() + { + _connection = new SqliteConnection("DataSource=:memory:"); + _connection.Open(); + + _context = new ProjectionTestDbContext( + new DbContextOptionsBuilder().UseSqlite(_connection).Options); + _context.Database.EnsureCreated(); + + Seed(); + } + + public void Dispose() + { + _context.Dispose(); + _connection.Dispose(); + } + + private void Seed() + { + var pending = PushNotification.Create("First", "Body one", sentByUserId: 1, recipientCount: 5).Value!; + var sent = PushNotification.Create("Second", "Body two", sentByUserId: 2, recipientCount: 9, scopeKey: "event:2").Value!; + sent.MarkAsSent(); + var failed = PushNotification.Create("Third", "Body three", sentByUserId: 3, recipientCount: 1).Value!; + failed.MarkAsFailed(); + + _context.AddRange(pending, sent, failed); + _context.SaveChanges(); + _context.ChangeTracker.Clear(); + } + + [Fact] + public void ProjectTo_TranslatesToSql() + { + var sql = _projector.ProjectTo(_context.PushNotifications.AsNoTracking()).ToQueryString(); + + sql.Should().Contain("SELECT"); + sql.Should().NotContain("*", "a projection exists to select the DTO's columns, not every column"); + } + + [Fact] + public async Task ProjectTo_MaterializesTheSameValuesAsTheMapper() + { + var projected = await _projector.ProjectTo(_context.PushNotifications.AsNoTracking()) + .OrderBy(d => d.Id) + .ToListAsync(); + + var entities = await _context.PushNotifications.AsNoTracking().OrderBy(e => e.Id).ToListAsync(); + var mapped = _mapper.MapToDTOs(entities); + + projected.Should().BeEquivalentTo(mapped); + } + + [Fact] + public async Task ProjectTo_RendersEveryStatusAsItsEnumName() + { + var statuses = await _projector.ProjectTo(_context.PushNotifications.AsNoTracking()) + .OrderBy(d => d.Id) + .Select(d => d.Status) + .ToListAsync(); + + statuses.Should().Equal( + nameof(PushNotificationStatus.Pending), + nameof(PushNotificationStatus.Sent), + nameof(PushNotificationStatus.Failed)); + } + + [Fact] + public async Task ProjectTo_StaysComposable() + { + // Composing after the projection is what makes it a pushdown rather than a materialize-then-map. + var titles = await _projector.ProjectTo(_context.PushNotifications.AsNoTracking()) + .Where(d => d.RecipientCount > 1) + .OrderByDescending(d => d.RecipientCount) + .Select(d => d.Title) + .ToListAsync(); + + titles.Should().Equal("Second", "First"); + } + + /// + /// A minimal SQLite-mappable context over the notification aggregate. The production + /// configuration is SQL Server specific (schema plus a bracketed filtered index), so the mapping + /// is declared here, but the ONE detail that matters for the projection is kept: the status is + /// stored through a string conversion, exactly as in production. + /// + public sealed class ProjectionTestDbContext(DbContextOptions options) : DbContext(options) + { + /// Gets the notification set. + public DbSet PushNotifications => Set(); + + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) => + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Property(e => e.Id).ValueGeneratedOnAdd(); + b.Property(e => e.Title); + b.Property(e => e.Body); + b.Property(e => e.ScopeKey); + b.Property(e => e.Status).HasConversion().HasMaxLength(20); + }); + } +} diff --git a/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/SpecificationEvaluatorTests.cs b/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/SpecificationEvaluatorTests.cs new file mode 100644 index 00000000..76041b79 --- /dev/null +++ b/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/SpecificationEvaluatorTests.cs @@ -0,0 +1,238 @@ +using System.Linq.Expressions; +using AwesomeAssertions; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using MMCA.Common.Domain.Specifications; +using MMCA.Common.Infrastructure.Persistence.Repositories; + +namespace MMCA.Common.Infrastructure.Tests.Persistence; + +/// +/// Exercises SpecificationEvaluator against a real provider (SQLite in-memory), because the +/// interesting parts are all translation: an ordering chain bound back to its concrete key type by +/// reflection, includes with the collection split-query switch, and Skip/Take. +/// +public sealed class SpecificationEvaluatorTests : IDisposable +{ + private readonly SqliteConnection _connection; + private readonly SpecificationTestDbContext _context; + + public SpecificationEvaluatorTests() + { + _connection = new SqliteConnection("DataSource=:memory:"); + _connection.Open(); + + _context = new SpecificationTestDbContext( + new DbContextOptionsBuilder().UseSqlite(_connection).Options); + _context.Database.EnsureCreated(); + + Seed(); + } + + public void Dispose() + { + _context.Dispose(); + _connection.Dispose(); + } + + private void Seed() + { + _context.AddRange( + new SpecTestEntity { Id = 1, Name = "beta", Rank = 2, Category = "x" }, + new SpecTestEntity { Id = 2, Name = "alpha", Rank = 3, Category = null }, + new SpecTestEntity { Id = 3, Name = "beta", Rank = 1, Category = "y" }, + new SpecTestEntity { Id = 4, Name = "gamma", Rank = 5, Category = "x" }); + + _context.AddRange( + new SpecTestChild { Id = 10, SpecTestEntityId = 1, Label = "one" }, + new SpecTestChild { Id = 11, SpecTestEntityId = 1, Label = "two" }); + + _context.SaveChanges(); + _context.ChangeTracker.Clear(); + } + + private IQueryable Source => _context.Entities.AsNoTracking(); + + // ── Criteria ── + [Fact] + public async Task Apply_AlwaysAppliesTheCriteria() + { + var query = SpecificationEvaluator.Apply(Source, new BetaSpecification()); + + var ids = await query.Select(e => e.Id).ToListAsync(); + + ids.Should().BeEquivalentTo([1, 3]); + } + + [Fact] + public void Apply_WithNullSource_Throws() + { + var act = () => SpecificationEvaluator.Apply(null!, new BetaSpecification()); + + act.Should().Throw(); + } + + [Fact] + public void Apply_WithNullSpecification_Throws() + { + var act = () => SpecificationEvaluator.Apply(Source, null!); + + act.Should().Throw(); + } + + [Fact] + public async Task Apply_WithAPlainSpecification_AddsNoShape() + { + // A plain (non-query) specification contributes criteria only, so the natural order survives. + var query = SpecificationEvaluator.Apply(Source, new BetaSpecification()); + + var sql = query.ToQueryString(); + var ids = await query.Select(e => e.Id).ToListAsync(); + + sql.Should().NotContain("ORDER BY"); + sql.Should().NotContain("LIMIT"); + ids.Should().HaveCount(2); + } + + // ── Ordering ── + [Fact] + public async Task Apply_AppliesTheOrderingChainInOrder() + { + var query = SpecificationEvaluator.Apply(Source, new OrderedSpecification()); + + var ids = await query.Select(e => e.Id).ToListAsync(); + + // Name ascending, then Rank descending: alpha(2), beta rank 2 (1), beta rank 1 (3), gamma(4). + ids.Should().Equal(2, 1, 3, 4); + } + + [Fact] + public async Task Apply_HonorsADescendingFirstKey() + { + var query = SpecificationEvaluator.Apply(Source, new RankDescendingSpecification()); + + var ids = await query.Select(e => e.Id).ToListAsync(); + + ids.Should().Equal(4, 2, 1, 3); + } + + [Fact] + public async Task Apply_WithNoOrdering_LeavesTheQueryUnordered() + { + var query = SpecificationEvaluator.Apply(Source, new UnorderedQuerySpecification()); + + query.ToQueryString().Should().NotContain("ORDER BY"); + (await query.CountAsync()).Should().Be(4); + } + + // ── Includes ── + [Fact] + public async Task Apply_AppliesIncludes() + { + var query = SpecificationEvaluator.Apply(Source, new IncludingSpecification()); + + var rows = await query.ToListAsync(); + + rows.Should().ContainSingle(); + rows[0].Children.Select(c => c.Label).Should().BeEquivalentTo("one", "two"); + } + + [Fact] + public void ApplyIncludes_WithACollectionNavigation_SwitchesToSplitQuery() + { + var withCollection = SpecificationEvaluator.ApplyIncludes(Source, ["Children"]).Expression.ToString(); + var withNothing = SpecificationEvaluator.ApplyIncludes(Source, []).Expression.ToString(); + + withCollection.Should().Contain( + "AsSplitQuery", + "a collection include must auto-switch to split query so sibling collections do not multiply rows"); + withNothing.Should().NotContain("AsSplitQuery", "there is nothing to split without a collection include"); + } + + [Fact] + public void ApplyIncludes_IgnoresBlankPaths() + { + var act = () => SpecificationEvaluator.ApplyIncludes(Source, [string.Empty, " "]).ToQueryString(); + + act.Should().NotThrow(); + } + + [Fact] + public void ApplyIncludes_WithNullIncludes_Throws() + { + var act = () => SpecificationEvaluator.ApplyIncludes(Source, null!); + + act.Should().Throw(); + } + + // ── Paging ── + [Fact] + public async Task Apply_AppliesSkipAndTake() + { + var query = SpecificationEvaluator.Apply(Source, new PagedSpecification()); + + var ids = await query.Select(e => e.Id).ToListAsync(); + + // Ordered by Id ascending, skip 1, take 2. + ids.Should().Equal(2, 3); + } + + [Fact] + public async Task Apply_WithApplyShapeFalse_IgnoresOrderingAndPaging() + { + var query = SpecificationEvaluator.Apply(Source, new PagedSpecification(), applyShape: false); + + var sql = query.ToQueryString(); + + sql.Should().NotContain("ORDER BY"); + sql.Should().NotContain("OFFSET"); + (await query.CountAsync()).Should().Be(4, "the count must see every matching row, not one page of them"); + } + + // ── Test specifications ── + private sealed class BetaSpecification : Specification + { + public override Expression> Criteria => e => e.Name == "beta"; + } + + private sealed class OrderedSpecification : QuerySpecification + { + public OrderedSpecification() + { + AddOrderBy(e => e.Name); + AddOrderBy(e => e.Rank, descending: true); + } + + public override Expression> Criteria => e => true; + } + + private sealed class RankDescendingSpecification : QuerySpecification + { + public RankDescendingSpecification() => AddOrderBy(e => e.Rank, descending: true); + + public override Expression> Criteria => e => true; + } + + private sealed class UnorderedQuerySpecification : QuerySpecification + { + public override Expression> Criteria => e => true; + } + + private sealed class IncludingSpecification : QuerySpecification + { + public IncludingSpecification() => AddInclude(nameof(SpecTestEntity.Children)); + + public override Expression> Criteria => e => e.Id == 1; + } + + private sealed class PagedSpecification : QuerySpecification + { + public PagedSpecification() + { + AddOrderBy(e => e.Id); + ApplyPaging(skip: 1, take: 2); + } + + public override Expression> Criteria => e => true; + } +} diff --git a/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/SpecificationTestContext.cs b/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/SpecificationTestContext.cs new file mode 100644 index 00000000..78f245a9 --- /dev/null +++ b/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/SpecificationTestContext.cs @@ -0,0 +1,75 @@ +using Microsoft.EntityFrameworkCore; +using MMCA.Common.Domain.Entities; +using MMCA.Common.Infrastructure.Persistence.DbContexts; + +namespace MMCA.Common.Infrastructure.Tests.Persistence; + +/// +/// A parent aggregate with a scalar sort key, a nullable sort key, and a child collection, used by +/// the specification and keyset paging tests to exercise ordering, includes, and paging against a +/// real provider. +/// +public sealed class SpecTestEntity : AuditableBaseEntity +{ + /// Gets or sets the non-unique display name (a deliberately duplicated sort key). + public string Name { get; set; } = string.Empty; + + /// Gets or sets the numeric rank. + public int Rank { get; set; } + + /// Gets or sets the optional category (a nullable sort key). + public string? Category { get; set; } + + /// Gets or sets the child collection, so includes can be exercised. + public ICollection Children { get; set; } = []; +} + +/// The child of , reached through a collection navigation. +public sealed class SpecTestChild : AuditableBaseEntity +{ + /// Gets or sets the owning parent's identifier. + public int SpecTestEntityId { get; set; } + + /// Gets or sets the child label. + public string Label { get; set; } = string.Empty; +} + +/// +/// A minimal SQLite-mappable context over the specification test entities. The production +/// configurations are SQL Server specific, so the mapping is declared here, exactly as the +/// neighbouring EF repository integration tests do. The soft-delete filter is registered under the +/// production NAME, because the repository drops that filter by name. +/// +public sealed class SpecificationTestDbContext(DbContextOptions options) : DbContext(options) +{ + /// Gets the parent set. + public DbSet Entities => Set(); + + /// Gets the child set. + public DbSet Children => Set(); + + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Property(e => e.Id).ValueGeneratedNever(); + b.Property(e => e.Name); + b.Property(e => e.Rank); + b.Property(e => e.Category); + b.Ignore(e => e.RowVersion); + b.HasMany(e => e.Children).WithOne().HasForeignKey(c => c.SpecTestEntityId); + b.HasQueryFilter(ApplicationDbContext.SoftDeleteFilterName, e => !e.IsDeleted); + }); + + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Property(e => e.Id).ValueGeneratedNever(); + b.Property(e => e.Label); + b.Ignore(e => e.RowVersion); + b.HasQueryFilter(ApplicationDbContext.SoftDeleteFilterName, e => !e.IsDeleted); + }); + } +} diff --git a/Tests/Core/MMCA.Common.Shared.Tests/Abstractions/KeysetPaginationTests.cs b/Tests/Core/MMCA.Common.Shared.Tests/Abstractions/KeysetPaginationTests.cs new file mode 100644 index 00000000..7b1bbdb7 --- /dev/null +++ b/Tests/Core/MMCA.Common.Shared.Tests/Abstractions/KeysetPaginationTests.cs @@ -0,0 +1,167 @@ +using AwesomeAssertions; +using MMCA.Common.Shared.Abstractions; + +namespace MMCA.Common.Shared.Tests.Abstractions; + +/// +/// Covers the keyset paging value types: the request's clamp semantics, the page result, and the +/// cursor codec (round-trip, version gate, and rejection of anything malformed). +/// +public sealed class KeysetPaginationTests +{ + // ── KeysetPageRequest ── + [Theory] + [InlineData(0, 1)] + [InlineData(-5, 1)] + [InlineData(1, 1)] + [InlineData(50, 50)] + [InlineData(1000, 1000)] + [InlineData(5000, 1000)] + public void PageSize_IsClampedIntoTheAllowedRange(int requested, int expected) => + new KeysetPageRequest(requested).PageSize.Should().Be(expected); + + [Fact] + public void PageSize_IsAlsoClampedThroughTheInitializer() + { + var request = new KeysetPageRequest(10) { PageSize = 999_999 }; + + request.PageSize.Should().Be(KeysetPageRequest.MaxPageSize); + } + + [Fact] + public void ParameterlessConstructor_ProducesAMinimalFirstPageRequest() + { + var request = new KeysetPageRequest(); + + request.PageSize.Should().Be(1); + request.SortColumn.Should().BeNull(); + request.Descending.Should().BeFalse(); + request.Cursor.Should().BeNull(); + } + + [Fact] + public void Constructor_KeepsTheSortAndCursorItWasGiven() + { + var request = new KeysetPageRequest(25, "CreatedOn", descending: true, cursor: "abc"); + + request.SortColumn.Should().Be("CreatedOn"); + request.Descending.Should().BeTrue(); + request.Cursor.Should().Be("abc"); + } + + // ── KeysetCollectionResult ── + [Fact] + public void KeysetCollectionResult_CarriesItemsAndCursor() + { + var result = new KeysetCollectionResult([1, 2, 3], "next"); + + result.Items.Should().Equal(1, 2, 3); + result.NextCursor.Should().Be("next"); + } + + [Fact] + public void KeysetCollectionResult_Empty_HasNoCursor() + { + var result = new KeysetCollectionResult(); + + result.Items.Should().BeEmpty(); + result.NextCursor.Should().BeNull(); + } + + [Fact] + public void KeysetCollectionResult_IsACollectionResult() => + new KeysetCollectionResult([1], null).Should().BeAssignableTo>(); + + [Fact] + public void KeysetCollectionResult_WithNullItems_Throws() + { + var act = () => new KeysetCollectionResult(null!, null); + + act.Should().Throw(); + } + + // ── KeysetCursor round-trip ── + [Theory] + [InlineData("Widget", "42")] + [InlineData("", "1")] + [InlineData("a|b|c", "7")] + [InlineData("v1|0||x", "8")] + [InlineData("Ünïcödé ✓", "9")] + [InlineData("2026-08-17T12:34:56.7890123Z", "10")] + public void Encode_ThenDecode_ReturnsTheSameValues(string sortValue, string id) + { + var cursor = KeysetCursor.Encode(sortValue, id); + + KeysetCursor.TryDecode(cursor, out var decodedSort, out var decodedId).Should().BeTrue(); + decodedSort.Should().Be(sortValue); + decodedId.Should().Be(id); + } + + [Fact] + public void Encode_ThenDecode_PreservesANullSortValue() + { + var cursor = KeysetCursor.Encode(null, "42"); + + KeysetCursor.TryDecode(cursor, out var decodedSort, out var decodedId).Should().BeTrue(); + decodedSort.Should().BeNull("a null sort value must not decode as an empty string"); + decodedId.Should().Be("42"); + } + + [Fact] + public void Encode_ProducesAnOpaqueUrlSafeToken() + { + var cursor = KeysetCursor.Encode("Widget", "42"); + + cursor.Should().NotContain("|").And.NotContain("+").And.NotContain("/").And.NotContain("="); + cursor.Should().NotContain("Widget", "the cursor is opaque, not a readable payload"); + } + + [Fact] + public void Encode_WithNullId_Throws() + { + var act = () => KeysetCursor.Encode("Widget", null!); + + act.Should().Throw(); + } + + // ── KeysetCursor rejection ── + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("not-a-cursor!!")] + [InlineData("////")] + public void TryDecode_RejectsMalformedInput(string? cursor) + { + KeysetCursor.TryDecode(cursor, out var sortValue, out var id).Should().BeFalse(); + sortValue.Should().BeNull(); + id.Should().BeEmpty(); + } + + [Fact] + public void TryDecode_RejectsAnUnknownFormatVersion() + { + var forged = Encode("v2|1|V2lkZ2V0|NDI"); + + KeysetCursor.TryDecode(forged, out _, out _).Should().BeFalse( + "the version prefix exists so a future encoding cannot be mis-read as this one"); + } + + [Fact] + public void TryDecode_RejectsAWrongSegmentCount() + { + KeysetCursor.TryDecode(Encode("v1|1|V2lkZ2V0"), out _, out _).Should().BeFalse(); + KeysetCursor.TryDecode(Encode("v1|1|V2lkZ2V0|NDI|extra"), out _, out _).Should().BeFalse(); + } + + [Fact] + public void TryDecode_RejectsAnUnknownNullFlag() => + KeysetCursor.TryDecode(Encode("v1|2|V2lkZ2V0|NDI"), out _, out _).Should().BeFalse(); + + [Fact] + public void TryDecode_RejectsAnUndecodableSegment() => + KeysetCursor.TryDecode(Encode("v1|1|!!!!|NDI"), out _, out _).Should().BeFalse(); + + private static string Encode(string payload) => + System.Buffers.Text.Base64Url.EncodeToString(System.Text.Encoding.UTF8.GetBytes(payload)); +} diff --git a/Tests/Presentation/MMCA.Common.API.Tests/Controllers/EntityControllerBaseExportTests.cs b/Tests/Presentation/MMCA.Common.API.Tests/Controllers/EntityControllerBaseExportTests.cs index c5dad4b4..8f1be6d3 100644 --- a/Tests/Presentation/MMCA.Common.API.Tests/Controllers/EntityControllerBaseExportTests.cs +++ b/Tests/Presentation/MMCA.Common.API.Tests/Controllers/EntityControllerBaseExportTests.cs @@ -12,6 +12,7 @@ using MMCA.Common.Application.Interfaces; using MMCA.Common.Application.Settings; using MMCA.Common.Domain.Entities; +using MMCA.Common.Domain.Interfaces; using MMCA.Common.Domain.Specifications; using MMCA.Common.Shared.Abstractions; using MMCA.Common.Shared.DTOs; @@ -520,14 +521,14 @@ public sealed class SpecificationHonoringQueryService(IEnumerable ids) private readonly List _rows = [.. ids.Select(id => new ExportTestEntity { Id = id })]; /// Gets the specification passed to each page query, in call order. - public List?> SpecificationsSeen { get; } = []; + public List?> SpecificationsSeen { get; } = []; public IEntityDTOMapper DTOMapper => throw new NotSupportedException(); public Task>> GetAllAsync( bool includeFKs = false, bool includeChildren = false, - Specification? specification = null, + ISpecification? specification = null, Dictionary? filters = null, string? sortColumn = null, string? sortDirection = null, @@ -564,7 +565,7 @@ .. matching public Task>> GetAllAsync( bool includeFKs = false, bool includeChildren = false, - Specification? specification = null, + ISpecification? specification = null, string? fields = null, bool asTracking = false, CancellationToken cancellationToken = default) => throw new NotSupportedException(); @@ -580,7 +581,7 @@ public Task> GetEntityByIdAsync( string? idField = null, bool includeFKs = false, bool includeChildren = false, - Specification? specification = null, + ISpecification? specification = null, string? fields = null, bool asTracking = false, CancellationToken cancellationToken = default) => throw new NotSupportedException(); @@ -589,7 +590,7 @@ public Task> GetByIdAsync( int id, bool includeFKs = false, bool includeChildren = false, - Specification? specification = null, + ISpecification? specification = null, string? fields = null, bool asTracking = false, CancellationToken cancellationToken = default) => throw new NotSupportedException(); From 3e8b1d6d8337fec807c9868f1fdb653ca6715775 Mon Sep 17 00:00:00 2001 From: Ivan Ball-llovera Date: Tue, 18 Aug 2026 11:31:06 -0400 Subject: [PATCH 4/4] feat: namespace-cycle and CancellationToken fitness rules, public API surface gate (A8); drop unread RegisterFaultConsumers setting - ArchitectureRules.Cycles: signature-level namespace dependency-cycle detection per layer assembly (SCC-based, whole-component allowance check); one real Infrastructure cycle (root -> Settings -> Persistence -> root) exempted with per-edge justification rather than refactored. - ArchitectureRules.CancellationTokens: public Task-returning methods in Application/Infrastructure must declare a trailing cancellationToken; auto-exempts externally-fixed signatures; the two real findings (NotificationHub join/leave) are exempted as SignalR wire contracts but now pass Context.ConnectionAborted through. - Microsoft.CodeAnalysis.PublicApiAnalyzers 5.6.0 on all 14 in-slnx Source projects (UI.Maui excluded, documented); 5,070-declaration Shipped baseline = v1.152.0 surface, discipline starts at v1.153.0; RS0016/RS0017 at error, RS0026/RS0027/RS0041 off with reasons; analyzer-config baseline unchanged (compare script passes). - MessageBusSettings.RegisterFaultConsumers removed before it ever ships: the framework never read it; the per-event registerFaultConsumer parameter is the opt-out. EnableInbox docs corrected (table is part of the relational model; Cosmos skips it). - FACTS: 102 fitness methods / 34 bases / 87 executed by Common. - 3618 tests green, 0 warnings, pack succeeds. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RDQ4jE9QP6pC1ShZVG8ov5 --- .editorconfig | 27 + Directory.Build.props | 26 +- Directory.Packages.props | 5 + FACTS.md | 4 +- .../PublicAPI.Shipped.txt | 794 ++++++++++++++ .../PublicAPI.Unshipped.txt | 1 + .../packages.lock.json | 6 + .../MMCA.Common.Domain/PublicAPI.Shipped.txt | 255 +++++ .../PublicAPI.Unshipped.txt | 1 + .../MMCA.Common.Domain/packages.lock.json | 6 + .../Hubs/NotificationHub.cs | 12 +- .../PublicAPI.Shipped.txt | 844 +++++++++++++++ .../PublicAPI.Unshipped.txt | 1 + .../Services/FaultIntegrationEventConsumer.cs | 3 +- .../IntegrationEventConsumerExtensions.cs | 4 +- .../Settings/MessageBusSettings.cs | 22 +- .../packages.lock.json | 6 + .../MMCA.Common.Shared/PublicAPI.Shipped.txt | 718 +++++++++++++ .../PublicAPI.Unshipped.txt | 1 + .../MMCA.Common.Shared/packages.lock.json | 6 + .../PublicAPI.Shipped.txt | 23 + .../PublicAPI.Unshipped.txt | 1 + .../packages.lock.json | 6 + .../MMCA.Common.Aspire/PublicAPI.Shipped.txt | 102 ++ .../PublicAPI.Unshipped.txt | 1 + .../MMCA.Common.Aspire/packages.lock.json | 6 + .../ArchitectureRules.CancellationTokens.cs | 190 ++++ .../ArchitectureRules.Cycles.cs | 379 +++++++ .../CancellationTokenConventionTestsBase.cs | 31 + .../Bases/NamespaceCycleTestsBase.cs | 31 + .../PublicAPI.Shipped.txt | 390 +++++++ .../PublicAPI.Unshipped.txt | 1 + .../packages.lock.json | 6 + .../PublicAPI.Shipped.txt | 224 ++++ .../PublicAPI.Unshipped.txt | 1 + .../packages.lock.json | 6 + .../PublicAPI.Shipped.txt | 110 ++ .../PublicAPI.Unshipped.txt | 1 + .../MMCA.Common.Testing.UI/packages.lock.json | 6 + .../MMCA.Common.Testing/PublicAPI.Shipped.txt | 126 +++ .../PublicAPI.Unshipped.txt | 1 + .../MMCA.Common.Testing/packages.lock.json | 6 + .../MMCA.Common.API/PublicAPI.Shipped.txt | 449 ++++++++ .../MMCA.Common.API/PublicAPI.Unshipped.txt | 1 + .../MMCA.Common.API/packages.lock.json | 6 + .../MMCA.Common.Grpc/PublicAPI.Shipped.txt | 39 + .../MMCA.Common.Grpc/PublicAPI.Unshipped.txt | 1 + .../MMCA.Common.Grpc/packages.lock.json | 6 + .../MMCA.Common.UI.Web/PublicAPI.Shipped.txt | 26 + .../PublicAPI.Unshipped.txt | 1 + .../MMCA.Common.UI.Web/packages.lock.json | 6 + .../MMCA.Common.UI/PublicAPI.Shipped.txt | 982 ++++++++++++++++++ .../MMCA.Common.UI/PublicAPI.Unshipped.txt | 1 + .../MMCA.Common.UI/packages.lock.json | 6 + .../CancellationTokenFixtures.cs | 77 ++ .../CancellationTokenConventionTests.cs | 28 + .../CancellationTokenFitnessTests.cs | 70 ++ .../CycleFixtures/Acyclic/AcyclicFixtures.cs | 10 + .../CycleFixtures/Left/LeftFixtures.cs | 17 + .../CycleFixtures/Right/RightFixtures.cs | 10 + .../NamespaceCycleFitnessTests.cs | 65 ++ .../NamespaceCycleTests.cs | 45 + .../Settings/SettingsTests.cs | 8 - 63 files changed, 6207 insertions(+), 37 deletions(-) create mode 100644 Source/Core/MMCA.Common.Application/PublicAPI.Shipped.txt create mode 100644 Source/Core/MMCA.Common.Application/PublicAPI.Unshipped.txt create mode 100644 Source/Core/MMCA.Common.Domain/PublicAPI.Shipped.txt create mode 100644 Source/Core/MMCA.Common.Domain/PublicAPI.Unshipped.txt create mode 100644 Source/Core/MMCA.Common.Infrastructure/PublicAPI.Shipped.txt create mode 100644 Source/Core/MMCA.Common.Infrastructure/PublicAPI.Unshipped.txt create mode 100644 Source/Core/MMCA.Common.Shared/PublicAPI.Shipped.txt create mode 100644 Source/Core/MMCA.Common.Shared/PublicAPI.Unshipped.txt create mode 100644 Source/Hosting/MMCA.Common.Aspire.Hosting/PublicAPI.Shipped.txt create mode 100644 Source/Hosting/MMCA.Common.Aspire.Hosting/PublicAPI.Unshipped.txt create mode 100644 Source/Hosting/MMCA.Common.Aspire/PublicAPI.Shipped.txt create mode 100644 Source/Hosting/MMCA.Common.Aspire/PublicAPI.Unshipped.txt create mode 100644 Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureRules.CancellationTokens.cs create mode 100644 Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureRules.Cycles.cs create mode 100644 Source/Hosting/MMCA.Common.Testing.Architecture/Bases/CancellationTokenConventionTestsBase.cs create mode 100644 Source/Hosting/MMCA.Common.Testing.Architecture/Bases/NamespaceCycleTestsBase.cs create mode 100644 Source/Hosting/MMCA.Common.Testing.Architecture/PublicAPI.Shipped.txt create mode 100644 Source/Hosting/MMCA.Common.Testing.Architecture/PublicAPI.Unshipped.txt create mode 100644 Source/Hosting/MMCA.Common.Testing.E2E/PublicAPI.Shipped.txt create mode 100644 Source/Hosting/MMCA.Common.Testing.E2E/PublicAPI.Unshipped.txt create mode 100644 Source/Hosting/MMCA.Common.Testing.UI/PublicAPI.Shipped.txt create mode 100644 Source/Hosting/MMCA.Common.Testing.UI/PublicAPI.Unshipped.txt create mode 100644 Source/Hosting/MMCA.Common.Testing/PublicAPI.Shipped.txt create mode 100644 Source/Hosting/MMCA.Common.Testing/PublicAPI.Unshipped.txt create mode 100644 Source/Presentation/MMCA.Common.API/PublicAPI.Shipped.txt create mode 100644 Source/Presentation/MMCA.Common.API/PublicAPI.Unshipped.txt create mode 100644 Source/Presentation/MMCA.Common.Grpc/PublicAPI.Shipped.txt create mode 100644 Source/Presentation/MMCA.Common.Grpc/PublicAPI.Unshipped.txt create mode 100644 Source/Presentation/MMCA.Common.UI.Web/PublicAPI.Shipped.txt create mode 100644 Source/Presentation/MMCA.Common.UI.Web/PublicAPI.Unshipped.txt create mode 100644 Source/Presentation/MMCA.Common.UI/PublicAPI.Shipped.txt create mode 100644 Source/Presentation/MMCA.Common.UI/PublicAPI.Unshipped.txt create mode 100644 Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs create mode 100644 Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationTokenConventionTests.cs create mode 100644 Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationTokenFitnessTests.cs create mode 100644 Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Acyclic/AcyclicFixtures.cs create mode 100644 Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Left/LeftFixtures.cs create mode 100644 Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Right/RightFixtures.cs create mode 100644 Tests/Architecture/MMCA.Common.Architecture.Tests/NamespaceCycleFitnessTests.cs create mode 100644 Tests/Architecture/MMCA.Common.Architecture.Tests/NamespaceCycleTests.cs diff --git a/.editorconfig b/.editorconfig index 5844bf9f..c9ab886d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -874,3 +874,30 @@ dotnet_diagnostic.IDE0130.severity = none [Tests/Presentation/MMCA.Common.API.Tests/Fakes/**.cs] dotnet_diagnostic.IDE0130.severity = none + +[*.cs] +# ───────────────────────────────────────────────────────────────────────────── +# Microsoft.CodeAnalysis.PublicApiAnalyzers (RS rules): overrides +# The public API surface gate is Source-only (see Directory.Build.props); the two rules that make it +# a gate (RS0016 for a public member missing from PublicAPI.Shipped.txt, RS0017 for a declared member +# that disappeared) stay at the global error severity. The rest are turned off deliberately. +# ───────────────────────────────────────────────────────────────────────────── + +# RS0026/RS0027: "do not add multiple public overloads with optional parameters". Sound advice for a +# NEW API, but the v1.152.0 surface being baselined already ships those pairs (the repository +# read/query methods above all), and obeying the rule now would mean a breaking signature change on +# every consumer. Off rather than silently baselined as a lie. +dotnet_diagnostic.RS0026.severity = none +dotnet_diagnostic.RS0027.severity = none +# RS0041: "public members should not use oblivious reference types". Every hit is inside Razor +# generated code (BuildRenderTree and friends), which is not nullable-annotated and is not ours to +# annotate; the rule cannot be satisfied from source. +dotnet_diagnostic.RS0041.severity = none +# RS0051-RS0056: the INTERNAL-API analog of the same analyzer (InternalAPI.Shipped.txt). Only the +# public, packaged surface is under contract here, so internal API tracking is not adopted. +dotnet_diagnostic.RS0051.severity = none +dotnet_diagnostic.RS0052.severity = none +dotnet_diagnostic.RS0053.severity = none +dotnet_diagnostic.RS0054.severity = none +dotnet_diagnostic.RS0055.severity = none +dotnet_diagnostic.RS0056.severity = none diff --git a/Directory.Build.props b/Directory.Build.props index a82ad157..b99019bd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -19,7 +19,12 @@ operator immediately fails the build with CS8625/CS8604. Suppressed HERE rather than in .editorconfig because a `dotnet_diagnostic` severity does not reach Razor-generated code. The compiler is the authority on nullability, not the analyzer. --> - $(NoWarn);CS1591;RMG020;S8970 + + $(NoWarn);CS1591;RMG020;S8970;RS0041 @@ -60,6 +65,25 @@ + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all diff --git a/Directory.Packages.props b/Directory.Packages.props index 23ef5469..d20c544c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -157,6 +157,11 @@ + + diff --git a/FACTS.md b/FACTS.md index 0721f6c0..3871462f 100644 --- a/FACTS.md +++ b/FACTS.md @@ -41,10 +41,10 @@ The ADRs live in the Website repo (`docs-src/adr/`), published at it owns the range/count and the one-line summaries. Do not restate the `(001-NNN)` range elsewhere. ## Architecture fitness functions -- **100 test methods across 32 abstract `*TestsBase` classes**, shipped once in the +- **102 test methods across 34 abstract `*TestsBase` classes**, shipped once in the `MMCA.Common.Testing.Architecture` package (ADR-015) and re-run as thin subclasses across all consuming repos (Common, ADC, Store). -- MMCA.Common's own build executes **79** of them (the methods of the bases its arch-tests +- MMCA.Common's own build executes **87** of them (the methods of the bases its arch-tests subclass, plus its Common-only direct tests, e.g. `FrameworkSanityTests`/`SpecificationFitnessTests`). ## Governance rubric diff --git a/Source/Core/MMCA.Common.Application/PublicAPI.Shipped.txt b/Source/Core/MMCA.Common.Application/PublicAPI.Shipped.txt new file mode 100644 index 00000000..b7d0f531 --- /dev/null +++ b/Source/Core/MMCA.Common.Application/PublicAPI.Shipped.txt @@ -0,0 +1,794 @@ +#nullable enable +MMCA.Common.Application.AssemblyReference +MMCA.Common.Application.Auditing.AuditTrailEntryDTO +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.$() -> MMCA.Common.Application.Auditing.AuditTrailEntryDTO! +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.AuditTrailEntryDTO() -> void +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.ChangedBy.get -> int? +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.ChangedBy.init -> void +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.ChangedOn.get -> System.DateTime +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.ChangedOn.init -> void +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.CorrelationId.get -> string? +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.CorrelationId.init -> void +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.EntityKey.get -> string! +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.EntityKey.init -> void +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.EntityType.get -> string! +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.EntityType.init -> void +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.Equals(MMCA.Common.Application.Auditing.AuditTrailEntryDTO? other) -> bool +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.Id.get -> System.Guid +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.Id.init -> void +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.NewValue.get -> string? +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.NewValue.init -> void +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.OldValue.get -> string? +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.OldValue.init -> void +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.Operation.get -> string! +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.Operation.init -> void +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.PropertyName.get -> string? +MMCA.Common.Application.Auditing.AuditTrailEntryDTO.PropertyName.init -> void +MMCA.Common.Application.Auth.AuthenticationServiceBase +MMCA.Common.Application.Auth.AuthenticationServiceBase.AuthenticationServiceBase(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, MMCA.Common.Application.Interfaces.Infrastructure.ITokenService! tokenService, MMCA.Common.Application.Interfaces.Infrastructure.IPasswordHasher! passwordHasher, MMCA.Common.Application.Auth.ILoginProtectionService! loginProtection, System.TimeProvider! timeProvider, MMCA.Common.Application.Auth.AuthenticationValidators! validators) -> void +MMCA.Common.Application.Auth.AuthenticationServiceBase.IssueTokensAsync(TUser! user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Auth.AuthenticationServiceBase.LoginAsync(MMCA.Common.Shared.Auth.LoginRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Auth.AuthenticationServiceBase.RefreshTokenAsync(MMCA.Common.Shared.Auth.RefreshTokenRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Auth.AuthenticationServiceBase.RegisterAsync(MMCA.Common.Shared.Auth.RegisterRequest request, string? ipAddress = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Auth.AuthenticationServiceBase.Repository.get -> MMCA.Common.Application.Interfaces.Infrastructure.IRepository! +MMCA.Common.Application.Auth.AuthenticationServiceBase.RevokeTokenAsync(int userId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Auth.AuthenticationServiceBase.TimeProvider.get -> System.TimeProvider! +MMCA.Common.Application.Auth.AuthenticationServiceBase.TokenService.get -> MMCA.Common.Application.Interfaces.Infrastructure.ITokenService! +MMCA.Common.Application.Auth.AuthenticationServiceBase.UnitOfWork.get -> MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! +MMCA.Common.Application.Auth.AuthenticationValidators +MMCA.Common.Application.Auth.AuthenticationValidators.AuthenticationValidators(FluentValidation.IValidator! login, FluentValidation.IValidator! register, FluentValidation.IValidator! refresh) -> void +MMCA.Common.Application.Auth.AuthenticationValidators.Login.get -> FluentValidation.IValidator! +MMCA.Common.Application.Auth.AuthenticationValidators.Refresh.get -> FluentValidation.IValidator! +MMCA.Common.Application.Auth.AuthenticationValidators.Register.get -> FluentValidation.IValidator! +MMCA.Common.Application.Auth.IAuthenticationService +MMCA.Common.Application.Auth.IAuthenticationService.ExternalLoginAsync(string! loginProvider, string! providerKey, string! email, string! firstName, string! lastName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Auth.IAuthenticationService.LoginAsync(MMCA.Common.Shared.Auth.LoginRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Auth.IAuthenticationService.RefreshTokenAsync(MMCA.Common.Shared.Auth.RefreshTokenRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Auth.IAuthenticationService.RegisterAsync(MMCA.Common.Shared.Auth.RegisterRequest request, string? ipAddress = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Auth.IAuthenticationService.RevokeTokenAsync(int userId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Auth.ILoginProtectionService +MMCA.Common.Application.Auth.ILoginProtectionService.CheckLockoutAsync(string! email, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Auth.ILoginProtectionService.CheckRegistrationRateLimitAsync(string? ipAddress, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Auth.ILoginProtectionService.IncrementFailedAttemptsAsync(string! email, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Auth.ILoginProtectionService.IncrementRegistrationCountAsync(string? ipAddress, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Auth.ILoginProtectionService.ResetFailedAttemptsAsync(string! email, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Auth.SoftDeletedUserCache +MMCA.Common.Application.Auth.Validation.LoginRequestValidator +MMCA.Common.Application.Auth.Validation.LoginRequestValidator.LoginRequestValidator() -> void +MMCA.Common.Application.Auth.Validation.RefreshTokenRequestValidator +MMCA.Common.Application.Auth.Validation.RefreshTokenRequestValidator.RefreshTokenRequestValidator() -> void +MMCA.Common.Application.ClassReference +MMCA.Common.Application.ClassReference.ClassReference() -> void +MMCA.Common.Application.DependencyInjection +MMCA.Common.Application.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!) +MMCA.Common.Application.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddApplication() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Application.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddApplicationDecorators() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Application.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddApplicationProfiling() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Application.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddUserDataExportSection() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Application.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).ScanModuleApplicationServices() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Application.DomainEvents.SafeDomainEventHandler +MMCA.Common.Application.DomainEvents.SafeDomainEventHandler.HandleAsync(TDomainEvent! domainEvent, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.DomainEvents.SafeDomainEventHandler.SafeDomainEventHandler(Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.Application.Extensions.ReadRepositoryExtensions +MMCA.Common.Application.Extensions.ReadRepositoryExtensions.extension(MMCA.Common.Application.Interfaces.Infrastructure.IReadRepository!) +MMCA.Common.Application.Extensions.ReadRepositoryExtensions.extension(MMCA.Common.Application.Interfaces.Infrastructure.IReadRepository!).GetByIdOrFailAsync(TIdentifierType id, string! source, System.Collections.Generic.IEnumerable? includes = null, bool asTracking = true, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Extensions.ValidationFailureExtensions +MMCA.Common.Application.Extensions.ValidationFailureExtensions.extension(FluentValidation.Results.ValidationResult!) +MMCA.Common.Application.Extensions.ValidationFailureExtensions.extension(FluentValidation.Results.ValidationResult!).ToErrors(string! source) -> System.Collections.Generic.IEnumerable! +MMCA.Common.Application.Interfaces.IAuditTrailReader +MMCA.Common.Application.Interfaces.IAuditTrailReader.GetForEntityAsync(string! entityType, string! entityKey, int page = 1, int pageSize = 50, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Interfaces.ICacheService +MMCA.Common.Application.Interfaces.ICacheService.GetAsync(string! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.ICacheService.GetOrCreateAsync(string! key, System.Func!>! factory, System.TimeSpan? expiration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.ICacheService.IncrementAsync(string! key, System.TimeSpan expiration, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.ICacheService.RemoveAsync(string! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.ICacheService.RemoveByPrefixAsync(string! prefix, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.ICacheService.SetAsync(string! key, T value, System.TimeSpan? expiration = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.ICorrelationContext +MMCA.Common.Application.Interfaces.ICorrelationContext.CorrelationId.get -> string! +MMCA.Common.Application.Interfaces.ICorrelationContext.SetCorrelationId(string! correlationId) -> void +MMCA.Common.Application.Interfaces.ICreateRequest +MMCA.Common.Application.Interfaces.IDistributedLock +MMCA.Common.Application.Interfaces.IDistributedLock.TryAcquireAsync(string! key, System.TimeSpan ttl, System.TimeSpan wait, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.IDomainEventDispatcher +MMCA.Common.Application.Interfaces.IDomainEventDispatcher.DispatchAsync(System.Collections.Generic.IEnumerable! domainEvents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.IDomainEventHandler +MMCA.Common.Application.Interfaces.IDomainEventHandler.HandleAsync(TDomainEvent domainEvent, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.IEntityDTOMapper +MMCA.Common.Application.Interfaces.IEntityDTOMapper.MapToDTO(TEntity! entity) -> TEntityDTO +MMCA.Common.Application.Interfaces.IEntityDTOMapper.MapToDTOs(System.Collections.Generic.IReadOnlyCollection! entityCollection) -> System.Collections.Generic.IReadOnlyCollection! +MMCA.Common.Application.Interfaces.IEntityDTOProjector +MMCA.Common.Application.Interfaces.IEntityDTOProjector.ProjectTo(System.Linq.IQueryable! source) -> System.Linq.IQueryable! +MMCA.Common.Application.Interfaces.IEntityQueryService +MMCA.Common.Application.Interfaces.IEntityQueryService.DTOMapper.get -> MMCA.Common.Application.Interfaces.IEntityDTOMapper! +MMCA.Common.Application.Interfaces.IEntityQueryService.ExistsAsync(System.Linq.Expressions.Expression!>! where, bool ignoreQueryFilters = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.IEntityQueryService.GetAllAsync(bool includeFKs = false, bool includeChildren = false, MMCA.Common.Domain.Interfaces.ISpecification? specification = null, System.Collections.Generic.Dictionary? filters = null, string? sortColumn = null, string? sortDirection = null, string? fields = null, int? pageNumber = null, int? pageSize = null, bool asTracking = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>! +MMCA.Common.Application.Interfaces.IEntityQueryService.GetAllAsync(bool includeFKs = false, bool includeChildren = false, MMCA.Common.Domain.Interfaces.ISpecification? specification = null, string? fields = null, bool asTracking = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>! +MMCA.Common.Application.Interfaces.IEntityQueryService.GetAllForLookupAsync(string! nameProperty, System.Linq.Expressions.Expression!>? where = null, bool asTracking = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>!>! +MMCA.Common.Application.Interfaces.IEntityQueryService.GetByIdAsync(TIdentifierType id, bool includeFKs = false, bool includeChildren = false, MMCA.Common.Domain.Interfaces.ISpecification? specification = null, string? fields = null, bool asTracking = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Interfaces.IEntityQueryService.GetEntityByIdAsync(string! idValue, string? idField = null, bool includeFKs = false, bool includeChildren = false, MMCA.Common.Domain.Interfaces.ISpecification? specification = null, string? fields = null, bool asTracking = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Interfaces.IEntityRequestMapper +MMCA.Common.Application.Interfaces.IEntityRequestMapper.CreateEntityAsync(TCreateRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Interfaces.IEventBus +MMCA.Common.Application.Interfaces.IEventBus.PublishAsync(MMCA.Common.Domain.Interfaces.IIntegrationEvent! integrationEvent, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.IEventBus.PublishAsync(System.Collections.Generic.IEnumerable! integrationEvents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.IIntegrationEventHandler +MMCA.Common.Application.Interfaces.IIntegrationEventHandler.HandleAsync(TIntegrationEvent integrationEvent, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.INavigationMetadata +MMCA.Common.Application.Interfaces.INavigationMetadata.SupportedIncludes.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Application.Interfaces.INavigationMetadata.UnsupportedIncludes.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Application.Interfaces.INavigationPopulator +MMCA.Common.Application.Interfaces.INavigationPopulator.PopulateAsync(System.Collections.Generic.IReadOnlyCollection! entities, MMCA.Common.Application.Interfaces.NavigationMetadata! navigationMetadata, bool includeFKs, bool includeChildren, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.IScheduledJob +MMCA.Common.Application.Interfaces.IScheduledJob.CronExpression.get -> string! +MMCA.Common.Application.Interfaces.IScheduledJob.ExecuteAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.IScheduledJob.Name.get -> string! +MMCA.Common.Application.Interfaces.ITenantContext +MMCA.Common.Application.Interfaces.ITenantContext.IsResolved.get -> bool +MMCA.Common.Application.Interfaces.ITenantContext.SetTenant(string! tenantId) -> void +MMCA.Common.Application.Interfaces.ITenantContext.TenantId.get -> string? +MMCA.Common.Application.Interfaces.Infrastructure.DataSource +MMCA.Common.Application.Interfaces.Infrastructure.DataSource.CosmosDB = 0 -> MMCA.Common.Application.Interfaces.Infrastructure.DataSource +MMCA.Common.Application.Interfaces.Infrastructure.DataSource.SQLServer = 2 -> MMCA.Common.Application.Interfaces.Infrastructure.DataSource +MMCA.Common.Application.Interfaces.Infrastructure.DataSource.Sqlite = 1 -> MMCA.Common.Application.Interfaces.Infrastructure.DataSource +MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey +MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey.DataSourceKey() -> void +MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey.DataSourceKey(MMCA.Common.Application.Interfaces.Infrastructure.DataSource Engine, string! Name) -> void +MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey.Deconstruct(out MMCA.Common.Application.Interfaces.Infrastructure.DataSource Engine, out string! Name) -> void +MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey.Engine.get -> MMCA.Common.Application.Interfaces.Infrastructure.DataSource +MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey.Engine.init -> void +MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey.Equals(MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey other) -> bool +MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey.Name.get -> string! +MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey.Name.init -> void +MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService +MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService.GetClaimValue(string! claimType) -> T? +MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService.IsInRole(string! roleName) -> bool +MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService.Role.get -> string? +MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService.Roles.get -> System.Collections.Generic.IEnumerable! +MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService.User.get -> System.Security.Claims.ClaimsPrincipal! +MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService.UserId.get -> int? +MMCA.Common.Application.Interfaces.Infrastructure.IDataSourceService +MMCA.Common.Application.Interfaces.Infrastructure.IDataSourceService.GetDataSource(System.Type! entityType) -> MMCA.Common.Application.Interfaces.Infrastructure.DataSource +MMCA.Common.Application.Interfaces.Infrastructure.IDataSourceService.GetDataSource(string! entityFullName) -> MMCA.Common.Application.Interfaces.Infrastructure.DataSource +MMCA.Common.Application.Interfaces.Infrastructure.IDataSourceService.GetDataSourceKey(System.Type! entityType) -> MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey +MMCA.Common.Application.Interfaces.Infrastructure.IDataSourceService.GetDataSourceKey(string! entityFullName) -> MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey +MMCA.Common.Application.Interfaces.Infrastructure.IDataSourceService.HaveIncludeSupport(MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey first, MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey second) -> bool +MMCA.Common.Application.Interfaces.Infrastructure.IDataSourceService.HaveIncludeSupport(string! firstEntityFullName, string! secondEntityFullName) -> bool +MMCA.Common.Application.Interfaces.Infrastructure.IEmailSender +MMCA.Common.Application.Interfaces.Infrastructure.IEmailSender.SendAsync(string! subject, string! body, bool isHtml = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IEmailSender.SendAsync(string! to, string! subject, string! body, bool isHtml = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IEntityConfigurationAssemblyProvider +MMCA.Common.Application.Interfaces.Infrastructure.IEntityConfigurationAssemblyProvider.GetConfigurationAssemblies() -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Application.Interfaces.Infrastructure.IEntityQuerier +MMCA.Common.Application.Interfaces.Infrastructure.IEntityQuerier.AnyAsync(MMCA.Common.Domain.Interfaces.ISpecification! specification, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IEntityQuerier.CountAsync(MMCA.Common.Domain.Interfaces.ISpecification! specification, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IEntityQuerier.CountAsync(System.Linq.Expressions.Expression!>! where, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IEntityQuerier.CountAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IEntityQuerier.GetAllAsync(System.Collections.Generic.IEnumerable! includes, System.Linq.Expressions.Expression!>? where = null, System.Linq.Expressions.Expression!>? orderBy = null, System.Linq.Expressions.Expression!>? select = null, bool asTracking = false, bool ignoreQueryFilters = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Interfaces.Infrastructure.IEntityQuerier.GetAllForLookupAsync(string! nameProperty, System.Linq.Expressions.Expression!>? where = null, bool asTracking = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>! +MMCA.Common.Application.Interfaces.Infrastructure.IEntityQuerier.GetPageByCursorAsync(MMCA.Common.Shared.Abstractions.KeysetPageRequest! request, MMCA.Common.Domain.Interfaces.ISpecification? specification = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>! +MMCA.Common.Application.Interfaces.Infrastructure.IEntityQuerier.GetProjectedAsync(System.Linq.Expressions.Expression!>! select, System.Linq.Expressions.Expression!>? where = null, bool asTracking = false, bool ignoreQueryFilters = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Interfaces.Infrastructure.IEntityQuerier.ListAsync(MMCA.Common.Domain.Interfaces.ISpecification! specification, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Interfaces.Infrastructure.IEntityQuerier.ListAsync(MMCA.Common.Domain.Interfaces.ISpecification! specification, System.Linq.Expressions.Expression!>! select, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Interfaces.Infrastructure.IEntityReader +MMCA.Common.Application.Interfaces.Infrastructure.IEntityReader.ExistsAsync(System.Linq.Expressions.Expression!>! where, bool ignoreQueryFilters = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IEntityReader.ExistsAsync(TIdentifierType id, bool ignoreQueryFilters = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IEntityReader.GetByIdAsync(TIdentifierType id, System.Collections.Generic.IEnumerable! includes, bool asTracking = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IEntityReader.GetByIdAsync(TIdentifierType id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IEntityReader.GetByIdsAsync(System.Collections.Generic.IEnumerable! ids, System.Collections.Generic.IEnumerable? includes = null, bool asTracking = false, bool ignoreQueryFilters = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Interfaces.Infrastructure.IFileStorageService +MMCA.Common.Application.Interfaces.Infrastructure.IFileStorageService.DeleteAsync(string! blobName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IFileStorageService.IsConfigured.get -> bool +MMCA.Common.Application.Interfaces.Infrastructure.IFileStorageService.UploadAsync(string! blobName, System.IO.Stream! content, string! contentType, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Interfaces.Infrastructure.IImageProcessor +MMCA.Common.Application.Interfaces.Infrastructure.IImageProcessor.NormalizeToSquareJpegAsync(System.IO.Stream! content, int size, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Interfaces.Infrastructure.ILiveChannelPublisher +MMCA.Common.Application.Interfaces.Infrastructure.ILiveChannelPublisher.PublishAsync(string! channelKey, string! eventName, string! payloadJson, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.INativePushSender +MMCA.Common.Application.Interfaces.Infrastructure.INativePushSender.BroadcastAsync(string! title, string! body, System.Collections.Generic.Dictionary? metadata = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.INativePushSender.SendToUsersAsync(System.Collections.Generic.IEnumerable! userIds, string! title, string! body, System.Collections.Generic.Dictionary? metadata = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.INotificationRecipientProvider +MMCA.Common.Application.Interfaces.Infrastructure.INotificationRecipientProvider.GetRecipientUserIdsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Interfaces.Infrastructure.IPasswordHasher +MMCA.Common.Application.Interfaces.Infrastructure.IPasswordHasher.HashPassword(string! password) -> (byte[]! Hash, byte[]! Salt) +MMCA.Common.Application.Interfaces.Infrastructure.IPasswordHasher.VerifyPassword(string! password, byte[]! hash, byte[]! salt) -> bool +MMCA.Common.Application.Interfaces.Infrastructure.IPushDeviceRegistrar +MMCA.Common.Application.Interfaces.Infrastructure.IPushDeviceRegistrar.DeleteAsync(int userId, string! installationId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IPushDeviceRegistrar.DeleteAsync(string! installationId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IPushDeviceRegistrar.UpsertAsync(int userId, MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IPushNotificationSender +MMCA.Common.Application.Interfaces.Infrastructure.IPushNotificationSender.BroadcastAsync(string! title, string! body, System.Collections.Generic.Dictionary? metadata = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IPushNotificationSender.SendToUserAsync(int userId, string! title, string! body, System.Collections.Generic.Dictionary? metadata = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IPushNotificationSender.SendToUsersAsync(System.Collections.Generic.IEnumerable! userIds, string! title, string! body, System.Collections.Generic.Dictionary? metadata = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IQueryableExecutor +MMCA.Common.Application.Interfaces.Infrastructure.IQueryableExecutor.AsSplitQuery(System.Linq.IQueryable! query) -> System.Linq.IQueryable! +MMCA.Common.Application.Interfaces.Infrastructure.IQueryableExecutor.CountAsync(System.Linq.IQueryable! query, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IQueryableExecutor.Include(System.Linq.IQueryable! query, string! navigationPropertyPath) -> System.Linq.IQueryable! +MMCA.Common.Application.Interfaces.Infrastructure.IQueryableExecutor.ToListAsync(System.Linq.IQueryable! query, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Interfaces.Infrastructure.IReadRepository +MMCA.Common.Application.Interfaces.Infrastructure.IReadRepository.Table.get -> System.Linq.IQueryable! +MMCA.Common.Application.Interfaces.Infrastructure.IReadRepository.TableNoTracking.get -> System.Linq.IQueryable! +MMCA.Common.Application.Interfaces.Infrastructure.IReadRepository.TableNoTrackingSingleQuery.get -> System.Linq.IQueryable! +MMCA.Common.Application.Interfaces.Infrastructure.IReadRepository.TableNoTrackingSplitQuery.get -> System.Linq.IQueryable! +MMCA.Common.Application.Interfaces.Infrastructure.IRepository +MMCA.Common.Application.Interfaces.Infrastructure.ISoftDeletedUserValidator +MMCA.Common.Application.Interfaces.Infrastructure.ISoftDeletedUserValidator.IsUserSoftDeletedAsync(int userId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.ITokenService +MMCA.Common.Application.Interfaces.Infrastructure.ITokenService.AccessTokenLifetime.get -> System.TimeSpan +MMCA.Common.Application.Interfaces.Infrastructure.ITokenService.GenerateAccessToken(int userId, string! email, string! role, string! fullName, System.Collections.Generic.IEnumerable? additionalClaims = null) -> string! +MMCA.Common.Application.Interfaces.Infrastructure.ITokenService.GenerateRefreshToken() -> string! +MMCA.Common.Application.Interfaces.Infrastructure.ITokenService.GetPrincipalFromExpiredToken(string! token) -> System.Security.Claims.ClaimsPrincipal? +MMCA.Common.Application.Interfaces.Infrastructure.ITokenService.RefreshTokenLifetime.get -> System.TimeSpan +MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork +MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork.BeginTransaction() -> void +MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork.CommitTransaction() -> void +MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork.ExecuteInTransactionAsync(System.Func!>! operation, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork.GetReadRepository() -> MMCA.Common.Application.Interfaces.Infrastructure.IReadRepository! +MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork.GetRepository() -> MMCA.Common.Application.Interfaces.Infrastructure.IRepository! +MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork.RequestIdentityInsert() -> void +MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork.RollbackTransaction() -> void +MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork.Save() -> int +MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork.SaveChangesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IUpdatePropertySetter +MMCA.Common.Application.Interfaces.Infrastructure.IUpdatePropertySetter.Set(System.Linq.Expressions.Expression!>! property, System.Linq.Expressions.Expression!>! valueFactory) -> MMCA.Common.Application.Interfaces.Infrastructure.IUpdatePropertySetter! +MMCA.Common.Application.Interfaces.Infrastructure.IUpdatePropertySetter.Set(System.Linq.Expressions.Expression!>! property, TProperty value) -> MMCA.Common.Application.Interfaces.Infrastructure.IUpdatePropertySetter! +MMCA.Common.Application.Interfaces.Infrastructure.IWriteRepository +MMCA.Common.Application.Interfaces.Infrastructure.IWriteRepository.AddAsync(TEntity! entity, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IWriteRepository.AddRangeAsync(System.Collections.Generic.IEnumerable! entities, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IWriteRepository.ExecuteDeleteAsync(System.Linq.Expressions.Expression!>! where, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IWriteRepository.ExecuteUpdateAsync(System.Linq.Expressions.Expression!>! where, System.Action!>! setProperties, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IWriteRepository.Save() -> int +MMCA.Common.Application.Interfaces.Infrastructure.IWriteRepository.SaveChangesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IWriteRepository.SetOriginalRowVersion(MMCA.Common.Domain.Interfaces.IRowVersioned! childEntity, byte[]? rowVersion) -> void +MMCA.Common.Application.Interfaces.Infrastructure.IWriteRepository.SetOriginalRowVersion(TEntity! entity, byte[]? rowVersion) -> void +MMCA.Common.Application.Interfaces.Infrastructure.IWriteRepository.UpdateAsync(TEntity! entity, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Interfaces.Infrastructure.IWriteRepository.UpdateRange(System.Collections.Generic.IEnumerable! entities) -> void +MMCA.Common.Application.Interfaces.Infrastructure.ImageContentSniffer +MMCA.Common.Application.Interfaces.Infrastructure.NullNotificationRecipientProvider +MMCA.Common.Application.Interfaces.Infrastructure.NullNotificationRecipientProvider.GetRecipientUserIdsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Interfaces.Infrastructure.NullNotificationRecipientProvider.NullNotificationRecipientProvider() -> void +MMCA.Common.Application.Interfaces.NavigationMetadata +MMCA.Common.Application.Interfaces.NavigationMetadata.NavigationMetadata() -> void +MMCA.Common.Application.Interfaces.NavigationMetadata.SupportedIncludes.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Application.Interfaces.NavigationMetadata.UnsupportedIncludes.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Application.Interfaces.NavigationPropertyInfo +MMCA.Common.Application.Interfaces.NavigationPropertyInfo.$() -> MMCA.Common.Application.Interfaces.NavigationPropertyInfo! +MMCA.Common.Application.Interfaces.NavigationPropertyInfo.DeclaringEntityType.get -> System.Type! +MMCA.Common.Application.Interfaces.NavigationPropertyInfo.DeclaringEntityType.init -> void +MMCA.Common.Application.Interfaces.NavigationPropertyInfo.Deconstruct(out string! PropertyName, out MMCA.Common.Application.Interfaces.NavigationType Type, out System.Type! DeclaringEntityType, out System.Type! TargetEntityType) -> void +MMCA.Common.Application.Interfaces.NavigationPropertyInfo.Equals(MMCA.Common.Application.Interfaces.NavigationPropertyInfo? other) -> bool +MMCA.Common.Application.Interfaces.NavigationPropertyInfo.NavigationPropertyInfo(string! PropertyName, MMCA.Common.Application.Interfaces.NavigationType Type, System.Type! DeclaringEntityType, System.Type! TargetEntityType) -> void +MMCA.Common.Application.Interfaces.NavigationPropertyInfo.PropertyName.get -> string! +MMCA.Common.Application.Interfaces.NavigationPropertyInfo.PropertyName.init -> void +MMCA.Common.Application.Interfaces.NavigationPropertyInfo.TargetEntityType.get -> System.Type! +MMCA.Common.Application.Interfaces.NavigationPropertyInfo.TargetEntityType.init -> void +MMCA.Common.Application.Interfaces.NavigationPropertyInfo.Type.get -> MMCA.Common.Application.Interfaces.NavigationType +MMCA.Common.Application.Interfaces.NavigationPropertyInfo.Type.init -> void +MMCA.Common.Application.Interfaces.NavigationType +MMCA.Common.Application.Interfaces.NavigationType.ChildCollection = 1 -> MMCA.Common.Application.Interfaces.NavigationType +MMCA.Common.Application.Interfaces.NavigationType.ForeignKey = 0 -> MMCA.Common.Application.Interfaces.NavigationType +MMCA.Common.Application.Messaging.IMessageBus +MMCA.Common.Application.Messaging.IMessageBus.PublishAsync(MMCA.Common.Domain.Interfaces.IIntegrationEvent! integrationEvent, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Messaging.IMessageBus.PublishAsync(System.Collections.Generic.IEnumerable! integrationEvents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Modules.IModule +MMCA.Common.Application.Modules.IModule.Dependencies.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Application.Modules.IModule.Name.get -> string! +MMCA.Common.Application.Modules.IModule.Register(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationBuilder! configuration, MMCA.Common.Application.Settings.ApplicationSettings! applicationSettings) -> void +MMCA.Common.Application.Modules.IModule.RegisterDisabledStubs(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> void +MMCA.Common.Application.Modules.IModule.RequiresDependencies.get -> bool +MMCA.Common.Application.Modules.IModuleSeeder +MMCA.Common.Application.Modules.IModuleSeeder.ModuleName.get -> string! +MMCA.Common.Application.Modules.IModuleSeeder.SeedAsync(System.IServiceProvider! serviceProvider, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Modules.ModuleLoader +MMCA.Common.Application.Modules.ModuleLoader.DisabledModuleNames.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Application.Modules.ModuleLoader.DiscoverAndRegister(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationBuilder! configurationBuilder, MMCA.Common.Application.Settings.ApplicationSettings! applicationSettings, MMCA.Common.Application.Settings.ModulesSettings! modulesSettings, string? environmentName = null) -> void +MMCA.Common.Application.Modules.ModuleLoader.DiscoverAndRegister(Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfigurationBuilder! configurationBuilder, MMCA.Common.Application.Settings.ApplicationSettings! applicationSettings, MMCA.Common.Application.Settings.ModulesSettings! modulesSettings, string? environmentName, System.Collections.Generic.IEnumerable? moduleAssemblies) -> void +MMCA.Common.Application.Modules.ModuleLoader.EnabledModules.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Application.Modules.ModuleLoader.Logger.get -> Microsoft.Extensions.Logging.ILogger! +MMCA.Common.Application.Modules.ModuleLoader.Logger.init -> void +MMCA.Common.Application.Modules.ModuleLoader.ModuleLoader() -> void +MMCA.Common.Application.Modules.ModuleLoader.SeedAllAsync(System.IServiceProvider! serviceProvider, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Modules.ModuleLoader.ValidateRemoteDependencies(System.IServiceProvider! serviceProvider) -> void +MMCA.Common.Application.Notifications.DependencyInjection +MMCA.Common.Application.Notifications.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!) +MMCA.Common.Application.Notifications.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddNotificationApplicationServices() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Application.Notifications.PushNotifications.DTOs.PushNotificationDTOMapper +MMCA.Common.Application.Notifications.PushNotifications.DTOs.PushNotificationDTOMapper.MapToDTO(MMCA.Common.Domain.Notifications.PushNotifications.PushNotification! entity) -> MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO! +MMCA.Common.Application.Notifications.PushNotifications.DTOs.PushNotificationDTOMapper.MapToDTOs(System.Collections.Generic.IReadOnlyCollection! entityCollection) -> System.Collections.Generic.IReadOnlyCollection! +MMCA.Common.Application.Notifications.PushNotifications.DTOs.PushNotificationDTOMapper.PushNotificationDTOMapper() -> void +MMCA.Common.Application.Notifications.PushNotifications.DTOs.PushNotificationDTOProjector +MMCA.Common.Application.Notifications.PushNotifications.DTOs.PushNotificationDTOProjector.ProjectTo(System.Linq.IQueryable! source) -> System.Linq.IQueryable! +MMCA.Common.Application.Notifications.PushNotifications.DTOs.PushNotificationDTOProjector.PushNotificationDTOProjector() -> void +MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryHandler +MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryHandler.GetNotificationHistoryHandler(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, MMCA.Common.Application.Interfaces.Infrastructure.IQueryableExecutor! queryableExecutor, MMCA.Common.Application.Notifications.PushNotifications.DTOs.PushNotificationDTOMapper! dtoMapper) -> void +MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryHandler.HandleAsync(MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery! query, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>! +MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery +MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery.$() -> MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery! +MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery.Deconstruct(out int PageNumber, out int PageSize) -> void +MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery.Equals(MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery? other) -> bool +MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery.GetNotificationHistoryQuery(int PageNumber = 1, int PageSize = 10) -> void +MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery.PageNumber.get -> int +MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery.PageNumber.init -> void +MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery.PageSize.get -> int +MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery.PageSize.init -> void +MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand +MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand.$() -> MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand! +MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand.Deconstruct(out MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest! Request, out int SentByUserId) -> void +MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand.DedupKey.get -> string? +MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand.DedupKey.init -> void +MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand.Equals(MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand? other) -> bool +MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand.Request.get -> MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest! +MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand.Request.init -> void +MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand.SendPushNotificationCommand(MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest! Request, int SentByUserId) -> void +MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand.SentByUserId.get -> int +MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand.SentByUserId.init -> void +MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationHandler +MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationHandler.HandleAsync(MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand! command, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationHandler.SendPushNotificationHandler(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, MMCA.Common.Application.Interfaces.Infrastructure.INotificationRecipientProvider! recipientProvider, MMCA.Common.Application.Interfaces.Infrastructure.IPushNotificationSender! pushNotificationSender, MMCA.Common.Application.Interfaces.Infrastructure.INativePushSender! nativePushSender, MMCA.Common.Application.Notifications.PushNotifications.DTOs.PushNotificationDTOMapper! dtoMapper, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationRequestValidator +MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationRequestValidator.SendPushNotificationRequestValidator() -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsHandler +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsHandler.GetMyNotificationsHandler(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, MMCA.Common.Application.Interfaces.Infrastructure.IQueryableExecutor! queryableExecutor) -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsHandler.HandleAsync(MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery! query, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>! +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery.$() -> MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery! +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery.Deconstruct(out int UserId, out int PageNumber, out int PageSize, out string? ScopeKey) -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery.Equals(MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery? other) -> bool +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery.GetMyNotificationsQuery(int UserId, int PageNumber = 1, int PageSize = 20, string? ScopeKey = null) -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery.PageNumber.get -> int +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery.PageNumber.init -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery.PageSize.get -> int +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery.PageSize.init -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery.ScopeKey.get -> string? +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery.ScopeKey.init -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery.UserId.get -> int +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery.UserId.init -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountHandler +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountHandler.GetUnreadNotificationCountHandler(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, MMCA.Common.Application.Interfaces.Infrastructure.IQueryableExecutor! queryableExecutor) -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountHandler.HandleAsync(MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery! query, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery.$() -> MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery! +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery.Deconstruct(out int UserId, out string? ScopeKey) -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery.Equals(MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery? other) -> bool +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery.GetUnreadNotificationCountQuery(int UserId, string? ScopeKey = null) -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery.ScopeKey.get -> string? +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery.ScopeKey.init -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery.UserId.get -> int +MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery.UserId.init -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand.$() -> MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand! +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand.Deconstruct(out int UserId, out string? ScopeKey) -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand.Equals(MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand? other) -> bool +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand.MarkAllNotificationsReadCommand(int UserId, string? ScopeKey = null) -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand.ScopeKey.get -> string? +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand.ScopeKey.init -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand.UserId.get -> int +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand.UserId.init -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadHandler +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadHandler.HandleAsync(MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand! command, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadHandler.MarkAllNotificationsReadHandler(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, MMCA.Common.Application.Interfaces.Infrastructure.IQueryableExecutor! queryableExecutor, System.TimeProvider! timeProvider) -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand.$() -> MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand! +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand.Deconstruct(out int NotificationId, out int UserId) -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand.Equals(MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand? other) -> bool +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand.MarkNotificationReadCommand(int NotificationId, int UserId) -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand.NotificationId.get -> int +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand.NotificationId.init -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand.UserId.get -> int +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand.UserId.init -> void +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadHandler +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadHandler.HandleAsync(MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand! command, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadHandler.MarkNotificationReadHandler(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, MMCA.Common.Application.Interfaces.Infrastructure.IQueryableExecutor! queryableExecutor, System.TimeProvider! timeProvider) -> void +MMCA.Common.Application.Services.DomainEventDispatcher +MMCA.Common.Application.Services.DomainEventDispatcher.DispatchAsync(System.Collections.Generic.IEnumerable! domainEvents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Services.DomainEventDispatcher.DomainEventDispatcher(System.IServiceProvider! serviceProvider, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.Application.Services.EntityQueryService +MMCA.Common.Application.Services.EntityQueryService.DTOMapper.get -> MMCA.Common.Application.Interfaces.IEntityDTOMapper! +MMCA.Common.Application.Services.EntityQueryService.DTOProjector.get -> MMCA.Common.Application.Interfaces.IEntityDTOProjector? +MMCA.Common.Application.Services.EntityQueryService.EntityQueryService(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, MMCA.Common.Application.Services.Query.INavigationMetadataProvider! navigationMetadataProvider, MMCA.Common.Application.Services.Query.IEntityQueryPipeline! queryPipeline, MMCA.Common.Application.Interfaces.IEntityDTOMapper! dtoMapper, MMCA.Common.Application.Interfaces.INavigationPopulator! navigationPopulator) -> void +MMCA.Common.Application.Services.EntityQueryService.EntityQueryService(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, MMCA.Common.Application.Services.Query.INavigationMetadataProvider! navigationMetadataProvider, MMCA.Common.Application.Services.Query.IEntityQueryPipeline! queryPipeline, MMCA.Common.Application.Interfaces.IEntityDTOMapper! dtoMapper, MMCA.Common.Application.Interfaces.INavigationPopulator! navigationPopulator, MMCA.Common.Application.Interfaces.IEntityDTOProjector! dtoProjector) -> void +MMCA.Common.Application.Services.EntityQueryService.ExistsAsync(System.Linq.Expressions.Expression!>! where, bool ignoreQueryFilters = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Services.EntityQueryService.NavigationPopulator.get -> MMCA.Common.Application.Interfaces.INavigationPopulator! +MMCA.Common.Application.Services.EntityQueryService.UnitOfWork.get -> MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! +MMCA.Common.Application.Services.Filtering.IFilterStrategy +MMCA.Common.Application.Services.Filtering.IFilterStrategy.Apply(System.Linq.IQueryable! query, string! property, string! op, string! value) -> System.Linq.IQueryable! +MMCA.Common.Application.Services.Filtering.IFilterStrategy.CanParseValue(string! op, string! value) -> bool +MMCA.Common.Application.Services.Filtering.IFilterStrategy.SupportedOperators.get -> System.Collections.Generic.IReadOnlySet? +MMCA.Common.Application.Services.Filtering.QueryFilterService +MMCA.Common.Application.Services.Navigation.ChildNavigationDescriptor +MMCA.Common.Application.Services.Navigation.ChildNavigationDescriptor.AssignAction.get -> System.Action!>! +MMCA.Common.Application.Services.Navigation.ChildNavigationDescriptor.AssignAction.init -> void +MMCA.Common.Application.Services.Navigation.ChildNavigationDescriptor.ChildForeignKeySelector.get -> System.Linq.Expressions.Expression!>! +MMCA.Common.Application.Services.Navigation.ChildNavigationDescriptor.ChildForeignKeySelector.init -> void +MMCA.Common.Application.Services.Navigation.ChildNavigationDescriptor.ChildNavigationDescriptor() -> void +MMCA.Common.Application.Services.Navigation.ChildNavigationDescriptor.LoadAsync(System.Collections.Generic.IReadOnlyCollection! entities, MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Services.Navigation.ChildNavigationDescriptor.ParentKeySelector.get -> System.Func! +MMCA.Common.Application.Services.Navigation.ChildNavigationDescriptor.ParentKeySelector.init -> void +MMCA.Common.Application.Services.Navigation.ChildNavigationDescriptor.PropertyName.get -> string! +MMCA.Common.Application.Services.Navigation.ChildNavigationDescriptor.PropertyName.init -> void +MMCA.Common.Application.Services.Navigation.ChildNavigationDescriptor.RequiresChildren.get -> bool +MMCA.Common.Application.Services.Navigation.DeclarativeNavigationPopulator +MMCA.Common.Application.Services.Navigation.DeclarativeNavigationPopulator.DeclarativeNavigationPopulator(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, System.Collections.Generic.IReadOnlyList!>! descriptors) -> void +MMCA.Common.Application.Services.Navigation.DeclarativeNavigationPopulator.PopulateAsync(System.Collections.Generic.IReadOnlyCollection! entities, MMCA.Common.Application.Interfaces.NavigationMetadata! navigationMetadata, bool includeFKs, bool includeChildren, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Services.Navigation.FKNavigationDescriptor +MMCA.Common.Application.Services.Navigation.FKNavigationDescriptor.AssignAction.get -> System.Action!>! +MMCA.Common.Application.Services.Navigation.FKNavigationDescriptor.AssignAction.init -> void +MMCA.Common.Application.Services.Navigation.FKNavigationDescriptor.ChildForeignKeySelector.get -> System.Linq.Expressions.Expression!>! +MMCA.Common.Application.Services.Navigation.FKNavigationDescriptor.ChildForeignKeySelector.init -> void +MMCA.Common.Application.Services.Navigation.FKNavigationDescriptor.FKNavigationDescriptor() -> void +MMCA.Common.Application.Services.Navigation.FKNavigationDescriptor.LoadAsync(System.Collections.Generic.IReadOnlyCollection! entities, MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Services.Navigation.FKNavigationDescriptor.ParentKeySelector.get -> System.Func! +MMCA.Common.Application.Services.Navigation.FKNavigationDescriptor.ParentKeySelector.init -> void +MMCA.Common.Application.Services.Navigation.FKNavigationDescriptor.PropertyName.get -> string! +MMCA.Common.Application.Services.Navigation.FKNavigationDescriptor.PropertyName.init -> void +MMCA.Common.Application.Services.Navigation.FKNavigationDescriptor.RequiresChildren.get -> bool +MMCA.Common.Application.Services.Navigation.INavigationDescriptor +MMCA.Common.Application.Services.Navigation.INavigationDescriptor.LoadAsync(System.Collections.Generic.IReadOnlyCollection! entities, MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Services.Navigation.INavigationDescriptor.PropertyName.get -> string! +MMCA.Common.Application.Services.Navigation.INavigationDescriptor.RequiresChildren.get -> bool +MMCA.Common.Application.Services.NavigationLoader +MMCA.Common.Application.Services.NullNavigationPopulator +MMCA.Common.Application.Services.NullNavigationPopulator.NullNavigationPopulator() -> void +MMCA.Common.Application.Services.NullNavigationPopulator.PopulateAsync(System.Collections.Generic.IReadOnlyCollection! entities, MMCA.Common.Application.Interfaces.NavigationMetadata! navigationMetadata, bool includeFKs, bool includeChildren, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Services.Query.EntityQueryParameters +MMCA.Common.Application.Services.Query.EntityQueryParameters.$() -> MMCA.Common.Application.Services.Query.EntityQueryParameters! +MMCA.Common.Application.Services.Query.EntityQueryParameters.Criteria.get -> System.Linq.Expressions.Expression!>? +MMCA.Common.Application.Services.Query.EntityQueryParameters.Criteria.init -> void +MMCA.Common.Application.Services.Query.EntityQueryParameters.DTOToEntityPropertyMap.get -> System.Collections.Generic.IReadOnlyDictionary! +MMCA.Common.Application.Services.Query.EntityQueryParameters.DTOToEntityPropertyMap.init -> void +MMCA.Common.Application.Services.Query.EntityQueryParameters.EntityQueryParameters() -> void +MMCA.Common.Application.Services.Query.EntityQueryParameters.Equals(MMCA.Common.Application.Services.Query.EntityQueryParameters? other) -> bool +MMCA.Common.Application.Services.Query.EntityQueryParameters.Fields.get -> string? +MMCA.Common.Application.Services.Query.EntityQueryParameters.Fields.init -> void +MMCA.Common.Application.Services.Query.EntityQueryParameters.Filters.get -> System.Collections.Generic.Dictionary? +MMCA.Common.Application.Services.Query.EntityQueryParameters.Filters.init -> void +MMCA.Common.Application.Services.Query.EntityQueryParameters.IncludeChildren.get -> bool +MMCA.Common.Application.Services.Query.EntityQueryParameters.IncludeChildren.init -> void +MMCA.Common.Application.Services.Query.EntityQueryParameters.IncludeFKs.get -> bool +MMCA.Common.Application.Services.Query.EntityQueryParameters.IncludeFKs.init -> void +MMCA.Common.Application.Services.Query.EntityQueryParameters.PageNumber.get -> int? +MMCA.Common.Application.Services.Query.EntityQueryParameters.PageNumber.init -> void +MMCA.Common.Application.Services.Query.EntityQueryParameters.PageSize.get -> int? +MMCA.Common.Application.Services.Query.EntityQueryParameters.PageSize.init -> void +MMCA.Common.Application.Services.Query.EntityQueryParameters.SortColumn.get -> string? +MMCA.Common.Application.Services.Query.EntityQueryParameters.SortColumn.init -> void +MMCA.Common.Application.Services.Query.EntityQueryParameters.SortDirection.get -> string? +MMCA.Common.Application.Services.Query.EntityQueryParameters.SortDirection.init -> void +MMCA.Common.Application.Services.Query.EntityQueryPipeline +MMCA.Common.Application.Services.Query.EntityQueryPipeline.EntityQueryPipeline(MMCA.Common.Application.Interfaces.Infrastructure.IQueryableExecutor! queryableExecutor) -> void +MMCA.Common.Application.Services.Query.EntityQueryPipeline.ExecuteAsync(System.Linq.IQueryable! baseQuery, MMCA.Common.Application.Interfaces.NavigationMetadata! navigationMetadata, MMCA.Common.Application.Services.Query.EntityQueryParameters! parameters, System.Func!, MMCA.Common.Application.Interfaces.NavigationMetadata!, bool, bool, System.Threading.CancellationToken, System.Threading.Tasks.Task!>! navigationPopulator, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<(System.Collections.Generic.IReadOnlyCollection! Items, int TotalCount)>! +MMCA.Common.Application.Services.Query.EntityQueryPipeline.ExecuteProjectedAsync(System.Linq.IQueryable! baseQuery, MMCA.Common.Application.Services.Query.EntityQueryParameters! parameters, System.Func!, System.Linq.IQueryable!>! project, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<(System.Collections.Generic.IReadOnlyCollection! Items, int TotalCount)>! +MMCA.Common.Application.Services.Query.IEntityQueryPipeline +MMCA.Common.Application.Services.Query.IEntityQueryPipeline.ExecuteAsync(System.Linq.IQueryable! baseQuery, MMCA.Common.Application.Interfaces.NavigationMetadata! navigationMetadata, MMCA.Common.Application.Services.Query.EntityQueryParameters! parameters, System.Func!, MMCA.Common.Application.Interfaces.NavigationMetadata!, bool, bool, System.Threading.CancellationToken, System.Threading.Tasks.Task!>! navigationPopulator, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<(System.Collections.Generic.IReadOnlyCollection! Items, int TotalCount)>! +MMCA.Common.Application.Services.Query.IEntityQueryPipeline.ExecuteProjectedAsync(System.Linq.IQueryable! baseQuery, MMCA.Common.Application.Services.Query.EntityQueryParameters! parameters, System.Func!, System.Linq.IQueryable!>! project, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task<(System.Collections.Generic.IReadOnlyCollection! Items, int TotalCount)>! +MMCA.Common.Application.Services.Query.INavigationMetadataProvider +MMCA.Common.Application.Services.Query.INavigationMetadataProvider.BuildIncludes(bool includeFKs, bool includeChildren) -> MMCA.Common.Application.Interfaces.NavigationMetadata! +MMCA.Common.Application.Services.Query.NavigationMetadataProvider +MMCA.Common.Application.Services.Query.NavigationMetadataProvider.BuildIncludes(bool includeFKs, bool includeChildren) -> MMCA.Common.Application.Interfaces.NavigationMetadata! +MMCA.Common.Application.Services.Query.NavigationMetadataProvider.NavigationMetadataProvider(MMCA.Common.Application.Interfaces.Infrastructure.IDataSourceService! dataSourceService) -> void +MMCA.Common.Application.Services.Query.PagingMath +MMCA.Common.Application.Services.QueryFieldService +MMCA.Common.Application.Services.QueryFieldService.QueryFieldService() -> void +MMCA.Common.Application.Settings.ApplicationSettings +MMCA.Common.Application.Settings.ApplicationSettings.ApplicationSettings() -> void +MMCA.Common.Application.Settings.ApplicationSettings.DatabaseInitStrategy.get -> string! +MMCA.Common.Application.Settings.ApplicationSettings.DatabaseInitStrategy.init -> void +MMCA.Common.Application.Settings.ApplicationSettings.MaxExportRows.get -> int +MMCA.Common.Application.Settings.ApplicationSettings.MaxExportRows.init -> void +MMCA.Common.Application.Settings.ApplicationSettings.MaxPageSize.get -> int +MMCA.Common.Application.Settings.ApplicationSettings.MaxPageSize.init -> void +MMCA.Common.Application.Settings.ApplicationSettings.UseMiniProfiler.get -> bool +MMCA.Common.Application.Settings.ApplicationSettings.UseMiniProfiler.init -> void +MMCA.Common.Application.Settings.IApplicationSettings +MMCA.Common.Application.Settings.IApplicationSettings.DatabaseInitStrategy.get -> string! +MMCA.Common.Application.Settings.IApplicationSettings.DatabaseInitStrategy.init -> void +MMCA.Common.Application.Settings.IApplicationSettings.MaxExportRows.get -> int +MMCA.Common.Application.Settings.IApplicationSettings.MaxExportRows.init -> void +MMCA.Common.Application.Settings.IApplicationSettings.MaxPageSize.get -> int +MMCA.Common.Application.Settings.IApplicationSettings.MaxPageSize.init -> void +MMCA.Common.Application.Settings.IApplicationSettings.UseMiniProfiler.get -> bool +MMCA.Common.Application.Settings.IApplicationSettings.UseMiniProfiler.init -> void +MMCA.Common.Application.Settings.ModuleSettings +MMCA.Common.Application.Settings.ModuleSettings.Enabled.get -> bool +MMCA.Common.Application.Settings.ModuleSettings.Enabled.init -> void +MMCA.Common.Application.Settings.ModuleSettings.ModuleSettings() -> void +MMCA.Common.Application.Settings.ModuleSettings.RemoteDependencies.get -> System.Collections.Generic.List! +MMCA.Common.Application.Settings.ModuleSettings.RemoteDependencies.set -> void +MMCA.Common.Application.Settings.ModulesSettings +MMCA.Common.Application.Settings.ModulesSettings.IsDependencyRemote(string! consumerModule, string! dependencyModule) -> bool +MMCA.Common.Application.Settings.ModulesSettings.IsModuleEnabled(string! moduleName) -> bool +MMCA.Common.Application.Settings.ModulesSettings.ModulesSettings() -> void +MMCA.Common.Application.Specifications.CrossSourceSpecification +MMCA.Common.Application.UseCases.Decorators.AuthorizationCommandDecorator +MMCA.Common.Application.UseCases.Decorators.AuthorizationCommandDecorator.AuthorizationCommandDecorator(MMCA.Common.Application.UseCases.ICommandHandler! inner, MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService! currentUser, MMCA.Common.Shared.Auth.IPermissionRegistry! permissionRegistry) -> void +MMCA.Common.Application.UseCases.Decorators.AuthorizationCommandDecorator.HandleAsync(TCommand command, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.UseCases.Decorators.AuthorizationQueryDecorator +MMCA.Common.Application.UseCases.Decorators.AuthorizationQueryDecorator.AuthorizationQueryDecorator(MMCA.Common.Application.UseCases.IQueryHandler! inner, MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService! currentUser, MMCA.Common.Shared.Auth.IPermissionRegistry! permissionRegistry) -> void +MMCA.Common.Application.UseCases.Decorators.AuthorizationQueryDecorator.HandleAsync(TQuery query, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.UseCases.Decorators.CachingCommandDecorator +MMCA.Common.Application.UseCases.Decorators.CachingCommandDecorator.CachingCommandDecorator(MMCA.Common.Application.UseCases.ICommandHandler! inner, MMCA.Common.Application.Interfaces.ICacheService! cacheService) -> void +MMCA.Common.Application.UseCases.Decorators.CachingCommandDecorator.CachingCommandDecorator(MMCA.Common.Application.UseCases.ICommandHandler! inner, MMCA.Common.Application.Interfaces.ICacheService! cacheService, Microsoft.Extensions.Logging.ILogger!>! logger, MMCA.Common.Application.Interfaces.ITenantContext? tenantContext = null) -> void +MMCA.Common.Application.UseCases.Decorators.CachingCommandDecorator.HandleAsync(TCommand command, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.UseCases.Decorators.CachingQueryDecorator +MMCA.Common.Application.UseCases.Decorators.CachingQueryDecorator.CachingQueryDecorator(MMCA.Common.Application.UseCases.IQueryHandler! inner, MMCA.Common.Application.Interfaces.ICacheService! cacheService) -> void +MMCA.Common.Application.UseCases.Decorators.CachingQueryDecorator.CachingQueryDecorator(MMCA.Common.Application.UseCases.IQueryHandler! inner, MMCA.Common.Application.Interfaces.ICacheService! cacheService, Microsoft.Extensions.Logging.ILogger!>! logger, MMCA.Common.Application.Interfaces.ITenantContext? tenantContext = null) -> void +MMCA.Common.Application.UseCases.Decorators.CachingQueryDecorator.HandleAsync(TQuery query, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.UseCases.Decorators.FeatureGateCommandDecorator +MMCA.Common.Application.UseCases.Decorators.FeatureGateCommandDecorator.FeatureGateCommandDecorator(MMCA.Common.Application.UseCases.ICommandHandler! inner, Microsoft.FeatureManagement.IFeatureManager! featureManager) -> void +MMCA.Common.Application.UseCases.Decorators.FeatureGateCommandDecorator.HandleAsync(TCommand command, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.UseCases.Decorators.FeatureGateQueryDecorator +MMCA.Common.Application.UseCases.Decorators.FeatureGateQueryDecorator.FeatureGateQueryDecorator(MMCA.Common.Application.UseCases.IQueryHandler! inner, Microsoft.FeatureManagement.IFeatureManager! featureManager) -> void +MMCA.Common.Application.UseCases.Decorators.FeatureGateQueryDecorator.HandleAsync(TQuery query, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.UseCases.Decorators.LoggingCommandDecorator +MMCA.Common.Application.UseCases.Decorators.LoggingCommandDecorator.HandleAsync(TCommand command, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.UseCases.Decorators.LoggingCommandDecorator.LoggingCommandDecorator(MMCA.Common.Application.UseCases.ICommandHandler! inner, MMCA.Common.Application.Interfaces.ICorrelationContext! correlationContext, Microsoft.Extensions.Logging.ILogger!>! logger) -> void +MMCA.Common.Application.UseCases.Decorators.LoggingQueryDecorator +MMCA.Common.Application.UseCases.Decorators.LoggingQueryDecorator.HandleAsync(TQuery query, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.UseCases.Decorators.LoggingQueryDecorator.LoggingQueryDecorator(MMCA.Common.Application.UseCases.IQueryHandler! inner, MMCA.Common.Application.Interfaces.ICorrelationContext! correlationContext, Microsoft.Extensions.Logging.ILogger!>! logger) -> void +MMCA.Common.Application.UseCases.Decorators.ProfilingCommandDecorator +MMCA.Common.Application.UseCases.Decorators.ProfilingCommandDecorator.HandleAsync(TCommand command, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.UseCases.Decorators.ProfilingCommandDecorator.ProfilingCommandDecorator(MMCA.Common.Application.UseCases.ICommandHandler! inner) -> void +MMCA.Common.Application.UseCases.Decorators.ProfilingQueryDecorator +MMCA.Common.Application.UseCases.Decorators.ProfilingQueryDecorator.HandleAsync(TQuery query, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.UseCases.Decorators.ProfilingQueryDecorator.ProfilingQueryDecorator(MMCA.Common.Application.UseCases.IQueryHandler! inner) -> void +MMCA.Common.Application.UseCases.Decorators.TimeoutCommandDecorator +MMCA.Common.Application.UseCases.Decorators.TimeoutCommandDecorator.HandleAsync(TCommand command, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.UseCases.Decorators.TimeoutCommandDecorator.TimeoutCommandDecorator(MMCA.Common.Application.UseCases.ICommandHandler! inner) -> void +MMCA.Common.Application.UseCases.Decorators.TimeoutQueryDecorator +MMCA.Common.Application.UseCases.Decorators.TimeoutQueryDecorator.HandleAsync(TQuery query, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.UseCases.Decorators.TimeoutQueryDecorator.TimeoutQueryDecorator(MMCA.Common.Application.UseCases.IQueryHandler! inner) -> void +MMCA.Common.Application.UseCases.Decorators.TransactionalCommandDecorator +MMCA.Common.Application.UseCases.Decorators.TransactionalCommandDecorator.HandleAsync(TCommand command, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.UseCases.Decorators.TransactionalCommandDecorator.TransactionalCommandDecorator(MMCA.Common.Application.UseCases.ICommandHandler! inner, MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork) -> void +MMCA.Common.Application.UseCases.Decorators.ValidatingCommandDecorator +MMCA.Common.Application.UseCases.Decorators.ValidatingCommandDecorator.HandleAsync(TCommand command, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.UseCases.Decorators.ValidatingCommandDecorator.ValidatingCommandDecorator(MMCA.Common.Application.UseCases.ICommandHandler! inner, System.Collections.Generic.IEnumerable!>! validators, Microsoft.Extensions.Logging.ILogger!>! logger) -> void +MMCA.Common.Application.UseCases.DeleteEntityCommand +MMCA.Common.Application.UseCases.DeleteEntityCommand.$() -> MMCA.Common.Application.UseCases.DeleteEntityCommand! +MMCA.Common.Application.UseCases.DeleteEntityCommand.CachePrefix.get -> string! +MMCA.Common.Application.UseCases.DeleteEntityCommand.CachePrefix.init -> void +MMCA.Common.Application.UseCases.DeleteEntityCommand.Deconstruct(out TIdentifierType Id) -> void +MMCA.Common.Application.UseCases.DeleteEntityCommand.DeleteEntityCommand(TIdentifierType Id) -> void +MMCA.Common.Application.UseCases.DeleteEntityCommand.Equals(MMCA.Common.Application.UseCases.DeleteEntityCommand? other) -> bool +MMCA.Common.Application.UseCases.DeleteEntityCommand.Id.get -> TIdentifierType +MMCA.Common.Application.UseCases.DeleteEntityCommand.Id.init -> void +MMCA.Common.Application.UseCases.DeleteEntityHandler +MMCA.Common.Application.UseCases.DeleteEntityHandler.DeleteEntityHandler(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork) -> void +MMCA.Common.Application.UseCases.DeleteEntityHandler.HandleAsync(MMCA.Common.Application.UseCases.DeleteEntityCommand! command, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.UseCases.ICacheInvalidating +MMCA.Common.Application.UseCases.ICacheInvalidating.CachePrefix.get -> string! +MMCA.Common.Application.UseCases.ICommandHandler +MMCA.Common.Application.UseCases.ICommandHandler.HandleAsync(TCommand command, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.UseCases.ICommandWithRequest +MMCA.Common.Application.UseCases.ICommandWithRequest.Request.get -> TRequest +MMCA.Common.Application.UseCases.IFeatureGated +MMCA.Common.Application.UseCases.IFeatureGated.FeatureName.get -> string! +MMCA.Common.Application.UseCases.IHasTimeout +MMCA.Common.Application.UseCases.IHasTimeout.Timeout.get -> System.TimeSpan +MMCA.Common.Application.UseCases.IQueryCacheable +MMCA.Common.Application.UseCases.IQueryCacheable.CacheDuration.get -> System.TimeSpan +MMCA.Common.Application.UseCases.IQueryCacheable.CacheKey.get -> string! +MMCA.Common.Application.UseCases.IQueryHandler +MMCA.Common.Application.UseCases.IQueryHandler.HandleAsync(TQuery query, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.UseCases.IRequiresPermission +MMCA.Common.Application.UseCases.IRequiresPermission.Permission.get -> string! +MMCA.Common.Application.UseCases.ITransactional +MMCA.Common.Application.Users.IUserOwnedRequest +MMCA.Common.Application.Users.IUserOwnedRequest.CurrentUserId.get -> int +MMCA.Common.Application.Users.IUserOwnedRequest.CurrentUserRole.get -> string? +MMCA.Common.Application.Users.IUserScopedCommand +MMCA.Common.Application.Users.IUserScopedCommand.Request.get -> TRequest +MMCA.Common.Application.Users.IUserScopedRequest +MMCA.Common.Application.Users.IUserScopedRequest.UserId.get -> int +MMCA.Common.Application.Users.SoftDeletedUserValidator +MMCA.Common.Application.Users.SoftDeletedUserValidator.IsUserSoftDeletedAsync(int userId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Users.SoftDeletedUserValidator.SoftDeletedUserValidator(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork) -> void +MMCA.Common.Application.Users.UseCases.ChangePassword.ChangePasswordHandlerBase +MMCA.Common.Application.Users.UseCases.ChangePassword.ChangePasswordHandlerBase.ChangePasswordHandlerBase(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, MMCA.Common.Application.Interfaces.Infrastructure.IPasswordHasher! passwordHasher, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.Application.Users.UseCases.ChangePassword.ChangePasswordHandlerBase.HandleAsync(TCommand command, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Users.UseCases.ChangePassword.ChangePasswordHandlerBase.UnitOfWork.get -> MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! +MMCA.Common.Application.Users.UseCases.ChangePreferences.ChangePreferencesHandlerBase +MMCA.Common.Application.Users.UseCases.ChangePreferences.ChangePreferencesHandlerBase.ChangePreferencesHandlerBase(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.Application.Users.UseCases.ChangePreferences.ChangePreferencesHandlerBase.HandleAsync(TCommand command, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Users.UseCases.ChangePreferences.ChangePreferencesHandlerBase.UnitOfWork.get -> MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! +MMCA.Common.Application.Users.UseCases.DeleteUser.DeleteUserHandlerBase +MMCA.Common.Application.Users.UseCases.DeleteUser.DeleteUserHandlerBase.DeleteUserHandlerBase(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.Application.Users.UseCases.DeleteUser.DeleteUserHandlerBase.HandleAsync(TCommand command, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Users.UseCases.DeleteUser.DeleteUserHandlerBase.UnitOfWork.get -> MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! +MMCA.Common.Application.Users.UseCases.ExportUserData.ExportUserDataHandlerBase +MMCA.Common.Application.Users.UseCases.ExportUserData.ExportUserDataHandlerBase.ExportUserDataHandlerBase(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, System.Collections.Generic.IEnumerable! sections, System.TimeProvider! timeProvider, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.Application.Users.UseCases.ExportUserData.ExportUserDataHandlerBase.HandleAsync(TQuery query, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Users.UseCases.ExportUserData.ExportUserDataHandlerBase.UnitOfWork.get -> MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! +MMCA.Common.Application.Users.UseCases.ExportUserData.IUserDataExportSection +MMCA.Common.Application.Users.UseCases.ExportUserData.IUserDataExportSection.ExportAsync(int userId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Application.Users.UseCases.ExportUserData.IUserDataExportSection.SectionName.get -> string! +MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionDefaults +MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult +MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.$() -> MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult! +MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.Available.get -> bool +MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.Available.init -> void +MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.Data.get -> object? +MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.Data.init -> void +MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.Equals(MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult? other) -> bool +MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.SectionName.get -> string! +MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.SectionName.init -> void +MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.UnavailableReason.get -> string? +MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.UnavailableReason.init -> void +MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.UserDataExportSectionResult() -> void +MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesHandlerBase +MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesHandlerBase.GetUserPreferencesHandlerBase(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork) -> void +MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesHandlerBase.HandleAsync(MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery! query, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery +MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery.$() -> MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery! +MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery.Deconstruct(out int UserId) -> void +MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery.Equals(MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery? other) -> bool +MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery.GetUserPreferencesQuery(int UserId) -> void +MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery.UserId.get -> int +MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery.UserId.init -> void +MMCA.Common.Application.Users.UserOwnershipRule +MMCA.Common.Application.Validation.AddressLine1Rules +MMCA.Common.Application.Validation.AddressLine1Rules.AddressLine1Rules(System.Linq.Expressions.Expression!>! selector) -> void +MMCA.Common.Application.Validation.AddressLine2Rules +MMCA.Common.Application.Validation.AddressLine2Rules.AddressLine2Rules(System.Linq.Expressions.Expression!>! selector) -> void +MMCA.Common.Application.Validation.AddressValidator +MMCA.Common.Application.Validation.AddressValidator.AddressValidator() -> void +MMCA.Common.Application.Validation.CityRules +MMCA.Common.Application.Validation.CityRules.CityRules(System.Linq.Expressions.Expression!>! selector) -> void +MMCA.Common.Application.Validation.CommandRequestValidator +MMCA.Common.Application.Validation.CommandRequestValidator.CommandRequestValidator(System.Collections.Generic.IEnumerable!>! requestValidators) -> void +MMCA.Common.Application.Validation.CountryRules +MMCA.Common.Application.Validation.CountryRules.CountryRules(System.Linq.Expressions.Expression!>! selector) -> void +MMCA.Common.Application.Validation.EmailRules +MMCA.Common.Application.Validation.EmailRules.EmailRules(System.Linq.Expressions.Expression!>! selector, string! fieldName, int maxLength) -> void +MMCA.Common.Application.Validation.NonNegativeIntRules +MMCA.Common.Application.Validation.NonNegativeIntRules.NonNegativeIntRules(System.Linq.Expressions.Expression!>! selector, string! fieldName) -> void +MMCA.Common.Application.Validation.OptionalStringRules +MMCA.Common.Application.Validation.OptionalStringRules.OptionalStringRules(System.Linq.Expressions.Expression!>! selector, string! fieldName, int maxLength) -> void +MMCA.Common.Application.Validation.PasswordRules +MMCA.Common.Application.Validation.PasswordRules.PasswordRules(System.Linq.Expressions.Expression!>! selector) -> void +MMCA.Common.Application.Validation.PositiveDecimalRules +MMCA.Common.Application.Validation.PositiveDecimalRules.PositiveDecimalRules(System.Linq.Expressions.Expression!>! selector, string! fieldName) -> void +MMCA.Common.Application.Validation.PositiveIntRules +MMCA.Common.Application.Validation.PositiveIntRules.PositiveIntRules(System.Linq.Expressions.Expression!>! selector, string! fieldName) -> void +MMCA.Common.Application.Validation.RequiredStringRules +MMCA.Common.Application.Validation.RequiredStringRules.RequiredStringRules(System.Linq.Expressions.Expression!>! selector, string! fieldName, int maxLength) -> void +MMCA.Common.Application.Validation.StateRules +MMCA.Common.Application.Validation.StateRules.StateRules(System.Linq.Expressions.Expression!>! selector) -> void +MMCA.Common.Application.Validation.StrongPasswordRules +MMCA.Common.Application.Validation.StrongPasswordRules.StrongPasswordRules(System.Linq.Expressions.Expression!>! selector) -> void +MMCA.Common.Application.Validation.ZipCodeRules +MMCA.Common.Application.Validation.ZipCodeRules.ZipCodeRules(System.Linq.Expressions.Expression!>! selector) -> void +abstract MMCA.Common.Application.Auth.AuthenticationServiceBase.CreateAccessToken(TUser! user) -> string! +abstract MMCA.Common.Application.Auth.AuthenticationServiceBase.CreateUser(MMCA.Common.Shared.Auth.RegisterRequest request, byte[]! passwordHash, byte[]! passwordSalt) -> MMCA.Common.Shared.Abstractions.Result! +abstract MMCA.Common.Application.Auth.AuthenticationServiceBase.EmailExistsAsync(MMCA.Common.Shared.ValueObjects.Email? email, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +abstract MMCA.Common.Application.Auth.AuthenticationServiceBase.FindUntrackedByEmailAsync(MMCA.Common.Shared.ValueObjects.Email? email, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +abstract MMCA.Common.Application.DomainEvents.SafeDomainEventHandler.HandleSafelyAsync(TDomainEvent! domainEvent, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +abstract MMCA.Common.Application.Users.UseCases.DeleteUser.DeleteUserHandlerBase.HasDeletePrivilege(string? currentUserRole) -> bool +abstract MMCA.Common.Application.Users.UseCases.ExportUserData.ExportUserDataHandlerBase.BuildSubjectSnapshotAsync(TUser! user, TQuery query, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +abstract MMCA.Common.Application.Users.UseCases.ExportUserData.ExportUserDataHandlerBase.HasExportPrivilege(string? currentUserRole) -> bool +const MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey.DefaultName = "Default" -> string! +const MMCA.Common.Application.Services.Query.EntityQueryPipeline.MaxUnboundedResultLimit = 1000 -> int +const MMCA.Common.Application.Users.UseCases.ExportUserData.ExportUserDataHandlerBase.CurrentFormatVersion = "1.0" -> string! +const MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionDefaults.UnavailableReason = "This section could not be retrieved. The data is unchanged; the export can be requested again later." -> string! +override MMCA.Common.Application.Auditing.AuditTrailEntryDTO.Equals(object? obj) -> bool +override MMCA.Common.Application.Auditing.AuditTrailEntryDTO.GetHashCode() -> int +override MMCA.Common.Application.Auditing.AuditTrailEntryDTO.ToString() -> string! +override MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey.GetHashCode() -> int +override MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey.ToString() -> string! +override MMCA.Common.Application.Interfaces.NavigationPropertyInfo.Equals(object? obj) -> bool +override MMCA.Common.Application.Interfaces.NavigationPropertyInfo.GetHashCode() -> int +override MMCA.Common.Application.Interfaces.NavigationPropertyInfo.ToString() -> string! +override MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery.Equals(object? obj) -> bool +override MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery.GetHashCode() -> int +override MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery.ToString() -> string! +override MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand.Equals(object? obj) -> bool +override MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand.GetHashCode() -> int +override MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand.ToString() -> string! +override MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery.Equals(object? obj) -> bool +override MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery.GetHashCode() -> int +override MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery.ToString() -> string! +override MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery.Equals(object? obj) -> bool +override MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery.GetHashCode() -> int +override MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery.ToString() -> string! +override MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand.Equals(object? obj) -> bool +override MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand.GetHashCode() -> int +override MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand.ToString() -> string! +override MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand.Equals(object? obj) -> bool +override MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand.GetHashCode() -> int +override MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand.ToString() -> string! +override MMCA.Common.Application.Services.Query.EntityQueryParameters.Equals(object? obj) -> bool +override MMCA.Common.Application.Services.Query.EntityQueryParameters.GetHashCode() -> int +override MMCA.Common.Application.Services.Query.EntityQueryParameters.ToString() -> string! +override MMCA.Common.Application.UseCases.DeleteEntityCommand.Equals(object? obj) -> bool +override MMCA.Common.Application.UseCases.DeleteEntityCommand.GetHashCode() -> int +override MMCA.Common.Application.UseCases.DeleteEntityCommand.ToString() -> string! +override MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.Equals(object? obj) -> bool +override MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.GetHashCode() -> int +override MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.ToString() -> string! +override MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery.Equals(object? obj) -> bool +override MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery.GetHashCode() -> int +override MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery.ToString() -> string! +static MMCA.Common.Application.Auditing.AuditTrailEntryDTO.operator !=(MMCA.Common.Application.Auditing.AuditTrailEntryDTO? left, MMCA.Common.Application.Auditing.AuditTrailEntryDTO? right) -> bool +static MMCA.Common.Application.Auditing.AuditTrailEntryDTO.operator ==(MMCA.Common.Application.Auditing.AuditTrailEntryDTO? left, MMCA.Common.Application.Auditing.AuditTrailEntryDTO? right) -> bool +static MMCA.Common.Application.Auth.SoftDeletedUserCache.KeyFor(int userId) -> string! +static MMCA.Common.Application.Auth.SoftDeletedUserCache.MarkDeletedAsync(MMCA.Common.Application.Interfaces.ICacheService! cache, int userId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static MMCA.Common.Application.Auth.SoftDeletedUserCache.MarkerDuration.get -> System.TimeSpan +static MMCA.Common.Application.DependencyInjection.AddApplication(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Application.DependencyInjection.AddApplicationDecorators(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Application.DependencyInjection.AddApplicationProfiling(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Application.DependencyInjection.AddUserDataExportSection(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Application.DependencyInjection.ScanModuleApplicationServices(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Application.Extensions.ReadRepositoryExtensions.GetByIdOrFailAsync(this MMCA.Common.Application.Interfaces.Infrastructure.IReadRepository! repository, TIdentifierType id, string! source, System.Collections.Generic.IEnumerable? includes = null, bool asTracking = true, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +static MMCA.Common.Application.Extensions.ValidationFailureExtensions.ToErrors(this FluentValidation.Results.ValidationResult! result, string! source) -> System.Collections.Generic.IEnumerable! +static MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey.Default(MMCA.Common.Application.Interfaces.Infrastructure.DataSource engine) -> MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey +static MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey.operator !=(MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey left, MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey right) -> bool +static MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey.operator ==(MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey left, MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey right) -> bool +static MMCA.Common.Application.Interfaces.Infrastructure.ImageContentSniffer.IsAllowedImage(System.ReadOnlySpan content) -> bool +static MMCA.Common.Application.Interfaces.Infrastructure.ImageContentSniffer.IsJpeg(System.ReadOnlySpan content) -> bool +static MMCA.Common.Application.Interfaces.Infrastructure.ImageContentSniffer.IsPng(System.ReadOnlySpan content) -> bool +static MMCA.Common.Application.Interfaces.Infrastructure.ImageContentSniffer.IsWebP(System.ReadOnlySpan content) -> bool +static MMCA.Common.Application.Interfaces.NavigationPropertyInfo.operator !=(MMCA.Common.Application.Interfaces.NavigationPropertyInfo? left, MMCA.Common.Application.Interfaces.NavigationPropertyInfo? right) -> bool +static MMCA.Common.Application.Interfaces.NavigationPropertyInfo.operator ==(MMCA.Common.Application.Interfaces.NavigationPropertyInfo? left, MMCA.Common.Application.Interfaces.NavigationPropertyInfo? right) -> bool +static MMCA.Common.Application.Notifications.DependencyInjection.AddNotificationApplicationServices(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery.operator !=(MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery? left, MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery? right) -> bool +static MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery.operator ==(MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery? left, MMCA.Common.Application.Notifications.PushNotifications.UseCases.GetHistory.GetNotificationHistoryQuery? right) -> bool +static MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand.operator !=(MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand? left, MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand? right) -> bool +static MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand.operator ==(MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand? left, MMCA.Common.Application.Notifications.PushNotifications.UseCases.Send.SendPushNotificationCommand? right) -> bool +static MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery.operator !=(MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery? left, MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery? right) -> bool +static MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery.operator ==(MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery? left, MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetInbox.GetMyNotificationsQuery? right) -> bool +static MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery.operator !=(MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery? left, MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery? right) -> bool +static MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery.operator ==(MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery? left, MMCA.Common.Application.Notifications.UserNotifications.UseCases.GetUnreadCount.GetUnreadNotificationCountQuery? right) -> bool +static MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand.operator !=(MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand? left, MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand? right) -> bool +static MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand.operator ==(MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand? left, MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkAllRead.MarkAllNotificationsReadCommand? right) -> bool +static MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand.operator !=(MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand? left, MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand? right) -> bool +static MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand.operator ==(MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand? left, MMCA.Common.Application.Notifications.UserNotifications.UseCases.MarkRead.MarkNotificationReadCommand? right) -> bool +static MMCA.Common.Application.Services.Filtering.QueryFilterService.ApplyFilters(System.Linq.IQueryable! query, System.Collections.Generic.Dictionary! filters, System.Collections.Generic.IReadOnlyDictionary! dtoToEntityPropertyMap) -> System.Linq.IQueryable! +static MMCA.Common.Application.Services.Filtering.QueryFilterService.RegisterStrategy(System.Type! propertyType, MMCA.Common.Application.Services.Filtering.IFilterStrategy! strategy) -> void +static MMCA.Common.Application.Services.Filtering.QueryFilterService.ValidateFilters(System.Collections.Generic.Dictionary? filters, System.Collections.Generic.IReadOnlyDictionary! dtoToEntityPropertyMap) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Application.Services.NavigationLoader.LoadChildrenPropertyAsync(System.Collections.Generic.IReadOnlyCollection! parents, System.Func! parentKeySelector, System.Linq.Expressions.Expression!>! childForeignKeySelector, MMCA.Common.Application.Interfaces.Infrastructure.IReadRepository! childRepository, System.Action!>! assignAction, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +static MMCA.Common.Application.Services.NavigationLoader.LoadFKPropertyAsync(System.Collections.Generic.IReadOnlyCollection! parents, System.Func! parentKeySelector, System.Linq.Expressions.Expression!>! childForeignKeySelector, MMCA.Common.Application.Interfaces.Infrastructure.IReadRepository! childRepository, System.Action!>! assignAction, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +static MMCA.Common.Application.Services.Query.EntityQueryParameters.operator !=(MMCA.Common.Application.Services.Query.EntityQueryParameters? left, MMCA.Common.Application.Services.Query.EntityQueryParameters? right) -> bool +static MMCA.Common.Application.Services.Query.EntityQueryParameters.operator ==(MMCA.Common.Application.Services.Query.EntityQueryParameters? left, MMCA.Common.Application.Services.Query.EntityQueryParameters? right) -> bool +static MMCA.Common.Application.Services.Query.PagingMath.Clamp(int pageNumber, int pageSize, int maxPageSize) -> (int Skip, int Take) +static MMCA.Common.Application.Services.QueryFieldService.ApplyFieldSelection(System.Linq.IQueryable! query, string? fields) -> System.Linq.IQueryable! +static MMCA.Common.Application.Services.QueryFieldService.ApplySorting(System.Linq.IQueryable! query, string? sortColumn, string? sortDirection, System.Collections.Generic.IReadOnlyDictionary! dtoToEntityPropertyMap, System.Linq.Expressions.Expression!>? defaultSort = null, string? tieBreakProperty = null) -> System.Linq.IQueryable! +static MMCA.Common.Application.Services.QueryFieldService.ShapeCollectionData(System.Collections.Generic.IEnumerable! entities, string? fields) -> System.Collections.Generic.List! +static MMCA.Common.Application.Services.QueryFieldService.ShapeData(TEntity entity, string? fields) -> System.Dynamic.ExpandoObject! +static MMCA.Common.Application.Services.QueryFieldService.Validate(string? fields, System.Collections.Generic.IReadOnlyDictionary! dtoToEntityPropertyMap, bool allowWriteableFields = false) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Application.Services.QueryFieldService.Validate(string? fields, bool allowWriteableFields = false) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Application.Services.QueryFieldService.ValidateSortDirection(string? sortDirection) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Application.Specifications.CrossSourceSpecification.BuildAsync(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, System.Linq.Expressions.Expression!>! principalPredicate, System.Linq.Expressions.Expression!>! dependentForeignKey, System.Linq.Expressions.Expression!>? localPredicate = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +static MMCA.Common.Application.UseCases.DeleteEntityCommand.operator !=(MMCA.Common.Application.UseCases.DeleteEntityCommand? left, MMCA.Common.Application.UseCases.DeleteEntityCommand? right) -> bool +static MMCA.Common.Application.UseCases.DeleteEntityCommand.operator ==(MMCA.Common.Application.UseCases.DeleteEntityCommand? left, MMCA.Common.Application.UseCases.DeleteEntityCommand? right) -> bool +static MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.Complete(string! sectionName, object? data) -> MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult! +static MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.Unavailable(string! sectionName, string? reason = null) -> MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult! +static MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.operator !=(MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult? left, MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult? right) -> bool +static MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult.operator ==(MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult? left, MMCA.Common.Application.Users.UseCases.ExportUserData.UserDataExportSectionResult? right) -> bool +static MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery.operator !=(MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery? left, MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery? right) -> bool +static MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery.operator ==(MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery? left, MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesQuery? right) -> bool +static MMCA.Common.Application.Users.UserOwnershipRule.CheckOwnership(MMCA.Common.Application.Users.IUserOwnedRequest! request, bool callerHasPrivilegedRole, string! code, string! message, string! source) -> MMCA.Common.Shared.Abstractions.Error? +static readonly MMCA.Common.Application.AssemblyReference.Assembly -> System.Reflection.Assembly! +static readonly MMCA.Common.Application.AssemblyReference.AssemblyName -> string! +static readonly MMCA.Common.Application.Settings.ApplicationSettings.SectionName -> string! +static readonly MMCA.Common.Application.Settings.ModulesSettings.SectionName -> string! +virtual MMCA.Common.Application.Auth.AuthenticationServiceBase.AccessTokenLifetime.get -> System.TimeSpan +virtual MMCA.Common.Application.Auth.AuthenticationServiceBase.CreateRefreshUserMissingError() -> MMCA.Common.Shared.Abstractions.Error! +virtual MMCA.Common.Application.Auth.AuthenticationServiceBase.OnUserRegisteredAsync(TUser! user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +virtual MMCA.Common.Application.Auth.AuthenticationServiceBase.RefreshTokenLifetime.get -> System.TimeSpan +virtual MMCA.Common.Application.Auth.AuthenticationServiceBase.ValidateLoginCandidateAsync(TUser! untrackedUser, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +virtual MMCA.Common.Application.Auth.AuthenticationServiceBase.ValidateRefreshCandidateAsync(TUser! user, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +virtual MMCA.Common.Application.Services.EntityQueryService.DTOToEntityPropertyMap.get -> System.Collections.Generic.IReadOnlyDictionary! +virtual MMCA.Common.Application.Services.EntityQueryService.GetAllAsync(bool includeFKs = false, bool includeChildren = false, MMCA.Common.Domain.Interfaces.ISpecification? specification = null, System.Collections.Generic.Dictionary? filters = null, string? sortColumn = null, string? sortDirection = null, string? fields = null, int? pageNumber = null, int? pageSize = null, bool asTracking = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>! +virtual MMCA.Common.Application.Services.EntityQueryService.GetAllAsync(bool includeFKs = false, bool includeChildren = false, MMCA.Common.Domain.Interfaces.ISpecification? specification = null, string? fields = null, bool asTracking = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>! +virtual MMCA.Common.Application.Services.EntityQueryService.GetAllForLookupAsync(string! nameProperty, System.Linq.Expressions.Expression!>? where = null, bool asTracking = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>!>! +virtual MMCA.Common.Application.Services.EntityQueryService.GetByIdAsync(TIdentifierType id, bool includeFKs = false, bool includeChildren = false, MMCA.Common.Domain.Interfaces.ISpecification? specification = null, string? fields = null, bool asTracking = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +virtual MMCA.Common.Application.Services.EntityQueryService.GetEntityByIdAsync(string! idValue, string? idField = null, bool includeFKs = false, bool includeChildren = false, MMCA.Common.Domain.Interfaces.ISpecification? specification = null, string? fields = null, bool asTracking = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +virtual MMCA.Common.Application.Services.EntityQueryService.Repository.get -> MMCA.Common.Application.Interfaces.Infrastructure.IReadRepository! +virtual MMCA.Common.Application.Users.UseCases.ChangePassword.ChangePasswordHandlerBase.HandlerName.get -> string! +virtual MMCA.Common.Application.Users.UseCases.ChangePreferences.ChangePreferencesHandlerBase.HandlerName.get -> string! +virtual MMCA.Common.Application.Users.UseCases.DeleteUser.DeleteUserHandlerBase.HandlerName.get -> string! +virtual MMCA.Common.Application.Users.UseCases.DeleteUser.DeleteUserHandlerBase.OnAfterSoftDeleteAsync(TUser! user, TCommand command, System.Collections.Generic.ICollection!>! afterCommit, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +virtual MMCA.Common.Application.Users.UseCases.ExportUserData.ExportUserDataHandlerBase.HandlerName.get -> string! +virtual MMCA.Common.Application.Users.UseCases.ExportUserData.ExportUserDataHandlerBase.OnExportCompletedAsync(TUser! user, TQuery query, MMCA.Common.Shared.Privacy.UserDataExportDTO! export, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +virtual MMCA.Common.Application.Users.UseCases.GetPreferences.GetUserPreferencesHandlerBase.HandlerName.get -> string! +~override MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey.Equals(object obj) -> bool diff --git a/Source/Core/MMCA.Common.Application/PublicAPI.Unshipped.txt b/Source/Core/MMCA.Common.Application/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/Source/Core/MMCA.Common.Application/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/Source/Core/MMCA.Common.Application/packages.lock.json b/Source/Core/MMCA.Common.Application/packages.lock.json index bb1d1b40..4c667e97 100644 --- a/Source/Core/MMCA.Common.Application/packages.lock.json +++ b/Source/Core/MMCA.Common.Application/packages.lock.json @@ -18,6 +18,12 @@ "resolved": "3.0.163", "contentHash": "l9iZ+OXBGlAFEKH1y4BSKPXHcmpJmikysSm0brjTau8fR7Qm7IVGLVig+dh0RH9mq4s3neZayuPQrHRweOpxxw==" }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[5.6.0, )", + "resolved": "5.6.0", + "contentHash": "W4kJGezNIKLzo0Ak5FAQDFvkMf2U7DtGL4THmHyRSApfKsKt5V+eX/bU0ZLKAt/uf9Bb2o1bi0YDKj/GRB/vYQ==" + }, "Microsoft.FeatureManagement": { "type": "Direct", "requested": "[4.6.0, )", diff --git a/Source/Core/MMCA.Common.Domain/PublicAPI.Shipped.txt b/Source/Core/MMCA.Common.Domain/PublicAPI.Shipped.txt new file mode 100644 index 00000000..684d45e8 --- /dev/null +++ b/Source/Core/MMCA.Common.Domain/PublicAPI.Shipped.txt @@ -0,0 +1,255 @@ +#nullable enable +MMCA.Common.Domain.AssemblyReference +MMCA.Common.Domain.Attributes.IdValueGeneratedAttribute +MMCA.Common.Domain.Attributes.IdValueGeneratedAttribute.IdValueGeneratedAttribute() -> void +MMCA.Common.Domain.Attributes.NavigationAttribute +MMCA.Common.Domain.Attributes.NavigationAttribute.IsCollection.get -> bool +MMCA.Common.Domain.Attributes.NavigationAttribute.IsCollection.init -> void +MMCA.Common.Domain.Attributes.NavigationAttribute.NavigationAttribute() -> void +MMCA.Common.Domain.Attributes.PiiAttribute +MMCA.Common.Domain.Attributes.PiiAttribute.PiiAttribute() -> void +MMCA.Common.Domain.Auth.IAuthUser +MMCA.Common.Domain.Auth.IAuthUser.PasswordHash.get -> byte[]! +MMCA.Common.Domain.Auth.IAuthUser.PasswordSalt.get -> byte[]! +MMCA.Common.Domain.Auth.IAuthUser.RefreshToken.get -> string? +MMCA.Common.Domain.Auth.IAuthUser.RefreshTokenExpiry.get -> System.DateTime? +MMCA.Common.Domain.Auth.IAuthUser.RevokeRefreshToken() -> void +MMCA.Common.Domain.Auth.IAuthUser.UpdateRefreshToken(string! refreshToken, System.DateTime expiry) -> void +MMCA.Common.Domain.Auth.IErasableUser +MMCA.Common.Domain.Auth.IErasableUser.Delete() -> MMCA.Common.Shared.Abstractions.Result! +MMCA.Common.Domain.Auth.IPasswordChangeableUser +MMCA.Common.Domain.Auth.IPasswordChangeableUser.ChangePassword(byte[]! newPasswordHash, byte[]! newPasswordSalt) -> MMCA.Common.Shared.Abstractions.Result! +MMCA.Common.Domain.Auth.IUserPreferences +MMCA.Common.Domain.Auth.IUserPreferences.PreferredCulture.get -> string? +MMCA.Common.Domain.Auth.IUserPreferences.PreferredTheme.get -> string? +MMCA.Common.Domain.Auth.IUserPreferences.UpdatePreferences(string? preferredCulture, string? preferredTheme) -> MMCA.Common.Shared.Abstractions.Result! +MMCA.Common.Domain.ClassReference +MMCA.Common.Domain.ClassReference.ClassReference() -> void +MMCA.Common.Domain.DomainEvents.BaseDomainEvent +MMCA.Common.Domain.DomainEvents.BaseDomainEvent.BaseDomainEvent() -> void +MMCA.Common.Domain.DomainEvents.BaseDomainEvent.BaseDomainEvent(MMCA.Common.Domain.DomainEvents.BaseDomainEvent! original) -> void +MMCA.Common.Domain.DomainEvents.BaseDomainEvent.DateOccurred.get -> System.DateTime +MMCA.Common.Domain.DomainEvents.BaseDomainEvent.DateOccurred.init -> void +MMCA.Common.Domain.DomainEvents.BaseDomainEvent.MessageId.get -> System.Guid +MMCA.Common.Domain.DomainEvents.BaseDomainEvent.MessageId.init -> void +MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent +MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent.BaseIntegrationEvent() -> void +MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent.BaseIntegrationEvent(MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent! original) -> void +MMCA.Common.Domain.DomainEvents.EntityChangedEvent +MMCA.Common.Domain.DomainEvents.EntityChangedEvent.Deconstruct(out MMCA.Common.Domain.Enums.DomainEntityState State, out TIdentifierType EntityId) -> void +MMCA.Common.Domain.DomainEvents.EntityChangedEvent.EntityChangedEvent(MMCA.Common.Domain.DomainEvents.EntityChangedEvent! original) -> void +MMCA.Common.Domain.DomainEvents.EntityChangedEvent.EntityChangedEvent(MMCA.Common.Domain.Enums.DomainEntityState State, TIdentifierType EntityId) -> void +MMCA.Common.Domain.DomainEvents.EntityChangedEvent.EntityId.get -> TIdentifierType +MMCA.Common.Domain.DomainEvents.EntityChangedEvent.EntityId.init -> void +MMCA.Common.Domain.DomainEvents.EntityChangedEvent.State.get -> MMCA.Common.Domain.Enums.DomainEntityState +MMCA.Common.Domain.DomainEvents.EntityChangedEvent.State.init -> void +MMCA.Common.Domain.Entities.AuditableAggregateRootEntity +MMCA.Common.Domain.Entities.AuditableAggregateRootEntity.AddDomainEvent(MMCA.Common.Domain.Interfaces.IDomainEvent! domainEvent) -> void +MMCA.Common.Domain.Entities.AuditableAggregateRootEntity.AuditableAggregateRootEntity() -> void +MMCA.Common.Domain.Entities.AuditableAggregateRootEntity.ClearDomainEvents() -> void +MMCA.Common.Domain.Entities.AuditableAggregateRootEntity.DomainEvents.get -> System.Collections.Generic.IReadOnlyCollection! +MMCA.Common.Domain.Entities.AuditableAggregateRootEntity.RemoveDomainEvents(System.Collections.Generic.IEnumerable! domainEvents) -> void +MMCA.Common.Domain.Entities.AuditableAggregateRootEntity.SetItems(System.Collections.Generic.List! collection, System.Collections.Generic.IEnumerable! items) -> void +MMCA.Common.Domain.Entities.AuditableBaseEntity +MMCA.Common.Domain.Entities.AuditableBaseEntity.AuditableBaseEntity() -> void +MMCA.Common.Domain.Entities.AuditableBaseEntity.RowVersion.get -> byte[]! +MMCA.Common.Domain.Entities.AuditableBaseEntity.Undelete() -> MMCA.Common.Shared.Abstractions.Result! +MMCA.Common.Domain.Entities.BaseEntity +MMCA.Common.Domain.Entities.BaseEntity.BaseEntity() -> void +MMCA.Common.Domain.Entities.BaseEntity.Id.get -> TIdentifierType +MMCA.Common.Domain.Entities.BaseEntity.Id.init -> void +MMCA.Common.Domain.Enums.DomainEntityState +MMCA.Common.Domain.Enums.DomainEntityState.Added = 1 -> MMCA.Common.Domain.Enums.DomainEntityState +MMCA.Common.Domain.Enums.DomainEntityState.Deleted = 3 -> MMCA.Common.Domain.Enums.DomainEntityState +MMCA.Common.Domain.Enums.DomainEntityState.Unchanged = 0 -> MMCA.Common.Domain.Enums.DomainEntityState +MMCA.Common.Domain.Enums.DomainEntityState.Updated = 2 -> MMCA.Common.Domain.Enums.DomainEntityState +MMCA.Common.Domain.Extensions.EntityTypeExtensions +MMCA.Common.Domain.Extensions.EntityTypeExtensions.extension(System.Type!) +MMCA.Common.Domain.Extensions.EntityTypeExtensions.extension(System.Type!).IsIdValueGenerated.get -> bool +MMCA.Common.Domain.Interfaces.IAggregateRoot +MMCA.Common.Domain.Interfaces.IAggregateRoot.AddDomainEvent(MMCA.Common.Domain.Interfaces.IDomainEvent! domainEvent) -> void +MMCA.Common.Domain.Interfaces.IAggregateRoot.ClearDomainEvents() -> void +MMCA.Common.Domain.Interfaces.IAggregateRoot.DomainEvents.get -> System.Collections.Generic.IReadOnlyCollection! +MMCA.Common.Domain.Interfaces.IAggregateRoot.RemoveDomainEvents(System.Collections.Generic.IEnumerable! domainEvents) -> void +MMCA.Common.Domain.Interfaces.IAnonymizable +MMCA.Common.Domain.Interfaces.IAnonymizable.Anonymize() -> MMCA.Common.Shared.Abstractions.Result! +MMCA.Common.Domain.Interfaces.IAuditableEntity +MMCA.Common.Domain.Interfaces.IAuditableEntity.CreatedBy.get -> int +MMCA.Common.Domain.Interfaces.IAuditableEntity.CreatedOn.get -> System.DateTime +MMCA.Common.Domain.Interfaces.IAuditableEntity.IsDeleted.get -> bool +MMCA.Common.Domain.Interfaces.IAuditableEntity.LastModifiedBy.get -> int? +MMCA.Common.Domain.Interfaces.IAuditableEntity.LastModifiedOn.get -> System.DateTime? +MMCA.Common.Domain.Interfaces.IAuditedEntity +MMCA.Common.Domain.Interfaces.IBaseEntity +MMCA.Common.Domain.Interfaces.IBaseEntity.Id.get -> TIdentifierType +MMCA.Common.Domain.Interfaces.IBaseEntity.Id.init -> void +MMCA.Common.Domain.Interfaces.IDomainEvent +MMCA.Common.Domain.Interfaces.IDomainEvent.DateOccurred.get -> System.DateTime +MMCA.Common.Domain.Interfaces.IDomainEvent.MessageId.get -> System.Guid +MMCA.Common.Domain.Interfaces.IIntegrationEvent +MMCA.Common.Domain.Interfaces.IRowVersioned +MMCA.Common.Domain.Interfaces.IRowVersioned.RowVersion.get -> byte[]! +MMCA.Common.Domain.Interfaces.ISpecification +MMCA.Common.Domain.Interfaces.ISpecification.Criteria.get -> System.Linq.Expressions.Expression!>! +MMCA.Common.Domain.Interfaces.ISpecification.IsSatisfiedBy(TEntity entity) -> bool +MMCA.Common.Domain.Interfaces.ITenantEntity +MMCA.Common.Domain.Interfaces.ITenantEntity.TenantId.get -> string! +MMCA.Common.Domain.Invariants.CommonInvariants +MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated +MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated.Deconstruct(out int NotificationId, out string! Title, out int RecipientCount) -> void +MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated.Equals(MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated? other) -> bool +MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated.NotificationId.get -> int +MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated.NotificationId.init -> void +MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated.PushNotificationCreated(int NotificationId, string! Title, int RecipientCount) -> void +MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated.RecipientCount.get -> int +MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated.RecipientCount.init -> void +MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated.Title.get -> string! +MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated.Title.init -> void +MMCA.Common.Domain.Notifications.PushNotifications.Invariants.PushNotificationInvariants +MMCA.Common.Domain.Notifications.PushNotifications.PushNotification +MMCA.Common.Domain.Notifications.PushNotifications.PushNotification.Body.get -> string! +MMCA.Common.Domain.Notifications.PushNotifications.PushNotification.DedupKey.get -> string? +MMCA.Common.Domain.Notifications.PushNotifications.PushNotification.MarkAsFailed() -> void +MMCA.Common.Domain.Notifications.PushNotifications.PushNotification.MarkAsSent() -> void +MMCA.Common.Domain.Notifications.PushNotifications.PushNotification.RecipientCount.get -> int +MMCA.Common.Domain.Notifications.PushNotifications.PushNotification.ScopeKey.get -> string? +MMCA.Common.Domain.Notifications.PushNotifications.PushNotification.SentByUserId.get -> int +MMCA.Common.Domain.Notifications.PushNotifications.PushNotification.Status.get -> MMCA.Common.Domain.Notifications.PushNotifications.PushNotificationStatus +MMCA.Common.Domain.Notifications.PushNotifications.PushNotification.Title.get -> string! +MMCA.Common.Domain.Notifications.PushNotifications.PushNotificationStatus +MMCA.Common.Domain.Notifications.PushNotifications.PushNotificationStatus.Failed = 2 -> MMCA.Common.Domain.Notifications.PushNotifications.PushNotificationStatus +MMCA.Common.Domain.Notifications.PushNotifications.PushNotificationStatus.Pending = 0 -> MMCA.Common.Domain.Notifications.PushNotifications.PushNotificationStatus +MMCA.Common.Domain.Notifications.PushNotifications.PushNotificationStatus.Sent = 1 -> MMCA.Common.Domain.Notifications.PushNotifications.PushNotificationStatus +MMCA.Common.Domain.Notifications.UserNotifications.UserNotification +MMCA.Common.Domain.Notifications.UserNotifications.UserNotification.IsRead.get -> bool +MMCA.Common.Domain.Notifications.UserNotifications.UserNotification.MarkAsRead(System.DateTime readOnUtc) -> void +MMCA.Common.Domain.Notifications.UserNotifications.UserNotification.PushNotificationId.get -> int +MMCA.Common.Domain.Notifications.UserNotifications.UserNotification.ReadOn.get -> System.DateTime? +MMCA.Common.Domain.Notifications.UserNotifications.UserNotification.UserId.get -> int +MMCA.Common.Domain.Privacy.PiiRedactor +MMCA.Common.Domain.Specifications.AndSpecification +MMCA.Common.Domain.Specifications.AndSpecification.AndSpecification(MMCA.Common.Domain.Interfaces.ISpecification! spec1, MMCA.Common.Domain.Interfaces.ISpecification! spec2) -> void +MMCA.Common.Domain.Specifications.InlineSpecification +MMCA.Common.Domain.Specifications.InlineSpecification.InlineSpecification(System.Linq.Expressions.Expression!>! criteria) -> void +MMCA.Common.Domain.Specifications.NotSpecification +MMCA.Common.Domain.Specifications.NotSpecification.NotSpecification(MMCA.Common.Domain.Interfaces.ISpecification! spec) -> void +MMCA.Common.Domain.Specifications.OrSpecification +MMCA.Common.Domain.Specifications.OrSpecification.OrSpecification(MMCA.Common.Domain.Interfaces.ISpecification! spec1, MMCA.Common.Domain.Interfaces.ISpecification! spec2) -> void +MMCA.Common.Domain.Specifications.OrderExpression +MMCA.Common.Domain.Specifications.OrderExpression.$() -> MMCA.Common.Domain.Specifications.OrderExpression! +MMCA.Common.Domain.Specifications.OrderExpression.Deconstruct(out System.Linq.Expressions.LambdaExpression! KeySelector, out bool Descending) -> void +MMCA.Common.Domain.Specifications.OrderExpression.Descending.get -> bool +MMCA.Common.Domain.Specifications.OrderExpression.Descending.init -> void +MMCA.Common.Domain.Specifications.OrderExpression.Equals(MMCA.Common.Domain.Specifications.OrderExpression? other) -> bool +MMCA.Common.Domain.Specifications.OrderExpression.KeySelector.get -> System.Linq.Expressions.LambdaExpression! +MMCA.Common.Domain.Specifications.OrderExpression.KeySelector.init -> void +MMCA.Common.Domain.Specifications.OrderExpression.OrderExpression(System.Linq.Expressions.LambdaExpression! KeySelector, bool Descending) -> void +MMCA.Common.Domain.Specifications.OwnedByUserSpecification +MMCA.Common.Domain.Specifications.OwnedByUserSpecification.OwnedByUserSpecification(int userId) -> void +MMCA.Common.Domain.Specifications.OwnedByUserSpecification.UserId.get -> int +MMCA.Common.Domain.Specifications.QuerySpecification +MMCA.Common.Domain.Specifications.QuerySpecification.AddInclude(string! path) -> void +MMCA.Common.Domain.Specifications.QuerySpecification.AddOrderBy(System.Linq.Expressions.Expression!>! keySelector, bool descending = false) -> void +MMCA.Common.Domain.Specifications.QuerySpecification.ApplyPaging(int skip, int take) -> void +MMCA.Common.Domain.Specifications.QuerySpecification.AsTracking.get -> bool +MMCA.Common.Domain.Specifications.QuerySpecification.IgnoreQueryFilters.get -> bool +MMCA.Common.Domain.Specifications.QuerySpecification.IncludePaths.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Domain.Specifications.QuerySpecification.OrderBy.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Domain.Specifications.QuerySpecification.QuerySpecification() -> void +MMCA.Common.Domain.Specifications.QuerySpecification.Skip.get -> int? +MMCA.Common.Domain.Specifications.QuerySpecification.Take.get -> int? +MMCA.Common.Domain.Specifications.QuerySpecification.WithSoftDeleted() -> void +MMCA.Common.Domain.Specifications.QuerySpecification.WithTracking() -> void +MMCA.Common.Domain.Specifications.Specification +MMCA.Common.Domain.Specifications.Specification.Specification() -> void +MMCA.Common.Domain.Specifications.SpecificationExtensions +MMCA.Common.Domain.Specifications.SpecificationExtensions.extension(MMCA.Common.Domain.Interfaces.ISpecification!) +MMCA.Common.Domain.Specifications.SpecificationExtensions.extension(MMCA.Common.Domain.Interfaces.ISpecification!).And(MMCA.Common.Domain.Interfaces.ISpecification! other) -> MMCA.Common.Domain.Specifications.AndSpecification! +MMCA.Common.Domain.Specifications.SpecificationExtensions.extension(MMCA.Common.Domain.Interfaces.ISpecification!).Not() -> MMCA.Common.Domain.Specifications.NotSpecification! +MMCA.Common.Domain.Specifications.SpecificationExtensions.extension(MMCA.Common.Domain.Interfaces.ISpecification!).Or(MMCA.Common.Domain.Interfaces.ISpecification! other) -> MMCA.Common.Domain.Specifications.OrSpecification! +abstract MMCA.Common.Domain.DomainEvents.BaseDomainEvent.$() -> MMCA.Common.Domain.DomainEvents.BaseDomainEvent! +abstract MMCA.Common.Domain.Specifications.Specification.Criteria.get -> System.Linq.Expressions.Expression!>! +const MMCA.Common.Domain.Invariants.CommonInvariants.DarkTheme = "dark" -> string! +const MMCA.Common.Domain.Invariants.CommonInvariants.LightTheme = "light" -> string! +const MMCA.Common.Domain.Notifications.PushNotifications.PushNotification.DedupKeyMaxLength = 128 -> int +const MMCA.Common.Domain.Notifications.PushNotifications.PushNotification.ScopeKeyMaxLength = 128 -> int +const MMCA.Common.Domain.Privacy.PiiRedactor.RedactedToken = "[REDACTED]" -> string! +override MMCA.Common.Domain.DomainEvents.BaseDomainEvent.Equals(object? obj) -> bool +override MMCA.Common.Domain.DomainEvents.BaseDomainEvent.GetHashCode() -> int +override MMCA.Common.Domain.DomainEvents.BaseDomainEvent.ToString() -> string! +override MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent.EqualityContract.get -> System.Type! +override MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent.Equals(object? obj) -> bool +override MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent.GetHashCode() -> int +override MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent.PrintMembers(System.Text.StringBuilder! builder) -> bool +override MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent.ToString() -> string! +override MMCA.Common.Domain.DomainEvents.EntityChangedEvent.EqualityContract.get -> System.Type! +override MMCA.Common.Domain.DomainEvents.EntityChangedEvent.Equals(object? obj) -> bool +override MMCA.Common.Domain.DomainEvents.EntityChangedEvent.GetHashCode() -> int +override MMCA.Common.Domain.DomainEvents.EntityChangedEvent.PrintMembers(System.Text.StringBuilder! builder) -> bool +override MMCA.Common.Domain.DomainEvents.EntityChangedEvent.ToString() -> string! +override MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated.$() -> MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated! +override MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated.Equals(object? obj) -> bool +override MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated.GetHashCode() -> int +override MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated.ToString() -> string! +override MMCA.Common.Domain.Specifications.AndSpecification.Criteria.get -> System.Linq.Expressions.Expression!>! +override MMCA.Common.Domain.Specifications.InlineSpecification.Criteria.get -> System.Linq.Expressions.Expression!>! +override MMCA.Common.Domain.Specifications.NotSpecification.Criteria.get -> System.Linq.Expressions.Expression!>! +override MMCA.Common.Domain.Specifications.OrSpecification.Criteria.get -> System.Linq.Expressions.Expression!>! +override MMCA.Common.Domain.Specifications.OrderExpression.Equals(object? obj) -> bool +override MMCA.Common.Domain.Specifications.OrderExpression.GetHashCode() -> int +override MMCA.Common.Domain.Specifications.OrderExpression.ToString() -> string! +override MMCA.Common.Domain.Specifications.OwnedByUserSpecification.Criteria.get -> System.Linq.Expressions.Expression!>! +override abstract MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent.$() -> MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent! +override abstract MMCA.Common.Domain.DomainEvents.EntityChangedEvent.$() -> MMCA.Common.Domain.DomainEvents.EntityChangedEvent! +override sealed MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent.Equals(MMCA.Common.Domain.DomainEvents.BaseDomainEvent? other) -> bool +override sealed MMCA.Common.Domain.DomainEvents.EntityChangedEvent.Equals(MMCA.Common.Domain.DomainEvents.BaseDomainEvent? other) -> bool +override sealed MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated.Equals(MMCA.Common.Domain.DomainEvents.BaseDomainEvent? other) -> bool +static MMCA.Common.Domain.DomainEvents.BaseDomainEvent.operator !=(MMCA.Common.Domain.DomainEvents.BaseDomainEvent? left, MMCA.Common.Domain.DomainEvents.BaseDomainEvent? right) -> bool +static MMCA.Common.Domain.DomainEvents.BaseDomainEvent.operator ==(MMCA.Common.Domain.DomainEvents.BaseDomainEvent? left, MMCA.Common.Domain.DomainEvents.BaseDomainEvent? right) -> bool +static MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent.operator !=(MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent? left, MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent? right) -> bool +static MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent.operator ==(MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent? left, MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent? right) -> bool +static MMCA.Common.Domain.DomainEvents.EntityChangedEvent.operator !=(MMCA.Common.Domain.DomainEvents.EntityChangedEvent? left, MMCA.Common.Domain.DomainEvents.EntityChangedEvent? right) -> bool +static MMCA.Common.Domain.DomainEvents.EntityChangedEvent.operator ==(MMCA.Common.Domain.DomainEvents.EntityChangedEvent? left, MMCA.Common.Domain.DomainEvents.EntityChangedEvent? right) -> bool +static MMCA.Common.Domain.Entities.AuditableAggregateRootEntity.GetChildOrNotFound(System.Collections.Generic.IEnumerable! collection, TChildId childId, string! source) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Domain.Extensions.EntityTypeExtensions.get_IsIdValueGenerated(System.Type! entityType) -> bool +static MMCA.Common.Domain.Invariants.CommonInvariants.EnsureBytesAreNotEmpty(byte[]! value, string! code, string! message, string! source, string! target) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Domain.Invariants.CommonInvariants.EnsureCollectionIsNotEmpty(System.Collections.Generic.IReadOnlyCollection! value, string! code, string! message, string! source, string! target) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Domain.Invariants.CommonInvariants.EnsureIdIsNotDefault(TId id, string! code, string! message, string! source, string! target) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Domain.Invariants.CommonInvariants.EnsureIntIsPositive(int value, string! code, string! message, string! source, string! target) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Domain.Invariants.CommonInvariants.EnsureMoneyIsNotNegative(MMCA.Common.Shared.ValueObjects.Money! value, string! code, string! message, string! source, string! target) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Domain.Invariants.CommonInvariants.EnsurePreferredCultureIsValid(string? culture, string! code, string! message, string! source, string! target) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Domain.Invariants.CommonInvariants.EnsurePreferredThemeIsValid(string? theme, string! code, string! message, string! source, string! target) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Domain.Invariants.CommonInvariants.EnsureStringIsNotEmpty(string! value, string! code, string! message, string! source, string! target) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Domain.Invariants.CommonInvariants.EnsureStringMaxLength(string? value, int maxLength, string! code, string! message, string! source, string! target) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated.operator !=(MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated? left, MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated? right) -> bool +static MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated.operator ==(MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated? left, MMCA.Common.Domain.Notifications.PushNotifications.DomainEvents.PushNotificationCreated? right) -> bool +static MMCA.Common.Domain.Notifications.PushNotifications.Invariants.PushNotificationInvariants.EnsureBodyIsValid(string! body, string! source) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Domain.Notifications.PushNotifications.Invariants.PushNotificationInvariants.EnsureTitleIsValid(string! title, string! source) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Domain.Notifications.PushNotifications.PushNotification.Create(string! title, string! body, int sentByUserId, int recipientCount, string? dedupKey = null, string? scopeKey = null) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Domain.Notifications.UserNotifications.UserNotification.Create(int userId, int pushNotificationId) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Domain.Privacy.PiiRedactor.HasPii(System.Type! type) -> bool +static MMCA.Common.Domain.Privacy.PiiRedactor.Redact(object? value) -> System.Collections.Generic.IReadOnlyDictionary! +static MMCA.Common.Domain.Privacy.PiiRedactor.RedactToString(object? value) -> string! +static MMCA.Common.Domain.Specifications.OrderExpression.operator !=(MMCA.Common.Domain.Specifications.OrderExpression? left, MMCA.Common.Domain.Specifications.OrderExpression? right) -> bool +static MMCA.Common.Domain.Specifications.OrderExpression.operator ==(MMCA.Common.Domain.Specifications.OrderExpression? left, MMCA.Common.Domain.Specifications.OrderExpression? right) -> bool +static MMCA.Common.Domain.Specifications.SpecificationExtensions.And(this MMCA.Common.Domain.Interfaces.ISpecification! specification, MMCA.Common.Domain.Interfaces.ISpecification! other) -> MMCA.Common.Domain.Specifications.AndSpecification! +static MMCA.Common.Domain.Specifications.SpecificationExtensions.Not(this MMCA.Common.Domain.Interfaces.ISpecification! specification) -> MMCA.Common.Domain.Specifications.NotSpecification! +static MMCA.Common.Domain.Specifications.SpecificationExtensions.Or(this MMCA.Common.Domain.Interfaces.ISpecification! specification, MMCA.Common.Domain.Interfaces.ISpecification! other) -> MMCA.Common.Domain.Specifications.OrSpecification! +static readonly MMCA.Common.Domain.AssemblyReference.Assembly -> System.Reflection.Assembly! +static readonly MMCA.Common.Domain.AssemblyReference.AssemblyName -> string! +static readonly MMCA.Common.Domain.Notifications.PushNotifications.Invariants.PushNotificationInvariants.BodyMaxLength -> int +static readonly MMCA.Common.Domain.Notifications.PushNotifications.Invariants.PushNotificationInvariants.TitleMaxLength -> int +virtual MMCA.Common.Domain.DomainEvents.BaseDomainEvent.EqualityContract.get -> System.Type! +virtual MMCA.Common.Domain.DomainEvents.BaseDomainEvent.Equals(MMCA.Common.Domain.DomainEvents.BaseDomainEvent? other) -> bool +virtual MMCA.Common.Domain.DomainEvents.BaseDomainEvent.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent.Equals(MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent? other) -> bool +virtual MMCA.Common.Domain.DomainEvents.BaseIntegrationEvent.SchemaVersion.get -> int +virtual MMCA.Common.Domain.DomainEvents.EntityChangedEvent.Equals(MMCA.Common.Domain.DomainEvents.EntityChangedEvent? other) -> bool +virtual MMCA.Common.Domain.Entities.AuditableAggregateRootEntity.ValidateSetItems(System.Collections.Generic.IList! currentItems, System.Collections.Generic.IList! incomingItems) -> void +virtual MMCA.Common.Domain.Entities.AuditableBaseEntity.CreatedBy.get -> int +virtual MMCA.Common.Domain.Entities.AuditableBaseEntity.CreatedOn.get -> System.DateTime +virtual MMCA.Common.Domain.Entities.AuditableBaseEntity.Delete() -> MMCA.Common.Shared.Abstractions.Result! +virtual MMCA.Common.Domain.Entities.AuditableBaseEntity.IsDeleted.get -> bool +virtual MMCA.Common.Domain.Entities.AuditableBaseEntity.LastModifiedBy.get -> int? +virtual MMCA.Common.Domain.Entities.AuditableBaseEntity.LastModifiedOn.get -> System.DateTime? +virtual MMCA.Common.Domain.Specifications.Specification.IsSatisfiedBy(TEntity entity) -> bool diff --git a/Source/Core/MMCA.Common.Domain/PublicAPI.Unshipped.txt b/Source/Core/MMCA.Common.Domain/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/Source/Core/MMCA.Common.Domain/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/Source/Core/MMCA.Common.Domain/packages.lock.json b/Source/Core/MMCA.Common.Domain/packages.lock.json index 72d21cbc..cd291fb1 100644 --- a/Source/Core/MMCA.Common.Domain/packages.lock.json +++ b/Source/Core/MMCA.Common.Domain/packages.lock.json @@ -8,6 +8,12 @@ "resolved": "3.0.163", "contentHash": "l9iZ+OXBGlAFEKH1y4BSKPXHcmpJmikysSm0brjTau8fR7Qm7IVGLVig+dh0RH9mq4s3neZayuPQrHRweOpxxw==" }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[5.6.0, )", + "resolved": "5.6.0", + "contentHash": "W4kJGezNIKLzo0Ak5FAQDFvkMf2U7DtGL4THmHyRSApfKsKt5V+eX/bU0ZLKAt/uf9Bb2o1bi0YDKj/GRB/vYQ==" + }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", "requested": "[18.7.23, )", diff --git a/Source/Core/MMCA.Common.Infrastructure/Hubs/NotificationHub.cs b/Source/Core/MMCA.Common.Infrastructure/Hubs/NotificationHub.cs index 72dc1c57..462aaa9a 100644 --- a/Source/Core/MMCA.Common.Infrastructure/Hubs/NotificationHub.cs +++ b/Source/Core/MMCA.Common.Infrastructure/Hubs/NotificationHub.cs @@ -44,7 +44,12 @@ public sealed class NotificationHub(IOptions settings) public async Task JoinChannelAsync(string channelKey) { EnsureValidChannelKey(channelKey); - await Groups.AddToGroupAsync(Context.ConnectionId, channelKey).ConfigureAwait(false); + + // The cancellation token comes from the connection rather than a method parameter: the hub + // method signature is the client-visible RPC contract, bound by SignalR's dispatcher, so it + // carries no CancellationToken argument (see CancellationTokenConventionTests' exemption). + await Groups.AddToGroupAsync(Context.ConnectionId, channelKey, Context.ConnectionAborted) + .ConfigureAwait(false); } /// Removes the calling connection from a channel (SignalR group). @@ -55,7 +60,10 @@ public async Task JoinChannelAsync(string channelKey) public async Task LeaveChannelAsync(string channelKey) { EnsureValidChannelKey(channelKey); - await Groups.RemoveFromGroupAsync(Context.ConnectionId, channelKey).ConfigureAwait(false); + + // Same as JoinChannelAsync: the token is the connection's, not a parameter on the RPC contract. + await Groups.RemoveFromGroupAsync(Context.ConnectionId, channelKey, Context.ConnectionAborted) + .ConfigureAwait(false); } private void EnsureValidChannelKey(string channelKey) diff --git a/Source/Core/MMCA.Common.Infrastructure/PublicAPI.Shipped.txt b/Source/Core/MMCA.Common.Infrastructure/PublicAPI.Shipped.txt new file mode 100644 index 00000000..07c2d38e --- /dev/null +++ b/Source/Core/MMCA.Common.Infrastructure/PublicAPI.Shipped.txt @@ -0,0 +1,844 @@ +#nullable enable +MMCA.Common.Infrastructure.AssemblyReference +MMCA.Common.Infrastructure.Auth.IJwksProvider +MMCA.Common.Infrastructure.Auth.IJwksProvider.GetJsonWebKeySet() -> Microsoft.IdentityModel.Tokens.JsonWebKeySet! +MMCA.Common.Infrastructure.Auth.LoginProtectionService +MMCA.Common.Infrastructure.Auth.LoginProtectionService.CheckLockoutAsync(string! email, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Auth.LoginProtectionService.CheckRegistrationRateLimitAsync(string? ipAddress, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Auth.LoginProtectionService.IncrementFailedAttemptsAsync(string! email, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Auth.LoginProtectionService.IncrementRegistrationCountAsync(string? ipAddress, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Auth.LoginProtectionService.LoginProtectionService(MMCA.Common.Application.Interfaces.ICacheService! cacheService, Microsoft.Extensions.Options.IOptions! settings) -> void +MMCA.Common.Infrastructure.Auth.LoginProtectionService.ResetFailedAttemptsAsync(string! email, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Auth.LoginProtectionSettings +MMCA.Common.Infrastructure.Auth.LoginProtectionSettings.FailedAttemptWindowMinutes.get -> int +MMCA.Common.Infrastructure.Auth.LoginProtectionSettings.FailedAttemptWindowMinutes.init -> void +MMCA.Common.Infrastructure.Auth.LoginProtectionSettings.LoginProtectionSettings() -> void +MMCA.Common.Infrastructure.Auth.LoginProtectionSettings.MaxFailedAttempts.get -> int +MMCA.Common.Infrastructure.Auth.LoginProtectionSettings.MaxFailedAttempts.init -> void +MMCA.Common.Infrastructure.Auth.LoginProtectionSettings.MaxLockoutSeconds.get -> int +MMCA.Common.Infrastructure.Auth.LoginProtectionSettings.MaxLockoutSeconds.init -> void +MMCA.Common.Infrastructure.Auth.LoginProtectionSettings.MaxRegistrationsPerIpPerHour.get -> int +MMCA.Common.Infrastructure.Auth.LoginProtectionSettings.MaxRegistrationsPerIpPerHour.init -> void +MMCA.Common.Infrastructure.Auth.LoginProtectionSettings.RegistrationRateLimitWindowMinutes.get -> int +MMCA.Common.Infrastructure.Auth.LoginProtectionSettings.RegistrationRateLimitWindowMinutes.init -> void +MMCA.Common.Infrastructure.Auth.RsaJwksProvider +MMCA.Common.Infrastructure.Auth.RsaJwksProvider.GetJsonWebKeySet() -> Microsoft.IdentityModel.Tokens.JsonWebKeySet! +MMCA.Common.Infrastructure.Auth.RsaJwksProvider.RsaJwksProvider(Microsoft.Extensions.Options.IOptions! options) -> void +MMCA.Common.Infrastructure.Caching.CacheKeyPrefixOptions +MMCA.Common.Infrastructure.Caching.CacheKeyPrefixOptions.CacheKeyPrefixOptions() -> void +MMCA.Common.Infrastructure.Caching.CacheKeyPrefixOptions.KeyPrefix.get -> string! +MMCA.Common.Infrastructure.Caching.CacheKeyPrefixOptions.KeyPrefix.init -> void +MMCA.Common.Infrastructure.Caching.CacheOptions +MMCA.Common.Infrastructure.ClassReference +MMCA.Common.Infrastructure.ClassReference.ClassReference() -> void +MMCA.Common.Infrastructure.DependencyInjection +MMCA.Common.Infrastructure.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!) +MMCA.Common.Infrastructure.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddAuditTrail(Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Infrastructure.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddAzureBlobFileStorage(Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Infrastructure.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddBrokerMessaging(Microsoft.Extensions.Configuration.IConfiguration! configuration, System.Action? configureConsumers = null) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Infrastructure.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddCaching(Microsoft.Extensions.Configuration.IConfiguration? configuration = null) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Infrastructure.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddCommonHybridCache(System.Action? configure = null) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Infrastructure.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddEntityConfigurationAssembly(System.Reflection.Assembly! assembly) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Infrastructure.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddInfrastructure(Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Infrastructure.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddMultiTenancy(Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Infrastructure.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddNativePushNotifications(Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Infrastructure.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddNotificationInfrastructure() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Infrastructure.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddPushNotifications(Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Infrastructure.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddScheduledJob() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Infrastructure.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddScheduledJobs(Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Infrastructure.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddServices() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Infrastructure.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddTypedServiceClient(string! serviceName) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +MMCA.Common.Infrastructure.Http.JwtForwardingDelegatingHandler +MMCA.Common.Infrastructure.Http.JwtForwardingDelegatingHandler.JwtForwardingDelegatingHandler(Microsoft.AspNetCore.Http.IHttpContextAccessor! httpContextAccessor) -> void +MMCA.Common.Infrastructure.Hubs.NotificationHub +MMCA.Common.Infrastructure.Hubs.NotificationHub.JoinChannelAsync(string! channelKey) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Hubs.NotificationHub.LeaveChannelAsync(string! channelKey) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Hubs.NotificationHub.NotificationHub(Microsoft.Extensions.Options.IOptions! settings) -> void +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.AuditTrailEntry() -> void +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.ChangedBy.get -> int? +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.ChangedBy.init -> void +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.ChangedOn.get -> System.DateTime +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.ChangedOn.init -> void +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.CorrelationId.get -> string? +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.CorrelationId.init -> void +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.EntityKey.get -> string! +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.EntityKey.set -> void +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.EntityType.get -> string! +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.EntityType.init -> void +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.Id.get -> System.Guid +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.Id.init -> void +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.NewValue.get -> string? +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.NewValue.init -> void +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.OldValue.get -> string? +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.OldValue.init -> void +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.Operation.get -> string! +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.Operation.init -> void +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.PropertyName.get -> string? +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.PropertyName.init -> void +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.TenantId.get -> string? +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailEntry.TenantId.init -> void +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailSaveChangesInterceptor +MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailSaveChangesInterceptor.AuditTrailSaveChangesInterceptor(System.TimeProvider! timeProvider) -> void +MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeBuilderExtensions +MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeBuilderExtensions.extension(Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder!) +MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeBuilderExtensions.extension(Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder!).OwnsMoney(System.Linq.Expressions.Expression!>! navigationExpression, string! amountColumnName, string! currencyColumnName, bool required = true) -> Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder! +MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeConfiguration.EntityTypeConfiguration +MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeConfiguration.EntityTypeConfiguration.EntityTypeConfiguration() -> void +MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeConfiguration.EntityTypeConfigurationBase +MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeConfiguration.EntityTypeConfigurationBase.EntityTypeConfigurationBase() -> void +MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeConfiguration.EntityTypeConfigurationCosmos +MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeConfiguration.EntityTypeConfigurationCosmos.EntityTypeConfigurationCosmos() -> void +MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeConfiguration.EntityTypeConfigurationSQLServer +MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeConfiguration.EntityTypeConfigurationSQLServer.EntityTypeConfigurationSQLServer() -> void +MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeConfiguration.EntityTypeConfigurationSqlite +MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeConfiguration.EntityTypeConfigurationSqlite.EntityTypeConfigurationSqlite() -> void +MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeConfiguration.IEntityTypeConfigurationBase +MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeConfiguration.IEntityTypeConfigurationBase.Configure(Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder! builder) -> void +MMCA.Common.Infrastructure.Persistence.Configuration.IndexBuilderExtensions +MMCA.Common.Infrastructure.Persistence.Configuration.IndexBuilderExtensions.extension(Microsoft.EntityFrameworkCore.Metadata.Builders.IndexBuilder!) +MMCA.Common.Infrastructure.Persistence.Configuration.IndexBuilderExtensions.extension(Microsoft.EntityFrameworkCore.Metadata.Builders.IndexBuilder!).HasSoftDeleteFilter(MMCA.Common.Application.Interfaces.Infrastructure.DataSource engine = MMCA.Common.Application.Interfaces.Infrastructure.DataSource.SQLServer, string? additionalFilter = null) -> Microsoft.EntityFrameworkCore.Metadata.Builders.IndexBuilder! +MMCA.Common.Infrastructure.Persistence.Conventions.CrossDataSourceDegradeConvention +MMCA.Common.Infrastructure.Persistence.Conventions.CrossDataSourceDegradeConvention.CrossDataSourceDegradeConvention(MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey contextKey, MMCA.Common.Infrastructure.Persistence.DataSources.IEntityDataSourceRegistry! registry) -> void +MMCA.Common.Infrastructure.Persistence.Conventions.CrossDataSourceDegradeConvention.ProcessModelFinalizing(Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionModelBuilder! modelBuilder, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConventionContext! context) -> void +MMCA.Common.Infrastructure.Persistence.Conventions.SoftDeleteUniqueIndexConvention +MMCA.Common.Infrastructure.Persistence.Conventions.SoftDeleteUniqueIndexConvention.ProcessModelFinalizing(Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionModelBuilder! modelBuilder, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConventionContext! context) -> void +MMCA.Common.Infrastructure.Persistence.Conventions.SoftDeleteUniqueIndexConvention.SoftDeleteUniqueIndexConvention(MMCA.Common.Application.Interfaces.Infrastructure.DataSource engine) -> void +MMCA.Common.Infrastructure.Persistence.Conversions.EmailValueConverter +MMCA.Common.Infrastructure.Persistence.Conversions.EmailValueConverter.EmailValueConverter() -> void +MMCA.Common.Infrastructure.Persistence.Conversions.EnumerationValueConverter +MMCA.Common.Infrastructure.Persistence.Conversions.EnumerationValueConverter.EnumerationValueConverter() -> void +MMCA.Common.Infrastructure.Persistence.Conversions.NullableEmailValueConverter +MMCA.Common.Infrastructure.Persistence.Conversions.NullableEmailValueConverter.NullableEmailValueConverter() -> void +MMCA.Common.Infrastructure.Persistence.Conversions.NullableEnumerationValueConverter +MMCA.Common.Infrastructure.Persistence.Conversions.NullableEnumerationValueConverter.NullableEnumerationValueConverter() -> void +MMCA.Common.Infrastructure.Persistence.Conversions.NullablePhoneNumberValueConverter +MMCA.Common.Infrastructure.Persistence.Conversions.NullablePhoneNumberValueConverter.NullablePhoneNumberValueConverter() -> void +MMCA.Common.Infrastructure.Persistence.Conversions.PhoneNumberValueConverter +MMCA.Common.Infrastructure.Persistence.Conversions.PhoneNumberValueConverter.PhoneNumberValueConverter() -> void +MMCA.Common.Infrastructure.Persistence.DataSources.DataSourceResolver +MMCA.Common.Infrastructure.Persistence.DataSources.DataSourceResolver.DataSourceResolver(MMCA.Common.Infrastructure.Settings.IConnectionStringSettings! connectionStrings, MMCA.Common.Infrastructure.Settings.DataSourcesSettings! dataSources, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.Infrastructure.Persistence.DataSources.DataSourceResolver.GetPhysical(MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey key) -> MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource! +MMCA.Common.Infrastructure.Persistence.DataSources.DataSourceResolver.ResolveLogical(MMCA.Common.Application.Interfaces.Infrastructure.DataSource engine, string! logicalName) -> MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey +MMCA.Common.Infrastructure.Persistence.DataSources.EntityDataSourceRegistry +MMCA.Common.Infrastructure.Persistence.DataSources.EntityDataSourceRegistry.EntityDataSourceRegistry(MMCA.Common.Application.Interfaces.Infrastructure.IEntityConfigurationAssemblyProvider! assemblyProvider, MMCA.Common.Infrastructure.Persistence.DataSources.IDataSourceResolver! resolver) -> void +MMCA.Common.Infrastructure.Persistence.DataSources.EntityDataSourceRegistry.GetDataSourceKey(System.Type! entityType) -> MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey +MMCA.Common.Infrastructure.Persistence.DataSources.EntityDataSourceRegistry.GetDataSourceKey(string! entityFullName) -> MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey +MMCA.Common.Infrastructure.Persistence.DataSources.EntityDataSourceRegistry.GetPhysicalSourcesInUse() -> System.Collections.Generic.IReadOnlyCollection! +MMCA.Common.Infrastructure.Persistence.DataSources.EntityDataSourceRegistry.TryGetDataSourceKey(string! entityFullName, out MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey key) -> bool +MMCA.Common.Infrastructure.Persistence.DataSources.IDataSourceResolver +MMCA.Common.Infrastructure.Persistence.DataSources.IDataSourceResolver.GetPhysical(MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey key) -> MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource! +MMCA.Common.Infrastructure.Persistence.DataSources.IDataSourceResolver.ResolveLogical(MMCA.Common.Application.Interfaces.Infrastructure.DataSource engine, string! logicalName) -> MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey +MMCA.Common.Infrastructure.Persistence.DataSources.IEntityDataSourceRegistry +MMCA.Common.Infrastructure.Persistence.DataSources.IEntityDataSourceRegistry.GetDataSourceKey(System.Type! entityType) -> MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey +MMCA.Common.Infrastructure.Persistence.DataSources.IEntityDataSourceRegistry.GetDataSourceKey(string! entityFullName) -> MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey +MMCA.Common.Infrastructure.Persistence.DataSources.IEntityDataSourceRegistry.GetPhysicalSourcesInUse() -> System.Collections.Generic.IReadOnlyCollection! +MMCA.Common.Infrastructure.Persistence.DataSources.IEntityDataSourceRegistry.TryGetDataSourceKey(string! entityFullName, out MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey key) -> bool +MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource +MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource.$() -> MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource! +MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource.ConnectionString.get -> string! +MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource.ConnectionString.init -> void +MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource.CosmosDatabaseName.get -> string! +MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource.CosmosDatabaseName.init -> void +MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource.Deconstruct(out MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey Key, out string! ConnectionString, out string? SqlServerMigrationsAssembly, out string! CosmosDatabaseName) -> void +MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource.Equals(MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource? other) -> bool +MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource.Key.get -> MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey +MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource.Key.init -> void +MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource.PhysicalDataSource(MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey Key, string! ConnectionString, string? SqlServerMigrationsAssembly, string! CosmosDatabaseName) -> void +MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource.SqlServerMigrationsAssembly.get -> string? +MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource.SqlServerMigrationsAssembly.init -> void +MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget +MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget.Deconstruct(out MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey Source, out string? TenantId) -> void +MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget.Equals(MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget other) -> bool +MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget.Source.get -> MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey +MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget.Source.init -> void +MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget.TenantDataSourceTarget() -> void +MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget.TenantDataSourceTarget(MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey Source, string? TenantId) -> void +MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget.TenantId.get -> string? +MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget.TenantId.init -> void +MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTargets +MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext +MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext.ApplicationDbContext(Microsoft.EntityFrameworkCore.DbContextOptions! options, System.IServiceProvider! serviceProvider, MMCA.Common.Application.Interfaces.Infrastructure.IEntityConfigurationAssemblyProvider! assemblyProvider, MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource! physicalDataSource) -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext.ApplyConfigurationsForEntitiesInContext(MMCA.Common.Application.Interfaces.Infrastructure.DataSource dataSource, Microsoft.EntityFrameworkCore.ModelBuilder! modelBuilder) -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext.ApplyTenantFilters(Microsoft.EntityFrameworkCore.ModelBuilder! modelBuilder) -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext.ConfigureConcurrencyTokens(Microsoft.EntityFrameworkCore.ModelBuilder! modelBuilder) -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext.CurrentTenantId.get -> string? +MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext.DataSourceKey.get -> MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey +MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext.SaveChanges(int? userId) -> int +MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext.SaveChangesAsync(int? userId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.DbContexts.CosmosDbContext +MMCA.Common.Infrastructure.Persistence.DbContexts.CosmosDbContext.CosmosDbContext(Microsoft.EntityFrameworkCore.DbContextOptions! options, System.IServiceProvider! serviceProvider, MMCA.Common.Application.Interfaces.Infrastructure.IEntityConfigurationAssemblyProvider! assemblyProvider, MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource! physicalDataSource) -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.DataSourceModelCacheKeyFactory +MMCA.Common.Infrastructure.Persistence.DbContexts.DataSourceModelCacheKeyFactory.Create(Microsoft.EntityFrameworkCore.DbContext! context, bool designTime) -> object! +MMCA.Common.Infrastructure.Persistence.DbContexts.DataSourceModelCacheKeyFactory.DataSourceModelCacheKeyFactory() -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Design.DesignTimeDbContextHelper +MMCA.Common.Infrastructure.Persistence.DbContexts.Design.DesignTimeDbContextOptions +MMCA.Common.Infrastructure.Persistence.DbContexts.Design.DesignTimeDbContextOptions.AddConfigurationAssembly(System.Reflection.Assembly! assembly) -> MMCA.Common.Infrastructure.Persistence.DbContexts.Design.DesignTimeDbContextOptions! +MMCA.Common.Infrastructure.Persistence.DbContexts.Design.DesignTimeDbContextOptions.ConfigurationAssemblies.get -> System.Collections.Generic.IList! +MMCA.Common.Infrastructure.Persistence.DbContexts.Design.DesignTimeDbContextOptions.ConnectionStrings.get -> MMCA.Common.Infrastructure.Settings.ConnectionStringSettings! +MMCA.Common.Infrastructure.Persistence.DbContexts.Design.DesignTimeDbContextOptions.ConnectionStrings.set -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Design.DesignTimeDbContextOptions.DataSourceName.get -> string? +MMCA.Common.Infrastructure.Persistence.DbContexts.Design.DesignTimeDbContextOptions.DataSourceName.set -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Design.DesignTimeDbContextOptions.DataSources.get -> System.Collections.Generic.Dictionary! +MMCA.Common.Infrastructure.Persistence.DbContexts.Design.DesignTimeDbContextOptions.DesignTimeDbContextOptions() -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Design.DesignTimeDbContextOptions.EnableAuditTrail.get -> bool +MMCA.Common.Infrastructure.Persistence.DbContexts.Design.DesignTimeDbContextOptions.EnableAuditTrail.set -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Design.DesignTimeDbContextOptions.EnableScheduler.get -> bool +MMCA.Common.Infrastructure.Persistence.DbContexts.Design.DesignTimeDbContextOptions.EnableScheduler.set -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.ApplicationDbContextEFFactory +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.ApplicationDbContextEFFactory.ApplicationDbContextEFFactory(System.IServiceProvider! serviceProvider, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.ApplicationDbContextEFFactory.CreateDbContext() -> MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.DbContextFactory +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.DbContextFactory.BeginTransaction() -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.DbContextFactory.CommitTransaction() -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.DbContextFactory.DbContextFactory(MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IPhysicalDbContextFactory! physicalDbContextFactory, MMCA.Common.Infrastructure.Persistence.DataSources.IEntityDataSourceRegistry! entityDataSourceRegistry, MMCA.Common.Infrastructure.Persistence.DataSources.IDataSourceResolver! dataSourceResolver, MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService! currentUserService, MMCA.Common.Application.Interfaces.ITenantContext? tenantContext = null, Microsoft.Extensions.Options.IOptions? tenancySettings = null) -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.DbContextFactory.Dispose() -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.DbContextFactory.DisposeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.DbContextFactory.EnsureCreatedAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.DbContextFactory.ExecuteInTransactionAsync(System.Func!>! operation, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.DbContextFactory.GetDbContext(MMCA.Common.Application.Interfaces.Infrastructure.DataSource dataSource) -> MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.DbContextFactory.GetDbContext(MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey dataSourceKey) -> MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.DbContextFactory.HasPendingMigrationsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.DbContextFactory.MigrateAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.DbContextFactory.RequestIdentityInsert() -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.DbContextFactory.RollbackTransaction() -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.DbContextFactory.SaveChanges() -> int +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.DbContextFactory.SaveChangesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IDbContextFactory +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IDbContextFactory.BeginTransaction() -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IDbContextFactory.CommitTransaction() -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IDbContextFactory.EnsureCreatedAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IDbContextFactory.ExecuteInTransactionAsync(System.Func!>! operation, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IDbContextFactory.GetDbContext(MMCA.Common.Application.Interfaces.Infrastructure.DataSource dataSource) -> MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IDbContextFactory.GetDbContext(MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey dataSourceKey) -> MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IDbContextFactory.HasPendingMigrationsAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IDbContextFactory.MigrateAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IDbContextFactory.RequestIdentityInsert() -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IDbContextFactory.RollbackTransaction() -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IDbContextFactory.SaveChanges() -> int +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IDbContextFactory.SaveChangesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IPhysicalDbContextFactory +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IPhysicalDbContextFactory.Create(MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey key) -> MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IPhysicalDbContextFactory.Create(MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey key, MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource! physicalDataSource) -> MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.PhysicalDbContextFactory +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.PhysicalDbContextFactory.Create(MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey key) -> MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.PhysicalDbContextFactory.Create(MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey key, MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource! physicalDataSource) -> MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.PhysicalDbContextFactory.PhysicalDbContextFactory(System.IServiceProvider! serviceProvider, MMCA.Common.Infrastructure.Persistence.DataSources.IDataSourceResolver! resolver, MMCA.Common.Application.Interfaces.Infrastructure.IEntityConfigurationAssemblyProvider! assemblyProvider) -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.TransactionCommitAmbiguousException +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.TransactionCommitAmbiguousException.AmbiguousSource.get -> string? +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.TransactionCommitAmbiguousException.CommittedSources.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.TransactionCommitAmbiguousException.RolledBackSources.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.TransactionCommitAmbiguousException.TransactionCommitAmbiguousException() -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.TransactionCommitAmbiguousException.TransactionCommitAmbiguousException(System.Exception! innerException) -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.TransactionCommitAmbiguousException.TransactionCommitAmbiguousException(System.Exception! innerException, System.Collections.Generic.IReadOnlyList! committedSources, string? ambiguousSource, System.Collections.Generic.IReadOnlyList! rolledBackSources) -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.TransactionCommitAmbiguousException.TransactionCommitAmbiguousException(string! message) -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.TransactionCommitAmbiguousException.TransactionCommitAmbiguousException(string! message, System.Exception! innerException) -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.SQLServerDbContext +MMCA.Common.Infrastructure.Persistence.DbContexts.SQLServerDbContext.SQLServerDbContext(Microsoft.EntityFrameworkCore.DbContextOptions! options, System.IServiceProvider! serviceProvider, MMCA.Common.Application.Interfaces.Infrastructure.IEntityConfigurationAssemblyProvider! assemblyProvider, MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource! physicalDataSource) -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.DbSeeder +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.DbSeeder.DbSeeder() -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.IDbSeeder +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.IDbSeeder.SeedAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.IdentityModuleDbSeederBase +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.IdentityModuleDbSeederBase.IdentityModuleDbSeederBase(MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! unitOfWork, MMCA.Common.Application.Interfaces.Infrastructure.IPasswordHasher! passwordHasher) -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.IdentityModuleDbSeederBase.PasswordHasher.get -> MMCA.Common.Application.Interfaces.Infrastructure.IPasswordHasher! +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.IdentityModuleDbSeederBase.UnitOfWork.get -> MMCA.Common.Application.Interfaces.Infrastructure.IUnitOfWork! +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.$() -> MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount! +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.Deconstruct(out string! Email, out string! Password, out string! Role, out string? FirstName, out string? LastName) -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.Email.get -> string! +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.Email.init -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.Equals(MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount? other) -> bool +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.FirstName.get -> string? +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.FirstName.init -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.LastName.get -> string? +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.LastName.init -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.Password.get -> string! +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.Password.init -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.Role.get -> string! +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.Role.init -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.SeedAccount(string! Email, string! Password, string! Role, string? FirstName = null, string? LastName = null) -> void +MMCA.Common.Infrastructure.Persistence.DbContexts.SqliteDbContext +MMCA.Common.Infrastructure.Persistence.DbContexts.SqliteDbContext.SqliteDbContext(Microsoft.EntityFrameworkCore.DbContextOptions! options, System.IServiceProvider! serviceProvider, MMCA.Common.Application.Interfaces.Infrastructure.IEntityConfigurationAssemblyProvider! assemblyProvider, MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource! physicalDataSource) -> void +MMCA.Common.Infrastructure.Persistence.DefaultEntityConfigurationAssemblyProvider +MMCA.Common.Infrastructure.Persistence.DefaultEntityConfigurationAssemblyProvider.DefaultEntityConfigurationAssemblyProvider(Microsoft.Extensions.Options.IOptions! options) -> void +MMCA.Common.Infrastructure.Persistence.DefaultEntityConfigurationAssemblyProvider.GetConfigurationAssemblies() -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Infrastructure.Persistence.Encryption.EncryptedStringConverter +MMCA.Common.Infrastructure.Persistence.Encryption.EncryptedStringConverter.EncryptedStringConverter(System.Collections.Generic.IReadOnlyDictionary! keyRing, byte currentKeyVersion) -> void +MMCA.Common.Infrastructure.Persistence.Encryption.EncryptedStringConverter.EncryptedStringConverter(byte[]! encryptionKey) -> void +MMCA.Common.Infrastructure.Persistence.EntityConfigurationOptions +MMCA.Common.Infrastructure.Persistence.EntityConfigurationOptions.AdditionalAssemblies.get -> System.Collections.Generic.List! +MMCA.Common.Infrastructure.Persistence.EntityConfigurationOptions.EntityConfigurationOptions() -> void +MMCA.Common.Infrastructure.Persistence.Inbox.EfInboxStore +MMCA.Common.Infrastructure.Persistence.Inbox.EfInboxStore.AlreadyProcessedAsync(System.Guid messageId, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.Inbox.EfInboxStore.EfInboxStore(MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IDbContextFactory! dbContextFactory, MMCA.Common.Infrastructure.Persistence.DataSources.IDataSourceResolver! dataSourceResolver, Microsoft.Extensions.Options.IOptions! outboxOptions, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.Infrastructure.Persistence.Inbox.EfInboxStore.MarkProcessedAsync(System.Guid messageId, string! eventType, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.Inbox.IInboxStore +MMCA.Common.Infrastructure.Persistence.Inbox.IInboxStore.AlreadyProcessedAsync(System.Guid messageId, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.Inbox.IInboxStore.MarkProcessedAsync(System.Guid messageId, string! eventType, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.Inbox.InboxMessage +MMCA.Common.Infrastructure.Persistence.Inbox.InboxMessage.EventType.get -> string! +MMCA.Common.Infrastructure.Persistence.Inbox.InboxMessage.EventType.init -> void +MMCA.Common.Infrastructure.Persistence.Inbox.InboxMessage.Id.get -> System.Guid +MMCA.Common.Infrastructure.Persistence.Inbox.InboxMessage.Id.init -> void +MMCA.Common.Infrastructure.Persistence.Inbox.InboxMessage.InboxMessage() -> void +MMCA.Common.Infrastructure.Persistence.Inbox.InboxMessage.MessageId.get -> System.Guid +MMCA.Common.Infrastructure.Persistence.Inbox.InboxMessage.MessageId.init -> void +MMCA.Common.Infrastructure.Persistence.Inbox.InboxMessage.ProcessedOn.get -> System.DateTime +MMCA.Common.Infrastructure.Persistence.Inbox.InboxMessage.ProcessedOn.init -> void +MMCA.Common.Infrastructure.Persistence.Interceptors.AuditSaveChangesInterceptor +MMCA.Common.Infrastructure.Persistence.Interceptors.AuditSaveChangesInterceptor.AuditSaveChangesInterceptor(System.TimeProvider! timeProvider) -> void +MMCA.Common.Infrastructure.Persistence.Interceptors.CrossTenantWriteException +MMCA.Common.Infrastructure.Persistence.Interceptors.CrossTenantWriteException.CrossTenantWriteException() -> void +MMCA.Common.Infrastructure.Persistence.Interceptors.CrossTenantWriteException.CrossTenantWriteException(string! message) -> void +MMCA.Common.Infrastructure.Persistence.Interceptors.CrossTenantWriteException.CrossTenantWriteException(string! message, System.Exception! innerException) -> void +MMCA.Common.Infrastructure.Persistence.Interceptors.CrossTenantWriteException.CurrentTenantId.get -> string? +MMCA.Common.Infrastructure.Persistence.Interceptors.CrossTenantWriteException.EntityTenantId.get -> string? +MMCA.Common.Infrastructure.Persistence.Interceptors.CrossTenantWriteException.EntityType.get -> string? +MMCA.Common.Infrastructure.Persistence.Interceptors.DomainEventSaveChangesInterceptor +MMCA.Common.Infrastructure.Persistence.Interceptors.DomainEventSaveChangesInterceptor.DomainEventSaveChangesInterceptor(MMCA.Common.Application.Interfaces.IDomainEventDispatcher! domainEventDispatcher, Microsoft.Extensions.Logging.ILogger! logger, MMCA.Common.Infrastructure.Persistence.Outbox.IOutboxSignal! outboxSignal) -> void +MMCA.Common.Infrastructure.Persistence.Interceptors.TenantSaveChangesInterceptor +MMCA.Common.Infrastructure.Persistence.Interceptors.TenantSaveChangesInterceptor.TenantSaveChangesInterceptor() -> void +MMCA.Common.Infrastructure.Persistence.Outbox.IOutboxSignal +MMCA.Common.Infrastructure.Persistence.Outbox.IOutboxSignal.Signal() -> void +MMCA.Common.Infrastructure.Persistence.Outbox.IOutboxSignal.WaitAsync(System.TimeSpan timeout, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxCleanupService +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxCleanupService.OutboxCleanupService(Microsoft.Extensions.DependencyInjection.IServiceScopeFactory! scopeFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Options.IOptions! outboxOptions, Microsoft.Extensions.Options.IOptions! messageBusOptions, MMCA.Common.Infrastructure.Persistence.DataSources.IEntityDataSourceRegistry! entityDataSourceRegistry, MMCA.Common.Infrastructure.Persistence.DataSources.IDataSourceResolver! dataSourceResolver, System.TimeProvider? timeProvider = null, Microsoft.Extensions.Options.IOptions? tenancyOptions = null) -> void +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.DeserializeEvent() -> MMCA.Common.Domain.Interfaces.IDomainEvent? +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.EventType.get -> string! +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.EventType.init -> void +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.Id.get -> System.Guid +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.Id.init -> void +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.LastError.get -> string? +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.LastError.set -> void +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.LockToken.get -> System.Guid? +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.LockToken.set -> void +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.LockedUntil.get -> System.DateTime? +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.LockedUntil.set -> void +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.OccurredOn.get -> System.DateTime +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.OccurredOn.init -> void +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.OutboxMessage() -> void +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.Payload.get -> string! +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.Payload.init -> void +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.ProcessedOn.get -> System.DateTime? +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.ProcessedOn.set -> void +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.RetryCount.get -> int +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.RetryCount.set -> void +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.SpanId.get -> string? +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.SpanId.init -> void +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.TraceId.get -> string? +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.TraceId.init -> void +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxProcessor +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxProcessor.OutboxProcessor(Microsoft.Extensions.DependencyInjection.IServiceScopeFactory! scopeFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Options.IOptions! outboxOptions, MMCA.Common.Infrastructure.Persistence.Outbox.IOutboxSignal! outboxSignal, MMCA.Common.Infrastructure.Persistence.DataSources.IEntityDataSourceRegistry! entityDataSourceRegistry, MMCA.Common.Infrastructure.Persistence.DataSources.IDataSourceResolver! dataSourceResolver, System.TimeProvider? timeProvider = null, Microsoft.Extensions.Options.IOptions? tenancyOptions = null) -> void +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxSignal +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxSignal.Dispose() -> void +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxSignal.OutboxSignal() -> void +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxSignal.Signal() -> void +MMCA.Common.Infrastructure.Persistence.Outbox.OutboxSignal.WaitAsync(System.TimeSpan timeout, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Persistence.Repositories.Factory.IRepositoryFactory +MMCA.Common.Infrastructure.Persistence.Repositories.Factory.IRepositoryFactory.Create(Microsoft.EntityFrameworkCore.DbContext! dbContext) -> MMCA.Common.Application.Interfaces.Infrastructure.IRepository! +MMCA.Common.Infrastructure.Persistence.Repositories.Factory.IRepositoryFactory.CreateReadOnly(Microsoft.EntityFrameworkCore.DbContext! dbContext) -> MMCA.Common.Application.Interfaces.Infrastructure.IReadRepository! +MMCA.Common.Infrastructure.Persistence.Repositories.Factory.RepositoryFactory +MMCA.Common.Infrastructure.Persistence.Repositories.Factory.RepositoryFactory.Create(Microsoft.EntityFrameworkCore.DbContext! dbContext) -> MMCA.Common.Application.Interfaces.Infrastructure.IRepository! +MMCA.Common.Infrastructure.Persistence.Repositories.Factory.RepositoryFactory.CreateReadOnly(Microsoft.EntityFrameworkCore.DbContext! dbContext) -> MMCA.Common.Application.Interfaces.Infrastructure.IReadRepository! +MMCA.Common.Infrastructure.Persistence.Repositories.Factory.RepositoryFactory.RepositoryFactory(System.IServiceProvider! serviceProvider, MMCA.Common.Application.Settings.IApplicationSettings! applicationSettings) -> void +MMCA.Common.Infrastructure.Persistence.ValueGenerators.CosmosIntIdValueGenerator +MMCA.Common.Infrastructure.Persistence.ValueGenerators.CosmosIntIdValueGenerator.CosmosIntIdValueGenerator() -> void +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.CronExpression.get -> string! +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.CronExpression.set -> void +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.JobName.get -> string! +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.JobName.init -> void +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.LastDurationMs.get -> long? +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.LastDurationMs.set -> void +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.LastError.get -> string? +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.LastError.set -> void +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.LastOutcome.get -> string? +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.LastOutcome.set -> void +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.LastRunOn.get -> System.DateTime? +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.LastRunOn.set -> void +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.LockToken.get -> System.Guid? +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.LockToken.set -> void +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.LockedUntil.get -> System.DateTime? +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.LockedUntil.set -> void +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.NextRunOn.get -> System.DateTime +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.NextRunOn.set -> void +MMCA.Common.Infrastructure.Scheduling.ScheduledJobEntry.ScheduledJobEntry() -> void +MMCA.Common.Infrastructure.Scheduling.ScheduledJobRunner +MMCA.Common.Infrastructure.Scheduling.ScheduledJobRunner.ScheduledJobRunner(Microsoft.Extensions.DependencyInjection.IServiceScopeFactory! scopeFactory, Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Options.IOptions! schedulerOptions, MMCA.Common.Infrastructure.Persistence.DataSources.IDataSourceResolver! dataSourceResolver, System.TimeProvider? timeProvider = null) -> void +MMCA.Common.Infrastructure.Services.AzureBlobFileStorageService +MMCA.Common.Infrastructure.Services.AzureBlobFileStorageService.AzureBlobFileStorageService(Azure.Storage.Blobs.BlobContainerClient! containerClient, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.Infrastructure.Services.AzureBlobFileStorageService.DeleteAsync(string! blobName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.AzureBlobFileStorageService.IsConfigured.get -> bool +MMCA.Common.Infrastructure.Services.AzureBlobFileStorageService.UploadAsync(string! blobName, System.IO.Stream! content, string! contentType, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Infrastructure.Services.AzureNotificationHubDeviceRegistrar +MMCA.Common.Infrastructure.Services.AzureNotificationHubDeviceRegistrar.AzureNotificationHubDeviceRegistrar(Microsoft.Azure.NotificationHubs.INotificationHubClient! hubClient, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.Infrastructure.Services.AzureNotificationHubDeviceRegistrar.DeleteAsync(int userId, string! installationId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.AzureNotificationHubDeviceRegistrar.DeleteAsync(string! installationId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.AzureNotificationHubDeviceRegistrar.UpsertAsync(int userId, MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.AzureNotificationHubNativePushSender +MMCA.Common.Infrastructure.Services.AzureNotificationHubNativePushSender.AzureNotificationHubNativePushSender(Microsoft.Azure.NotificationHubs.INotificationHubClient! hubClient, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.Infrastructure.Services.AzureNotificationHubNativePushSender.BroadcastAsync(string! title, string! body, System.Collections.Generic.Dictionary? metadata = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.AzureNotificationHubNativePushSender.SendToUsersAsync(System.Collections.Generic.IEnumerable! userIds, string! title, string! body, System.Collections.Generic.Dictionary? metadata = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.BrokerEventBus +MMCA.Common.Infrastructure.Services.BrokerEventBus.BrokerEventBus(MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IDbContextFactory! dbContextFactory, MMCA.Common.Infrastructure.Persistence.Outbox.IOutboxSignal! outboxSignal, MMCA.Common.Infrastructure.Persistence.DataSources.IDataSourceResolver! dataSourceResolver, Microsoft.Extensions.Options.IOptions! outboxOptions) -> void +MMCA.Common.Infrastructure.Services.BrokerEventBus.PublishAsync(MMCA.Common.Domain.Interfaces.IIntegrationEvent! integrationEvent, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.BrokerEventBus.PublishAsync(System.Collections.Generic.IEnumerable! integrationEvents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.BrokerMessageBus +MMCA.Common.Infrastructure.Services.BrokerMessageBus.BrokerMessageBus(MassTransit.IPublishEndpoint! publishEndpoint) -> void +MMCA.Common.Infrastructure.Services.BrokerMessageBus.PublishAsync(MMCA.Common.Domain.Interfaces.IIntegrationEvent! integrationEvent, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.BrokerMessageBus.PublishAsync(System.Collections.Generic.IEnumerable! integrationEvents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.ClaimBasedUserIdProvider +MMCA.Common.Infrastructure.Services.ClaimBasedUserIdProvider.ClaimBasedUserIdProvider() -> void +MMCA.Common.Infrastructure.Services.ClaimBasedUserIdProvider.GetUserId(Microsoft.AspNetCore.SignalR.HubConnectionContext! connection) -> string? +MMCA.Common.Infrastructure.Services.CorrelationContext +MMCA.Common.Infrastructure.Services.CorrelationContext.CorrelationContext() -> void +MMCA.Common.Infrastructure.Services.CorrelationContext.CorrelationId.get -> string! +MMCA.Common.Infrastructure.Services.CorrelationContext.SetCorrelationId(string! correlationId) -> void +MMCA.Common.Infrastructure.Services.CurrentUserService +MMCA.Common.Infrastructure.Services.CurrentUserService.CurrentUserService(Microsoft.AspNetCore.Http.IHttpContextAccessor! httpContextAccessor) -> void +MMCA.Common.Infrastructure.Services.CurrentUserService.GetClaimValue(string! claimType) -> T? +MMCA.Common.Infrastructure.Services.CurrentUserService.Role.get -> string? +MMCA.Common.Infrastructure.Services.CurrentUserService.User.get -> System.Security.Claims.ClaimsPrincipal! +MMCA.Common.Infrastructure.Services.CurrentUserService.UserId.get -> int? +MMCA.Common.Infrastructure.Services.DataSourceService +MMCA.Common.Infrastructure.Services.DataSourceService.DataSourceService(MMCA.Common.Infrastructure.Persistence.DataSources.IEntityDataSourceRegistry! registry) -> void +MMCA.Common.Infrastructure.Services.DataSourceService.GetDataSource(System.Type! entityType) -> MMCA.Common.Application.Interfaces.Infrastructure.DataSource +MMCA.Common.Infrastructure.Services.DataSourceService.GetDataSource(string! entityFullName) -> MMCA.Common.Application.Interfaces.Infrastructure.DataSource +MMCA.Common.Infrastructure.Services.DataSourceService.GetDataSourceKey(System.Type! entityType) -> MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey +MMCA.Common.Infrastructure.Services.DataSourceService.GetDataSourceKey(string! entityFullName) -> MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey +MMCA.Common.Infrastructure.Services.DataSourceService.HaveIncludeSupport(MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey first, MMCA.Common.Application.Interfaces.Infrastructure.DataSourceKey second) -> bool +MMCA.Common.Infrastructure.Services.DataSourceService.HaveIncludeSupport(string! firstEntityFullName, string! secondEntityFullName) -> bool +MMCA.Common.Infrastructure.Services.FaultIntegrationEventConsumer +MMCA.Common.Infrastructure.Services.FaultIntegrationEventConsumer.Consume(MassTransit.ConsumeContext!>! context) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.FaultIntegrationEventConsumer.FaultIntegrationEventConsumer(Microsoft.Extensions.Logging.ILogger!>! logger) -> void +MMCA.Common.Infrastructure.Services.ImageSharpImageProcessor +MMCA.Common.Infrastructure.Services.ImageSharpImageProcessor.ImageSharpImageProcessor() -> void +MMCA.Common.Infrastructure.Services.ImageSharpImageProcessor.NormalizeToSquareJpegAsync(System.IO.Stream! content, int size, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Infrastructure.Services.InProcessEventBus +MMCA.Common.Infrastructure.Services.InProcessEventBus.InProcessEventBus(MMCA.Common.Infrastructure.Persistence.DbContexts.Factory.IDbContextFactory! dbContextFactory, MMCA.Common.Application.Interfaces.IDomainEventDispatcher! domainEventDispatcher, MMCA.Common.Infrastructure.Persistence.DataSources.IDataSourceResolver! dataSourceResolver, Microsoft.Extensions.Options.IOptions! outboxOptions) -> void +MMCA.Common.Infrastructure.Services.InProcessEventBus.PublishAsync(MMCA.Common.Domain.Interfaces.IIntegrationEvent! integrationEvent, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.InProcessEventBus.PublishAsync(System.Collections.Generic.IEnumerable! integrationEvents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.InProcessMessageBus +MMCA.Common.Infrastructure.Services.InProcessMessageBus.InProcessMessageBus(MMCA.Common.Application.Interfaces.IDomainEventDispatcher! domainEventDispatcher) -> void +MMCA.Common.Infrastructure.Services.InProcessMessageBus.PublishAsync(MMCA.Common.Domain.Interfaces.IIntegrationEvent! integrationEvent, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.InProcessMessageBus.PublishAsync(System.Collections.Generic.IEnumerable! integrationEvents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.IntegrationEventConsumer +MMCA.Common.Infrastructure.Services.IntegrationEventConsumer.Consume(MassTransit.ConsumeContext! context) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.IntegrationEventConsumer.IntegrationEventConsumer(System.Collections.Generic.IEnumerable!>! handlers, MMCA.Common.Infrastructure.Persistence.Inbox.IInboxStore! inbox, Microsoft.Extensions.Logging.ILogger!>! logger) -> void +MMCA.Common.Infrastructure.Services.IntegrationEventConsumerExtensions +MMCA.Common.Infrastructure.Services.IntegrationEventConsumerExtensions.extension(MassTransit.IBusRegistrationConfigurator!) +MMCA.Common.Infrastructure.Services.IntegrationEventConsumerExtensions.extension(MassTransit.IBusRegistrationConfigurator!).RegisterIntegrationEventConsumer(bool registerFaultConsumer = true) -> MassTransit.IBusRegistrationConfigurator! +MMCA.Common.Infrastructure.Services.NullFileStorageService +MMCA.Common.Infrastructure.Services.NullFileStorageService.DeleteAsync(string! blobName, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.NullFileStorageService.IsConfigured.get -> bool +MMCA.Common.Infrastructure.Services.NullFileStorageService.NullFileStorageService() -> void +MMCA.Common.Infrastructure.Services.NullFileStorageService.UploadAsync(string! blobName, System.IO.Stream! content, string! contentType, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.Infrastructure.Services.NullLiveChannelPublisher +MMCA.Common.Infrastructure.Services.NullLiveChannelPublisher.NullLiveChannelPublisher() -> void +MMCA.Common.Infrastructure.Services.NullLiveChannelPublisher.PublishAsync(string! channelKey, string! eventName, string! payloadJson, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.NullNativePushSender +MMCA.Common.Infrastructure.Services.NullNativePushSender.BroadcastAsync(string! title, string! body, System.Collections.Generic.Dictionary? metadata = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.NullNativePushSender.NullNativePushSender() -> void +MMCA.Common.Infrastructure.Services.NullNativePushSender.SendToUsersAsync(System.Collections.Generic.IEnumerable! userIds, string! title, string! body, System.Collections.Generic.Dictionary? metadata = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.NullPushDeviceRegistrar +MMCA.Common.Infrastructure.Services.NullPushDeviceRegistrar.DeleteAsync(int userId, string! installationId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.NullPushDeviceRegistrar.DeleteAsync(string! installationId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.NullPushDeviceRegistrar.NullPushDeviceRegistrar() -> void +MMCA.Common.Infrastructure.Services.NullPushDeviceRegistrar.UpsertAsync(int userId, MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.NullPushNotificationSender +MMCA.Common.Infrastructure.Services.NullPushNotificationSender.BroadcastAsync(string! title, string! body, System.Collections.Generic.Dictionary? metadata = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.NullPushNotificationSender.NullPushNotificationSender() -> void +MMCA.Common.Infrastructure.Services.NullPushNotificationSender.SendToUserAsync(int userId, string! title, string! body, System.Collections.Generic.Dictionary? metadata = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.NullPushNotificationSender.SendToUsersAsync(System.Collections.Generic.IEnumerable! userIds, string! title, string! body, System.Collections.Generic.Dictionary? metadata = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.PasswordHasher +MMCA.Common.Infrastructure.Services.PasswordHasher.HashPassword(string! password) -> (byte[]! Hash, byte[]! Salt) +MMCA.Common.Infrastructure.Services.PasswordHasher.PasswordHasher() -> void +MMCA.Common.Infrastructure.Services.PasswordHasher.VerifyPassword(string! password, byte[]! hash, byte[]! salt) -> bool +MMCA.Common.Infrastructure.Services.PeriodicBackgroundService +MMCA.Common.Infrastructure.Services.PeriodicBackgroundService.PeriodicBackgroundService(System.TimeProvider! timeProvider, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.Infrastructure.Services.SignalRLiveChannelPublisher +MMCA.Common.Infrastructure.Services.SignalRLiveChannelPublisher.PublishAsync(string! channelKey, string! eventName, string! payloadJson, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.SignalRLiveChannelPublisher.SignalRLiveChannelPublisher(Microsoft.AspNetCore.SignalR.IHubContext! hubContext) -> void +MMCA.Common.Infrastructure.Services.SignalRPushNotificationSender +MMCA.Common.Infrastructure.Services.SignalRPushNotificationSender.BroadcastAsync(string! title, string! body, System.Collections.Generic.Dictionary? metadata = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.SignalRPushNotificationSender.SendToUserAsync(int userId, string! title, string! body, System.Collections.Generic.Dictionary? metadata = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.SignalRPushNotificationSender.SendToUsersAsync(System.Collections.Generic.IEnumerable! userIds, string! title, string! body, System.Collections.Generic.Dictionary? metadata = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.SignalRPushNotificationSender.SignalRPushNotificationSender(Microsoft.AspNetCore.SignalR.IHubContext! hubContext) -> void +MMCA.Common.Infrastructure.Services.SmtpEmailSender +MMCA.Common.Infrastructure.Services.SmtpEmailSender.SendAsync(string! subject, string! body, bool isHtml = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.SmtpEmailSender.SendAsync(string! to, string! subject, string! body, bool isHtml = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Infrastructure.Services.SmtpEmailSender.SmtpEmailSender(MMCA.Common.Infrastructure.Settings.ISmtpSettings! smtpSettings) -> void +MMCA.Common.Infrastructure.Services.TenantContext +MMCA.Common.Infrastructure.Services.TenantContext.IsResolved.get -> bool +MMCA.Common.Infrastructure.Services.TenantContext.SetTenant(string! tenantId) -> void +MMCA.Common.Infrastructure.Services.TenantContext.TenantContext() -> void +MMCA.Common.Infrastructure.Services.TenantContext.TenantId.get -> string? +MMCA.Common.Infrastructure.Services.TokenService +MMCA.Common.Infrastructure.Services.TokenService.AccessTokenLifetime.get -> System.TimeSpan +MMCA.Common.Infrastructure.Services.TokenService.Dispose() -> void +MMCA.Common.Infrastructure.Services.TokenService.GenerateAccessToken(int userId, string! email, string! role, string! fullName, System.Collections.Generic.IEnumerable? additionalClaims = null) -> string! +MMCA.Common.Infrastructure.Services.TokenService.GenerateRefreshToken() -> string! +MMCA.Common.Infrastructure.Services.TokenService.GetPrincipalFromExpiredToken(string! token) -> System.Security.Claims.ClaimsPrincipal? +MMCA.Common.Infrastructure.Services.TokenService.RefreshTokenLifetime.get -> System.TimeSpan +MMCA.Common.Infrastructure.Services.TokenService.TokenService(MMCA.Common.Infrastructure.Settings.IJwtSettings! jwtSettings, System.TimeProvider? timeProvider = null) -> void +MMCA.Common.Infrastructure.Settings.AuditTrailSettings +MMCA.Common.Infrastructure.Settings.AuditTrailSettings.AuditTrailSettings() -> void +MMCA.Common.Infrastructure.Settings.AuditTrailSettings.DataSource.get -> MMCA.Common.Application.Interfaces.Infrastructure.DataSource +MMCA.Common.Infrastructure.Settings.AuditTrailSettings.DataSource.init -> void +MMCA.Common.Infrastructure.Settings.AuditTrailSettings.Enabled.get -> bool +MMCA.Common.Infrastructure.Settings.AuditTrailSettings.Enabled.init -> void +MMCA.Common.Infrastructure.Settings.AuditTrailSettings.RetentionDays.get -> int +MMCA.Common.Infrastructure.Settings.AuditTrailSettings.RetentionDays.init -> void +MMCA.Common.Infrastructure.Settings.ConnectionStringSettings +MMCA.Common.Infrastructure.Settings.ConnectionStringSettings.ConnectionStringSettings() -> void +MMCA.Common.Infrastructure.Settings.ConnectionStringSettings.CosmosConnectionString.get -> string! +MMCA.Common.Infrastructure.Settings.ConnectionStringSettings.CosmosConnectionString.init -> void +MMCA.Common.Infrastructure.Settings.ConnectionStringSettings.CosmosDatabaseName.get -> string! +MMCA.Common.Infrastructure.Settings.ConnectionStringSettings.CosmosDatabaseName.init -> void +MMCA.Common.Infrastructure.Settings.ConnectionStringSettings.SQLServerConnectionString.get -> string! +MMCA.Common.Infrastructure.Settings.ConnectionStringSettings.SQLServerConnectionString.init -> void +MMCA.Common.Infrastructure.Settings.ConnectionStringSettings.SQLServerMigrationsAssembly.get -> string! +MMCA.Common.Infrastructure.Settings.ConnectionStringSettings.SQLServerMigrationsAssembly.init -> void +MMCA.Common.Infrastructure.Settings.ConnectionStringSettings.SqliteConnectionString.get -> string! +MMCA.Common.Infrastructure.Settings.ConnectionStringSettings.SqliteConnectionString.init -> void +MMCA.Common.Infrastructure.Settings.DataSourceEntrySettings +MMCA.Common.Infrastructure.Settings.DataSourceEntrySettings.CosmosConnectionString.get -> string! +MMCA.Common.Infrastructure.Settings.DataSourceEntrySettings.CosmosConnectionString.init -> void +MMCA.Common.Infrastructure.Settings.DataSourceEntrySettings.CosmosDatabaseName.get -> string! +MMCA.Common.Infrastructure.Settings.DataSourceEntrySettings.CosmosDatabaseName.init -> void +MMCA.Common.Infrastructure.Settings.DataSourceEntrySettings.DataSourceEntrySettings() -> void +MMCA.Common.Infrastructure.Settings.DataSourceEntrySettings.SQLServerConnectionString.get -> string! +MMCA.Common.Infrastructure.Settings.DataSourceEntrySettings.SQLServerConnectionString.init -> void +MMCA.Common.Infrastructure.Settings.DataSourceEntrySettings.SQLServerMigrationsAssembly.get -> string! +MMCA.Common.Infrastructure.Settings.DataSourceEntrySettings.SQLServerMigrationsAssembly.init -> void +MMCA.Common.Infrastructure.Settings.DataSourceEntrySettings.SqliteConnectionString.get -> string! +MMCA.Common.Infrastructure.Settings.DataSourceEntrySettings.SqliteConnectionString.init -> void +MMCA.Common.Infrastructure.Settings.DataSourcesSettings +MMCA.Common.Infrastructure.Settings.DataSourcesSettings.DataSourcesSettings(System.Collections.Generic.IReadOnlyDictionary? sources = null) -> void +MMCA.Common.Infrastructure.Settings.DataSourcesSettings.Sources.get -> System.Collections.Generic.IReadOnlyDictionary! +MMCA.Common.Infrastructure.Settings.FileStorageSettings +MMCA.Common.Infrastructure.Settings.FileStorageSettings.ConnectionString.get -> string? +MMCA.Common.Infrastructure.Settings.FileStorageSettings.ConnectionString.init -> void +MMCA.Common.Infrastructure.Settings.FileStorageSettings.ContainerName.get -> string? +MMCA.Common.Infrastructure.Settings.FileStorageSettings.ContainerName.init -> void +MMCA.Common.Infrastructure.Settings.FileStorageSettings.FileStorageSettings() -> void +MMCA.Common.Infrastructure.Settings.FileStorageSettings.ServiceUri.get -> System.Uri? +MMCA.Common.Infrastructure.Settings.FileStorageSettings.ServiceUri.init -> void +MMCA.Common.Infrastructure.Settings.IConnectionStringSettings +MMCA.Common.Infrastructure.Settings.IConnectionStringSettings.CosmosConnectionString.get -> string! +MMCA.Common.Infrastructure.Settings.IConnectionStringSettings.CosmosConnectionString.init -> void +MMCA.Common.Infrastructure.Settings.IConnectionStringSettings.CosmosDatabaseName.get -> string! +MMCA.Common.Infrastructure.Settings.IConnectionStringSettings.CosmosDatabaseName.init -> void +MMCA.Common.Infrastructure.Settings.IConnectionStringSettings.SQLServerConnectionString.get -> string! +MMCA.Common.Infrastructure.Settings.IConnectionStringSettings.SQLServerConnectionString.init -> void +MMCA.Common.Infrastructure.Settings.IConnectionStringSettings.SQLServerMigrationsAssembly.get -> string! +MMCA.Common.Infrastructure.Settings.IConnectionStringSettings.SQLServerMigrationsAssembly.init -> void +MMCA.Common.Infrastructure.Settings.IConnectionStringSettings.SqliteConnectionString.get -> string! +MMCA.Common.Infrastructure.Settings.IConnectionStringSettings.SqliteConnectionString.init -> void +MMCA.Common.Infrastructure.Settings.IJwtSettings +MMCA.Common.Infrastructure.Settings.IJwtSettings.AccessTokenExpirationMinutes.get -> int +MMCA.Common.Infrastructure.Settings.IJwtSettings.AccessTokenExpirationMinutes.init -> void +MMCA.Common.Infrastructure.Settings.IJwtSettings.Audience.get -> string! +MMCA.Common.Infrastructure.Settings.IJwtSettings.Audience.init -> void +MMCA.Common.Infrastructure.Settings.IJwtSettings.Issuer.get -> string! +MMCA.Common.Infrastructure.Settings.IJwtSettings.Issuer.init -> void +MMCA.Common.Infrastructure.Settings.IJwtSettings.RefreshTokenExpirationDays.get -> int +MMCA.Common.Infrastructure.Settings.IJwtSettings.RefreshTokenExpirationDays.init -> void +MMCA.Common.Infrastructure.Settings.IJwtSettings.RsaPrivateKeyPem.get -> string? +MMCA.Common.Infrastructure.Settings.IJwtSettings.RsaPrivateKeyPem.init -> void +MMCA.Common.Infrastructure.Settings.IJwtSettings.RsaPublicKeyPem.get -> string? +MMCA.Common.Infrastructure.Settings.IJwtSettings.RsaPublicKeyPem.init -> void +MMCA.Common.Infrastructure.Settings.IJwtSettings.SecretForKey.get -> string! +MMCA.Common.Infrastructure.Settings.IJwtSettings.SecretForKey.init -> void +MMCA.Common.Infrastructure.Settings.IJwtSettings.SigningAlgorithm.get -> MMCA.Common.Infrastructure.Settings.JwtSigningAlgorithm +MMCA.Common.Infrastructure.Settings.IJwtSettings.SigningAlgorithm.init -> void +MMCA.Common.Infrastructure.Settings.IPushNotificationSettings +MMCA.Common.Infrastructure.Settings.IPushNotificationSettings.Enabled.get -> bool +MMCA.Common.Infrastructure.Settings.IPushNotificationSettings.Enabled.init -> void +MMCA.Common.Infrastructure.Settings.IPushNotificationSettings.HubPath.get -> string! +MMCA.Common.Infrastructure.Settings.IPushNotificationSettings.HubPath.init -> void +MMCA.Common.Infrastructure.Settings.ISmtpSettings +MMCA.Common.Infrastructure.Settings.ISmtpSettings.EnableSsl.get -> bool +MMCA.Common.Infrastructure.Settings.ISmtpSettings.EnableSsl.init -> void +MMCA.Common.Infrastructure.Settings.ISmtpSettings.From.get -> string! +MMCA.Common.Infrastructure.Settings.ISmtpSettings.From.init -> void +MMCA.Common.Infrastructure.Settings.ISmtpSettings.Host.get -> string! +MMCA.Common.Infrastructure.Settings.ISmtpSettings.Host.init -> void +MMCA.Common.Infrastructure.Settings.ISmtpSettings.Password.get -> string! +MMCA.Common.Infrastructure.Settings.ISmtpSettings.Password.init -> void +MMCA.Common.Infrastructure.Settings.ISmtpSettings.Port.get -> int +MMCA.Common.Infrastructure.Settings.ISmtpSettings.Port.init -> void +MMCA.Common.Infrastructure.Settings.ISmtpSettings.To.get -> string! +MMCA.Common.Infrastructure.Settings.ISmtpSettings.To.init -> void +MMCA.Common.Infrastructure.Settings.ISmtpSettings.Username.get -> string! +MMCA.Common.Infrastructure.Settings.ISmtpSettings.Username.init -> void +MMCA.Common.Infrastructure.Settings.JwksSettings +MMCA.Common.Infrastructure.Settings.JwksSettings.Enabled.get -> bool +MMCA.Common.Infrastructure.Settings.JwksSettings.Enabled.init -> void +MMCA.Common.Infrastructure.Settings.JwksSettings.JwksSettings() -> void +MMCA.Common.Infrastructure.Settings.JwksSettings.KeyId.get -> string! +MMCA.Common.Infrastructure.Settings.JwksSettings.KeyId.init -> void +MMCA.Common.Infrastructure.Settings.JwksSettings.RsaPublicKeyPath.get -> string? +MMCA.Common.Infrastructure.Settings.JwksSettings.RsaPublicKeyPath.init -> void +MMCA.Common.Infrastructure.Settings.JwksSettings.RsaPublicKeyPem.get -> string? +MMCA.Common.Infrastructure.Settings.JwksSettings.RsaPublicKeyPem.init -> void +MMCA.Common.Infrastructure.Settings.JwtSettings +MMCA.Common.Infrastructure.Settings.JwtSettings.AccessTokenExpirationMinutes.get -> int +MMCA.Common.Infrastructure.Settings.JwtSettings.AccessTokenExpirationMinutes.init -> void +MMCA.Common.Infrastructure.Settings.JwtSettings.Audience.get -> string! +MMCA.Common.Infrastructure.Settings.JwtSettings.Audience.init -> void +MMCA.Common.Infrastructure.Settings.JwtSettings.Issuer.get -> string! +MMCA.Common.Infrastructure.Settings.JwtSettings.Issuer.init -> void +MMCA.Common.Infrastructure.Settings.JwtSettings.JwtSettings() -> void +MMCA.Common.Infrastructure.Settings.JwtSettings.RefreshTokenExpirationDays.get -> int +MMCA.Common.Infrastructure.Settings.JwtSettings.RefreshTokenExpirationDays.init -> void +MMCA.Common.Infrastructure.Settings.JwtSettings.RsaPrivateKeyPem.get -> string? +MMCA.Common.Infrastructure.Settings.JwtSettings.RsaPrivateKeyPem.init -> void +MMCA.Common.Infrastructure.Settings.JwtSettings.RsaPublicKeyPem.get -> string? +MMCA.Common.Infrastructure.Settings.JwtSettings.RsaPublicKeyPem.init -> void +MMCA.Common.Infrastructure.Settings.JwtSettings.SecretForKey.get -> string! +MMCA.Common.Infrastructure.Settings.JwtSettings.SecretForKey.init -> void +MMCA.Common.Infrastructure.Settings.JwtSettings.SigningAlgorithm.get -> MMCA.Common.Infrastructure.Settings.JwtSigningAlgorithm +MMCA.Common.Infrastructure.Settings.JwtSettings.SigningAlgorithm.init -> void +MMCA.Common.Infrastructure.Settings.JwtSettings.Validate(System.ComponentModel.DataAnnotations.ValidationContext! validationContext) -> System.Collections.Generic.IEnumerable! +MMCA.Common.Infrastructure.Settings.JwtSigningAlgorithm +MMCA.Common.Infrastructure.Settings.JwtSigningAlgorithm.HS256 = 0 -> MMCA.Common.Infrastructure.Settings.JwtSigningAlgorithm +MMCA.Common.Infrastructure.Settings.JwtSigningAlgorithm.RS256 = 1 -> MMCA.Common.Infrastructure.Settings.JwtSigningAlgorithm +MMCA.Common.Infrastructure.Settings.MessageBusProvider +MMCA.Common.Infrastructure.Settings.MessageBusProvider.AzureServiceBus = 2 -> MMCA.Common.Infrastructure.Settings.MessageBusProvider +MMCA.Common.Infrastructure.Settings.MessageBusProvider.InProcess = 0 -> MMCA.Common.Infrastructure.Settings.MessageBusProvider +MMCA.Common.Infrastructure.Settings.MessageBusProvider.RabbitMq = 1 -> MMCA.Common.Infrastructure.Settings.MessageBusProvider +MMCA.Common.Infrastructure.Settings.MessageBusSettings +MMCA.Common.Infrastructure.Settings.MessageBusSettings.ConnectionString.get -> string? +MMCA.Common.Infrastructure.Settings.MessageBusSettings.ConnectionString.init -> void +MMCA.Common.Infrastructure.Settings.MessageBusSettings.EnableDelayedRedelivery.get -> bool +MMCA.Common.Infrastructure.Settings.MessageBusSettings.EnableDelayedRedelivery.init -> void +MMCA.Common.Infrastructure.Settings.MessageBusSettings.EnableInbox.get -> bool +MMCA.Common.Infrastructure.Settings.MessageBusSettings.EnableInbox.init -> void +MMCA.Common.Infrastructure.Settings.MessageBusSettings.EndpointPrefix.get -> string? +MMCA.Common.Infrastructure.Settings.MessageBusSettings.EndpointPrefix.init -> void +MMCA.Common.Infrastructure.Settings.MessageBusSettings.MessageBusSettings() -> void +MMCA.Common.Infrastructure.Settings.MessageBusSettings.Provider.get -> MMCA.Common.Infrastructure.Settings.MessageBusProvider +MMCA.Common.Infrastructure.Settings.MessageBusSettings.Provider.init -> void +MMCA.Common.Infrastructure.Settings.MessageBusSettings.RedeliveryIntervalsSeconds.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Infrastructure.Settings.MessageBusSettings.RedeliveryIntervalsSeconds.init -> void +MMCA.Common.Infrastructure.Settings.MessageBusSettings.RetryLimit.get -> int +MMCA.Common.Infrastructure.Settings.MessageBusSettings.RetryLimit.init -> void +MMCA.Common.Infrastructure.Settings.MessageBusSettings.RetryMaxIntervalSeconds.get -> int +MMCA.Common.Infrastructure.Settings.MessageBusSettings.RetryMaxIntervalSeconds.init -> void +MMCA.Common.Infrastructure.Settings.MessageBusSettings.RetryMinIntervalSeconds.get -> int +MMCA.Common.Infrastructure.Settings.MessageBusSettings.RetryMinIntervalSeconds.init -> void +MMCA.Common.Infrastructure.Settings.NativePushSettings +MMCA.Common.Infrastructure.Settings.NativePushSettings.ConnectionString.get -> string? +MMCA.Common.Infrastructure.Settings.NativePushSettings.ConnectionString.init -> void +MMCA.Common.Infrastructure.Settings.NativePushSettings.Enabled.get -> bool +MMCA.Common.Infrastructure.Settings.NativePushSettings.Enabled.init -> void +MMCA.Common.Infrastructure.Settings.NativePushSettings.HubName.get -> string? +MMCA.Common.Infrastructure.Settings.NativePushSettings.HubName.init -> void +MMCA.Common.Infrastructure.Settings.NativePushSettings.NativePushSettings() -> void +MMCA.Common.Infrastructure.Settings.OutboxSettings +MMCA.Common.Infrastructure.Settings.OutboxSettings.BatchSize.get -> int +MMCA.Common.Infrastructure.Settings.OutboxSettings.BatchSize.init -> void +MMCA.Common.Infrastructure.Settings.OutboxSettings.CleanupIntervalHours.get -> int +MMCA.Common.Infrastructure.Settings.OutboxSettings.CleanupIntervalHours.init -> void +MMCA.Common.Infrastructure.Settings.OutboxSettings.DataSource.get -> MMCA.Common.Application.Interfaces.Infrastructure.DataSource +MMCA.Common.Infrastructure.Settings.OutboxSettings.DataSource.init -> void +MMCA.Common.Infrastructure.Settings.OutboxSettings.DatabaseName.get -> string! +MMCA.Common.Infrastructure.Settings.OutboxSettings.DatabaseName.init -> void +MMCA.Common.Infrastructure.Settings.OutboxSettings.DeadLetterRetentionDays.get -> int +MMCA.Common.Infrastructure.Settings.OutboxSettings.DeadLetterRetentionDays.init -> void +MMCA.Common.Infrastructure.Settings.OutboxSettings.LeaseSeconds.get -> int +MMCA.Common.Infrastructure.Settings.OutboxSettings.LeaseSeconds.init -> void +MMCA.Common.Infrastructure.Settings.OutboxSettings.MaxRetries.get -> int +MMCA.Common.Infrastructure.Settings.OutboxSettings.MaxRetries.init -> void +MMCA.Common.Infrastructure.Settings.OutboxSettings.OutboxSettings() -> void +MMCA.Common.Infrastructure.Settings.OutboxSettings.PollingIntervalSeconds.get -> int +MMCA.Common.Infrastructure.Settings.OutboxSettings.PollingIntervalSeconds.init -> void +MMCA.Common.Infrastructure.Settings.OutboxSettings.ProcessingDelaySeconds.get -> int +MMCA.Common.Infrastructure.Settings.OutboxSettings.ProcessingDelaySeconds.init -> void +MMCA.Common.Infrastructure.Settings.OutboxSettings.RetentionDays.get -> int +MMCA.Common.Infrastructure.Settings.OutboxSettings.RetentionDays.init -> void +MMCA.Common.Infrastructure.Settings.OutboxSettings.RetryBackoffBaseSeconds.get -> int +MMCA.Common.Infrastructure.Settings.OutboxSettings.RetryBackoffBaseSeconds.init -> void +MMCA.Common.Infrastructure.Settings.PersistenceSettings +MMCA.Common.Infrastructure.Settings.PersistenceSettings.CommandTimeoutSeconds.get -> int +MMCA.Common.Infrastructure.Settings.PersistenceSettings.CommandTimeoutSeconds.init -> void +MMCA.Common.Infrastructure.Settings.PersistenceSettings.PersistenceSettings() -> void +MMCA.Common.Infrastructure.Settings.PushNotificationSettings +MMCA.Common.Infrastructure.Settings.PushNotificationSettings.ChannelKeyPattern.get -> string! +MMCA.Common.Infrastructure.Settings.PushNotificationSettings.ChannelKeyPattern.init -> void +MMCA.Common.Infrastructure.Settings.PushNotificationSettings.Enabled.get -> bool +MMCA.Common.Infrastructure.Settings.PushNotificationSettings.Enabled.init -> void +MMCA.Common.Infrastructure.Settings.PushNotificationSettings.HubPath.get -> string! +MMCA.Common.Infrastructure.Settings.PushNotificationSettings.HubPath.init -> void +MMCA.Common.Infrastructure.Settings.PushNotificationSettings.PushNotificationSettings() -> void +MMCA.Common.Infrastructure.Settings.ScheduledJobOverrideSettings +MMCA.Common.Infrastructure.Settings.ScheduledJobOverrideSettings.Cron.get -> string? +MMCA.Common.Infrastructure.Settings.ScheduledJobOverrideSettings.Cron.init -> void +MMCA.Common.Infrastructure.Settings.ScheduledJobOverrideSettings.ScheduledJobOverrideSettings() -> void +MMCA.Common.Infrastructure.Settings.SchedulerSettings +MMCA.Common.Infrastructure.Settings.SchedulerSettings.DataSource.get -> MMCA.Common.Application.Interfaces.Infrastructure.DataSource +MMCA.Common.Infrastructure.Settings.SchedulerSettings.DataSource.init -> void +MMCA.Common.Infrastructure.Settings.SchedulerSettings.Enabled.get -> bool +MMCA.Common.Infrastructure.Settings.SchedulerSettings.Enabled.init -> void +MMCA.Common.Infrastructure.Settings.SchedulerSettings.Jobs.get -> System.Collections.Generic.Dictionary! +MMCA.Common.Infrastructure.Settings.SchedulerSettings.LeaseSeconds.get -> int +MMCA.Common.Infrastructure.Settings.SchedulerSettings.LeaseSeconds.init -> void +MMCA.Common.Infrastructure.Settings.SchedulerSettings.PollingIntervalSeconds.get -> int +MMCA.Common.Infrastructure.Settings.SchedulerSettings.PollingIntervalSeconds.init -> void +MMCA.Common.Infrastructure.Settings.SchedulerSettings.SchedulerSettings() -> void +MMCA.Common.Infrastructure.Settings.SmtpSettings +MMCA.Common.Infrastructure.Settings.SmtpSettings.EnableSsl.get -> bool +MMCA.Common.Infrastructure.Settings.SmtpSettings.EnableSsl.init -> void +MMCA.Common.Infrastructure.Settings.SmtpSettings.From.get -> string! +MMCA.Common.Infrastructure.Settings.SmtpSettings.From.init -> void +MMCA.Common.Infrastructure.Settings.SmtpSettings.Host.get -> string! +MMCA.Common.Infrastructure.Settings.SmtpSettings.Host.init -> void +MMCA.Common.Infrastructure.Settings.SmtpSettings.Password.get -> string! +MMCA.Common.Infrastructure.Settings.SmtpSettings.Password.init -> void +MMCA.Common.Infrastructure.Settings.SmtpSettings.Port.get -> int +MMCA.Common.Infrastructure.Settings.SmtpSettings.Port.init -> void +MMCA.Common.Infrastructure.Settings.SmtpSettings.SmtpSettings() -> void +MMCA.Common.Infrastructure.Settings.SmtpSettings.To.get -> string! +MMCA.Common.Infrastructure.Settings.SmtpSettings.To.init -> void +MMCA.Common.Infrastructure.Settings.SmtpSettings.Username.get -> string! +MMCA.Common.Infrastructure.Settings.SmtpSettings.Username.init -> void +MMCA.Common.Infrastructure.Settings.TenancySettings +MMCA.Common.Infrastructure.Settings.TenancySettings.ClaimType.get -> string! +MMCA.Common.Infrastructure.Settings.TenancySettings.ClaimType.init -> void +MMCA.Common.Infrastructure.Settings.TenancySettings.EffectiveExcludedPathPrefixes.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Infrastructure.Settings.TenancySettings.EffectiveResolutionOrder.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Infrastructure.Settings.TenancySettings.Enabled.get -> bool +MMCA.Common.Infrastructure.Settings.TenancySettings.Enabled.init -> void +MMCA.Common.Infrastructure.Settings.TenancySettings.ExcludedPathPrefixes.get -> System.Collections.Generic.List! +MMCA.Common.Infrastructure.Settings.TenancySettings.HeaderName.get -> string! +MMCA.Common.Infrastructure.Settings.TenancySettings.HeaderName.init -> void +MMCA.Common.Infrastructure.Settings.TenancySettings.RequireTenant.get -> bool +MMCA.Common.Infrastructure.Settings.TenancySettings.RequireTenant.init -> void +MMCA.Common.Infrastructure.Settings.TenancySettings.ResolutionOrder.get -> System.Collections.Generic.List! +MMCA.Common.Infrastructure.Settings.TenancySettings.TenancySettings() -> void +MMCA.Common.Infrastructure.Settings.TenancySettings.Tenants.get -> System.Collections.Generic.Dictionary! +MMCA.Common.Infrastructure.Settings.TenantDataSourceOverrideSettings +MMCA.Common.Infrastructure.Settings.TenantDataSourceOverrideSettings.CosmosConnectionString.get -> string? +MMCA.Common.Infrastructure.Settings.TenantDataSourceOverrideSettings.CosmosConnectionString.init -> void +MMCA.Common.Infrastructure.Settings.TenantDataSourceOverrideSettings.CosmosDatabaseName.get -> string? +MMCA.Common.Infrastructure.Settings.TenantDataSourceOverrideSettings.CosmosDatabaseName.init -> void +MMCA.Common.Infrastructure.Settings.TenantDataSourceOverrideSettings.SQLServerConnectionString.get -> string? +MMCA.Common.Infrastructure.Settings.TenantDataSourceOverrideSettings.SQLServerConnectionString.init -> void +MMCA.Common.Infrastructure.Settings.TenantDataSourceOverrideSettings.SqliteConnectionString.get -> string? +MMCA.Common.Infrastructure.Settings.TenantDataSourceOverrideSettings.SqliteConnectionString.init -> void +MMCA.Common.Infrastructure.Settings.TenantDataSourceOverrideSettings.TenantDataSourceOverrideSettings() -> void +MMCA.Common.Infrastructure.Settings.TenantEntrySettings +MMCA.Common.Infrastructure.Settings.TenantEntrySettings.DataSources.get -> System.Collections.Generic.Dictionary! +MMCA.Common.Infrastructure.Settings.TenantEntrySettings.TenantEntrySettings() -> void +MMCA.Common.Infrastructure.Settings.TenantResolutionStrategy +MMCA.Common.Infrastructure.Settings.TenantResolutionStrategy.Claim = 0 -> MMCA.Common.Infrastructure.Settings.TenantResolutionStrategy +MMCA.Common.Infrastructure.Settings.TenantResolutionStrategy.Header = 1 -> MMCA.Common.Infrastructure.Settings.TenantResolutionStrategy +MMCA.Common.Infrastructure.Settings.TenantResolutionStrategy.Host = 2 -> MMCA.Common.Infrastructure.Settings.TenantResolutionStrategy +MMCA.Common.Infrastructure.UseDataSourceAttribute +MMCA.Common.Infrastructure.UseDataSourceAttribute.DataSource.get -> MMCA.Common.Application.Interfaces.Infrastructure.DataSource +MMCA.Common.Infrastructure.UseDataSourceAttribute.UseDataSourceAttribute(MMCA.Common.Application.Interfaces.Infrastructure.DataSource dataSource) -> void +MMCA.Common.Infrastructure.UseDatabaseAttribute +MMCA.Common.Infrastructure.UseDatabaseAttribute.Name.get -> string! +MMCA.Common.Infrastructure.UseDatabaseAttribute.UseDatabaseAttribute(string! name) -> void +abstract MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.DbSeeder.SeedAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +abstract MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.IdentityModuleDbSeederBase.Accounts.get -> System.Collections.Generic.IReadOnlyList! +abstract MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.IdentityModuleDbSeederBase.CreateUser(MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount! account, byte[]! passwordHash, byte[]! passwordSalt) -> MMCA.Common.Shared.Abstractions.Result! +abstract MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.IdentityModuleDbSeederBase.EmailExistsAsync(MMCA.Common.Shared.ValueObjects.Email? email, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +abstract MMCA.Common.Infrastructure.Services.PeriodicBackgroundService.ExecuteCycleAsync(System.Threading.CancellationToken stoppingToken) -> System.Threading.Tasks.Task! +abstract MMCA.Common.Infrastructure.Services.PeriodicBackgroundService.Interval.get -> System.TimeSpan +const MMCA.Common.Infrastructure.Auth.LoginProtectionSettings.SectionName = "LoginProtection" -> string! +const MMCA.Common.Infrastructure.Caching.CacheKeyPrefixOptions.SectionName = "Cache" -> string! +const MMCA.Common.Infrastructure.Hubs.NotificationHub.JoinChannelMethod = "JoinChannel" -> string! +const MMCA.Common.Infrastructure.Hubs.NotificationHub.LeaveChannelMethod = "LeaveChannel" -> string! +const MMCA.Common.Infrastructure.Hubs.NotificationHub.ReceiveChannelEventMethod = "ReceiveChannelEvent" -> string! +const MMCA.Common.Infrastructure.Hubs.NotificationHub.ReceiveNotificationMethod = "ReceiveNotification" -> string! +override MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailSaveChangesInterceptor.SavedChanges(Microsoft.EntityFrameworkCore.Diagnostics.SaveChangesCompletedEventData! eventData, int result) -> int +override MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailSaveChangesInterceptor.SavedChangesAsync(Microsoft.EntityFrameworkCore.Diagnostics.SaveChangesCompletedEventData! eventData, int result, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +override MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailSaveChangesInterceptor.SavingChanges(Microsoft.EntityFrameworkCore.Diagnostics.DbContextEventData! eventData, Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult result) -> Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult +override MMCA.Common.Infrastructure.Persistence.AuditTrail.AuditTrailSaveChangesInterceptor.SavingChangesAsync(Microsoft.EntityFrameworkCore.Diagnostics.DbContextEventData! eventData, Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult result, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask> +override MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeConfiguration.EntityTypeConfiguration.Configure(Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder! builder) -> void +override MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource.Equals(object? obj) -> bool +override MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource.GetHashCode() -> int +override MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource.ToString() -> string! +override MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget.GetHashCode() -> int +override MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget.ToString() -> string! +override MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext.ConfigureConventions(Microsoft.EntityFrameworkCore.ModelConfigurationBuilder! configurationBuilder) -> void +override MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext.OnConfiguring(Microsoft.EntityFrameworkCore.DbContextOptionsBuilder! optionsBuilder) -> void +override MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext.OnModelCreating(Microsoft.EntityFrameworkCore.ModelBuilder! modelBuilder) -> void +override MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext.SaveChanges(bool acceptAllChangesOnSuccess) -> int +override MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext.SaveChangesAsync(bool acceptAllChangesOnSuccess, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +override MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext.Set() -> Microsoft.EntityFrameworkCore.DbSet! +override MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.IdentityModuleDbSeederBase.SeedAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +override MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.Equals(object? obj) -> bool +override MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.GetHashCode() -> int +override MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.ToString() -> string! +override MMCA.Common.Infrastructure.Persistence.Interceptors.AuditSaveChangesInterceptor.SavingChanges(Microsoft.EntityFrameworkCore.Diagnostics.DbContextEventData! eventData, Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult result) -> Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult +override MMCA.Common.Infrastructure.Persistence.Interceptors.AuditSaveChangesInterceptor.SavingChangesAsync(Microsoft.EntityFrameworkCore.Diagnostics.DbContextEventData! eventData, Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult result, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask> +override MMCA.Common.Infrastructure.Persistence.Interceptors.DomainEventSaveChangesInterceptor.SavedChanges(Microsoft.EntityFrameworkCore.Diagnostics.SaveChangesCompletedEventData! eventData, int result) -> int +override MMCA.Common.Infrastructure.Persistence.Interceptors.DomainEventSaveChangesInterceptor.SavedChangesAsync(Microsoft.EntityFrameworkCore.Diagnostics.SaveChangesCompletedEventData! eventData, int result, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +override MMCA.Common.Infrastructure.Persistence.Interceptors.DomainEventSaveChangesInterceptor.SavingChanges(Microsoft.EntityFrameworkCore.Diagnostics.DbContextEventData! eventData, Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult result) -> Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult +override MMCA.Common.Infrastructure.Persistence.Interceptors.DomainEventSaveChangesInterceptor.SavingChangesAsync(Microsoft.EntityFrameworkCore.Diagnostics.DbContextEventData! eventData, Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult result, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask> +override MMCA.Common.Infrastructure.Persistence.Interceptors.TenantSaveChangesInterceptor.SavingChanges(Microsoft.EntityFrameworkCore.Diagnostics.DbContextEventData! eventData, Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult result) -> Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult +override MMCA.Common.Infrastructure.Persistence.Interceptors.TenantSaveChangesInterceptor.SavingChangesAsync(Microsoft.EntityFrameworkCore.Diagnostics.DbContextEventData! eventData, Microsoft.EntityFrameworkCore.Diagnostics.InterceptionResult result, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask> +override MMCA.Common.Infrastructure.Persistence.ValueGenerators.CosmosIntIdValueGenerator.GeneratesTemporaryValues.get -> bool +override MMCA.Common.Infrastructure.Persistence.ValueGenerators.CosmosIntIdValueGenerator.Next(Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry! entry) -> int +override MMCA.Common.Infrastructure.Services.PeriodicBackgroundService.ExecuteAsync(System.Threading.CancellationToken stoppingToken) -> System.Threading.Tasks.Task! +static MMCA.Common.Infrastructure.Caching.CacheOptions.Create(System.TimeSpan? expiration) -> Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions! +static MMCA.Common.Infrastructure.Caching.CacheOptions.DefaultDuration.get -> System.TimeSpan +static MMCA.Common.Infrastructure.Caching.CacheOptions.DefaultExpiration.get -> Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions! +static MMCA.Common.Infrastructure.DependencyInjection.AddAuditTrail(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Infrastructure.DependencyInjection.AddAzureBlobFileStorage(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Infrastructure.DependencyInjection.AddBrokerMessaging(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration, System.Action? configureConsumers = null) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Infrastructure.DependencyInjection.AddCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration? configuration = null) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Infrastructure.DependencyInjection.AddCommonHybridCache(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action? configure = null) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Infrastructure.DependencyInjection.AddEntityConfigurationAssembly(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Reflection.Assembly! assembly) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Infrastructure.DependencyInjection.AddInfrastructure(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Infrastructure.DependencyInjection.AddMultiTenancy(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Infrastructure.DependencyInjection.AddNativePushNotifications(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Infrastructure.DependencyInjection.AddNotificationInfrastructure(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Infrastructure.DependencyInjection.AddPushNotifications(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Infrastructure.DependencyInjection.AddScheduledJob(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Infrastructure.DependencyInjection.AddScheduledJobs(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Infrastructure.DependencyInjection.AddServices(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Infrastructure.DependencyInjection.AddTypedServiceClient(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! serviceName) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeBuilderExtensions.OwnsMoney(this Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder! builder, System.Linq.Expressions.Expression!>! navigationExpression, string! amountColumnName, string! currencyColumnName, bool required = true) -> Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder! +static MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeConfiguration.EntityTypeConfiguration.ApplyEngineConventions(Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder! builder, MMCA.Common.Application.Interfaces.Infrastructure.DataSource engine) -> void +static MMCA.Common.Infrastructure.Persistence.Configuration.IndexBuilderExtensions.HasSoftDeleteFilter(this Microsoft.EntityFrameworkCore.Metadata.Builders.IndexBuilder! indexBuilder, MMCA.Common.Application.Interfaces.Infrastructure.DataSource engine = MMCA.Common.Application.Interfaces.Infrastructure.DataSource.SQLServer, string? additionalFilter = null) -> Microsoft.EntityFrameworkCore.Metadata.Builders.IndexBuilder! +static MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource.operator !=(MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource? left, MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource? right) -> bool +static MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource.operator ==(MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource? left, MMCA.Common.Infrastructure.Persistence.DataSources.PhysicalDataSource? right) -> bool +static MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget.operator !=(MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget left, MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget right) -> bool +static MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget.operator ==(MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget left, MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget right) -> bool +static MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTargets.Expand(System.Collections.Generic.IEnumerable! sources, MMCA.Common.Infrastructure.Settings.TenancySettings? settings) -> System.Collections.Generic.List! +static MMCA.Common.Infrastructure.Persistence.DbContexts.ApplicationDbContext.ApplySoftDeleteFilters(Microsoft.EntityFrameworkCore.ModelBuilder! modelBuilder) -> void +static MMCA.Common.Infrastructure.Persistence.DbContexts.Design.DesignTimeDbContextHelper.CreateSqlServer(string![]! args, System.Action! configure) -> MMCA.Common.Infrastructure.Persistence.DbContexts.SQLServerDbContext! +static MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.DbSeeder.GetId(int id) -> TIdentifier +static MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.operator !=(MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount? left, MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount? right) -> bool +static MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount.operator ==(MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount? left, MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.SeedAccount? right) -> bool +static MMCA.Common.Infrastructure.Persistence.Encryption.EncryptedStringConverter.GenerateKey() -> byte[]! +static MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage.FromDomainEvent(MMCA.Common.Domain.Interfaces.IDomainEvent! domainEvent) -> MMCA.Common.Infrastructure.Persistence.Outbox.OutboxMessage! +static MMCA.Common.Infrastructure.Services.IntegrationEventConsumerExtensions.RegisterIntegrationEventConsumer(this MassTransit.IBusRegistrationConfigurator! x, bool registerFaultConsumer = true) -> MassTransit.IBusRegistrationConfigurator! +static readonly MMCA.Common.Infrastructure.AssemblyReference.Assembly -> System.Reflection.Assembly! +static readonly MMCA.Common.Infrastructure.AssemblyReference.AssemblyName -> string! +static readonly MMCA.Common.Infrastructure.Settings.AuditTrailSettings.SectionName -> string! +static readonly MMCA.Common.Infrastructure.Settings.ConnectionStringSettings.SectionName -> string! +static readonly MMCA.Common.Infrastructure.Settings.DataSourcesSettings.SectionName -> string! +static readonly MMCA.Common.Infrastructure.Settings.FileStorageSettings.SectionName -> string! +static readonly MMCA.Common.Infrastructure.Settings.JwksSettings.SectionName -> string! +static readonly MMCA.Common.Infrastructure.Settings.JwtSettings.SectionName -> string! +static readonly MMCA.Common.Infrastructure.Settings.MessageBusSettings.SectionName -> string! +static readonly MMCA.Common.Infrastructure.Settings.NativePushSettings.SectionName -> string! +static readonly MMCA.Common.Infrastructure.Settings.OutboxSettings.SectionName -> string! +static readonly MMCA.Common.Infrastructure.Settings.PersistenceSettings.SectionName -> string! +static readonly MMCA.Common.Infrastructure.Settings.PushNotificationSettings.SectionName -> string! +static readonly MMCA.Common.Infrastructure.Settings.SchedulerSettings.SectionName -> string! +static readonly MMCA.Common.Infrastructure.Settings.SmtpSettings.DefaultSmtpPort -> int +static readonly MMCA.Common.Infrastructure.Settings.SmtpSettings.SectionName -> string! +static readonly MMCA.Common.Infrastructure.Settings.TenancySettings.SectionName -> string! +virtual MMCA.Common.Infrastructure.Persistence.Configuration.EntityTypeConfiguration.EntityTypeConfigurationBase.Configure(Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder! builder) -> void +virtual MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding.IdentityModuleDbSeederBase.ShouldSeed.get -> bool +virtual MMCA.Common.Infrastructure.Services.PeriodicBackgroundService.IsEnabled.get -> bool +virtual MMCA.Common.Infrastructure.Services.PeriodicBackgroundService.StartupDelay.get -> System.TimeSpan +~override MMCA.Common.Infrastructure.Persistence.DataSources.TenantDataSourceTarget.Equals(object obj) -> bool diff --git a/Source/Core/MMCA.Common.Infrastructure/PublicAPI.Unshipped.txt b/Source/Core/MMCA.Common.Infrastructure/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/Source/Core/MMCA.Common.Infrastructure/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/Source/Core/MMCA.Common.Infrastructure/Services/FaultIntegrationEventConsumer.cs b/Source/Core/MMCA.Common.Infrastructure/Services/FaultIntegrationEventConsumer.cs index ca760512..a77ade00 100644 --- a/Source/Core/MMCA.Common.Infrastructure/Services/FaultIntegrationEventConsumer.cs +++ b/Source/Core/MMCA.Common.Infrastructure/Services/FaultIntegrationEventConsumer.cs @@ -13,8 +13,7 @@ namespace MMCA.Common.Infrastructure.Services; /// /// Registered automatically alongside every consumer wired through /// RegisterIntegrationEventConsumer<TEvent> (opt out per event with its -/// registerFaultConsumer parameter, or globally with -/// MessageBus:RegisterFaultConsumers=false). +/// registerFaultConsumer parameter). /// /// /// This consumer never throws. A fault consumer that faults would publish diff --git a/Source/Core/MMCA.Common.Infrastructure/Services/IntegrationEventConsumerExtensions.cs b/Source/Core/MMCA.Common.Infrastructure/Services/IntegrationEventConsumerExtensions.cs index fa51b176..693ec214 100644 --- a/Source/Core/MMCA.Common.Infrastructure/Services/IntegrationEventConsumerExtensions.cs +++ b/Source/Core/MMCA.Common.Infrastructure/Services/IntegrationEventConsumerExtensions.cs @@ -32,9 +32,7 @@ public static class IntegrationEventConsumerExtensions /// event. Defaults to . Pass for an event /// whose faults a host routes itself (a dedicated fault service, or a custom /// IConsumer<Fault<TEvent>>), so two consumers do not compete for the - /// same fault topic. This parameter is the per-event switch; the host-wide default is - /// MessageBus:RegisterFaultConsumers, which callers read themselves because this - /// extension has no access to configuration. + /// same fault topic. /// public IBusRegistrationConfigurator RegisterIntegrationEventConsumer( bool registerFaultConsumer = true) diff --git a/Source/Core/MMCA.Common.Infrastructure/Settings/MessageBusSettings.cs b/Source/Core/MMCA.Common.Infrastructure/Settings/MessageBusSettings.cs index ee4a3c98..a2a9047a 100644 --- a/Source/Core/MMCA.Common.Infrastructure/Settings/MessageBusSettings.cs +++ b/Source/Core/MMCA.Common.Infrastructure/Settings/MessageBusSettings.cs @@ -58,8 +58,9 @@ public sealed class MessageBusSettings /// /// Gets a value indicating whether the consumer-side idempotency inbox is enabled. When /// , IntegrationEventConsumer dedups already-processed messages via - /// an InboxMessages table in the consumer's database — which requires that table to exist - /// (apply the AddInboxMessages migration). Defaults to . + /// the InboxMessages table in the consumer's database. The table is part of the shared + /// relational model (created by the standard migrations; Cosmos hosts skip it), so enabling + /// this on a migrated relational host needs no schema work. Defaults to . /// /// RECOMMENDED for any broker-connected host. Broker delivery is /// at-least-once by contract: a consumer that acks after a network blip, a redelivered message @@ -109,23 +110,6 @@ public sealed class MessageBusSettings /// /// public IReadOnlyList RedeliveryIntervalsSeconds { get; init; } = [60, 600, 3600]; - - /// - /// Gets a value indicating whether a FaultIntegrationEventConsumer<TEvent> is - /// registered alongside each integration-event consumer. MassTransit publishes a - /// Fault<TEvent> message when a consumer exhausts its retries, and with nothing - /// subscribed to that topic the only trace of an undelivered event is a row in the broker's - /// _error queue that no dashboard is watching. The fault consumer turns that into one - /// structured Error log plus a broker.fault.count metric. Defaults to - /// . - /// - /// Hosts that route faults themselves (a dedicated fault service, or a per-event opt-out via - /// the registerFaultConsumer parameter on - /// RegisterIntegrationEventConsumer<TEvent>) can set this to - /// to document the intent. - /// - /// - public bool RegisterFaultConsumers { get; init; } = true; } /// Available message bus transports. diff --git a/Source/Core/MMCA.Common.Infrastructure/packages.lock.json b/Source/Core/MMCA.Common.Infrastructure/packages.lock.json index ae44678a..e2b53606 100644 --- a/Source/Core/MMCA.Common.Infrastructure/packages.lock.json +++ b/Source/Core/MMCA.Common.Infrastructure/packages.lock.json @@ -92,6 +92,12 @@ "Newtonsoft.Json": "13.0.1" } }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[5.6.0, )", + "resolved": "5.6.0", + "contentHash": "W4kJGezNIKLzo0Ak5FAQDFvkMf2U7DtGL4THmHyRSApfKsKt5V+eX/bU0ZLKAt/uf9Bb2o1bi0YDKj/GRB/vYQ==" + }, "Microsoft.EntityFrameworkCore.Cosmos": { "type": "Direct", "requested": "[10.0.11, )", diff --git a/Source/Core/MMCA.Common.Shared/PublicAPI.Shipped.txt b/Source/Core/MMCA.Common.Shared/PublicAPI.Shipped.txt new file mode 100644 index 00000000..80864156 --- /dev/null +++ b/Source/Core/MMCA.Common.Shared/PublicAPI.Shipped.txt @@ -0,0 +1,718 @@ +#nullable enable +MMCA.Common.Shared.Abstractions.CollectionResult +MMCA.Common.Shared.Abstractions.CollectionResult.CollectionResult() -> void +MMCA.Common.Shared.Abstractions.CollectionResult.CollectionResult(MMCA.Common.Shared.Abstractions.CollectionResult! original) -> void +MMCA.Common.Shared.Abstractions.CollectionResult.CollectionResult(System.Collections.Generic.IReadOnlyCollection! items) -> void +MMCA.Common.Shared.Abstractions.CollectionResult.Items.get -> System.Collections.Generic.ICollection! +MMCA.Common.Shared.Abstractions.CollectionResult.Items.init -> void +MMCA.Common.Shared.Abstractions.Error +MMCA.Common.Shared.Abstractions.Error.Code.get -> string! +MMCA.Common.Shared.Abstractions.Error.Code.init -> void +MMCA.Common.Shared.Abstractions.Error.Deconstruct(out string! Code, out string! Message, out MMCA.Common.Shared.Abstractions.ErrorType Type, out string? Source, out string? Target) -> void +MMCA.Common.Shared.Abstractions.Error.Error(MMCA.Common.Shared.Abstractions.Error! original) -> void +MMCA.Common.Shared.Abstractions.Error.Error(string! Code, string! Message, MMCA.Common.Shared.Abstractions.ErrorType Type, string? Source = null, string? Target = null) -> void +MMCA.Common.Shared.Abstractions.Error.Message.get -> string! +MMCA.Common.Shared.Abstractions.Error.Message.init -> void +MMCA.Common.Shared.Abstractions.Error.Source.get -> string? +MMCA.Common.Shared.Abstractions.Error.Source.init -> void +MMCA.Common.Shared.Abstractions.Error.Target.get -> string? +MMCA.Common.Shared.Abstractions.Error.Target.init -> void +MMCA.Common.Shared.Abstractions.Error.Type.get -> MMCA.Common.Shared.Abstractions.ErrorType +MMCA.Common.Shared.Abstractions.Error.Type.init -> void +MMCA.Common.Shared.Abstractions.Error.WithSource(string! source) -> MMCA.Common.Shared.Abstractions.Error! +MMCA.Common.Shared.Abstractions.Error.WithTarget(string! target) -> MMCA.Common.Shared.Abstractions.Error! +MMCA.Common.Shared.Abstractions.ErrorType +MMCA.Common.Shared.Abstractions.ErrorType.Conflict = 3 -> MMCA.Common.Shared.Abstractions.ErrorType +MMCA.Common.Shared.Abstractions.ErrorType.Failure = 7 -> MMCA.Common.Shared.Abstractions.ErrorType +MMCA.Common.Shared.Abstractions.ErrorType.Forbidden = 5 -> MMCA.Common.Shared.Abstractions.ErrorType +MMCA.Common.Shared.Abstractions.ErrorType.Invariant = 1 -> MMCA.Common.Shared.Abstractions.ErrorType +MMCA.Common.Shared.Abstractions.ErrorType.NotFound = 2 -> MMCA.Common.Shared.Abstractions.ErrorType +MMCA.Common.Shared.Abstractions.ErrorType.Unauthorized = 4 -> MMCA.Common.Shared.Abstractions.ErrorType +MMCA.Common.Shared.Abstractions.ErrorType.UnprocessableEntity = 6 -> MMCA.Common.Shared.Abstractions.ErrorType +MMCA.Common.Shared.Abstractions.ErrorType.Validation = 0 -> MMCA.Common.Shared.Abstractions.ErrorType +MMCA.Common.Shared.Abstractions.KeysetCollectionResult +MMCA.Common.Shared.Abstractions.KeysetCollectionResult.Equals(MMCA.Common.Shared.Abstractions.KeysetCollectionResult? other) -> bool +MMCA.Common.Shared.Abstractions.KeysetCollectionResult.KeysetCollectionResult() -> void +MMCA.Common.Shared.Abstractions.KeysetCollectionResult.KeysetCollectionResult(System.Collections.Generic.IReadOnlyCollection! items, string? nextCursor) -> void +MMCA.Common.Shared.Abstractions.KeysetCollectionResult.NextCursor.get -> string? +MMCA.Common.Shared.Abstractions.KeysetCollectionResult.NextCursor.init -> void +MMCA.Common.Shared.Abstractions.KeysetCursor +MMCA.Common.Shared.Abstractions.KeysetPageRequest +MMCA.Common.Shared.Abstractions.KeysetPageRequest.$() -> MMCA.Common.Shared.Abstractions.KeysetPageRequest! +MMCA.Common.Shared.Abstractions.KeysetPageRequest.Cursor.get -> string? +MMCA.Common.Shared.Abstractions.KeysetPageRequest.Cursor.init -> void +MMCA.Common.Shared.Abstractions.KeysetPageRequest.Descending.get -> bool +MMCA.Common.Shared.Abstractions.KeysetPageRequest.Descending.init -> void +MMCA.Common.Shared.Abstractions.KeysetPageRequest.Equals(MMCA.Common.Shared.Abstractions.KeysetPageRequest? other) -> bool +MMCA.Common.Shared.Abstractions.KeysetPageRequest.KeysetPageRequest() -> void +MMCA.Common.Shared.Abstractions.KeysetPageRequest.KeysetPageRequest(int pageSize, string? sortColumn = null, bool descending = false, string? cursor = null) -> void +MMCA.Common.Shared.Abstractions.KeysetPageRequest.PageSize.get -> int +MMCA.Common.Shared.Abstractions.KeysetPageRequest.PageSize.init -> void +MMCA.Common.Shared.Abstractions.KeysetPageRequest.SortColumn.get -> string? +MMCA.Common.Shared.Abstractions.KeysetPageRequest.SortColumn.init -> void +MMCA.Common.Shared.Abstractions.PagedCollectionResult +MMCA.Common.Shared.Abstractions.PagedCollectionResult.Equals(MMCA.Common.Shared.Abstractions.PagedCollectionResult? other) -> bool +MMCA.Common.Shared.Abstractions.PagedCollectionResult.PagedCollectionResult() -> void +MMCA.Common.Shared.Abstractions.PagedCollectionResult.PagedCollectionResult(System.Collections.Generic.IReadOnlyCollection! items, MMCA.Common.Shared.Abstractions.PaginationMetadata! paginationMetadata) -> void +MMCA.Common.Shared.Abstractions.PagedCollectionResult.PaginationMetadata.get -> MMCA.Common.Shared.Abstractions.PaginationMetadata! +MMCA.Common.Shared.Abstractions.PagedCollectionResult.PaginationMetadata.init -> void +MMCA.Common.Shared.Abstractions.PaginationMetadata +MMCA.Common.Shared.Abstractions.PaginationMetadata.$() -> MMCA.Common.Shared.Abstractions.PaginationMetadata! +MMCA.Common.Shared.Abstractions.PaginationMetadata.CurrentPage.get -> int +MMCA.Common.Shared.Abstractions.PaginationMetadata.CurrentPage.init -> void +MMCA.Common.Shared.Abstractions.PaginationMetadata.Equals(MMCA.Common.Shared.Abstractions.PaginationMetadata? other) -> bool +MMCA.Common.Shared.Abstractions.PaginationMetadata.FirstRowOnPage.get -> int +MMCA.Common.Shared.Abstractions.PaginationMetadata.LastRowOnPage.get -> int +MMCA.Common.Shared.Abstractions.PaginationMetadata.PageSize.get -> int +MMCA.Common.Shared.Abstractions.PaginationMetadata.PageSize.init -> void +MMCA.Common.Shared.Abstractions.PaginationMetadata.PaginationMetadata() -> void +MMCA.Common.Shared.Abstractions.PaginationMetadata.PaginationMetadata(int totalItemCount, int pageSize, int currentPage) -> void +MMCA.Common.Shared.Abstractions.PaginationMetadata.TotalItemCount.get -> int +MMCA.Common.Shared.Abstractions.PaginationMetadata.TotalItemCount.init -> void +MMCA.Common.Shared.Abstractions.PaginationMetadata.TotalPageCount.get -> int +MMCA.Common.Shared.Abstractions.Result +MMCA.Common.Shared.Abstractions.Result.AddErrors(System.Collections.Generic.IEnumerable! errors) -> void +MMCA.Common.Shared.Abstractions.Result.Errors.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Shared.Abstractions.Result.IsFailure.get -> bool +MMCA.Common.Shared.Abstractions.Result.IsSuccess.get -> bool +MMCA.Common.Shared.Abstractions.Result.Result() -> void +MMCA.Common.Shared.Abstractions.Result +MMCA.Common.Shared.Abstractions.Result.BindAsync(System.Func!>!>! binder) -> System.Threading.Tasks.Task!>! +MMCA.Common.Shared.Abstractions.Result.Map(System.Func! mapper) -> MMCA.Common.Shared.Abstractions.Result! +MMCA.Common.Shared.Abstractions.Result.Match(System.Func! onSuccess, System.Func!, TResult>! onFailure) -> TResult +MMCA.Common.Shared.Abstractions.Result.Value.get -> T? +MMCA.Common.Shared.Abstractions.ServiceContractAttribute +MMCA.Common.Shared.Abstractions.ServiceContractAttribute.ServiceContractAttribute() -> void +MMCA.Common.Shared.Abstractions.ServiceContractAttribute.ServiceContractAttribute(string! version) -> void +MMCA.Common.Shared.Abstractions.ServiceContractAttribute.Version.get -> string! +MMCA.Common.Shared.Auth.AuthClaimTypes +MMCA.Common.Shared.Auth.AuthenticationResponse +MMCA.Common.Shared.Auth.AuthenticationResponse.AccessToken.get -> string! +MMCA.Common.Shared.Auth.AuthenticationResponse.AccessToken.init -> void +MMCA.Common.Shared.Auth.AuthenticationResponse.AccessTokenExpiry.get -> System.DateTime +MMCA.Common.Shared.Auth.AuthenticationResponse.AccessTokenExpiry.init -> void +MMCA.Common.Shared.Auth.AuthenticationResponse.AuthenticationResponse() -> void +MMCA.Common.Shared.Auth.AuthenticationResponse.AuthenticationResponse(string! AccessToken, string! RefreshToken, System.DateTime AccessTokenExpiry) -> void +MMCA.Common.Shared.Auth.AuthenticationResponse.Deconstruct(out string! AccessToken, out string! RefreshToken, out System.DateTime AccessTokenExpiry) -> void +MMCA.Common.Shared.Auth.AuthenticationResponse.Equals(MMCA.Common.Shared.Auth.AuthenticationResponse other) -> bool +MMCA.Common.Shared.Auth.AuthenticationResponse.RefreshToken.get -> string! +MMCA.Common.Shared.Auth.AuthenticationResponse.RefreshToken.init -> void +MMCA.Common.Shared.Auth.ChangePasswordRequest +MMCA.Common.Shared.Auth.ChangePasswordRequest.ChangePasswordRequest() -> void +MMCA.Common.Shared.Auth.ChangePasswordRequest.ChangePasswordRequest(string! CurrentPassword, string! NewPassword) -> void +MMCA.Common.Shared.Auth.ChangePasswordRequest.CurrentPassword.get -> string! +MMCA.Common.Shared.Auth.ChangePasswordRequest.CurrentPassword.init -> void +MMCA.Common.Shared.Auth.ChangePasswordRequest.Deconstruct(out string! CurrentPassword, out string! NewPassword) -> void +MMCA.Common.Shared.Auth.ChangePasswordRequest.Equals(MMCA.Common.Shared.Auth.ChangePasswordRequest other) -> bool +MMCA.Common.Shared.Auth.ChangePasswordRequest.NewPassword.get -> string! +MMCA.Common.Shared.Auth.ChangePasswordRequest.NewPassword.init -> void +MMCA.Common.Shared.Auth.ChangePreferencesRequest +MMCA.Common.Shared.Auth.ChangePreferencesRequest.$() -> MMCA.Common.Shared.Auth.ChangePreferencesRequest! +MMCA.Common.Shared.Auth.ChangePreferencesRequest.ChangePreferencesRequest(string? Culture, string? Theme) -> void +MMCA.Common.Shared.Auth.ChangePreferencesRequest.Culture.get -> string? +MMCA.Common.Shared.Auth.ChangePreferencesRequest.Culture.init -> void +MMCA.Common.Shared.Auth.ChangePreferencesRequest.Deconstruct(out string? Culture, out string? Theme) -> void +MMCA.Common.Shared.Auth.ChangePreferencesRequest.Equals(MMCA.Common.Shared.Auth.ChangePreferencesRequest? other) -> bool +MMCA.Common.Shared.Auth.ChangePreferencesRequest.Theme.get -> string? +MMCA.Common.Shared.Auth.ChangePreferencesRequest.Theme.init -> void +MMCA.Common.Shared.Auth.IPermissionRegistry +MMCA.Common.Shared.Auth.IPermissionRegistry.GetPermissions(string! role) -> System.Collections.Generic.IReadOnlySet! +MMCA.Common.Shared.Auth.IPermissionRegistry.HasPermission(System.Collections.Generic.IEnumerable! roles, string! permission) -> bool +MMCA.Common.Shared.Auth.LoginRequest +MMCA.Common.Shared.Auth.LoginRequest.Deconstruct(out string! Email, out string! Password) -> void +MMCA.Common.Shared.Auth.LoginRequest.Email.get -> string! +MMCA.Common.Shared.Auth.LoginRequest.Email.init -> void +MMCA.Common.Shared.Auth.LoginRequest.Equals(MMCA.Common.Shared.Auth.LoginRequest other) -> bool +MMCA.Common.Shared.Auth.LoginRequest.LoginRequest() -> void +MMCA.Common.Shared.Auth.LoginRequest.LoginRequest(string! Email, string! Password) -> void +MMCA.Common.Shared.Auth.LoginRequest.Password.get -> string! +MMCA.Common.Shared.Auth.LoginRequest.Password.init -> void +MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest +MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest.Code.get -> string! +MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest.Code.init -> void +MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest.Deconstruct(out string! Code) -> void +MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest.Equals(MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest other) -> bool +MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest.OAuthCodeExchangeRequest() -> void +MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest.OAuthCodeExchangeRequest(string! Code) -> void +MMCA.Common.Shared.Auth.PermissionRegistry +MMCA.Common.Shared.Auth.PermissionRegistry.GetPermissions(string! role) -> System.Collections.Generic.IReadOnlySet! +MMCA.Common.Shared.Auth.PermissionRegistry.HasPermission(System.Collections.Generic.IEnumerable! roles, string! permission) -> bool +MMCA.Common.Shared.Auth.PermissionRegistry.PermissionRegistry(System.Collections.Generic.IReadOnlyDictionary!>! rolePermissions) -> void +MMCA.Common.Shared.Auth.PermissionRegistryBuilder +MMCA.Common.Shared.Auth.PermissionRegistryBuilder.Build() -> MMCA.Common.Shared.Auth.PermissionRegistry! +MMCA.Common.Shared.Auth.PermissionRegistryBuilder.Grant(string! role, params string![]! permissions) -> MMCA.Common.Shared.Auth.PermissionRegistryBuilder! +MMCA.Common.Shared.Auth.PermissionRegistryBuilder.PermissionRegistryBuilder() -> void +MMCA.Common.Shared.Auth.RefreshTokenRequest +MMCA.Common.Shared.Auth.RefreshTokenRequest.AccessToken.get -> string! +MMCA.Common.Shared.Auth.RefreshTokenRequest.AccessToken.init -> void +MMCA.Common.Shared.Auth.RefreshTokenRequest.Deconstruct(out string! AccessToken, out string! RefreshToken) -> void +MMCA.Common.Shared.Auth.RefreshTokenRequest.Equals(MMCA.Common.Shared.Auth.RefreshTokenRequest other) -> bool +MMCA.Common.Shared.Auth.RefreshTokenRequest.RefreshToken.get -> string! +MMCA.Common.Shared.Auth.RefreshTokenRequest.RefreshToken.init -> void +MMCA.Common.Shared.Auth.RefreshTokenRequest.RefreshTokenRequest() -> void +MMCA.Common.Shared.Auth.RefreshTokenRequest.RefreshTokenRequest(string! AccessToken, string! RefreshToken) -> void +MMCA.Common.Shared.Auth.RegisterRequest +MMCA.Common.Shared.Auth.RegisterRequest.Address.get -> MMCA.Common.Shared.ValueObjects.Address? +MMCA.Common.Shared.Auth.RegisterRequest.Address.init -> void +MMCA.Common.Shared.Auth.RegisterRequest.Deconstruct(out string! Email, out string! Password, out string! FirstName, out string! LastName, out MMCA.Common.Shared.ValueObjects.Address? Address) -> void +MMCA.Common.Shared.Auth.RegisterRequest.Email.get -> string! +MMCA.Common.Shared.Auth.RegisterRequest.Email.init -> void +MMCA.Common.Shared.Auth.RegisterRequest.Equals(MMCA.Common.Shared.Auth.RegisterRequest other) -> bool +MMCA.Common.Shared.Auth.RegisterRequest.FirstName.get -> string! +MMCA.Common.Shared.Auth.RegisterRequest.FirstName.init -> void +MMCA.Common.Shared.Auth.RegisterRequest.LastName.get -> string! +MMCA.Common.Shared.Auth.RegisterRequest.LastName.init -> void +MMCA.Common.Shared.Auth.RegisterRequest.Password.get -> string! +MMCA.Common.Shared.Auth.RegisterRequest.Password.init -> void +MMCA.Common.Shared.Auth.RegisterRequest.RegisterRequest() -> void +MMCA.Common.Shared.Auth.RegisterRequest.RegisterRequest(string! Email, string! Password, string! FirstName, string! LastName, MMCA.Common.Shared.ValueObjects.Address? Address = null) -> void +MMCA.Common.Shared.Auth.RoleNames +MMCA.Common.Shared.Auth.RoleValue +MMCA.Common.Shared.Auth.RoleValue.RoleValue(string! value) -> void +MMCA.Common.Shared.Auth.RoleValue.Value.get -> string! +MMCA.Common.Shared.Auth.UserPreferencesResponse +MMCA.Common.Shared.Auth.UserPreferencesResponse.$() -> MMCA.Common.Shared.Auth.UserPreferencesResponse! +MMCA.Common.Shared.Auth.UserPreferencesResponse.Culture.get -> string? +MMCA.Common.Shared.Auth.UserPreferencesResponse.Culture.init -> void +MMCA.Common.Shared.Auth.UserPreferencesResponse.Deconstruct(out string? Culture, out string? Theme) -> void +MMCA.Common.Shared.Auth.UserPreferencesResponse.Equals(MMCA.Common.Shared.Auth.UserPreferencesResponse? other) -> bool +MMCA.Common.Shared.Auth.UserPreferencesResponse.Theme.get -> string? +MMCA.Common.Shared.Auth.UserPreferencesResponse.Theme.init -> void +MMCA.Common.Shared.Auth.UserPreferencesResponse.UserPreferencesResponse(string? Culture, string? Theme) -> void +MMCA.Common.Shared.AuthenticationRequest +MMCA.Common.Shared.AuthenticationRequest.AuthenticationRequest() -> void +MMCA.Common.Shared.AuthenticationRequest.AuthenticationRequest(string! DeviceId, string! Email, string! DeviceFormFactor, string! DevicePlatform, string! DeviceModel, string! DeviceManufacturer, string! DeviceName, string! DeviceType) -> void +MMCA.Common.Shared.AuthenticationRequest.Deconstruct(out string! DeviceId, out string! Email, out string! DeviceFormFactor, out string! DevicePlatform, out string! DeviceModel, out string! DeviceManufacturer, out string! DeviceName, out string! DeviceType) -> void +MMCA.Common.Shared.AuthenticationRequest.DeviceFormFactor.get -> string! +MMCA.Common.Shared.AuthenticationRequest.DeviceFormFactor.init -> void +MMCA.Common.Shared.AuthenticationRequest.DeviceId.get -> string! +MMCA.Common.Shared.AuthenticationRequest.DeviceId.init -> void +MMCA.Common.Shared.AuthenticationRequest.DeviceManufacturer.get -> string! +MMCA.Common.Shared.AuthenticationRequest.DeviceManufacturer.init -> void +MMCA.Common.Shared.AuthenticationRequest.DeviceModel.get -> string! +MMCA.Common.Shared.AuthenticationRequest.DeviceModel.init -> void +MMCA.Common.Shared.AuthenticationRequest.DeviceName.get -> string! +MMCA.Common.Shared.AuthenticationRequest.DeviceName.init -> void +MMCA.Common.Shared.AuthenticationRequest.DevicePlatform.get -> string! +MMCA.Common.Shared.AuthenticationRequest.DevicePlatform.init -> void +MMCA.Common.Shared.AuthenticationRequest.DeviceType.get -> string! +MMCA.Common.Shared.AuthenticationRequest.DeviceType.init -> void +MMCA.Common.Shared.AuthenticationRequest.Email.get -> string! +MMCA.Common.Shared.AuthenticationRequest.Email.init -> void +MMCA.Common.Shared.AuthenticationRequest.Equals(MMCA.Common.Shared.AuthenticationRequest other) -> bool +MMCA.Common.Shared.Calendars.IcsCalendarBuilder +MMCA.Common.Shared.Calendars.IcsEvent +MMCA.Common.Shared.Calendars.IcsEvent.$() -> MMCA.Common.Shared.Calendars.IcsEvent! +MMCA.Common.Shared.Calendars.IcsEvent.Deconstruct(out string! Uid, out string! Summary, out System.DateTimeOffset StartsAtUtc, out System.DateTimeOffset EndsAtUtc, out string? Description, out string? Location) -> void +MMCA.Common.Shared.Calendars.IcsEvent.Description.get -> string? +MMCA.Common.Shared.Calendars.IcsEvent.Description.init -> void +MMCA.Common.Shared.Calendars.IcsEvent.EndsAtUtc.get -> System.DateTimeOffset +MMCA.Common.Shared.Calendars.IcsEvent.EndsAtUtc.init -> void +MMCA.Common.Shared.Calendars.IcsEvent.Equals(MMCA.Common.Shared.Calendars.IcsEvent? other) -> bool +MMCA.Common.Shared.Calendars.IcsEvent.IcsEvent(string! Uid, string! Summary, System.DateTimeOffset StartsAtUtc, System.DateTimeOffset EndsAtUtc, string? Description = null, string? Location = null) -> void +MMCA.Common.Shared.Calendars.IcsEvent.Location.get -> string? +MMCA.Common.Shared.Calendars.IcsEvent.Location.init -> void +MMCA.Common.Shared.Calendars.IcsEvent.StartsAtUtc.get -> System.DateTimeOffset +MMCA.Common.Shared.Calendars.IcsEvent.StartsAtUtc.init -> void +MMCA.Common.Shared.Calendars.IcsEvent.Summary.get -> string! +MMCA.Common.Shared.Calendars.IcsEvent.Summary.init -> void +MMCA.Common.Shared.Calendars.IcsEvent.Uid.get -> string! +MMCA.Common.Shared.Calendars.IcsEvent.Uid.init -> void +MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe +MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.AcquireAsync(string! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.KeyedSemaphoreStripe() -> void +MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.KeyedSemaphoreStripe(int width) -> void +MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.Releaser +MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.Releaser.Dispose() -> void +MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.Releaser.Equals(MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.Releaser other) -> bool +MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.Releaser.Releaser() -> void +MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.Width.get -> int +MMCA.Common.Shared.DTOs.BaseLookup +MMCA.Common.Shared.DTOs.BaseLookup.BaseLookup() -> void +MMCA.Common.Shared.DTOs.BaseLookup.BaseLookup(MMCA.Common.Shared.DTOs.BaseLookup! original) -> void +MMCA.Common.Shared.DTOs.BaseLookup.Id.get -> TIdentifierType +MMCA.Common.Shared.DTOs.BaseLookup.Id.init -> void +MMCA.Common.Shared.DTOs.BaseLookup.Name.get -> string! +MMCA.Common.Shared.DTOs.BaseLookup.Name.init -> void +MMCA.Common.Shared.DTOs.ConcurrencyTokenRequest +MMCA.Common.Shared.DTOs.ConcurrencyTokenRequest.$() -> MMCA.Common.Shared.DTOs.ConcurrencyTokenRequest! +MMCA.Common.Shared.DTOs.ConcurrencyTokenRequest.ConcurrencyTokenRequest() -> void +MMCA.Common.Shared.DTOs.ConcurrencyTokenRequest.Equals(MMCA.Common.Shared.DTOs.ConcurrencyTokenRequest? other) -> bool +MMCA.Common.Shared.DTOs.ConcurrencyTokenRequest.RowVersion.get -> byte[]? +MMCA.Common.Shared.DTOs.ConcurrencyTokenRequest.RowVersion.init -> void +MMCA.Common.Shared.DTOs.IBaseDTO +MMCA.Common.Shared.DTOs.IBaseDTO.Id.get -> TIdentifierType +MMCA.Common.Shared.DTOs.IBaseDTO.Id.init -> void +MMCA.Common.Shared.DTOs.IConcurrencyAware +MMCA.Common.Shared.DTOs.IConcurrencyAware.RowVersion.get -> byte[]? +MMCA.Common.Shared.DTOs.IConcurrencyAware.RowVersion.init -> void +MMCA.Common.Shared.Exceptions.DomainException +MMCA.Common.Shared.Exceptions.DomainException.DomainException() -> void +MMCA.Common.Shared.Exceptions.DomainException.DomainException(string! message) -> void +MMCA.Common.Shared.Exceptions.DomainException.DomainException(string! message, System.Exception! innerException) -> void +MMCA.Common.Shared.Exceptions.DomainInvariantViolationException +MMCA.Common.Shared.Exceptions.DomainInvariantViolationException.DomainInvariantViolationException() -> void +MMCA.Common.Shared.Exceptions.DomainInvariantViolationException.DomainInvariantViolationException(string! message) -> void +MMCA.Common.Shared.Exceptions.DomainInvariantViolationException.DomainInvariantViolationException(string! message, System.Exception! innerException) -> void +MMCA.Common.Shared.Extensions.DomainHelper +MMCA.Common.Shared.Extensions.DomainHelper.extension(string?) +MMCA.Common.Shared.Extensions.DomainHelper.extension(string?).Parse() -> TIdentifier +MMCA.Common.Shared.Extensions.DomainHelper.extension(string?).TryParse(out TIdentifier result) -> bool +MMCA.Common.Shared.Globalization.SupportedCultures +MMCA.Common.Shared.Http.IdempotencyHeaders +MMCA.Common.Shared.Notifications.NotificationFeatures +MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest +MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest.$() -> MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest! +MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest.DeviceInstallationRequest() -> void +MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest.Equals(MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest? other) -> bool +MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest.InstallationId.get -> string! +MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest.InstallationId.init -> void +MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest.Platform.get -> string! +MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest.Platform.init -> void +MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest.PushChannel.get -> string! +MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest.PushChannel.init -> void +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.Body.get -> string! +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.Body.init -> void +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.CreatedOn.get -> System.DateTime +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.CreatedOn.init -> void +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.Id.get -> int +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.Id.init -> void +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.PushNotificationDTO() -> void +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.PushNotificationDTO(MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO! original) -> void +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.RecipientCount.get -> int +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.RecipientCount.init -> void +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.ScopeKey.get -> string? +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.ScopeKey.init -> void +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.SentByUserId.get -> int +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.SentByUserId.init -> void +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.Status.get -> string! +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.Status.init -> void +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.Title.get -> string! +MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.Title.init -> void +MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest +MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest.$() -> MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest! +MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest.Body.get -> string! +MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest.Body.init -> void +MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest.Deconstruct(out string! Title, out string! Body) -> void +MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest.Equals(MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest? other) -> bool +MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest.ScopeKey.get -> string? +MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest.ScopeKey.init -> void +MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest.SendPushNotificationRequest(string! Title, string! Body) -> void +MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest.Title.get -> string! +MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest.Title.init -> void +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.$() -> MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO! +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.Body.get -> string! +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.Body.init -> void +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.Equals(MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO? other) -> bool +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.Id.get -> int +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.Id.init -> void +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.IsRead.get -> bool +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.IsRead.init -> void +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.PushNotificationId.get -> int +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.PushNotificationId.init -> void +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.ReadOn.get -> System.DateTime? +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.ReadOn.init -> void +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.SentOn.get -> System.DateTime +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.SentOn.init -> void +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.Title.get -> string! +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.Title.init -> void +MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.UserNotificationDTO() -> void +MMCA.Common.Shared.Privacy.PrivacyFeatures +MMCA.Common.Shared.Privacy.UserDataExportDTO +MMCA.Common.Shared.Privacy.UserDataExportDTO.$() -> MMCA.Common.Shared.Privacy.UserDataExportDTO! +MMCA.Common.Shared.Privacy.UserDataExportDTO.Equals(MMCA.Common.Shared.Privacy.UserDataExportDTO? other) -> bool +MMCA.Common.Shared.Privacy.UserDataExportDTO.FormatVersion.get -> string! +MMCA.Common.Shared.Privacy.UserDataExportDTO.FormatVersion.init -> void +MMCA.Common.Shared.Privacy.UserDataExportDTO.GeneratedOn.get -> System.DateTimeOffset +MMCA.Common.Shared.Privacy.UserDataExportDTO.GeneratedOn.init -> void +MMCA.Common.Shared.Privacy.UserDataExportDTO.Sections.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Shared.Privacy.UserDataExportDTO.Sections.init -> void +MMCA.Common.Shared.Privacy.UserDataExportDTO.Subject.get -> object? +MMCA.Common.Shared.Privacy.UserDataExportDTO.Subject.init -> void +MMCA.Common.Shared.Privacy.UserDataExportDTO.UserDataExportDTO() -> void +MMCA.Common.Shared.Privacy.UserDataExportDTO.UserId.get -> int +MMCA.Common.Shared.Privacy.UserDataExportDTO.UserId.init -> void +MMCA.Common.Shared.Privacy.UserDataExportSectionDTO +MMCA.Common.Shared.Privacy.UserDataExportSectionDTO.$() -> MMCA.Common.Shared.Privacy.UserDataExportSectionDTO! +MMCA.Common.Shared.Privacy.UserDataExportSectionDTO.Available.get -> bool +MMCA.Common.Shared.Privacy.UserDataExportSectionDTO.Available.init -> void +MMCA.Common.Shared.Privacy.UserDataExportSectionDTO.Data.get -> object? +MMCA.Common.Shared.Privacy.UserDataExportSectionDTO.Data.init -> void +MMCA.Common.Shared.Privacy.UserDataExportSectionDTO.Equals(MMCA.Common.Shared.Privacy.UserDataExportSectionDTO? other) -> bool +MMCA.Common.Shared.Privacy.UserDataExportSectionDTO.SectionName.get -> string! +MMCA.Common.Shared.Privacy.UserDataExportSectionDTO.SectionName.init -> void +MMCA.Common.Shared.Privacy.UserDataExportSectionDTO.UnavailableReason.get -> string? +MMCA.Common.Shared.Privacy.UserDataExportSectionDTO.UnavailableReason.init -> void +MMCA.Common.Shared.Privacy.UserDataExportSectionDTO.UserDataExportSectionDTO() -> void +MMCA.Common.Shared.Resilience.BrokerResilienceDefaults +MMCA.Common.Shared.Resilience.HttpResilienceDefaults +MMCA.Common.Shared.Serialization.ResultJsonConverterFactory +MMCA.Common.Shared.Serialization.ResultJsonConverterFactory.ResultJsonConverterFactory() -> void +MMCA.Common.Shared.ValueObjects.Address +MMCA.Common.Shared.ValueObjects.Address.AddressLine1.get -> string! +MMCA.Common.Shared.ValueObjects.Address.AddressLine2.get -> string? +MMCA.Common.Shared.ValueObjects.Address.City.get -> string? +MMCA.Common.Shared.ValueObjects.Address.Country.get -> string? +MMCA.Common.Shared.ValueObjects.Address.Equals(MMCA.Common.Shared.ValueObjects.Address? other) -> bool +MMCA.Common.Shared.ValueObjects.Address.State.get -> string? +MMCA.Common.Shared.ValueObjects.Address.ZipCode.get -> string? +MMCA.Common.Shared.ValueObjects.AddressInvariants +MMCA.Common.Shared.ValueObjects.Currency +MMCA.Common.Shared.ValueObjects.Currency.Code.get -> string! +MMCA.Common.Shared.ValueObjects.Currency.Code.init -> void +MMCA.Common.Shared.ValueObjects.Currency.Equals(MMCA.Common.Shared.ValueObjects.Currency? other) -> bool +MMCA.Common.Shared.ValueObjects.CurrencyJsonConverter +MMCA.Common.Shared.ValueObjects.CurrencyJsonConverter.CurrencyJsonConverter() -> void +MMCA.Common.Shared.ValueObjects.DateRange +MMCA.Common.Shared.ValueObjects.DateRange.Contains(System.DateOnly instant) -> bool +MMCA.Common.Shared.ValueObjects.DateRange.Deconstruct(out System.DateOnly start, out System.DateOnly end) -> void +MMCA.Common.Shared.ValueObjects.DateRange.End.get -> System.DateOnly +MMCA.Common.Shared.ValueObjects.DateRange.Equals(MMCA.Common.Shared.ValueObjects.DateRange? other) -> bool +MMCA.Common.Shared.ValueObjects.DateRange.LengthInDays.get -> int +MMCA.Common.Shared.ValueObjects.DateRange.Overlaps(MMCA.Common.Shared.ValueObjects.DateRange! other) -> bool +MMCA.Common.Shared.ValueObjects.DateRange.Start.get -> System.DateOnly +MMCA.Common.Shared.ValueObjects.DateTimeRange +MMCA.Common.Shared.ValueObjects.DateTimeRange.Contains(System.DateTime instant) -> bool +MMCA.Common.Shared.ValueObjects.DateTimeRange.Deconstruct(out System.DateTime start, out System.DateTime end) -> void +MMCA.Common.Shared.ValueObjects.DateTimeRange.Duration.get -> System.TimeSpan +MMCA.Common.Shared.ValueObjects.DateTimeRange.End.get -> System.DateTime +MMCA.Common.Shared.ValueObjects.DateTimeRange.Equals(MMCA.Common.Shared.ValueObjects.DateTimeRange? other) -> bool +MMCA.Common.Shared.ValueObjects.DateTimeRange.Overlaps(MMCA.Common.Shared.ValueObjects.DateTimeRange! other) -> bool +MMCA.Common.Shared.ValueObjects.DateTimeRange.Start.get -> System.DateTime +MMCA.Common.Shared.ValueObjects.Email +MMCA.Common.Shared.ValueObjects.Email.Equals(MMCA.Common.Shared.ValueObjects.Email? other) -> bool +MMCA.Common.Shared.ValueObjects.Email.Value.get -> string! +MMCA.Common.Shared.ValueObjects.EmailInvariants +MMCA.Common.Shared.ValueObjects.Enumeration +MMCA.Common.Shared.ValueObjects.Enumeration.Enumeration(int value, string! name) -> void +MMCA.Common.Shared.ValueObjects.Enumeration.Name.get -> string! +MMCA.Common.Shared.ValueObjects.Enumeration.Value.get -> int +MMCA.Common.Shared.ValueObjects.EnumerationJsonConverterFactory +MMCA.Common.Shared.ValueObjects.EnumerationJsonConverterFactory.EnumerationJsonConverterFactory() -> void +MMCA.Common.Shared.ValueObjects.Money +MMCA.Common.Shared.ValueObjects.Money.Amount.get -> decimal +MMCA.Common.Shared.ValueObjects.Money.Amount.init -> void +MMCA.Common.Shared.ValueObjects.Money.Currency.get -> MMCA.Common.Shared.ValueObjects.Currency! +MMCA.Common.Shared.ValueObjects.Money.Currency.init -> void +MMCA.Common.Shared.ValueObjects.Money.Equals(MMCA.Common.Shared.ValueObjects.Money? other) -> bool +MMCA.Common.Shared.ValueObjects.Money.IsNegative.get -> bool +MMCA.Common.Shared.ValueObjects.Money.IsZero() -> bool +MMCA.Common.Shared.ValueObjects.PhoneNumber +MMCA.Common.Shared.ValueObjects.PhoneNumber.Equals(MMCA.Common.Shared.ValueObjects.PhoneNumber? other) -> bool +MMCA.Common.Shared.ValueObjects.PhoneNumber.Value.get -> string! +MMCA.Common.Shared.ValueObjects.PhoneNumberInvariants +MMCA.Common.Shared.ValueObjects.ValueObject +MMCA.Common.Shared.ValueObjects.ValueObject.ValueObject() -> void +MMCA.Common.Shared.ValueObjects.ValueObject.ValueObject(MMCA.Common.Shared.ValueObjects.ValueObject! original) -> void +abstract MMCA.Common.Shared.ValueObjects.ValueObject.$() -> MMCA.Common.Shared.ValueObjects.ValueObject! +const MMCA.Common.Shared.Abstractions.KeysetPageRequest.MaxPageSize = 1000 -> int +const MMCA.Common.Shared.Auth.AuthClaimTypes.Permission = "permission" -> string! +const MMCA.Common.Shared.Auth.RoleNames.Admin = "Admin" -> string! +const MMCA.Common.Shared.Auth.RoleNames.Attendee = "Attendee" -> string! +const MMCA.Common.Shared.Auth.RoleNames.ContentEditor = "ContentEditor" -> string! +const MMCA.Common.Shared.Auth.RoleNames.Customer = "Customer" -> string! +const MMCA.Common.Shared.Auth.RoleNames.Organizer = "Organizer" -> string! +const MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.DefaultWidth = 256 -> int +const MMCA.Common.Shared.Globalization.SupportedCultures.Default = "en-US" -> string! +const MMCA.Common.Shared.Globalization.SupportedCultures.PseudoLocale = "qps-Ploc" -> string! +const MMCA.Common.Shared.Http.IdempotencyHeaders.IdempotencyKey = "Idempotency-Key" -> string! +const MMCA.Common.Shared.Http.IdempotencyHeaders.IdempotentReplay = "X-Idempotent-Replay" -> string! +const MMCA.Common.Shared.Notifications.NotificationFeatures.PushNotifications = "Notification.PushNotifications" -> string! +const MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest.ApnsPlatform = "apns" -> string! +const MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest.FcmV1Platform = "fcmv1" -> string! +const MMCA.Common.Shared.Privacy.PrivacyFeatures.DataExport = "Privacy.DataExport" -> string! +override MMCA.Common.Shared.Abstractions.CollectionResult.Equals(object? obj) -> bool +override MMCA.Common.Shared.Abstractions.CollectionResult.GetHashCode() -> int +override MMCA.Common.Shared.Abstractions.CollectionResult.ToString() -> string! +override MMCA.Common.Shared.Abstractions.Error.Equals(object? obj) -> bool +override MMCA.Common.Shared.Abstractions.Error.GetHashCode() -> int +override MMCA.Common.Shared.Abstractions.Error.ToString() -> string! +override MMCA.Common.Shared.Abstractions.KeysetCollectionResult.$() -> MMCA.Common.Shared.Abstractions.KeysetCollectionResult! +override MMCA.Common.Shared.Abstractions.KeysetCollectionResult.Equals(object? obj) -> bool +override MMCA.Common.Shared.Abstractions.KeysetCollectionResult.GetHashCode() -> int +override MMCA.Common.Shared.Abstractions.KeysetCollectionResult.ToString() -> string! +override MMCA.Common.Shared.Abstractions.KeysetPageRequest.Equals(object? obj) -> bool +override MMCA.Common.Shared.Abstractions.KeysetPageRequest.GetHashCode() -> int +override MMCA.Common.Shared.Abstractions.KeysetPageRequest.ToString() -> string! +override MMCA.Common.Shared.Abstractions.PagedCollectionResult.$() -> MMCA.Common.Shared.Abstractions.PagedCollectionResult! +override MMCA.Common.Shared.Abstractions.PagedCollectionResult.Equals(object? obj) -> bool +override MMCA.Common.Shared.Abstractions.PagedCollectionResult.GetHashCode() -> int +override MMCA.Common.Shared.Abstractions.PagedCollectionResult.ToString() -> string! +override MMCA.Common.Shared.Abstractions.PaginationMetadata.Equals(object? obj) -> bool +override MMCA.Common.Shared.Abstractions.PaginationMetadata.GetHashCode() -> int +override MMCA.Common.Shared.Abstractions.PaginationMetadata.ToString() -> string! +override MMCA.Common.Shared.Auth.AuthenticationResponse.GetHashCode() -> int +override MMCA.Common.Shared.Auth.ChangePasswordRequest.GetHashCode() -> int +override MMCA.Common.Shared.Auth.ChangePreferencesRequest.Equals(object? obj) -> bool +override MMCA.Common.Shared.Auth.ChangePreferencesRequest.GetHashCode() -> int +override MMCA.Common.Shared.Auth.ChangePreferencesRequest.ToString() -> string! +override MMCA.Common.Shared.Auth.LoginRequest.GetHashCode() -> int +override MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest.GetHashCode() -> int +override MMCA.Common.Shared.Auth.RefreshTokenRequest.GetHashCode() -> int +override MMCA.Common.Shared.Auth.RegisterRequest.GetHashCode() -> int +override MMCA.Common.Shared.Auth.RoleValue.Equals(object? obj) -> bool +override MMCA.Common.Shared.Auth.RoleValue.GetHashCode() -> int +override MMCA.Common.Shared.Auth.RoleValue.ToString() -> string! +override MMCA.Common.Shared.Auth.UserPreferencesResponse.Equals(object? obj) -> bool +override MMCA.Common.Shared.Auth.UserPreferencesResponse.GetHashCode() -> int +override MMCA.Common.Shared.Auth.UserPreferencesResponse.ToString() -> string! +override MMCA.Common.Shared.AuthenticationRequest.GetHashCode() -> int +override MMCA.Common.Shared.Calendars.IcsEvent.Equals(object? obj) -> bool +override MMCA.Common.Shared.Calendars.IcsEvent.GetHashCode() -> int +override MMCA.Common.Shared.Calendars.IcsEvent.ToString() -> string! +override MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.Releaser.GetHashCode() -> int +override MMCA.Common.Shared.DTOs.BaseLookup.Equals(object? obj) -> bool +override MMCA.Common.Shared.DTOs.BaseLookup.GetHashCode() -> int +override MMCA.Common.Shared.DTOs.BaseLookup.ToString() -> string! +override MMCA.Common.Shared.DTOs.ConcurrencyTokenRequest.Equals(object? obj) -> bool +override MMCA.Common.Shared.DTOs.ConcurrencyTokenRequest.GetHashCode() -> int +override MMCA.Common.Shared.DTOs.ConcurrencyTokenRequest.ToString() -> string! +override MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest.Equals(object? obj) -> bool +override MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest.GetHashCode() -> int +override MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest.ToString() -> string! +override MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.Equals(object? obj) -> bool +override MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.GetHashCode() -> int +override MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.ToString() -> string! +override MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest.Equals(object? obj) -> bool +override MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest.GetHashCode() -> int +override MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest.ToString() -> string! +override MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.Equals(object? obj) -> bool +override MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.GetHashCode() -> int +override MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.ToString() -> string! +override MMCA.Common.Shared.Privacy.UserDataExportDTO.Equals(object? obj) -> bool +override MMCA.Common.Shared.Privacy.UserDataExportDTO.GetHashCode() -> int +override MMCA.Common.Shared.Privacy.UserDataExportDTO.ToString() -> string! +override MMCA.Common.Shared.Privacy.UserDataExportSectionDTO.Equals(object? obj) -> bool +override MMCA.Common.Shared.Privacy.UserDataExportSectionDTO.GetHashCode() -> int +override MMCA.Common.Shared.Privacy.UserDataExportSectionDTO.ToString() -> string! +override MMCA.Common.Shared.Serialization.ResultJsonConverterFactory.CanConvert(System.Type! typeToConvert) -> bool +override MMCA.Common.Shared.Serialization.ResultJsonConverterFactory.CreateConverter(System.Type! typeToConvert, System.Text.Json.JsonSerializerOptions! options) -> System.Text.Json.Serialization.JsonConverter? +override MMCA.Common.Shared.ValueObjects.Address.$() -> MMCA.Common.Shared.ValueObjects.Address! +override MMCA.Common.Shared.ValueObjects.Address.Equals(object? obj) -> bool +override MMCA.Common.Shared.ValueObjects.Address.GetHashCode() -> int +override MMCA.Common.Shared.ValueObjects.Address.ToString() -> string! +override MMCA.Common.Shared.ValueObjects.Currency.$() -> MMCA.Common.Shared.ValueObjects.Currency! +override MMCA.Common.Shared.ValueObjects.Currency.Equals(object? obj) -> bool +override MMCA.Common.Shared.ValueObjects.Currency.GetHashCode() -> int +override MMCA.Common.Shared.ValueObjects.Currency.ToString() -> string! +override MMCA.Common.Shared.ValueObjects.CurrencyJsonConverter.Read(ref System.Text.Json.Utf8JsonReader reader, System.Type! typeToConvert, System.Text.Json.JsonSerializerOptions! options) -> MMCA.Common.Shared.ValueObjects.Currency? +override MMCA.Common.Shared.ValueObjects.CurrencyJsonConverter.Write(System.Text.Json.Utf8JsonWriter! writer, MMCA.Common.Shared.ValueObjects.Currency! value, System.Text.Json.JsonSerializerOptions! options) -> void +override MMCA.Common.Shared.ValueObjects.DateRange.$() -> MMCA.Common.Shared.ValueObjects.DateRange! +override MMCA.Common.Shared.ValueObjects.DateRange.Equals(object? obj) -> bool +override MMCA.Common.Shared.ValueObjects.DateRange.GetHashCode() -> int +override MMCA.Common.Shared.ValueObjects.DateRange.ToString() -> string! +override MMCA.Common.Shared.ValueObjects.DateTimeRange.$() -> MMCA.Common.Shared.ValueObjects.DateTimeRange! +override MMCA.Common.Shared.ValueObjects.DateTimeRange.Equals(object? obj) -> bool +override MMCA.Common.Shared.ValueObjects.DateTimeRange.GetHashCode() -> int +override MMCA.Common.Shared.ValueObjects.DateTimeRange.ToString() -> string! +override MMCA.Common.Shared.ValueObjects.Email.$() -> MMCA.Common.Shared.ValueObjects.Email! +override MMCA.Common.Shared.ValueObjects.Email.Equals(object? obj) -> bool +override MMCA.Common.Shared.ValueObjects.Email.GetHashCode() -> int +override MMCA.Common.Shared.ValueObjects.Email.ToString() -> string! +override MMCA.Common.Shared.ValueObjects.Enumeration.Equals(object? obj) -> bool +override MMCA.Common.Shared.ValueObjects.Enumeration.GetHashCode() -> int +override MMCA.Common.Shared.ValueObjects.Enumeration.ToString() -> string! +override MMCA.Common.Shared.ValueObjects.EnumerationJsonConverterFactory.CanConvert(System.Type! typeToConvert) -> bool +override MMCA.Common.Shared.ValueObjects.EnumerationJsonConverterFactory.CreateConverter(System.Type! typeToConvert, System.Text.Json.JsonSerializerOptions! options) -> System.Text.Json.Serialization.JsonConverter? +override MMCA.Common.Shared.ValueObjects.Money.$() -> MMCA.Common.Shared.ValueObjects.Money! +override MMCA.Common.Shared.ValueObjects.Money.Equals(object? obj) -> bool +override MMCA.Common.Shared.ValueObjects.Money.GetHashCode() -> int +override MMCA.Common.Shared.ValueObjects.Money.ToString() -> string! +override MMCA.Common.Shared.ValueObjects.PhoneNumber.$() -> MMCA.Common.Shared.ValueObjects.PhoneNumber! +override MMCA.Common.Shared.ValueObjects.PhoneNumber.Equals(object? obj) -> bool +override MMCA.Common.Shared.ValueObjects.PhoneNumber.GetHashCode() -> int +override MMCA.Common.Shared.ValueObjects.PhoneNumber.ToString() -> string! +override MMCA.Common.Shared.ValueObjects.ValueObject.Equals(object? obj) -> bool +override MMCA.Common.Shared.ValueObjects.ValueObject.GetHashCode() -> int +override MMCA.Common.Shared.ValueObjects.ValueObject.ToString() -> string! +override sealed MMCA.Common.Shared.Abstractions.KeysetCollectionResult.Equals(MMCA.Common.Shared.Abstractions.CollectionResult? other) -> bool +override sealed MMCA.Common.Shared.Abstractions.PagedCollectionResult.Equals(MMCA.Common.Shared.Abstractions.CollectionResult? other) -> bool +override sealed MMCA.Common.Shared.ValueObjects.Address.Equals(MMCA.Common.Shared.ValueObjects.ValueObject? other) -> bool +override sealed MMCA.Common.Shared.ValueObjects.Currency.Equals(MMCA.Common.Shared.ValueObjects.ValueObject? other) -> bool +override sealed MMCA.Common.Shared.ValueObjects.DateRange.Equals(MMCA.Common.Shared.ValueObjects.ValueObject? other) -> bool +override sealed MMCA.Common.Shared.ValueObjects.DateTimeRange.Equals(MMCA.Common.Shared.ValueObjects.ValueObject? other) -> bool +override sealed MMCA.Common.Shared.ValueObjects.Email.Equals(MMCA.Common.Shared.ValueObjects.ValueObject? other) -> bool +override sealed MMCA.Common.Shared.ValueObjects.Money.Equals(MMCA.Common.Shared.ValueObjects.ValueObject? other) -> bool +override sealed MMCA.Common.Shared.ValueObjects.PhoneNumber.Equals(MMCA.Common.Shared.ValueObjects.ValueObject? other) -> bool +static MMCA.Common.Shared.Abstractions.CollectionResult.operator !=(MMCA.Common.Shared.Abstractions.CollectionResult? left, MMCA.Common.Shared.Abstractions.CollectionResult? right) -> bool +static MMCA.Common.Shared.Abstractions.CollectionResult.operator ==(MMCA.Common.Shared.Abstractions.CollectionResult? left, MMCA.Common.Shared.Abstractions.CollectionResult? right) -> bool +static MMCA.Common.Shared.Abstractions.Error.Conflict(string! code, string! message, string? source = null, string? target = null) -> MMCA.Common.Shared.Abstractions.Error! +static MMCA.Common.Shared.Abstractions.Error.Failure(string! code, string! message, string? source = null, string? target = null) -> MMCA.Common.Shared.Abstractions.Error! +static MMCA.Common.Shared.Abstractions.Error.Forbidden(string! code, string! message, string? source = null, string? target = null) -> MMCA.Common.Shared.Abstractions.Error! +static MMCA.Common.Shared.Abstractions.Error.Invariant(string! code, string! message, string? source = null, string? target = null) -> MMCA.Common.Shared.Abstractions.Error! +static MMCA.Common.Shared.Abstractions.Error.NotFoundError(string! code, string! message, string? source = null, string? target = null) -> MMCA.Common.Shared.Abstractions.Error! +static MMCA.Common.Shared.Abstractions.Error.Unauthorized(string! code, string! message, string? source = null, string? target = null) -> MMCA.Common.Shared.Abstractions.Error! +static MMCA.Common.Shared.Abstractions.Error.UnprocessableEntity(string! code, string! message, string? source = null, string? target = null) -> MMCA.Common.Shared.Abstractions.Error! +static MMCA.Common.Shared.Abstractions.Error.Validation(string! code, string! message, string? source = null, string? target = null) -> MMCA.Common.Shared.Abstractions.Error! +static MMCA.Common.Shared.Abstractions.Error.operator !=(MMCA.Common.Shared.Abstractions.Error? left, MMCA.Common.Shared.Abstractions.Error? right) -> bool +static MMCA.Common.Shared.Abstractions.Error.operator ==(MMCA.Common.Shared.Abstractions.Error? left, MMCA.Common.Shared.Abstractions.Error? right) -> bool +static MMCA.Common.Shared.Abstractions.KeysetCollectionResult.operator !=(MMCA.Common.Shared.Abstractions.KeysetCollectionResult? left, MMCA.Common.Shared.Abstractions.KeysetCollectionResult? right) -> bool +static MMCA.Common.Shared.Abstractions.KeysetCollectionResult.operator ==(MMCA.Common.Shared.Abstractions.KeysetCollectionResult? left, MMCA.Common.Shared.Abstractions.KeysetCollectionResult? right) -> bool +static MMCA.Common.Shared.Abstractions.KeysetCursor.Encode(string? sortValue, string! id) -> string! +static MMCA.Common.Shared.Abstractions.KeysetCursor.TryDecode(string? cursor, out string? sortValue, out string! id) -> bool +static MMCA.Common.Shared.Abstractions.KeysetPageRequest.operator !=(MMCA.Common.Shared.Abstractions.KeysetPageRequest? left, MMCA.Common.Shared.Abstractions.KeysetPageRequest? right) -> bool +static MMCA.Common.Shared.Abstractions.KeysetPageRequest.operator ==(MMCA.Common.Shared.Abstractions.KeysetPageRequest? left, MMCA.Common.Shared.Abstractions.KeysetPageRequest? right) -> bool +static MMCA.Common.Shared.Abstractions.PagedCollectionResult.operator !=(MMCA.Common.Shared.Abstractions.PagedCollectionResult? left, MMCA.Common.Shared.Abstractions.PagedCollectionResult? right) -> bool +static MMCA.Common.Shared.Abstractions.PagedCollectionResult.operator ==(MMCA.Common.Shared.Abstractions.PagedCollectionResult? left, MMCA.Common.Shared.Abstractions.PagedCollectionResult? right) -> bool +static MMCA.Common.Shared.Abstractions.PaginationMetadata.operator !=(MMCA.Common.Shared.Abstractions.PaginationMetadata? left, MMCA.Common.Shared.Abstractions.PaginationMetadata? right) -> bool +static MMCA.Common.Shared.Abstractions.PaginationMetadata.operator ==(MMCA.Common.Shared.Abstractions.PaginationMetadata? left, MMCA.Common.Shared.Abstractions.PaginationMetadata? right) -> bool +static MMCA.Common.Shared.Abstractions.Result.Combine(params System.ReadOnlySpan results) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.Abstractions.Result.Failure(MMCA.Common.Shared.Abstractions.Error! error) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.Abstractions.Result.Failure(System.Collections.Generic.IEnumerable! errors) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.Abstractions.Result.Failure(MMCA.Common.Shared.Abstractions.Error! error) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.Abstractions.Result.Failure(System.Collections.Generic.IEnumerable! errors) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.Abstractions.Result.Success() -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.Abstractions.Result.Success(T value) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.Auth.AuthenticationResponse.operator !=(MMCA.Common.Shared.Auth.AuthenticationResponse left, MMCA.Common.Shared.Auth.AuthenticationResponse right) -> bool +static MMCA.Common.Shared.Auth.AuthenticationResponse.operator ==(MMCA.Common.Shared.Auth.AuthenticationResponse left, MMCA.Common.Shared.Auth.AuthenticationResponse right) -> bool +static MMCA.Common.Shared.Auth.ChangePasswordRequest.operator !=(MMCA.Common.Shared.Auth.ChangePasswordRequest left, MMCA.Common.Shared.Auth.ChangePasswordRequest right) -> bool +static MMCA.Common.Shared.Auth.ChangePasswordRequest.operator ==(MMCA.Common.Shared.Auth.ChangePasswordRequest left, MMCA.Common.Shared.Auth.ChangePasswordRequest right) -> bool +static MMCA.Common.Shared.Auth.ChangePreferencesRequest.operator !=(MMCA.Common.Shared.Auth.ChangePreferencesRequest? left, MMCA.Common.Shared.Auth.ChangePreferencesRequest? right) -> bool +static MMCA.Common.Shared.Auth.ChangePreferencesRequest.operator ==(MMCA.Common.Shared.Auth.ChangePreferencesRequest? left, MMCA.Common.Shared.Auth.ChangePreferencesRequest? right) -> bool +static MMCA.Common.Shared.Auth.LoginRequest.operator !=(MMCA.Common.Shared.Auth.LoginRequest left, MMCA.Common.Shared.Auth.LoginRequest right) -> bool +static MMCA.Common.Shared.Auth.LoginRequest.operator ==(MMCA.Common.Shared.Auth.LoginRequest left, MMCA.Common.Shared.Auth.LoginRequest right) -> bool +static MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest.operator !=(MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest left, MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest right) -> bool +static MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest.operator ==(MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest left, MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest right) -> bool +static MMCA.Common.Shared.Auth.RefreshTokenRequest.operator !=(MMCA.Common.Shared.Auth.RefreshTokenRequest left, MMCA.Common.Shared.Auth.RefreshTokenRequest right) -> bool +static MMCA.Common.Shared.Auth.RefreshTokenRequest.operator ==(MMCA.Common.Shared.Auth.RefreshTokenRequest left, MMCA.Common.Shared.Auth.RefreshTokenRequest right) -> bool +static MMCA.Common.Shared.Auth.RegisterRequest.operator !=(MMCA.Common.Shared.Auth.RegisterRequest left, MMCA.Common.Shared.Auth.RegisterRequest right) -> bool +static MMCA.Common.Shared.Auth.RegisterRequest.operator ==(MMCA.Common.Shared.Auth.RegisterRequest left, MMCA.Common.Shared.Auth.RegisterRequest right) -> bool +static MMCA.Common.Shared.Auth.RoleValue.BuildLookup(params TRole![]! roles) -> System.Collections.Frozen.FrozenDictionary! +static MMCA.Common.Shared.Auth.RoleValue.Validate(string! role, System.Collections.Generic.IReadOnlySet! knownRoles, string! source) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.Auth.UserPreferencesResponse.operator !=(MMCA.Common.Shared.Auth.UserPreferencesResponse? left, MMCA.Common.Shared.Auth.UserPreferencesResponse? right) -> bool +static MMCA.Common.Shared.Auth.UserPreferencesResponse.operator ==(MMCA.Common.Shared.Auth.UserPreferencesResponse? left, MMCA.Common.Shared.Auth.UserPreferencesResponse? right) -> bool +static MMCA.Common.Shared.AuthenticationRequest.operator !=(MMCA.Common.Shared.AuthenticationRequest left, MMCA.Common.Shared.AuthenticationRequest right) -> bool +static MMCA.Common.Shared.AuthenticationRequest.operator ==(MMCA.Common.Shared.AuthenticationRequest left, MMCA.Common.Shared.AuthenticationRequest right) -> bool +static MMCA.Common.Shared.Calendars.IcsCalendarBuilder.Build(string! productId, System.Collections.Generic.IReadOnlyCollection! events, System.DateTimeOffset dtStamp) -> string! +static MMCA.Common.Shared.Calendars.IcsEvent.operator !=(MMCA.Common.Shared.Calendars.IcsEvent? left, MMCA.Common.Shared.Calendars.IcsEvent? right) -> bool +static MMCA.Common.Shared.Calendars.IcsEvent.operator ==(MMCA.Common.Shared.Calendars.IcsEvent? left, MMCA.Common.Shared.Calendars.IcsEvent? right) -> bool +static MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.Releaser.operator !=(MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.Releaser left, MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.Releaser right) -> bool +static MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.Releaser.operator ==(MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.Releaser left, MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.Releaser right) -> bool +static MMCA.Common.Shared.DTOs.BaseLookup.operator !=(MMCA.Common.Shared.DTOs.BaseLookup? left, MMCA.Common.Shared.DTOs.BaseLookup? right) -> bool +static MMCA.Common.Shared.DTOs.BaseLookup.operator ==(MMCA.Common.Shared.DTOs.BaseLookup? left, MMCA.Common.Shared.DTOs.BaseLookup? right) -> bool +static MMCA.Common.Shared.DTOs.ConcurrencyTokenRequest.operator !=(MMCA.Common.Shared.DTOs.ConcurrencyTokenRequest? left, MMCA.Common.Shared.DTOs.ConcurrencyTokenRequest? right) -> bool +static MMCA.Common.Shared.DTOs.ConcurrencyTokenRequest.operator ==(MMCA.Common.Shared.DTOs.ConcurrencyTokenRequest? left, MMCA.Common.Shared.DTOs.ConcurrencyTokenRequest? right) -> bool +static MMCA.Common.Shared.Extensions.DomainHelper.Parse(this string? id) -> TIdentifier +static MMCA.Common.Shared.Extensions.DomainHelper.TryParse(this string? id, out TIdentifier result) -> bool +static MMCA.Common.Shared.Globalization.SupportedCultures.All.get -> System.Collections.Generic.IReadOnlyList! +static MMCA.Common.Shared.Globalization.SupportedCultures.IsPseudoLocale(string? culture) -> bool +static MMCA.Common.Shared.Globalization.SupportedCultures.IsSupported(string? culture) -> bool +static MMCA.Common.Shared.Globalization.SupportedCultures.ResolveClosest(string? culture) -> string! +static MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest.operator !=(MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest? left, MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest? right) -> bool +static MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest.operator ==(MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest? left, MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest? right) -> bool +static MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.operator !=(MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO? left, MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO? right) -> bool +static MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.operator ==(MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO? left, MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO? right) -> bool +static MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest.operator !=(MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest? left, MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest? right) -> bool +static MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest.operator ==(MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest? left, MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest? right) -> bool +static MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.operator !=(MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO? left, MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO? right) -> bool +static MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO.operator ==(MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO? left, MMCA.Common.Shared.Notifications.UserNotifications.UserNotificationDTO? right) -> bool +static MMCA.Common.Shared.Privacy.UserDataExportDTO.operator !=(MMCA.Common.Shared.Privacy.UserDataExportDTO? left, MMCA.Common.Shared.Privacy.UserDataExportDTO? right) -> bool +static MMCA.Common.Shared.Privacy.UserDataExportDTO.operator ==(MMCA.Common.Shared.Privacy.UserDataExportDTO? left, MMCA.Common.Shared.Privacy.UserDataExportDTO? right) -> bool +static MMCA.Common.Shared.Privacy.UserDataExportSectionDTO.operator !=(MMCA.Common.Shared.Privacy.UserDataExportSectionDTO? left, MMCA.Common.Shared.Privacy.UserDataExportSectionDTO? right) -> bool +static MMCA.Common.Shared.Privacy.UserDataExportSectionDTO.operator ==(MMCA.Common.Shared.Privacy.UserDataExportSectionDTO? left, MMCA.Common.Shared.Privacy.UserDataExportSectionDTO? right) -> bool +static MMCA.Common.Shared.Resilience.BrokerResilienceDefaults.BreakDuration.get -> System.TimeSpan +static MMCA.Common.Shared.Resilience.BrokerResilienceDefaults.FailureRatio.get -> double +static MMCA.Common.Shared.Resilience.BrokerResilienceDefaults.MinimumThroughput.get -> int +static MMCA.Common.Shared.Resilience.BrokerResilienceDefaults.SamplingDuration.get -> System.TimeSpan +static MMCA.Common.Shared.Resilience.HttpResilienceDefaults.AttemptTimeout.get -> System.TimeSpan +static MMCA.Common.Shared.Resilience.HttpResilienceDefaults.CircuitBreakerSamplingDuration.get -> System.TimeSpan +static MMCA.Common.Shared.Resilience.HttpResilienceDefaults.KeepAlivePingDelay.get -> System.TimeSpan +static MMCA.Common.Shared.Resilience.HttpResilienceDefaults.KeepAlivePingTimeout.get -> System.TimeSpan +static MMCA.Common.Shared.Resilience.HttpResilienceDefaults.MaxRetryAttempts.get -> int +static MMCA.Common.Shared.Resilience.HttpResilienceDefaults.PooledConnectionIdleTimeout.get -> System.TimeSpan +static MMCA.Common.Shared.Resilience.HttpResilienceDefaults.PooledConnectionLifetime.get -> System.TimeSpan +static MMCA.Common.Shared.Resilience.HttpResilienceDefaults.TotalRequestTimeout.get -> System.TimeSpan +static MMCA.Common.Shared.ValueObjects.Address.Create(string! addressLine1, string? addressLine2, string? city, string? state, string? zipCode, string? country) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.ValueObjects.Address.operator !=(MMCA.Common.Shared.ValueObjects.Address? left, MMCA.Common.Shared.ValueObjects.Address? right) -> bool +static MMCA.Common.Shared.ValueObjects.Address.operator ==(MMCA.Common.Shared.ValueObjects.Address? left, MMCA.Common.Shared.ValueObjects.Address? right) -> bool +static MMCA.Common.Shared.ValueObjects.AddressInvariants.EnsureAddressIsValid(MMCA.Common.Shared.ValueObjects.Address? address, string! source) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.ValueObjects.AddressInvariants.EnsureAddressLine1IsValid(string! addressLine1, string! source) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.ValueObjects.Currency.FromCode(string! code) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.ValueObjects.Currency.operator !=(MMCA.Common.Shared.ValueObjects.Currency? left, MMCA.Common.Shared.ValueObjects.Currency? right) -> bool +static MMCA.Common.Shared.ValueObjects.Currency.operator ==(MMCA.Common.Shared.ValueObjects.Currency? left, MMCA.Common.Shared.ValueObjects.Currency? right) -> bool +static MMCA.Common.Shared.ValueObjects.DateRange.Create(System.DateOnly start, System.DateOnly end) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.ValueObjects.DateRange.operator !=(MMCA.Common.Shared.ValueObjects.DateRange? left, MMCA.Common.Shared.ValueObjects.DateRange? right) -> bool +static MMCA.Common.Shared.ValueObjects.DateRange.operator ==(MMCA.Common.Shared.ValueObjects.DateRange? left, MMCA.Common.Shared.ValueObjects.DateRange? right) -> bool +static MMCA.Common.Shared.ValueObjects.DateTimeRange.Create(System.DateTime start, System.DateTime end) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.ValueObjects.DateTimeRange.operator !=(MMCA.Common.Shared.ValueObjects.DateTimeRange? left, MMCA.Common.Shared.ValueObjects.DateTimeRange? right) -> bool +static MMCA.Common.Shared.ValueObjects.DateTimeRange.operator ==(MMCA.Common.Shared.ValueObjects.DateTimeRange? left, MMCA.Common.Shared.ValueObjects.DateTimeRange? right) -> bool +static MMCA.Common.Shared.ValueObjects.Email.Create(string! value) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.ValueObjects.Email.implicit operator string!(MMCA.Common.Shared.ValueObjects.Email! email) -> string! +static MMCA.Common.Shared.ValueObjects.Email.operator !=(MMCA.Common.Shared.ValueObjects.Email? left, MMCA.Common.Shared.ValueObjects.Email? right) -> bool +static MMCA.Common.Shared.ValueObjects.Email.operator ==(MMCA.Common.Shared.ValueObjects.Email? left, MMCA.Common.Shared.ValueObjects.Email? right) -> bool +static MMCA.Common.Shared.ValueObjects.EmailInvariants.EnsureEmailIsValid(string! email, string! source) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.ValueObjects.Enumeration.All.get -> System.Collections.Generic.IReadOnlyCollection! +static MMCA.Common.Shared.ValueObjects.Enumeration.FromName(string! name) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.ValueObjects.Enumeration.FromValue(int value) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.ValueObjects.Money.Add(MMCA.Common.Shared.ValueObjects.Money! first, MMCA.Common.Shared.ValueObjects.Money! second) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.ValueObjects.Money.Create(decimal amount, MMCA.Common.Shared.ValueObjects.Currency! currency) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.ValueObjects.Money.Multiply(MMCA.Common.Shared.ValueObjects.Money! first, int quantity) -> MMCA.Common.Shared.ValueObjects.Money! +static MMCA.Common.Shared.ValueObjects.Money.Zero() -> MMCA.Common.Shared.ValueObjects.Money! +static MMCA.Common.Shared.ValueObjects.Money.Zero(MMCA.Common.Shared.ValueObjects.Currency! currency) -> MMCA.Common.Shared.ValueObjects.Money! +static MMCA.Common.Shared.ValueObjects.Money.operator !=(MMCA.Common.Shared.ValueObjects.Money? left, MMCA.Common.Shared.ValueObjects.Money? right) -> bool +static MMCA.Common.Shared.ValueObjects.Money.operator *(MMCA.Common.Shared.ValueObjects.Money! first, int quantity) -> MMCA.Common.Shared.ValueObjects.Money! +static MMCA.Common.Shared.ValueObjects.Money.operator +(MMCA.Common.Shared.ValueObjects.Money! first, MMCA.Common.Shared.ValueObjects.Money! second) -> MMCA.Common.Shared.ValueObjects.Money! +static MMCA.Common.Shared.ValueObjects.Money.operator ==(MMCA.Common.Shared.ValueObjects.Money? left, MMCA.Common.Shared.ValueObjects.Money? right) -> bool +static MMCA.Common.Shared.ValueObjects.PhoneNumber.Create(string! value) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.ValueObjects.PhoneNumber.implicit operator string!(MMCA.Common.Shared.ValueObjects.PhoneNumber! phoneNumber) -> string! +static MMCA.Common.Shared.ValueObjects.PhoneNumber.operator !=(MMCA.Common.Shared.ValueObjects.PhoneNumber? left, MMCA.Common.Shared.ValueObjects.PhoneNumber? right) -> bool +static MMCA.Common.Shared.ValueObjects.PhoneNumber.operator ==(MMCA.Common.Shared.ValueObjects.PhoneNumber? left, MMCA.Common.Shared.ValueObjects.PhoneNumber? right) -> bool +static MMCA.Common.Shared.ValueObjects.PhoneNumberInvariants.EnsurePhoneNumberIsValid(string! phoneNumber, string! source) -> MMCA.Common.Shared.Abstractions.Result! +static MMCA.Common.Shared.ValueObjects.ValueObject.operator !=(MMCA.Common.Shared.ValueObjects.ValueObject? left, MMCA.Common.Shared.ValueObjects.ValueObject? right) -> bool +static MMCA.Common.Shared.ValueObjects.ValueObject.operator ==(MMCA.Common.Shared.ValueObjects.ValueObject? left, MMCA.Common.Shared.ValueObjects.ValueObject? right) -> bool +static readonly MMCA.Common.Shared.Abstractions.Error.AlreadyDeleted -> MMCA.Common.Shared.Abstractions.Error! +static readonly MMCA.Common.Shared.Abstractions.Error.InvalidEntityField -> MMCA.Common.Shared.Abstractions.Error! +static readonly MMCA.Common.Shared.Abstractions.Error.NotFound -> MMCA.Common.Shared.Abstractions.Error! +static readonly MMCA.Common.Shared.ValueObjects.AddressInvariants.AddressLine1MaxLength -> int +static readonly MMCA.Common.Shared.ValueObjects.AddressInvariants.AddressLine2MaxLength -> int +static readonly MMCA.Common.Shared.ValueObjects.AddressInvariants.CityMaxLength -> int +static readonly MMCA.Common.Shared.ValueObjects.AddressInvariants.CountryMaxLength -> int +static readonly MMCA.Common.Shared.ValueObjects.AddressInvariants.StateMaxLength -> int +static readonly MMCA.Common.Shared.ValueObjects.AddressInvariants.ZipCodeMaxLength -> int +static readonly MMCA.Common.Shared.ValueObjects.Currency.All -> System.Collections.Generic.IReadOnlyCollection! +static readonly MMCA.Common.Shared.ValueObjects.Currency.EmptyCurrency -> MMCA.Common.Shared.Abstractions.Error! +static readonly MMCA.Common.Shared.ValueObjects.Currency.Eur -> MMCA.Common.Shared.ValueObjects.Currency! +static readonly MMCA.Common.Shared.ValueObjects.Currency.InvalidCurrency -> MMCA.Common.Shared.Abstractions.Error! +static readonly MMCA.Common.Shared.ValueObjects.Currency.Usd -> MMCA.Common.Shared.ValueObjects.Currency! +static readonly MMCA.Common.Shared.ValueObjects.EmailInvariants.MaxLength -> int +static readonly MMCA.Common.Shared.ValueObjects.Money.CurrencyMismatch -> MMCA.Common.Shared.Abstractions.Error! +static readonly MMCA.Common.Shared.ValueObjects.Money.NoCurrency -> MMCA.Common.Shared.Abstractions.Error! +static readonly MMCA.Common.Shared.ValueObjects.PhoneNumberInvariants.MaxLength -> int +static readonly MMCA.Common.Shared.ValueObjects.PhoneNumberInvariants.MinLength -> int +virtual MMCA.Common.Shared.Abstractions.CollectionResult.$() -> MMCA.Common.Shared.Abstractions.CollectionResult! +virtual MMCA.Common.Shared.Abstractions.CollectionResult.EqualityContract.get -> System.Type! +virtual MMCA.Common.Shared.Abstractions.CollectionResult.Equals(MMCA.Common.Shared.Abstractions.CollectionResult? other) -> bool +virtual MMCA.Common.Shared.Abstractions.CollectionResult.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual MMCA.Common.Shared.Abstractions.Error.$() -> MMCA.Common.Shared.Abstractions.Error! +virtual MMCA.Common.Shared.Abstractions.Error.EqualityContract.get -> System.Type! +virtual MMCA.Common.Shared.Abstractions.Error.Equals(MMCA.Common.Shared.Abstractions.Error? other) -> bool +virtual MMCA.Common.Shared.Abstractions.Error.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual MMCA.Common.Shared.DTOs.BaseLookup.$() -> MMCA.Common.Shared.DTOs.BaseLookup! +virtual MMCA.Common.Shared.DTOs.BaseLookup.EqualityContract.get -> System.Type! +virtual MMCA.Common.Shared.DTOs.BaseLookup.Equals(MMCA.Common.Shared.DTOs.BaseLookup? other) -> bool +virtual MMCA.Common.Shared.DTOs.BaseLookup.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.$() -> MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO! +virtual MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.EqualityContract.get -> System.Type! +virtual MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.Equals(MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO? other) -> bool +virtual MMCA.Common.Shared.Notifications.PushNotifications.PushNotificationDTO.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual MMCA.Common.Shared.ValueObjects.ValueObject.EqualityContract.get -> System.Type! +virtual MMCA.Common.Shared.ValueObjects.ValueObject.Equals(MMCA.Common.Shared.ValueObjects.ValueObject? other) -> bool +virtual MMCA.Common.Shared.ValueObjects.ValueObject.PrintMembers(System.Text.StringBuilder! builder) -> bool +~override MMCA.Common.Shared.Auth.AuthenticationResponse.Equals(object obj) -> bool +~override MMCA.Common.Shared.Auth.AuthenticationResponse.ToString() -> string +~override MMCA.Common.Shared.Auth.ChangePasswordRequest.Equals(object obj) -> bool +~override MMCA.Common.Shared.Auth.ChangePasswordRequest.ToString() -> string +~override MMCA.Common.Shared.Auth.LoginRequest.Equals(object obj) -> bool +~override MMCA.Common.Shared.Auth.LoginRequest.ToString() -> string +~override MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest.Equals(object obj) -> bool +~override MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest.ToString() -> string +~override MMCA.Common.Shared.Auth.RefreshTokenRequest.Equals(object obj) -> bool +~override MMCA.Common.Shared.Auth.RefreshTokenRequest.ToString() -> string +~override MMCA.Common.Shared.Auth.RegisterRequest.Equals(object obj) -> bool +~override MMCA.Common.Shared.Auth.RegisterRequest.ToString() -> string +~override MMCA.Common.Shared.AuthenticationRequest.Equals(object obj) -> bool +~override MMCA.Common.Shared.AuthenticationRequest.ToString() -> string +~override MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.Releaser.Equals(object obj) -> bool +~override MMCA.Common.Shared.Concurrency.KeyedSemaphoreStripe.Releaser.ToString() -> string diff --git a/Source/Core/MMCA.Common.Shared/PublicAPI.Unshipped.txt b/Source/Core/MMCA.Common.Shared/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/Source/Core/MMCA.Common.Shared/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/Source/Core/MMCA.Common.Shared/packages.lock.json b/Source/Core/MMCA.Common.Shared/packages.lock.json index e20d1850..2f083524 100644 --- a/Source/Core/MMCA.Common.Shared/packages.lock.json +++ b/Source/Core/MMCA.Common.Shared/packages.lock.json @@ -8,6 +8,12 @@ "resolved": "3.0.163", "contentHash": "l9iZ+OXBGlAFEKH1y4BSKPXHcmpJmikysSm0brjTau8fR7Qm7IVGLVig+dh0RH9mq4s3neZayuPQrHRweOpxxw==" }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[5.6.0, )", + "resolved": "5.6.0", + "contentHash": "W4kJGezNIKLzo0Ak5FAQDFvkMf2U7DtGL4THmHyRSApfKsKt5V+eX/bU0ZLKAt/uf9Bb2o1bi0YDKj/GRB/vYQ==" + }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", "requested": "[18.7.23, )", diff --git a/Source/Hosting/MMCA.Common.Aspire.Hosting/PublicAPI.Shipped.txt b/Source/Hosting/MMCA.Common.Aspire.Hosting/PublicAPI.Shipped.txt new file mode 100644 index 00000000..ccdd959e --- /dev/null +++ b/Source/Hosting/MMCA.Common.Aspire.Hosting/PublicAPI.Shipped.txt @@ -0,0 +1,23 @@ +#nullable enable +MMCA.Common.Aspire.Hosting.Extensions +MMCA.Common.Aspire.Hosting.Extensions.extension(Aspire.Hosting.ApplicationModel.IResourceBuilder!) +MMCA.Common.Aspire.Hosting.Extensions.extension(Aspire.Hosting.ApplicationModel.IResourceBuilder!).WithCosmosDataSource(Aspire.Hosting.ApplicationModel.IResourceBuilder! database, string! logicalName) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +MMCA.Common.Aspire.Hosting.Extensions.extension(Aspire.Hosting.ApplicationModel.IResourceBuilder!).WithE2eRegistrationThrottleLift(bool alsoLiftWhen = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +MMCA.Common.Aspire.Hosting.Extensions.extension(Aspire.Hosting.ApplicationModel.IResourceBuilder!).WithE2eRsaKeys() -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +MMCA.Common.Aspire.Hosting.Extensions.extension(Aspire.Hosting.ApplicationModel.IResourceBuilder!).WithSQLServerDataSource(Aspire.Hosting.ApplicationModel.IResourceBuilder! database, string! logicalName) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +MMCA.Common.Aspire.Hosting.Extensions.extension(Aspire.Hosting.ApplicationModel.IResourceBuilder!).WithSqliteDataSource(string! logicalName, string! filePath) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +MMCA.Common.Aspire.Hosting.Extensions.extension(Aspire.Hosting.IDistributedApplicationBuilder!) +MMCA.Common.Aspire.Hosting.Extensions.extension(Aspire.Hosting.IDistributedApplicationBuilder!).AddMessageBroker(string! name = "rabbitmq") -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +MMCA.Common.Aspire.Hosting.Extensions.extension(Aspire.Hosting.ApplicationModel.IResourceBuilder!) +MMCA.Common.Aspire.Hosting.Extensions.extension(Aspire.Hosting.ApplicationModel.IResourceBuilder!).WithBroker(Aspire.Hosting.ApplicationModel.IResourceBuilder! broker) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +MMCA.Common.Aspire.Hosting.Extensions.extension(Aspire.Hosting.ApplicationModel.IResourceBuilder!).WithJwksDiscovery(Aspire.Hosting.ApplicationModel.IResourceBuilder! identity, Aspire.Hosting.ApplicationModel.IResourceBuilder? gateway = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +const MMCA.Common.Aspire.Hosting.Extensions.DefaultBrokerResourceName = "rabbitmq" -> string! +const MMCA.Common.Aspire.Hosting.Extensions.E2eRegistrationsPerIpPerHour = 1000 -> int +static MMCA.Common.Aspire.Hosting.Extensions.AddMessageBroker(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name = "rabbitmq") -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static MMCA.Common.Aspire.Hosting.Extensions.WithBroker(this Aspire.Hosting.ApplicationModel.IResourceBuilder! service, Aspire.Hosting.ApplicationModel.IResourceBuilder! broker) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static MMCA.Common.Aspire.Hosting.Extensions.WithCosmosDataSource(this Aspire.Hosting.ApplicationModel.IResourceBuilder! service, Aspire.Hosting.ApplicationModel.IResourceBuilder! database, string! logicalName) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static MMCA.Common.Aspire.Hosting.Extensions.WithE2eRegistrationThrottleLift(this Aspire.Hosting.ApplicationModel.IResourceBuilder! identity, bool alsoLiftWhen = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static MMCA.Common.Aspire.Hosting.Extensions.WithE2eRsaKeys(this Aspire.Hosting.ApplicationModel.IResourceBuilder! identity) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static MMCA.Common.Aspire.Hosting.Extensions.WithJwksDiscovery(this Aspire.Hosting.ApplicationModel.IResourceBuilder! service, Aspire.Hosting.ApplicationModel.IResourceBuilder! identity, Aspire.Hosting.ApplicationModel.IResourceBuilder? gateway = null) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static MMCA.Common.Aspire.Hosting.Extensions.WithSQLServerDataSource(this Aspire.Hosting.ApplicationModel.IResourceBuilder! service, Aspire.Hosting.ApplicationModel.IResourceBuilder! database, string! logicalName) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static MMCA.Common.Aspire.Hosting.Extensions.WithSqliteDataSource(this Aspire.Hosting.ApplicationModel.IResourceBuilder! service, string! logicalName, string! filePath) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! diff --git a/Source/Hosting/MMCA.Common.Aspire.Hosting/PublicAPI.Unshipped.txt b/Source/Hosting/MMCA.Common.Aspire.Hosting/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/Source/Hosting/MMCA.Common.Aspire.Hosting/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/Source/Hosting/MMCA.Common.Aspire.Hosting/packages.lock.json b/Source/Hosting/MMCA.Common.Aspire.Hosting/packages.lock.json index 7b93d085..b9076e8f 100644 --- a/Source/Hosting/MMCA.Common.Aspire.Hosting/packages.lock.json +++ b/Source/Hosting/MMCA.Common.Aspire.Hosting/packages.lock.json @@ -181,6 +181,12 @@ "resolved": "3.0.163", "contentHash": "l9iZ+OXBGlAFEKH1y4BSKPXHcmpJmikysSm0brjTau8fR7Qm7IVGLVig+dh0RH9mq4s3neZayuPQrHRweOpxxw==" }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[5.6.0, )", + "resolved": "5.6.0", + "contentHash": "W4kJGezNIKLzo0Ak5FAQDFvkMf2U7DtGL4THmHyRSApfKsKt5V+eX/bU0ZLKAt/uf9Bb2o1bi0YDKj/GRB/vYQ==" + }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", "requested": "[18.7.23, )", diff --git a/Source/Hosting/MMCA.Common.Aspire/PublicAPI.Shipped.txt b/Source/Hosting/MMCA.Common.Aspire/PublicAPI.Shipped.txt new file mode 100644 index 00000000..b7091e17 --- /dev/null +++ b/Source/Hosting/MMCA.Common.Aspire/PublicAPI.Shipped.txt @@ -0,0 +1,102 @@ +#nullable enable +MMCA.Common.Aspire.DataProtectionExtensions +MMCA.Common.Aspire.DataProtectionExtensions.extension(TBuilder) +MMCA.Common.Aspire.DataProtectionExtensions.extension(TBuilder).AddCommonDataProtection() -> TBuilder +MMCA.Common.Aspire.Extensions +MMCA.Common.Aspire.Extensions.extension(Microsoft.AspNetCore.Builder.WebApplication!) +MMCA.Common.Aspire.Extensions.extension(Microsoft.AspNetCore.Builder.WebApplication!).MapDefaultEndpoints() -> Microsoft.AspNetCore.Builder.WebApplication! +MMCA.Common.Aspire.Extensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!) +MMCA.Common.Aspire.Extensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddWarmupTask() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Aspire.Extensions.extension(TBuilder) +MMCA.Common.Aspire.Extensions.extension(TBuilder).AddDefaultHealthChecks() -> TBuilder +MMCA.Common.Aspire.Extensions.extension(TBuilder).AddInfrastructureHealthChecks(bool requireSqlServer = false) -> TBuilder +MMCA.Common.Aspire.Extensions.extension(TBuilder).AddServiceDefaults() -> TBuilder +MMCA.Common.Aspire.Extensions.extension(TBuilder).AddWarmupReadiness() -> TBuilder +MMCA.Common.Aspire.Extensions.extension(TBuilder).ConfigureOpenTelemetry() -> TBuilder +MMCA.Common.Aspire.GatewayCorsExtensions +MMCA.Common.Aspire.GatewayCorsExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!) +MMCA.Common.Aspire.GatewayCorsExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddCommonGatewayCors(Microsoft.Extensions.Configuration.IConfiguration! configuration, Microsoft.Extensions.Hosting.IHostEnvironment! environment) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Aspire.HealthCheckTags +MMCA.Common.Aspire.Kestrel.KestrelEndpointExtensions +MMCA.Common.Aspire.Kestrel.KestrelEndpointExtensions.extension(Microsoft.AspNetCore.Builder.WebApplicationBuilder!) +MMCA.Common.Aspire.Kestrel.KestrelEndpointExtensions.extension(Microsoft.AspNetCore.Builder.WebApplicationBuilder!).ConfigureEndpointsWithHealthProbe(Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols defaultProtocols, bool redeclareCleartextEndpoint = true, int cleartextPort = 8080) -> Microsoft.AspNetCore.Builder.WebApplicationBuilder! +MMCA.Common.Aspire.KeyVaultConfigurationExtensions +MMCA.Common.Aspire.KeyVaultConfigurationExtensions.extension(TBuilder) +MMCA.Common.Aspire.KeyVaultConfigurationExtensions.extension(TBuilder).AddCommonKeyVaultConfiguration() -> TBuilder +MMCA.Common.Aspire.Security.CspPolicy +MMCA.Common.Aspire.Security.CspPolicy.$() -> MMCA.Common.Aspire.Security.CspPolicy! +MMCA.Common.Aspire.Security.CspPolicy.CspPolicy(string! Value, bool Enforce) -> void +MMCA.Common.Aspire.Security.CspPolicy.Deconstruct(out string! Value, out bool Enforce) -> void +MMCA.Common.Aspire.Security.CspPolicy.Enforce.get -> bool +MMCA.Common.Aspire.Security.CspPolicy.Enforce.init -> void +MMCA.Common.Aspire.Security.CspPolicy.Equals(MMCA.Common.Aspire.Security.CspPolicy? other) -> bool +MMCA.Common.Aspire.Security.CspPolicy.Value.get -> string! +MMCA.Common.Aspire.Security.CspPolicy.Value.init -> void +MMCA.Common.Aspire.Security.ICspPolicyProvider +MMCA.Common.Aspire.Security.ICspPolicyProvider.GetPolicy(Microsoft.AspNetCore.Http.HttpContext! context) -> MMCA.Common.Aspire.Security.CspPolicy? +MMCA.Common.Aspire.Security.SecurityHeadersExtensions +MMCA.Common.Aspire.Security.SecurityHeadersExtensions.extension(Microsoft.AspNetCore.Builder.IApplicationBuilder!) +MMCA.Common.Aspire.Security.SecurityHeadersExtensions.extension(Microsoft.AspNetCore.Builder.IApplicationBuilder!).UseCommonSecurityHeaders() -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +MMCA.Common.Aspire.Security.SecurityHeadersExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!) +MMCA.Common.Aspire.Security.SecurityHeadersExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddCommonSecurityHeaders(Microsoft.Extensions.Configuration.IConfiguration? configuration = null, System.Action? configure = null) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Aspire.Security.SecurityHeadersMiddleware +MMCA.Common.Aspire.Security.SecurityHeadersMiddleware.InvokeAsync(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task! +MMCA.Common.Aspire.Security.SecurityHeadersMiddleware.SecurityHeadersMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, MMCA.Common.Aspire.Security.ICspPolicyProvider! cspPolicyProvider, Microsoft.AspNetCore.Hosting.IWebHostEnvironment! environment) -> void +MMCA.Common.Aspire.Security.SecurityHeadersSettings +MMCA.Common.Aspire.Security.SecurityHeadersSettings.ContentSecurityPolicy.get -> string? +MMCA.Common.Aspire.Security.SecurityHeadersSettings.ContentSecurityPolicy.set -> void +MMCA.Common.Aspire.Security.SecurityHeadersSettings.EnableHsts.get -> bool +MMCA.Common.Aspire.Security.SecurityHeadersSettings.EnableHsts.set -> void +MMCA.Common.Aspire.Security.SecurityHeadersSettings.EnforceContentSecurityPolicy.get -> bool +MMCA.Common.Aspire.Security.SecurityHeadersSettings.EnforceContentSecurityPolicy.set -> void +MMCA.Common.Aspire.Security.SecurityHeadersSettings.FrameOptions.get -> string! +MMCA.Common.Aspire.Security.SecurityHeadersSettings.FrameOptions.set -> void +MMCA.Common.Aspire.Security.SecurityHeadersSettings.HstsValue.get -> string! +MMCA.Common.Aspire.Security.SecurityHeadersSettings.HstsValue.set -> void +MMCA.Common.Aspire.Security.SecurityHeadersSettings.PermissionsPolicy.get -> string! +MMCA.Common.Aspire.Security.SecurityHeadersSettings.PermissionsPolicy.set -> void +MMCA.Common.Aspire.Security.SecurityHeadersSettings.ReferrerPolicy.get -> string! +MMCA.Common.Aspire.Security.SecurityHeadersSettings.ReferrerPolicy.set -> void +MMCA.Common.Aspire.Security.SecurityHeadersSettings.SecurityHeadersSettings() -> void +MMCA.Common.Aspire.Telemetry.OutboxPollFilterProcessor +MMCA.Common.Aspire.Telemetry.OutboxPollFilterProcessor.OutboxPollFilterProcessor() -> void +MMCA.Common.Aspire.Warmup.IWarmupTask +MMCA.Common.Aspire.Warmup.IWarmupTask.ExecuteAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Aspire.Warmup.IWarmupTask.Name.get -> string! +MMCA.Common.Aspire.Warmup.SelfHttpWarmupTaskBase +MMCA.Common.Aspire.Warmup.SelfHttpWarmupTaskBase.ExecuteAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.Aspire.Warmup.SelfHttpWarmupTaskBase.SelfHttpWarmupTaskBase(Microsoft.AspNetCore.Hosting.Server.IServer! server, Microsoft.Extensions.Configuration.IConfiguration! configuration, Microsoft.Extensions.Hosting.IHostEnvironment! environment, Microsoft.Extensions.Hosting.IHostApplicationLifetime! lifetime, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.Aspire.Warmup.WarmupReadinessGate +MMCA.Common.Aspire.Warmup.WarmupReadinessGate.IsReady.get -> bool +MMCA.Common.Aspire.Warmup.WarmupReadinessGate.WarmupReadinessGate() -> void +abstract MMCA.Common.Aspire.Warmup.SelfHttpWarmupTaskBase.Name.get -> string! +abstract MMCA.Common.Aspire.Warmup.SelfHttpWarmupTaskBase.WarmupPaths.get -> System.Collections.Generic.IReadOnlyList! +const MMCA.Common.Aspire.HealthCheckTags.Live = "live" -> string! +const MMCA.Common.Aspire.HealthCheckTags.Optional = "optional" -> string! +const MMCA.Common.Aspire.HealthCheckTags.Ready = "ready" -> string! +const MMCA.Common.Aspire.Kestrel.KestrelEndpointExtensions.DefaultCleartextPort = 8080 -> int +const MMCA.Common.Aspire.Kestrel.KestrelEndpointExtensions.HealthProbePortConfigKey = "HealthProbe:Port" -> string! +const MMCA.Common.Aspire.Security.SecurityHeadersSettings.SectionName = "SecurityHeaders" -> string! +const MMCA.Common.Aspire.Warmup.SelfHttpWarmupTaskBase.DefaultPort = 8080 -> int +override MMCA.Common.Aspire.Security.CspPolicy.Equals(object? obj) -> bool +override MMCA.Common.Aspire.Security.CspPolicy.GetHashCode() -> int +override MMCA.Common.Aspire.Security.CspPolicy.ToString() -> string! +override MMCA.Common.Aspire.Telemetry.OutboxPollFilterProcessor.OnEnd(System.Diagnostics.Activity! data) -> void +static MMCA.Common.Aspire.DataProtectionExtensions.AddCommonDataProtection(this TBuilder builder) -> TBuilder +static MMCA.Common.Aspire.Extensions.AddDefaultHealthChecks(this TBuilder builder) -> TBuilder +static MMCA.Common.Aspire.Extensions.AddInfrastructureHealthChecks(this TBuilder builder, bool requireSqlServer = false) -> TBuilder +static MMCA.Common.Aspire.Extensions.AddServiceDefaults(this TBuilder builder) -> TBuilder +static MMCA.Common.Aspire.Extensions.AddWarmupReadiness(this TBuilder builder) -> TBuilder +static MMCA.Common.Aspire.Extensions.AddWarmupTask(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Aspire.Extensions.ConfigureOpenTelemetry(this TBuilder builder) -> TBuilder +static MMCA.Common.Aspire.Extensions.MapDefaultEndpoints(this Microsoft.AspNetCore.Builder.WebApplication! app) -> Microsoft.AspNetCore.Builder.WebApplication! +static MMCA.Common.Aspire.GatewayCorsExtensions.AddCommonGatewayCors(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration, Microsoft.Extensions.Hosting.IHostEnvironment! environment) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Aspire.Kestrel.KestrelEndpointExtensions.ConfigureEndpointsWithHealthProbe(this Microsoft.AspNetCore.Builder.WebApplicationBuilder! builder, Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols defaultProtocols, bool redeclareCleartextEndpoint = true, int cleartextPort = 8080) -> Microsoft.AspNetCore.Builder.WebApplicationBuilder! +static MMCA.Common.Aspire.KeyVaultConfigurationExtensions.AddCommonKeyVaultConfiguration(this TBuilder builder) -> TBuilder +static MMCA.Common.Aspire.Security.CspPolicy.operator !=(MMCA.Common.Aspire.Security.CspPolicy? left, MMCA.Common.Aspire.Security.CspPolicy? right) -> bool +static MMCA.Common.Aspire.Security.CspPolicy.operator ==(MMCA.Common.Aspire.Security.CspPolicy? left, MMCA.Common.Aspire.Security.CspPolicy? right) -> bool +static MMCA.Common.Aspire.Security.SecurityHeadersExtensions.AddCommonSecurityHeaders(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration? configuration = null, System.Action? configure = null) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Aspire.Security.SecurityHeadersExtensions.UseCommonSecurityHeaders(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +virtual MMCA.Common.Aspire.Warmup.SelfHttpWarmupTaskBase.RequestVersion.get -> System.Version! +virtual MMCA.Common.Aspire.Warmup.SelfHttpWarmupTaskBase.RequestVersionPolicy.get -> System.Net.Http.HttpVersionPolicy +virtual MMCA.Common.Aspire.Warmup.SelfHttpWarmupTaskBase.RequireSuccessStatusCode.get -> bool diff --git a/Source/Hosting/MMCA.Common.Aspire/PublicAPI.Unshipped.txt b/Source/Hosting/MMCA.Common.Aspire/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/Source/Hosting/MMCA.Common.Aspire/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/Source/Hosting/MMCA.Common.Aspire/packages.lock.json b/Source/Hosting/MMCA.Common.Aspire/packages.lock.json index 4f619967..03e4862a 100644 --- a/Source/Hosting/MMCA.Common.Aspire/packages.lock.json +++ b/Source/Hosting/MMCA.Common.Aspire/packages.lock.json @@ -93,6 +93,12 @@ "resolved": "3.0.163", "contentHash": "l9iZ+OXBGlAFEKH1y4BSKPXHcmpJmikysSm0brjTau8fR7Qm7IVGLVig+dh0RH9mq4s3neZayuPQrHRweOpxxw==" }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[5.6.0, )", + "resolved": "5.6.0", + "contentHash": "W4kJGezNIKLzo0Ak5FAQDFvkMf2U7DtGL4THmHyRSApfKsKt5V+eX/bU0ZLKAt/uf9Bb2o1bi0YDKj/GRB/vYQ==" + }, "Microsoft.Extensions.Http.Resilience": { "type": "Direct", "requested": "[10.9.0, )", diff --git a/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureRules.CancellationTokens.cs b/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureRules.CancellationTokens.cs new file mode 100644 index 00000000..059b9bde --- /dev/null +++ b/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureRules.CancellationTokens.cs @@ -0,0 +1,190 @@ +using System.Runtime.CompilerServices; + +namespace MMCA.Common.Testing.Architecture; + +public static partial class ArchitectureRules +{ + /// + /// Every public asynchronous method on a public Application- or Infrastructure-layer type must accept + /// a as its LAST parameter, named cancellationToken. An async + /// method that cannot be cancelled is a request that outlives its caller: a cancelled HTTP request, an + /// expired CQRS timeout budget or a stopping host all leave the work running against the database, and + /// the uniform trailing position plus name is what lets the decorator pipeline, the repositories and + /// generated clients pass the linked token through mechanically instead of case by case. + /// + /// Scope: methods returning Task, Task<T>, ValueTask or + /// ValueTask<T>, declared (not inherited) as public members of a publicly-visible type in + /// a or assembly. A method with no + /// parameters is NOT excused: the token would simply be its only parameter. + /// + /// + /// Automatic exemptions (the signature is not the repo's to change): + /// Dispose/DisposeAsync, compiler-generated and special-name members (property + /// accessors, operators, event accessors), members of delegate types, an override of a base method + /// declared outside the map's assemblies, and an implicit implementation of an interface method + /// declared outside the map's assemblies (IHostedService.StartAsync, IHealthCheck, + /// framework middleware, and so on). + /// + /// + /// The repo's architecture map. + /// + /// Additional exemptions in "TypeName.MethodName" form (simple type name, generic arity + /// stripped). Use only where adding the parameter would be a breaking change to a shipped public API, + /// and record the reason next to the entry. + /// + public static void AsyncMethodsDeclareTrailingCancellationToken( + IArchitectureMap map, + IReadOnlyCollection? exemptMethods = null) + { + ArgumentNullException.ThrowIfNull(map); + + var exempt = new HashSet(exemptMethods ?? [], StringComparer.Ordinal); + var mapAssemblies = map.Layers.Select(l => l.Assembly).ToHashSet(); + var violations = new List(); + + var assemblies = map.OfLayer(Layer.Application) + .Concat(map.OfLayer(Layer.Infrastructure)) + .Distinct(); + + foreach (var assembly in assemblies) + { + foreach (var type in assembly.LoadableTypes.Where(IsCandidateType)) + { + CheckType(type, mapAssemblies, exempt, violations); + } + } + + ArchitectureAssert.NoViolations( + violations, + "public async methods on Application/Infrastructure types must take a trailing " + + "'CancellationToken cancellationToken' - work that cannot be cancelled keeps running against " + + "the database after its caller is gone, and only a uniform trailing token lets the decorator " + + "pipeline and repositories forward the linked token mechanically"); + } + + private static void CheckType( + Type type, + HashSet mapAssemblies, + HashSet exempt, + List violations) + { + var externallyFixed = ExternallyFixedMethods(type, mapAssemblies); + + foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance + | BindingFlags.Static | BindingFlags.DeclaredOnly)) + { + if (!IsCandidateMethod(method) + || externallyFixed.Contains(method) + || exempt.Contains($"{type.SimpleName}.{method.Name}")) + { + continue; + } + + var problem = TokenProblem(method); + if (problem is not null) + { + violations.Add($" - {type.FullName}.{method.Name}: {problem}"); + } + } + } + + /// Publicly-visible, non-delegate, non-compiler-generated types. + private static bool IsCandidateType(Type type) => + type.IsVisible + && !type.IsSubclassOf(typeof(MulticastDelegate)) + && !type.IsDefined(typeof(CompilerGeneratedAttribute), inherit: false); + + private static bool IsCandidateMethod(MethodInfo method) => + method.IsPublic + && !method.IsSpecialName + && !method.IsDefined(typeof(CompilerGeneratedAttribute), inherit: false) + && !string.Equals(method.Name, "Dispose", StringComparison.Ordinal) + && !string.Equals(method.Name, "DisposeAsync", StringComparison.Ordinal) + && IsAwaitableReturn(method.ReturnType); + + private static bool IsAwaitableReturn(Type returnType) + { + var definition = returnType.IsGenericType ? returnType.GetGenericTypeDefinition() : returnType; + return definition == typeof(Task) + || definition == typeof(Task<>) + || definition == typeof(ValueTask) + || definition == typeof(ValueTask<>); + } + + /// The problem with the method's token parameter, or null when it is correct. + private static string? TokenProblem(MethodInfo method) + { + var parameters = method.GetParameters(); + var last = parameters.Length == 0 ? null : parameters[^1]; + + if (last is not null + && last.ParameterType == typeof(CancellationToken) + && string.Equals(last.Name, "cancellationToken", StringComparison.Ordinal)) + { + return null; + } + + if (Array.Exists(parameters, p => p.ParameterType == typeof(CancellationToken))) + { + return last?.ParameterType == typeof(CancellationToken) + ? $"the trailing CancellationToken must be named 'cancellationToken' (found '{last.Name}')" + : "the CancellationToken must be the LAST parameter"; + } + + return "no CancellationToken parameter"; + } + + /// + /// The methods whose signature the repo cannot change: overrides of a base method declared outside the + /// map, and implicit implementations of an interface method declared outside the map. + /// + private static HashSet ExternallyFixedMethods(Type type, HashSet mapAssemblies) + { + var fixedMethods = new HashSet(); + + foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance + | BindingFlags.Static | BindingFlags.DeclaredOnly)) + { + var baseDefinition = method.GetBaseDefinition(); + if (baseDefinition.DeclaringType is { } declaring + && declaring != type + && !mapAssemblies.Contains(declaring.Assembly)) + { + fixedMethods.Add(method); + } + } + + if (type.IsInterface) + { + return fixedMethods; + } + + foreach (var contract in type.GetInterfaces()) + { + if (mapAssemblies.Contains(contract.Assembly)) + { + continue; + } + + foreach (var target in InterfaceTargets(type, contract)) + { + fixedMethods.Add(target); + } + } + + return fixedMethods; + } + + private static IEnumerable InterfaceTargets(Type type, Type contract) + { + try + { + return [.. type.GetInterfaceMap(contract).TargetMethods]; + } + catch (Exception ex) when (ex is ArgumentException or InvalidOperationException or TypeLoadException) + { + // A generic-parameter or otherwise unmappable interface: nothing to exempt. + return []; + } + } +} diff --git a/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureRules.Cycles.cs b/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureRules.Cycles.cs new file mode 100644 index 00000000..aac37ce7 --- /dev/null +++ b/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureRules.Cycles.cs @@ -0,0 +1,379 @@ +using System.Runtime.CompilerServices; + +namespace MMCA.Common.Testing.Architecture; + +public static partial class ArchitectureRules +{ + private const BindingFlags AllDeclaredMembers = + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance + | BindingFlags.Static | BindingFlags.DeclaredOnly; + + /// + /// No dependency cycles between the top-level namespaces of a layer assembly. Types are grouped by + /// the namespace segment directly beneath their (types sitting + /// in the root namespace itself form their own node), a directed edge A -> B is recorded + /// whenever a type in A structurally references a type in B in the SAME assembly, and + /// every strongly connected component of that graph is reported as a cycle path. + /// + /// A namespace cycle is the classic "big ball of mud" early warning: it makes the two folders + /// impossible to reason about, test or extract independently, and it is exactly the coupling that + /// blocks the framework's monolith-to-microservice promise. Breaking one is usually a matter of + /// moving the shared abstraction down (into the root namespace or a dedicated Abstractions + /// namespace) rather than letting the two folders reach across at each other. + /// + /// + /// Edges considered: base types, implemented interfaces, field types, property types, method + /// return and parameter types (public AND non-public declared members) and attribute types, with + /// generic arguments, array/by-ref/pointer element types recursively expanded. + /// + /// + /// Limitation - method-body blindness. This package carries no IL or Roslyn dependency, so the + /// rule is pure reflection over signatures. A reference that exists ONLY inside a method body (a + /// local variable, a constructor call, a static call) is invisible to it, so the rule catches + /// STRUCTURAL cycles (the ones that show up in the type surface) and cannot claim a clean report + /// means zero coupling. Compiler-generated types (closure/iterator classes, which do leak some body + /// references) are skipped deliberately so the result stays a signature-level statement rather than a + /// half-body one. Types outside the layer's root namespace are ignored entirely. + /// + /// + /// The repo's architecture map. + /// + /// Fully-qualified namespace nodes whose cycles are accepted by design. A cycle is skipped only when + /// EVERY namespace on its path is listed, so an allowance can never hide a new cycle that merely + /// touches an accepted namespace. + /// + public static void NamespacesHaveNoDependencyCycles( + IArchitectureMap map, + IReadOnlyCollection? allowedCycleNamespaces = null) + { + ArgumentNullException.ThrowIfNull(map); + + var allowed = new HashSet(allowedCycleNamespaces ?? [], StringComparer.Ordinal); + var violations = new List(); + + foreach (var layer in map.Layers) + { + foreach (var (component, path) in FindNamespaceCycles(BuildNamespaceGraph(layer))) + { + // The allowance must cover the WHOLE strongly connected component, not just the shortest + // path rendered in the message: otherwise a new namespace joining an accepted tangle + // would slip in without changing the reported path. + if (component.TrueForAll(allowed.Contains)) + { + continue; + } + + var extra = component.Where(n => !path.Contains(n, StringComparer.Ordinal)).ToList(); + var suffix = extra.Count == 0 ? string.Empty : $" (component also contains: {string.Join(", ", extra)})"; + violations.Add($" - {layer.Assembly.GetName().Name}: {string.Join(" -> ", path)}{suffix}"); + } + } + + ArchitectureAssert.NoViolations( + violations, + "the top-level namespaces of a layer assembly must form a directed acyclic graph - a cycle " + + "means neither namespace can be understood, tested or extracted without the other; move the " + + "shared abstraction down instead of letting the folders reach across at each other (accept a " + + "by-design cycle explicitly via the allowed-cycle namespaces)"); + } + + /// + /// Builds the namespace-to-namespace dependency graph of one layer assembly. Keys and values are + /// fully-qualified namespace nodes; every referenced node is guaranteed to exist as a key. + /// + private static SortedDictionary> BuildNamespaceGraph(LayerRef layer) + { + var graph = new SortedDictionary>(StringComparer.Ordinal); + + foreach (var type in layer.Assembly.LoadableTypes) + { + if (type.IsDefined(typeof(CompilerGeneratedAttribute), inherit: false)) + { + continue; + } + + var from = NamespaceNode(type, layer.RootNamespace); + if (from is null) + { + continue; + } + + var targets = Edges(graph, from); + foreach (var referenced in ReferencedTypes(type)) + { + if (!ReferenceEquals(referenced.Assembly, layer.Assembly)) + { + continue; + } + + var to = NamespaceNode(referenced, layer.RootNamespace); + if (to is not null && !string.Equals(to, from, StringComparison.Ordinal)) + { + targets.Add(to); + _ = Edges(graph, to); + } + } + } + + return graph; + } + + private static SortedSet Edges(SortedDictionary> graph, string node) + { + if (graph.TryGetValue(node, out var existing)) + { + return existing; + } + + var created = new SortedSet(StringComparer.Ordinal); + graph[node] = created; + return created; + } + + /// + /// The top-level namespace node a type belongs to: the root namespace itself, or the root plus the + /// one segment directly beneath it. Null when the type sits outside the layer's root namespace. + /// + private static string? NamespaceNode(Type type, string rootNamespace) + { + var ns = type.Namespace; + if (ns is null) + { + return null; + } + + if (string.Equals(ns, rootNamespace, StringComparison.Ordinal)) + { + return rootNamespace; + } + + var prefix = rootNamespace + "."; + if (!ns.StartsWith(prefix, StringComparison.Ordinal)) + { + return null; + } + + var rest = ns[prefix.Length..]; + var dot = rest.IndexOf('.', StringComparison.Ordinal); + var segment = dot < 0 ? rest : rest[..dot]; + return string.Concat(prefix, segment); + } + + /// Every type structurally referenced by a type's signature surface, generics expanded. + private static HashSet ReferencedTypes(Type type) + { + var result = new HashSet(); + foreach (var candidate in SignatureTypes(type)) + { + Expand(candidate, result); + } + + return result; + } + + private static IEnumerable SignatureTypes(Type type) + { + yield return type.BaseType; + + foreach (var contract in type.GetInterfaces()) + { + yield return contract; + } + + foreach (var attribute in AttributeTypes(type)) + { + yield return attribute; + } + + foreach (var field in type.GetFields(AllDeclaredMembers)) + { + yield return field.FieldType; + } + + foreach (var property in type.GetProperties(AllDeclaredMembers)) + { + yield return property.PropertyType; + + foreach (var attribute in AttributeTypes(property)) + { + yield return attribute; + } + } + + foreach (var method in type.GetMethods(AllDeclaredMembers)) + { + yield return method.ReturnType; + + foreach (var parameter in method.GetParameters()) + { + yield return parameter.ParameterType; + } + } + } + + /// The attribute types applied to a member, tolerating an attribute that cannot be loaded. + private static IReadOnlyList AttributeTypes(MemberInfo member) + { + try + { + return [.. member.GetCustomAttributesData().Select(a => a.AttributeType)]; + } + catch (Exception ex) when (ex is TypeLoadException or FileNotFoundException or FileLoadException) + { + return []; + } + } + + /// Adds a candidate and every type nested inside it (array element, generic argument) to the set. + private static void Expand(Type? candidate, HashSet into) + { + while (candidate is not null && (candidate.IsArray || candidate.IsByRef || candidate.IsPointer)) + { + candidate = candidate.GetElementType(); + } + + if (candidate is null || candidate.IsGenericParameter || !into.Add(candidate)) + { + return; + } + + if (candidate.IsGenericType) + { + foreach (var argument in candidate.GetGenericArguments()) + { + Expand(argument, into); + } + } + } + + /// + /// Every cycle in the namespace graph, one per strongly connected component: the component's full + /// member list plus the shortest path leading from its first node back to itself. + /// + private static List<(List Component, List Path)> FindNamespaceCycles( + SortedDictionary> graph) + { + var nodes = graph.Keys.ToList(); + var count = nodes.Count; + var position = new Dictionary(StringComparer.Ordinal); + for (var i = 0; i < count; i++) + { + position[nodes[i]] = i; + } + + var edge = new bool[count][]; + var reach = new bool[count][]; + for (var i = 0; i < count; i++) + { + edge[i] = new bool[count]; + reach[i] = new bool[count]; + foreach (var target in graph[nodes[i]]) + { + edge[i][position[target]] = true; + reach[i][position[target]] = true; + } + } + + Close(reach, count); + + var cycles = new List<(List Component, List Path)>(); + var claimed = new bool[count]; + for (var i = 0; i < count; i++) + { + if (claimed[i]) + { + continue; + } + + claimed[i] = true; + var component = new HashSet(); + for (var j = i; j < count; j++) + { + if (reach[i][j] && reach[j][i]) + { + component.Add(j); + claimed[j] = true; + } + } + + if (component.Count > 1) + { + var members = component.Select(n => nodes[n]).Order(StringComparer.Ordinal).ToList(); + cycles.Add((members, ShortestCycle(i, component, edge, nodes))); + } + } + + return cycles; + } + + /// Transitive closure of the reachability matrix (Floyd-Warshall over booleans). + private static void Close(bool[][] reach, int count) + { + for (var k = 0; k < count; k++) + { + for (var i = 0; i < count; i++) + { + if (!reach[i][k]) + { + continue; + } + + for (var j = 0; j < count; j++) + { + reach[i][j] = reach[i][j] || reach[k][j]; + } + } + } + } + + /// Breadth-first search for the shortest path from a node back to itself inside its component. + private static List ShortestCycle(int start, HashSet component, bool[][] edge, List nodes) + { + var previous = new Dictionary(); + var seen = new HashSet { start }; + var queue = new Queue(); + queue.Enqueue(start); + + while (queue.Count > 0) + { + var current = queue.Dequeue(); + foreach (var next in component) + { + if (!edge[current][next]) + { + continue; + } + + if (next == start) + { + return TracePath(start, current, previous, nodes); + } + + if (seen.Add(next)) + { + previous[next] = current; + queue.Enqueue(next); + } + } + } + + // Unreachable for a component of size > 1, but a total function beats an exception here. + return [.. component.Select(n => nodes[n])]; + } + + private static List TracePath(int start, int last, Dictionary previous, List nodes) + { + var path = new List(); + var at = last; + while (at != start) + { + path.Add(nodes[at]); + at = previous[at]; + } + + path.Add(nodes[start]); + path.Reverse(); + path.Add(nodes[start]); + return path; + } +} diff --git a/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/CancellationTokenConventionTestsBase.cs b/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/CancellationTokenConventionTestsBase.cs new file mode 100644 index 00000000..b3f069bf --- /dev/null +++ b/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/CancellationTokenConventionTestsBase.cs @@ -0,0 +1,31 @@ +namespace MMCA.Common.Testing.Architecture; + +/// +/// Fitness function: every public async method on a public Application- or Infrastructure-layer type +/// declares a trailing CancellationToken cancellationToken. Cancellation is only end-to-end if it +/// is uniform: one method that swallows the token turns a cancelled request, an expired CQRS timeout +/// budget or a stopping host into work that keeps running against the database, and a token in a +/// non-trailing position or under a different name defeats the mechanical forwarding the decorator +/// pipeline and the repositories rely on. +/// +/// Signatures the repo does not own are exempt automatically (overrides and interface implementations +/// from outside the map's assemblies, Dispose/DisposeAsync, compiler-generated members); +/// see . +/// +/// +public abstract class CancellationTokenConventionTestsBase +{ + /// The repo's architecture map. + protected abstract IArchitectureMap Map { get; } + + /// + /// Additional exemptions in "TypeName.MethodName" form. Reserve these for a shipped public API + /// where adding the parameter would break consumers, and justify each entry with a comment; a method + /// that simply has not been updated yet should be fixed instead. Empty by default. + /// + protected virtual IReadOnlyList CancellationTokenExemptMethods => []; + + [Fact] + public void AsyncMethods_ShouldDeclare_TrailingCancellationToken() => + ArchitectureRules.AsyncMethodsDeclareTrailingCancellationToken(Map, CancellationTokenExemptMethods); +} diff --git a/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/NamespaceCycleTestsBase.cs b/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/NamespaceCycleTestsBase.cs new file mode 100644 index 00000000..8912d815 --- /dev/null +++ b/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/NamespaceCycleTestsBase.cs @@ -0,0 +1,31 @@ +namespace MMCA.Common.Testing.Architecture; + +/// +/// Fitness function: the top-level namespaces inside each layer assembly must form a directed acyclic +/// graph. A namespace cycle is the first visible symptom of a folder pair that has grown into one +/// tangled unit: neither half can be read, tested or lifted into its own service without the other, so +/// the rule guards the framework's extraction promise at the finest granularity the two coarse layer +/// rules (LayerDependencyTestsBase, ModuleIsolationTestsBase) cannot see inside. +/// +/// The rule is signature-level reflection and is blind to method bodies (see +/// ), +/// so a green result means "no STRUCTURAL cycle", not "no coupling". +/// +/// +public abstract class NamespaceCycleTestsBase +{ + /// The repo's architecture map. + protected abstract IArchitectureMap Map { get; } + + /// + /// Fully-qualified namespace nodes (e.g. "MMCA.Common.Domain.Entities") whose cycle is accepted + /// by design. A cycle is skipped only when EVERY namespace on its reported path appears in this list, + /// so an allowance can never hide a NEW cycle that merely touches an accepted namespace. Empty by + /// default; each entry a subclass adds should carry a comment justifying why the tangle is deliberate. + /// + protected virtual IReadOnlyList AllowedCycleNamespaces => []; + + [Fact] + public void Namespaces_ShouldNotHave_DependencyCycles() => + ArchitectureRules.NamespacesHaveNoDependencyCycles(Map, AllowedCycleNamespaces); +} diff --git a/Source/Hosting/MMCA.Common.Testing.Architecture/PublicAPI.Shipped.txt b/Source/Hosting/MMCA.Common.Testing.Architecture/PublicAPI.Shipped.txt new file mode 100644 index 00000000..16ffaecc --- /dev/null +++ b/Source/Hosting/MMCA.Common.Testing.Architecture/PublicAPI.Shipped.txt @@ -0,0 +1,390 @@ +#nullable enable +MMCA.Common.Testing.Architecture.AggregateConventionTestsBase +MMCA.Common.Testing.Architecture.AggregateConventionTestsBase.AggregateConventionTestsBase() -> void +MMCA.Common.Testing.Architecture.AggregateConventionTestsBase.AggregateRoots_ShouldHave_NoPublicConstructors() -> void +MMCA.Common.Testing.Architecture.AggregateConventionTestsBase.AggregateRoots_ShouldHave_ResultReturningCreateFactory() -> void +MMCA.Common.Testing.Architecture.AggregateConventionTestsBase.DomainFactories_ShouldReturn_Result() -> void +MMCA.Common.Testing.Architecture.AggregateConventionTestsBase.Domain_ShouldExpose_AggregateRoots() -> void +MMCA.Common.Testing.Architecture.ArchitectureAssert +MMCA.Common.Testing.Architecture.ArchitectureMapBase +MMCA.Common.Testing.Architecture.ArchitectureMapBase.Api() -> System.Collections.Generic.IEnumerable! +MMCA.Common.Testing.Architecture.ArchitectureMapBase.ArchitectureMapBase() -> void +MMCA.Common.Testing.Architecture.ArchitectureMapBase.For(string! module, MMCA.Common.Testing.Architecture.Layer layer) -> System.Reflection.Assembly? +MMCA.Common.Testing.Architecture.ArchitectureMapBase.Infrastructure() -> System.Collections.Generic.IEnumerable! +MMCA.Common.Testing.Architecture.ArchitectureMapBase.Layers.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Testing.Architecture.ArchitectureMapBase.Module(string! module, MMCA.Common.Testing.Architecture.Layer layer, System.Reflection.Assembly! assembly) -> MMCA.Common.Testing.Architecture.LayerRef! +MMCA.Common.Testing.Architecture.ArchitectureMapBase.ModuleApplication() -> System.Collections.Generic.IEnumerable! +MMCA.Common.Testing.Architecture.ArchitectureMapBase.ModuleDomain() -> System.Collections.Generic.IEnumerable! +MMCA.Common.Testing.Architecture.ArchitectureMapBase.ModuleNames.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Testing.Architecture.ArchitectureMapBase.ModuleOf(System.Reflection.Assembly! assembly) -> string! +MMCA.Common.Testing.Architecture.ArchitectureMapBase.ModuleShared() -> System.Collections.Generic.IEnumerable! +MMCA.Common.Testing.Architecture.ArchitectureMapBase.OfLayer(MMCA.Common.Testing.Architecture.Layer layer) -> System.Collections.Generic.IEnumerable! +MMCA.Common.Testing.Architecture.ArchitectureMapBase.OtherModuleNamespaces(string! module, MMCA.Common.Testing.Architecture.Layer layer) -> string![]! +MMCA.Common.Testing.Architecture.ArchitectureMapBase.RootNamespace(string! module, MMCA.Common.Testing.Architecture.Layer layer) -> string! +MMCA.Common.Testing.Architecture.ArchitectureRules +MMCA.Common.Testing.Architecture.BrandColorTokenTestsBase +MMCA.Common.Testing.Architecture.BrandColorTokenTestsBase.BrandColorTokenTestsBase() -> void +MMCA.Common.Testing.Architecture.BrandColorTokenTestsBase.LandingPageCss_SourcesBrandColorFromToken_NotHardcodedHex() -> void +MMCA.Common.Testing.Architecture.CancellationTokenConventionTestsBase +MMCA.Common.Testing.Architecture.CancellationTokenConventionTestsBase.AsyncMethods_ShouldDeclare_TrailingCancellationToken() -> void +MMCA.Common.Testing.Architecture.CancellationTokenConventionTestsBase.CancellationTokenConventionTestsBase() -> void +MMCA.Common.Testing.Architecture.ConcurrencyConventionTestsBase +MMCA.Common.Testing.Architecture.ConcurrencyConventionTestsBase.ConcurrencyConventionTestsBase() -> void +MMCA.Common.Testing.Architecture.ConcurrencyConventionTestsBase.UpdateRequests_ShouldImplement_IConcurrencyAware() -> void +MMCA.Common.Testing.Architecture.ConstructorDependencyCountTestsBase +MMCA.Common.Testing.Architecture.ConstructorDependencyCountTestsBase.ApplicationServices_DoNotExceedConstructorDependencyCeiling() -> void +MMCA.Common.Testing.Architecture.ConstructorDependencyCountTestsBase.ConstructorDependencyCountTestsBase() -> void +MMCA.Common.Testing.Architecture.ControllerConventionTestsBase +MMCA.Common.Testing.Architecture.ControllerConventionTestsBase.ControllerConventionTestsBase() -> void +MMCA.Common.Testing.Architecture.ControllerConventionTestsBase.Controllers_ShouldBe_Sealed() -> void +MMCA.Common.Testing.Architecture.ControllerConventionTestsBase.Controllers_ShouldInherit_ApiControllerBase() -> void +MMCA.Common.Testing.Architecture.ControllerConventionTestsBase.Controllers_ShouldNotDependOn_EntityFrameworkCore() -> void +MMCA.Common.Testing.Architecture.ControllerConventionTestsBase.Controllers_ShouldNotDependOn_Infrastructure() -> void +MMCA.Common.Testing.Architecture.DataResidencyTestsBase +MMCA.Common.Testing.Architecture.DataResidencyTestsBase.DataResidencyTestsBase() -> void +MMCA.Common.Testing.Architecture.DataResidencyTestsBase.PrivacyPolicy_DataStorageRegion_MatchesDeployedRegion() -> void +MMCA.Common.Testing.Architecture.DependencyVersionTestsBase +MMCA.Common.Testing.Architecture.DependencyVersionTestsBase.DependencyVersionTestsBase() -> void +MMCA.Common.Testing.Architecture.DependencyVersionTestsBase.ImageSharp_MustNotExceed_MajorVersion3() -> void +MMCA.Common.Testing.Architecture.DependencyVersionTestsBase.MassTransit_MustNotExceed_MajorVersion8() -> void +MMCA.Common.Testing.Architecture.DomainPurityTestsBase +MMCA.Common.Testing.Architecture.DomainPurityTestsBase.Application_ShouldNotDependOn_AspNetCore() -> void +MMCA.Common.Testing.Architecture.DomainPurityTestsBase.Application_ShouldNotDependOn_EntityFrameworkCore() -> void +MMCA.Common.Testing.Architecture.DomainPurityTestsBase.DomainPurityTestsBase() -> void +MMCA.Common.Testing.Architecture.DomainPurityTestsBase.Domain_ShouldBe_FrameworkFree() -> void +MMCA.Common.Testing.Architecture.DomainPurityTestsBase.Shared_ShouldBe_FrameworkFree() -> void +MMCA.Common.Testing.Architecture.EntityConventionTestsBase +MMCA.Common.Testing.Architecture.EntityConventionTestsBase.AggregateRoots_ShouldHave_NoPublicConstructors() -> void +MMCA.Common.Testing.Architecture.EntityConventionTestsBase.AggregateRoots_ShouldHave_ResultReturningCreateFactory() -> void +MMCA.Common.Testing.Architecture.EntityConventionTestsBase.DomainEntities_ShouldBe_Sealed() -> void +MMCA.Common.Testing.Architecture.EntityConventionTestsBase.DomainEntities_ShouldReside_InDomainLayer() -> void +MMCA.Common.Testing.Architecture.EntityConventionTestsBase.DomainFactories_ShouldReturn_Result() -> void +MMCA.Common.Testing.Architecture.EntityConventionTestsBase.Domain_ShouldExpose_AggregateRoots() -> void +MMCA.Common.Testing.Architecture.EntityConventionTestsBase.DtosAndRequests_ShouldNotResideIn_DomainOrInfrastructure() -> void +MMCA.Common.Testing.Architecture.EntityConventionTestsBase.EntityConventionTestsBase() -> void +MMCA.Common.Testing.Architecture.EventConventionTestsBase +MMCA.Common.Testing.Architecture.EventConventionTestsBase.EventConventionTestsBase() -> void +MMCA.Common.Testing.Architecture.EventConventionTestsBase.IntegrationEvents_ShouldDeclare_SchemaVersion() -> void +MMCA.Common.Testing.Architecture.EventConventionTestsBase.IntegrationEvents_ShouldInherit_BaseIntegrationEvent() -> void +MMCA.Common.Testing.Architecture.EventConventionTestsBase.IntegrationEvents_ShouldResideIn_SharedIntegrationEventsNamespace() -> void +MMCA.Common.Testing.Architecture.FormsConventionTestsBase +MMCA.Common.Testing.Architecture.FormsConventionTestsBase.AdminCreateForms_KeepUnsavedChangesGuardAndValidation() -> void +MMCA.Common.Testing.Architecture.FormsConventionTestsBase.FormsConventionTestsBase() -> void +MMCA.Common.Testing.Architecture.FrameworkVersionConsistencyTestsBase +MMCA.Common.Testing.Architecture.FrameworkVersionConsistencyTestsBase.AllMmcaCommonPackages_ArePinnedToOneVersion() -> void +MMCA.Common.Testing.Architecture.FrameworkVersionConsistencyTestsBase.FrameworkVersionConsistencyTestsBase() -> void +MMCA.Common.Testing.Architecture.HandlerConventionTestsBase +MMCA.Common.Testing.Architecture.HandlerConventionTestsBase.ApplicationServices_ShouldNotExceed_ConstructorArity() -> void +MMCA.Common.Testing.Architecture.HandlerConventionTestsBase.ApplicationServices_ShouldNotInject_Handlers() -> void +MMCA.Common.Testing.Architecture.HandlerConventionTestsBase.EventHandlers_ShouldResideIn_ApplicationLayer_AndBeSealed() -> void +MMCA.Common.Testing.Architecture.HandlerConventionTestsBase.HandlerConventionTestsBase() -> void +MMCA.Common.Testing.Architecture.HandlerConventionTestsBase.Handlers_ShouldNotInject_OtherHandlers() -> void +MMCA.Common.Testing.Architecture.HandlerConventionTestsBase.Handlers_ShouldResideIn_ApplicationLayer() -> void +MMCA.Common.Testing.Architecture.HandlerConventionTestsBase.Validators_ShouldResideIn_ApplicationLayer() -> void +MMCA.Common.Testing.Architecture.HandlerResultConventionTestsBase +MMCA.Common.Testing.Architecture.HandlerResultConventionTestsBase.ApplicationLayers_DeclareAtLeastOneHandler() -> void +MMCA.Common.Testing.Architecture.HandlerResultConventionTestsBase.CommandHandlers_Return_ResultTypes() -> void +MMCA.Common.Testing.Architecture.HandlerResultConventionTestsBase.HandlerResultConventionTestsBase() -> void +MMCA.Common.Testing.Architecture.HandlerResultConventionTestsBase.QueryHandlers_Return_ResultTypes() -> void +MMCA.Common.Testing.Architecture.IArchitectureMap +MMCA.Common.Testing.Architecture.IArchitectureMap.Api() -> System.Collections.Generic.IEnumerable! +MMCA.Common.Testing.Architecture.IArchitectureMap.For(string! module, MMCA.Common.Testing.Architecture.Layer layer) -> System.Reflection.Assembly? +MMCA.Common.Testing.Architecture.IArchitectureMap.Infrastructure() -> System.Collections.Generic.IEnumerable! +MMCA.Common.Testing.Architecture.IArchitectureMap.Layers.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Testing.Architecture.IArchitectureMap.ModuleApplication() -> System.Collections.Generic.IEnumerable! +MMCA.Common.Testing.Architecture.IArchitectureMap.ModuleDomain() -> System.Collections.Generic.IEnumerable! +MMCA.Common.Testing.Architecture.IArchitectureMap.ModuleNames.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Testing.Architecture.IArchitectureMap.ModuleOf(System.Reflection.Assembly! assembly) -> string! +MMCA.Common.Testing.Architecture.IArchitectureMap.ModuleShared() -> System.Collections.Generic.IEnumerable! +MMCA.Common.Testing.Architecture.IArchitectureMap.OfLayer(MMCA.Common.Testing.Architecture.Layer layer) -> System.Collections.Generic.IEnumerable! +MMCA.Common.Testing.Architecture.IArchitectureMap.OtherModuleNamespaces(string! module, MMCA.Common.Testing.Architecture.Layer layer) -> string![]! +MMCA.Common.Testing.Architecture.IArchitectureMap.RepoToken.get -> string! +MMCA.Common.Testing.Architecture.IArchitectureMap.RootNamespace(string! module, MMCA.Common.Testing.Architecture.Layer layer) -> string! +MMCA.Common.Testing.Architecture.ImmutabilityTestsBase +MMCA.Common.Testing.Architecture.ImmutabilityTestsBase.CommandsAndQueries_ShouldBe_Immutable() -> void +MMCA.Common.Testing.Architecture.ImmutabilityTestsBase.DomainEvents_ShouldBe_Immutable() -> void +MMCA.Common.Testing.Architecture.ImmutabilityTestsBase.Dtos_ShouldBe_Immutable() -> void +MMCA.Common.Testing.Architecture.ImmutabilityTestsBase.ImmutabilityTestsBase() -> void +MMCA.Common.Testing.Architecture.ImmutabilityTestsBase.IntegrationEvents_ShouldBe_Immutable() -> void +MMCA.Common.Testing.Architecture.ImmutabilityTestsBase.ValueObjects_ShouldBe_ImmutableSealedAndInShared() -> void +MMCA.Common.Testing.Architecture.IntegrationEventContractTestsBase +MMCA.Common.Testing.Architecture.IntegrationEventContractTestsBase.IntegrationEventContractTestsBase() -> void +MMCA.Common.Testing.Architecture.IntegrationEventContractTestsBase.IntegrationEventContracts_ShouldMatch_TheFrozenSnapshot() -> void +MMCA.Common.Testing.Architecture.Layer +MMCA.Common.Testing.Architecture.Layer.Api = 4 -> MMCA.Common.Testing.Architecture.Layer +MMCA.Common.Testing.Architecture.Layer.Application = 2 -> MMCA.Common.Testing.Architecture.Layer +MMCA.Common.Testing.Architecture.Layer.Contracts = 7 -> MMCA.Common.Testing.Architecture.Layer +MMCA.Common.Testing.Architecture.Layer.Domain = 1 -> MMCA.Common.Testing.Architecture.Layer +MMCA.Common.Testing.Architecture.Layer.Grpc = 6 -> MMCA.Common.Testing.Architecture.Layer +MMCA.Common.Testing.Architecture.Layer.Infrastructure = 3 -> MMCA.Common.Testing.Architecture.Layer +MMCA.Common.Testing.Architecture.Layer.ServiceHost = 8 -> MMCA.Common.Testing.Architecture.Layer +MMCA.Common.Testing.Architecture.Layer.Shared = 0 -> MMCA.Common.Testing.Architecture.Layer +MMCA.Common.Testing.Architecture.Layer.Ui = 5 -> MMCA.Common.Testing.Architecture.Layer +MMCA.Common.Testing.Architecture.LayerDependencyTestsBase +MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.Application_ShouldNotDependOn_Api() -> void +MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.Application_ShouldNotDependOn_Infrastructure() -> void +MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.Domain_ShouldNotDependOn_Api() -> void +MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.Domain_ShouldNotDependOn_Application() -> void +MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.Domain_ShouldNotDependOn_Infrastructure() -> void +MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.Infrastructure_ShouldNotDependOn_Api() -> void +MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.LayerDependencyTestsBase() -> void +MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.LayerMap_DeclaresEveryExpectedLayer() -> void +MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.LayerMap_ModulesDeclareEveryExpectedLayer() -> void +MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.Shared_ShouldNotDependOn_Api() -> void +MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.Shared_ShouldNotDependOn_Application() -> void +MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.Shared_ShouldNotDependOn_Domain() -> void +MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.Shared_ShouldNotDependOn_Infrastructure() -> void +MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.Ui_ShouldNotDependOn_Application() -> void +MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.Ui_ShouldNotDependOn_Domain() -> void +MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.Ui_ShouldNotDependOn_Infrastructure() -> void +MMCA.Common.Testing.Architecture.LayerRef +MMCA.Common.Testing.Architecture.LayerRef.$() -> MMCA.Common.Testing.Architecture.LayerRef! +MMCA.Common.Testing.Architecture.LayerRef.Assembly.get -> System.Reflection.Assembly! +MMCA.Common.Testing.Architecture.LayerRef.Assembly.init -> void +MMCA.Common.Testing.Architecture.LayerRef.Deconstruct(out string! Module, out MMCA.Common.Testing.Architecture.Layer Layer, out System.Reflection.Assembly! Assembly, out string! RootNamespace) -> void +MMCA.Common.Testing.Architecture.LayerRef.Equals(MMCA.Common.Testing.Architecture.LayerRef? other) -> bool +MMCA.Common.Testing.Architecture.LayerRef.Layer.get -> MMCA.Common.Testing.Architecture.Layer +MMCA.Common.Testing.Architecture.LayerRef.Layer.init -> void +MMCA.Common.Testing.Architecture.LayerRef.LayerRef(string! Module, MMCA.Common.Testing.Architecture.Layer Layer, System.Reflection.Assembly! Assembly, string! RootNamespace) -> void +MMCA.Common.Testing.Architecture.LayerRef.Module.get -> string! +MMCA.Common.Testing.Architecture.LayerRef.Module.init -> void +MMCA.Common.Testing.Architecture.LayerRef.RootNamespace.get -> string! +MMCA.Common.Testing.Architecture.LayerRef.RootNamespace.init -> void +MMCA.Common.Testing.Architecture.LocalizationResourceTestsBase +MMCA.Common.Testing.Architecture.LocalizationResourceTestsBase.LocalizationResourceTestsBase() -> void +MMCA.Common.Testing.Architecture.LocalizationResourceTestsBase.Translations_AreComplete_ForEveryRequiredCulture() -> void +MMCA.Common.Testing.Architecture.LocalizedTextConventionTestsBase +MMCA.Common.Testing.Architecture.LocalizedTextConventionTestsBase.LocalizedTextConventionTestsBase() -> void +MMCA.Common.Testing.Architecture.LocalizedTextConventionTestsBase.UserVisibleText_IsLocalized() -> void +MMCA.Common.Testing.Architecture.MicroserviceExtractionTestsBase +MMCA.Common.Testing.Architecture.MicroserviceExtractionTestsBase.CoreLayers_ShouldNotDependOn_Transport() -> void +MMCA.Common.Testing.Architecture.MicroserviceExtractionTestsBase.MicroserviceExtractionTestsBase() -> void +MMCA.Common.Testing.Architecture.ModuleConformanceTestsBase +MMCA.Common.Testing.Architecture.ModuleConformanceTestsBase.ModuleConformanceTestsBase() -> void +MMCA.Common.Testing.Architecture.ModuleConformanceTestsBase.Module_ShouldDeclare_ExpectedDependencies() -> void +MMCA.Common.Testing.Architecture.ModuleConformanceTestsBase.Module_ShouldDeclare_ExpectedName() -> void +MMCA.Common.Testing.Architecture.ModuleConformanceTestsBase.Module_ShouldDeclare_ExpectedRequiresDependencies() -> void +MMCA.Common.Testing.Architecture.ModuleConformanceTestsBase.Module_ShouldRegister_ExpectedDisabledStubs() -> void +MMCA.Common.Testing.Architecture.ModuleIsolationTestsBase +MMCA.Common.Testing.Architecture.ModuleIsolationTestsBase.ModuleApis_ShouldBe_Isolated() -> void +MMCA.Common.Testing.Architecture.ModuleIsolationTestsBase.ModuleApplications_ShouldBe_Isolated() -> void +MMCA.Common.Testing.Architecture.ModuleIsolationTestsBase.ModuleApplications_ShouldNotReach_OtherModuleInfrastructures() -> void +MMCA.Common.Testing.Architecture.ModuleIsolationTestsBase.ModuleDomains_ShouldBe_Isolated() -> void +MMCA.Common.Testing.Architecture.ModuleIsolationTestsBase.ModuleDomains_ShouldNotReach_OtherModuleInfrastructures() -> void +MMCA.Common.Testing.Architecture.ModuleIsolationTestsBase.ModuleInfrastructures_ShouldBe_Isolated() -> void +MMCA.Common.Testing.Architecture.ModuleIsolationTestsBase.ModuleIsolationTestsBase() -> void +MMCA.Common.Testing.Architecture.NamespaceCycleTestsBase +MMCA.Common.Testing.Architecture.NamespaceCycleTestsBase.NamespaceCycleTestsBase() -> void +MMCA.Common.Testing.Architecture.NamespaceCycleTestsBase.Namespaces_ShouldNotHave_DependencyCycles() -> void +MMCA.Common.Testing.Architecture.NamingConventionTestsBase +MMCA.Common.Testing.Architecture.NamingConventionTestsBase.Commands_ShouldHave_CommandOrRequestSuffix() -> void +MMCA.Common.Testing.Architecture.NamingConventionTestsBase.DomainEvents_ShouldBeSealed_InDomainEventsNamespace() -> void +MMCA.Common.Testing.Architecture.NamingConventionTestsBase.EfConfigurations_ShouldBeSealed_WithConfigurationSuffix() -> void +MMCA.Common.Testing.Architecture.NamingConventionTestsBase.Handlers_ShouldBeSealed_WithHandlerSuffix() -> void +MMCA.Common.Testing.Architecture.NamingConventionTestsBase.InvariantClasses_ShouldBe_Static() -> void +MMCA.Common.Testing.Architecture.NamingConventionTestsBase.NamingConventionTestsBase() -> void +MMCA.Common.Testing.Architecture.NamingConventionTestsBase.Queries_ShouldHave_QuerySuffix() -> void +MMCA.Common.Testing.Architecture.NamingConventionTestsBase.Repositories_ShouldHave_RepositorySuffix() -> void +MMCA.Common.Testing.Architecture.NamingConventionTestsBase.SharedDtos_ShouldHave_DtoOrLookupSuffix() -> void +MMCA.Common.Testing.Architecture.NamingConventionTestsBase.Specifications_ShouldBeSealed_WithSpecificationSuffix() -> void +MMCA.Common.Testing.Architecture.NamingConventionTestsBase.Validators_ShouldHave_ValidatorOrRulesSuffix() -> void +MMCA.Common.Testing.Architecture.ObservabilityConventionTestsBase +MMCA.Common.Testing.Architecture.ObservabilityConventionTestsBase.EveryProvisionedSloAlert_HasASeverityCorrectRunbookSection() -> void +MMCA.Common.Testing.Architecture.ObservabilityConventionTestsBase.EveryRunbookAlertSection_MapsToAProvisionedAlert() -> void +MMCA.Common.Testing.Architecture.ObservabilityConventionTestsBase.ObservabilityConventionTestsBase() -> void +MMCA.Common.Testing.Architecture.ObservabilityConventionTestsBase.SloAlertSpecs_AreDiscovered_GateIsNotVacuous() -> void +MMCA.Common.Testing.Architecture.PiiConventionTestsBase +MMCA.Common.Testing.Architecture.PiiConventionTestsBase.EntitiesWithPiiProperties_ShouldImplement_IAnonymizable() -> void +MMCA.Common.Testing.Architecture.PiiConventionTestsBase.PiiConventionTestsBase() -> void +MMCA.Common.Testing.Architecture.RawQueryableConventionTestsBase +MMCA.Common.Testing.Architecture.RawQueryableConventionTestsBase.ApplicationLayer_DoesNotUseRawQueryableSurfaces() -> void +MMCA.Common.Testing.Architecture.RawQueryableConventionTestsBase.RawQueryableConventionTestsBase() -> void +MMCA.Common.Testing.Architecture.RouteAuthorizationTestsBase +MMCA.Common.Testing.Architecture.RouteAuthorizationTestsBase.GovernedPageSet_IsNotEmpty() -> void +MMCA.Common.Testing.Architecture.RouteAuthorizationTestsBase.GovernedPages_RequireDeclaredRole() -> void +MMCA.Common.Testing.Architecture.RouteAuthorizationTestsBase.RouteAuthorizationTestsBase() -> void +MMCA.Common.Testing.Architecture.SharedLayerTestsBase +MMCA.Common.Testing.Architecture.SharedLayerTestsBase.ModuleShared_ShouldBe_Isolated() -> void +MMCA.Common.Testing.Architecture.SharedLayerTestsBase.ModuleShared_ShouldNotDependOn_EntityFrameworkCore() -> void +MMCA.Common.Testing.Architecture.SharedLayerTestsBase.ModuleShared_ShouldNotDependOn_OwnInternalLayers() -> void +MMCA.Common.Testing.Architecture.SharedLayerTestsBase.SharedLayerTestsBase() -> void +MMCA.Common.Testing.Architecture.SliceCohesionTestsBase +MMCA.Common.Testing.Architecture.SliceCohesionTestsBase.Handlers_ShouldBeCoLocatedWith_TheirContracts() -> void +MMCA.Common.Testing.Architecture.SliceCohesionTestsBase.SliceCohesionTestsBase() -> void +MMCA.Common.Testing.Architecture.SliceCohesionTestsBase.Validators_ShouldBeCoLocatedWith_TheirContracts() -> void +MMCA.Common.Testing.Architecture.SpecificationConventionTestsBase +MMCA.Common.Testing.Architecture.SpecificationConventionTestsBase.SpecificationConventionTestsBase() -> void +MMCA.Common.Testing.Architecture.SpecificationConventionTestsBase.Specifications_ShouldNotNavigate_ToOtherEntities() -> void +MMCA.Common.Testing.Architecture.StateManagementConventionTestsBase +MMCA.Common.Testing.Architecture.StateManagementConventionTestsBase.StateManagementConventionTestsBase() -> void +MMCA.Common.Testing.Architecture.StateManagementConventionTestsBase.UiAssemblies_CarryNoMutableStaticState() -> void +MMCA.Common.Testing.Architecture.StateManagementConventionTestsBase.UiProjects_RegisterStatefulServicesScoped() -> void +MMCA.Common.Testing.Architecture.UIArchitectureConventionTestsBase +MMCA.Common.Testing.Architecture.UIArchitectureConventionTestsBase.CodeBehinds_StayWithinTheLineCap() -> void +MMCA.Common.Testing.Architecture.UIArchitectureConventionTestsBase.RazorFiles_KeepInlineCodeBlocksSmall() -> void +MMCA.Common.Testing.Architecture.UIArchitectureConventionTestsBase.UIArchitectureConventionTestsBase() -> void +abstract MMCA.Common.Testing.Architecture.AggregateConventionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.ArchitectureMapBase.DefineLayers() -> System.Collections.Generic.IEnumerable! +abstract MMCA.Common.Testing.Architecture.ArchitectureMapBase.RepoToken.get -> string! +abstract MMCA.Common.Testing.Architecture.BrandColorTokenTestsBase.EmbeddedCssLogicalNames.get -> System.Collections.Generic.IReadOnlyList! +abstract MMCA.Common.Testing.Architecture.CancellationTokenConventionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.ConcurrencyConventionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.ConstructorDependencyCountTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.ConstructorDependencyCountTestsBase.MaxConstructorDependencies.get -> int +abstract MMCA.Common.Testing.Architecture.ControllerConventionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.DataResidencyTestsBase.ExtractDeployedRegion(string! repoRoot) -> string! +abstract MMCA.Common.Testing.Architecture.DataResidencyTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.DomainPurityTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.EntityConventionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.EventConventionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.FormsConventionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.FrameworkVersionConsistencyTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.HandlerConventionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.HandlerResultConventionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.ImmutabilityTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.IntegrationEventContractTestsBase.ExpectedContract.get -> System.Collections.Generic.IReadOnlyList! +abstract MMCA.Common.Testing.Architecture.IntegrationEventContractTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.LocalizationResourceTestsBase.RequiredCultures.get -> System.Collections.Generic.IReadOnlyCollection! +abstract MMCA.Common.Testing.Architecture.LocalizedTextConventionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.MicroserviceExtractionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.ModuleConformanceTestsBase.ExpectedName.get -> string! +abstract MMCA.Common.Testing.Architecture.ModuleIsolationTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.NamespaceCycleTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.NamingConventionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.PiiConventionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.RawQueryableConventionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.RouteAuthorizationTestsBase.IsGovernedPage(System.Type! pageType) -> bool +abstract MMCA.Common.Testing.Architecture.RouteAuthorizationTestsBase.RequiredRole.get -> string! +abstract MMCA.Common.Testing.Architecture.RouteAuthorizationTestsBase.TargetAssembly.get -> System.Reflection.Assembly! +abstract MMCA.Common.Testing.Architecture.SharedLayerTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.SliceCohesionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.SpecificationConventionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.StateManagementConventionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +abstract MMCA.Common.Testing.Architecture.UIArchitectureConventionTestsBase.Map.get -> MMCA.Common.Testing.Architecture.IArchitectureMap! +override MMCA.Common.Testing.Architecture.LayerRef.Equals(object? obj) -> bool +override MMCA.Common.Testing.Architecture.LayerRef.GetHashCode() -> int +override MMCA.Common.Testing.Architecture.LayerRef.ToString() -> string! +static MMCA.Common.Testing.Architecture.ArchitectureAssert.NoViolations(NetArchTest.Rules.TestResult! result, string! reason) -> void +static MMCA.Common.Testing.Architecture.ArchitectureAssert.NoViolations(System.Collections.Generic.IEnumerable! violations, string! reason) -> void +static MMCA.Common.Testing.Architecture.ArchitectureMapBase.FindRepoRoot(string! solutionFileName) -> string! +static MMCA.Common.Testing.Architecture.ArchitectureMapBase.Framework(MMCA.Common.Testing.Architecture.Layer layer, System.Reflection.Assembly! assembly) -> MMCA.Common.Testing.Architecture.LayerRef! +static MMCA.Common.Testing.Architecture.ArchitectureRules.AggregateRootsHaveNoPublicConstructors(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.AggregateRootsHaveResultFactory(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ApplicationDoesNotDependOnApi(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ApplicationDoesNotDependOnAspNetCore(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ApplicationDoesNotDependOnEntityFrameworkCore(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ApplicationDoesNotDependOnInfrastructure(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ApplicationLayersDeclareHandlers(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ApplicationServicesDoNotInjectHandlers(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ApplicationServicesRespectConstructorArity(MMCA.Common.Testing.Architecture.IArchitectureMap! map, int maxParameters = 8) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.AsyncMethodsDeclareTrailingCancellationToken(MMCA.Common.Testing.Architecture.IArchitectureMap! map, System.Collections.Generic.IReadOnlyCollection? exemptMethods = null) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.BuildIntegrationEventContract(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> System.Collections.Generic.List! +static MMCA.Common.Testing.Architecture.ArchitectureRules.CommandHandlersReturnResult(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.CommandsAndQueriesAreImmutable(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.CommandsHaveCommandOrRequestSuffix(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ControllersAreSealed(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ControllersDoNotDependOnEntityFrameworkCore(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ControllersDoNotDependOnInfrastructure(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ControllersInheritApiControllerBase(MMCA.Common.Testing.Architecture.IArchitectureMap! map, System.Collections.Generic.IEnumerable? exemptFullNames = null) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.DomainAggregateRootsHaveNoPublicConstructors(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.DomainDoesNotDependOnApi(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.DomainDoesNotDependOnApplication(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.DomainDoesNotDependOnInfrastructure(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.DomainEntitiesAreSealed(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.DomainEventHandlersResideInApplicationAndSealed(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.DomainEventsAreImmutable(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.DomainEventsAreSealedInDomainEventsNamespace(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.DomainExposesAggregateRoots(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.DomainFactoriesReturnResult(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.DomainIsFrameworkFree(MMCA.Common.Testing.Architecture.IArchitectureMap! map, System.Collections.Generic.IEnumerable? extra = null) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.DtosAndRequestsAreNotInDomainOrInfrastructure(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.DtosAreImmutable(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.EfConfigurationsAreSealedWithConfigurationSuffix(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.EntitiesResideInDomainLayer(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.EntitiesWithPiiImplementAnonymizable(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.HandlersAreCoLocatedWithTheirContracts(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.HandlersAreSealedWithHandlerSuffix(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.HandlersDoNotInjectOtherHandlers(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.HandlersResideInApplicationLayer(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.InfrastructureDoesNotDependOnApi(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.IntegrationEventsAreImmutable(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.IntegrationEventsDeclareSchemaVersion(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.IntegrationEventsInheritBaseIntegrationEvent(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.IntegrationEventsResideInSharedIntegrationEventsNamespace(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.InvariantClassesAreStatic(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.LayerMapDeclaresLayers(MMCA.Common.Testing.Architecture.IArchitectureMap! map, System.Collections.Generic.IEnumerable! required) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ModuleApisAreIsolated(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ModuleApplicationsAreIsolated(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ModuleApplicationsDoNotReachOtherInfrastructures(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ModuleDomainsAreIsolated(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ModuleDomainsDoNotReachOtherInfrastructures(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ModuleInfrastructuresAreIsolated(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ModuleSharedAreIsolated(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ModuleSharedDoesNotDependOnOwnInternalLayers(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ModuleSharedIsFrameworkFree(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ModulesDeclareLayers(MMCA.Common.Testing.Architecture.IArchitectureMap! map, System.Collections.Generic.IEnumerable! required) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.NamespacesHaveNoDependencyCycles(MMCA.Common.Testing.Architecture.IArchitectureMap! map, System.Collections.Generic.IReadOnlyCollection? allowedCycleNamespaces = null) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.PinnedPackageMajorBelow(string! packageId, int exclusiveMajorCeiling, string! reason) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.QueriesHaveQuerySuffix(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.QueryHandlersReturnResult(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.RepositoriesHaveRepositorySuffix(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ResourceTranslationsAreComplete(System.Collections.Generic.IReadOnlyCollection! requiredCultures, int minimumBaseResources = 0) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.SharedDoesNotDependOnApi(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.SharedDoesNotDependOnApplication(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.SharedDoesNotDependOnDomain(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.SharedDoesNotDependOnInfrastructure(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.SharedDtosHaveDtoOrLookupSuffix(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.SharedIsFrameworkFree(MMCA.Common.Testing.Architecture.IArchitectureMap! map, System.Collections.Generic.IEnumerable? extra = null) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.SpecificationsAreSealedWithSpecificationSuffix(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.SpecificationsDoNotNavigateToOtherEntities(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.TransportDoesNotLeakIntoCoreLayers(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.UiDoesNotDependOnApplication(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.UiDoesNotDependOnDomain(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.UiDoesNotDependOnInfrastructure(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.UpdateRequestsAreConcurrencyAware(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.UserVisibleTextIsLocalized(string! sourceRoot, System.Collections.Generic.IReadOnlyCollection! allowedFileSuffixes, int minimumScannedFiles = 0) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ValidatorsAreCoLocatedWithTheirContracts(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ValidatorsHaveValidatorOrRulesSuffix(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ValidatorsResideInApplicationLayer(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.ArchitectureRules.ValueObjectsAreImmutableSealedInShared(MMCA.Common.Testing.Architecture.IArchitectureMap! map) -> void +static MMCA.Common.Testing.Architecture.LayerRef.operator !=(MMCA.Common.Testing.Architecture.LayerRef? left, MMCA.Common.Testing.Architecture.LayerRef? right) -> bool +static MMCA.Common.Testing.Architecture.LayerRef.operator ==(MMCA.Common.Testing.Architecture.LayerRef? left, MMCA.Common.Testing.Architecture.LayerRef? right) -> bool +static MMCA.Common.Testing.Architecture.RouteAuthorizationTestsBase.HasAuthorizeAttribute(System.Type! type) -> bool +static MMCA.Common.Testing.Architecture.RouteAuthorizationTestsBase.IsRoutablePage(System.Type! type) -> bool +static MMCA.Common.Testing.Architecture.RouteAuthorizationTestsBase.RequiresRole(System.Type! type, string! role) -> bool +static MMCA.Common.Testing.Architecture.RouteAuthorizationTestsBase.Routes(System.Type! type) -> string! +static readonly MMCA.Common.Testing.Architecture.ArchitectureRules.ForbiddenDomainDependencies -> System.Collections.Generic.IReadOnlyList! +static readonly MMCA.Common.Testing.Architecture.ArchitectureRules.TransportDependencies -> System.Collections.Generic.IReadOnlyList! +virtual MMCA.Common.Testing.Architecture.CancellationTokenConventionTestsBase.CancellationTokenExemptMethods.get -> System.Collections.Generic.IReadOnlyList! +virtual MMCA.Common.Testing.Architecture.ControllerConventionTestsBase.ControllersExemptFromApiControllerBase.get -> System.Collections.Generic.IEnumerable! +virtual MMCA.Common.Testing.Architecture.DataResidencyTestsBase.ForbiddenResidencyClaims.get -> System.Collections.Generic.IReadOnlyList! +virtual MMCA.Common.Testing.Architecture.DependencyVersionTestsBase.ImageSharpPackageIds.get -> System.Collections.Generic.IReadOnlyList! +virtual MMCA.Common.Testing.Architecture.DependencyVersionTestsBase.MassTransitPackageIds.get -> System.Collections.Generic.IReadOnlyList! +virtual MMCA.Common.Testing.Architecture.DomainPurityTestsBase.ExtraForbiddenDomainDependencies.get -> System.Collections.Generic.IEnumerable! +virtual MMCA.Common.Testing.Architecture.FormsConventionTestsBase.MinimumCreateForms.get -> int +virtual MMCA.Common.Testing.Architecture.FormsConventionTestsBase.RequiredMarkers.get -> System.Collections.Generic.IReadOnlyList! +virtual MMCA.Common.Testing.Architecture.FrameworkVersionConsistencyTestsBase.MinimumCommonPackageCount.get -> int +virtual MMCA.Common.Testing.Architecture.HandlerConventionTestsBase.MaxServiceConstructorParameters.get -> int +virtual MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.RequiredLayers.get -> System.Collections.Generic.IReadOnlyList! +virtual MMCA.Common.Testing.Architecture.LayerDependencyTestsBase.RequiredModuleLayers.get -> System.Collections.Generic.IReadOnlyList! +virtual MMCA.Common.Testing.Architecture.LocalizationResourceTestsBase.MinimumBaseResources.get -> int +virtual MMCA.Common.Testing.Architecture.LocalizedTextConventionTestsBase.AllowedFiles.get -> System.Collections.Generic.IReadOnlyCollection! +virtual MMCA.Common.Testing.Architecture.LocalizedTextConventionTestsBase.MinimumScannedFiles.get -> int +virtual MMCA.Common.Testing.Architecture.ModuleConformanceTestsBase.AssertDisabledStubs(TModule! module) -> void +virtual MMCA.Common.Testing.Architecture.ModuleConformanceTestsBase.CreateModule() -> TModule! +virtual MMCA.Common.Testing.Architecture.ModuleConformanceTestsBase.ExpectedDependencies.get -> System.Collections.Generic.IReadOnlyCollection! +virtual MMCA.Common.Testing.Architecture.ModuleConformanceTestsBase.ExpectedRequiresDependencies.get -> bool +virtual MMCA.Common.Testing.Architecture.NamespaceCycleTestsBase.AllowedCycleNamespaces.get -> System.Collections.Generic.IReadOnlyList! +virtual MMCA.Common.Testing.Architecture.ObservabilityConventionTestsBase.BicepResource.get -> string! +virtual MMCA.Common.Testing.Architecture.ObservabilityConventionTestsBase.MinimumAlertSpecs.get -> int +virtual MMCA.Common.Testing.Architecture.ObservabilityConventionTestsBase.ResourceAssembly.get -> System.Reflection.Assembly! +virtual MMCA.Common.Testing.Architecture.ObservabilityConventionTestsBase.RunbookResource.get -> string! +virtual MMCA.Common.Testing.Architecture.RawQueryableConventionTestsBase.AllowedFiles.get -> System.Collections.Generic.IReadOnlyList! +virtual MMCA.Common.Testing.Architecture.RawQueryableConventionTestsBase.ApplicationSourceDirectories() -> System.Collections.Generic.IEnumerable! +virtual MMCA.Common.Testing.Architecture.RouteAuthorizationTestsBase.MinimumGovernedPages.get -> int +virtual MMCA.Common.Testing.Architecture.StateManagementConventionTestsBase.AllowedStaticMembers.get -> System.Collections.Generic.IReadOnlyList! +virtual MMCA.Common.Testing.Architecture.UIArchitectureConventionTestsBase.ExcludedPathFragments.get -> System.Collections.Generic.IReadOnlyList! +virtual MMCA.Common.Testing.Architecture.UIArchitectureConventionTestsBase.MaxCodeBehindLines.get -> int +virtual MMCA.Common.Testing.Architecture.UIArchitectureConventionTestsBase.MaxInlineCodeLines.get -> int +virtual MMCA.Common.Testing.Architecture.UIArchitectureConventionTestsBase.MinimumCodeBehindFiles.get -> int diff --git a/Source/Hosting/MMCA.Common.Testing.Architecture/PublicAPI.Unshipped.txt b/Source/Hosting/MMCA.Common.Testing.Architecture/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/Source/Hosting/MMCA.Common.Testing.Architecture/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/Source/Hosting/MMCA.Common.Testing.Architecture/packages.lock.json b/Source/Hosting/MMCA.Common.Testing.Architecture/packages.lock.json index b133806c..a311c170 100644 --- a/Source/Hosting/MMCA.Common.Testing.Architecture/packages.lock.json +++ b/Source/Hosting/MMCA.Common.Testing.Architecture/packages.lock.json @@ -14,6 +14,12 @@ "resolved": "3.0.163", "contentHash": "l9iZ+OXBGlAFEKH1y4BSKPXHcmpJmikysSm0brjTau8fR7Qm7IVGLVig+dh0RH9mq4s3neZayuPQrHRweOpxxw==" }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[5.6.0, )", + "resolved": "5.6.0", + "contentHash": "W4kJGezNIKLzo0Ak5FAQDFvkMf2U7DtGL4THmHyRSApfKsKt5V+eX/bU0ZLKAt/uf9Bb2o1bi0YDKj/GRB/vYQ==" + }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", "requested": "[18.7.23, )", diff --git a/Source/Hosting/MMCA.Common.Testing.E2E/PublicAPI.Shipped.txt b/Source/Hosting/MMCA.Common.Testing.E2E/PublicAPI.Shipped.txt new file mode 100644 index 00000000..0e329cde --- /dev/null +++ b/Source/Hosting/MMCA.Common.Testing.E2E/PublicAPI.Shipped.txt @@ -0,0 +1,224 @@ +#nullable enable +MMCA.Common.Testing.E2E.Infrastructure.AccessibilityViolationException +MMCA.Common.Testing.E2E.Infrastructure.AccessibilityViolationException.AccessibilityViolationException() -> void +MMCA.Common.Testing.E2E.Infrastructure.AccessibilityViolationException.AccessibilityViolationException(string! message) -> void +MMCA.Common.Testing.E2E.Infrastructure.AccessibilityViolationException.AccessibilityViolationException(string! message, System.Exception! innerException) -> void +MMCA.Common.Testing.E2E.Infrastructure.AxeOptions +MMCA.Common.Testing.E2E.Infrastructure.E2ETestBase +MMCA.Common.Testing.E2E.Infrastructure.E2ETestBase.DisposeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.Testing.E2E.Infrastructure.E2ETestBase.E2ETestBase(MMCA.Common.Testing.E2E.Infrastructure.PlaywrightFixture! fixture) -> void +MMCA.Common.Testing.E2E.Infrastructure.E2ETestBase.InitializeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.Testing.E2E.Infrastructure.E2ETestBase.LoginAsAdminAsync() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Infrastructure.E2ETestBase.LoginAsUserAsync() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Infrastructure.E2ETestBase.LoginAsync(string! email, string! password) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Infrastructure.E2ETestBase.NavigateAndWaitAsync(string! path) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Infrastructure.E2ETestBase.Page.get -> Microsoft.Playwright.IPage! +MMCA.Common.Testing.E2E.Infrastructure.E2ETestBase.RegisterNewUserAsync(string? firstName = null, string? lastName = null) -> System.Threading.Tasks.Task<(string! Email, string! Password)>! +MMCA.Common.Testing.E2E.Infrastructure.E2ETestBase.ScanAsync() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Infrastructure.E2ETestBase.ScanGridAsync() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Infrastructure.E2ETestCollection +MMCA.Common.Testing.E2E.Infrastructure.E2ETestCollection.E2ETestCollection() -> void +MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration +MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.AdminCredentials +MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.UserCredentials +MMCA.Common.Testing.E2E.Infrastructure.PageExtensions +MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.extension(Microsoft.Playwright.ILocator!) +MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.extension(Microsoft.Playwright.ILocator!).ClickAndVerifyAsync(Microsoft.Playwright.ILocator! expected, float timeout = 15000) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.extension(Microsoft.Playwright.ILocator!).ClickAndWaitForUrlAsync(Microsoft.Playwright.IPage! page, string! urlPattern) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.extension(Microsoft.Playwright.ILocator!).FillAndVerifyAsync(string! value, float timeout = 10000) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.extension(Microsoft.Playwright.IPage!) +MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.extension(Microsoft.Playwright.IPage!).AssertNoAccessibilityViolationsAsync(Deque.AxeCore.Commons.AxeRunOptions? options = null) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.extension(Microsoft.Playwright.IPage!).BlazorNavigateAsync(string! path) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.extension(Microsoft.Playwright.IPage!).GotoAndWaitForBlazorAsync(string! path) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.extension(Microsoft.Playwright.IPage!).GotoProtectedAsync(string! path) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.extension(Microsoft.Playwright.IPage!).WaitForBlazorAsync(float timeout = 30000) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.extension(Microsoft.Playwright.IPage!).WaitForPageAndBlazorAsync() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Infrastructure.PlaywrightFixture +MMCA.Common.Testing.E2E.Infrastructure.PlaywrightFixture.Browser.get -> Microsoft.Playwright.IBrowser! +MMCA.Common.Testing.E2E.Infrastructure.PlaywrightFixture.DisposeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.Testing.E2E.Infrastructure.PlaywrightFixture.InitializeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.Testing.E2E.Infrastructure.PlaywrightFixture.Playwright.get -> Microsoft.Playwright.IPlaywright! +MMCA.Common.Testing.E2E.Infrastructure.PlaywrightFixture.PlaywrightFixture() -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact.$() -> MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact! +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact.Deconstruct(out string! Label, out string! Path, out MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample! Vitals) -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact.Equals(MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact? other) -> bool +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact.Label.get -> string! +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact.Label.init -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact.Path.get -> string! +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact.Path.init -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact.Vitals.get -> MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample! +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact.Vitals.init -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact.WebVitalsArtifact(string! Label, string! Path, MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample! Vitals) -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.$() -> MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget! +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.AssertWithinBudget(MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample! sample, string! label, string! path, System.Action? writeLine = null) -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.Cls.get -> double +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.Cls.init -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.Deconstruct(out double Lcp, out double Fcp, out double Ttfb, out double Cls, out double Inp) -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.Equals(MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget? other) -> bool +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.Fcp.get -> double +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.Fcp.init -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.Inp.get -> double +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.Inp.init -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.Lcp.get -> double +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.Lcp.init -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.Ttfb.get -> double +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.Ttfb.init -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.WebVitalsBudget(double Lcp = 2500, double Fcp = 1800, double Ttfb = 800, double Cls = 0.1, double Inp = 500) -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsCollector +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.$() -> MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample! +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.Cls.get -> double +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.Cls.init -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.Equals(MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample? other) -> bool +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.Fcp.get -> double +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.Fcp.init -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.Inp.get -> double +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.Inp.init -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.Lcp.get -> double +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.Lcp.init -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.Ttfb.get -> double +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.Ttfb.init -> void +MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.WebVitalsSample() -> void +MMCA.Common.Testing.E2E.PageObjects.LoginPage +MMCA.Common.Testing.E2E.PageObjects.LoginPage.CreateAccountLink.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.LoginPage.EmailField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.LoginPage.ErrorAlert.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.LoginPage.GotoAsync() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.PageObjects.LoginPage.LoginAsync(string! email, string! password) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.PageObjects.LoginPage.LoginButton.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.LoginPage.LoginPage(Microsoft.Playwright.IPage! page) -> void +MMCA.Common.Testing.E2E.PageObjects.LoginPage.PasswordField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.ProfilePage +MMCA.Common.Testing.E2E.PageObjects.ProfilePage.AddressLine1Field.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.ProfilePage.AddressLine2Field.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.ProfilePage.ChangePasswordButton.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.ProfilePage.CityField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.ProfilePage.ConfirmNewPasswordField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.ProfilePage.CountryField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.ProfilePage.CurrentPasswordField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.ProfilePage.ErrorAlert.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.ProfilePage.FirstNameField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.ProfilePage.GotoAsync() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.PageObjects.ProfilePage.LastNameField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.ProfilePage.NewPasswordField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.ProfilePage.ProfilePage(Microsoft.Playwright.IPage! page) -> void +MMCA.Common.Testing.E2E.PageObjects.ProfilePage.SaveAddressButton.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.ProfilePage.SaveNameButton.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.ProfilePage.StateField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.ProfilePage.ZipCodeField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.RegisterPage +MMCA.Common.Testing.E2E.PageObjects.RegisterPage.AddressLine1Field.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.RegisterPage.AddressPanel.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.RegisterPage.AlreadyHaveAccountLink.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.RegisterPage.CityField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.RegisterPage.ConfirmPasswordField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.RegisterPage.CountryField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.RegisterPage.EmailField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.RegisterPage.ErrorAlert.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.RegisterPage.FirstNameField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.RegisterPage.GotoAsync() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.PageObjects.RegisterPage.LastNameField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.RegisterPage.PasswordField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.RegisterPage.RegisterAsync(string! firstName, string! lastName, string! email, string! password) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.PageObjects.RegisterPage.RegisterButton.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.RegisterPage.RegisterPage(Microsoft.Playwright.IPage! page) -> void +MMCA.Common.Testing.E2E.PageObjects.RegisterPage.StateField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.PageObjects.RegisterPage.ZipCodeField.get -> Microsoft.Playwright.ILocator! +MMCA.Common.Testing.E2E.Workflows.Identity.AuthorizationTestsBase +MMCA.Common.Testing.E2E.Workflows.Identity.AuthorizationTestsBase.AnonymousUser_ProtectedPages_ShouldRedirectToLogin() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.AuthorizationTestsBase.AnonymousUser_PublicPages_ShouldBeAccessible() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.AuthorizationTestsBase.AuthorizationTestsBase(MMCA.Common.Testing.E2E.Infrastructure.PlaywrightFixture! fixture) -> void +MMCA.Common.Testing.E2E.Workflows.Identity.AuthorizationTestsBase.RegisteredUser_AdminPages_ShouldBeForbidden() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.AuthorizationTestsBase.RegisteredUser_AuthenticatedPage_ShouldBeAccessible() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.LogoutTestsBase +MMCA.Common.Testing.E2E.Workflows.Identity.LogoutTestsBase.LogoutTestsBase(MMCA.Common.Testing.E2E.Infrastructure.PlaywrightFixture! fixture) -> void +MMCA.Common.Testing.E2E.Workflows.Identity.LogoutTestsBase.Logout_ShouldPreventAccessToProtectedPages() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.LogoutTestsBase.Logout_ShouldRedirectToLoginPage() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.ProfileManagementTestsBase +MMCA.Common.Testing.E2E.Workflows.Identity.ProfileManagementTestsBase.ChangeAddress_ShouldUpdateProfileAddress() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.ProfileManagementTestsBase.ChangeEmail_ShouldUpdateEmail() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.ProfileManagementTestsBase.ChangeName_ShouldUpdateProfileName() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.ProfileManagementTestsBase.ChangePassword_WithValidCurrentPassword_ShouldSucceed() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.ProfileManagementTestsBase.ProfileManagementTestsBase(MMCA.Common.Testing.E2E.Infrastructure.PlaywrightFixture! fixture) -> void +MMCA.Common.Testing.E2E.Workflows.Identity.ProfileManagementTestsBase.ProfilePage_ShouldHaveNoAccessibilityViolations() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.ProfileManagementTestsBase.ProfilePage_ShouldLoadWithUserData() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.UserLoginTestsBase +MMCA.Common.Testing.E2E.Workflows.Identity.UserLoginTestsBase.LoginPage_ShouldHaveNoAccessibilityViolations() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.UserLoginTestsBase.Login_NavigateToCreateAccount_ShouldGoToRegisterPage() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.UserLoginTestsBase.Login_WithInvalidPassword_ShouldShowError() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.UserLoginTestsBase.Login_WithValidCredentials_ShouldNavigateToHomePage() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.UserLoginTestsBase.UserLoginTestsBase(MMCA.Common.Testing.E2E.Infrastructure.PlaywrightFixture! fixture) -> void +MMCA.Common.Testing.E2E.Workflows.Identity.UserRegistrationTestsBase +MMCA.Common.Testing.E2E.Workflows.Identity.UserRegistrationTestsBase.RegisterPage_ShouldHaveNoAccessibilityViolations() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.UserRegistrationTestsBase.Register_WithDuplicateEmail_ShouldShowError() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.UserRegistrationTestsBase.Register_WithMismatchedPasswords_ShouldShowError() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.UserRegistrationTestsBase.Register_WithValidData_ShouldNavigateToHomePage() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Identity.UserRegistrationTestsBase.UserRegistrationTestsBase(MMCA.Common.Testing.E2E.Infrastructure.PlaywrightFixture! fixture) -> void +MMCA.Common.Testing.E2E.Workflows.Preferences.UserPreferencesTestsBase +MMCA.Common.Testing.E2E.Workflows.Preferences.UserPreferencesTestsBase.CultureSwitch_ToSpanish_ShouldLocalizeAndPersist() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Preferences.UserPreferencesTestsBase.MobileViewport_CultureAndTheme_ShouldBeReachable() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Preferences.UserPreferencesTestsBase.ThemeToggle_ToDark_ShouldApplyAndPersist() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.E2E.Workflows.Preferences.UserPreferencesTestsBase.UserPreferencesTestsBase(MMCA.Common.Testing.E2E.Infrastructure.PlaywrightFixture! fixture) -> void +abstract MMCA.Common.Testing.E2E.Workflows.Identity.AuthorizationTestsBase.ProtectedPaths.get -> System.Collections.Generic.IReadOnlyList! +abstract MMCA.Common.Testing.E2E.Workflows.Identity.AuthorizationTestsBase.PublicPaths.get -> System.Collections.Generic.IReadOnlyList! +const MMCA.Common.Testing.E2E.Infrastructure.E2ETestCollection.Name = "E2E" -> string! +override MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact.Equals(object? obj) -> bool +override MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact.GetHashCode() -> int +override MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact.ToString() -> string! +override MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.Equals(object? obj) -> bool +override MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.GetHashCode() -> int +override MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.ToString() -> string! +override MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.Equals(object? obj) -> bool +override MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.GetHashCode() -> int +override MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.ToString() -> string! +static MMCA.Common.Testing.E2E.Infrastructure.AxeOptions.Wcag21Aa.get -> Deque.AxeCore.Commons.AxeRunOptions! +static MMCA.Common.Testing.E2E.Infrastructure.AxeOptions.Wcag21AaExceptMudPagerCombobox.get -> Deque.AxeCore.Commons.AxeRunOptions! +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestBase.BaseUrl.get -> string! +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestBase.FillFieldAsync(Microsoft.Playwright.ILocator! field, string! value) -> System.Threading.Tasks.Task! +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestBase.UniqueId() -> string! +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.AdminCredentials.DefaultEmail.get -> string! +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.AdminCredentials.DefaultEmail.set -> void +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.AdminCredentials.DefaultPassword.get -> string! +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.AdminCredentials.DefaultPassword.set -> void +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.AdminCredentials.Email.get -> string! +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.AdminCredentials.Password.get -> string! +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.AuthGraceTimeout.get -> float +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.AuthTimeout.get -> float +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.BaseUrl.get -> string! +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.Browser.get -> string! +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.DefaultBaseUrl.get -> string! +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.DefaultBaseUrl.set -> void +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.DefaultTimeout.get -> float +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.Headless.get -> bool +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.SlowMo.get -> float +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.TracePath.get -> string? +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.UserCredentials.DefaultEmail.get -> string! +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.UserCredentials.DefaultEmail.set -> void +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.UserCredentials.DefaultPassword.get -> string! +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.UserCredentials.DefaultPassword.set -> void +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.UserCredentials.Email.get -> string! +static MMCA.Common.Testing.E2E.Infrastructure.E2ETestConfiguration.UserCredentials.Password.get -> string! +static MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.AssertNoAccessibilityViolationsAsync(this Microsoft.Playwright.IPage! page, Deque.AxeCore.Commons.AxeRunOptions? options = null) -> System.Threading.Tasks.Task! +static MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.BlazorNavigateAsync(this Microsoft.Playwright.IPage! page, string! path) -> System.Threading.Tasks.Task! +static MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.ClickAndVerifyAsync(this Microsoft.Playwright.ILocator! locator, Microsoft.Playwright.ILocator! expected, float timeout = 15000) -> System.Threading.Tasks.Task! +static MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.ClickAndWaitForUrlAsync(this Microsoft.Playwright.ILocator! locator, Microsoft.Playwright.IPage! page, string! urlPattern) -> System.Threading.Tasks.Task! +static MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.FillAndVerifyAsync(this Microsoft.Playwright.ILocator! locator, string! value, float timeout = 10000) -> System.Threading.Tasks.Task! +static MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.GotoAndWaitForBlazorAsync(this Microsoft.Playwright.IPage! page, string! path) -> System.Threading.Tasks.Task! +static MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.GotoProtectedAsync(this Microsoft.Playwright.IPage! page, string! path) -> System.Threading.Tasks.Task! +static MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.WaitForBlazorAsync(this Microsoft.Playwright.IPage! page, float timeout = 30000) -> System.Threading.Tasks.Task! +static MMCA.Common.Testing.E2E.Infrastructure.PageExtensions.WaitForPageAndBlazorAsync(this Microsoft.Playwright.IPage! page) -> System.Threading.Tasks.Task! +static MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact.operator !=(MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact? left, MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact? right) -> bool +static MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact.operator ==(MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact? left, MMCA.Common.Testing.E2E.Infrastructure.WebVitalsArtifact? right) -> bool +static MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.Describe(string! label, string! path, MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample! sample) -> string! +static MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.operator !=(MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget? left, MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget? right) -> bool +static MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget.operator ==(MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget? left, MMCA.Common.Testing.E2E.Infrastructure.WebVitalsBudget? right) -> bool +static MMCA.Common.Testing.E2E.Infrastructure.WebVitalsCollector.CollectAsync(Microsoft.Playwright.IPage! page) -> System.Threading.Tasks.Task! +static MMCA.Common.Testing.E2E.Infrastructure.WebVitalsCollector.InstallAsync(Microsoft.Playwright.IPage! page) -> System.Threading.Tasks.Task! +static MMCA.Common.Testing.E2E.Infrastructure.WebVitalsCollector.WriteArtifactAsync(string! label, string! path, MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample! sample) -> System.Threading.Tasks.Task! +static MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.operator !=(MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample? left, MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample? right) -> bool +static MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample.operator ==(MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample? left, MMCA.Common.Testing.E2E.Infrastructure.WebVitalsSample? right) -> bool +virtual MMCA.Common.Testing.E2E.Workflows.Identity.AuthorizationTestsBase.AdminPaths.get -> System.Collections.Generic.IReadOnlyList! +virtual MMCA.Common.Testing.E2E.Workflows.Identity.AuthorizationTestsBase.AuthenticatedUserPath.get -> string? +virtual MMCA.Common.Testing.E2E.Workflows.Identity.ProfileManagementTestsBase.ProfileSupportsEmailChange.get -> bool diff --git a/Source/Hosting/MMCA.Common.Testing.E2E/PublicAPI.Unshipped.txt b/Source/Hosting/MMCA.Common.Testing.E2E/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/Source/Hosting/MMCA.Common.Testing.E2E/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/Source/Hosting/MMCA.Common.Testing.E2E/packages.lock.json b/Source/Hosting/MMCA.Common.Testing.E2E/packages.lock.json index 2fedbb25..6a5fe11d 100644 --- a/Source/Hosting/MMCA.Common.Testing.E2E/packages.lock.json +++ b/Source/Hosting/MMCA.Common.Testing.E2E/packages.lock.json @@ -34,6 +34,12 @@ "resolved": "3.0.163", "contentHash": "l9iZ+OXBGlAFEKH1y4BSKPXHcmpJmikysSm0brjTau8fR7Qm7IVGLVig+dh0RH9mq4s3neZayuPQrHRweOpxxw==" }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[5.6.0, )", + "resolved": "5.6.0", + "contentHash": "W4kJGezNIKLzo0Ak5FAQDFvkMf2U7DtGL4THmHyRSApfKsKt5V+eX/bU0ZLKAt/uf9Bb2o1bi0YDKj/GRB/vYQ==" + }, "Microsoft.Playwright": { "type": "Direct", "requested": "[1.62.0, )", diff --git a/Source/Hosting/MMCA.Common.Testing.UI/PublicAPI.Shipped.txt b/Source/Hosting/MMCA.Common.Testing.UI/PublicAPI.Shipped.txt new file mode 100644 index 00000000..a57c05f1 --- /dev/null +++ b/Source/Hosting/MMCA.Common.Testing.UI/PublicAPI.Shipped.txt @@ -0,0 +1,110 @@ +#nullable enable +MMCA.Common.Testing.UI.BunitComponentTestBase +MMCA.Common.Testing.UI.BunitComponentTestBase.BunitComponentTestBase() -> void +MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles +MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles.$() -> MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles! +MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles.Deconstruct(out Bunit.IRenderedComponent! Popover, out Bunit.IRenderedComponent! Dialog, out Bunit.IRenderedComponent! Snackbar) -> void +MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles.Dialog.get -> Bunit.IRenderedComponent! +MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles.Dialog.init -> void +MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles.Equals(MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles? other) -> bool +MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles.MudProviderHandles(Bunit.IRenderedComponent! Popover, Bunit.IRenderedComponent! Dialog, Bunit.IRenderedComponent! Snackbar) -> void +MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles.Popover.get -> Bunit.IRenderedComponent! +MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles.Popover.init -> void +MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles.Snackbar.get -> Bunit.IRenderedComponent! +MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles.Snackbar.init -> void +MMCA.Common.Testing.UI.BunitComponentTestBase.RenderAs(System.Security.Claims.ClaimsPrincipal! principal, System.Action!>! parameters) -> Bunit.IRenderedComponent! +MMCA.Common.Testing.UI.BunitComponentTestBase.RenderMudProviders() -> MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles! +MMCA.Common.Testing.UI.BunitComponentTestBase.RenderUnderTest(System.Action!>! parameters) -> Bunit.IRenderedComponent! +MMCA.Common.Testing.UI.BunitComponentTestBase.SetUser(System.Security.Claims.ClaimsPrincipal! principal) -> void +MMCA.Common.Testing.UI.BunitInteractionExtensions +MMCA.Common.Testing.UI.BunitInteractionExtensions.extension(Bunit.IRenderedComponent!) +MMCA.Common.Testing.UI.BunitInteractionExtensions.extension(Bunit.IRenderedComponent!).ClickButtonByText(string! text) -> void +MMCA.Common.Testing.UI.BunitInteractionExtensions.extension(Bunit.IRenderedComponent!).FindButtonByText(string! text) -> AngleSharp.Dom.IElement! +MMCA.Common.Testing.UI.BunitInteractionExtensions.extension(Bunit.IRenderedComponent!).HasText(string! text) -> bool +MMCA.Common.Testing.UI.CapturedRequest +MMCA.Common.Testing.UI.CapturedRequest.$() -> MMCA.Common.Testing.UI.CapturedRequest! +MMCA.Common.Testing.UI.CapturedRequest.Authorization.get -> string? +MMCA.Common.Testing.UI.CapturedRequest.Authorization.init -> void +MMCA.Common.Testing.UI.CapturedRequest.Body.get -> string? +MMCA.Common.Testing.UI.CapturedRequest.Body.init -> void +MMCA.Common.Testing.UI.CapturedRequest.CapturedRequest(System.Net.Http.HttpMethod! Method, System.Uri? Uri, string! Path, string! PathAndQuery, string? Authorization, string? Body) -> void +MMCA.Common.Testing.UI.CapturedRequest.Deconstruct(out System.Net.Http.HttpMethod! Method, out System.Uri? Uri, out string! Path, out string! PathAndQuery, out string? Authorization, out string? Body) -> void +MMCA.Common.Testing.UI.CapturedRequest.Equals(MMCA.Common.Testing.UI.CapturedRequest? other) -> bool +MMCA.Common.Testing.UI.CapturedRequest.Method.get -> System.Net.Http.HttpMethod! +MMCA.Common.Testing.UI.CapturedRequest.Method.init -> void +MMCA.Common.Testing.UI.CapturedRequest.Path.get -> string! +MMCA.Common.Testing.UI.CapturedRequest.Path.init -> void +MMCA.Common.Testing.UI.CapturedRequest.PathAndQuery.get -> string! +MMCA.Common.Testing.UI.CapturedRequest.PathAndQuery.init -> void +MMCA.Common.Testing.UI.CapturedRequest.Uri.get -> System.Uri? +MMCA.Common.Testing.UI.CapturedRequest.Uri.init -> void +MMCA.Common.Testing.UI.CapturingHttpMessageHandler +MMCA.Common.Testing.UI.CapturingHttpMessageHandler.CapturingHttpMessageHandler() -> void +MMCA.Common.Testing.UI.CapturingHttpMessageHandler.CapturingHttpMessageHandler(System.Func! respond) -> void +MMCA.Common.Testing.UI.CapturingHttpMessageHandler.Requests.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Testing.UI.CapturingHttpMessageHandler.RequestsFor(System.Net.Http.HttpMethod! method, string! absolutePath) -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Testing.UI.CapturingHttpMessageHandler.SetResponse(System.Net.Http.HttpMethod! method, string! absolutePath, System.Net.HttpStatusCode statusCode, object? body = null) -> void +MMCA.Common.Testing.UI.FreshApiClientFactory +MMCA.Common.Testing.UI.FreshApiClientFactory.CreateClient(string! name) -> System.Net.Http.HttpClient! +MMCA.Common.Testing.UI.FreshApiClientFactory.FreshApiClientFactory(System.Net.Http.HttpMessageHandler! handler, System.Uri! baseAddress) -> void +MMCA.Common.Testing.UI.HttpTestDoubles +MMCA.Common.Testing.UI.MarkupSnapshot +MMCA.Common.Testing.UI.MarkupSnapshotResult +MMCA.Common.Testing.UI.MarkupSnapshotResult.Deconstruct(out bool IsMatch, out string! Message) -> void +MMCA.Common.Testing.UI.MarkupSnapshotResult.Equals(MMCA.Common.Testing.UI.MarkupSnapshotResult other) -> bool +MMCA.Common.Testing.UI.MarkupSnapshotResult.IsMatch.get -> bool +MMCA.Common.Testing.UI.MarkupSnapshotResult.IsMatch.init -> void +MMCA.Common.Testing.UI.MarkupSnapshotResult.MarkupSnapshotResult() -> void +MMCA.Common.Testing.UI.MarkupSnapshotResult.MarkupSnapshotResult(bool IsMatch, string! Message) -> void +MMCA.Common.Testing.UI.MarkupSnapshotResult.Message.get -> string! +MMCA.Common.Testing.UI.MarkupSnapshotResult.Message.init -> void +MMCA.Common.Testing.UI.StubTokenStorageService +MMCA.Common.Testing.UI.StubTokenStorageService.AccessToken.get -> string? +MMCA.Common.Testing.UI.StubTokenStorageService.AccessToken.set -> void +MMCA.Common.Testing.UI.StubTokenStorageService.AccessTokenProvider.get -> System.Func!>! +MMCA.Common.Testing.UI.StubTokenStorageService.AccessTokenProvider.set -> void +MMCA.Common.Testing.UI.StubTokenStorageService.ClearTokensAsync() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.UI.StubTokenStorageService.GetAccessTokenAsync() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.UI.StubTokenStorageService.GetRefreshTokenAsync() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.UI.StubTokenStorageService.RefreshToken.get -> string? +MMCA.Common.Testing.UI.StubTokenStorageService.RefreshToken.set -> void +MMCA.Common.Testing.UI.StubTokenStorageService.SetTokensAsync(string! accessToken, string! refreshToken) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.UI.StubTokenStorageService.StubTokenStorageService(string? accessToken = "test-token", string? refreshToken = "test-refresh-token") -> void +MMCA.Common.Testing.UI.TestPrincipal +MMCA.Common.Testing.UI.UiHttpServiceHarness +MMCA.Common.Testing.UI.UiHttpServiceHarness.BaseAddress.get -> System.Uri! +MMCA.Common.Testing.UI.UiHttpServiceHarness.ClientFactory.get -> System.Net.Http.IHttpClientFactory! +MMCA.Common.Testing.UI.UiHttpServiceHarness.Dispose() -> void +MMCA.Common.Testing.UI.UiHttpServiceHarness.Handler.get -> MMCA.Common.Testing.UI.CapturingHttpMessageHandler! +MMCA.Common.Testing.UI.UiHttpServiceHarness.TokenStorage.get -> MMCA.Common.Testing.UI.StubTokenStorageService! +MMCA.Common.Testing.UI.UiHttpServiceHarness.UiHttpServiceHarness(System.Func! respond, string? accessToken = "test-token", System.Uri? baseAddress = null) -> void +MMCA.Common.Testing.UI.UiHttpServiceHarness.UiHttpServiceHarness(string? accessToken = "test-token", System.Uri? baseAddress = null) -> void +override MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles.Equals(object? obj) -> bool +override MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles.GetHashCode() -> int +override MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles.ToString() -> string! +override MMCA.Common.Testing.UI.CapturedRequest.Equals(object? obj) -> bool +override MMCA.Common.Testing.UI.CapturedRequest.GetHashCode() -> int +override MMCA.Common.Testing.UI.CapturedRequest.ToString() -> string! +override MMCA.Common.Testing.UI.MarkupSnapshotResult.GetHashCode() -> int +static MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles.operator !=(MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles? left, MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles? right) -> bool +static MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles.operator ==(MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles? left, MMCA.Common.Testing.UI.BunitComponentTestBase.MudProviderHandles? right) -> bool +static MMCA.Common.Testing.UI.BunitInteractionExtensions.ClickButtonByText(this Bunit.IRenderedComponent! cut, string! text) -> void +static MMCA.Common.Testing.UI.BunitInteractionExtensions.FindButtonByText(this Bunit.IRenderedComponent! cut, string! text) -> AngleSharp.Dom.IElement! +static MMCA.Common.Testing.UI.BunitInteractionExtensions.HasText(this Bunit.IRenderedComponent! cut, string! text) -> bool +static MMCA.Common.Testing.UI.CapturedRequest.operator !=(MMCA.Common.Testing.UI.CapturedRequest? left, MMCA.Common.Testing.UI.CapturedRequest? right) -> bool +static MMCA.Common.Testing.UI.CapturedRequest.operator ==(MMCA.Common.Testing.UI.CapturedRequest? left, MMCA.Common.Testing.UI.CapturedRequest? right) -> bool +static MMCA.Common.Testing.UI.HttpTestDoubles.ClientFactory(System.Net.Http.HttpMessageHandler! handler, System.Uri? baseAddress = null) -> System.Net.Http.IHttpClientFactory! +static MMCA.Common.Testing.UI.HttpTestDoubles.EmptyResponse(System.Net.HttpStatusCode statusCode = System.Net.HttpStatusCode.NoContent) -> System.Net.Http.HttpResponseMessage! +static MMCA.Common.Testing.UI.HttpTestDoubles.JsonResponse(T payload, System.Net.HttpStatusCode statusCode = System.Net.HttpStatusCode.OK) -> System.Net.Http.HttpResponseMessage! +static MMCA.Common.Testing.UI.HttpTestDoubles.ProblemResponse(string! detail, string! title = "Domain Exception", System.Net.HttpStatusCode statusCode = System.Net.HttpStatusCode.BadRequest) -> System.Net.Http.HttpResponseMessage! +static MMCA.Common.Testing.UI.HttpTestDoubles.TokenStorage(string? accessToken = "test-token") -> MMCA.Common.UI.Services.Auth.ITokenStorageService! +static MMCA.Common.Testing.UI.MarkupSnapshot.Match(string! markup, string! snapshotName, string! callerFilePath = "") -> MMCA.Common.Testing.UI.MarkupSnapshotResult +static MMCA.Common.Testing.UI.MarkupSnapshotResult.operator !=(MMCA.Common.Testing.UI.MarkupSnapshotResult left, MMCA.Common.Testing.UI.MarkupSnapshotResult right) -> bool +static MMCA.Common.Testing.UI.MarkupSnapshotResult.operator ==(MMCA.Common.Testing.UI.MarkupSnapshotResult left, MMCA.Common.Testing.UI.MarkupSnapshotResult right) -> bool +static MMCA.Common.Testing.UI.TestPrincipal.AuthenticatedUser(string! userId = "1", string! name = "Test User", params string![]! roles) -> System.Security.Claims.ClaimsPrincipal! +static MMCA.Common.Testing.UI.TestPrincipal.Organizer(string! userId = "1") -> System.Security.Claims.ClaimsPrincipal! +static readonly MMCA.Common.Testing.UI.BunitComponentTestBase.Anonymous -> System.Security.Claims.ClaimsPrincipal! +static readonly MMCA.Common.Testing.UI.HttpTestDoubles.BaseAddress -> System.Uri! +static readonly MMCA.Common.Testing.UI.UiHttpServiceHarness.DefaultBaseAddress -> System.Uri! +~override MMCA.Common.Testing.UI.MarkupSnapshotResult.Equals(object obj) -> bool +~override MMCA.Common.Testing.UI.MarkupSnapshotResult.ToString() -> string diff --git a/Source/Hosting/MMCA.Common.Testing.UI/PublicAPI.Unshipped.txt b/Source/Hosting/MMCA.Common.Testing.UI/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/Source/Hosting/MMCA.Common.Testing.UI/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/Source/Hosting/MMCA.Common.Testing.UI/packages.lock.json b/Source/Hosting/MMCA.Common.Testing.UI/packages.lock.json index 49256b76..44d91206 100644 --- a/Source/Hosting/MMCA.Common.Testing.UI/packages.lock.json +++ b/Source/Hosting/MMCA.Common.Testing.UI/packages.lock.json @@ -27,6 +27,12 @@ "resolved": "3.0.163", "contentHash": "l9iZ+OXBGlAFEKH1y4BSKPXHcmpJmikysSm0brjTau8fR7Qm7IVGLVig+dh0RH9mq4s3neZayuPQrHRweOpxxw==" }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[5.6.0, )", + "resolved": "5.6.0", + "contentHash": "W4kJGezNIKLzo0Ak5FAQDFvkMf2U7DtGL4THmHyRSApfKsKt5V+eX/bU0ZLKAt/uf9Bb2o1bi0YDKj/GRB/vYQ==" + }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", "requested": "[18.7.23, )", diff --git a/Source/Hosting/MMCA.Common.Testing/PublicAPI.Shipped.txt b/Source/Hosting/MMCA.Common.Testing/PublicAPI.Shipped.txt new file mode 100644 index 00000000..1e0a2c18 --- /dev/null +++ b/Source/Hosting/MMCA.Common.Testing/PublicAPI.Shipped.txt @@ -0,0 +1,126 @@ +#nullable enable +MMCA.Common.Testing.Builders.EntityBuilderBase +MMCA.Common.Testing.Builders.EntityBuilderBase.EntityBuilderBase() -> void +MMCA.Common.Testing.CrossServiceDataSource +MMCA.Common.Testing.CrossServiceDataSource.$() -> MMCA.Common.Testing.CrossServiceDataSource! +MMCA.Common.Testing.CrossServiceDataSource.CrossServiceDataSource(string! LogicalName, string! DatabaseName) -> void +MMCA.Common.Testing.CrossServiceDataSource.DatabaseName.get -> string! +MMCA.Common.Testing.CrossServiceDataSource.DatabaseName.init -> void +MMCA.Common.Testing.CrossServiceDataSource.Deconstruct(out string! LogicalName, out string! DatabaseName) -> void +MMCA.Common.Testing.CrossServiceDataSource.Equals(MMCA.Common.Testing.CrossServiceDataSource? other) -> bool +MMCA.Common.Testing.CrossServiceDataSource.LogicalName.get -> string! +MMCA.Common.Testing.CrossServiceDataSource.LogicalName.init -> void +MMCA.Common.Testing.CrossServiceFixtureBase +MMCA.Common.Testing.CrossServiceFixtureBase.BuildConnectionString(string! databaseName) -> string! +MMCA.Common.Testing.CrossServiceFixtureBase.CrossServiceFixtureBase() -> void +MMCA.Common.Testing.CrossServiceFixtureBase.DisposeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.Testing.CrossServiceFixtureBase.InitializeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.Testing.CrossServiceFixtureBase.RabbitMqConnectionString.get -> string! +MMCA.Common.Testing.CrossServiceFixtureBase.SetEnvironmentVariable(string! key, string? value) -> void +MMCA.Common.Testing.CrossServiceFixtureBase.SetHostConnectionString(string! connectionString) -> void +MMCA.Common.Testing.CrossServiceFixtureBase.SqlServerBaseConnectionString.get -> string! +MMCA.Common.Testing.DecoratorPipelineOrderTestsBase +MMCA.Common.Testing.DecoratorPipelineOrderTestsBase.CommandPipeline_NestsDecorators_InAdr014Order() -> void +MMCA.Common.Testing.DecoratorPipelineOrderTestsBase.DecoratorPipelineOrderTestsBase() -> void +MMCA.Common.Testing.DecoratorPipelineOrderTestsBase.QueryPipeline_NestsDecorators_InAdr014Order() -> void +MMCA.Common.Testing.DependencyInjectionAssert +MMCA.Common.Testing.FeatureManagementTestExtensions +MMCA.Common.Testing.FeatureManagementTestExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!) +MMCA.Common.Testing.FeatureManagementTestExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).ConfigureTestFeatureFlags(System.Collections.Generic.Dictionary! features) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Testing.GracefulShutdownTestsBase +MMCA.Common.Testing.GracefulShutdownTestsBase.GracefulShutdownTestsBase() -> void +MMCA.Common.Testing.GracefulShutdownTestsBase.Host_StopsGracefully_FiringLifetimeEventsWithinTimeout() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.HandlerTestBase +MMCA.Common.Testing.HandlerTestBase.HandlerTestBase() -> void +MMCA.Common.Testing.HandlerTestBase.Logger.get -> Microsoft.Extensions.Logging.ILogger! +MMCA.Common.Testing.HandlerTestBase.RegisterReadRepository() -> Moq.Mock!>! +MMCA.Common.Testing.HandlerTestBase.RegisterRepository() -> Moq.Mock!>! +MMCA.Common.Testing.HandlerTestBase.UnitOfWork.get -> Moq.Mock! +MMCA.Common.Testing.IIntegrationTestFixture +MMCA.Common.Testing.IIntegrationTestFixture.CreateClient() -> System.Net.Http.HttpClient! +MMCA.Common.Testing.IIntegrationTestFixture.ResetDatabaseAsync() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.IntegrationTestBase +MMCA.Common.Testing.IntegrationTestBase.ClearAuthentication() -> void +MMCA.Common.Testing.IntegrationTestBase.Client.get -> System.Net.Http.HttpClient! +MMCA.Common.Testing.IntegrationTestBase.DeleteAsync(string! url) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.IntegrationTestBase.DisposeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.Testing.IntegrationTestBase.Fixture.get -> TFixture +MMCA.Common.Testing.IntegrationTestBase.GetAsync(string! url) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.IntegrationTestBase.InitializeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.Testing.IntegrationTestBase.IntegrationTestBase(TFixture fixture) -> void +MMCA.Common.Testing.IntegrationTestBase.PostAsync(string! url, T body) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.IntegrationTestBase.PutAsync(string! url) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.IntegrationTestBase.PutAsync(string! url, T body) -> System.Threading.Tasks.Task! +MMCA.Common.Testing.IntegrationTestBase.SetBearerToken(string! token) -> void +MMCA.Common.Testing.JwtTokenGenerator +MMCA.Common.Testing.OpenApiContractTestsBase +MMCA.Common.Testing.OpenApiContractTestsBase.GetOpenApiJsonAsync() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.OpenApiContractTestsBase.OpenApiContractTestsBase(TFixture fixture) -> void +MMCA.Common.Testing.OpenApiContractTestsBase.OpenApiDocument_DescribesEveryCorePublicResource() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.OpenApiContractTestsBase.OpenApiDocument_IsServed_AsWellFormedOpenApiDescribingTheApiSurface() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.ProblemDetailsContractTestsBase +MMCA.Common.Testing.ProblemDetailsContractTestsBase.NotFound_404_HasProblemDetailsShape() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.ProblemDetailsContractTestsBase.ProblemDetailsContractTestsBase(TFixture fixture) -> void +MMCA.Common.Testing.ProblemDetailsContractTestsBase.Validation_400_HasProblemDetailsShape() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.ProductionHostApplicationFactory +MMCA.Common.Testing.ProductionHostApplicationFactory.ProductionHostApplicationFactory() -> void +MMCA.Common.Testing.ProductionHostApplicationFactory.StartedHost.get -> Microsoft.Extensions.Hosting.IHost? +MMCA.Common.Testing.SecurityHeadersTestsBase +MMCA.Common.Testing.SecurityHeadersTestsBase.AliveResponse_CarriesHardenedSecurityHeaders() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.SecurityHeadersTestsBase.SecurityHeadersTestsBase() -> void +MMCA.Common.Testing.ServiceInfoVersioningContractTestsBase +MMCA.Common.Testing.ServiceInfoVersioningContractTestsBase.ServiceInfoVersioningContractTestsBase(TFixture fixture) -> void +MMCA.Common.Testing.ServiceInfoVersioningContractTestsBase.ServiceInfo_V1_ReturnsMinimalShape_AndIsReportedDeprecated() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.ServiceInfoVersioningContractTestsBase.ServiceInfo_V2_ReturnsEvolvedShape_AndIsReportedSupported() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.SqlServerIntegrationTestFixtureBase +MMCA.Common.Testing.SqlServerIntegrationTestFixtureBase.Client.get -> System.Net.Http.HttpClient? +MMCA.Common.Testing.SqlServerIntegrationTestFixtureBase.ConnectionString.get -> string! +MMCA.Common.Testing.SqlServerIntegrationTestFixtureBase.CreateClient() -> System.Net.Http.HttpClient! +MMCA.Common.Testing.SqlServerIntegrationTestFixtureBase.DisposeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.Testing.SqlServerIntegrationTestFixtureBase.InitializeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.Testing.SqlServerIntegrationTestFixtureBase.ResetDatabaseAsync() -> System.Threading.Tasks.Task! +MMCA.Common.Testing.SqlServerIntegrationTestFixtureBase.Services.get -> System.IServiceProvider! +MMCA.Common.Testing.SqlServerIntegrationTestFixtureBase.SqlServerIntegrationTestFixtureBase() -> void +MMCA.Common.Testing.TestPolling +abstract MMCA.Common.Testing.Builders.EntityBuilderBase.Build() -> TEntity +abstract MMCA.Common.Testing.CrossServiceFixtureBase.BootHostsAsync() -> System.Threading.Tasks.ValueTask +abstract MMCA.Common.Testing.CrossServiceFixtureBase.DataSources.get -> System.Collections.Generic.IReadOnlyList! +abstract MMCA.Common.Testing.CrossServiceFixtureBase.DisposeHostsAsync() -> System.Threading.Tasks.ValueTask +abstract MMCA.Common.Testing.CrossServiceFixtureBase.MigrationsAssemblyPrefix.get -> string! +abstract MMCA.Common.Testing.DecoratorPipelineOrderTestsBase.ConfigureServices(Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> void +abstract MMCA.Common.Testing.OpenApiContractTestsBase.CorePublicResources.get -> System.Collections.Generic.IReadOnlyList! +abstract MMCA.Common.Testing.OpenApiContractTestsBase.MinimumPathCount.get -> int +abstract MMCA.Common.Testing.ProblemDetailsContractTestsBase.SendNotFoundProbeAsync() -> System.Threading.Tasks.Task! +abstract MMCA.Common.Testing.ProblemDetailsContractTestsBase.SendValidationErrorProbeAsync() -> System.Threading.Tasks.Task! +abstract MMCA.Common.Testing.SecurityHeadersTestsBase.CreateClient() -> System.Net.Http.HttpClient! +abstract MMCA.Common.Testing.SqlServerIntegrationTestFixtureBase.CreateFactory() -> Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory! +abstract MMCA.Common.Testing.SqlServerIntegrationTestFixtureBase.DatabaseNamePrefix.get -> string! +abstract MMCA.Common.Testing.SqlServerIntegrationTestFixtureBase.SqlBaseEnvironmentVariable.get -> string! +const MMCA.Common.Testing.JwtTokenGenerator.DefaultIssuer = "https://localhost:6001" -> string! +const MMCA.Common.Testing.JwtTokenGenerator.DefaultKeyId = "mmca-test-key" -> string! +const MMCA.Common.Testing.JwtTokenGenerator.DefaultPrivateKeyPem = "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCedtKTDLOuAnty\nz7CUuupqR+sK9s4iIMzj4C3At9FPsiZmQkBBgr50NFTxJd1nVTL2w+PXK8GyEV8A\no5h/ZvSsRyNm9T8meJ5F5w0NiS55yruymVezGtF0oG/3LWVEyBBy7U2jHyuYQMRE\nsSATEulXjJPez0Yo8WiU/5pXtEm1XliZYYGM9KfMCjKSJiUoLDESNFVWqNNDH9ns\ngrFaUMX6XOWv9eeYMKJ7gbm2KGEX5AS/I0GXl7ctCzn98Z8797YUjsVfXB402vX+\nQ9dVwn2U5qR+8um9pZgZhjsj3QoF8eza+oG/AqOHIT9pGIOlDLcU1gHkGR8mu1Ra\n6UGNh3iNAgMBAAECggEAJ3pEtZu5e8VkidLSFAuI8Ndf6AhajEgNo0urOlLRE4C3\nbkxdA7UVy49qBfW/9clU/AMLVQSyqbEIMPmQuVSl6fGDEVhR1jzAeXu5VcDyUic+\noZzwK0+oFN0PZ83oZ71L08XozJWsX67q9o4GBpp0hXohMKYEvZh+zHftNKJWppco\n1G6Lto3ELzlShhbOosoNce1Y8jByY2cc617MO1S/I6tB3NPsiZMRKgC/j3ATWimD\nx9ElTIc3lsT+aopol95pkGsIvfh3tCXeaUuCZ+rWEX4xtVtPpm+f1hjDxZcGHwL4\nDxGZKtTAmkbA9u2beMEhtCYE6fwVKDzD/K4ZF8HzYQKBgQDbZD9t7i3EYTuF4Ej2\nLoDeRIG1g7eXBcHqWWeBZnQa/fqlFnzqkphVOKxzVB9s9HDK0Ku3khe9wJI5XHFx\ntDWUaIMTdAGUIXs9aMPbRz1RKXXuKDGdDlyoNhziy21NI6wJiwQh8ul4mImZ3AzV\n9kcleRwrscRhgVVBONcg6LVBhQKBgQC45+/SSC/Z8mlrhizzHfjm9bsb40o5Lz9b\nDOTJF7y0o1FXaXMSSotKuEo8TE8PastMmcRmArtx69gcmESn+7VglMCOW6m554Jd\nM3T7dEbU9Dl8WOciWphplOkGljOQco4HR9iEQttCDxMmlTyvuyYt0bsLBVy0K636\nRiXrLcgFaQKBgFy1WhBsK67mn66M/of5Ur+aF23KwVPyPOV47kJCNyII3VfRzuuZ\nEwJANq9thvIVwWwTDd0+wQWQULoolE/GJTYXi/w2c0xTca6bjNgmnISljo4bMgv6\nO7FhXIeCCygjwNkvg4mNCpfJbaw0zr/DmID/UqYsMp45dvtet5nSfHW5AoGACD5/\nGNcTGxqNzLd8xZIuiM2n/ARUSNxsbLjUcorWZQ9rDwSqlsQwFbLFzI5yb/OJAO9S\nLGIuzVOAnTXEyeCVI6s+MpqvpJRH4bProVJ73f7NmVe1Zni3lu2Gvj5wKh6Rao2v\nf6YjdHyLlArPW95yQ1S4jDM/AOF5rlD2W9f8tRECgYB4i3qu1eOUP8ND29qpiz3Z\nkX0Kz8MJF/W8SI3EuWR06iOtnEQvUA19GGl+eP31+eeOLEDyJceGGGMHH42ERxq0\nNLZaLd5FHebRvFwxbzD24nYBU8q/WawRmyG/u2TEGZ09jvtzX/XgkrxCFNFQpEoZ\nDuIPM0H4W9l7v/kASh6DKQ==\n-----END PRIVATE KEY-----" -> string! +const MMCA.Common.Testing.JwtTokenGenerator.DefaultPublicKeyPem = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnnbSkwyzrgJ7cs+wlLrq\nakfrCvbOIiDM4+AtwLfRT7ImZkJAQYK+dDRU8SXdZ1Uy9sPj1yvBshFfAKOYf2b0\nrEcjZvU/JnieRecNDYkuecq7splXsxrRdKBv9y1lRMgQcu1Nox8rmEDERLEgExLp\nV4yT3s9GKPFolP+aV7RJtV5YmWGBjPSnzAoykiYlKCwxEjRVVqjTQx/Z7IKxWlDF\n+lzlr/XnmDCie4G5tihhF+QEvyNBl5e3LQs5/fGfO/e2FI7FX1weNNr1/kPXVcJ9\nlOakfvLpvaWYGYY7I90KBfHs2vqBvwKjhyE/aRiDpQy3FNYB5BkfJrtUWulBjYd4\njQIDAQAB\n-----END PUBLIC KEY-----" -> string! +override MMCA.Common.Testing.CrossServiceDataSource.Equals(object? obj) -> bool +override MMCA.Common.Testing.CrossServiceDataSource.GetHashCode() -> int +override MMCA.Common.Testing.CrossServiceDataSource.ToString() -> string! +override MMCA.Common.Testing.ProductionHostApplicationFactory.CreateHost(Microsoft.Extensions.Hosting.IHostBuilder! builder) -> Microsoft.Extensions.Hosting.IHost! +static MMCA.Common.Testing.CrossServiceDataSource.operator !=(MMCA.Common.Testing.CrossServiceDataSource? left, MMCA.Common.Testing.CrossServiceDataSource? right) -> bool +static MMCA.Common.Testing.CrossServiceDataSource.operator ==(MMCA.Common.Testing.CrossServiceDataSource? left, MMCA.Common.Testing.CrossServiceDataSource? right) -> bool +static MMCA.Common.Testing.CrossServiceFixtureBase.ComposeConnectionString(string! baseConnectionString, string! databaseName, string? applicationName = null) -> string! +static MMCA.Common.Testing.DependencyInjectionAssert.ReturnsSameCollection(System.Func! register) -> void +static MMCA.Common.Testing.FeatureManagementTestExtensions.ConfigureTestFeatureFlags(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Collections.Generic.Dictionary! features) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Testing.IntegrationTestBase.NextId() -> int +static MMCA.Common.Testing.JwtTokenGenerator.ConfigureInProcessTokenValidation(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options, string! audience) -> void +static MMCA.Common.Testing.JwtTokenGenerator.GenerateToken(string! audience, int userId, string! role, System.Collections.Generic.IEnumerable? additionalClaims = null, string? privateKeyPem = null, string! issuer = "https://localhost:6001", string! keyId = "mmca-test-key") -> string! +static MMCA.Common.Testing.ProblemDetailsContractTestsBase.AssertProblemDetailsShapeAsync(System.Net.Http.HttpResponseMessage! response, System.Net.HttpStatusCode expected) -> System.Threading.Tasks.Task! +static MMCA.Common.Testing.TestPolling.PollUntilAsync(System.Func!>! probe, System.Func! isSatisfied, System.TimeSpan? timeout = null, System.TimeSpan? interval = null) -> System.Threading.Tasks.Task! +virtual MMCA.Common.Testing.CrossServiceFixtureBase.ConfigureSharedEnvironment(System.Action! setEnvironmentVariable) -> void +virtual MMCA.Common.Testing.CrossServiceFixtureBase.OnContainersStartedAsync() -> System.Threading.Tasks.ValueTask +virtual MMCA.Common.Testing.DecoratorPipelineOrderTestsBase.ExpectedCommandDecorators.get -> System.Collections.Generic.IReadOnlyList! +virtual MMCA.Common.Testing.DecoratorPipelineOrderTestsBase.ExpectedQueryDecorators.get -> System.Collections.Generic.IReadOnlyList! +virtual MMCA.Common.Testing.GracefulShutdownTestsBase.CreateFactory() -> MMCA.Common.Testing.ProductionHostApplicationFactory! +virtual MMCA.Common.Testing.GracefulShutdownTestsBase.ShutdownTimeoutSeconds.get -> int +virtual MMCA.Common.Testing.OpenApiContractTestsBase.MinimumPathCountBecause.get -> string! +virtual MMCA.Common.Testing.OpenApiContractTestsBase.OpenApiDocumentPath.get -> string! +virtual MMCA.Common.Testing.SecurityHeadersTestsBase.ProbePath.get -> string! +virtual MMCA.Common.Testing.SqlServerIntegrationTestFixtureBase.ConfigureTestEnvironment(System.Action! setEnvironmentVariable) -> void diff --git a/Source/Hosting/MMCA.Common.Testing/PublicAPI.Unshipped.txt b/Source/Hosting/MMCA.Common.Testing/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/Source/Hosting/MMCA.Common.Testing/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/Source/Hosting/MMCA.Common.Testing/packages.lock.json b/Source/Hosting/MMCA.Common.Testing/packages.lock.json index 85a941b5..97ce73f9 100644 --- a/Source/Hosting/MMCA.Common.Testing/packages.lock.json +++ b/Source/Hosting/MMCA.Common.Testing/packages.lock.json @@ -33,6 +33,12 @@ "Microsoft.Extensions.DependencyModel": "10.0.11" } }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[5.6.0, )", + "resolved": "5.6.0", + "contentHash": "W4kJGezNIKLzo0Ak5FAQDFvkMf2U7DtGL4THmHyRSApfKsKt5V+eX/bU0ZLKAt/uf9Bb2o1bi0YDKj/GRB/vYQ==" + }, "Microsoft.Data.SqlClient": { "type": "Direct", "requested": "[6.1.6, )", diff --git a/Source/Presentation/MMCA.Common.API/PublicAPI.Shipped.txt b/Source/Presentation/MMCA.Common.API/PublicAPI.Shipped.txt new file mode 100644 index 00000000..f9b9cc3c --- /dev/null +++ b/Source/Presentation/MMCA.Common.API/PublicAPI.Shipped.txt @@ -0,0 +1,449 @@ +#nullable enable +MMCA.Common.API.AssemblyReference +MMCA.Common.API.Authentication.ExternalAuthExtensions +MMCA.Common.API.Authentication.ExternalAuthExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!) +MMCA.Common.API.Authentication.ExternalAuthExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddExternalAuthProviders(Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.Authorization.AllowMissingOwnerAttribute +MMCA.Common.API.Authorization.AllowMissingOwnerAttribute.AllowMissingOwnerAttribute() -> void +MMCA.Common.API.Authorization.AuthorizationExtensions +MMCA.Common.API.Authorization.AuthorizationExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!) +MMCA.Common.API.Authorization.AuthorizationExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddAuthorizationPolicies() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.Authorization.AuthorizationExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddPermissions(System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.Authorization.AuthorizationPolicies +MMCA.Common.API.Authorization.HasPermissionAttribute +MMCA.Common.API.Authorization.HasPermissionAttribute.HasPermissionAttribute(string! permission) -> void +MMCA.Common.API.Authorization.HasPermissionAttribute.Permission.get -> string! +MMCA.Common.API.Authorization.OwnerOrAdminFilter +MMCA.Common.API.Authorization.OwnerOrAdminFilter.OnActionExecutionAsync(Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext! context, Microsoft.AspNetCore.Mvc.Filters.ActionExecutionDelegate! next) -> System.Threading.Tasks.Task! +MMCA.Common.API.Authorization.OwnerOrAdminFilter.OwnerOrAdminFilter(MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService! currentUserService, Microsoft.Extensions.Options.IOptions! options) -> void +MMCA.Common.API.Authorization.OwnerOrAdminFilterOptions +MMCA.Common.API.Authorization.OwnerOrAdminFilterOptions.BypassRole.get -> string! +MMCA.Common.API.Authorization.OwnerOrAdminFilterOptions.BypassRole.set -> void +MMCA.Common.API.Authorization.OwnerOrAdminFilterOptions.OwnerClaimType.get -> string! +MMCA.Common.API.Authorization.OwnerOrAdminFilterOptions.OwnerClaimType.set -> void +MMCA.Common.API.Authorization.OwnerOrAdminFilterOptions.OwnerOrAdminFilterOptions() -> void +MMCA.Common.API.Authorization.OwnerOrAdminFilterOptions.OwnerParameterName.get -> string! +MMCA.Common.API.Authorization.OwnerOrAdminFilterOptions.OwnerParameterName.set -> void +MMCA.Common.API.Authorization.OwnershipHelper +MMCA.Common.API.Authorization.PermissionAuthorizationHandler +MMCA.Common.API.Authorization.PermissionAuthorizationHandler.PermissionAuthorizationHandler(MMCA.Common.Shared.Auth.IPermissionRegistry! permissionRegistry) -> void +MMCA.Common.API.Authorization.PermissionPolicy +MMCA.Common.API.Authorization.PermissionPolicyProvider +MMCA.Common.API.Authorization.PermissionPolicyProvider.GetDefaultPolicyAsync() -> System.Threading.Tasks.Task! +MMCA.Common.API.Authorization.PermissionPolicyProvider.GetFallbackPolicyAsync() -> System.Threading.Tasks.Task! +MMCA.Common.API.Authorization.PermissionPolicyProvider.GetPolicyAsync(string! policyName) -> System.Threading.Tasks.Task! +MMCA.Common.API.Authorization.PermissionPolicyProvider.PermissionPolicyProvider(Microsoft.Extensions.Options.IOptions! options) -> void +MMCA.Common.API.Authorization.PermissionRequirement +MMCA.Common.API.Authorization.PermissionRequirement.Permission.get -> string! +MMCA.Common.API.Authorization.PermissionRequirement.PermissionRequirement(string! permission) -> void +MMCA.Common.API.Caching.OutputCacheOptionsExtensions +MMCA.Common.API.Caching.OutputCacheOptionsExtensions.extension(Microsoft.AspNetCore.OutputCaching.OutputCacheOptions!) +MMCA.Common.API.Caching.OutputCacheOptionsExtensions.extension(Microsoft.AspNetCore.OutputCaching.OutputCacheOptions!).AddPublicEndpointPolicy(string! name, System.TimeSpan expiration, params string![]! tags) -> void +MMCA.Common.API.Caching.OutputCacheOptionsExtensions.extension(Microsoft.AspNetCore.OutputCaching.OutputCacheOptions!).AddPublicEndpointPolicy(string! name, System.TimeSpan expiration, string![]! bypassRoles, params string![]! tags) -> void +MMCA.Common.API.Caching.PublicEndpointOutputCachePolicy +MMCA.Common.API.Caching.PublicEndpointOutputCachePolicy.PublicEndpointOutputCachePolicy(System.TimeSpan expiration, params string![]! tags) -> void +MMCA.Common.API.Caching.PublicEndpointOutputCachePolicy.PublicEndpointOutputCachePolicy(System.TimeSpan expiration, string![]! bypassRoles, string![]! tags) -> void +MMCA.Common.API.ClassReference +MMCA.Common.API.ClassReference.ClassReference() -> void +MMCA.Common.API.Controllers.AggregateRootEntityControllerBase +MMCA.Common.API.Controllers.AggregateRootEntityControllerBase.AggregateRootEntityControllerBase(MMCA.Common.Application.Interfaces.IEntityQueryService! queryService, MMCA.Common.Application.UseCases.ICommandHandler!>! createHandler, MMCA.Common.Application.UseCases.ICommandHandler!, MMCA.Common.Shared.Abstractions.Result!>! deleteHandler, Microsoft.Extensions.Logging.ILogger!>! logger) -> void +MMCA.Common.API.Controllers.AggregateRootEntityControllerBase.CreateHandler.get -> MMCA.Common.Application.UseCases.ICommandHandler!>! +MMCA.Common.API.Controllers.ApiControllerBase +MMCA.Common.API.Controllers.ApiControllerBase.ApiControllerBase() -> void +MMCA.Common.API.Controllers.AuthControllerBase +MMCA.Common.API.Controllers.AuthControllerBase.AuthControllerBase(MMCA.Common.Application.Auth.IAuthenticationService! authenticationService, MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService! currentUserService) -> void +MMCA.Common.API.Controllers.AuthControllerBase.AuthenticationService.get -> MMCA.Common.Application.Auth.IAuthenticationService! +MMCA.Common.API.Controllers.AuthControllerBase.CurrentUserService.get -> MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService! +MMCA.Common.API.Controllers.EntityControllerBase +MMCA.Common.API.Controllers.EntityControllerBase.EntityControllerBase(MMCA.Common.Application.Interfaces.IEntityQueryService! queryService, Microsoft.Extensions.Logging.ILogger!>! logger) -> void +MMCA.Common.API.Controllers.EntityControllerBase.EntityName.get -> string! +MMCA.Common.API.Controllers.EntityControllerBase.Logger.get -> Microsoft.Extensions.Logging.ILogger! +MMCA.Common.API.Controllers.EntityControllerBase.MaxExportRows.get -> int +MMCA.Common.API.Controllers.EntityControllerBase.MaxPageSize.get -> int +MMCA.Common.API.Controllers.EntityControllerBase.QueryService.get -> MMCA.Common.Application.Interfaces.IEntityQueryService! +MMCA.Common.API.Controllers.IAggregateRootEntityControllerBase +MMCA.Common.API.Controllers.IAggregateRootEntityControllerBase.CreateAsync(TCreateRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.API.Controllers.IAggregateRootEntityControllerBase.DeleteAsync(TIdentifierType id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.API.Controllers.IEntityControllerBase +MMCA.Common.API.Controllers.IEntityControllerBase.GetAllAsync(bool includeFKs = false, bool includeChildren = false, string? sortColumn = null, string? sortDirection = null, string? fields = null, int pageNumber = 1, int pageSize = 10, System.Collections.Generic.Dictionary? filters = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>! +MMCA.Common.API.Controllers.IEntityControllerBase.GetAllAsync(string? fields = null, bool includeFKs = false, bool includeChildren = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>! +MMCA.Common.API.Controllers.IEntityControllerBase.GetAllForLookupAsync(string! nameProperty, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>!>! +MMCA.Common.API.Controllers.IEntityControllerBase.GetByIdAsync(TIdentifierType id, bool includeFKs = true, bool includeChildren = false, string? fields = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.API.Controllers.Notifications.DevicesController +MMCA.Common.API.Controllers.Notifications.DevicesController.DeleteAsync(string! installationId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.API.Controllers.Notifications.DevicesController.DevicesController(MMCA.Common.Application.Interfaces.Infrastructure.IPushDeviceRegistrar! registrar, MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService! currentUserService) -> void +MMCA.Common.API.Controllers.Notifications.DevicesController.UpsertAsync(MMCA.Common.Shared.Notifications.PushNotifications.DeviceInstallationRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.API.Controllers.Notifications.InboxController +MMCA.Common.API.Controllers.Notifications.InboxController.GetInboxAsync(int pageNumber = 1, int pageSize = 20, string? scope = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>! +MMCA.Common.API.Controllers.Notifications.InboxController.GetUnreadCountAsync(string? scope = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.API.Controllers.Notifications.InboxController.InboxController(MMCA.Common.Application.UseCases.IQueryHandler!>!>! inboxHandler, MMCA.Common.Application.UseCases.IQueryHandler!>! unreadCountHandler, MMCA.Common.Application.UseCases.ICommandHandler! markReadHandler, MMCA.Common.Application.UseCases.ICommandHandler! markAllReadHandler, MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService! currentUserService) -> void +MMCA.Common.API.Controllers.Notifications.InboxController.MarkAllReadAsync(string? scope = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.API.Controllers.Notifications.InboxController.MarkReadAsync(int id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.API.Controllers.Notifications.NotificationsController +MMCA.Common.API.Controllers.Notifications.NotificationsController.GetHistoryAsync(int pageNumber = 1, int pageSize = 10, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>! +MMCA.Common.API.Controllers.Notifications.NotificationsController.NotificationsController(MMCA.Common.Application.UseCases.ICommandHandler!>! sendHandler, MMCA.Common.Application.UseCases.IQueryHandler!>!>! historyHandler, MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService! currentUserService) -> void +MMCA.Common.API.Controllers.Notifications.NotificationsController.SendAsync(MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +MMCA.Common.API.Controllers.OAuthControllerBase +MMCA.Common.API.Controllers.OAuthControllerBase.CompleteAsync() -> System.Threading.Tasks.Task! +MMCA.Common.API.Controllers.OAuthControllerBase.ExchangeAsync(MMCA.Common.Shared.Auth.OAuthCodeExchangeRequest request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.API.Controllers.OAuthControllerBase.GitHubLogin(System.Uri? returnUrl = null) -> Microsoft.AspNetCore.Mvc.ChallengeResult! +MMCA.Common.API.Controllers.OAuthControllerBase.GoogleLogin(System.Uri? returnUrl = null) -> Microsoft.AspNetCore.Mvc.ChallengeResult! +MMCA.Common.API.Controllers.OAuthControllerBase.OAuthControllerBase(MMCA.Common.Application.Auth.IAuthenticationService! authenticationService, MMCA.Common.Application.Interfaces.ICacheService! cacheService, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> void +MMCA.Common.API.Controllers.Privacy.DataExportControllerBase +MMCA.Common.API.Controllers.Privacy.DataExportControllerBase.CurrentUserService.get -> MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService! +MMCA.Common.API.Controllers.Privacy.DataExportControllerBase.DataExportControllerBase(MMCA.Common.Application.UseCases.IQueryHandler!>! exportHandler, MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService! currentUserService) -> void +MMCA.Common.API.Controllers.ServiceInfoControllerBase +MMCA.Common.API.Controllers.ServiceInfoControllerBase.GetV1() -> Microsoft.AspNetCore.Mvc.ActionResult! +MMCA.Common.API.Controllers.ServiceInfoControllerBase.GetV2() -> Microsoft.AspNetCore.Mvc.ActionResult! +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoControllerBase() -> void +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse.$() -> MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse! +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse.ApiVersion.get -> string! +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse.ApiVersion.init -> void +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse.Deconstruct(out string! Service, out string! ApiVersion) -> void +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse.Equals(MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse? other) -> bool +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse.Service.get -> string! +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse.Service.init -> void +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse.ServiceInfoResponse(string! Service, string! ApiVersion) -> void +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response.$() -> MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response! +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response.ApiVersion.get -> string! +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response.ApiVersion.init -> void +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response.Deconstruct(out string! Service, out string! ApiVersion, out System.Collections.Generic.IReadOnlyList! SupportedVersions, out System.Collections.Generic.IReadOnlyList! DeprecatedVersions) -> void +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response.DeprecatedVersions.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response.DeprecatedVersions.init -> void +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response.Equals(MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response? other) -> bool +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response.Service.get -> string! +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response.Service.init -> void +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response.ServiceInfoV2Response(string! Service, string! ApiVersion, System.Collections.Generic.IReadOnlyList! SupportedVersions, System.Collections.Generic.IReadOnlyList! DeprecatedVersions) -> void +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response.SupportedVersions.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response.SupportedVersions.init -> void +MMCA.Common.API.Controllers.UserAccountAuthControllerBase +MMCA.Common.API.Controllers.UserAccountAuthControllerBase.ChangePasswordHandler.get -> MMCA.Common.Application.UseCases.ICommandHandler! +MMCA.Common.API.Controllers.UserAccountAuthControllerBase.ChangePreferencesHandler.get -> MMCA.Common.Application.UseCases.ICommandHandler! +MMCA.Common.API.Controllers.UserAccountAuthControllerBase.GetUserPreferencesHandler.get -> MMCA.Common.Application.UseCases.IQueryHandler!>! +MMCA.Common.API.Controllers.UserAccountAuthControllerBase.UserAccountAuthControllerBase(MMCA.Common.Application.Auth.IAuthenticationService! authenticationService, MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService! currentUserService, MMCA.Common.Application.UseCases.ICommandHandler! changePasswordHandler, MMCA.Common.Application.UseCases.ICommandHandler! changePreferencesHandler, MMCA.Common.Application.UseCases.IQueryHandler!>! getUserPreferencesHandler) -> void +MMCA.Common.API.DependencyInjection +MMCA.Common.API.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!) +MMCA.Common.API.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddAPI(MMCA.Common.Application.Settings.ModulesSettings? modulesSettings = null, Microsoft.Extensions.Configuration.IConfiguration? configuration = null) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddCommonExceptionHandlers() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddErrorLocalization() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddErrorResources() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddModuleHealthChecks(MMCA.Common.Application.Modules.ModuleLoader! moduleLoader) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddServerAuthSessionCookie(string! apiBaseAddress) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.FeatureManagement.CurrentUserTargetingContextAccessor +MMCA.Common.API.FeatureManagement.CurrentUserTargetingContextAccessor.CurrentUserTargetingContextAccessor(Microsoft.AspNetCore.Http.IHttpContextAccessor! httpContextAccessor) -> void +MMCA.Common.API.FeatureManagement.CurrentUserTargetingContextAccessor.GetContextAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.API.FeatureManagement.DisabledFeatureHandler +MMCA.Common.API.FeatureManagement.DisabledFeatureHandler.DisabledFeatureHandler() -> void +MMCA.Common.API.FeatureManagement.DisabledFeatureHandler.HandleDisabledFeatures(System.Collections.Generic.IEnumerable! features, Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext! context) -> System.Threading.Tasks.Task! +MMCA.Common.API.Idempotency.IdempotencyFilter +MMCA.Common.API.Idempotency.IdempotencyFilter.IdempotencyFilter(Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.API.Idempotency.IdempotencyFilter.OnActionExecutionAsync(Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext! context, Microsoft.AspNetCore.Mvc.Filters.ActionExecutionDelegate! next) -> System.Threading.Tasks.Task! +MMCA.Common.API.Idempotency.IdempotencyFilter.OnResourceExecutionAsync(Microsoft.AspNetCore.Mvc.Filters.ResourceExecutingContext! context, Microsoft.AspNetCore.Mvc.Filters.ResourceExecutionDelegate! next) -> System.Threading.Tasks.Task! +MMCA.Common.API.Idempotency.IdempotencyRecord +MMCA.Common.API.Idempotency.IdempotencyRecord.$() -> MMCA.Common.API.Idempotency.IdempotencyRecord! +MMCA.Common.API.Idempotency.IdempotencyRecord.Deconstruct(out int StatusCode, out string! ResponseBody, out string? RequestBodyHash) -> void +MMCA.Common.API.Idempotency.IdempotencyRecord.Equals(MMCA.Common.API.Idempotency.IdempotencyRecord? other) -> bool +MMCA.Common.API.Idempotency.IdempotencyRecord.IdempotencyRecord(int StatusCode, string! ResponseBody, string? RequestBodyHash = null) -> void +MMCA.Common.API.Idempotency.IdempotencyRecord.RequestBodyHash.get -> string? +MMCA.Common.API.Idempotency.IdempotencyRecord.RequestBodyHash.init -> void +MMCA.Common.API.Idempotency.IdempotencyRecord.ResponseBody.get -> string! +MMCA.Common.API.Idempotency.IdempotencyRecord.ResponseBody.init -> void +MMCA.Common.API.Idempotency.IdempotencyRecord.StatusCode.get -> int +MMCA.Common.API.Idempotency.IdempotencyRecord.StatusCode.init -> void +MMCA.Common.API.Idempotency.IdempotencySettings +MMCA.Common.API.Idempotency.IdempotencySettings.CacheExpirationHours.get -> int +MMCA.Common.API.Idempotency.IdempotencySettings.CacheExpirationHours.init -> void +MMCA.Common.API.Idempotency.IdempotencySettings.IdempotencySettings() -> void +MMCA.Common.API.Idempotency.IdempotentAttribute +MMCA.Common.API.Idempotency.IdempotentAttribute.IdempotentAttribute() -> void +MMCA.Common.API.JsonConverters.CurrencyJsonConverter +MMCA.Common.API.JsonConverters.CurrencyJsonConverter.CurrencyJsonConverter() -> void +MMCA.Common.API.Localization.ErrorResourceSource +MMCA.Common.API.Localization.ErrorResourceSource.ErrorResourceSource(Microsoft.Extensions.Localization.IStringLocalizer! localizer) -> void +MMCA.Common.API.Localization.ErrorResourceSource.Localizer.get -> Microsoft.Extensions.Localization.IStringLocalizer! +MMCA.Common.API.Localization.IErrorLocalizer +MMCA.Common.API.Localization.IErrorLocalizer.Localize(string! code, string! fallbackMessage) -> string! +MMCA.Common.API.Middleware.CorrelationIdMiddleware +MMCA.Common.API.Middleware.CorrelationIdMiddleware.CorrelationIdMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next) -> void +MMCA.Common.API.Middleware.CorrelationIdMiddleware.InvokeAsync(Microsoft.AspNetCore.Http.HttpContext! context, MMCA.Common.Application.Interfaces.ICorrelationContext! correlationContext) -> System.Threading.Tasks.Task! +MMCA.Common.API.Middleware.DbUpdateExceptionHandler +MMCA.Common.API.Middleware.DbUpdateExceptionHandler.DbUpdateExceptionHandler(Microsoft.AspNetCore.Http.IProblemDetailsService! problemDetailsService, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.API.Middleware.DbUpdateExceptionHandler.TryHandleAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, System.Exception! exception, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +MMCA.Common.API.Middleware.DomainExceptionHandler +MMCA.Common.API.Middleware.DomainExceptionHandler.DomainExceptionHandler(Microsoft.AspNetCore.Http.IProblemDetailsService! problemDetailsService, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.API.Middleware.DomainExceptionHandler.TryHandleAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, System.Exception! exception, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +MMCA.Common.API.Middleware.GlobalExceptionHandler +MMCA.Common.API.Middleware.GlobalExceptionHandler.GlobalExceptionHandler(Microsoft.AspNetCore.Http.IProblemDetailsService! problemDetailsService, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.API.Middleware.GlobalExceptionHandler.TryHandleAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, System.Exception! exception, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +MMCA.Common.API.Middleware.OperationCanceledExceptionHandler +MMCA.Common.API.Middleware.OperationCanceledExceptionHandler.OperationCanceledExceptionHandler(Microsoft.AspNetCore.Http.IProblemDetailsService! problemDetailsService, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.API.Middleware.OperationCanceledExceptionHandler.TryHandleAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, System.Exception! exception, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +MMCA.Common.API.Middleware.SoftDeletedUserMiddleware +MMCA.Common.API.Middleware.SoftDeletedUserMiddleware.InvokeAsync(Microsoft.AspNetCore.Http.HttpContext! context, MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService! currentUserService, MMCA.Common.Application.Interfaces.ICacheService! cacheService, Microsoft.Extensions.Logging.ILogger! logger) -> System.Threading.Tasks.Task! +MMCA.Common.API.Middleware.SoftDeletedUserMiddleware.SoftDeletedUserMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next) -> void +MMCA.Common.API.Middleware.TenantResolutionMiddleware +MMCA.Common.API.Middleware.TenantResolutionMiddleware.InvokeAsync(Microsoft.AspNetCore.Http.HttpContext! context, MMCA.Common.Application.Interfaces.ITenantContext! tenantContext, Microsoft.Extensions.Options.IOptions! tenancyOptions, Microsoft.AspNetCore.Http.IProblemDetailsService! problemDetailsService) -> System.Threading.Tasks.Task! +MMCA.Common.API.Middleware.TenantResolutionMiddleware.TenantResolutionMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next) -> void +MMCA.Common.API.Middleware.UnhandledResultFailureFilter +MMCA.Common.API.Middleware.UnhandledResultFailureFilter.OnResultExecuted(Microsoft.AspNetCore.Mvc.Filters.ResultExecutedContext! context) -> void +MMCA.Common.API.Middleware.UnhandledResultFailureFilter.OnResultExecuting(Microsoft.AspNetCore.Mvc.Filters.ResultExecutingContext! context) -> void +MMCA.Common.API.Middleware.UnhandledResultFailureFilter.UnhandledResultFailureFilter(Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.API.Middleware.ValidationExceptionHandler +MMCA.Common.API.Middleware.ValidationExceptionHandler.TryHandleAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, System.Exception! exception, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +MMCA.Common.API.Middleware.ValidationExceptionHandler.ValidationExceptionHandler(Microsoft.AspNetCore.Http.IProblemDetailsService! problemDetailsService, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.API.ModelBinders.QueryFilterModelBinder +MMCA.Common.API.ModelBinders.QueryFilterModelBinder.BindModelAsync(Microsoft.AspNetCore.Mvc.ModelBinding.ModelBindingContext! bindingContext) -> System.Threading.Tasks.Task! +MMCA.Common.API.ModelBinders.QueryFilterModelBinder.QueryFilterModelBinder() -> void +MMCA.Common.API.ModuleControllerFeatureProvider +MMCA.Common.API.ModuleControllerFeatureProvider.ModuleControllerFeatureProvider(MMCA.Common.Application.Settings.ModulesSettings! modulesSettings) -> void +MMCA.Common.API.ModuleControllerFeatureProvider.PopulateFeature(System.Collections.Generic.IEnumerable! parts, Microsoft.AspNetCore.Mvc.Controllers.ControllerFeature! feature) -> void +MMCA.Common.API.Notifications.DependencyInjection +MMCA.Common.API.Notifications.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IMvcBuilder!) +MMCA.Common.API.Notifications.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IMvcBuilder!).AddNotificationControllers() -> Microsoft.Extensions.DependencyInjection.IMvcBuilder! +MMCA.Common.API.RateLimiting.RateLimitAlgorithm +MMCA.Common.API.RateLimiting.RateLimitAlgorithm.FixedWindow = 0 -> MMCA.Common.API.RateLimiting.RateLimitAlgorithm +MMCA.Common.API.RateLimiting.RateLimitAlgorithm.SlidingWindow = 1 -> MMCA.Common.API.RateLimiting.RateLimitAlgorithm +MMCA.Common.API.RateLimiting.RateLimitingSettings +MMCA.Common.API.RateLimiting.RateLimitingSettings.Algorithm.get -> MMCA.Common.API.RateLimiting.RateLimitAlgorithm +MMCA.Common.API.RateLimiting.RateLimitingSettings.Algorithm.init -> void +MMCA.Common.API.RateLimiting.RateLimitingSettings.AuthIpPermitLimit.get -> int +MMCA.Common.API.RateLimiting.RateLimitingSettings.AuthIpPermitLimit.init -> void +MMCA.Common.API.RateLimiting.RateLimitingSettings.Distributed.get -> bool +MMCA.Common.API.RateLimiting.RateLimitingSettings.Distributed.init -> void +MMCA.Common.API.RateLimiting.RateLimitingSettings.GlobalPermitLimit.get -> int +MMCA.Common.API.RateLimiting.RateLimitingSettings.GlobalPermitLimit.init -> void +MMCA.Common.API.RateLimiting.RateLimitingSettings.PerUserPermitLimit.get -> int +MMCA.Common.API.RateLimiting.RateLimitingSettings.PerUserPermitLimit.init -> void +MMCA.Common.API.RateLimiting.RateLimitingSettings.PermitLimit.get -> int +MMCA.Common.API.RateLimiting.RateLimitingSettings.PermitLimit.init -> void +MMCA.Common.API.RateLimiting.RateLimitingSettings.QueueLimit.get -> int +MMCA.Common.API.RateLimiting.RateLimitingSettings.QueueLimit.init -> void +MMCA.Common.API.RateLimiting.RateLimitingSettings.RateLimitingSettings() -> void +MMCA.Common.API.RateLimiting.RateLimitingSettings.SegmentsPerWindow.get -> int +MMCA.Common.API.RateLimiting.RateLimitingSettings.SegmentsPerWindow.init -> void +MMCA.Common.API.RateLimiting.RedisFixedWindowRateLimiter +MMCA.Common.API.RateLimiting.RedisFixedWindowRateLimiter.RedisFixedWindowRateLimiter(StackExchange.Redis.IConnectionMultiplexer! connection, string! partitionKey, int permitLimit, Microsoft.Extensions.Logging.ILogger! logger, System.TimeProvider? timeProvider = null) -> void +MMCA.Common.API.Resources.ErrorResources +MMCA.Common.API.Resources.ErrorResources.ErrorResources() -> void +MMCA.Common.API.SessionCookies.CookieSessionRefreshMiddleware +MMCA.Common.API.SessionCookies.CookieSessionRefreshMiddleware.CookieSessionRefreshMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, MMCA.Common.API.SessionCookies.ICookieSessionRefresher! refresher) -> void +MMCA.Common.API.SessionCookies.CookieSessionRefreshMiddleware.InvokeAsync(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task! +MMCA.Common.API.SessionCookies.CookieSessionRefreshMiddlewareExtensions +MMCA.Common.API.SessionCookies.CookieSessionRefreshMiddlewareExtensions.extension(Microsoft.AspNetCore.Builder.IApplicationBuilder!) +MMCA.Common.API.SessionCookies.CookieSessionRefreshMiddlewareExtensions.extension(Microsoft.AspNetCore.Builder.IApplicationBuilder!).UseCookieSessionRefresh() -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +MMCA.Common.API.SessionCookies.CookieTokenReader +MMCA.Common.API.SessionCookies.CookieTokenReader.CookieTokenReader(Microsoft.AspNetCore.Http.IHttpContextAccessor! httpContextAccessor) -> void +MMCA.Common.API.SessionCookies.CookieTokenReader.ReadAccessToken() -> string? +MMCA.Common.API.SessionCookies.CookieTokenReader.ReadRefreshToken() -> string? +MMCA.Common.API.SessionCookies.ICookieSessionRefresher +MMCA.Common.API.SessionCookies.ICookieSessionRefresher.GetOrRefreshAsync(Microsoft.AspNetCore.Http.HttpContext! context, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.API.SessionCookies.SessionCookieAuthenticationExtensions +MMCA.Common.API.SessionCookies.SessionCookieAuthenticationExtensions.extension(Microsoft.AspNetCore.Authentication.AuthenticationBuilder!) +MMCA.Common.API.SessionCookies.SessionCookieAuthenticationExtensions.extension(Microsoft.AspNetCore.Authentication.AuthenticationBuilder!).AddSessionCookieAuthentication() -> Microsoft.AspNetCore.Authentication.AuthenticationBuilder! +MMCA.Common.API.SessionCookies.SessionCookieAuthenticationHandler +MMCA.Common.API.SessionCookies.SessionCookieAuthenticationHandler.SessionCookieAuthenticationHandler(Microsoft.Extensions.Options.IOptionsMonitor! options, Microsoft.Extensions.Logging.ILoggerFactory! logger, System.Text.Encodings.Web.UrlEncoder! encoder, MMCA.Common.API.SessionCookies.CookieTokenReader! cookieTokenReader) -> void +MMCA.Common.API.SessionCookies.SessionCookieEndpoints +MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest +MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest.$() -> MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest! +MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest.AccessToken.get -> string! +MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest.AccessToken.init -> void +MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest.Deconstruct(out string! AccessToken, out string! RefreshToken) -> void +MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest.Equals(MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest? other) -> bool +MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest.RefreshToken.get -> string! +MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest.RefreshToken.init -> void +MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest.SessionCookieRequest(string! AccessToken, string! RefreshToken) -> void +MMCA.Common.API.SessionCookies.SessionCookieEndpoints.extension(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder!) +MMCA.Common.API.SessionCookies.SessionCookieEndpoints.extension(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder!).MapSessionCookieEndpoints() -> Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! +MMCA.Common.API.SessionCookies.SessionTokenResponse +MMCA.Common.API.SessionCookies.SessionTokenResponse.$() -> MMCA.Common.API.SessionCookies.SessionTokenResponse! +MMCA.Common.API.SessionCookies.SessionTokenResponse.AccessToken.get -> string! +MMCA.Common.API.SessionCookies.SessionTokenResponse.AccessToken.init -> void +MMCA.Common.API.SessionCookies.SessionTokenResponse.AccessTokenExpiry.get -> System.DateTime +MMCA.Common.API.SessionCookies.SessionTokenResponse.AccessTokenExpiry.init -> void +MMCA.Common.API.SessionCookies.SessionTokenResponse.Deconstruct(out string! AccessToken, out System.DateTime AccessTokenExpiry) -> void +MMCA.Common.API.SessionCookies.SessionTokenResponse.Equals(MMCA.Common.API.SessionCookies.SessionTokenResponse? other) -> bool +MMCA.Common.API.SessionCookies.SessionTokenResponse.SessionTokenResponse(string! AccessToken, System.DateTime AccessTokenExpiry) -> void +MMCA.Common.API.SessionCookies.SessionTokenResult +MMCA.Common.API.SessionCookies.SessionTokenResult.AccessToken.get -> string! +MMCA.Common.API.SessionCookies.SessionTokenResult.AccessToken.init -> void +MMCA.Common.API.SessionCookies.SessionTokenResult.AccessTokenExpiry.get -> System.DateTime +MMCA.Common.API.SessionCookies.SessionTokenResult.AccessTokenExpiry.init -> void +MMCA.Common.API.SessionCookies.SessionTokenResult.Deconstruct(out string! AccessToken, out System.DateTime AccessTokenExpiry) -> void +MMCA.Common.API.SessionCookies.SessionTokenResult.Equals(MMCA.Common.API.SessionCookies.SessionTokenResult other) -> bool +MMCA.Common.API.SessionCookies.SessionTokenResult.SessionTokenResult() -> void +MMCA.Common.API.SessionCookies.SessionTokenResult.SessionTokenResult(string! AccessToken, System.DateTime AccessTokenExpiry) -> void +MMCA.Common.API.Startup.AppAssociationEndpointExtensions +MMCA.Common.API.Startup.AppAssociationEndpointExtensions.extension(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder!) +MMCA.Common.API.Startup.AppAssociationEndpointExtensions.extension(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder!).MapAppAssociationEndpoints(MMCA.Common.API.Startup.AppAssociationOptions! options) -> Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! +MMCA.Common.API.Startup.AppAssociationOptions +MMCA.Common.API.Startup.AppAssociationOptions.AndroidCertFingerprints.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.API.Startup.AppAssociationOptions.AndroidCertFingerprints.init -> void +MMCA.Common.API.Startup.AppAssociationOptions.AndroidPackageName.get -> string! +MMCA.Common.API.Startup.AppAssociationOptions.AndroidPackageName.init -> void +MMCA.Common.API.Startup.AppAssociationOptions.AppAssociationOptions() -> void +MMCA.Common.API.Startup.AppAssociationOptions.AppleAppId.get -> string! +MMCA.Common.API.Startup.AppAssociationOptions.AppleAppId.init -> void +MMCA.Common.API.Startup.AppAssociationOptions.AppleAppLinkComponents.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.API.Startup.AppAssociationOptions.AppleAppLinkComponents.init -> void +MMCA.Common.API.Startup.DatabaseInitializationExtensions +MMCA.Common.API.Startup.DatabaseInitializationExtensions.extension(System.IServiceProvider!) +MMCA.Common.API.Startup.DatabaseInitializationExtensions.extension(System.IServiceProvider!).InitializeDatabaseAsync(MMCA.Common.Application.Settings.ApplicationSettings! applicationSettings, MMCA.Common.Application.Modules.ModuleLoader! moduleLoader, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.API.Startup.JwksEndpointExtensions +MMCA.Common.API.Startup.JwksEndpointExtensions.extension(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder!) +MMCA.Common.API.Startup.JwksEndpointExtensions.extension(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder!).MapJwksEndpoint() -> Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! +MMCA.Common.API.Startup.MiniProfilerExtensions +MMCA.Common.API.Startup.MiniProfilerExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!) +MMCA.Common.API.Startup.MiniProfilerExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddMiniProfilerIfEnabled(MMCA.Common.Application.Settings.ApplicationSettings! applicationSettings) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.Startup.OidcDiscoveryEndpointExtensions +MMCA.Common.API.Startup.OidcDiscoveryEndpointExtensions.extension(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder!) +MMCA.Common.API.Startup.OidcDiscoveryEndpointExtensions.extension(Microsoft.AspNetCore.Routing.IEndpointRouteBuilder!).MapOidcDiscoveryEndpoint() -> Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! +MMCA.Common.API.Startup.OpenApiEndpointExtensions +MMCA.Common.API.Startup.OpenApiEndpointExtensions.extension(Microsoft.AspNetCore.Builder.WebApplication!) +MMCA.Common.API.Startup.OpenApiEndpointExtensions.extension(Microsoft.AspNetCore.Builder.WebApplication!).MapCommonOpenApi() -> Microsoft.AspNetCore.Builder.WebApplication! +MMCA.Common.API.Startup.OpenApiEndpointExtensions.extension(Microsoft.AspNetCore.Builder.WebApplication!).MapCommonScalarUi() -> Microsoft.AspNetCore.Builder.WebApplication! +MMCA.Common.API.Startup.SignalRExtensions +MMCA.Common.API.Startup.SignalRExtensions.extension(Microsoft.AspNetCore.Builder.WebApplication!) +MMCA.Common.API.Startup.SignalRExtensions.extension(Microsoft.AspNetCore.Builder.WebApplication!).MapNotificationHub() -> Microsoft.AspNetCore.Builder.WebApplication! +MMCA.Common.API.Startup.WebApplicationBuilderExtensions +MMCA.Common.API.Startup.WebApplicationBuilderExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!) +MMCA.Common.API.Startup.WebApplicationBuilderExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddCommonApiVersioning() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.Startup.WebApplicationBuilderExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddCommonAuthentication(Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.Startup.WebApplicationBuilderExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddCommonCors(Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.Startup.WebApplicationBuilderExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddCommonOpenApi() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.Startup.WebApplicationBuilderExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddCommonRateLimiting(MMCA.Common.API.RateLimiting.RateLimitingSettings! settings) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.Startup.WebApplicationBuilderExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddCommonRateLimiting(Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.Startup.WebApplicationBuilderExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddCommonRateLimiting(int permitLimit = 100, int queueLimit = 2, int perUserPermitLimit = 30, int globalPermitLimit = 300, int authIpPermitLimit = 30) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.Startup.WebApplicationBuilderExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddCommonResponseCompression() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.Startup.WebApplicationBuilderExtensions.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddForwardedJwtBearer(string! authority, string! audience, bool requireHttpsMetadata = false) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.API.Startup.WebApplicationExtensions +MMCA.Common.API.Startup.WebApplicationExtensions.extension(Microsoft.AspNetCore.Builder.WebApplication!) +MMCA.Common.API.Startup.WebApplicationExtensions.extension(Microsoft.AspNetCore.Builder.WebApplication!).MapCultureEndpoint() -> Microsoft.AspNetCore.Builder.WebApplication! +MMCA.Common.API.Startup.WebApplicationExtensions.extension(Microsoft.AspNetCore.Builder.WebApplication!).UseCommonMiddlewarePipeline() -> Microsoft.AspNetCore.Builder.WebApplication! +MMCA.Common.API.Startup.WebApplicationExtensions.extension(Microsoft.AspNetCore.Builder.WebApplication!).UseCommonRequestLocalization() -> Microsoft.AspNetCore.Builder.WebApplication! +abstract MMCA.Common.API.Controllers.Privacy.DataExportControllerBase.CreateQuery(int userId, int currentUserId, string? currentUserRole) -> TQuery +abstract MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceName.get -> string! +abstract MMCA.Common.API.Controllers.UserAccountAuthControllerBase.CreateChangePasswordCommand(int userId, MMCA.Common.Shared.Auth.ChangePasswordRequest request) -> TChangePasswordCommand +abstract MMCA.Common.API.Controllers.UserAccountAuthControllerBase.CreateChangePreferencesCommand(int userId, MMCA.Common.Shared.Auth.ChangePreferencesRequest! request) -> TChangePreferencesCommand +const MMCA.Common.API.Authentication.ExternalAuthExtensions.ExternalLoginScheme = "ExternalLogin" -> string! +const MMCA.Common.API.Authorization.AuthorizationPolicies.RequireAdmin = "RequireAdmin" -> string! +const MMCA.Common.API.Authorization.AuthorizationPolicies.RequireAttendee = "RequireAttendee" -> string! +const MMCA.Common.API.Authorization.AuthorizationPolicies.RequireAuthenticated = "RequireAuthenticated" -> string! +const MMCA.Common.API.Authorization.AuthorizationPolicies.RequireOrganizer = "RequireOrganizer" -> string! +const MMCA.Common.API.Authorization.PermissionPolicy.Prefix = "perm:" -> string! +const MMCA.Common.API.Controllers.EntityControllerBase.CsvContentType = "text/csv; charset=utf-8" -> string! +const MMCA.Common.API.Controllers.EntityControllerBase.DefaultMaxExportRows = 100000 -> int +const MMCA.Common.API.Controllers.EntityControllerBase.ExportRowLimitHeaderName = "X-Export-Row-Limit" -> string! +const MMCA.Common.API.Controllers.Privacy.DataExportControllerBase.ExportContentType = "application/json" -> string! +const MMCA.Common.API.Middleware.CorrelationIdMiddleware.HeaderName = "X-Correlation-ID" -> string! +const MMCA.Common.API.ModelBinders.QueryFilterModelBinder.MaxFilters = 50 -> int +const MMCA.Common.API.SessionCookies.SessionCookieAuthenticationHandler.SchemeName = "SessionCookie" -> string! +const MMCA.Common.API.SessionCookies.SessionCookieEndpoints.AccessTokenCookieName = "mmca_auth_access" -> string! +const MMCA.Common.API.SessionCookies.SessionCookieEndpoints.RefreshTokenCookieName = "mmca_auth_refresh" -> string! +const MMCA.Common.API.Startup.AppAssociationEndpointExtensions.AppleAppSiteAssociationPath = "/.well-known/apple-app-site-association" -> string! +const MMCA.Common.API.Startup.AppAssociationEndpointExtensions.AssetLinksPath = "/.well-known/assetlinks.json" -> string! +const MMCA.Common.API.Startup.JwksEndpointExtensions.DefaultJwksPath = "/.well-known/jwks.json" -> string! +const MMCA.Common.API.Startup.OidcDiscoveryEndpointExtensions.DefaultOidcDiscoveryPath = "/.well-known/openid-configuration" -> string! +const MMCA.Common.API.Startup.WebApplicationBuilderExtensions.CorsPolicyAllowAll = "_allowAll" -> string! +const MMCA.Common.API.Startup.WebApplicationBuilderExtensions.CorsPolicyAllowSpecificOrigins = "_allowSpecificOrigins" -> string! +const MMCA.Common.API.Startup.WebApplicationBuilderExtensions.RateLimitPolicyAuthIp = "auth-ip" -> string! +override MMCA.Common.API.Controllers.EntityControllerBase.HandleFailure(System.Collections.Generic.IEnumerable! errors) -> Microsoft.AspNetCore.Mvc.ObjectResult! +override MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse.Equals(object? obj) -> bool +override MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse.GetHashCode() -> int +override MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse.ToString() -> string! +override MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response.Equals(object? obj) -> bool +override MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response.GetHashCode() -> int +override MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response.ToString() -> string! +override MMCA.Common.API.Idempotency.IdempotencyRecord.Equals(object? obj) -> bool +override MMCA.Common.API.Idempotency.IdempotencyRecord.GetHashCode() -> int +override MMCA.Common.API.Idempotency.IdempotencyRecord.ToString() -> string! +override MMCA.Common.API.JsonConverters.CurrencyJsonConverter.Read(ref System.Text.Json.Utf8JsonReader reader, System.Type! typeToConvert, System.Text.Json.JsonSerializerOptions! options) -> MMCA.Common.Shared.ValueObjects.Currency! +override MMCA.Common.API.JsonConverters.CurrencyJsonConverter.Write(System.Text.Json.Utf8JsonWriter! writer, MMCA.Common.Shared.ValueObjects.Currency! value, System.Text.Json.JsonSerializerOptions! options) -> void +override MMCA.Common.API.RateLimiting.RedisFixedWindowRateLimiter.GetStatistics() -> System.Threading.RateLimiting.RateLimiterStatistics? +override MMCA.Common.API.RateLimiting.RedisFixedWindowRateLimiter.IdleDuration.get -> System.TimeSpan? +override MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest.Equals(object? obj) -> bool +override MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest.GetHashCode() -> int +override MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest.ToString() -> string! +override MMCA.Common.API.SessionCookies.SessionTokenResponse.Equals(object? obj) -> bool +override MMCA.Common.API.SessionCookies.SessionTokenResponse.GetHashCode() -> int +override MMCA.Common.API.SessionCookies.SessionTokenResponse.ToString() -> string! +override MMCA.Common.API.SessionCookies.SessionTokenResult.GetHashCode() -> int +static MMCA.Common.API.Authentication.ExternalAuthExtensions.AddExternalAuthProviders(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.Authorization.AuthorizationExtensions.AddAuthorizationPolicies(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.Authorization.AuthorizationExtensions.AddPermissions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.Authorization.OwnershipHelper.GetOwnershipSpecification(MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService! currentUserService, string! claimType, System.Func! specFactory, string! bypassRole = "Admin") -> TSpec? +static MMCA.Common.API.Authorization.OwnershipHelper.GetOwnershipSpecification(MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService! currentUserService, System.Func! specFactory) -> TSpec? +static MMCA.Common.API.Authorization.OwnershipHelper.IsAdmin(MMCA.Common.Application.Interfaces.Infrastructure.ICurrentUserService! currentUserService, string! bypassRole = "Admin") -> bool +static MMCA.Common.API.Authorization.PermissionPolicy.NameFor(string! permission) -> string! +static MMCA.Common.API.Caching.OutputCacheOptionsExtensions.AddPublicEndpointPolicy(this Microsoft.AspNetCore.OutputCaching.OutputCacheOptions! options, string! name, System.TimeSpan expiration, params string![]! tags) -> void +static MMCA.Common.API.Caching.OutputCacheOptionsExtensions.AddPublicEndpointPolicy(this Microsoft.AspNetCore.OutputCaching.OutputCacheOptions! options, string! name, System.TimeSpan expiration, string![]! bypassRoles, params string![]! tags) -> void +static MMCA.Common.API.Controllers.Privacy.DataExportControllerBase.BuildFileName(int userId, System.DateTimeOffset generatedOn) -> string! +static MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse.operator !=(MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse? left, MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse? right) -> bool +static MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse.operator ==(MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse? left, MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoResponse? right) -> bool +static MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response.operator !=(MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response? left, MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response? right) -> bool +static MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response.operator ==(MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response? left, MMCA.Common.API.Controllers.ServiceInfoControllerBase.ServiceInfoV2Response? right) -> bool +static MMCA.Common.API.DependencyInjection.AddAPI(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, MMCA.Common.Application.Settings.ModulesSettings? modulesSettings = null, Microsoft.Extensions.Configuration.IConfiguration? configuration = null) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.DependencyInjection.AddCommonExceptionHandlers(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.DependencyInjection.AddErrorLocalization(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.DependencyInjection.AddErrorResources(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.DependencyInjection.AddModuleHealthChecks(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, MMCA.Common.Application.Modules.ModuleLoader! moduleLoader) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.DependencyInjection.AddServerAuthSessionCookie(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! apiBaseAddress) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.Idempotency.IdempotencyFilter.IdempotencyKeyHeader.get -> string! +static MMCA.Common.API.Idempotency.IdempotencyRecord.operator !=(MMCA.Common.API.Idempotency.IdempotencyRecord? left, MMCA.Common.API.Idempotency.IdempotencyRecord? right) -> bool +static MMCA.Common.API.Idempotency.IdempotencyRecord.operator ==(MMCA.Common.API.Idempotency.IdempotencyRecord? left, MMCA.Common.API.Idempotency.IdempotencyRecord? right) -> bool +static MMCA.Common.API.Notifications.DependencyInjection.AddNotificationControllers(this Microsoft.Extensions.DependencyInjection.IMvcBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IMvcBuilder! +static MMCA.Common.API.SessionCookies.CookieSessionRefreshMiddlewareExtensions.UseCookieSessionRefresh(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +static MMCA.Common.API.SessionCookies.SessionCookieAuthenticationExtensions.AddSessionCookieAuthentication(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder) -> Microsoft.AspNetCore.Authentication.AuthenticationBuilder! +static MMCA.Common.API.SessionCookies.SessionCookieEndpoints.MapSessionCookieEndpoints(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints) -> Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! +static MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest.operator !=(MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest? left, MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest? right) -> bool +static MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest.operator ==(MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest? left, MMCA.Common.API.SessionCookies.SessionCookieEndpoints.SessionCookieRequest? right) -> bool +static MMCA.Common.API.SessionCookies.SessionTokenResponse.operator !=(MMCA.Common.API.SessionCookies.SessionTokenResponse? left, MMCA.Common.API.SessionCookies.SessionTokenResponse? right) -> bool +static MMCA.Common.API.SessionCookies.SessionTokenResponse.operator ==(MMCA.Common.API.SessionCookies.SessionTokenResponse? left, MMCA.Common.API.SessionCookies.SessionTokenResponse? right) -> bool +static MMCA.Common.API.SessionCookies.SessionTokenResult.operator !=(MMCA.Common.API.SessionCookies.SessionTokenResult left, MMCA.Common.API.SessionCookies.SessionTokenResult right) -> bool +static MMCA.Common.API.SessionCookies.SessionTokenResult.operator ==(MMCA.Common.API.SessionCookies.SessionTokenResult left, MMCA.Common.API.SessionCookies.SessionTokenResult right) -> bool +static MMCA.Common.API.Startup.AppAssociationEndpointExtensions.MapAppAssociationEndpoints(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints, MMCA.Common.API.Startup.AppAssociationOptions! options) -> Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! +static MMCA.Common.API.Startup.DatabaseInitializationExtensions.InitializeDatabaseAsync(this System.IServiceProvider! services, MMCA.Common.Application.Settings.ApplicationSettings! applicationSettings, MMCA.Common.Application.Modules.ModuleLoader! moduleLoader, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static MMCA.Common.API.Startup.JwksEndpointExtensions.MapJwksEndpoint(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints) -> Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! +static MMCA.Common.API.Startup.MiniProfilerExtensions.AddMiniProfilerIfEnabled(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, MMCA.Common.Application.Settings.ApplicationSettings! applicationSettings) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.Startup.OidcDiscoveryEndpointExtensions.MapOidcDiscoveryEndpoint(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints) -> Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! +static MMCA.Common.API.Startup.OpenApiEndpointExtensions.MapCommonOpenApi(this Microsoft.AspNetCore.Builder.WebApplication! app) -> Microsoft.AspNetCore.Builder.WebApplication! +static MMCA.Common.API.Startup.OpenApiEndpointExtensions.MapCommonScalarUi(this Microsoft.AspNetCore.Builder.WebApplication! app) -> Microsoft.AspNetCore.Builder.WebApplication! +static MMCA.Common.API.Startup.SignalRExtensions.MapNotificationHub(this Microsoft.AspNetCore.Builder.WebApplication! app) -> Microsoft.AspNetCore.Builder.WebApplication! +static MMCA.Common.API.Startup.WebApplicationBuilderExtensions.AddCommonApiVersioning(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.Startup.WebApplicationBuilderExtensions.AddCommonAuthentication(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.Startup.WebApplicationBuilderExtensions.AddCommonCors(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.Startup.WebApplicationBuilderExtensions.AddCommonOpenApi(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.Startup.WebApplicationBuilderExtensions.AddCommonRateLimiting(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, MMCA.Common.API.RateLimiting.RateLimitingSettings! settings) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.Startup.WebApplicationBuilderExtensions.AddCommonRateLimiting(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.Startup.WebApplicationBuilderExtensions.AddCommonRateLimiting(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, int permitLimit = 100, int queueLimit = 2, int perUserPermitLimit = 30, int globalPermitLimit = 300, int authIpPermitLimit = 30) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.Startup.WebApplicationBuilderExtensions.AddCommonResponseCompression(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.Startup.WebApplicationBuilderExtensions.AddForwardedJwtBearer(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! authority, string! audience, bool requireHttpsMetadata = false) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.API.Startup.WebApplicationExtensions.MapCultureEndpoint(this Microsoft.AspNetCore.Builder.WebApplication! app) -> Microsoft.AspNetCore.Builder.WebApplication! +static MMCA.Common.API.Startup.WebApplicationExtensions.UseCommonMiddlewarePipeline(this Microsoft.AspNetCore.Builder.WebApplication! app) -> Microsoft.AspNetCore.Builder.WebApplication! +static MMCA.Common.API.Startup.WebApplicationExtensions.UseCommonRequestLocalization(this Microsoft.AspNetCore.Builder.WebApplication! app) -> Microsoft.AspNetCore.Builder.WebApplication! +static readonly MMCA.Common.API.AssemblyReference.Assembly -> System.Reflection.Assembly! +static readonly MMCA.Common.API.AssemblyReference.AssemblyName -> string! +static readonly MMCA.Common.API.Idempotency.IdempotencySettings.SectionName -> string! +static readonly MMCA.Common.API.RateLimiting.RateLimitingSettings.SectionName -> string! +virtual MMCA.Common.API.Controllers.AggregateRootEntityControllerBase.CreateAsync(TCreateRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +virtual MMCA.Common.API.Controllers.AggregateRootEntityControllerBase.DeleteAsync(TIdentifierType id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual MMCA.Common.API.Controllers.ApiControllerBase.HandleFailure(System.Collections.Generic.IEnumerable! errors) -> Microsoft.AspNetCore.Mvc.ObjectResult! +virtual MMCA.Common.API.Controllers.AuthControllerBase.LoginAsync(MMCA.Common.Shared.Auth.LoginRequest request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +virtual MMCA.Common.API.Controllers.AuthControllerBase.RefreshAsync(MMCA.Common.Shared.Auth.RefreshTokenRequest request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +virtual MMCA.Common.API.Controllers.AuthControllerBase.RegisterAsync(MMCA.Common.Shared.Auth.RegisterRequest request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +virtual MMCA.Common.API.Controllers.AuthControllerBase.RevokeAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +virtual MMCA.Common.API.Controllers.EntityControllerBase.BuildExportFileName(System.DateTimeOffset timestamp) -> string! +virtual MMCA.Common.API.Controllers.EntityControllerBase.ExportAsync(bool includeFKs = false, string? sortColumn = null, string? sortDirection = null, string? fields = null, System.Collections.Generic.Dictionary? filters = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual MMCA.Common.API.Controllers.EntityControllerBase.ExportFileNamePrefix.get -> string! +virtual MMCA.Common.API.Controllers.EntityControllerBase.GetAllAsync(bool includeFKs = false, bool includeChildren = false, string? sortColumn = null, string? sortDirection = null, string? fields = null, int pageNumber = 1, int pageSize = 10, System.Collections.Generic.Dictionary? filters = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>! +virtual MMCA.Common.API.Controllers.EntityControllerBase.GetAllAsync(string? fields = null, bool includeFKs = false, bool includeChildren = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>! +virtual MMCA.Common.API.Controllers.EntityControllerBase.GetAllForLookupAsync(string! nameProperty, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>!>! +virtual MMCA.Common.API.Controllers.EntityControllerBase.GetByIdAsync(TIdentifierType id, bool includeFKs = true, bool includeChildren = false, string? fields = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>! +virtual MMCA.Common.API.Controllers.EntityControllerBase.GetExportSpecification() -> MMCA.Common.Domain.Specifications.Specification? +virtual MMCA.Common.API.Controllers.Privacy.DataExportControllerBase.ExportAsync(int userId, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual MMCA.Common.API.Controllers.UserAccountAuthControllerBase.ChangePasswordAsync(MMCA.Common.Shared.Auth.ChangePasswordRequest request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +virtual MMCA.Common.API.Controllers.UserAccountAuthControllerBase.ChangePreferencesAsync(MMCA.Common.Shared.Auth.ChangePreferencesRequest! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +virtual MMCA.Common.API.Controllers.UserAccountAuthControllerBase.GetPreferencesAsync(System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!>! +~override MMCA.Common.API.SessionCookies.SessionTokenResult.Equals(object obj) -> bool +~override MMCA.Common.API.SessionCookies.SessionTokenResult.ToString() -> string diff --git a/Source/Presentation/MMCA.Common.API/PublicAPI.Unshipped.txt b/Source/Presentation/MMCA.Common.API/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/Source/Presentation/MMCA.Common.API/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/Source/Presentation/MMCA.Common.API/packages.lock.json b/Source/Presentation/MMCA.Common.API/packages.lock.json index 85803f17..75a68266 100644 --- a/Source/Presentation/MMCA.Common.API/packages.lock.json +++ b/Source/Presentation/MMCA.Common.API/packages.lock.json @@ -67,6 +67,12 @@ "Microsoft.OpenApi": "[2.7.5, 3.0.0)" } }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[5.6.0, )", + "resolved": "5.6.0", + "contentHash": "W4kJGezNIKLzo0Ak5FAQDFvkMf2U7DtGL4THmHyRSApfKsKt5V+eX/bU0ZLKAt/uf9Bb2o1bi0YDKj/GRB/vYQ==" + }, "Microsoft.FeatureManagement.AspNetCore": { "type": "Direct", "requested": "[4.6.0, )", diff --git a/Source/Presentation/MMCA.Common.Grpc/PublicAPI.Shipped.txt b/Source/Presentation/MMCA.Common.Grpc/PublicAPI.Shipped.txt new file mode 100644 index 00000000..4fafdad8 --- /dev/null +++ b/Source/Presentation/MMCA.Common.Grpc/PublicAPI.Shipped.txt @@ -0,0 +1,39 @@ +#nullable enable +MMCA.Common.Grpc.DependencyInjection +MMCA.Common.Grpc.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!) +MMCA.Common.Grpc.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddGrpcServiceDefaults() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.Grpc.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddTypedGrpcClient(string! serviceName) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +MMCA.Common.Grpc.Exceptions.ResultFailureException +MMCA.Common.Grpc.Exceptions.ResultFailureException.Errors.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.Grpc.Exceptions.ResultFailureException.ResultFailureException() -> void +MMCA.Common.Grpc.Exceptions.ResultFailureException.ResultFailureException(System.Collections.Generic.IReadOnlyList! errors) -> void +MMCA.Common.Grpc.Exceptions.ResultFailureException.ResultFailureException(string! message) -> void +MMCA.Common.Grpc.Exceptions.ResultFailureException.ResultFailureException(string! message, System.Exception! innerException) -> void +MMCA.Common.Grpc.Interceptors.GrpcResultExceptionInterceptor +MMCA.Common.Grpc.Interceptors.GrpcResultExceptionInterceptor.GrpcResultExceptionInterceptor(Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.Grpc.Interceptors.JwtForwardingClientInterceptor +MMCA.Common.Grpc.Interceptors.JwtForwardingClientInterceptor.JwtForwardingClientInterceptor(Microsoft.AspNetCore.Http.IHttpContextAccessor! httpContextAccessor) -> void +MMCA.Common.Grpc.ResultGrpcExtensions +MMCA.Common.Grpc.ResultGrpcExtensions.extension(MMCA.Common.Shared.Abstractions.ErrorType) +MMCA.Common.Grpc.ResultGrpcExtensions.extension(MMCA.Common.Shared.Abstractions.ErrorType).ToGrpcStatusCode() -> Grpc.Core.StatusCode +MMCA.Common.Grpc.ResultGrpcExtensions.extension(MMCA.Common.Shared.Abstractions.Result!) +MMCA.Common.Grpc.ResultGrpcExtensions.extension(MMCA.Common.Shared.Abstractions.Result!).ThrowIfFailure() -> void +MMCA.Common.Grpc.ResultGrpcExtensions.extension(System.Collections.Generic.IReadOnlyList!) +MMCA.Common.Grpc.ResultGrpcExtensions.extension(System.Collections.Generic.IReadOnlyList!).ToRpcException() -> Grpc.Core.RpcException! +MMCA.Common.Grpc.ResultGrpcExtensions.extension(MMCA.Common.Shared.Abstractions.Result!) +MMCA.Common.Grpc.ResultGrpcExtensions.extension(MMCA.Common.Shared.Abstractions.Result!).UnwrapOrThrow() -> T +override MMCA.Common.Grpc.Interceptors.GrpcResultExceptionInterceptor.ClientStreamingServerHandler(Grpc.Core.IAsyncStreamReader! requestStream, Grpc.Core.ServerCallContext! context, Grpc.Core.ClientStreamingServerMethod! continuation) -> System.Threading.Tasks.Task! +override MMCA.Common.Grpc.Interceptors.GrpcResultExceptionInterceptor.DuplexStreamingServerHandler(Grpc.Core.IAsyncStreamReader! requestStream, Grpc.Core.IServerStreamWriter! responseStream, Grpc.Core.ServerCallContext! context, Grpc.Core.DuplexStreamingServerMethod! continuation) -> System.Threading.Tasks.Task! +override MMCA.Common.Grpc.Interceptors.GrpcResultExceptionInterceptor.ServerStreamingServerHandler(TRequest! request, Grpc.Core.IServerStreamWriter! responseStream, Grpc.Core.ServerCallContext! context, Grpc.Core.ServerStreamingServerMethod! continuation) -> System.Threading.Tasks.Task! +override MMCA.Common.Grpc.Interceptors.GrpcResultExceptionInterceptor.UnaryServerHandler(TRequest! request, Grpc.Core.ServerCallContext! context, Grpc.Core.UnaryServerMethod! continuation) -> System.Threading.Tasks.Task! +override MMCA.Common.Grpc.Interceptors.JwtForwardingClientInterceptor.AsyncClientStreamingCall(Grpc.Core.Interceptors.ClientInterceptorContext context, Grpc.Core.Interceptors.Interceptor.AsyncClientStreamingCallContinuation! continuation) -> Grpc.Core.AsyncClientStreamingCall! +override MMCA.Common.Grpc.Interceptors.JwtForwardingClientInterceptor.AsyncDuplexStreamingCall(Grpc.Core.Interceptors.ClientInterceptorContext context, Grpc.Core.Interceptors.Interceptor.AsyncDuplexStreamingCallContinuation! continuation) -> Grpc.Core.AsyncDuplexStreamingCall! +override MMCA.Common.Grpc.Interceptors.JwtForwardingClientInterceptor.AsyncServerStreamingCall(TRequest! request, Grpc.Core.Interceptors.ClientInterceptorContext context, Grpc.Core.Interceptors.Interceptor.AsyncServerStreamingCallContinuation! continuation) -> Grpc.Core.AsyncServerStreamingCall! +override MMCA.Common.Grpc.Interceptors.JwtForwardingClientInterceptor.AsyncUnaryCall(TRequest! request, Grpc.Core.Interceptors.ClientInterceptorContext context, Grpc.Core.Interceptors.Interceptor.AsyncUnaryCallContinuation! continuation) -> Grpc.Core.AsyncUnaryCall! +override MMCA.Common.Grpc.Interceptors.JwtForwardingClientInterceptor.BlockingUnaryCall(TRequest! request, Grpc.Core.Interceptors.ClientInterceptorContext context, Grpc.Core.Interceptors.Interceptor.BlockingUnaryCallContinuation! continuation) -> TResponse! +static MMCA.Common.Grpc.DependencyInjection.AddGrpcServiceDefaults(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.Grpc.DependencyInjection.AddTypedGrpcClient(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, string! serviceName) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static MMCA.Common.Grpc.ResultGrpcExtensions.ThrowIfFailure(this MMCA.Common.Shared.Abstractions.Result! result) -> void +static MMCA.Common.Grpc.ResultGrpcExtensions.ToGrpcStatusCode(this MMCA.Common.Shared.Abstractions.ErrorType errorType) -> Grpc.Core.StatusCode +static MMCA.Common.Grpc.ResultGrpcExtensions.ToRpcException(this System.Collections.Generic.IReadOnlyList! errors) -> Grpc.Core.RpcException! +static MMCA.Common.Grpc.ResultGrpcExtensions.UnwrapOrThrow(this MMCA.Common.Shared.Abstractions.Result! result) -> T diff --git a/Source/Presentation/MMCA.Common.Grpc/PublicAPI.Unshipped.txt b/Source/Presentation/MMCA.Common.Grpc/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/Source/Presentation/MMCA.Common.Grpc/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/Source/Presentation/MMCA.Common.Grpc/packages.lock.json b/Source/Presentation/MMCA.Common.Grpc/packages.lock.json index 1b354465..902ff69c 100644 --- a/Source/Presentation/MMCA.Common.Grpc/packages.lock.json +++ b/Source/Presentation/MMCA.Common.Grpc/packages.lock.json @@ -39,6 +39,12 @@ "resolved": "3.0.163", "contentHash": "l9iZ+OXBGlAFEKH1y4BSKPXHcmpJmikysSm0brjTau8fR7Qm7IVGLVig+dh0RH9mq4s3neZayuPQrHRweOpxxw==" }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[5.6.0, )", + "resolved": "5.6.0", + "contentHash": "W4kJGezNIKLzo0Ak5FAQDFvkMf2U7DtGL4THmHyRSApfKsKt5V+eX/bU0ZLKAt/uf9Bb2o1bi0YDKj/GRB/vYQ==" + }, "Microsoft.Extensions.Http.Resilience": { "type": "Direct", "requested": "[10.9.0, )", diff --git a/Source/Presentation/MMCA.Common.UI.Web/PublicAPI.Shipped.txt b/Source/Presentation/MMCA.Common.UI.Web/PublicAPI.Shipped.txt new file mode 100644 index 00000000..397735a1 --- /dev/null +++ b/Source/Presentation/MMCA.Common.UI.Web/PublicAPI.Shipped.txt @@ -0,0 +1,26 @@ +#nullable enable +MMCA.Common.UI.Web.Components.Pages.Error +MMCA.Common.UI.Web.Components.Pages.Error.Error() -> void +MMCA.Common.UI.Web.DependencyInjection +MMCA.Common.UI.Web.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!) +MMCA.Common.UI.Web.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddCommonBlazorCsp() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.UI.Web.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddCommonServerTokenStorage() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.UI.Web.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddCommonWebFormFactor() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.UI.Web.Services.ServerTokenStorageService +MMCA.Common.UI.Web.Services.ServerTokenStorageService.ClearTokensAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Web.Services.ServerTokenStorageService.GetAccessTokenAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Web.Services.ServerTokenStorageService.GetRefreshTokenAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Web.Services.ServerTokenStorageService.ServerTokenStorageService(Microsoft.AspNetCore.Http.IHttpContextAccessor! httpContextAccessor, MMCA.Common.API.SessionCookies.CookieTokenReader! cookieTokenReader, MMCA.Common.UI.Services.Auth.ISessionCookieSync! sessionCookieSync, MMCA.Common.UI.Services.Auth.ITokenRefresher! tokenRefresher) -> void +MMCA.Common.UI.Web.Services.ServerTokenStorageService.SetTokensAsync(string! accessToken, string! refreshToken) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Web.Services.WebFormFactor +MMCA.Common.UI.Web.Services.WebFormFactor.GetFormFactor() -> string! +MMCA.Common.UI.Web.Services.WebFormFactor.GetPlatform() -> string! +MMCA.Common.UI.Web.Services.WebFormFactor.WebFormFactor() -> void +MMCA.Common.UI.Web._Imports +MMCA.Common.UI.Web._Imports.Execute() -> void +MMCA.Common.UI.Web._Imports._Imports() -> void +override MMCA.Common.UI.Web.Components.Pages.Error.OnInitialized() -> void +static MMCA.Common.UI.Web.DependencyInjection.AddCommonBlazorCsp(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.UI.Web.DependencyInjection.AddCommonServerTokenStorage(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.UI.Web.DependencyInjection.AddCommonWebFormFactor(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +~override MMCA.Common.UI.Web.Components.Pages.Error.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void diff --git a/Source/Presentation/MMCA.Common.UI.Web/PublicAPI.Unshipped.txt b/Source/Presentation/MMCA.Common.UI.Web/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/Source/Presentation/MMCA.Common.UI.Web/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/Source/Presentation/MMCA.Common.UI.Web/packages.lock.json b/Source/Presentation/MMCA.Common.UI.Web/packages.lock.json index 46455471..b06dfe61 100644 --- a/Source/Presentation/MMCA.Common.UI.Web/packages.lock.json +++ b/Source/Presentation/MMCA.Common.UI.Web/packages.lock.json @@ -8,6 +8,12 @@ "resolved": "3.0.163", "contentHash": "l9iZ+OXBGlAFEKH1y4BSKPXHcmpJmikysSm0brjTau8fR7Qm7IVGLVig+dh0RH9mq4s3neZayuPQrHRweOpxxw==" }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[5.6.0, )", + "resolved": "5.6.0", + "contentHash": "W4kJGezNIKLzo0Ak5FAQDFvkMf2U7DtGL4THmHyRSApfKsKt5V+eX/bU0ZLKAt/uf9Bb2o1bi0YDKj/GRB/vYQ==" + }, "Microsoft.VisualStudio.Threading.Analyzers": { "type": "Direct", "requested": "[18.7.23, )", diff --git a/Source/Presentation/MMCA.Common.UI/PublicAPI.Shipped.txt b/Source/Presentation/MMCA.Common.UI/PublicAPI.Shipped.txt new file mode 100644 index 00000000..62103ca8 --- /dev/null +++ b/Source/Presentation/MMCA.Common.UI/PublicAPI.Shipped.txt @@ -0,0 +1,982 @@ +#nullable enable +MMCA.Common.UI.Common.BreakpointConstants +MMCA.Common.UI.Common.Interfaces.IEntityService +MMCA.Common.UI.Common.Interfaces.IEntityService.AddAsync(TEntityDTO entity, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Common.Interfaces.IEntityService.DeleteAsync(TIdentifierType id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Common.Interfaces.IEntityService.GetAllAsync(bool includeFKs = false, bool includeChildren = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task?>! +MMCA.Common.UI.Common.Interfaces.IEntityService.GetAllForLookupAsync(string! nameProperty, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>! +MMCA.Common.UI.Common.Interfaces.IEntityService.GetByIdAsync(TIdentifierType id, bool includeChildren = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Common.Interfaces.IEntityService.GetPagedAsync(System.Collections.Generic.Dictionary! filters, int pageNumber, int pageSize, string? sortColumn, string? sortDirection, bool includeChildren = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<(System.Collections.Generic.IReadOnlyList! Items, int TotalItems)>! +MMCA.Common.UI.Common.Interfaces.IEntityService.UpdateAsync(TEntityDTO entity, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Common.Interfaces.IHomePageContent +MMCA.Common.UI.Common.Interfaces.IHomePageContent.ComponentType.get -> System.Type! +MMCA.Common.UI.Common.Interfaces.IHomePageContent.PageTitle.get -> string! +MMCA.Common.UI.Common.Interfaces.IUIModule +MMCA.Common.UI.Common.Interfaces.IUIModule.AppBarComponentTypes.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.UI.Common.Interfaces.IUIModule.Assembly.get -> System.Reflection.Assembly! +MMCA.Common.UI.Common.Interfaces.IUIModule.LayoutComponentTypes.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.UI.Common.Interfaces.IUIModule.NavItems.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.UI.Common.NavItem +MMCA.Common.UI.Common.NavItem.Deconstruct(out string! Title, out string! Href, out string! Icon, out string? RequiredRole, out string? RequiredClaim, out MMCA.Common.UI.Common.NavSection Section, out string? Group, out System.Type? TitleResource) -> void +MMCA.Common.UI.Common.NavItem.Group.get -> string? +MMCA.Common.UI.Common.NavItem.Group.init -> void +MMCA.Common.UI.Common.NavItem.Href.get -> string! +MMCA.Common.UI.Common.NavItem.Href.init -> void +MMCA.Common.UI.Common.NavItem.Icon.get -> string! +MMCA.Common.UI.Common.NavItem.Icon.init -> void +MMCA.Common.UI.Common.NavItem.NavItem(MMCA.Common.UI.Common.NavItem! original) -> void +MMCA.Common.UI.Common.NavItem.NavItem(string! Title, string! Href, string! Icon, string? RequiredRole = null, string? RequiredClaim = null, MMCA.Common.UI.Common.NavSection Section = MMCA.Common.UI.Common.NavSection.General, string? Group = null, System.Type? TitleResource = null) -> void +MMCA.Common.UI.Common.NavItem.RequiredClaim.get -> string? +MMCA.Common.UI.Common.NavItem.RequiredClaim.init -> void +MMCA.Common.UI.Common.NavItem.RequiredRole.get -> string? +MMCA.Common.UI.Common.NavItem.RequiredRole.init -> void +MMCA.Common.UI.Common.NavItem.Section.get -> MMCA.Common.UI.Common.NavSection +MMCA.Common.UI.Common.NavItem.Section.init -> void +MMCA.Common.UI.Common.NavItem.Title.get -> string! +MMCA.Common.UI.Common.NavItem.Title.init -> void +MMCA.Common.UI.Common.NavItem.TitleResource.get -> System.Type? +MMCA.Common.UI.Common.NavItem.TitleResource.init -> void +MMCA.Common.UI.Common.NavSection +MMCA.Common.UI.Common.NavSection.Admin = 2 -> MMCA.Common.UI.Common.NavSection +MMCA.Common.UI.Common.NavSection.General = 0 -> MMCA.Common.UI.Common.NavSection +MMCA.Common.UI.Common.NavSection.User = 1 -> MMCA.Common.UI.Common.NavSection +MMCA.Common.UI.Common.NotificationRoutePaths +MMCA.Common.UI.Common.RoutePaths +MMCA.Common.UI.Common.Settings.ApiSettings +MMCA.Common.UI.Common.Settings.ApiSettings.ApiEndpoint.get -> string? +MMCA.Common.UI.Common.Settings.ApiSettings.ApiEndpoint.init -> void +MMCA.Common.UI.Common.Settings.ApiSettings.ApiSettings() -> void +MMCA.Common.UI.Common.Settings.ApiSettings.WasmApiEndpoint.get -> string? +MMCA.Common.UI.Common.Settings.ApiSettings.WasmApiEndpoint.init -> void +MMCA.Common.UI.Common.Settings.IApiSettings +MMCA.Common.UI.Common.Settings.IApiSettings.ApiEndpoint.get -> string? +MMCA.Common.UI.Common.Settings.IApiSettings.WasmApiEndpoint.get -> string? +MMCA.Common.UI.Common.Settings.LayoutSettings +MMCA.Common.UI.Common.Settings.LayoutSettings.BrandName.get -> string! +MMCA.Common.UI.Common.Settings.LayoutSettings.BrandName.init -> void +MMCA.Common.UI.Common.Settings.LayoutSettings.FooterText.get -> string! +MMCA.Common.UI.Common.Settings.LayoutSettings.FooterText.init -> void +MMCA.Common.UI.Common.Settings.LayoutSettings.LayoutSettings() -> void +MMCA.Common.UI.Common.Settings.UIModuleConfiguration +MMCA.Common.UI.Components.Capabilities.BiometricGate +MMCA.Common.UI.Components.Capabilities.BiometricGate.BiometricGate() -> void +MMCA.Common.UI.Components.Capabilities.DeepLinkListener +MMCA.Common.UI.Components.Capabilities.DeepLinkListener.DeepLinkListener() -> void +MMCA.Common.UI.Components.Capabilities.DeepLinkListener.Dispose() -> void +MMCA.Common.UI.Components.Capabilities.ExternalLink +MMCA.Common.UI.Components.Capabilities.ExternalLink.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment? +MMCA.Common.UI.Components.Capabilities.ExternalLink.ChildContent.set -> void +MMCA.Common.UI.Components.Capabilities.ExternalLink.Class.get -> string? +MMCA.Common.UI.Components.Capabilities.ExternalLink.Class.set -> void +MMCA.Common.UI.Components.Capabilities.ExternalLink.ExternalLink() -> void +MMCA.Common.UI.Components.Capabilities.ExternalLink.Href.get -> string! +MMCA.Common.UI.Components.Capabilities.ExternalLink.Href.set -> void +MMCA.Common.UI.Components.Capabilities.ExternalLink.Underline.get -> MudBlazor.Underline +MMCA.Common.UI.Components.Capabilities.ExternalLink.Underline.set -> void +MMCA.Common.UI.Components.Capabilities.OfflineBanner +MMCA.Common.UI.Components.Capabilities.OfflineBanner.Dispose() -> void +MMCA.Common.UI.Components.Capabilities.OfflineBanner.OfflineBanner() -> void +MMCA.Common.UI.Components.Capabilities.PushRegistrationListener +MMCA.Common.UI.Components.Capabilities.PushRegistrationListener.Dispose() -> void +MMCA.Common.UI.Components.Capabilities.PushRegistrationListener.PushRegistrationListener() -> void +MMCA.Common.UI.Components.CultureSwitcher +MMCA.Common.UI.Components.CultureSwitcher.CultureSwitcher() -> void +MMCA.Common.UI.Components.DeleteConfirmation +MMCA.Common.UI.Components.DeleteConfirmation.DeleteConfirmation() -> void +MMCA.Common.UI.Components.DeleteConfirmation.EntityType.get -> string? +MMCA.Common.UI.Components.DeleteConfirmation.EntityType.set -> void +MMCA.Common.UI.Components.DeleteConfirmation.ShowAsync(string? entityName) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Components.DocumentLanguage +MMCA.Common.UI.Components.DocumentLanguage.DocumentLanguage() -> void +MMCA.Common.UI.Components.EmptyState +MMCA.Common.UI.Components.EmptyState.EmptyState() -> void +MMCA.Common.UI.Components.EmptyState.Icon.get -> string! +MMCA.Common.UI.Components.EmptyState.Icon.set -> void +MMCA.Common.UI.Components.EmptyState.Message.get -> string? +MMCA.Common.UI.Components.EmptyState.Message.set -> void +MMCA.Common.UI.Components.MmcaThemeProviders +MMCA.Common.UI.Components.MmcaThemeProviders.Dispose() -> void +MMCA.Common.UI.Components.MmcaThemeProviders.MmcaThemeProviders() -> void +MMCA.Common.UI.Components.MobileCardList +MMCA.Common.UI.Components.MobileCardList.CardTemplate.get -> Microsoft.AspNetCore.Components.RenderFragment! +MMCA.Common.UI.Components.MobileCardList.CardTemplate.set -> void +MMCA.Common.UI.Components.MobileCardList.CurrentPage.get -> int +MMCA.Common.UI.Components.MobileCardList.CurrentPage.set -> void +MMCA.Common.UI.Components.MobileCardList.EmptyIcon.get -> string! +MMCA.Common.UI.Components.MobileCardList.EmptyIcon.set -> void +MMCA.Common.UI.Components.MobileCardList.EmptyMessage.get -> string? +MMCA.Common.UI.Components.MobileCardList.EmptyMessage.set -> void +MMCA.Common.UI.Components.MobileCardList.IsLoading.get -> bool +MMCA.Common.UI.Components.MobileCardList.IsLoading.set -> void +MMCA.Common.UI.Components.MobileCardList.Items.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.UI.Components.MobileCardList.Items.set -> void +MMCA.Common.UI.Components.MobileCardList.MobileCardList() -> void +MMCA.Common.UI.Components.MobileCardList.OnCardClick.get -> Microsoft.AspNetCore.Components.EventCallback +MMCA.Common.UI.Components.MobileCardList.OnCardClick.set -> void +MMCA.Common.UI.Components.MobileCardList.OnPageChanged.get -> Microsoft.AspNetCore.Components.EventCallback +MMCA.Common.UI.Components.MobileCardList.OnPageChanged.set -> void +MMCA.Common.UI.Components.MobileCardList.PageSize.get -> int +MMCA.Common.UI.Components.MobileCardList.PageSize.set -> void +MMCA.Common.UI.Components.MobileCardList.TotalItems.get -> int +MMCA.Common.UI.Components.MobileCardList.TotalItems.set -> void +MMCA.Common.UI.Components.MobileInfiniteScrollList +MMCA.Common.UI.Components.MobileInfiniteScrollList.CardTemplate.get -> Microsoft.AspNetCore.Components.RenderFragment! +MMCA.Common.UI.Components.MobileInfiniteScrollList.CardTemplate.set -> void +MMCA.Common.UI.Components.MobileInfiniteScrollList.DisposeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Components.MobileInfiniteScrollList.EmptyIcon.get -> string! +MMCA.Common.UI.Components.MobileInfiniteScrollList.EmptyIcon.set -> void +MMCA.Common.UI.Components.MobileInfiniteScrollList.EmptyMessage.get -> string? +MMCA.Common.UI.Components.MobileInfiniteScrollList.EmptyMessage.set -> void +MMCA.Common.UI.Components.MobileInfiniteScrollList.FetchPage.get -> System.Func! Items, int TotalItems)>!>! +MMCA.Common.UI.Components.MobileInfiniteScrollList.FetchPage.set -> void +MMCA.Common.UI.Components.MobileInfiniteScrollList.MaxRenderedItems.get -> int +MMCA.Common.UI.Components.MobileInfiniteScrollList.MaxRenderedItems.set -> void +MMCA.Common.UI.Components.MobileInfiniteScrollList.MobileInfiniteScrollList() -> void +MMCA.Common.UI.Components.MobileInfiniteScrollList.OnCardClick.get -> Microsoft.AspNetCore.Components.EventCallback +MMCA.Common.UI.Components.MobileInfiniteScrollList.OnCardClick.set -> void +MMCA.Common.UI.Components.MobileInfiniteScrollList.OnSentinelVisible() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Components.MobileInfiniteScrollList.PageSize.get -> int +MMCA.Common.UI.Components.MobileInfiniteScrollList.PageSize.set -> void +MMCA.Common.UI.Components.MobileInfiniteScrollList.ResetAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Components.Notifications.NotificationBell +MMCA.Common.UI.Components.Notifications.NotificationBell.Dispose() -> void +MMCA.Common.UI.Components.Notifications.NotificationBell.NotificationBell() -> void +MMCA.Common.UI.Components.Notifications.NotificationListener +MMCA.Common.UI.Components.Notifications.NotificationListener.DisposeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Components.Notifications.NotificationListener.NotificationListener() -> void +MMCA.Common.UI.Components.PageErrorState +MMCA.Common.UI.Components.PageErrorState.Message.get -> string? +MMCA.Common.UI.Components.PageErrorState.Message.set -> void +MMCA.Common.UI.Components.PageErrorState.PageErrorState() -> void +MMCA.Common.UI.Components.PageHeader +MMCA.Common.UI.Components.PageHeader.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment? +MMCA.Common.UI.Components.PageHeader.ChildContent.set -> void +MMCA.Common.UI.Components.PageHeader.PageHeader() -> void +MMCA.Common.UI.Components.PageHeader.Title.get -> string! +MMCA.Common.UI.Components.PageHeader.Title.set -> void +MMCA.Common.UI.Components.PageLoadingState +MMCA.Common.UI.Components.PageLoadingState.Label.get -> string? +MMCA.Common.UI.Components.PageLoadingState.Label.set -> void +MMCA.Common.UI.Components.PageLoadingState.PageLoadingState() -> void +MMCA.Common.UI.Components.PageStateScope +MMCA.Common.UI.Components.PageStateScope.DisposeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Components.PageStateScope.OnPageRestoredAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Components.PageStateScope.OnRestore.get -> Microsoft.AspNetCore.Components.EventCallback +MMCA.Common.UI.Components.PageStateScope.OnRestore.set -> void +MMCA.Common.UI.Components.PageStateScope.PageStateScope() -> void +MMCA.Common.UI.Components.QrCodeImage +MMCA.Common.UI.Components.QrCodeImage.AltText.get -> string! +MMCA.Common.UI.Components.QrCodeImage.AltText.set -> void +MMCA.Common.UI.Components.QrCodeImage.Class.get -> string? +MMCA.Common.UI.Components.QrCodeImage.Class.set -> void +MMCA.Common.UI.Components.QrCodeImage.ErrorCorrection.get -> MMCA.Common.UI.Components.QrErrorCorrectionLevel +MMCA.Common.UI.Components.QrCodeImage.ErrorCorrection.set -> void +MMCA.Common.UI.Components.QrCodeImage.Payload.get -> string! +MMCA.Common.UI.Components.QrCodeImage.Payload.set -> void +MMCA.Common.UI.Components.QrCodeImage.PixelsPerModule.get -> int +MMCA.Common.UI.Components.QrCodeImage.PixelsPerModule.set -> void +MMCA.Common.UI.Components.QrCodeImage.QrCodeImage() -> void +MMCA.Common.UI.Components.QrErrorCorrectionLevel +MMCA.Common.UI.Components.QrErrorCorrectionLevel.High = 3 -> MMCA.Common.UI.Components.QrErrorCorrectionLevel +MMCA.Common.UI.Components.QrErrorCorrectionLevel.Low = 0 -> MMCA.Common.UI.Components.QrErrorCorrectionLevel +MMCA.Common.UI.Components.QrErrorCorrectionLevel.Medium = 1 -> MMCA.Common.UI.Components.QrErrorCorrectionLevel +MMCA.Common.UI.Components.QrErrorCorrectionLevel.Quartile = 2 -> MMCA.Common.UI.Components.QrErrorCorrectionLevel +MMCA.Common.UI.Components.RedirectToLogin +MMCA.Common.UI.Components.RedirectToLogin.RedirectToLogin() -> void +MMCA.Common.UI.Components.ThemeToggle +MMCA.Common.UI.Components.ThemeToggle.Dispose() -> void +MMCA.Common.UI.Components.ThemeToggle.ThemeToggle() -> void +MMCA.Common.UI.Components.UnsavedChangesGuard +MMCA.Common.UI.Components.UnsavedChangesGuard.IsDirty.get -> bool +MMCA.Common.UI.Components.UnsavedChangesGuard.IsDirty.set -> void +MMCA.Common.UI.Components.UnsavedChangesGuard.IsDirtyAccessor.get -> System.Func? +MMCA.Common.UI.Components.UnsavedChangesGuard.IsDirtyAccessor.set -> void +MMCA.Common.UI.Components.UnsavedChangesGuard.LeaveButtonText.get -> string? +MMCA.Common.UI.Components.UnsavedChangesGuard.LeaveButtonText.set -> void +MMCA.Common.UI.Components.UnsavedChangesGuard.Message.get -> string? +MMCA.Common.UI.Components.UnsavedChangesGuard.Message.set -> void +MMCA.Common.UI.Components.UnsavedChangesGuard.StayButtonText.get -> string? +MMCA.Common.UI.Components.UnsavedChangesGuard.StayButtonText.set -> void +MMCA.Common.UI.Components.UnsavedChangesGuard.Title.get -> string? +MMCA.Common.UI.Components.UnsavedChangesGuard.Title.set -> void +MMCA.Common.UI.Components.UnsavedChangesGuard.UnsavedChangesGuard() -> void +MMCA.Common.UI.DependencyInjection +MMCA.Common.UI.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!) +MMCA.Common.UI.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddClientAuthSessionCookieSync() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.UI.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddUIModule() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.UI.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddUIShared(Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.UI.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddWasmFormFactor() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.UI.Extensions.MoneyExtensions +MMCA.Common.UI.Extensions.MoneyExtensions.extension(MMCA.Common.Shared.ValueObjects.Money!) +MMCA.Common.UI.Extensions.MoneyExtensions.extension(MMCA.Common.Shared.ValueObjects.Money!).ToDisplayString() -> string! +MMCA.Common.UI.Extensions.MoneyExtensions.extension(System.Collections.Generic.IReadOnlyCollection!) +MMCA.Common.UI.Extensions.MoneyExtensions.extension(System.Collections.Generic.IReadOnlyCollection!).ToDisplayRange() -> string! +MMCA.Common.UI.Extensions.WebApplicationExtensions +MMCA.Common.UI.Extensions.WebApplicationExtensions.extension(Microsoft.AspNetCore.Builder.IApplicationBuilder!) +MMCA.Common.UI.Extensions.WebApplicationExtensions.extension(Microsoft.AspNetCore.Builder.IApplicationBuilder!).UseAuthenticatedNoStore() -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +MMCA.Common.UI.Globalization.PseudoLocalizer +MMCA.Common.UI.Globalization.PseudoStringLocalizer +MMCA.Common.UI.Globalization.PseudoStringLocalizer.GetAllStrings(bool includeParentCultures) -> System.Collections.Generic.IEnumerable! +MMCA.Common.UI.Globalization.PseudoStringLocalizer.PseudoStringLocalizer(Microsoft.Extensions.Localization.IStringLocalizer! inner) -> void +MMCA.Common.UI.Globalization.PseudoStringLocalizer.this[string! name, params object![]! arguments].get -> Microsoft.Extensions.Localization.LocalizedString! +MMCA.Common.UI.Globalization.PseudoStringLocalizer.this[string! name].get -> Microsoft.Extensions.Localization.LocalizedString! +MMCA.Common.UI.Globalization.PseudoStringLocalizerFactory +MMCA.Common.UI.Globalization.PseudoStringLocalizerFactory.Create(System.Type! resourceSource) -> Microsoft.Extensions.Localization.IStringLocalizer! +MMCA.Common.UI.Globalization.PseudoStringLocalizerFactory.Create(string! baseName, string! location) -> Microsoft.Extensions.Localization.IStringLocalizer! +MMCA.Common.UI.Globalization.PseudoStringLocalizerFactory.PseudoStringLocalizerFactory(Microsoft.Extensions.Localization.IStringLocalizerFactory! inner) -> void +MMCA.Common.UI.Layout.MainLayout +MMCA.Common.UI.Layout.MainLayout.MainLayout() -> void +MMCA.Common.UI.Layout.NavMenu +MMCA.Common.UI.Layout.NavMenu.NavMenu() -> void +MMCA.Common.UI.Layout.ReconnectModal +MMCA.Common.UI.Layout.ReconnectModal.ReconnectModal() -> void +MMCA.Common.UI.Notifications.DependencyInjection +MMCA.Common.UI.Notifications.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!) +MMCA.Common.UI.Notifications.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddNotificationUI() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.UI.Notifications.NotificationUIModule +MMCA.Common.UI.Notifications.NotificationUIModule.AppBarComponentTypes.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.UI.Notifications.NotificationUIModule.Assembly.get -> System.Reflection.Assembly! +MMCA.Common.UI.Notifications.NotificationUIModule.LayoutComponentTypes.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.UI.Notifications.NotificationUIModule.NavItems.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.UI.Notifications.NotificationUIModule.NotificationUIModule() -> void +MMCA.Common.UI.Pages.Auth.Login +MMCA.Common.UI.Pages.Auth.Login.Login() -> void +MMCA.Common.UI.Pages.Auth.Login.ReturnUrl.get -> string? +MMCA.Common.UI.Pages.Auth.Login.ReturnUrl.set -> void +MMCA.Common.UI.Pages.Auth.LoginModel +MMCA.Common.UI.Pages.Auth.LoginModel.Email.get -> string! +MMCA.Common.UI.Pages.Auth.LoginModel.Email.set -> void +MMCA.Common.UI.Pages.Auth.LoginModel.LoginModel() -> void +MMCA.Common.UI.Pages.Auth.LoginModel.Password.get -> string! +MMCA.Common.UI.Pages.Auth.LoginModel.Password.set -> void +MMCA.Common.UI.Pages.Auth.OAuthComplete +MMCA.Common.UI.Pages.Auth.OAuthComplete.Code.get -> string? +MMCA.Common.UI.Pages.Auth.OAuthComplete.Code.set -> void +MMCA.Common.UI.Pages.Auth.OAuthComplete.Error.get -> string? +MMCA.Common.UI.Pages.Auth.OAuthComplete.Error.set -> void +MMCA.Common.UI.Pages.Auth.OAuthComplete.OAuthComplete() -> void +MMCA.Common.UI.Pages.Auth.OAuthComplete.ReturnUrl.get -> string? +MMCA.Common.UI.Pages.Auth.OAuthComplete.ReturnUrl.set -> void +MMCA.Common.UI.Pages.Auth.PasswordComplexityAttribute +MMCA.Common.UI.Pages.Auth.PasswordComplexityAttribute.PasswordComplexityAttribute() -> void +MMCA.Common.UI.Pages.Auth.Register +MMCA.Common.UI.Pages.Auth.Register.Register() -> void +MMCA.Common.UI.Pages.Auth.RegisterModel +MMCA.Common.UI.Pages.Auth.RegisterModel.AddressLine1.get -> string! +MMCA.Common.UI.Pages.Auth.RegisterModel.AddressLine1.set -> void +MMCA.Common.UI.Pages.Auth.RegisterModel.AddressLine2.get -> string? +MMCA.Common.UI.Pages.Auth.RegisterModel.AddressLine2.set -> void +MMCA.Common.UI.Pages.Auth.RegisterModel.City.get -> string? +MMCA.Common.UI.Pages.Auth.RegisterModel.City.set -> void +MMCA.Common.UI.Pages.Auth.RegisterModel.ConfirmPassword.get -> string! +MMCA.Common.UI.Pages.Auth.RegisterModel.ConfirmPassword.set -> void +MMCA.Common.UI.Pages.Auth.RegisterModel.Country.get -> string? +MMCA.Common.UI.Pages.Auth.RegisterModel.Country.set -> void +MMCA.Common.UI.Pages.Auth.RegisterModel.Email.get -> string! +MMCA.Common.UI.Pages.Auth.RegisterModel.Email.set -> void +MMCA.Common.UI.Pages.Auth.RegisterModel.FirstName.get -> string! +MMCA.Common.UI.Pages.Auth.RegisterModel.FirstName.set -> void +MMCA.Common.UI.Pages.Auth.RegisterModel.LastName.get -> string! +MMCA.Common.UI.Pages.Auth.RegisterModel.LastName.set -> void +MMCA.Common.UI.Pages.Auth.RegisterModel.Password.get -> string! +MMCA.Common.UI.Pages.Auth.RegisterModel.Password.set -> void +MMCA.Common.UI.Pages.Auth.RegisterModel.RegisterModel() -> void +MMCA.Common.UI.Pages.Auth.RegisterModel.State.get -> string? +MMCA.Common.UI.Pages.Auth.RegisterModel.State.set -> void +MMCA.Common.UI.Pages.Auth.RegisterModel.ZipCode.get -> string? +MMCA.Common.UI.Pages.Auth.RegisterModel.ZipCode.set -> void +MMCA.Common.UI.Pages.Common.DataGridListPageBase +MMCA.Common.UI.Pages.Common.DataGridListPageBase.CancelLoading() -> void +MMCA.Common.UI.Pages.Common.DataGridListPageBase.CurrentPageState.get -> int +MMCA.Common.UI.Pages.Common.DataGridListPageBase.CurrentPageState.set -> void +MMCA.Common.UI.Pages.Common.DataGridListPageBase.DataGridListPageBase() -> void +MMCA.Common.UI.Pages.Common.DataGridListPageBase.DenseGrid.get -> bool +MMCA.Common.UI.Pages.Common.DataGridListPageBase.Dispose() -> void +MMCA.Common.UI.Pages.Common.DataGridListPageBase.DisposeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Pages.Common.DataGridListPageBase.Id.get -> System.Guid +MMCA.Common.UI.Pages.Common.DataGridListPageBase.IsLoading.get -> bool +MMCA.Common.UI.Pages.Common.DataGridListPageBase.IsMobile.get -> bool +MMCA.Common.UI.Pages.Common.DataGridListPageBase.LoadFailed.get -> bool +MMCA.Common.UI.Pages.Common.DataGridListPageBase.LoadMobileDataAsync(System.Func!, int, int, string?, string?, System.Threading.CancellationToken, System.Threading.Tasks.Task<(System.Collections.Generic.IReadOnlyList! Items, int TotalItems)>!>! fetchAsync, System.Action!>? additionalFilters = null) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Pages.Common.DataGridListPageBase.LoadServerDataAsync(MudBlazor.GridState! state, System.Func!, int, int, string?, string?, System.Threading.CancellationToken, System.Threading.Tasks.Task<(System.Collections.Generic.IReadOnlyList! Items, int TotalItems)>!>! fetchAsync, System.Action!>? additionalFilters = null, bool showCancelSnackbar = true) -> System.Threading.Tasks.Task!>! +MMCA.Common.UI.Pages.Common.DataGridListPageBase.MobileCurrentPage.get -> int +MMCA.Common.UI.Pages.Common.DataGridListPageBase.MobileCurrentPage.set -> void +MMCA.Common.UI.Pages.Common.DataGridListPageBase.MobileItems.get -> System.Collections.Generic.IReadOnlyList! +MMCA.Common.UI.Pages.Common.DataGridListPageBase.MobilePageSize.get -> int +MMCA.Common.UI.Pages.Common.DataGridListPageBase.MobilePageSize.set -> void +MMCA.Common.UI.Pages.Common.DataGridListPageBase.MobileTotalItems.get -> int +MMCA.Common.UI.Pages.Common.DataGridListPageBase.NotifyBrowserViewportChangeAsync(MudBlazor.BrowserViewportEventArgs! browserViewportEventArgs) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Pages.Common.DataGridListPageBase.OnScrollPositionChanged(double scrollTop) -> void +MMCA.Common.UI.Pages.Common.DataGridListPageBase.ResizeOptions.get -> MudBlazor.Services.ResizeOptions! +MMCA.Common.UI.Pages.Common.DataGridListPageBase.RowsPerPageState.get -> int +MMCA.Common.UI.Pages.Common.DataGridListPageBase.RowsPerPageState.set -> void +MMCA.Common.UI.Pages.Common.DataGridListPageBase.Snackbar.get -> MudBlazor.ISnackbar! +MMCA.Common.UI.Pages.Common.DataGridListPageBase.Snackbar.set -> void +MMCA.Common.UI.Pages.Common.DataGridListPageBase.ToggleDensity() -> void +MMCA.Common.UI.Pages.Common.ErrorMessages +MMCA.Common.UI.Pages.Forbidden +MMCA.Common.UI.Pages.Forbidden.Forbidden() -> void +MMCA.Common.UI.Pages.Home +MMCA.Common.UI.Pages.Home.Home() -> void +MMCA.Common.UI.Pages.NotFound +MMCA.Common.UI.Pages.NotFound.NotFound() -> void +MMCA.Common.UI.Pages.Notifications.NotificationInbox +MMCA.Common.UI.Pages.Notifications.NotificationInbox.Dispose() -> void +MMCA.Common.UI.Pages.Notifications.NotificationInbox.IsLoading.get -> bool +MMCA.Common.UI.Pages.Notifications.NotificationInbox.IsSaving.get -> bool +MMCA.Common.UI.Pages.Notifications.NotificationInbox.NotificationInbox() -> void +MMCA.Common.UI.Pages.Notifications.NotificationList +MMCA.Common.UI.Pages.Notifications.NotificationList.Dispose() -> void +MMCA.Common.UI.Pages.Notifications.NotificationList.IsLoading.get -> bool +MMCA.Common.UI.Pages.Notifications.NotificationList.NotificationList() -> void +MMCA.Common.UI.Pages.Notifications.NotificationSend +MMCA.Common.UI.Pages.Notifications.NotificationSend.Dispose() -> void +MMCA.Common.UI.Pages.Notifications.NotificationSend.IsSaving.get -> bool +MMCA.Common.UI.Pages.Notifications.NotificationSend.NotificationSend() -> void +MMCA.Common.UI.Resources.MudTranslations +MMCA.Common.UI.Resources.MudTranslations.MudTranslations() -> void +MMCA.Common.UI.Resources.SharedResource +MMCA.Common.UI.Resources.SharedResource.SharedResource() -> void +MMCA.Common.UI.Routes +MMCA.Common.UI.Routes.Routes() -> void +MMCA.Common.UI.Services.ApiUserPreferenceReader +MMCA.Common.UI.Services.ApiUserPreferenceReader.ApiUserPreferenceReader(System.Net.Http.IHttpClientFactory! httpClientFactory, MMCA.Common.UI.Services.Auth.ITokenStorageService! tokenStorageService) -> void +MMCA.Common.UI.Services.ApiUserPreferenceReader.GetAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.ApiUserPreferenceWriter +MMCA.Common.UI.Services.ApiUserPreferenceWriter.ApiUserPreferenceWriter(System.Net.Http.IHttpClientFactory! httpClientFactory, MMCA.Common.UI.Services.Auth.ITokenStorageService! tokenStorageService) -> void +MMCA.Common.UI.Services.ApiUserPreferenceWriter.SaveAsync(string? culture, string? theme, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.AuthDelegatingHandler +MMCA.Common.UI.Services.Auth.AuthDelegatingHandler.AuthDelegatingHandler(MMCA.Common.UI.Services.Auth.ITokenStorageService! tokenStorageService) -> void +MMCA.Common.UI.Services.Auth.AuthUIService +MMCA.Common.UI.Services.Auth.AuthUIService.AuthUIService(System.Net.Http.IHttpClientFactory! httpClientFactory, MMCA.Common.UI.Services.Auth.ITokenStorageService! tokenStorageService, MMCA.Common.UI.Services.Auth.ITokenRefresher! tokenRefresher, Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider! authStateProvider, MMCA.Common.UI.Services.Capabilities.IPushRegistrationService! pushRegistration) -> void +MMCA.Common.UI.Services.Auth.AuthUIService.ChangePasswordAsync(string! currentPassword, string! newPassword, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.AuthUIService.ExchangeOAuthCodeAsync(string! code, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.AuthUIService.LastError.get -> string? +MMCA.Common.UI.Services.Auth.AuthUIService.LoginAsync(MMCA.Common.Shared.Auth.LoginRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.AuthUIService.LogoutAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.AuthUIService.RegisterAsync(MMCA.Common.Shared.Auth.RegisterRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.AuthUIService.TryRefreshTokenAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.ConfigurationOAuthUISettings +MMCA.Common.UI.Services.Auth.ConfigurationOAuthUISettings.ConfigurationOAuthUISettings(Microsoft.Extensions.Configuration.IConfiguration! configuration) -> void +MMCA.Common.UI.Services.Auth.ConfigurationOAuthUISettings.GitHubEnabled.get -> bool +MMCA.Common.UI.Services.Auth.ConfigurationOAuthUISettings.GoogleEnabled.get -> bool +MMCA.Common.UI.Services.Auth.DirectApiTokenRefresher +MMCA.Common.UI.Services.Auth.DirectApiTokenRefresher.AcquireAccessTokenAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.DirectApiTokenRefresher.DirectApiTokenRefresher(System.Net.Http.IHttpClientFactory! httpClientFactory, MMCA.Common.UI.Services.Auth.ITokenStorageService! tokenStorageService) -> void +MMCA.Common.UI.Services.Auth.IAuthUIService +MMCA.Common.UI.Services.Auth.IAuthUIService.ChangePasswordAsync(string! currentPassword, string! newPassword, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.IAuthUIService.ExchangeOAuthCodeAsync(string! code, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.IAuthUIService.LastError.get -> string? +MMCA.Common.UI.Services.Auth.IAuthUIService.LoginAsync(MMCA.Common.Shared.Auth.LoginRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.IAuthUIService.LogoutAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.IAuthUIService.RegisterAsync(MMCA.Common.Shared.Auth.RegisterRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.IAuthUIService.TryRefreshTokenAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.IOAuthUISettings +MMCA.Common.UI.Services.Auth.IOAuthUISettings.GitHubEnabled.get -> bool +MMCA.Common.UI.Services.Auth.IOAuthUISettings.GoogleEnabled.get -> bool +MMCA.Common.UI.Services.Auth.ISessionCookieSync +MMCA.Common.UI.Services.Auth.ISessionCookieSync.ClearAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.ISessionCookieSync.SyncAsync(string! accessToken, string! refreshToken) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.ITokenRefresher +MMCA.Common.UI.Services.Auth.ITokenRefresher.AcquireAccessTokenAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.ITokenStorageService +MMCA.Common.UI.Services.Auth.ITokenStorageService.ClearTokensAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.ITokenStorageService.GetAccessTokenAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.ITokenStorageService.GetRefreshTokenAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.ITokenStorageService.SetTokensAsync(string! accessToken, string! refreshToken) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.JsFetchSessionCookieSync +MMCA.Common.UI.Services.Auth.JsFetchSessionCookieSync.ClearAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.JsFetchSessionCookieSync.JsFetchSessionCookieSync(Microsoft.JSInterop.IJSRuntime! jsRuntime) -> void +MMCA.Common.UI.Services.Auth.JsFetchSessionCookieSync.SyncAsync(string! accessToken, string! refreshToken) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.JwtAuthenticationStateProvider +MMCA.Common.UI.Services.Auth.JwtAuthenticationStateProvider.JwtAuthenticationStateProvider(MMCA.Common.UI.Services.Auth.ITokenStorageService! tokenStorageService) -> void +MMCA.Common.UI.Services.Auth.JwtAuthenticationStateProvider.NotifyUserAuthentication(string! token) -> void +MMCA.Common.UI.Services.Auth.JwtAuthenticationStateProvider.NotifyUserLogout() -> void +MMCA.Common.UI.Services.Auth.JwtTokenInfo +MMCA.Common.UI.Services.Auth.SameOriginProxyTokenRefresher +MMCA.Common.UI.Services.Auth.SameOriginProxyTokenRefresher.AcquireAccessTokenAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.SameOriginProxyTokenRefresher.SameOriginProxyTokenRefresher(Microsoft.JSInterop.IJSRuntime! jsRuntime) -> void +MMCA.Common.UI.Services.Auth.WasmTokenStorageService +MMCA.Common.UI.Services.Auth.WasmTokenStorageService.ClearTokensAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.WasmTokenStorageService.GetAccessTokenAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.WasmTokenStorageService.GetRefreshTokenAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.WasmTokenStorageService.SetTokensAsync(string! accessToken, string! refreshToken) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Auth.WasmTokenStorageService.WasmTokenStorageService(MMCA.Common.UI.Services.Auth.ISessionCookieSync! sessionCookieSync, MMCA.Common.UI.Services.Auth.ITokenRefresher! tokenRefresher) -> void +MMCA.Common.UI.Services.AuthenticatedServiceBase +MMCA.Common.UI.Services.AuthenticatedServiceBase.AuthenticatedServiceBase(System.Net.Http.IHttpClientFactory! httpClientFactory, MMCA.Common.UI.Services.Auth.ITokenStorageService! tokenStorageService) -> void +MMCA.Common.UI.Services.AuthenticatedServiceBase.CreateAuthenticatedClientAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Browser.BrowserAccessibilityAnnouncer +MMCA.Common.UI.Services.Capabilities.Browser.BrowserAccessibilityAnnouncer.AnnounceAsync(string! message, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Browser.BrowserAccessibilityAnnouncer.BrowserAccessibilityAnnouncer(MMCA.Common.UI.Services.Capabilities.Browser.CapabilitiesJsModule! module) -> void +MMCA.Common.UI.Services.Capabilities.Browser.BrowserClipboardService +MMCA.Common.UI.Services.Capabilities.Browser.BrowserClipboardService.BrowserClipboardService(MMCA.Common.UI.Services.Capabilities.Browser.CapabilitiesJsModule! module) -> void +MMCA.Common.UI.Services.Capabilities.Browser.BrowserClipboardService.SetTextAsync(string! text, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Browser.BrowserConnectivityStatusService +MMCA.Common.UI.Services.Capabilities.Browser.BrowserConnectivityStatusService.BrowserConnectivityStatusService(MMCA.Common.UI.Services.Capabilities.Browser.CapabilitiesJsModule! module) -> void +MMCA.Common.UI.Services.Capabilities.Browser.BrowserConnectivityStatusService.ConnectivityChanged -> System.EventHandler? +MMCA.Common.UI.Services.Capabilities.Browser.BrowserConnectivityStatusService.DisposeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Services.Capabilities.Browser.BrowserConnectivityStatusService.InitializeAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Services.Capabilities.Browser.BrowserConnectivityStatusService.IsOnline.get -> bool +MMCA.Common.UI.Services.Capabilities.Browser.BrowserConnectivityStatusService.OnBrowserConnectivityChanged(bool isOnline) -> void +MMCA.Common.UI.Services.Capabilities.Browser.BrowserDevicePreferences +MMCA.Common.UI.Services.Capabilities.Browser.BrowserDevicePreferences.BrowserDevicePreferences(MMCA.Common.UI.Services.Capabilities.Browser.CapabilitiesJsModule! module) -> void +MMCA.Common.UI.Services.Capabilities.Browser.BrowserDevicePreferences.GetAsync(string! key, T fallback, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Browser.BrowserDevicePreferences.IsPersistent.get -> bool +MMCA.Common.UI.Services.Capabilities.Browser.BrowserDevicePreferences.RemoveAsync(string! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Browser.BrowserDevicePreferences.SetAsync(string! key, T value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Browser.BrowserExternalLinkService +MMCA.Common.UI.Services.Capabilities.Browser.BrowserExternalLinkService.BrowserExternalLinkService(MMCA.Common.UI.Services.Capabilities.Browser.CapabilitiesJsModule! module) -> void +MMCA.Common.UI.Services.Capabilities.Browser.BrowserExternalLinkService.InterceptsLinks.get -> bool +MMCA.Common.UI.Services.Capabilities.Browser.BrowserExternalLinkService.OpenAsync(System.Uri! uri, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Browser.BrowserLocalCacheStore +MMCA.Common.UI.Services.Capabilities.Browser.BrowserLocalCacheStore.BrowserLocalCacheStore(MMCA.Common.UI.Services.Capabilities.Browser.CapabilitiesJsModule! module) -> void +MMCA.Common.UI.Services.Capabilities.Browser.BrowserLocalCacheStore.GetAsync(string! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Browser.BrowserLocalCacheStore.IsAvailable.get -> bool +MMCA.Common.UI.Services.Capabilities.Browser.BrowserLocalCacheStore.RemoveAsync(string! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Browser.BrowserLocalCacheStore.SetAsync(string! key, T value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Browser.BrowserMapNavigationService +MMCA.Common.UI.Services.Capabilities.Browser.BrowserMapNavigationService.BrowserMapNavigationService(MMCA.Common.UI.Services.Capabilities.IExternalLinkService! externalLinkService) -> void +MMCA.Common.UI.Services.Capabilities.Browser.BrowserMapNavigationService.OpenAddressAsync(string! address, string? label, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Browser.BrowserShareService +MMCA.Common.UI.Services.Capabilities.Browser.BrowserShareService.BrowserShareService(MMCA.Common.UI.Services.Capabilities.Browser.CapabilitiesJsModule! module) -> void +MMCA.Common.UI.Services.Capabilities.Browser.BrowserShareService.ShareFileAsync(string! title, string! filePath, string! contentType, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Browser.BrowserShareService.ShareLinkAsync(string! title, System.Uri! uri, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Browser.CapabilitiesJsModule +MMCA.Common.UI.Services.Capabilities.Browser.CapabilitiesJsModule.CapabilitiesJsModule(Microsoft.JSInterop.IJSRuntime! jsRuntime) -> void +MMCA.Common.UI.Services.Capabilities.Browser.CapabilitiesJsModule.DisposeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Services.Capabilities.Browser.CapabilitiesJsModule.InvokeOrDefaultAsync(string! identifier, object?[]! args, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Services.Capabilities.DeepLinkDispatcher +MMCA.Common.UI.Services.Capabilities.DeepLinkDispatcher.DeepLinkDispatcher() -> void +MMCA.Common.UI.Services.Capabilities.DeepLinkDispatcher.Publish(string! route) -> void +MMCA.Common.UI.Services.Capabilities.DeepLinkDispatcher.RouteRequested -> System.EventHandler? +MMCA.Common.UI.Services.Capabilities.DeepLinkDispatcher.TryConsumePending(out string? route) -> bool +MMCA.Common.UI.Services.Capabilities.DeepLinkRouteEventArgs +MMCA.Common.UI.Services.Capabilities.DeepLinkRouteEventArgs.DeepLinkRouteEventArgs(string! route) -> void +MMCA.Common.UI.Services.Capabilities.DeepLinkRouteEventArgs.Route.get -> string! +MMCA.Common.UI.Services.Capabilities.DependencyInjection +MMCA.Common.UI.Services.Capabilities.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!) +MMCA.Common.UI.Services.Capabilities.DependencyInjection.extension(Microsoft.Extensions.DependencyInjection.IServiceCollection!).AddBrowserDeviceCapabilities() -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +MMCA.Common.UI.Services.Capabilities.DevicePreferenceKeys +MMCA.Common.UI.Services.Capabilities.Fallbacks.AlwaysOnlineConnectivityStatusService +MMCA.Common.UI.Services.Capabilities.Fallbacks.AlwaysOnlineConnectivityStatusService.AlwaysOnlineConnectivityStatusService() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.AlwaysOnlineConnectivityStatusService.ConnectivityChanged -> System.EventHandler? +MMCA.Common.UI.Services.Capabilities.Fallbacks.AlwaysOnlineConnectivityStatusService.InitializeAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Services.Capabilities.Fallbacks.AlwaysOnlineConnectivityStatusService.IsOnline.get -> bool +MMCA.Common.UI.Services.Capabilities.Fallbacks.InMemoryDevicePreferences +MMCA.Common.UI.Services.Capabilities.Fallbacks.InMemoryDevicePreferences.GetAsync(string! key, T fallback, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.InMemoryDevicePreferences.InMemoryDevicePreferences() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.InMemoryDevicePreferences.IsPersistent.get -> bool +MMCA.Common.UI.Services.Capabilities.Fallbacks.InMemoryDevicePreferences.RemoveAsync(string! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.InMemoryDevicePreferences.SetAsync(string! key, T value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullAccessibilityAnnouncer +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullAccessibilityAnnouncer.AnnounceAsync(string! message, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullAccessibilityAnnouncer.NullAccessibilityAnnouncer() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullBarcodeScannerService +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullBarcodeScannerService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullBarcodeScannerService.NullBarcodeScannerService() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullBarcodeScannerService.ScanAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullBatteryStatusService +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullBatteryStatusService.EnergySaverChanged -> System.EventHandler? +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullBatteryStatusService.IsEnergySaverOn.get -> bool +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullBatteryStatusService.NullBatteryStatusService() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullBiometricAuthenticator +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullBiometricAuthenticator.AuthenticateAsync(string! reason, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullBiometricAuthenticator.IsAvailableAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullBiometricAuthenticator.NullBiometricAuthenticator() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullClipboardService +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullClipboardService.NullClipboardService() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullClipboardService.SetTextAsync(string! text, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullExternalLinkService +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullExternalLinkService.InterceptsLinks.get -> bool +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullExternalLinkService.NullExternalLinkService() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullExternalLinkService.OpenAsync(System.Uri! uri, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullGeocodingService +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullGeocodingService.GeocodeAsync(string! address, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullGeocodingService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullGeocodingService.NullGeocodingService() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullGeolocationService +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullGeolocationService.GetCurrentOrLastKnownAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullGeolocationService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullGeolocationService.NullGeolocationService() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullHapticFeedbackService +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullHapticFeedbackService.Click() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullHapticFeedbackService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullHapticFeedbackService.LongPress() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullHapticFeedbackService.NullHapticFeedbackService() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullHapticFeedbackService.Vibrate(System.TimeSpan duration) -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullLocalCacheStore +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullLocalCacheStore.GetAsync(string! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullLocalCacheStore.IsAvailable.get -> bool +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullLocalCacheStore.NullLocalCacheStore() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullLocalCacheStore.RemoveAsync(string! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullLocalCacheStore.SetAsync(string! key, T value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullLocalNotificationService +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullLocalNotificationService.CancelAllAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullLocalNotificationService.CancelAsync(System.Collections.Generic.IReadOnlyCollection! ids, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullLocalNotificationService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullLocalNotificationService.NullLocalNotificationService() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullLocalNotificationService.RequestPermissionAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullLocalNotificationService.ScheduleAsync(MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullMapNavigationService +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullMapNavigationService.NullMapNavigationService() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullMapNavigationService.OpenAddressAsync(string! address, string? label, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullMediaPickerService +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullMediaPickerService.CapturePhotoAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullMediaPickerService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullMediaPickerService.NullMediaPickerService() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullMediaPickerService.PickPhotoAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullPushDeviceTokenProvider +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullPushDeviceTokenProvider.GetTokenAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullPushDeviceTokenProvider.NullPushDeviceTokenProvider() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullPushRegistrationService +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullPushRegistrationService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullPushRegistrationService.NullPushRegistrationService() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullPushRegistrationService.RegisterAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullPushRegistrationService.UnregisterAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullScreenshotService +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullScreenshotService.CaptureToFileAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullScreenshotService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullScreenshotService.NullScreenshotService() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullShareService +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullShareService.NullShareService() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullShareService.ShareFileAsync(string! title, string! filePath, string! contentType, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullShareService.ShareLinkAsync(string! title, System.Uri! uri, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullSpeechToTextService +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullSpeechToTextService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullSpeechToTextService.ListenAsync(System.Globalization.CultureInfo! culture, System.IProgress? partialResults, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullSpeechToTextService.NullSpeechToTextService() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullTextToSpeechService +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullTextToSpeechService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullTextToSpeechService.NullTextToSpeechService() -> void +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullTextToSpeechService.SpeakAsync(string! text, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.NullTextToSpeechService.StopAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.UnavailableExternalAuthBroker +MMCA.Common.UI.Services.Capabilities.Fallbacks.UnavailableExternalAuthBroker.IsAvailable.get -> bool +MMCA.Common.UI.Services.Capabilities.Fallbacks.UnavailableExternalAuthBroker.SignInAsync(string! provider, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.Fallbacks.UnavailableExternalAuthBroker.UnavailableExternalAuthBroker() -> void +MMCA.Common.UI.Services.Capabilities.GeoPoint +MMCA.Common.UI.Services.Capabilities.GeoPoint.$() -> MMCA.Common.UI.Services.Capabilities.GeoPoint! +MMCA.Common.UI.Services.Capabilities.GeoPoint.Deconstruct(out double Latitude, out double Longitude) -> void +MMCA.Common.UI.Services.Capabilities.GeoPoint.DistanceKmTo(MMCA.Common.UI.Services.Capabilities.GeoPoint! other) -> double +MMCA.Common.UI.Services.Capabilities.GeoPoint.Equals(MMCA.Common.UI.Services.Capabilities.GeoPoint? other) -> bool +MMCA.Common.UI.Services.Capabilities.GeoPoint.GeoPoint(double Latitude, double Longitude) -> void +MMCA.Common.UI.Services.Capabilities.GeoPoint.Latitude.get -> double +MMCA.Common.UI.Services.Capabilities.GeoPoint.Latitude.init -> void +MMCA.Common.UI.Services.Capabilities.GeoPoint.Longitude.get -> double +MMCA.Common.UI.Services.Capabilities.GeoPoint.Longitude.init -> void +MMCA.Common.UI.Services.Capabilities.IAccessibilityAnnouncer +MMCA.Common.UI.Services.Capabilities.IAccessibilityAnnouncer.AnnounceAsync(string! message, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IBarcodeScannerService +MMCA.Common.UI.Services.Capabilities.IBarcodeScannerService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.IBarcodeScannerService.ScanAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IBatteryStatusService +MMCA.Common.UI.Services.Capabilities.IBatteryStatusService.EnergySaverChanged -> System.EventHandler? +MMCA.Common.UI.Services.Capabilities.IBatteryStatusService.IsEnergySaverOn.get -> bool +MMCA.Common.UI.Services.Capabilities.IBiometricAuthenticator +MMCA.Common.UI.Services.Capabilities.IBiometricAuthenticator.AuthenticateAsync(string! reason, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IBiometricAuthenticator.IsAvailableAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IClipboardService +MMCA.Common.UI.Services.Capabilities.IClipboardService.SetTextAsync(string! text, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IConnectivityStatusService +MMCA.Common.UI.Services.Capabilities.IConnectivityStatusService.ConnectivityChanged -> System.EventHandler? +MMCA.Common.UI.Services.Capabilities.IConnectivityStatusService.InitializeAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Services.Capabilities.IConnectivityStatusService.IsOnline.get -> bool +MMCA.Common.UI.Services.Capabilities.IDeepLinkDispatcher +MMCA.Common.UI.Services.Capabilities.IDeepLinkDispatcher.Publish(string! route) -> void +MMCA.Common.UI.Services.Capabilities.IDeepLinkDispatcher.RouteRequested -> System.EventHandler? +MMCA.Common.UI.Services.Capabilities.IDeepLinkDispatcher.TryConsumePending(out string? route) -> bool +MMCA.Common.UI.Services.Capabilities.IDevicePreferences +MMCA.Common.UI.Services.Capabilities.IDevicePreferences.GetAsync(string! key, T fallback, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IDevicePreferences.IsPersistent.get -> bool +MMCA.Common.UI.Services.Capabilities.IDevicePreferences.RemoveAsync(string! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IDevicePreferences.SetAsync(string! key, T value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IExternalAuthBroker +MMCA.Common.UI.Services.Capabilities.IExternalAuthBroker.IsAvailable.get -> bool +MMCA.Common.UI.Services.Capabilities.IExternalAuthBroker.SignInAsync(string! provider, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IExternalLinkService +MMCA.Common.UI.Services.Capabilities.IExternalLinkService.InterceptsLinks.get -> bool +MMCA.Common.UI.Services.Capabilities.IExternalLinkService.OpenAsync(System.Uri! uri, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IGeocodingService +MMCA.Common.UI.Services.Capabilities.IGeocodingService.GeocodeAsync(string! address, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IGeocodingService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.IGeolocationService +MMCA.Common.UI.Services.Capabilities.IGeolocationService.GetCurrentOrLastKnownAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IGeolocationService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.IHapticFeedbackService +MMCA.Common.UI.Services.Capabilities.IHapticFeedbackService.Click() -> void +MMCA.Common.UI.Services.Capabilities.IHapticFeedbackService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.IHapticFeedbackService.LongPress() -> void +MMCA.Common.UI.Services.Capabilities.IHapticFeedbackService.Vibrate(System.TimeSpan duration) -> void +MMCA.Common.UI.Services.Capabilities.ILocalCacheStore +MMCA.Common.UI.Services.Capabilities.ILocalCacheStore.GetAsync(string! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.ILocalCacheStore.IsAvailable.get -> bool +MMCA.Common.UI.Services.Capabilities.ILocalCacheStore.RemoveAsync(string! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.ILocalCacheStore.SetAsync(string! key, T value, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.ILocalNotificationService +MMCA.Common.UI.Services.Capabilities.ILocalNotificationService.CancelAllAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.ILocalNotificationService.CancelAsync(System.Collections.Generic.IReadOnlyCollection! ids, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.ILocalNotificationService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.ILocalNotificationService.RequestPermissionAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.ILocalNotificationService.ScheduleAsync(MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IMapNavigationService +MMCA.Common.UI.Services.Capabilities.IMapNavigationService.OpenAddressAsync(string! address, string? label, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IMediaPickerService +MMCA.Common.UI.Services.Capabilities.IMediaPickerService.CapturePhotoAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IMediaPickerService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.IMediaPickerService.PickPhotoAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IPushDeviceTokenProvider +MMCA.Common.UI.Services.Capabilities.IPushDeviceTokenProvider.GetTokenAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IPushRegistrationService +MMCA.Common.UI.Services.Capabilities.IPushRegistrationService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.IPushRegistrationService.RegisterAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IPushRegistrationService.UnregisterAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IScreenshotService +MMCA.Common.UI.Services.Capabilities.IScreenshotService.CaptureToFileAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IScreenshotService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.IShareService +MMCA.Common.UI.Services.Capabilities.IShareService.ShareFileAsync(string! title, string! filePath, string! contentType, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.IShareService.ShareLinkAsync(string! title, System.Uri! uri, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.ISpeechToTextService +MMCA.Common.UI.Services.Capabilities.ISpeechToTextService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.ISpeechToTextService.ListenAsync(System.Globalization.CultureInfo! culture, System.IProgress? partialResults, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.ITextToSpeechService +MMCA.Common.UI.Services.Capabilities.ITextToSpeechService.IsSupported.get -> bool +MMCA.Common.UI.Services.Capabilities.ITextToSpeechService.SpeakAsync(string! text, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.ITextToSpeechService.StopAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest +MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.$() -> MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest! +MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.Body.get -> string! +MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.Body.init -> void +MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.Deconstruct(out int Id, out string! Title, out string! Body, out System.DateTimeOffset DeliverAt, out string? DeepLinkRoute) -> void +MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.DeepLinkRoute.get -> string? +MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.DeepLinkRoute.init -> void +MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.DeliverAt.get -> System.DateTimeOffset +MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.DeliverAt.init -> void +MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.Equals(MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest? other) -> bool +MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.Id.get -> int +MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.Id.init -> void +MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.LocalNotificationRequest(int Id, string! Title, string! Body, System.DateTimeOffset DeliverAt, string? DeepLinkRoute) -> void +MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.Title.get -> string! +MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.Title.init -> void +MMCA.Common.UI.Services.Capabilities.PickedMedia +MMCA.Common.UI.Services.Capabilities.PickedMedia.Content.get -> System.IO.Stream! +MMCA.Common.UI.Services.Capabilities.PickedMedia.ContentType.get -> string! +MMCA.Common.UI.Services.Capabilities.PickedMedia.Dispose() -> void +MMCA.Common.UI.Services.Capabilities.PickedMedia.FileName.get -> string! +MMCA.Common.UI.Services.Capabilities.PickedMedia.PickedMedia(System.IO.Stream! content, string! fileName, string! contentType) -> void +MMCA.Common.UI.Services.Capabilities.PushDeviceToken +MMCA.Common.UI.Services.Capabilities.PushDeviceToken.$() -> MMCA.Common.UI.Services.Capabilities.PushDeviceToken! +MMCA.Common.UI.Services.Capabilities.PushDeviceToken.Deconstruct(out string! Platform, out string! Token) -> void +MMCA.Common.UI.Services.Capabilities.PushDeviceToken.Equals(MMCA.Common.UI.Services.Capabilities.PushDeviceToken? other) -> bool +MMCA.Common.UI.Services.Capabilities.PushDeviceToken.Platform.get -> string! +MMCA.Common.UI.Services.Capabilities.PushDeviceToken.Platform.init -> void +MMCA.Common.UI.Services.Capabilities.PushDeviceToken.PushDeviceToken(string! Platform, string! Token) -> void +MMCA.Common.UI.Services.Capabilities.PushDeviceToken.Token.get -> string! +MMCA.Common.UI.Services.Capabilities.PushDeviceToken.Token.init -> void +MMCA.Common.UI.Services.ChildEntityServiceBase +MMCA.Common.UI.Services.ChildEntityServiceBase.ChildEntityServiceBase(System.Net.Http.IHttpClientFactory! httpClientFactory, MMCA.Common.UI.Services.Auth.ITokenStorageService! tokenStorageService, string! endpoint) -> void +MMCA.Common.UI.Services.ChildEntityServiceBase.DeleteByIdAsync(string! id, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.ChildEntityServiceBase.PostAsync(TRequest request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.CultureDelegatingHandler +MMCA.Common.UI.Services.CultureDelegatingHandler.CultureDelegatingHandler() -> void +MMCA.Common.UI.Services.EndpointCultureApplier +MMCA.Common.UI.Services.EndpointCultureApplier.ApplyAsync(string! culture, string! returnPath, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.EndpointCultureApplier.EndpointCultureApplier(Microsoft.AspNetCore.Components.NavigationManager! navigation) -> void +MMCA.Common.UI.Services.EntityServiceBase +MMCA.Common.UI.Services.EntityServiceBase.Endpoint.get -> string! +MMCA.Common.UI.Services.EntityServiceBase.EntityServiceBase(string! endpoint, System.Net.Http.IHttpClientFactory! httpClientFactory, MMCA.Common.UI.Services.Auth.ITokenStorageService! tokenStorageService) -> void +MMCA.Common.UI.Services.EntityServiceBase.SendRequestAsync(System.Func!>! httpAction, System.Threading.CancellationToken cancellationToken, bool treatNotFoundAsDefault = false, bool throwIfNull = false, bool expectContent = true, string? idempotencyKey = null) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.ICultureApplier +MMCA.Common.UI.Services.ICultureApplier.ApplyAsync(string! culture, string! returnPath, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.IFormFactor +MMCA.Common.UI.Services.IFormFactor.GetFormFactor() -> string! +MMCA.Common.UI.Services.IFormFactor.GetPlatform() -> string! +MMCA.Common.UI.Services.IUserPreferenceReader +MMCA.Common.UI.Services.IUserPreferenceReader.GetAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.IUserPreferenceWriter +MMCA.Common.UI.Services.IUserPreferenceWriter.SaveAsync(string? culture, string? theme, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.ListPageQueryStateService +MMCA.Common.UI.Services.ListPageQueryStateService.ListPageQueryStateService(Microsoft.AspNetCore.Components.NavigationManager! navigation) -> void +MMCA.Common.UI.Services.ListPageQueryStateService.ReadCurrent() -> MMCA.Common.UI.Services.ListPageState! +MMCA.Common.UI.Services.ListPageQueryStateService.ReplaceState(string! basePath, MMCA.Common.UI.Services.ListPageState! state) -> void +MMCA.Common.UI.Services.ListPageState +MMCA.Common.UI.Services.ListPageState.$() -> MMCA.Common.UI.Services.ListPageState! +MMCA.Common.UI.Services.ListPageState.DenseGrid.get -> bool +MMCA.Common.UI.Services.ListPageState.DenseGrid.init -> void +MMCA.Common.UI.Services.ListPageState.Equals(MMCA.Common.UI.Services.ListPageState? other) -> bool +MMCA.Common.UI.Services.ListPageState.Filters.get -> System.Collections.Generic.IReadOnlyDictionary! +MMCA.Common.UI.Services.ListPageState.Filters.init -> void +MMCA.Common.UI.Services.ListPageState.ListPageState() -> void +MMCA.Common.UI.Services.ListPageState.MobilePage.get -> int +MMCA.Common.UI.Services.ListPageState.MobilePage.init -> void +MMCA.Common.UI.Services.ListPageState.Page.get -> int +MMCA.Common.UI.Services.ListPageState.Page.init -> void +MMCA.Common.UI.Services.ListPageState.PageSize.get -> int +MMCA.Common.UI.Services.ListPageState.PageSize.init -> void +MMCA.Common.UI.Services.ListPageState.ScrollPosition.get -> double +MMCA.Common.UI.Services.ListPageState.ScrollPosition.init -> void +MMCA.Common.UI.Services.ListPageState.SortColumn.get -> string? +MMCA.Common.UI.Services.ListPageState.SortColumn.init -> void +MMCA.Common.UI.Services.ListPageState.SortDescending.get -> bool +MMCA.Common.UI.Services.ListPageState.SortDescending.init -> void +MMCA.Common.UI.Services.ListPageStateService +MMCA.Common.UI.Services.ListPageStateService.DisposeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Services.ListPageStateService.GetState(string! routePath) -> MMCA.Common.UI.Services.ListPageState? +MMCA.Common.UI.Services.ListPageStateService.HydrateFromSessionAsync(string! routePath) -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Services.ListPageStateService.ListPageStateService(Microsoft.JSInterop.IJSRuntime! js) -> void +MMCA.Common.UI.Services.ListPageStateService.PersistToSessionAsync(string! routePath) -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Services.ListPageStateService.SaveState(string! routePath, MMCA.Common.UI.Services.ListPageState! state) -> void +MMCA.Common.UI.Services.ListPageStateService.UpdateScrollPosition(string! routePath, double scrollPosition) -> void +MMCA.Common.UI.Services.MmcaCultureBootstrap +MMCA.Common.UI.Services.Navigation.BackNavigationResult +MMCA.Common.UI.Services.Navigation.BackNavigationResult.$() -> MMCA.Common.UI.Services.Navigation.BackNavigationResult! +MMCA.Common.UI.Services.Navigation.BackNavigationResult.AtRoot.get -> bool +MMCA.Common.UI.Services.Navigation.BackNavigationResult.AtRoot.init -> void +MMCA.Common.UI.Services.Navigation.BackNavigationResult.BackNavigationResult(bool Handled, bool AtRoot) -> void +MMCA.Common.UI.Services.Navigation.BackNavigationResult.Deconstruct(out bool Handled, out bool AtRoot) -> void +MMCA.Common.UI.Services.Navigation.BackNavigationResult.Equals(MMCA.Common.UI.Services.Navigation.BackNavigationResult? other) -> bool +MMCA.Common.UI.Services.Navigation.BackNavigationResult.Handled.get -> bool +MMCA.Common.UI.Services.Navigation.BackNavigationResult.Handled.init -> void +MMCA.Common.UI.Services.Navigation.MauiBackNavigationBridge +MMCA.Common.UI.Services.Navigation.NavigationHistoryService +MMCA.Common.UI.Services.Navigation.NavigationHistoryService.CanGoBackAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Services.Navigation.NavigationHistoryService.DisposeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Services.Navigation.NavigationHistoryService.GoBackAsync(string! fallback = "/") -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Services.Navigation.NavigationHistoryService.NavigationHistoryService(Microsoft.AspNetCore.Components.NavigationManager! navigation, Microsoft.JSInterop.IJSRuntime! js) -> void +MMCA.Common.UI.Services.Navigation.ReturnUrlProtector +MMCA.Common.UI.Services.Notifications.INotificationInboxUIService +MMCA.Common.UI.Services.Notifications.INotificationInboxUIService.GetInboxAsync(int pageNumber = 1, int pageSize = 20, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task?>! +MMCA.Common.UI.Services.Notifications.INotificationInboxUIService.GetUnreadCountAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Notifications.INotificationInboxUIService.MarkAllReadAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Notifications.INotificationInboxUIService.MarkReadAsync(int id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Notifications.INotificationScopeProvider +MMCA.Common.UI.Services.Notifications.INotificationScopeProvider.GetCurrentScopeKeyAsync(System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Notifications.IPushNotificationUIService +MMCA.Common.UI.Services.Notifications.IPushNotificationUIService.GetHistoryAsync(int pageNumber = 1, int pageSize = 10, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task?>! +MMCA.Common.UI.Services.Notifications.IPushNotificationUIService.SendAsync(MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Notifications.NotificationHubService +MMCA.Common.UI.Services.Notifications.NotificationHubService.DisposeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Services.Notifications.NotificationHubService.IsConnected.get -> bool +MMCA.Common.UI.Services.Notifications.NotificationHubService.JoinChannelAsync(string! channelKey) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Notifications.NotificationHubService.LeaveChannelAsync(string! channelKey) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Notifications.NotificationHubService.NotificationCallback.get -> System.Func? +MMCA.Common.UI.Services.Notifications.NotificationHubService.NotificationCallback.set -> void +MMCA.Common.UI.Services.Notifications.NotificationHubService.NotificationHubService(MMCA.Common.UI.Services.Auth.ITokenStorageService! tokenStorageService, Microsoft.Extensions.Options.IOptions! apiSettings, Microsoft.Extensions.Logging.ILogger! logger) -> void +MMCA.Common.UI.Services.Notifications.NotificationHubService.OnChannelEvent(string! channelKey, System.Func! handler) -> System.IDisposable! +MMCA.Common.UI.Services.Notifications.NotificationHubService.StartAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Notifications.NotificationHubService.StopAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Notifications.NotificationInboxService +MMCA.Common.UI.Services.Notifications.NotificationInboxService.GetInboxAsync(int pageNumber = 1, int pageSize = 20, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task?>! +MMCA.Common.UI.Services.Notifications.NotificationInboxService.GetUnreadCountAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Notifications.NotificationInboxService.MarkAllReadAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Notifications.NotificationInboxService.MarkReadAsync(int id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Notifications.NotificationInboxService.NotificationInboxService(System.Net.Http.IHttpClientFactory! httpClientFactory, MMCA.Common.UI.Services.Auth.ITokenStorageService! tokenStorageService, MMCA.Common.UI.Services.Notifications.INotificationScopeProvider! scopeProvider) -> void +MMCA.Common.UI.Services.Notifications.NotificationState +MMCA.Common.UI.Services.Notifications.NotificationState.IncrementUnreadCount() -> void +MMCA.Common.UI.Services.Notifications.NotificationState.NotificationState() -> void +MMCA.Common.UI.Services.Notifications.NotificationState.OnChange -> System.EventHandler? +MMCA.Common.UI.Services.Notifications.NotificationState.OnRefreshRequested -> System.EventHandler? +MMCA.Common.UI.Services.Notifications.NotificationState.RequestRefresh() -> void +MMCA.Common.UI.Services.Notifications.NotificationState.SetUnreadCount(int count) -> void +MMCA.Common.UI.Services.Notifications.NotificationState.TryRegisterPoller() -> bool +MMCA.Common.UI.Services.Notifications.NotificationState.UnreadCount.get -> int +MMCA.Common.UI.Services.Notifications.NotificationState.UnregisterPoller() -> void +MMCA.Common.UI.Services.Notifications.NullNotificationScopeProvider +MMCA.Common.UI.Services.Notifications.NullNotificationScopeProvider.GetCurrentScopeKeyAsync(System.Threading.CancellationToken ct = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.Notifications.NullNotificationScopeProvider.NullNotificationScopeProvider() -> void +MMCA.Common.UI.Services.Notifications.PushNotificationService +MMCA.Common.UI.Services.Notifications.PushNotificationService.GetHistoryAsync(int pageNumber = 1, int pageSize = 10, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task?>! +MMCA.Common.UI.Services.Notifications.PushNotificationService.PushNotificationService(System.Net.Http.IHttpClientFactory! httpClientFactory, MMCA.Common.UI.Services.Auth.ITokenStorageService! tokenStorageService, MMCA.Common.UI.Services.Notifications.INotificationScopeProvider! scopeProvider) -> void +MMCA.Common.UI.Services.Notifications.PushNotificationService.SendAsync(MMCA.Common.Shared.Notifications.PushNotifications.SendPushNotificationRequest! request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.ServiceExceptionHelper +MMCA.Common.UI.Services.ThemeService +MMCA.Common.UI.Services.ThemeService.DisposeAsync() -> System.Threading.Tasks.ValueTask +MMCA.Common.UI.Services.ThemeService.InitializeAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.ThemeService.IsDarkMode.get -> bool +MMCA.Common.UI.Services.ThemeService.IsInitialized.get -> bool +MMCA.Common.UI.Services.ThemeService.OnChange -> System.EventHandler? +MMCA.Common.UI.Services.ThemeService.SetDarkModeAsync(bool isDarkMode) -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.ThemeService.ThemeService(Microsoft.JSInterop.IJSRuntime! jsRuntime) -> void +MMCA.Common.UI.Services.ThemeService.ToggleAsync() -> System.Threading.Tasks.Task! +MMCA.Common.UI.Services.UserPreferences +MMCA.Common.UI.Services.UserPreferences.$() -> MMCA.Common.UI.Services.UserPreferences! +MMCA.Common.UI.Services.UserPreferences.Culture.get -> string? +MMCA.Common.UI.Services.UserPreferences.Culture.init -> void +MMCA.Common.UI.Services.UserPreferences.Deconstruct(out string? Culture, out string? Theme) -> void +MMCA.Common.UI.Services.UserPreferences.Equals(MMCA.Common.UI.Services.UserPreferences? other) -> bool +MMCA.Common.UI.Services.UserPreferences.Theme.get -> string? +MMCA.Common.UI.Services.UserPreferences.Theme.init -> void +MMCA.Common.UI.Services.UserPreferences.UserPreferences(string? Culture, string? Theme) -> void +MMCA.Common.UI.Services.WasmFormFactor +MMCA.Common.UI.Services.WasmFormFactor.GetFormFactor() -> string! +MMCA.Common.UI.Services.WasmFormFactor.GetPlatform() -> string! +MMCA.Common.UI.Services.WasmFormFactor.WasmFormFactor() -> void +MMCA.Common.UI.Theme.BrandColors +MMCA.Common.UI.Theme.MMCATheme +MMCA.Common.UI.UISharedAssemblyReference +MMCA.Common.UI.UISharedAssemblyReference.UISharedAssemblyReference() -> void +MMCA.Common.UI._Imports +MMCA.Common.UI._Imports.Execute() -> void +MMCA.Common.UI._Imports._Imports() -> void +abstract MMCA.Common.UI.Pages.Common.DataGridListPageBase.Title.get -> string! +const MMCA.Common.UI.Services.Capabilities.DevicePreferenceKeys.AppLockEnabled = "applock.enabled" -> string! +const MMCA.Common.UI.Theme.BrandColors.Primary = "#1565C0" -> string! +const MMCA.Common.UI.Theme.BrandColors.PrimaryDark = "#0D47A1" -> string! +const MMCA.Common.UI.Theme.BrandColors.PrimaryLight = "#42A5F5" -> string! +const MMCA.Common.UI.Theme.BrandColors.Secondary = "#00796B" -> string! +const MMCA.Common.UI.Theme.BrandColors.SecondaryDark = "#00695C" -> string! +const MMCA.Common.UI.Theme.BrandColors.SecondaryLight = "#4DB6AC" -> string! +override MMCA.Common.UI.Common.NavItem.Equals(object? obj) -> bool +override MMCA.Common.UI.Common.NavItem.GetHashCode() -> int +override MMCA.Common.UI.Common.NavItem.ToString() -> string! +override MMCA.Common.UI.Components.Capabilities.BiometricGate.OnAfterRenderAsync(bool firstRender) -> System.Threading.Tasks.Task! +override MMCA.Common.UI.Components.Capabilities.DeepLinkListener.OnAfterRender(bool firstRender) -> void +override MMCA.Common.UI.Components.Capabilities.OfflineBanner.OnAfterRenderAsync(bool firstRender) -> System.Threading.Tasks.Task! +override MMCA.Common.UI.Components.Capabilities.PushRegistrationListener.OnAfterRenderAsync(bool firstRender) -> System.Threading.Tasks.Task! +override MMCA.Common.UI.Components.DocumentLanguage.OnAfterRenderAsync(bool firstRender) -> System.Threading.Tasks.Task! +override MMCA.Common.UI.Components.MmcaThemeProviders.OnAfterRenderAsync(bool firstRender) -> System.Threading.Tasks.Task! +override MMCA.Common.UI.Components.MmcaThemeProviders.OnInitialized() -> void +override MMCA.Common.UI.Components.MobileInfiniteScrollList.OnAfterRenderAsync(bool firstRender) -> System.Threading.Tasks.Task! +override MMCA.Common.UI.Components.MobileInfiniteScrollList.OnInitializedAsync() -> System.Threading.Tasks.Task! +override MMCA.Common.UI.Components.Notifications.NotificationBell.OnAfterRenderAsync(bool firstRender) -> System.Threading.Tasks.Task! +override MMCA.Common.UI.Components.Notifications.NotificationListener.OnAfterRenderAsync(bool firstRender) -> System.Threading.Tasks.Task! +override MMCA.Common.UI.Components.PageStateScope.OnAfterRenderAsync(bool firstRender) -> System.Threading.Tasks.Task! +override MMCA.Common.UI.Components.QrCodeImage.OnParametersSet() -> void +override MMCA.Common.UI.Components.RedirectToLogin.OnInitialized() -> void +override MMCA.Common.UI.Components.ThemeToggle.OnInitialized() -> void +override MMCA.Common.UI.Layout.MainLayout.OnInitialized() -> void +override MMCA.Common.UI.Layout.NavMenu.OnInitializedAsync() -> System.Threading.Tasks.Task! +override MMCA.Common.UI.Pages.Auth.Login.OnInitialized() -> void +override MMCA.Common.UI.Pages.Auth.OAuthComplete.OnAfterRenderAsync(bool firstRender) -> System.Threading.Tasks.Task! +override MMCA.Common.UI.Pages.Common.DataGridListPageBase.OnAfterRenderAsync(bool firstRender) -> System.Threading.Tasks.Task! +override MMCA.Common.UI.Pages.Common.DataGridListPageBase.OnInitialized() -> void +override MMCA.Common.UI.Pages.Home.OnInitialized() -> void +override MMCA.Common.UI.Pages.Notifications.NotificationInbox.OnInitializedAsync() -> System.Threading.Tasks.Task! +override MMCA.Common.UI.Pages.Notifications.NotificationList.OnInitializedAsync() -> System.Threading.Tasks.Task! +override MMCA.Common.UI.Pages.Notifications.NotificationSend.OnInitialized() -> void +override MMCA.Common.UI.Services.Auth.JwtAuthenticationStateProvider.GetAuthenticationStateAsync() -> System.Threading.Tasks.Task! +override MMCA.Common.UI.Services.Capabilities.GeoPoint.Equals(object? obj) -> bool +override MMCA.Common.UI.Services.Capabilities.GeoPoint.GetHashCode() -> int +override MMCA.Common.UI.Services.Capabilities.GeoPoint.ToString() -> string! +override MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.Equals(object? obj) -> bool +override MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.GetHashCode() -> int +override MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.ToString() -> string! +override MMCA.Common.UI.Services.Capabilities.PushDeviceToken.Equals(object? obj) -> bool +override MMCA.Common.UI.Services.Capabilities.PushDeviceToken.GetHashCode() -> int +override MMCA.Common.UI.Services.Capabilities.PushDeviceToken.ToString() -> string! +override MMCA.Common.UI.Services.ListPageState.Equals(object? obj) -> bool +override MMCA.Common.UI.Services.ListPageState.GetHashCode() -> int +override MMCA.Common.UI.Services.ListPageState.ToString() -> string! +override MMCA.Common.UI.Services.Navigation.BackNavigationResult.Equals(object? obj) -> bool +override MMCA.Common.UI.Services.Navigation.BackNavigationResult.GetHashCode() -> int +override MMCA.Common.UI.Services.Navigation.BackNavigationResult.ToString() -> string! +override MMCA.Common.UI.Services.UserPreferences.Equals(object? obj) -> bool +override MMCA.Common.UI.Services.UserPreferences.GetHashCode() -> int +override MMCA.Common.UI.Services.UserPreferences.ToString() -> string! +static MMCA.Common.UI.Common.BreakpointConstants.IsMobileBreakpoint(MudBlazor.Breakpoint breakpoint) -> bool +static MMCA.Common.UI.Common.NavItem.operator !=(MMCA.Common.UI.Common.NavItem? left, MMCA.Common.UI.Common.NavItem? right) -> bool +static MMCA.Common.UI.Common.NavItem.operator ==(MMCA.Common.UI.Common.NavItem? left, MMCA.Common.UI.Common.NavItem? right) -> bool +static MMCA.Common.UI.Common.Settings.UIModuleConfiguration.IsModuleEnabled(Microsoft.Extensions.Configuration.IConfiguration! configuration, string! moduleName) -> bool +static MMCA.Common.UI.DependencyInjection.AddClientAuthSessionCookieSync(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.UI.DependencyInjection.AddUIModule(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.UI.DependencyInjection.AddUIShared(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, Microsoft.Extensions.Configuration.IConfiguration! configuration) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.UI.DependencyInjection.AddWasmFormFactor(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.UI.Extensions.MoneyExtensions.ToDisplayRange(this System.Collections.Generic.IReadOnlyCollection! prices) -> string! +static MMCA.Common.UI.Extensions.MoneyExtensions.ToDisplayString(this MMCA.Common.Shared.ValueObjects.Money! price) -> string! +static MMCA.Common.UI.Extensions.WebApplicationExtensions.UseAuthenticatedNoStore(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +static MMCA.Common.UI.Globalization.PseudoLocalizer.Transform(string! value) -> string! +static MMCA.Common.UI.Notifications.DependencyInjection.AddNotificationUI(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.UI.Pages.Common.ErrorMessages.ActionError(System.Exception! ex, string! localizedFallback) -> string! +static MMCA.Common.UI.Pages.Common.ErrorMessages.Configure(Microsoft.Extensions.Localization.IStringLocalizer! localizer) -> void +static MMCA.Common.UI.Pages.Common.ErrorMessages.DeleteError(string! entityName, System.Exception! ex) -> string! +static MMCA.Common.UI.Pages.Common.ErrorMessages.DeleteFailed(string! entityName) -> string! +static MMCA.Common.UI.Pages.Common.ErrorMessages.LoadError(string! entityName, System.Exception! ex) -> string! +static MMCA.Common.UI.Pages.Common.ErrorMessages.NotFound(string! entityName, object! id) -> string! +static MMCA.Common.UI.Pages.Common.ErrorMessages.SaveError(string! entityName, System.Exception! ex) -> string! +static MMCA.Common.UI.Pages.Common.ErrorMessages.Success(string! entityName, string! action) -> string! +static MMCA.Common.UI.Pages.Common.ErrorMessages.ValidationError.get -> string! +static MMCA.Common.UI.Services.Auth.JwtTokenInfo.IsFresh(string? token, System.TimeSpan skew) -> bool +static MMCA.Common.UI.Services.AuthenticatedServiceBase.NewIdempotencyKey() -> string! +static MMCA.Common.UI.Services.Capabilities.DependencyInjection.AddBrowserDeviceCapabilities(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static MMCA.Common.UI.Services.Capabilities.GeoPoint.operator !=(MMCA.Common.UI.Services.Capabilities.GeoPoint? left, MMCA.Common.UI.Services.Capabilities.GeoPoint? right) -> bool +static MMCA.Common.UI.Services.Capabilities.GeoPoint.operator ==(MMCA.Common.UI.Services.Capabilities.GeoPoint? left, MMCA.Common.UI.Services.Capabilities.GeoPoint? right) -> bool +static MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.operator !=(MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest? left, MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest? right) -> bool +static MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest.operator ==(MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest? left, MMCA.Common.UI.Services.Capabilities.LocalNotificationRequest? right) -> bool +static MMCA.Common.UI.Services.Capabilities.PushDeviceToken.operator !=(MMCA.Common.UI.Services.Capabilities.PushDeviceToken? left, MMCA.Common.UI.Services.Capabilities.PushDeviceToken? right) -> bool +static MMCA.Common.UI.Services.Capabilities.PushDeviceToken.operator ==(MMCA.Common.UI.Services.Capabilities.PushDeviceToken? left, MMCA.Common.UI.Services.Capabilities.PushDeviceToken? right) -> bool +static MMCA.Common.UI.Services.ListPageQueryStateService.BuildPath(string! basePath, MMCA.Common.UI.Services.ListPageState! state) -> string! +static MMCA.Common.UI.Services.ListPageQueryStateService.ParseQueryString(string? queryString) -> MMCA.Common.UI.Services.ListPageState! +static MMCA.Common.UI.Services.ListPageState.operator !=(MMCA.Common.UI.Services.ListPageState? left, MMCA.Common.UI.Services.ListPageState? right) -> bool +static MMCA.Common.UI.Services.ListPageState.operator ==(MMCA.Common.UI.Services.ListPageState? left, MMCA.Common.UI.Services.ListPageState? right) -> bool +static MMCA.Common.UI.Services.MmcaCultureBootstrap.SetBrowserCultureAsync(Microsoft.JSInterop.IJSRuntime! jsRuntime) -> System.Threading.Tasks.Task! +static MMCA.Common.UI.Services.Navigation.BackNavigationResult.operator !=(MMCA.Common.UI.Services.Navigation.BackNavigationResult? left, MMCA.Common.UI.Services.Navigation.BackNavigationResult? right) -> bool +static MMCA.Common.UI.Services.Navigation.BackNavigationResult.operator ==(MMCA.Common.UI.Services.Navigation.BackNavigationResult? left, MMCA.Common.UI.Services.Navigation.BackNavigationResult? right) -> bool +static MMCA.Common.UI.Services.Navigation.MauiBackNavigationBridge.HandleBackPressedAsync(Microsoft.JSInterop.IJSRuntime! js) -> System.Threading.Tasks.ValueTask +static MMCA.Common.UI.Services.Navigation.ReturnUrlProtector.Sanitize(string? candidate, string! fallback = "/") -> string! +static MMCA.Common.UI.Services.ServiceExceptionHelper.ThrowIfDomainExceptionAsync(System.Net.Http.HttpResponseMessage! response, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +static MMCA.Common.UI.Services.UserPreferences.operator !=(MMCA.Common.UI.Services.UserPreferences? left, MMCA.Common.UI.Services.UserPreferences? right) -> bool +static MMCA.Common.UI.Services.UserPreferences.operator ==(MMCA.Common.UI.Services.UserPreferences? left, MMCA.Common.UI.Services.UserPreferences? right) -> bool +static MMCA.Common.UI.Theme.MMCATheme.Instance.get -> MudBlazor.MudTheme! +static readonly MMCA.Common.UI.Common.NotificationRoutePaths.NotificationInbox -> string! +static readonly MMCA.Common.UI.Common.NotificationRoutePaths.NotificationSend -> string! +static readonly MMCA.Common.UI.Common.NotificationRoutePaths.Notifications -> string! +static readonly MMCA.Common.UI.Common.RoutePaths.Home -> string! +static readonly MMCA.Common.UI.Common.Settings.ApiSettings.SectionName -> string! +static readonly MMCA.Common.UI.Common.Settings.LayoutSettings.SectionName -> string! +static readonly MMCA.Common.UI.Services.AuthenticatedServiceBase.RetryPolicy -> Polly.Retry.AsyncRetryPolicy! +virtual MMCA.Common.UI.Common.NavItem.$() -> MMCA.Common.UI.Common.NavItem! +virtual MMCA.Common.UI.Common.NavItem.EqualityContract.get -> System.Type! +virtual MMCA.Common.UI.Common.NavItem.Equals(MMCA.Common.UI.Common.NavItem? other) -> bool +virtual MMCA.Common.UI.Common.NavItem.PrintMembers(System.Text.StringBuilder! builder) -> bool +virtual MMCA.Common.UI.Components.Notifications.NotificationBell.Dispose(bool disposing) -> void +virtual MMCA.Common.UI.Pages.Common.DataGridListPageBase.Dispose(bool disposing) -> void +virtual MMCA.Common.UI.Pages.Common.DataGridListPageBase.GridRef.get -> MudBlazor.MudDataGrid? +virtual MMCA.Common.UI.Pages.Common.DataGridListPageBase.OnMobileDataRequestedAsync() -> System.Threading.Tasks.Task! +virtual MMCA.Common.UI.Pages.Common.DataGridListPageBase.RestoreFilters(System.Collections.Generic.IReadOnlyDictionary! filters) -> void +virtual MMCA.Common.UI.Pages.Common.DataGridListPageBase.SaveFilters(System.Collections.Generic.Dictionary! filters) -> void +virtual MMCA.Common.UI.Pages.Notifications.NotificationInbox.Dispose(bool disposing) -> void +virtual MMCA.Common.UI.Pages.Notifications.NotificationList.Dispose(bool disposing) -> void +virtual MMCA.Common.UI.Pages.Notifications.NotificationSend.Dispose(bool disposing) -> void +virtual MMCA.Common.UI.Services.EntityServiceBase.AddAsync(TEntityDTO entity, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual MMCA.Common.UI.Services.EntityServiceBase.DeleteAsync(TIdentifierType id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual MMCA.Common.UI.Services.EntityServiceBase.GetAllAsync(bool includeFKs = false, bool includeChildren = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task?>! +virtual MMCA.Common.UI.Services.EntityServiceBase.GetAllForLookupAsync(string! nameProperty, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!>! +virtual MMCA.Common.UI.Services.EntityServiceBase.GetByIdAsync(TIdentifierType id, bool includeChildren = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +virtual MMCA.Common.UI.Services.EntityServiceBase.GetEntityId(TEntityDTO entity) -> TIdentifierType +virtual MMCA.Common.UI.Services.EntityServiceBase.GetPagedAsync(System.Collections.Generic.Dictionary! filters, int pageNumber, int pageSize, string? sortColumn, string? sortDirection, bool includeChildren = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<(System.Collections.Generic.IReadOnlyList! Items, int TotalItems)>! +virtual MMCA.Common.UI.Services.EntityServiceBase.UpdateAsync(TEntityDTO entity, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +~override MMCA.Common.UI.Components.Capabilities.BiometricGate.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.Capabilities.DeepLinkListener.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.Capabilities.ExternalLink.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.Capabilities.OfflineBanner.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.Capabilities.PushRegistrationListener.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.CultureSwitcher.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.DeleteConfirmation.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.DocumentLanguage.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.EmptyState.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.MmcaThemeProviders.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.MobileCardList.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.MobileInfiniteScrollList.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.Notifications.NotificationBell.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.Notifications.NotificationListener.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.PageErrorState.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.PageHeader.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.PageLoadingState.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.PageStateScope.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.QrCodeImage.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.RedirectToLogin.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.ThemeToggle.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Components.UnsavedChangesGuard.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Layout.MainLayout.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Layout.NavMenu.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Layout.ReconnectModal.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Pages.Auth.Login.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Pages.Auth.OAuthComplete.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Pages.Auth.Register.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Pages.Forbidden.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Pages.Home.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Pages.NotFound.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Pages.Notifications.NotificationInbox.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Pages.Notifications.NotificationList.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Pages.Notifications.NotificationSend.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void +~override MMCA.Common.UI.Routes.BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) -> void diff --git a/Source/Presentation/MMCA.Common.UI/PublicAPI.Unshipped.txt b/Source/Presentation/MMCA.Common.UI/PublicAPI.Unshipped.txt new file mode 100644 index 00000000..7dc5c581 --- /dev/null +++ b/Source/Presentation/MMCA.Common.UI/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/Source/Presentation/MMCA.Common.UI/packages.lock.json b/Source/Presentation/MMCA.Common.UI/packages.lock.json index 262169cd..1da3fe9a 100644 --- a/Source/Presentation/MMCA.Common.UI/packages.lock.json +++ b/Source/Presentation/MMCA.Common.UI/packages.lock.json @@ -61,6 +61,12 @@ "Microsoft.AspNetCore.SignalR.Client.Core": "10.0.11" } }, + "Microsoft.CodeAnalysis.PublicApiAnalyzers": { + "type": "Direct", + "requested": "[5.6.0, )", + "resolved": "5.6.0", + "contentHash": "W4kJGezNIKLzo0Ak5FAQDFvkMf2U7DtGL4THmHyRSApfKsKt5V+eX/bU0ZLKAt/uf9Bb2o1bi0YDKj/GRB/vYQ==" + }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Direct", "requested": "[10.0.11, )", diff --git a/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs b/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs new file mode 100644 index 00000000..815729ed --- /dev/null +++ b/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationFixtures/CancellationTokenFixtures.cs @@ -0,0 +1,77 @@ +using System.Diagnostics.CodeAnalysis; + +namespace MMCA.Common.Architecture.Tests.CancellationFixtures; + +/// Compliant: every public async method takes a trailing cancellationToken. +public sealed class CompliantFixtureService +{ + /// A compliant Task-returning method. + public Task GetAsync(string id, CancellationToken cancellationToken) => + Task.FromResult(id.Length + (cancellationToken.IsCancellationRequested ? 1 : 0)); + + /// A compliant ValueTask-returning method whose only parameter is the token. + public ValueTask DoAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + return ValueTask.CompletedTask; + } + + /// Not async, so out of scope entirely. + public int Count(string id) => id.Length; + + /// Non-public, so out of scope even though it is async and token-less. + internal Task HiddenAsync() => Task.CompletedTask; +} + +/// Offender: public async methods with no token at all. +public sealed class MissingTokenFixtureService +{ + /// Offender: a token-less async method with parameters. + public Task RunAsync(string id) => Task.FromResult(id); + + /// Offender: a token-less async method with no parameters (the token would be its only one). + public Task PingAsync() => Task.CompletedTask; +} + +/// Offender: the token is present but is not the last parameter. +public sealed class MisplacedTokenFixtureService +{ + /// Offender: the token leads instead of trailing. + [SuppressMessage( + "Design", + "CA1068:CancellationToken parameters must come last", + Justification = "Deliberate: this fixture exists to prove the fitness function catches exactly this shape.")] + public Task RunAsync(CancellationToken cancellationToken, string id) => + Task.FromResult(cancellationToken.IsCancellationRequested && id.Length > 0); +} + +/// Offender: the token trails but carries a different name. +public sealed class MisnamedTokenFixtureService +{ + /// Offender: the trailing token is named token. + public Task RunAsync(string id, CancellationToken token) => + Task.FromResult(token.IsCancellationRequested && id.Length > 0); +} + +/// Offender used to prove the per-method exemption list suppresses exactly one entry. +public sealed class ExemptableFixtureService +{ + /// Offender unless listed in the exemptions. + public Task LegacyAsync(string id) => Task.FromResult(id); +} + +/// +/// Auto-exempt: MoveNextAsync is an implicit implementation of , +/// declared outside the map's assemblies, so its token-less signature is not this repo's to change. +/// +public sealed class ExternalContractFixtureService : IAsyncEnumerator +{ + /// + public int Current => 0; + + /// + public ValueTask MoveNextAsync() => ValueTask.FromResult(false); + + /// + public ValueTask DisposeAsync() => ValueTask.CompletedTask; +} diff --git a/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationTokenConventionTests.cs b/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationTokenConventionTests.cs new file mode 100644 index 00000000..15ac2637 --- /dev/null +++ b/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationTokenConventionTests.cs @@ -0,0 +1,28 @@ +using MMCA.Common.Testing.Architecture; + +namespace MMCA.Common.Architecture.Tests; + +/// +/// Trailing-CancellationToken convention for the MMCA.Common Application and Infrastructure packages, +/// driven by the shared rule library () over +/// . +/// +public sealed class CancellationTokenConventionTests : CancellationTokenConventionTestsBase +{ + protected override IArchitectureMap Map { get; } = new CommonArchitectureMap(); + + /// + /// The two SignalR hub methods on NotificationHub. A hub method signature is not an ordinary + /// public API: it IS the client-visible RPC contract, bound by name and argument list by SignalR's + /// dispatcher, and every shipped consumer's JavaScript/Blazor client already invokes + /// JoinChannel/LeaveChannel with exactly one argument. Adding a parameter would change + /// that wire contract for a token the hub already has: both methods pass + /// Context.ConnectionAborted straight into the group calls, so the work IS cancellable, just + /// not through a parameter the rule can see. + /// + protected override IReadOnlyList CancellationTokenExemptMethods => + [ + "NotificationHub.JoinChannelAsync", + "NotificationHub.LeaveChannelAsync", + ]; +} diff --git a/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationTokenFitnessTests.cs b/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationTokenFitnessTests.cs new file mode 100644 index 00000000..d3a2d5cf --- /dev/null +++ b/Tests/Architecture/MMCA.Common.Architecture.Tests/CancellationTokenFitnessTests.cs @@ -0,0 +1,70 @@ +using MMCA.Common.Architecture.Tests.CancellationFixtures; +using MMCA.Common.Testing.Architecture; + +namespace MMCA.Common.Architecture.Tests; + +/// +/// Verifies the AsyncMethodsDeclareTrailingCancellationToken fitness function against +/// deliberately-shaped fixture services: it flags a missing, misplaced or misnamed token, leaves a +/// compliant service (and a non-async or non-public member) alone, honors the per-method exemption list, +/// and never flags a signature the repo does not own. +/// +public sealed class CancellationTokenFitnessTests +{ + [Fact] + public void Rule_FlagsMissingToken_ButNotCompliantOrNonAsyncMembers() + { + var message = RunRule(); + + message.Should().Contain($"{nameof(MissingTokenFixtureService)}.RunAsync", "the method takes no token"); + message.Should().Contain( + $"{nameof(MissingTokenFixtureService)}.PingAsync", + "a parameterless async method is not excused: the token would simply be its only parameter"); + message.Should().NotContain(nameof(CompliantFixtureService), "every one of its async methods trails the token"); + message.Should().NotContain("HiddenAsync", "non-public methods are out of scope"); + } + + [Fact] + public void Rule_FlagsMisplacedAndMisnamedTokens() + { + var message = RunRule(); + + message.Should().Contain($"{nameof(MisplacedTokenFixtureService)}.RunAsync"); + message.Should().Contain("must be the LAST parameter"); + message.Should().Contain($"{nameof(MisnamedTokenFixtureService)}.RunAsync"); + message.Should().Contain("must be named 'cancellationToken'"); + } + + [Fact] + public void Rule_HonorsExemptions_AndSignaturesTheRepoDoesNotOwn() + { + RunRule().Should().Contain( + $"{nameof(ExemptableFixtureService)}.LegacyAsync", + "without an exemption the offender is reported"); + + var exempted = RunRule([$"{nameof(ExemptableFixtureService)}.LegacyAsync"]); + + exempted.Should().NotContain($"{nameof(ExemptableFixtureService)}.LegacyAsync"); + exempted.Should().NotContain( + "MoveNextAsync", + "an implicit implementation of an interface declared outside the map is auto-exempt: the signature is externally fixed"); + } + + private static string RunRule(IReadOnlyCollection? exemptMethods = null) + { + var act = () => ArchitectureRules.AsyncMethodsDeclareTrailingCancellationToken( + new CancellationTestMap(), + exemptMethods); + + return act.Should().Throw().Which.Message; + } + + /// A map whose single Application layer is this test assembly, so the fixtures are in scope. + private sealed class CancellationTestMap : ArchitectureMapBase + { + public override string RepoToken => "MMCA.Common"; + + protected override IEnumerable DefineLayers() => + [Framework(Layer.Application, typeof(CancellationTokenFitnessTests).Assembly)]; + } +} diff --git a/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Acyclic/AcyclicFixtures.cs b/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Acyclic/AcyclicFixtures.cs new file mode 100644 index 00000000..665203af --- /dev/null +++ b/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Acyclic/AcyclicFixtures.cs @@ -0,0 +1,10 @@ +using MMCA.Common.Architecture.Tests.CycleFixtures.Left; + +namespace MMCA.Common.Architecture.Tests.CycleFixtures.Acyclic; + +/// An acyclic neighbour: it points at Left and nothing in Left or Right points back at it. +public sealed class AcyclicConsumer +{ + /// The one-way Acyclic -> Left edge. + public LeftService? Service { get; set; } +} diff --git a/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Left/LeftFixtures.cs b/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Left/LeftFixtures.cs new file mode 100644 index 00000000..28f91932 --- /dev/null +++ b/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Left/LeftFixtures.cs @@ -0,0 +1,17 @@ +using MMCA.Common.Architecture.Tests.CycleFixtures.Right; + +namespace MMCA.Common.Architecture.Tests.CycleFixtures.Left; + +/// Half of the deliberate namespace cycle: a Left type whose property type lives in Right. +public sealed class LeftService +{ + /// The Left -> Right edge the cycle rule must see. + public RightModel? Model { get; set; } +} + +/// The base type Right derives from, closing the cycle back into Left. +public abstract class LeftModelBase +{ + /// An arbitrary member so the fixture type is not empty. + public int Id { get; set; } +} diff --git a/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Right/RightFixtures.cs b/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Right/RightFixtures.cs new file mode 100644 index 00000000..dbf9555b --- /dev/null +++ b/Tests/Architecture/MMCA.Common.Architecture.Tests/CycleFixtures/Right/RightFixtures.cs @@ -0,0 +1,10 @@ +using MMCA.Common.Architecture.Tests.CycleFixtures.Left; + +namespace MMCA.Common.Architecture.Tests.CycleFixtures.Right; + +/// Half of the deliberate namespace cycle: a Right type deriving from a Left base. +public sealed class RightModel : LeftModelBase +{ + /// An arbitrary member so the fixture type is not empty. + public string Name { get; set; } = string.Empty; +} diff --git a/Tests/Architecture/MMCA.Common.Architecture.Tests/NamespaceCycleFitnessTests.cs b/Tests/Architecture/MMCA.Common.Architecture.Tests/NamespaceCycleFitnessTests.cs new file mode 100644 index 00000000..4198c8c1 --- /dev/null +++ b/Tests/Architecture/MMCA.Common.Architecture.Tests/NamespaceCycleFitnessTests.cs @@ -0,0 +1,65 @@ +using MMCA.Common.Testing.Architecture; + +namespace MMCA.Common.Architecture.Tests; + +/// +/// Verifies the NamespacesHaveNoDependencyCycles fitness function against deliberately-shaped +/// fixture namespaces under CycleFixtures: it flags the two-namespace cycle (Left holds a +/// Right property, Right derives from a Left base) and leaves the acyclic namespace +/// alone. +/// +public sealed class NamespaceCycleFitnessTests +{ + private const string FixtureRoot = "MMCA.Common.Architecture.Tests.CycleFixtures"; + + [Fact] + public void Rule_FlagsTwoNamespaceCycle_ButNotAcyclicNamespaces() + { + var act = () => ArchitectureRules.NamespacesHaveNoDependencyCycles(new CycleTestMap()); + + var exception = act.Should().Throw().Which; + exception.Message.Should().Contain($"{FixtureRoot}.Left", "Left references Right through a property"); + exception.Message.Should().Contain($"{FixtureRoot}.Right", "Right derives from a Left base type"); + exception.Message.Should().NotContain( + $"{FixtureRoot}.Acyclic", + "the acyclic fixture namespace only points one way and nothing points back at it"); + } + + [Fact] + public void Rule_Passes_WhenTheWholeCycleIsAllowed() + { + var act = () => ArchitectureRules.NamespacesHaveNoDependencyCycles( + new CycleTestMap(), + [$"{FixtureRoot}.Left", $"{FixtureRoot}.Right"]); + + act.Should().NotThrow("an allowance covering every namespace of the component accepts the cycle"); + } + + [Fact] + public void Rule_StillFails_WhenOnlyPartOfTheCycleIsAllowed() + { + var act = () => ArchitectureRules.NamespacesHaveNoDependencyCycles( + new CycleTestMap(), + [$"{FixtureRoot}.Left"]); + + act.Should().Throw("a partial allowance must never hide a cycle"); + } + + /// + /// A map whose single layer is this test assembly rooted at the fixture namespace, so the rule sees + /// only the CycleFixtures.* types and none of the real test code. + /// + private sealed class CycleTestMap : ArchitectureMapBase + { + public override string RepoToken => "MMCA.Common"; + + protected override IEnumerable DefineLayers() => + [ + new LayerRef( + string.Empty, + Layer.Application, + typeof(NamespaceCycleFitnessTests).Assembly, + FixtureRoot), + ]; + } +} diff --git a/Tests/Architecture/MMCA.Common.Architecture.Tests/NamespaceCycleTests.cs b/Tests/Architecture/MMCA.Common.Architecture.Tests/NamespaceCycleTests.cs new file mode 100644 index 00000000..ca901b5b --- /dev/null +++ b/Tests/Architecture/MMCA.Common.Architecture.Tests/NamespaceCycleTests.cs @@ -0,0 +1,45 @@ +using MMCA.Common.Testing.Architecture; + +namespace MMCA.Common.Architecture.Tests; + +/// +/// Namespace acyclicity for the MMCA.Common framework packages, driven by the shared rule library +/// () over . +/// +public sealed class NamespaceCycleTests : NamespaceCycleTestsBase +{ + protected override IArchitectureMap Map { get; } = new CommonArchitectureMap(); + + /// + /// The one accepted tangle in the framework today, inside MMCA.Common.Infrastructure: + /// root -> Settings -> Persistence -> root. Each edge is deliberate and none of the + /// three namespaces is extractable on its own anyway (they are one assembly, one package): + /// + /// + /// root -> Settings: DependencyInjection lives in the root namespace and binds every + /// settings class, which is the point of a composition root. + /// + /// + /// Settings -> Persistence: TenancySettingsValidator takes an optional + /// IDataSourceResolver so a tenant override that names a non-existent physical source fails the + /// boot rather than silently resolving cross-tenant at runtime (validation must see the resolved + /// sources; a settings class that cannot check itself against reality is worth less than the edge). + /// + /// + /// Persistence -> root: the EntityTypeConfiguration* shims carry + /// [UseDataSource]/[UseDatabase], marker attributes that live in the root namespace + /// precisely BECAUSE consumers annotate their own configurations with them. Pushing them down into + /// Persistence would make the public annotation surface deeper for every consumer to fix an + /// internal graph edge. + /// + /// + /// The allowance covers the whole strongly connected component, so a fourth namespace joining this + /// tangle still fails the test. + /// + protected override IReadOnlyList AllowedCycleNamespaces => + [ + "MMCA.Common.Infrastructure", + "MMCA.Common.Infrastructure.Persistence", + "MMCA.Common.Infrastructure.Settings", + ]; +} diff --git a/Tests/Core/MMCA.Common.Infrastructure.Tests/Settings/SettingsTests.cs b/Tests/Core/MMCA.Common.Infrastructure.Tests/Settings/SettingsTests.cs index 684893f0..c517120a 100644 --- a/Tests/Core/MMCA.Common.Infrastructure.Tests/Settings/SettingsTests.cs +++ b/Tests/Core/MMCA.Common.Infrastructure.Tests/Settings/SettingsTests.cs @@ -249,12 +249,6 @@ public void Default_EnableDelayedRedelivery_IsFalse() => public void Default_RedeliveryIntervalsSeconds_IsOneMinuteTenMinutesOneHour() => new MessageBusSettings().RedeliveryIntervalsSeconds.Should().Equal(60, 600, 3600); - // Default-ON: a faulted event with no fault consumer leaves no trace outside the broker's - // error queue, which is the failure mode this consumer exists to close. - [Fact] - public void Default_RegisterFaultConsumers_IsTrue() => - new MessageBusSettings().RegisterFaultConsumers.Should().BeTrue(); - [Fact] public void ResilienceProperties_RoundTrip() { @@ -263,13 +257,11 @@ public void ResilienceProperties_RoundTrip() EnableInbox = true, EnableDelayedRedelivery = true, RedeliveryIntervalsSeconds = [5, 15], - RegisterFaultConsumers = false, }; sut.EnableInbox.Should().BeTrue(); sut.EnableDelayedRedelivery.Should().BeTrue(); sut.RedeliveryIntervalsSeconds.Should().Equal(5, 15); - sut.RegisterFaultConsumers.Should().BeFalse(); } }