Skip to content

chore(#3793): resolve connection roles from RoleConstants - #3903

Draft
Thuen wants to merge 1 commit into
mainfrom
chore/3793-roleconstants-enricher
Draft

chore(#3793): resolve connection roles from RoleConstants#3903
Thuen wants to merge 1 commit into
mainfrom
chore/3793-roleconstants-enricher

Conversation

@Thuen

@Thuen Thuen commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Description

ConnectionEntityEnricher.FetchRolesAsync loaded all 148 roles, joined to provider and provider type, on every enrichment call — which is essentially every enduser connection query. Roles are seeded from RoleConstants and nothing writes the table at runtime, so it is only a projection of the constants and the round-trip buys nothing. The method already carried a TODO pointing at this issue; it is now gone along with the method.

One thing the issue got wrong

The issue states that RoleConstants includes "their Provider and Provider.Type relationships". It does not — the constants only hold foreign keys:

// RoleConstants.Rightholder.Entity
ProviderId = ProviderConstants.Altinn3,   // Provider navigation is null

// ProviderConstants.Altinn2.Entity
TypeId = ProviderTypeConstants.System,    // Type navigation is null

A straight swap to TryGetById would therefore have shipped Role.Provider == null, and that graph reaches the API: RoleDto carries Provider, and ProviderDto carries Type. ConnectionsControllerTest+GetRoles already asserts r.Role.Provider?.Code == "sys-altinn2", so it would have caught it — but only after the fact. The lookup therefore rebuilds the same graph the query produced with its includes, from RoleConstants, ProviderConstants and ProviderTypeConstants.

Role.EntityType is deliberately left unset, because the previous query did not include it either.

Why the graph is rebuilt rather than assigned onto the constants

Setting Provider on the constants' own Entity instances would be the obvious shortcut, but those instances are exactly what StaticDataIngest hands to EF as seeds. DbSet.Add tracks the entity and every reachable untracked related entity as Added, so a populated navigation on a seed makes adding a new role cascade an insert of its provider.

That failure would have been invisible in CI: on a fresh database the provider constants are themselves tracked from their own ingest pass, so nothing happens. On an existing database the tracked instances are the ones loaded from the table, the constants stay untracked, and the first newly added role would hit a PK violation on provider at startup. Keeping the seeds free of navigations avoids the whole question.

Fallback for unknown ids

ApplyEnrichment looks roles up with the dictionary indexer. Previously the dictionary came from the database, so any role an assignment referenced was present by construction. Resolving from constants opens a gap: StaticDataIngest never deletes, so a role dropped from RoleConstants in an earlier release can still exist in the table and be referenced by an assignment made while it was current.

ResolveRolesAsync collects the RoleId and ViaRoleId values the constants do not cover, logs a warning naming them, and queries for just those rows with the same includes. When nothing is missing — the normal path — it returns the shared lookup with no query, no allocation and no logging. An id absent from both constants and database still throws, which is correct for a genuinely dangling FK.

Reaching the fallback means the constants and the table have drifted, which is worth an alert, hence LogWarning. ConnectionQuery now takes an ILogger to pass on to the enricher, following TranslationService — constructor-injected ILogger<T> with structured PascalCase placeholders — which is the existing logging precedent in this project. Its two manual construction sites are updated: the FFB tool injects the logger, and ConnectionQueryTests passes NullLogger<ConnectionQuery>.Instance as the other TranslationService tests already do.

Related Issue(s)

Verification

  • Your code builds clean without any errors or warnings
  • Manual testing done (required)
  • Relevant automated test added (if you find this hard, leave it and we'll help out)
  • All tests run green

Run against a real Postgres:

  • ConnectionsControllerTest+GetRoles: 10/10 — includes the Provider?.Code == "sys-altinn2" assertion, which is the proof the provider graph survives the refactor.
  • ConnectionsControllerTest+GetRolesRoleFallback: 1/1 — new. Writes a role that exists only in the database, assigns it, and asserts the endpoint returns it with name, code, Provider.Code and Provider.Type populated. Uses its own ApiFixture rather than the shared read-only collection because it writes. Verified non-vacuous: disabling the fallback makes it fail with KeyNotFoundException on that role id, surfacing as a 500.
  • Altinn.AccessMgmt.PersistenceEF.Tests: 47/47 — includes 2 new tests asserting every role resolves its provider and every provider resolves its type, so a constant pointing at a non-constant cannot silently produce a null Provider. Verified non-vacuous by pointing one role at a random Guid.
  • Full enduser suite: 480 tests, 30 failures — all pre-existing. Every one is 127.0.0.1:10000, the Azurite blob emulator used by PolicyRepository, which was not running locally. Baseline on a clean tree was 32 failures from the same cause.

Two verification gaps worth naming:

  • The LogWarning itself is not asserted. It sits unconditionally on the fallback path, and the test above proves that path is taken, so the call does execute — but ApiFixture sets Logging:LogLevel:* = Error, so Warning is filtered out of integration-test output and cannot be observed without changing the fixture for all tests.
  • Altinn.AccessManagement.Tests cannot be built locally, because its test-data file paths exceed Windows MAX_PATH under this worktree. The one-line change to ConnectionQueryTests was checked by confirming the build produces 142 MSB3030 file-copy errors and zero CS errors, so the C# compiles; running ConnectionQueryTests is left to CI.

Documentation

  • User documentation is updated with a separate linked PR in altinn-studio-docs. (if applicable)

🤖 Generated with Claude Code

ConnectionEntityEnricher loaded all 148 roles, joined to provider and
provider type, on every enrichment call. Roles are seeded from
RoleConstants and never written at runtime, so the table is only a
projection of the constants and the round-trip buys nothing.

The constants hold foreign keys rather than navigations, so the lookup
rebuilds the role -> provider -> provider type graph the query used to
produce with its includes. That graph reaches the API: RoleDto carries
Provider and ProviderDto carries Type. It is rebuilt into its own
structure rather than assigned onto the constants' entities, because
those instances are the seeds StaticDataIngest hands to EF, and a
populated reference navigation on a seed makes DbSet.Add cascade an
insert of the referenced provider.

StaticDataIngest never deletes, so a role dropped from the constants in
an earlier release can still exist in the database and be referenced by
an assignment made while it was current. Unknown ids therefore fall back
to a query for just those rows, instead of failing the whole read on one
stale row, and log a warning naming them because reaching that path means
the constants and the table have drifted. The normal path returns the
shared lookup with no query and no allocation.

ConnectionQuery takes an ILogger to pass on to the enricher, following
TranslationService, which is the existing logging precedent in this
project. Its two manual construction sites are updated accordingly.

Removes the TODO this issue was filed from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Thuen
Thuen force-pushed the chore/3793-roleconstants-enricher branch from d7c2a74 to a9ee77a Compare August 17, 2026 11:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant