FLS Developer Guide
3-Layer Overview
FLS follows the project-wide 3-tier architecture: Service Layer (SL) handles HTTP endpoints and cache keys, Business Logic Layer (BLL) handles validation, orchestration, and event publishing, Data Access Layer (DAL) handles all Dapper queries and stored procedure calls.
FLSSL โ FastEndpoints
- All endpoints inherit BaseEndpoint<Params, ResponseStandardDTO<object>>
- GetCacheKey() returns null for all FLS endpoints โ forms are dynamic per respondent, never cached
- Request/response DTOs in FLSSL/Parameters/
- SignalR Hub: FLSSL/Hubs/FlsMonitorHub.cs
FLSBLL โ Business Logic
- Validation, orchestration, event publishing, cache invalidation
- All save methods follow: GB5Trace.Step("validate") โ validate โ GB5Trace.Step("save") โ DAL call โ GB5Trace.Step("event-publish") โ publish event
- Catches exceptions: GB5Trace.MarkFailed(reason, ex) + _logger.LogError(...) + throw
- Key implementations: FlsInstanceBLL, FlsSessionBLL, FlsRespondentBLL, SurveyInstrumentBLL, QuickRatingBLL, FlsDispatchBLL, FlsEventBLL
FLSDAL โ Dapper + SQL Server
- All SQL in QueryBuilders: FlsQB.cs, SurveyQB.cs, QuickRatingQB.cs, FlsLifecycleQB.cs
- Write operations use stored procedures (SPs): SAVE_REGISTRATION, SP_TMS_DISPATCHFEEDBACK, SAVEDRAFTANSWERS, etc.
- Read operations use parameterised SELECT queries via IQueryExecutor
- Bulk respondent add uses Table-Valued Parameter (TVP) for performance
- All DAL methods accept and forward CancellationToken ct
Module Structure
GB5Solution/FLS/
โโโ FLSSL/
โ โโโ Endpoints/
โ โ โโโ Instance/ CreateFlsInstance, GetFlsInstance, ControlFlsInstance,
โ โ โ ScheduleFlsInstance, OpenFlsInstance
โ โ โโโ Registration/ CreateFlsRegistration, GetFlsRegistration,
โ โ โ SaveFlsAccessRule, DeleteFlsAccessRule
โ โ โโโ Respondent/ GetFlsRespondents, AddFlsRespondents, NudgeFlsRespondent,
โ โ โ FlsRespondentOptOut, GetFlsPendingForms
โ โ โโโ Session/ OpenFlsSession, SaveFlsSessionDraft, SubmitFlsSession,
โ โ โ MarkExternalSubmit
โ โ โโโ Survey/ GetSurveyInstrument, SaveSurveyDraftAnswers,
โ โ โ SubmitSurveyAnswers, GetSurveyResults, GetSurveyAnswers
โ โ โโโ QuickRating/ GetQuickRatingWidget, SubmitRating, GetEntityRatings, GetRatingTrend
โ โโโ Hubs/
โ โ โโโ FlsMonitorHub.cs
โ โ โโโ IFlsMonitorClient.cs
โ โโโ Program.cs
โโโ FLSBLL/
โ โโโ Implementations/ FlsInstanceBLL, FlsSessionBLL, FlsRespondentBLL,
โ โ SurveyInstrumentBLL, QuickRatingBLL, FlsEventBLL, FlsDispatchBLL
โ โโโ Interfaces/
โโโ FLSDAL/
โโโ DTOs/ FlsRows.cs, SurveyRows.cs, QuickRatingRows.cs, FlsDtos.cs,
โ SurveyDtos.cs, QuickRatingDtos.cs, FlsRequests.cs
โโโ QueryBuilders/ FlsQB.cs, SurveyQB.cs, QuickRatingQB.cs, FlsLifecycleQB.cs
โโโ Implementations/ + Interfaces/
Key Dependencies
| Dependency | Used for | Injected via |
|---|---|---|
| IQueryExecutor | All Dapper queries and SPs | Constructor injection in every DAL class |
| IFlsEventBLL | Publishing FormSubmitted, InstanceCompleted, etc. to LFLSEVENTLOG | Injected into FlsSessionBLL, FlsInstanceBLL |
| IFlsDispatchBLL | Token generation, EIP dispatch, LFLSDISPATCHLOG writes | Injected into FlsInstanceBLL.OpenInstanceAsync |
| IHubContext<FlsMonitorHub, IFlsMonitorClient> | Pushing real-time updates to incharges watching an instance | Injected into FlsSessionBLL |
| IKeyInvalidate | Cache invalidation after state changes | Injected into FlsInstanceBLL (after open/pause/close) |
| IEventLogPublish | Business audit trail publishing via Dapr | Injected into all BLL classes with write operations |
Entity Relationship Summary
| Entity | Parent | Link Column | Purpose |
|---|---|---|---|
| FLS Registration | (root) | โ | Master config template (form type) |
| FLS Instance | Registration | FlsRegistrationId | Active deployment to specific people/time |
| FLS Instance Schedule | Instance | FlsInstanceId | OpensFrom, ClosesOn, Quartz job IDs |
| FLS Group | Instance | FlsInstanceId | Target audience sub-group |
| FLS Respondent | Group | FlsInstanceId, GroupId | Individual form assignee + token |
| FLS Session | Respondent | FlsRespondentId | Session tracking per attempt |
| FLS Access Rule | Registration | FlsRegistrationId | View/manage permissions per user/role |
| FLS Incharge | Instance | FlsInstanceId | Admin users assigned to monitor instance |
| Survey Instrument Config | Registration | FlsRegistrationId | The questionnaire for this registration |
| Survey Section | Instrument Config | InstrumentConfigId | Page grouping of questions |
| Survey Question (bank) | (reusable) | โ | Reusable question definitions |
| Survey Instance Question | Section | InstrumentSectionId | Question assignment to section with overrides |
| Survey Answer | Session | FlsSessionId, InstQuestionId | Individual respondent answer to a question |
| Survey Summary | Instance+Group | FlsInstanceId, GroupId | Pre-aggregated stats per question |
| QuickRating Config | (standalone) | ObjectTypeId, BizTransactionTypeId | Rating scale config for an entity type |
| QuickRating | Config | QuickRatingConfigId | Individual rating submitted |
| QuickRating Summary | Config+Entity | ObjectTypeId, ObjectId | Aggregated rating stats per entity |
| FLS Event Log | Instance | FlsInstanceId | Event queue for cross-module notifications |
| FLS Bridge Config | (standalone) | ModuleId | Module-level integration rules |
| FLS Event Subscription | Bridge Config | BridgeConfigId | Which events trigger which handlers |
| FLS Dispatch Log | Respondent | FlsRespondentId | EIP notification dispatch tracking |
| FLS Reminder Rule | Registration | FlsRegistrationId | Automated nudge/escalation schedule |
FLS Core Tables
| Table | Key Columns |
|---|---|
| MFLSREGISTRATION | FlsRegistrationId, RegistrationCode, ModuleId, CompletionMethod, AllowDraftResponse, AllowMultiAttempt, IsMandatory, TokenExpiryHours, DefaultEipTemplCode |
| MFLSREMINDERRULE | ReminderRuleId, FlsRegistrationId, DayOffset, StatusFilter, ReminderAction, EipTemplCode, MaxFireCount |
| MFLSBRIDGECONFIG | BridgeConfigId, ModuleId, ModuleCode, IsDapr, DaprPubSubTopic, EventHandlerUrl, SummarySpName, ResponseVisibility, AllowRespondentAdd |
| MFLSEVENTSUBSCRIPTION | EventSubscriptionId, BridgeConfigId, FlsRegistrationId (-1=all), EventType, IsActive, MaxRetries, RetryBackoffMin |
| MFLSACCESSRULE | AccessRuleId, FlsRegistrationId, PrincipalType, PrincipalId, AccessType (0/1/2), Scope, ScopeObjectId |
| MFLSINSTANCE | FlsInstanceId, FlsRegistrationId, InstanceCode, InstanceName, InstanceStatus, EntityObjectId, PeriodId, CreatedBy, CreatedDate |
| MFLSINSTANCESCHEDULE | FlsInstanceId, OpensFrom, ClosesOn, GlobalDeadlineDt, JobOpenId, JobCloseId, JobRemindId |
| MFLSINSTANCEGROUP | GroupId, FlsInstanceId, GroupCode, GroupName, ContextObjectTypeId, ContextObjectId, StepCount, BlockingFlag, BlockingSp, RespondentCount, SubmittedCount, OpensFrom, ClosesOn, DeadlineDt |
| MFLSRESPONDENT | FlsRespondentId, FlsInstanceId, GroupId, StepNo, RespondentType, RespondentRefId, RespondentName, RespondentMail, AccessToken (GUID), TokenExpiry, RespondentSts, FirstOpenedAt, LastSavedAt, SubmittedAt, NudgeCount, OptedOutAt |
| MFLSINCHARGE | FlsInchargeId, FlsInstanceId, UserId, AccessType |
| Table | Key Columns |
|---|---|
| TFLSRESPONSESESSION | FlsSessionId, FlsRespondentId, FlsInstanceId, GroupId, StepNo, AttemptNo, SessionStatus, StartedAt, LastSavedAt, SubmittedAt, TimeTakenSeconds |
Survey Tables
| Table | Key Columns |
|---|---|
| MSURVEYINSTRUMENTCONFIG | InstrumentConfigId, FlsRegistrationId, InstrumentTitle, InstrumentTypeId, IsAnonymous, ShowProgressBar, LangDefault, IsNpsEnabled, IsLocked, VersionNo |
| MSURVEYINSTRUMENTSECTION | InstrumentSectionId, InstrumentConfigId, SectionCode, SectionName, Description, DisplayOrder, IsActive |
| MSURVEYQUESTIONBANK | QuestionId, QuestionCode, QuestionText, QuestionTypeId (0-5), DefaultScaleMin, DefaultScaleMax, IsActive |
| MSURVEYQUESTIONOPTION | OptionId, QuestionId, OptionText, DisplayOrder, IsDefault |
| MSURVEYINSTQUESTION | InstQuestionId, InstrumentSectionId, QuestionId, DisplayOrder, IsRequiredOverride, Weight, BranchRulesJson, ScaleMin, ScaleMax |
| TSURVEYRESPONSEANSWER | AnswerId, FlsSessionId, InstQuestionId, QuestionId, AnswerText, AnswerNumeric, AnswerOptionIds (CSV), IsDraft |
| TSURVEYRESPONSESUMMARY | SummaryId, InstrumentConfigId, FlsInstanceId, GroupId, InstQuestionId, ResponseCount, SkippedCount, AvgRating, NpsPromoters, NpsPassives, NpsDetractors, NpsScore, OptionDistJson, FreeTextCount |
QuickRating Tables
| Table | Key Columns |
|---|---|
| MQUICKRATINGCONFIG | QuickRatingConfigId, ObjectTypeId, BizTransactionTypeId, ConfigName, ScaleMin, ScaleMax, IdentityMode, AllowMultiRating, AllowComment, CommentMaxLength, WidgetLabel, ThankyouLabel |
| TQUICKRATING | QuickRatingId, QuickRatingConfigId, ObjectTypeId, ObjectId, Rating (decimal), CommentText, RatingBy (user type), RatingByRefId, IsLatest, RatedOn |
| TQUICKRATINGSUMMARY | SummaryId, QuickRatingConfigId, ObjectTypeId, ObjectId, RatingCount, AvgRating, MinRating, MaxRating, RatingDistJson, NpsPromoters, NpsPassives, NpsDetractors, NpsScore, LastUpdated |
Log Tables
| Table | Key Columns |
|---|---|
| LFLSEVENTLOG | FlsEventLogId, FlsInstanceId, FlsRespondentId, BridgeConfigId, EventType (0โ6), EventStatus (0=Pending, 1=Success, 2=Failed, 3=Retry), PayloadJson (FlsEventPayload), CorrelationId, RetryCount, NextRetryOn, FailureReason |
| LFLSDISPATCHLOG | DispatchLogId, FlsRespondentId, DispatchType (0=Initial, 1=Reminder, 2=Escalation), DispatchedAt, DeliveryStatus, EipJobId, FailureReason |
Status Enums
InstanceStatus
| Value | Name |
|---|---|
| 0 | Draft |
| 1 | Scheduled |
| 2 | Open |
| 3 | Paused |
| 4 | Closed |
| 5 | Archived |
RespondentSts
| Value | Name |
|---|---|
| 0 | NotStarted |
| 1 | Opened |
| 2 | InProgress |
| 3 | Submitted |
| 4 | Expired |
| 5 | OptedOut |
Instance Endpoints
Registration Endpoints
Respondent Endpoints
Session Endpoints
Survey Endpoints
QuickRating Endpoints
Token-based Access Flow
This is the critical flow every FLS integration depends on. Understand it before building anything.
POST /fls/instances/{id}/open is called. FlsInstanceBLL transitions status to Open (2) and calls FlsDispatchBLL.DispatchAsync().
FlsDispatchBLL generates one AccessToken (GUID) per respondent in MFLSRESPONDENT. Sets TokenExpiry = now + TokenExpiryHours.
FlsDispatchBLL writes LFLSDISPATCHLOG rows (DispatchType=0, DeliveryStatus=Pending). Publishes EIP dispatch event via Dapr.
EIP picks up the job. Renders the dispatch template (DefaultEipTemplCode) with the tokenised URL embedded. Sends personalised email to each RespondentMail. Updates LFLSDISPATCHLOG.DeliveryStatus.
Respondent clicks link โ Angular survey shell calls GET /fls/respondents/{token} to validate. If expired โ shows Expired screen. If valid โ calls POST /fls/sessions/open.
POST /fls/sessions/open creates TFLSRESPONSESESSION row. Sets RespondentSts=Opened (1). Returns FlsSessionContextDto containing: FlsSessionId, survey instrument (SurveyInstrumentDto), any existing draft answers.
Respondent fills form. Auto-save fires POST /fls/surveys/{sessionId}/answers/draft โ sets RespondentSts=InProgress (2).
Respondent submits โ POST /fls/surveys/{sessionId}/answers/submit โ validates required answers โ sets RespondentSts=Submitted (3) โ triggers summary aggregation โ FlsSessionBLL fires FormSubmitted event to LFLSEVENTLOG โ pushes SignalR update to incharges.
Multi-step Forms
Multi-step works by creating multiple respondent records with different StepNo values for the same instance. Step N respondents cannot open their form until Step N-1 is complete (enforced by BlockingFlag + BlockingSp).
-- How FlsDispatchBLL enforces multi-step:
-- Step 2 tokens are generated ONLY after StepCompleted event fires for Step 1
IF @StepNo > 1
BEGIN
-- Check if previous step is complete for this group
EXEC @IsBlocked = @BlockingSp @GroupId, @StepNo - 1
IF @IsBlocked = 1
RAISERROR ('Previous step not complete' , 16 , 1 )
END
Use RespondentType to identify who fills each step: 0=Employee (Step 1), 1=Manager (Step 2), 2=HR (Step 3). This lets the SurveyResultsDto filter context โ managers see Step 1 answers alongside their Step 2 form.
Bridge Config Setup
Every module that needs to receive FLS events must have a row in MFLSBRIDGECONFIG. This is a migration-time setup โ run it once per module integration.
-- Insert Bridge Config for a new module (e.g., CRM = ModuleId 701)
INSERT INTO MFLSBRIDGECONFIG (
ModuleId, ModuleCode, IsDapr, DaprPubSubTopic,
EventHandlerUrl, SummarySpName,
ResponseVisibility, AllowRespondentAdd, AllowManualClose
) VALUES (
701 , 'CRM' ,
1 , -- 1=Dapr, 0=HTTP webhook
'crm-fls-events' , -- Dapr pub/sub topic
NULL , -- not used when IsDapr=1
'SP_CRM_FLS_UPDATESUMMARY' ,
0 , -- 0=all can view, 1=incharge only, 2=HR only
1 , -- allow adding respondents after open
1 -- allow manual close
)
Event Subscriptions
After creating a Bridge Config, subscribe to the events you need:
-- Subscribe CRM to FormSubmitted and InstanceCompleted events
-- FlsRegistrationId = -1 means: all registrations linked to this bridge config
INSERT INTO MFLSEVENTSUBSCRIPTION (BridgeConfigId, FlsRegistrationId, EventType, IsActive, MaxRetries, RetryBackoffMin)
VALUES
(@BridgeConfigId , -1 , 0 , 1 , 3 , 5 ), -- FormSubmitted: 3 retries, 5 min backoff
(@BridgeConfigId , -1 , 2 , 1 , 3 , 5 ) -- InstanceCompleted: 3 retries, 5 min backoff
To subscribe for a specific registration only (e.g., only the "CRM Satisfaction Survey" registration):
INSERT INTO MFLSEVENTSUBSCRIPTION (BridgeConfigId, FlsRegistrationId, EventType, IsActive, MaxRetries, RetryBackoffMin)
VALUES (@BridgeConfigId , 12 , 0 , 1 , 3 , 5 ) -- FlsRegistrationId=12 only
Event Retry Logic
LFLSEVENTLOG.EventStatus tracks the delivery state:
| Column | Purpose |
|---|---|
| RetryCount | Number of delivery attempts made so far |
| MaxRetries | From MFLSEVENTSUBSCRIPTION.MaxRetries โ after this many failures, EventStatus stays at 2 |
| NextRetryOn | DateTime when the retry job should attempt delivery again. Set to now + (RetryCount ร RetryBackoffMin) minutes (exponential backoff). |
| FailureReason | HTTP status code + response body, or exception message. Useful for diagnosing delivery failures. |
Integrating a New Module โ Step-by-Step Checklist
- Create MFLSREGISTRATION row (or call POST /fls/registrations) with your ModuleId, CompletionMethod, and EIP template code.
- Create MFLSBRIDGECONFIG row for your ModuleId with IsDapr/DaprTopic or EventHandlerUrl, and SummarySpName.
- Create MFLSEVENTSUBSCRIPTION rows for each FlsEventType you need (FormSubmitted, StepCompleted, InstanceCompleted are the most common).
- If using Dapr: create a Dapr subscriber in your module's SL that listens on the configured DaprPubSubTopic. If HTTP: implement a webhook endpoint that FLS can call.
- Implement your event handler BLL method. On FormSubmitted: query TSURVEYRESPONSEANSWER or TQUICKRATING for the respondent's answers; update your module's tables.
- On InstanceCompleted: call your SummarySpName (e.g., SP_CRM_FLS_UPDATESUMMARY) to aggregate final results into your module's summary table.
- Implement your module's dispatch BLL method (equivalent of TmsFeedbackBLL.DispatchBatchFeedbackAsync). Call POST /fls/instances + POST /fls/instances/{id}/respondents + POST /fls/instances/{id}/open.
- Store the returned FlsInstanceId in your module's table (e.g., TBATCH.FlsInstanceId) for cross-reference.
- Embed <app-fls-summary-widget [instanceId]="flsInstanceId"> in your module's UI to show completion %.
- Add NudgeFlsRespondent action in your module's UI for incharges to send reminders.
- If certificate/workflow blocking is needed: implement BlockingSp and reference it in the MFLSINSTANCEGROUP insert.
- Register all new DB objects (Bridge Config row, Event Subscription rows, SPs) in your module's migration script.
Implement Event Handler (Dapr Subscriber Pattern)
// In your module SL โ Dapr subscriber endpoint
public class CrmFlsEventHandler : BaseEndpoint <FlsEventPayload , ResponseStandardDTO <object >>
{
private readonly ICrmFeedbackBLL _bll;
public CrmFlsEventHandler (ICrmFeedbackBLL bll) => _bll = bll;
public override void Configure()
{
Post("/crm/webhooks/fls" ); // matches EventHandlerUrl in MFLSBRIDGECONFIG
AllowAnonymous(); // or validate Dapr header
}
protected override async Task <ResponseStandardDTO <object >> ExecuteAsync(
FlsEventPayload req, LoginDTO login, CancellationToken ct)
{
return req.EventType switch {
FlsEventType .FormSubmitted => await _bll.HandleFormSubmittedAsync(req, ct),
FlsEventType .InstanceCompleted => await _bll.HandleInstanceCompletedAsync(req, ct),
_ => ResponseStandardDTO <object >.Success("Unhandled event type" )
};
}
}
// FlsEventPayload DTO (from FLSDAL/DTOs/FlsDtos.cs)
public class FlsEventPayload
{
public int FlsEventLogId { get ; init ; }
public int FlsInstanceId { get ; init ; }
public int FlsRespondentId { get ; init ; }
public FlsEventType EventType { get ; init ; }
public string CorrelationId { get ; init ; } = string .Empty;
public string ? PayloadJson { get ; init ; }
}
SP_TMS_DISPATCHFEEDBACK โ Spec
This SP is the single-call interface from TMS to FLS. It creates the full instance structure in one transaction.
| Parameter | Type | Example | Notes |
|---|---|---|---|
| @BatchId | INT | 2047 | TMS training batch ID |
| @InstrumentConfigId | INT | 18 | Survey template to use |
| @BlockCertificate | BIT | 1 | 1=gate certificate on submission |
| @EnrollmentTvp | TVP (EnrollmentType) | See below | Table-valued parameter: one row per learner |
-- TVP type for enrollment list
CREATE TYPE EnrollmentType AS TABLE (
EnrollmentId INT ,
EmployeeId INT ,
EmployeeName NVARCHAR (200 ),
EmployeeEmail NVARCHAR (254 )
)
-- What the SP does internally:
-- 1. INSERT MFLSINSTANCE (creates instance for this batch)
-- 2. INSERT MFLSINSTANCEGROUP (one group, BlockingFlag=@BlockCertificate)
-- 3. INSERT MFLSRESPONDENT ร N (one per enrollment, generates GUIDs)
-- 4. SELECT @FlsInstanceId, @RespondentCount as output
// BLL call pattern (TmsFeedbackBLL.cs)
GB5Trace .Step("dispatch-feedback" , new { req.BatchId });
var result = await _dal.DispatchBatchFeedbackAsync(req, login, ct)
.ConfigureAwait(false );
// Then open the instance to trigger token dispatch
await _flsInstance.OpenInstanceAsync(result.FlsInstanceId, login, ct)
.ConfigureAwait(false );
SP_TMS_GETBATCHFEEDBACKSTATUS โ Spec
Returns per-learner feedback status for a batch. Called by TmsFeedbackBLL.GetBatchStatusAsync().
| Column | Type | Notes |
|---|---|---|
| EnrollmentId | INT | TMS enrollment record ID |
| EmployeeId | INT | HR employee ID |
| EmployeeName | NVARCHAR(200) | Display name |
| FlsRespondentId | INT | Cross-reference to MFLSRESPONDENT |
| RespondentSts | INT | 0โ5: current status |
| DeliveryStatus | INT | LFLSDISPATCHLOG delivery status |
| FirstOpenedAt | DATETIME | NULL if not opened yet |
| SubmittedAt | DATETIME | NULL if not submitted |
| BlocksCertificate | BIT | 1=certificate is blocked for this learner |
| FeedbackCompleted | BIT | 1=RespondentSts=3 (Submitted) |
Trainer Summary Aggregation
SP_TMS_FLS_UPDATESUMMARY is called by the TMS event handler after each FormSubmitted event. It aggregates answer values for specific question IDs into trainer-level KPIs.
-- SP aggregates survey answers into trainer KPI scores.
-- InstQuestionId mapping must match the Survey Instrument Config.
UPDATE TTRAINERFEEDBACKSUMMARY
SET
AvgKnowledge = (SELECT AVG (CAST (AnswerNumeric AS FLOAT ))
FROM TSURVEYRESPONSEANSWER
WHERE InstQuestionId = 101 -- TMS-TRAINER-KNOWLEDGE question
AND FlsInstanceId = @FlsInstanceId),
AvgClarity = (SELECT AVG (CAST (AnswerNumeric AS FLOAT ))
FROM TSURVEYRESPONSEANSWER
WHERE InstQuestionId = 102 ), -- TMS-TRAINER-CLARITY
AvgEngagement = (SELECT AVG (CAST (AnswerNumeric AS FLOAT ))
FROM TSURVEYRESPONSEANSWER
WHERE InstQuestionId = 103 ), -- TMS-TRAINER-ENGAGEMENT
OverallAvg = (AvgKnowledge + AvgClarity + AvgEngagement) / 3.0 ,
LastUpdated = GETUTCDATE ()
WHERE TrainerId = @TrainerId
The SP references specific InstQuestionIds (101, 102, 103). If the Survey Instrument Config is updated (e.g., questions reorganised with new IDs), the SP must be updated too. Consider storing the mapping in a config table (MTMSQUESTIONDIMENSION) rather than hardcoding IDs in the SP.
FlsMonitorHub Contract
// IFlsMonitorClient.cs โ strongly-typed client contract
public interface IFlsMonitorClient
{
Task ReceiveSummaryUpdate(FlsInstanceSummaryDto summary);
Task ReceiveError(string message);
}
// FlsMonitorHub.cs
public class FlsMonitorHub : Hub <IFlsMonitorClient >
{
public async Task JoinInstanceMonitor(int flsInstanceId, CancellationToken ct)
{
var login = LoginDTO .FromHttpContext(Context .GetHttpContext()!);
await Groups.AddToGroupAsync(
Context .ConnectionId,
$"fls:instance:{flsInstanceId}" , ct);
}
public async Task LeaveInstanceMonitor(int flsInstanceId, CancellationToken ct)
{
await Groups.RemoveFromGroupAsync(
Context .ConnectionId,
$"fls:instance:{flsInstanceId}" , ct);
}
}
Pushing Updates from BLL
// In FlsSessionBLL.SubmitFormAsync โ after successful submission
var updatedSummary = await _dal.GetInstanceSummaryAsync(flsInstanceId, login, ct)
.ConfigureAwait(false );
// Push live update to all incharges watching this instance
await _hubContext.Clients
.Group($"fls:instance:{flsInstanceId}" )
.ReceiveSummaryUpdate(updatedSummary)
.ConfigureAwait(false );
The Hub class is transient โ one instance per method call. For BLL-initiated pushes (outside a Hub method), always inject IHubContext<FlsMonitorHub, IFlsMonitorClient> โ never try to hold a Hub reference. This pattern is used correctly in all GB5 BLL classes.
Widget Inputs / Outputs
<!-- Angular component inputs -->
<app-quick-rating-widget
[objectTypeId ]="objectTypeId()" <!-- required: entity type -->
[objectId ]="objectId()" <!-- required: entity ID -->
[bizTypeId ]="sessionId()" <!-- optional: further scope -->
[submitterType ]="-1" <!-- optional: caller type -->
[submitterId ]="-1" <!-- optional: caller ID -->
[showSummary ]="true" <!-- show avg badge after submit -->
/>
Backend Config Lookup
-- QuickRatingQB.GET_CONFIG โ how the widget loads its configuration
SELECT qrc.QuickRatingConfigId, qrc.ScaleMin, qrc.ScaleMax,
qrc.AllowComment, qrc.CommentMaxLength,
qrc.WidgetLabel, qrc.ThankyouLabel,
qrc.AllowMultiRating, qrc.IdentityMode,
qrs.RatingCount, qrs.AvgRating, qrs.RatingDistJson -- summary
FROM MQUICKRATINGCONFIG qrc
LEFT JOIN TQUICKRATINGSUMMARY qrs
ON qrs.QuickRatingConfigId = qrc.QuickRatingConfigId
AND qrs.ObjectId = @ObjectId
WHERE qrc.ObjectTypeId = @ObjectTypeId
AND qrc.BizTransactionTypeId = @BizTransactionTypeId
AND qrc.ClientId = @ClientId -- tenant filter
Event Retry Pattern
// FlsEventBLL.PublishAsync โ writes to LFLSEVENTLOG
public async Task PublishAsync(int flsInstanceId, int flsRespondentId,
FlsEventType eventType, LoginDTO login, CancellationToken ct)
{
GB5Trace .Step("publish-fls-event" , new { flsInstanceId, eventType });
var subscriptions = await _dal.GetActiveSubscriptionsAsync(
flsInstanceId, eventType, login, ct)
.ConfigureAwait(false );
foreach (var sub in subscriptions)
{
await _dal.InsertEventLogAsync(new FlsEventLogRow
{
FlsInstanceId = flsInstanceId,
FlsRespondentId = flsRespondentId,
BridgeConfigId = sub.BridgeConfigId,
EventType = (int )eventType,
EventStatus = 0 , // Pending
PayloadJson = JsonSerializer .Serialize(new FlsEventPayload { ... }),
CorrelationId = Guid .NewGuid().ToString()
}, login, ct).ConfigureAwait(false );
}
}
// Background Quartz job (FlsEventDispatchJob) polls LFLSEVENTLOG:
// WHERE EventStatus IN (0, 3) AND (NextRetryOn IS NULL OR NextRetryOn <= GETUTCDATE())
// Dispatches via Dapr or HTTP; updates EventStatus to 1 (success) or 2/3 (fail/retry)
Dead Letter Handling
When RetryCount >= MaxRetries and delivery still fails, the event row stays at EventStatus=2 (Failed). There is no automatic dead-letter queue โ failed events must be handled manually or by alerting.
Recommended approach:
- Create a monitoring query or Grafana alert on WHERE EventStatus=2 AND RetryCount >= MaxRetries
- Investigate the FailureReason column (HTTP error body or exception message)
- Fix the downstream handler if it was a handler bug
- Reset EventStatus=3, RetryCount=0, NextRetryOn=null to force re-delivery
- If the event is no longer relevant (e.g., instance already archived), set EventStatus=1 manually with a note in FailureReason
Testing Checklist
- Token generation and expiry: Open an instance, verify MFLSRESPONDENT.AccessToken is non-null and TokenExpiry is set correctly (now + TokenExpiryHours). Test the expired path: manually backdate TokenExpiry, call /fls/respondents/{token} โ expect "Expired" response.
- Draft auto-save: Open session, submit draft answers, close browser, re-open session. Verify FlsSessionContextDto includes resumeDraftAnswers with the saved answers.
- Submit validates required fields: Call submit with a missing IsRequired answer. Expect 400 with validation error. Then fill all required fields โ expect 200.
- Multi-step sequencing: Create 2-step instance. Verify Step 2 respondents cannot open form until Step 1 submitted. Submit Step 1 โ StepCompleted fires โ Step 2 tokens generated.
- Anonymous mode hides names: Create anonymous instrument. Submit responses. Call /fls/surveys/{id}/responses โ verify respondent names are null/empty, not the actual names.
- FormSubmitted event fires: Submit a form. Query LFLSEVENTLOG WHERE FlsInstanceId=X and EventType=0. Expect a row with EventStatus=0 (Pending) or 1 (Success).
- Bridge event delivery: Verify your event handler receives the FlsEventPayload with the correct FlsInstanceId, FlsRespondentId, and EventType after submission.
- Dapr retry simulation: Bring down your event handler temporarily. Submit a form. Verify LFLSEVENTLOG shows EventStatus=3 (Retry) with RetryCount incrementing. Restart handler โ verify EventStatus transitions to 1.
- Certificate gating: Dispatch with BlockCertificate=1. Verify SP_TMS_CHECKFEEDBACKBLOCK returns 1 (blocked) for a Not Started learner. Submit feedback โ verify it now returns 0 (allowed).
- SignalR push: Connect to FlsMonitorHub (JoinInstanceMonitor). Submit a form. Verify ReceiveSummaryUpdate is called on the connected client with incremented submitted count.
- QuickRating summary update: Submit a rating. Call /quickrating/widget again for the same entity. Verify RatingCount incremented and AvgRating updated.
- Nudge re-dispatch: Nudge a respondent. Verify LFLSDISPATCHLOG has a new row with DispatchType=1 (Reminder) and MFLSRESPONDENT.NudgeCount incremented.
GoodBooks GB5 ยท FLS Developer Guide ยท 2026-07-01