Skip to content

Add a public exception hierarchy (C2) - #28

Merged
guy-lud merged 1 commit into
masterfrom
refactor/c2-exception-hierarchy
Jul 13, 2026
Merged

Add a public exception hierarchy (C2)#28
guy-lud merged 1 commit into
masterfrom
refactor/c2-exception-hierarchy

Conversation

@guy-lud

@guy-lud guy-lud commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

C2 — public exception hierarchy

The library's 10 exceptions derived straight from Exception with no common base (consumers couldn't catch (SimpleSettingsException)); 4 were internal yet escaped the public build path (uncatchable by type); context lived only in message strings; and the reachable not-an-interface guard threw an untyped InvalidOperationException.

Changes

  • public abstract class SimpleSettingsException : Exception — protected (message) + (message, inner) ctors (no parameterless/[Serializable] ctor; BinaryFormatter is obsolete on net8/net10). All 10 exceptions reparented → catch (SimpleSettingsException) catches the family.
  • Promote 4 build-path escapees to public: SettingsPropertyValueException, SettingsPropertyNullException, TypeGenerationException, SettingsPropertyExtractionException.
  • Flatten 3 mis-namespaced types to the root: SettingsExtractionException (was .Core), TypeGenerationException + SettingsPropertyExtractionException (were .Core.Reflection).
  • Leak-safe structured properties so consumers stop parsing messages: SettingsBindingException.{BinderType,Section,Key}, SettingsPropertyValueException.{SettingsType,PropertyName,TargetType,ConversionErrorType}, and SettingsType/OptionType/ArgumentName on the rest.
  • Typed SettingsTypeNotInterfaceException replaces the InvalidOperationException(TypeIsNotInterface) throws. The two documented-unreachable "No converter found" InvalidOperationExceptions are left as internal invariant guards.

S1 redaction is now structural (not conventional)

  • SettingsPropertyValueException's ctor takes the failure's Type (not the Exception), so a value-bearing object can't cross its boundary — and InnerException stays null (asserted by test).
  • SettingsBindingException stores primitives instead of retaining the BindingContext (which holds the bound value).

Breaking

Pre-stable, so mostly free. The one real runtime break: a non-interface type now throws SettingsTypeNotInterfaceException instead of InvalidOperationException. Reparenting and namespace moves are source-compatible for catch of the concrete types or Exception; internal→public is additive. Release-note the not-interface change.

Tests (+6, suite 82 net10)

SimpleSettings/ExceptionHierarchyTests.cs: base is public+abstract; a reflection invariant that every library exception derives from the base (fails if a future exception isn't reparented); the 4 promotions are public (via reflection — InternalsVisibleTo masks it from a plain reference); not-interface→typed+catchable-as-base; conversion→structured metadata + InnerException == null; binder-throws→context. Existing not-interface test updated to the new type.

Review

  • Plan reviewed by security-auditor (ENDORSE-WITH-CHANGES — no leak reopened; adopted the Type-not-Exception hardening) and dotnet-architect (ENDORSE-WITH-CHANGES — namespace flatten, Section/ConversionErrorType naming, reflection-based accessibility tests).
  • Code reviewed via /code-review (high) + Roslyn detect_antipatterns (0). Build clean, both TFMs.

Follows #27 (S1).

The library's exceptions derived straight from Exception with no common base,
so consumers couldn't catch (SimpleSettingsException); four were internal yet
escaped the public build path (uncatchable by type); context lived only in
message strings; and the reachable not-an-interface guard threw an untyped
InvalidOperationException.

- New public abstract SimpleSettingsException : Exception (protected (message)
  and (message, inner) ctors; no parameterless or [Serializable] ctor -
  BinaryFormatter is obsolete on the net8/net10 targets). All 10 library
  exceptions now derive from it.
- Promote the four build-path escapees to public: SettingsPropertyValueException,
  SettingsPropertyNullException, TypeGenerationException,
  SettingsPropertyExtractionException.
- Flatten three mis-namespaced types to the root namespace so the public
  surface is coherent: SettingsExtractionException (was .Core),
  TypeGenerationException and SettingsPropertyExtractionException
  (were .Core.Reflection).
- Expose leak-safe structured properties instead of forcing consumers to parse
  messages: SettingsBindingException.{BinderType,Section,Key};
  SettingsPropertyValueException.{SettingsType,PropertyName,TargetType,
  ConversionErrorType}; SettingsType / OptionType / ArgumentName on the rest.
- Replace the untyped InvalidOperationException(TypeIsNotInterface) throws with
  a typed SettingsTypeNotInterfaceException (the one runtime-behavior break -
  see release notes). The two documented-unreachable "No converter found"
  InvalidOperationExceptions are left as internal invariant guards.

S1 secret-redaction is now structural, not conventional:
SettingsPropertyValueException's ctor takes the failure's Type (not the
Exception), so a value-bearing object cannot cross its boundary; and
SettingsBindingException stores primitives instead of retaining the
BindingContext, which holds the bound value.

Reviewed: plan by security-auditor + dotnet-architect (both
ENDORSE-WITH-CHANGES; adopted the Type-not-Exception hardening, namespace
flattening, Section/ConversionErrorType naming, and reflection-based
accessibility tests since InternalsVisibleTo masks it); code by /code-review
plus Roslyn detect_antipatterns (0).

Tests: SimpleSettings/ExceptionHierarchyTests.cs (+6) - base is public+abstract,
a reflection invariant that every library exception derives from the base, the
four promotions are public, non-interface -> typed + catchable as base,
conversion failure -> structured metadata + InnerException == null, and
binder-throws -> binder context. The existing not-interface test updated to the
new type. Suite 82 net10 (was 76).

Also refreshes FIX-PLAN.md (C2 marked done, stale "bare Exception" line fixed)
and SESSION-HANDOFF.md.
@guy-lud
guy-lud merged commit 13b78dd into master Jul 13, 2026
2 checks passed
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