The partial class ClientController had two constructors declared across ClientController.cs and ClientController.Collections.cs. ASP.NET Core DI failed to pick one at request time with: System.InvalidOperationException: Multiple constructors accepting all given argument types have been found in type 'Yavsc.Controllers.ClientController'. Move IHtmlLocalizer<ClientController> into the primary constructor in ClientController.cs and drop the duplicate one in ClientController.Collections.cs. The Collections partial now keeps only its readonly field and action methods; the constructor and field assignment are unified on the main file. Also add the missing 'using Microsoft.AspNetCore.Mvc.Localization;' to ClientController.cs so IHtmlLocalizer resolves.
13 KiB
Client editor overhaul — Yavsc.Org administration
Goal
Bring the OAuth2 client administration UI (/Client/Edit/{id} and friends)
in Yavsc.Org to feature parity with the IdentityServer8 Client entity
model. Today the editor only exposes a handful of scalar fields and a few
single-line inputs for collections; the bulk of the entity and its
related collections are unreachable from the UI.
Inventory — current state
Properties exposed by Views/Client/Edit.cshtml
| Field | Type | Notes |
|---|---|---|
ClientId |
string | hidden, identifier |
Enabled |
bool | checkbox |
ClientName |
string | display name |
FrontChannelLogoutUri |
string | only front-channel, no back-channel |
RedirectUris |
collection | rendered as a single text input |
IdentityTokenLifetime |
int | seconds |
AbsoluteRefreshTokenLifetime |
int | seconds |
ClientSecrets |
collection | rendered as a single text input |
AccessTokenType |
enum | dropdown (custom SetAppTypesInputValues) |
Properties of IdentityServer8.EntityFramework.Entities.Client NOT in the editor
Core scalars (16 fields missing):
DescriptionClientUriLogoUriRequireConsentRequirePkceRequireRequestObjectRequireClientSecretAllowPlainTextPkceAllowOfflineAccessAllowRememberConsentAlwaysIncludeUserClaimsInIdTokenAlwaysSendClientClaimsAuthorizationCodeLifetimeBackChannelLogoutUriBackChannelLogoutSessionRequiredCibaLifetimeClientClaimsPrefixConsentLifetimeCreatedDeviceCodeLifetimeEnableLocalLoginEnabledFrontChannelLogoutSessionRequiredIncludeJwtIdLastAccessedLogoUriNonEditablePairwiseSubjectSaltPollingIntervalProtocolTypeRefreshTokenExpirationRefreshTokenUsageSlidingRefreshTokenLifetimeUpdateAccessTokenClaimsOnRefreshUpdatedUserCodeTypeUserSsoLifetime
Collections (8 missing — currently either not exposed at all, or jammed into a single-line text input that doesn't work for an IEnumerable):
AllowedGrantTypes→ClientGrantType(GrantType)AllowedScopes→ClientScope(Scope)RedirectUris→ClientRedirectUri(RedirectUri) — exposed but brokenPostLogoutRedirectUris→ClientPostLogoutRedirectUri(PostLogoutRedirectUri)AllowedCorsOrigins→ClientCorsOrigin(Origin)IdentityProviderRestrictions→ClientIdPRestriction(Provider)Claims→ClientClaim(Type, Value)Properties→ClientProperty(Key, Value)ClientSecrets→ClientSecret(Type, Value, Description, Created, Expiration) — exposed but brokenAllowedSigningAlgorithms→ scalar string collection on Client itself
Pages to add
Pattern: one Razor page per collection under
Views/Client/Edit{Collection}.cshtml. Each page lists existing rows,
offers an "Add" form with the relevant fields, and a per-row
remove button. The main Edit.cshtml becomes a hub page with links
to each subpage plus the scalar fields it already has.
| Page | Route | Form fields |
|---|---|---|
Edit.cshtml |
GET /Client/Edit/{id} (existing) |
scalar fields + nav links |
EditRedirectUris.cshtml |
GET /Client/EditRedirectUris/{id} |
RedirectUri |
EditPostLogoutRedirectUris.cshtml |
GET /Client/EditPostLogoutRedirectUris/{id} |
PostLogoutRedirectUri |
EditScopes.cshtml |
GET /Client/EditScopes/{id} |
Scope (with select of known scopes) |
EditGrantTypes.cshtml |
GET /Client/EditGrantTypes/{id} |
GrantType (with select of known types) |
EditCorsOrigins.cshtml |
GET /Client/EditCorsOrigins/{id} |
Origin |
EditIdPRestrictions.cshtml |
GET /Client/EditIdPRestrictions/{id} |
Provider |
EditClaims.cshtml |
GET /Client/EditClaims/{id} |
Type, Value |
EditProperties.cshtml |
GET /Client/EditProperties/{id} |
Key, Value |
EditSecrets.cshtml (replacement) |
GET /Client/EditSecrets/{id} |
Type, Value, Description, Expiration |
Partial view _EditableList.cshtml factored once and consumed by all
of the above.
Controller actions to add
For each collection Foo:
GET EditFoo(int id)— load the client, render the pagePOST AddFoo(int id, …)— append a row, redirect toEditFooPOST RemoveFoo(int id, int rowId)— delete a row, redirect
Verification
dotnet build src/Yavsc.Org/Yavsc.Org.csproj→ 0 errors- No tests in
Yavsc.Org.Testsexercise the controller today (perfind … -name "ClientController*" -not -path "*/bin/*"). Smoke-test by logging in as admin, hitting/Client/Edit/1, then eachEdit*/1page, and verifying the add/remove POSTs. - Existing seed flow (
MigratePostItClientToPublicinHostingExtensions.cs) must keep working — the editor changes are additive, not destructive.
Out of scope
- Tests (no MVC test infrastructure currently exists for this controller)
- Migration of existing collection fields (the broken
RedirectUristext input will simply be replaced by the new subpage) - Per-collection authorization policies (the controller is already
[Authorize("AdministratorOnly")]) - Client cloning / templating / JSON import-export
Status
2026-06-21 16:04 — kickoff. Inventory done. Pages not yet started.
2026-06-21 16:11 — first delivery, build does not compile by design (per Paul: "Tu peux même me laisser un travail qui ne compile pas"). The structural work is done; the residual errors are easy fixes Paul will do in a debug session.
Files added (working tree, not yet committed):
src/Yavsc.Org/Controllers/Administration/ClientController.Collections.cs— partial class with the per-collection GET / Add / Remove actions.src/Yavsc.Org/Views/Client/EditRedirectUris.cshtmlsrc/Yavsc.Org/Views/Client/EditPostLogoutRedirectUris.cshtmlsrc/Yavsc.Org/Views/Client/EditScopes.cshtmlsrc/Yavsc.Org/Views/Client/EditGrantTypes.cshtmlsrc/Yavsc.Org/Views/Client/EditCorsOrigins.cshtmlsrc/Yavsc.Org/Views/Client/EditIdPRestrictions.cshtmlsrc/Yavsc.Org/Views/Client/EditClaims.cshtmlsrc/Yavsc.Org/Views/Client/EditProperties.cshtmlsrc/Yavsc.Org/Views/Client/EditSecrets.cshtmlsrc/Yavsc.Org/Views/Client/_EditableStringList.cshtml— partial consumed by the single-string-field collection pages.
Files modified:
src/Yavsc.Org/Controllers/Administration/ClientController.cs—class→partial class; theEdit(int id)GET now usesLoadClientAsyncto load all navigations (so the new Edit.cshtml can render counts in its nav links).src/Yavsc.Org/Views/Client/Edit.cshtml— significantly enriched: nav links to the 9 sub-pages, all the scalar fields split into fieldsets (Security, Logout, Tokens, Device / CIBA, Tokens-extra), ClientId / Id hidden.
Known residual compile errors (4 errors total)
Paul is fixing these in a debug session. The structure is sound; the
errors are missing properties on the Client entity, a Razor
nullable quirk, and a Localizer injection miss.
Edit.cshtml:249—PairwiseSubjectSaltdoesn't exist onIdentityServer8.EntityFramework.Entities.Client. Fix: drop the field from Edit.cshtml; IdentityServer8 likely uses a different property name (e.g. on a related entity) or doesn't expose it.Edit.cshtml:221—CibaLifetimedoesn't exist onClient. Fix: same as above. CIBA flow may be configured elsewhere (resource-level) or via a different property.ClientController.Collections.cslines 181, 217, 253, 304 —Localizeris not available in the partial class. Fix: injectIStringLocalizer<ClientController>via the constructor, or inline the strings ("BothTypeAndValueRequired", "KeyRequired", "ValueRequired", "SecretValueRequired").EditSecrets.cshtml:44—s.Expiration?.ToString("u")on aDateTime?. Fix: justs.Expiration?.ToString("u")works if you writes.Expiration.Value.ToString("u"), or use(s.Expiration is null ? "" : s.Expiration.Value.ToString("u")), ors.Expiration?.ToString("u") ?? string.Empty.
Suggested next session
Once the 4 compile errors are fixed and the pages render:
- Smoke test by logging in as admin, hitting
/Client/Edit/1, then eachEdit*/1page, and verifying add/remove POSTs. - Add a confirmation prompt (or 2-step form) for Remove actions — removing a Redirect URI is destructive and one click is too easy.
- Wire up some collection-level validation (e.g. redirect URI must be a valid URL) at the controller level.
- Add tests — the project doesn't have MVC test infrastructure
today; consider adding a
Yavsc.Org.Testsproject that drives the controller viaWebApplicationFactory<Program>.
Test bootstrap notes (session of 2026-06-21 17:00+)
When adding new integration tests against WebServerFixture:
-
Skip
/Account/Loginroundtrip. The fixture ships withoutMapRazorPages()(commented out inHostingExtensions.ConfigurePipeline), so/Identity/Account/Loginis 404, and the custom/Account/Loginroute requires a complex antiforgery dance. Instead, build aClaimsPrincipalfor the test user viaUserManager+IUserClaimsPrincipalFactory<ApplicationUser>, then callIAuthenticationService.SignInAsyncon a syntheticDefaultHttpContextand replay the resultingSet-Cookieheader into the testHttpClient. SeeClientControllerCollectionTests.IssueIdentityCookie. -
Create the
Administratorrole before assigning it. ASP.NET Identity stores roles inAspNetRoles; there is no automatic seed. The constant name isYavscConstants.AdminGroupName="Administrator". UseRoleManager<IdentityRole>.CreateAsync(new IdentityRole("Administrator"))beforeAddToRoleAsync. -
Use
InMemoryconnection string to bypass the prod signing-cert requirement.HostingExtensions.AddIdentityServerrequires a PEM cert unlessbuilder.Environment.IsDevelopment()ORUsesInMemoryProvider(connectionString). The fixture already usesInMemory, soAddDeveloperSigningCredential()is called automatically — but only after we wired this check in (see commit history). -
Field-name gotchas (from disassembling HigginsSoft IdentityServer8.EntityFramework.Entities.Client 8.0.5-preview-net9):
PairWiseSubjectSalt(capital W on "Wise"), notPairwiseSubjectSalt.CibaLifetimeandPollingIntervaldo NOT exist onClientin this version.ConsentLifetimeandUserSsoLifetimeareint?.
-
MapStaticAssets()fails on test projects. CallingMapStaticAssets()resolves a manifest file (<project>.staticwebassets.endpoints.json) that test projects don't produce. Skip whenWebRootPathpoints at the test assembly directory. -
Routing 404 on /Client/Edit/{id} via WebServerFixture. As of this session, the GET endpoint returns 404 even with admin header. The route mapping is intact (
MapDefaultControllerRoute()), so this is likely an MVC convention routing issue with theControllers/Administration/subdirectory. To investigate next session: log middleware pipeline or hit/Clientindex first to see if any Client route resolves. -
MapStaticAssets()is unconditional in prod, but blocks tests.WebApplication.CreateBuilderdefaultsContentRootPathtoAppContext.BaseDirectory. In test runs that resolves tosrc/Yavsc.Org.Tests/bin/Debug/net10.0/, whereYavsc.Org.Tests.staticwebassets.endpoints.jsondoesn't exist (it's generated only by projects with the Web SDK). Theapp.MapStaticAssets()call insideConfigurePipelinethen throws and the fixture fails to start — taking every test in the[Collection("Yavsc Server")]down with it.This is a pre-existing fragility of the WebServerFixture that the new test work surfaced. Fixing it cleanly requires either: (a) moving the test project to the Web SDK so it produces its own manifest, (b) copying the manifest at build time via an MSBuild target, or (c) routing
MapStaticAssetsthrough an assembly-resolution fallback. None attempted in this session — recorded for next session.