AutoMate Technisch Help

OrgModel Draft Resoluties

Waarom resoluties?

De impact-evaluatie kan blokkerende problemen vinden — situaties waarbij het activeren van een concept ongewenste bijwerkingen heeft op downstream systemen. Een voorbeeld: een persona-filter verwijst naar een attribuut dat in het concept wordt hernoemd of verwijderd. Zonder tussenkomst zou activatie die filter ongeldig maken.

In plaats van activatie hard te blokkeren tot een beheerder handmatig ingrijpt, biedt het resolutiemechanisme een begeleide keuzeflow: de evaluator beschrijft welke acties beschikbaar zijn om het probleem op te lossen, de gebruiker kiest een actie, en de API voert die actie asynchroon uit als een Hangfire-job. Zodra een resolutie de status Completed bereikt, onderdrukt de impact-evaluatie het bijbehorende probleem en is CanActivate niet langer geblokkeerd door dat specifieke probleem.

Volledige flow

HangfireDatabaseAPIHangfireDatabaseAPIBeheerderGET /draft/impactLaad concept + changes + completed resolutiesleutels200 { CanActivate: false, blockingIssues: [..., resolutionActions: [...]] }POST /draft/resolutions\n[{ issueCode, affectedEntityId, chosenActionType, actionPayload }]Upsert resolutierecord (Status = Pending)Enqueue OrgModelResolutionJob(resolutionId)201 { resolutions: [{ resolutionId, hangfireJobId }] }Status = ProcessingVoer handler uit (bijv. PersonaFilterUpdateHandler)Status = Completed (of Failed + retry)GET /draft/impactCompleted resolutiesleutels bevatten nu dit probleem200 { CanActivate: true }POST /draft/activateHervoer impact (guard)Activeer concept + snapshot204 No ContentBeheerder

Resolutie statussen

Status

Betekenis voor activatie

Pending

Job is ingepland maar nog niet gestart. Het bijbehorende probleem blokkeert activatie nog.

Processing

Job is gestart door een Hangfire-worker. Blokkeert activatie nog.

Completed

Handler heeft de actie succesvol uitgevoerd. De evaluator slaat dit probleem voortaan over.

Failed

Handler heeft een fout gegooid na alle retries. Blokkeert activatie. Zie het Error-veld.

Alleen Completed onderdrukt een blokkerend probleem. Een Failed resolutie kan opnieuw worden ingediend via POST /draft/resolutions; dit overschrijft het vorige record en plant een nieuwe job in.

OrgModelDraftResolutionRecord

Dit is het database-entiteitsmodel voor een resolutierecord (OrgModel_DraftResolutions):

Kolom

Type

Nullable

Beschrijving

Id

UNIQUEIDENTIFIER

Nee

Primaire sleutel. Nieuw GUID bij aanmaak; behouden bij upsert.

DraftVersionId

UNIQUEIDENTIFIER

Nee

Verwijst naar het concept waarop de resolutie van toepassing is.

TenantId

UNIQUEIDENTIFIER

Nee

Tenant waarvoor de resolutie geldt.

IssueCode

NVARCHAR(200)

Nee

Machine-leesbare probleemcode, bijv. persona.attribute_in_use.

AffectedEntityId

NVARCHAR(200)

Ja

ID van het betrokken object (bijv. persona-ID). NULL voor algemene issues.

ChosenActionType

NVARCHAR(200)

Nee

Gekozen actietype, bijv. persona.update_filter_value.

ActionPayload

NVARCHAR(MAX)

Ja

JSON-string met actie-specifieke parameters, opgeslagen als vrije tekst.

Status

NVARCHAR(50)

Nee

Huidige status: Pending, Processing, Completed of Failed.

HangfireJobId

NVARCHAR(200)

Ja

ID van de ingeplande Hangfire-job. Ingevuld na enqueue.

ScheduledAt

DATETIME2

Nee

Tijdstip van indiening (UTC).

ExecutedAt

DATETIME2

Ja

Tijdstip waarop de job is voltooid of mislukt (UTC).

Error

NVARCHAR(MAX)

Ja

Foutmelding bij Failed-status; NULL bij succes.

OrgModelResolutionJob

OrgModelResolutionJob is de Hangfire-job die een ingediende resolutie uitvoert. De klasse is gedecoreerd met [AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Fail)], wat betekent dat Hangfire de job automatisch tot drie keer opnieuw probeert bij een fout.

Stappenplan

  1. Record ophalen — de job laadt het OrgModelDraftResolutionRecord op basis van het meegegeven resolutionId. Als het record ontbreekt, gooit de job een InvalidOperationException (Hangfire zal ook dit als een mislukte poging tellen en opnieuw proberen).

  2. Status naar Processing — het record wordt bijgewerkt zodat de UI de voortgang kan tonen.

  3. Handler opzoeken — alle geregistreerde IOrgModelResolutionHandler-implementaties worden doorzocht op ActionType == resolution.ChosenActionType. Als er geen handler is gevonden, gooit de job een InvalidOperationException.

  4. Handler aanroepenhandler.ExecuteAsync(resolution, ct) voert de feitelijke resolutielogica uit.

  5. Status naar Completed — bij succes wordt het record bijgewerkt met ExecutedAt = DateTime.UtcNow.

  6. Foutafhandeling — bij een uitzondering wordt het record bijgewerkt met Status = Failed en de foutmelding in Error, waarna de uitzondering opnieuw wordt gegooid. Hangfire ziet de re-throw en plant de volgende retry in.

[AutomaticRetry(Attempts = 3, OnAttemptsExceeded = AttemptsExceededAction.Fail)] public class OrgModelResolutionJob(...) { [JobDisplayName("OrgModel Draft Resolution [{0}]")] public async Task ExecuteAsync(Guid resolutionId, CancellationToken ct = default) { // ... stap 1 t/m 6 throw; // Re-throw zodat Hangfire de retry triggert } }

De displaynaam "OrgModel Draft Resolution [{0}]" maakt jobs goed herkenbaar in het Hangfire-dashboard.

IOrgModelResolutionHandler

Elke resolutieactie wordt uitgevoerd door een implementatie van IOrgModelResolutionHandler:

public interface IOrgModelResolutionHandler { /// <summary>Uniek actietype dat deze handler afhandelt, bijv. "persona.update_filter_value".</summary> string ActionType { get; } /// <summary>Voert de resolutie uit. Aangeroepen vanuit OrgModelResolutionJob.</summary> Task ExecuteAsync(OrgModelDraftResolutionRecord resolution, CancellationToken ct); }

Handlers worden via DI geregistreerd. OrgModelResolutionJob zoekt at runtime de juiste handler op via ActionType. Er is geen centrale registratie van actietypen nodig.

Nieuwe resolution handler implementeren

Stap 1: Handler-klasse aanmaken

internal sealed class PersonaFilterUpdateHandler( IPersonaRepository personas) : IOrgModelResolutionHandler { public string ActionType => "persona.update_filter_value"; public async Task ExecuteAsync(OrgModelDraftResolutionRecord resolution, CancellationToken ct) { if (resolution.AffectedEntityId is null) throw new InvalidOperationException("AffectedEntityId is required for persona filter updates."); var payload = JsonSerializer.Deserialize<PersonaFilterUpdatePayload>( resolution.ActionPayload ?? "{}"); var personaId = Guid.Parse(resolution.AffectedEntityId); await personas.UpdateFilterValueAsync( resolution.TenantId, personaId, payload!.NewAttributeId, ct); } }

Stap 2: Registreer in DI

services.AddScoped<IOrgModelResolutionHandler, PersonaFilterUpdateHandler>();

Er zijn verder geen wijzigingen nodig. De job vindt de handler automatisch op basis van ActionType.

Resolutie-bewuste impact context

OrgModelImpactContext bevat de property CompletedResolutionKeys — een IReadOnlySet<string> met sleutels in het formaat "{IssueCode}:{AffectedEntityId ?? string.Empty}". Evaluators gebruiken de helpermethode IsResolved om te bepalen of een probleem al is afgehandeld:

// Sleutelformaat: "persona.attribute_in_use:3fa85f64-5717-4562-b3fc-2c963f66afa6" public bool IsResolved(string code, string? entityId = null) => CompletedResolutionKeys.Contains($"{code}:{entityId ?? string.Empty}");

Een evaluator past dit als volgt toe:

foreach (var change in context.Changes.Where(c => c.ChangeType == "Remove")) { // Sla over als dit specifieke probleem al is opgelost if (context.IsResolved("persona.attribute_in_use", change.AttributeId.ToString())) continue; // Voeg anders een blokkerend probleem toe context.BlockingIssues.Add(...); }

De service laadt de completed keys via IOrgModelDraftResolutionsRepository.GetCompletedResolutionKeysAsync en vult de context voor de evaluatorpipeline wordt gestart.

POST /draft/resolutions endpoint

Request

POST /usertypes/org-model/versions/draft/resolutions X-Tenant-Id: {tenantId} Content-Type: application/json { "resolutions": [ { "issueCode": "persona.attribute_in_use", "affectedEntityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "chosenActionType": "persona.update_filter_value", "actionPayload": { "newAttributeId": "d290f1ee-6c54-4b01-90e6-d701748f0851", "newAttributeName": "Afdeling (nieuw)" } } ] }

Het veld actionPayload is een vrije JSON-structuur. De inhoud wordt als raw JSON-string opgeslagen en later doorgegeven aan de handler. De handler is verantwoordelijk voor deserialisatie.

Response

HTTP/1.1 201 Created { "resolutions": [ { "resolutionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "issueCode": "persona.attribute_in_use", "affectedEntityId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "hangfireJobId": "12345" } ] }

Upsert-gedrag

Het endpoint werkt met een upsert op de sleutel (DraftVersionId, IssueCode, AffectedEntityId). Als een resolutie opnieuw wordt ingediend voor hetzelfde probleem:

  • Het bestaande record wordt bijgewerkt met de nieuwe ChosenActionType en ActionPayload.

  • De status wordt teruggezet naar Pending.

  • HangfireJobId en ExecutedAt worden gewist.

  • Een nieuwe Hangfire-job wordt ingepland.

Dit maakt het mogelijk om een Failed resolutie te corrigeren door dezelfde of een andere actie opnieuw in te dienen.

Database tabel

De tabel OrgModel_DraftResolutions wordt aangemaakt via FluentMigrator-migratie 069:

Kolom

SQL-type

Nullable

Constraint

Id

UNIQUEIDENTIFIER

Nee

PK_OrgModel_DraftResolutions

DraftVersionId

UNIQUEIDENTIFIER

Nee

TenantId

UNIQUEIDENTIFIER

Nee

IssueCode

NVARCHAR(200)

Nee

AffectedEntityId

NVARCHAR(200)

Ja

ChosenActionType

NVARCHAR(200)

Nee

ActionPayload

NVARCHAR(MAX)

Ja

Status

NVARCHAR(50)

Nee

Default: 'Pending'

HangfireJobId

NVARCHAR(200)

Ja

ScheduledAt

DATETIME2

Nee

ExecutedAt

DATETIME2

Ja

Error

NVARCHAR(MAX)

Ja

Upsert-sleutel: (DraftVersionId, IssueCode, AffectedEntityId) — de MERGE-query gebruikt deze drie kolommen om te bepalen of een record wordt aangemaakt of bijgewerkt.

Index: IX_odr_DraftVersionId_IssueCode op (DraftVersionId ASC, IssueCode ASC, AffectedEntityId ASC) versnelt de lookups door de resolutiepipeline.

Foutcodes

Foutcode

HTTP-status

Situatie

orgmodel.version.no_draft_to_submit_resolutions

422

Er is geen open concept om resoluties aan te koppelen.

orgmodel.version.blocking_issues_not_resolved

422

Activatie geblokkeerd; er zijn nog blokkerende problemen zonder Completed-resolutie.

Observeerbaarheid

Hangfire-dashboard

Elke resolutie-job is zichtbaar in het Hangfire-dashboard onder de displaynaam "OrgModel Draft Resolution [{resolutionId}]". Vanuit het dashboard zijn de volgende zaken te inzien:

  • De huidige status van de job (Enqueued, Processing, Succeeded, Failed).

  • Het aantal uitgevoerde pogingen en de volgende geplande retry.

  • De volledige stack trace bij een mislukte poging.

Databasekolom Error

Bij een mislukte resolutie slaat de job de uitzonderingsboodschap op in de kolom Error van OrgModel_DraftResolutions. Dit maakt directe diagnose mogelijk zonder het Hangfire-dashboard te raadplegen:

SELECT IssueCode, ChosenActionType, Status, Error, ExecutedAt FROM dbo.OrgModel_DraftResolutions WHERE DraftVersionId = '...' AND Status = 'Failed';

Gerelateerde pagina's

Last modified: 23 March 2026