Skip to content

Let consumers register their own Credential subclasses [minor] - #194

Merged
matt-edmondson merged 2 commits into
mainfrom
fix/171-register-credential-types
Oct 27, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
fix/171-register-credential-types

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #171

What was wrong

Credential's polymorphic type set was closed: only the three [JsonDerivedType] subclasses on the base could be serialized.

  • A subclass defined in a consuming assembly failed on every platform store with a raw NotSupportedException.
  • InMemoryCredentialStore never serializes, so it accepted the same subclass and hid the failure in consumers' tests.
  • The README recipe only worked by editing Credential.cs in this repository.

Change

This takes the "open the type set" option from the issue and triage.

  • CredentialSerialization.RegisterDerivedType<T>(string typeDiscriminator) (new public API) adds a subclass to the set through a DefaultJsonTypeInfoResolver modifier.
    • The options are rebuilt on each registration, because System.Text.Json freezes the polymorphic type set the first time an options instance is used. A type registered later would otherwise never be seen.
    • Registering the same type with the same discriminator again does nothing.
    • It throws ArgumentException for:
      • an empty discriminator,
      • an abstract type,
      • one of the library's own types,
      • a type already registered under a different discriminator,
      • a discriminator that already names another type, including the built-in ones.
  • AddOrReplace now refuses an unregistered subclass with a CredentialStoreException naming the type, before touching the store. The cache and the store are left unchanged. The check sits in CredentialCache rather than in each store, so every store behaves the same, InMemoryCredentialStore included.
  • README: the recipe now uses the registration call, and the "polymorphic record class" wording is corrected to abstract class.

This is a behaviour change for anyone who relied on the in-memory store accepting unregistered subclasses. That behaviour was the bug, but it is why this is a [minor] and not a patch. One existing test (RegisterMultipleCredentialFactoriesCreatesCredentialsCorrectly) did exactly that, so it now registers its AnotherCredential type first.

Tests

New tests in CustomCredentialTypeTests:

  • a registered subclass round-trips through Serialize/Deserialize and through NativeSecretBuffer.OfCredential/ReadCredential;
  • an unregistered subclass is refused by AddOrReplace with a CredentialStoreException naming it, with the cache and store unchanged;
  • re-registration is idempotent, and conflicts are refused, including the library's own and abstract types;
  • a refused registration leaves nothing behind.

Proof that the tests depend on the fix:

  • With the AddOrReplace guard reverted, AddOrReplaceRefusesAnUnregisteredSubclassAndLeavesTheCacheUnchanged and ADiscriminatorThatAlreadyNamesAnotherTypeIsRefused fail.
  • The round-trip tests depend on the new API and could not pass before it.

Full suite on Linux/net10.0: 80 passed, 5 skipped (native-store tests, since libsecret is not available in this container), 0 failed.


Generated by Claude Code

Credential's polymorphic type set was closed: only the three subclasses
declared on the base could be serialized, so a subclass defined in a
consuming assembly failed on every platform store with a raw
NotSupportedException, while InMemoryCredentialStore, which never
serializes, accepted it and hid the failure in tests.

CredentialSerialization.RegisterDerivedType<T>(discriminator) adds a
subclass to the set through a DefaultJsonTypeInfoResolver modifier,
rebuilding the options so registrations made after first use still take
effect. AddOrReplace now refuses an unregistered subclass with a
CredentialStoreException naming the type, for every store, and leaves the
cache and the store unchanged.

Fixes #171

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UuQscGFrjteUf9Ui3jWdd
…vedType throws

S3928: T is a type argument, not a parameter, so it is not a valid
paramName. Also switch the message assertion to Assert.Contains
(MSTEST0046).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UuQscGFrjteUf9Ui3jWdd
@sonarqubecloud

Copy link
Copy Markdown

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.

Saving a consumer-defined Credential subclass throws raw NotSupportedException on every platform store, but succeeds on InMemoryCredentialStore

2 participants