// =============================================================================
// GB5 Framework — IReportConnectionResolver
// Namespace : FrameworkBLL.ReportOrchestration
// Purpose : Resolves the correct IDbConnection for a given DataSourceType,
// derived entirely from the LoginDTO session (which carries
// ServerConfigId → MSERVERCONFIG). No runtime DB lookup required.
//
// Resolution logic:
// DataSourceType.Oltp → LoginDTO.ServerConfigId
// DataSourceType.ReportDb → MSERVERCONFIG.REPORTSERVERCONFIGID (-1 = OLTP fallback)
// DataSourceType.ArchiveDb → MSERVERCONFIG.ARCHIVESERVERCONFIGID (-1 = OLTP fallback)
// DataSourceType.Auto → caller must resolve via IDataSourceRuleEvaluator first
//
// Provider:
// LoginDTO.DatabaseType → DatabaseProviderType enum
// 0 = SqlServer (Microsoft.Data.SqlClient)
// 1 = Oracle (Oracle.ManagedDataAccess)
// 2 = PostgreSQL (Npgsql)
// 3 = MySQL (MySql.Data)
//
// Lifetime: Scoped (one instance per HTTP request; caches resolved configs
// within the request to avoid repeated MSERVERCONFIG lookups).
// =============================================================================
using System.Data;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.DTO.ReportOrchestration;
using GB5Shared.Enums.ReportOrchestration;
namespace FrameworkBLL.ReportOrchestration
{
///
/// Resolves a ready-to-open for the requested
/// , using the session context in
/// and the pre-loaded .
///
///
/// Implementations must:
///
/// - Be registered as Scoped in DI.
/// - Cache resolved instances
/// within the request (avoid repeated lookups per pipeline stage).
/// - Return connections in a closed state; the caller opens
/// them so that Dapper / ADO.NET lifetime is controlled explicitly.
/// - Never log or expose connection strings.
///
///
public interface IReportConnectionResolver
{
///
/// Resolves a closed for the given
/// using the session in
/// .
///
///
/// The authenticated session. Must carry a valid
/// .
///
///
/// The logical data source. Must not be
/// — resolve Auto to a concrete
/// source via before calling.
///
/// Propagated from the HTTP request.
///
/// A closed of the correct provider type.
/// Caller is responsible for opening and disposing.
///
///
/// Thrown when is
/// .
///
///
/// Thrown when the MSERVERCONFIG row cannot be located or the
/// connection string cannot be formed.
///
Task ResolveConnectionAsync(
LoginDTO loginDTO,
DataSourceType dataSource,
CancellationToken cancellationToken = default);
///
/// Returns the resolved for the
/// given without opening a connection.
/// Use this when you need metadata (ProviderType, IsDedicatedAnalyticsDb)
/// to make decisions (e.g. whether to apply NOLOCK hints) before
/// opening a connection.
///
/// The authenticated session.
///
/// The logical data source. Must not be .
///
/// Propagated from the HTTP request.
Task ResolveConfigAsync(
LoginDTO loginDTO,
DataSourceType dataSource,
CancellationToken cancellationToken = default);
///
/// Checks whether a dedicated analytics database is configured for
/// . Returns false when the
/// config falls back to OLTP (REPORTSERVERCONFIGID /
/// ARCHIVESERVERCONFIGID = -1), meaning the report will execute
/// against OLTP — callers should log a warning in this case.
///
/// The authenticated session.
///
/// ReportDb or ArchiveDb. Returns false for Oltp (trivially true).
///
bool IsDedicatedDbConfigured(LoginDTO loginDTO, DataSourceType dataSource);
}
// =========================================================================
// IDataSourceRuleEvaluator
// Resolves DataSourceType.Auto → concrete source using MREPORTDATASOURCERULE
// =========================================================================
///
/// Evaluates reports by applying the
/// MREPORTDATASOURCERULE rows (ordered by RULEPRIORITY ascending) for the
/// given report against the current request context.
/// Returns the first matching rule's TARGETDATASOURCE, or the configured
/// default from if no rule matches.
///
///
/// Rules are loaded once per request and cached within the evaluator.
/// Rule types supported:
///
/// - 0 — DateRangeDays: matches when
/// >
/// MREPORTDATASOURCERULE.THRESHOLDVALUE.
/// - 2 — TimeOfDay: matches when the current server hour
/// (UTC) is ≥ THRESHOLDVALUE (e.g. route off-hours batches to Archive).
/// - 3 — Manual: never matched automatically; only via
/// .
///
/// Registered as Scoped.
///
public interface IDataSourceRuleEvaluator
{
///
/// Resolves the concrete for the given
/// execution context. Safe to call even when the config's
/// DataSourceType is already concrete (returns it unchanged).
///
///
/// The partially-initialised execution context. Must have
/// set.
///
/// Propagated from the HTTP request.
///
/// A concrete — never
/// .
///
Task EvaluateAsync(
ReportExecutionContext context,
CancellationToken cancellationToken = default);
}
// =========================================================================
// ServerConfigCache — typed cache populated at login / first access
// =========================================================================
///
/// In-memory cache of MSERVERCONFIG rows for the current session.
/// Keyed by SERVERCONFIGID. Populated lazily on first resolver call.
/// Avoids repeated DB roundtrips when the same datasource is resolved
/// across multiple pipeline stages within one HTTP request.
///
///
/// Registered as Scoped. Populated by
/// on first access.
/// Exposes only the fields required for connection resolution and
/// NOLOCK/timeout decisions — not the full MSERVERCONFIG entity.
///
public interface IServerConfigCache
{
///
/// Returns the cached for the given
/// , loading from DB if not yet cached.
///
Task GetAsync(
int serverConfigId,
CancellationToken cancellationToken = default);
}
///
/// Projection of MSERVERCONFIG fields required by the resolver.
///
public sealed class CachedServerConfig
{
public int ServerConfigId { get; init; }
/// MSERVERCONFIG.DATABASETYPE — maps to DatabaseProviderType.
public DatabaseProviderType ProviderType { get; init; }
public string DatabaseName { get; init; } = string.Empty;
public string DatabaseUserName { get; init; } = string.Empty;
///
/// Decrypted at load time; held in memory only.
/// Never logged, never serialised.
///
public string DatabasePassword { get; init; } = string.Empty;
public string DatabasePort { get; init; } = string.Empty;
public string ServerIp { get; init; } = string.Empty;
public string ServerMachineName { get; init; } = string.Empty;
/// MSERVERCONFIG.REPORTSERVERCONFIGID (-1 = no dedicated ReportDB).
public int ReportServerConfigId { get; init; } = -1;
/// MSERVERCONFIG.ARCHIVESERVERCONFIGID (-1 = no dedicated ArchiveDB).
public int ArchiveServerConfigId { get; init; } = -1;
/// MSERVERCONFIG.NATURE (0 Online, 1 Offline, 2 ReferAll).
public byte Nature { get; init; }
}
}