using System.Text.Json; using Dapr; using FrameworkBLL.VersionSync; using GB5Shared.DTO.Framework.Login; using Microsoft.AspNetCore.Mvc; namespace FrameworkSL.Controllers.EventSub { // sqlworkbench.changerequest.applied -> VersionSyncBLL.SyncVersionAsync. Published by // SwBLL.ChangeRequest.ChangeRequestBLL.Execute() for SchemaChange/DataMigration change // requests, via GB5Shared.EventLogPublish.PublishEventLogAsync on Dapr pubsub component // "pubsub" (same component EventLogPublish always uses). // // Registered as a fixed subscription in DaprSubscriptionBLL.GetSubscriptionsAsync — this // host replaces app.MapSubscribeHandler() with a dynamic, MEVENTTYPE-driven /dapr/subscribe // (DaprSubscribeController), so a plain [Topic] attribute here would never actually be // registered with the Dapr sidecar. The [Topic] attribute below is kept for documentation // only; DaprSubscriptionBLL's fixed-entry list is what actually drives the subscription, // mirroring the existing ATTACHMENT-RESOLVE precedent in that same method. // // Binds ClientDatabaseId from Request.DataId (a plain int — EventLogPublish sets // EventLogDTO.DataId directly, unlike Request.Data which is a double-JSON-encoded string) // and forwards Login as-is: EventLogPublish always re-attaches the original, unencoded // LoginDTO onto PublishDTO.Login. That's the exact Login/TenantId context that already // succeeded moments earlier inside SqlWorkbench's own ChangeRequestBLL.Execute() for this // ClientDatabaseId — this subscriber has no HTTP session of its own to build one from, and // reusing the event's Login guarantees SqlWorkbench's TenantId-scoped lookups resolve. [ApiController] public class VersionSyncSubscribeController : ControllerBase { private readonly IVersionSyncBLL _VersionSyncBLL; private readonly ILogger _Logger; public VersionSyncSubscribeController(IVersionSyncBLL versionSyncBLL, ILogger logger) { _VersionSyncBLL = versionSyncBLL; _Logger = logger; } [Topic("pubsub", "sqlworkbench.changerequest.applied")] [HttpPost("/versionsync/handle")] public async Task HandleAsync([FromBody] JsonElement payload, CancellationToken ct) { if (!TryParseEvent(payload, out int clientDatabaseId, out LoginDTO? login)) { _Logger.LogWarning("VersionSyncSubscribe: could not parse ClientDatabaseId/Login from payload — acking without action"); return Ok(); // malformed/unexpected payload shape should not retry forever } try { await _VersionSyncBLL.SyncVersionAsync(clientDatabaseId, login, ct).ConfigureAwait(false); return Ok(); } catch (Exception ex) { _Logger.LogError(ex, "VersionSyncSubscribe failed for ClientDatabaseId={ClientDatabaseId}", clientDatabaseId); return StatusCode(500, ex.Message); // 500 = NACK -> Dapr retries } } private static bool TryParseEvent(JsonElement payload, out int clientDatabaseId, out LoginDTO? login) { clientDatabaseId = 0; login = null; if (payload.ValueKind != JsonValueKind.Object) return false; if (!TryGetPropertyCaseInsensitive(payload, "Request", out var request) || request.ValueKind != JsonValueKind.Object || !TryGetPropertyCaseInsensitive(request, "DataId", out var dataId) || dataId.ValueKind != JsonValueKind.Number) return false; clientDatabaseId = dataId.GetInt32(); // Real SW.MSWCLIENTDATABASE.CLIENTDBID values are commonly large negative // autonumbers, like most GB5 identifiers — only 0 (JSON default / genuinely unset) // is treated as malformed. A `<= 0` guard here would silently no-op on every // legitimately-provisioned tenant's event, since CLIENTDBID is essentially never // a small positive number in practice. if (clientDatabaseId == 0) return false; if (!TryGetPropertyCaseInsensitive(payload, "Login", out var loginEl) || loginEl.ValueKind != JsonValueKind.Object) return false; login = loginEl.Deserialize(new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return login is not null; } private static bool TryGetPropertyCaseInsensitive(JsonElement element, string name, out JsonElement value) { foreach (var prop in element.EnumerateObject()) { if (string.Equals(prop.Name, name, StringComparison.OrdinalIgnoreCase)) { value = prop.Value; return true; } } value = default; return false; } } }