fix(billing): tolerate ReflectionTypeLoadException during init #19

Merged
notazof merged 1 commit from fix/billing-init-reflection into release/1.0.6 2026-08-16 16:43:10 +01:00
Owner

Backport of 7f84d4d9 to release/1.0.6.

ConfigureBillingService() walks AppDomain.CurrentDomain.GetAssemblies()
and calls Assembly.GetTypes() on each. In CI under xunit v3, one of
those assemblies has a type that fails to resolve, so GetTypes() throws
ReflectionTypeLoadException. The exception was unhandled, so after
Clearing the collections at the top of the method, the exception
escaped before the RegisterBilling<> calls could re-populate
BillingService.Billing.

Symptom in CI: the test
Yavsc.BillingServiceTests.ConfigureBillingService_CanBeCalledTwiceWithoutThrowing
fails with 'Assert.Equal() Failure: Expected 3, Actual 0' because the
second ConfigureBillingService() call replays the exact same failure
on the same loaded assembly, leaving BillingService.Billing empty.

Fix: catch ReflectionTypeLoadException and use the partial Types()
list (the successfully-resolved subset); also catch broader assembly
load failures (FileNotFoundException, etc.) with continue. The fix
is best-effort: the user-settings populating loop and the final
RegisterBilling<> calls are both after the reflective phase and run
independently of what was (or wasn't) successfully loaded.

Backport of 7f84d4d9 to release/1.0.6. ConfigureBillingService() walks AppDomain.CurrentDomain.GetAssemblies() and calls Assembly.GetTypes() on each. In CI under xunit v3, one of those assemblies has a type that fails to resolve, so GetTypes() throws ReflectionTypeLoadException. The exception was unhandled, so after Clearing the collections at the top of the method, the exception escaped before the RegisterBilling<> calls could re-populate BillingService.Billing. Symptom in CI: the test Yavsc.BillingServiceTests.ConfigureBillingService_CanBeCalledTwiceWithoutThrowing fails with 'Assert.Equal() Failure: Expected 3, Actual 0' because the second ConfigureBillingService() call replays the exact same failure on the same loaded assembly, leaving BillingService.Billing empty. Fix: catch ReflectionTypeLoadException and use the partial Types() list (the successfully-resolved subset); also catch broader assembly load failures (FileNotFoundException, etc.) with continue. The fix is best-effort: the user-settings populating loop and the final RegisterBilling<> calls are both after the reflective phase and run independently of what was (or wasn't) successfully loaded.
ConfigureBillingService() walks AppDomain.CurrentDomain.GetAssemblies()
and calls Assembly.GetTypes() on each. If any of the loaded assemblies
has a type that fails to resolve (a flaky dependency, an AddOn with a
broken reference, a test dependency that's been rewritten after compile),
GetTypes() throws ReflectionTypeLoadException (or, less commonly,
FileNotFoundException / TypeLoadException for the assembly itself).

In CI on the forgejo-runner (and especially in test discovery under
xunit v3), one such assembly is loaded somewhere between test runs and
silently throws. The exception is not handled, so:

  1. Collections are Cleared at the top of ConfigureBillingService().
  2. The reflection loop throws before reaching the
     RegisterBilling<HairCutQuery/HairMultiCutQuery/RdvQuery> calls.
  3. BillingService.Billing ends up empty (Count = 0).
  4. The second ConfigureBillingService() call sees the same assembly
     loaded (xunit v3 keeps the AppDomain warm for the whole suite),
     throws identically, and the test
     Yavsc.BillingServiceTests.ConfigureBillingService_CanBeCalledTwiceWithoutThrowing
     fails with 'Assert.Equal() Failure: Expected 3, Actual 0'.

Fix: catch ReflectionTypeLoadException and use the partial
.Types() list (the successfully-resolved subset), and use a
broader catch (with continue) for any other assembly-level
load failure. The lost user-settings types are not material;
they are derived from ApplicationDbContext in a separate loop
right after, and the RegisterBilling<>() calls that populate
BillingService.Billing run last, after both reflective phases
have completed best-effort.

The test still passes locally because the local test environment
loads a clean set of assemblies; only the CI runner (with its
extra test-time tooling) hits this path.
notazof left a comment

MERGE

MERGE
notazof merged commit f74577f80a into release/1.0.6 2026-08-16 16:43:10 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
notazof/yavsc!19
No description provided.