using System; using System.Collections.Concurrent; using System.Reflection; using GB5Shared.DTO.Framework.Login; namespace GB5Shared.ListQuery { // ───────────────────────────────────────────────────────────────────── // IQueryGuard // // Optional per-query validation executed BEFORE the QB builds SQL. // Use to enforce domain-specific mandatory-filter rules (e.g. date range // required, minimum criteria supplied, OUId mandatory for heavy queries). // // Register as IQueryGuard — Scrutor / AddListInfrastructure() scans // module assemblies automatically. Multiple guards per query are supported; // all run in registration order. GenericListHandler requests them via // IEnumerable> — empty enumerable = permissive (no guard). // // Rules: // • Throw ArgumentException (or a domain-specific exception) to reject. // • Do NOT perform DB calls inside a guard — guards are sync and fast. // • Guards run before dialect resolution and QB — no SQL is built on reject. // ───────────────────────────────────────────────────────────────────── public interface IQueryGuard { void Validate(TQuery query, LoginDTO login); } // ───────────────────────────────────────────────────────────────────── // QueryIntent – marks the intended execution target of a query // // Used by IQueryExecutor (Phase 2) to route to main DB vs read replica. // Default is Transactional (main DB). Override with [QuerySource] only // where the semantic difference matters. // // Rules: // • Transactional: short OLTP queries that need fresh data. // • Reporting: heavier reads acceptable on read replica (seconds stale). // • Analytics: cross-period aggregations — data warehouse only. // • Never mark as Reporting/Analytics if the result affects a workflow // decision that requires up-to-the-second accuracy. // ───────────────────────────────────────────────────────────────────── public enum QueryIntent { Transactional = 0, // main DB — OLTP queries, must be current Reporting = 1, // read replica — heavier reads, seconds stale is fine Analytics = 2, // data warehouse — cross-period aggregations } // ───────────────────────────────────────────────────────────────────── // [QuerySource] – decorate a query record to declare its intent // // GenericListHandler reads this once per TQuery type at startup (cached). // If absent, defaults to QueryIntent.Transactional. // // Example: // [QuerySource(QueryIntent.Reporting)] // public sealed record PendingMMDocumentsQuery(PendingMMDocumentCriteria Criteria) // : IListQuery; // ───────────────────────────────────────────────────────────────────── [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)] public sealed class QuerySourceAttribute : Attribute { public QueryIntent Intent { get; } public QuerySourceAttribute(QueryIntent intent) => Intent = intent; } // ───────────────────────────────────────────────────────────────────── // QueryIntentResolver – cached, one read per TQuery type // // Called once per type by GenericListHandler's static initialiser. // The result is cached in a ConcurrentDictionary — zero runtime overhead // after the first call for each query type. // ───────────────────────────────────────────────────────────────────── internal static class QueryIntentResolver { private static readonly ConcurrentDictionary _cache = new(); public static QueryIntent Get(Type queryType) => _cache.GetOrAdd(queryType, t => t.GetCustomAttribute()?.Intent ?? QueryIntent.Transactional); } }