Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
746 changes: 746 additions & 0 deletions docs/plans/completed/trimming-safe-factory-registration.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/release-notes/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Releases with new features, breaking changes, or bug fixes.

| Version | Date | Highlights |
|---------|------|------------|
| [v0.21.2](v0.21.2.md) | 2026-03-08 | Trimming-safe factory registration for all factory types |
| [v0.21.1](v0.21.1.md) | 2026-03-08 | Fix factories trimmed when first overload has no callers |
| [v0.21.0](v0.21.0.md) | 2026-03-08 | [Remote] requires internal (breaking), Can* auth-driven guards, trimming fixes |
| [v0.20.1](v0.20.1.md) | 2026-03-07 | Fix internal methods excluded from public factory interfaces |
Expand Down Expand Up @@ -44,6 +45,7 @@ Releases with new features, breaking changes, or bug fixes.

## All Releases

- [v0.21.2](v0.21.2.md) - 2026-03-08 - Trimming-safe factory registration for all factory types
- [v0.21.1](v0.21.1.md) - 2026-03-08 - Fix [DynamicDependency] trimming on multi-overload factories
- [v0.21.0](v0.21.0.md) - 2026-03-08 - [Remote] requires internal, trimming fixes
- [v0.20.1](v0.20.1.md) - 2026-03-07 - Fix internal methods on public factory interfaces
Expand Down
2 changes: 1 addition & 1 deletion docs/release-notes/v0.21.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ layout: default
title: "v0.21.1"
description: "Release notes for Neatoo RemoteFactory v0.21.1"
parent: Release Notes
nav_order: 1
nav_order: 2
---

# v0.21.1 - Fix [DynamicDependency] Trimming on Multi-Overload Factories
Expand Down
41 changes: 41 additions & 0 deletions docs/release-notes/v0.21.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
layout: default
title: "v0.21.2"
description: "Release notes for Neatoo RemoteFactory v0.21.2"
parent: Release Notes
nav_order: 1
---

# v0.21.2 - Trimming-Safe Factory Registration

**Release Date:** 2026-03-08
**NuGet:** [Neatoo.RemoteFactory 0.21.2](https://nuget.org/packages/Neatoo.RemoteFactory/0.21.2)

## Overview

Replaces the trim-unsafe `assembly.GetTypes()` factory discovery with a `NeatooFactoryRegistrarAttribute` assembly-level attribute pattern. All three factory types (class, static, interface) are now protected from IL trimming. This supersedes the v0.21.1 `[DynamicDependency]` workaround.

## What's New

None

## Breaking Changes

None

## Bug Fixes

- **Fixed static and interface factories trimmed in Blazor WASM apps.** The v0.21.1 fix protected class factories with `[DynamicDependency]` on interface methods, but static factories (nested delegate types) and interface factories had no protection. `RegisterFactories()` used `assembly.GetTypes()` + `GetMethod()` reflection which the IL trimmer cannot follow. Now the generator emits `[assembly: NeatooFactoryRegistrar(typeof(X))]` for each factory, and `RegisterFactories()` enumerates assembly attributes instead of scanning types. The `[DynamicallyAccessedMembers]` annotation on the attribute creates a dataflow contract the trimmer follows, preserving `FactoryServiceRegistrar` methods.
- **Removed `[DynamicDependency]` from class factory interfaces.** The v0.21.1 workaround is replaced by the assembly attribute approach, which covers all factory types uniformly.

## Commits

- Add `NeatooFactoryRegistrarAttribute` with `[DynamicallyAccessedMembers]` for trim-safe factory discovery
- Emit `[assembly: NeatooFactoryRegistrar(typeof(X))]` in all three renderers
- Update `RegisterFactories()` to use assembly attribute enumeration
- Remove `[DynamicDependency]` from `ClassFactoryRenderer`
- Add static factory to TrimmingTests project
- Add `AssemblyAttributeEmissionTests` for all factory types

**Related:**
- [Trimming-Safe Factory Registration Plan](../plans/completed/trimming-safe-factory-registration.md)
163 changes: 163 additions & 0 deletions docs/todos/completed/trimming-safe-factory-registration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Trimming-Safe Factory Registration

**Status:** Complete
**Priority:** High
**Created:** 2026-03-08
**Last Updated:** 2026-03-08


---

## Problem

Factory classes are being trimmed during IL trimming in Blazor WASM apps. The v0.21.1 fix (emitting `[DynamicDependency]` on every class factory interface method) solved class factories, but static factories are still trimmed because they don't have factory interfaces — they use nested delegate types, and `[DynamicDependency]` is not valid on delegate declarations (CS0592).

The root cause affects all factory types: `RegisterFactories()` in `AddRemoteFactoryServices.cs` discovers `FactoryServiceRegistrar` methods via `assembly.GetTypes()` + `GetMethod()` reflection, which is inherently trim-unsafe.

## Solution

Replace the reflection-based factory discovery with a trimming-safe pattern:

1. Create a `NeatooFactoryRegistrarAttribute` (assembly-level, with `[DynamicallyAccessedMembers]`) in the library
2. Generate `[assembly: NeatooFactoryRegistrar(typeof(X))]` per factory — the `typeof()` is a static reference the trimmer follows
3. Update `RegisterFactories()` to enumerate assembly attributes instead of `assembly.GetTypes()`
4. Apply to all factory types (class, static, interface) — replacing the `[DynamicDependency]` on interface methods from v0.21.1

Key files:
- `src/RemoteFactory/AddRemoteFactoryServices.cs` — reflection-based `RegisterFactories()` (line 123-132)
- `src/Generator/Renderer/ClassFactoryRenderer.cs` — current `[DynamicDependency]` on interface methods
- `src/Generator/Renderer/StaticFactoryRenderer.cs` — no trimming protection currently
- `src/Generator/Renderer/InterfaceFactoryRenderer.cs` — needs checking for same issue

---

## Clarifications

### Q1: Old generator path in FactoryGenerator.cs (Architect)
**Q:** Is `FactoryGenerator.cs` still active alongside the Renderer classes, or have the Renderers fully replaced it?
**A:** Both paths are active. `FactoryGenerator.cs` has inline `FactoryServiceRegistrar` generation at lines 264 (static factories) and 822 (class/interface factories) using `context.AddSource()`. The Renderer classes also generate via `spc.AddSource()`. **User decision: Fully replace the old inline generation path. The assembly attribute should only be emitted from the Renderer classes, and the old FactoryGenerator.cs inline path should be removed or migrated.**

### Q2: Attribute design — Option A vs B (Architect)
**Q:** Should the attribute just hold the type reference (Option A, simpler, `RegisterFactories()` still uses `GetMethod()`), or fully eliminate reflection (Option B)?
**A:** Option A is sufficient. The dotnet-runtime-debugger agent confirmed that `[DynamicallyAccessedMembers(PublicMethods | NonPublicMethods)]` on both the constructor parameter AND the property creates a dataflow contract the trimmer follows. The downstream `GetMethod()` call becomes trim-safe with no warnings. The annotation must appear on both sides (constructor param and property) for end-to-end trimmer tracking.

### Q3: `using System.Diagnostics.CodeAnalysis;` cleanup (Architect)
**Q:** Should we remove the `using System.Diagnostics.CodeAnalysis;` from ClassFactoryRenderer when removing `[DynamicDependency]`?
**A:** Yes, do the cleanup. Remove it since it was only added for `[DynamicDependency]`.

### Q4: InterfaceFactoryRenderer vulnerability (Architect)
**Q:** Does InterfaceFactoryRenderer have the same trimming problem as static factories?
**A:** Yes. The dotnet-runtime-debugger agent confirmed interface factories ARE vulnerable. The trimmer preserves the type and its interface-contract methods via DI, but `FactoryServiceRegistrar` is `internal static`, satisfies no interface, and has no static callers — so it gets trimmed. All three factory types need the fix.

**Architect confirmed: Ready to proceed.**

---

## Requirements Review

**Reviewer:** business-requirements-reviewer
**Reviewed:** 2026-03-08
**Verdict:** APPROVED

### Relevant Requirements Found

1. **Trimming architecture (feature switch guards).** `docs/trimming.md` and `src/Design/CLAUDE-DESIGN.md` (Critical Rule 2, "Factory Method Visibility Controls Guard Emission and Trimming") document that the generator emits `if (NeatooRuntime.IsServerRuntime)` guards and that these are the mechanism for IL trimming. The proposed change does not alter guard emission — it only changes how `RegisterFactories()` discovers the `FactoryServiceRegistrar` methods. The guard behavior is orthogonal and unaffected.

2. **Three factory types must all be covered.** `src/Design/CLAUDE-DESIGN.md` (Decision Table, Quick Reference) documents Class, Interface, and Static factory patterns. The todo correctly identifies that all three need the fix. Confirmed in the Renderer code:
- `src/Generator/Renderer/ClassFactoryRenderer.cs` — currently emits `[DynamicDependency]` on interface methods (lines 126-130)
- `src/Generator/Renderer/StaticFactoryRenderer.cs` — has no trimming protection (confirmed: no `DynamicDependency` anywhere in file)
- `src/Generator/Renderer/InterfaceFactoryRenderer.cs` — has no trimming protection (confirmed: no `DynamicDependency` anywhere in file)

3. **`FactoryServiceRegistrar` is generated with consistent signatures.** All three renderers generate an `internal static void FactoryServiceRegistrar(IServiceCollection services, NeatooFactory remoteLocal)` method. The `RegisterFactories()` method in `src/RemoteFactory/AddRemoteFactoryServices.cs` (line 127) discovers these via `assembly.GetTypes()` + `GetMethod("FactoryServiceRegistrar", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)`. The proposed change replaces only the discovery mechanism, not the generated method signature or behavior.

4. **Existing assembly-level attribute pattern.** `src/RemoteFactory/FactoryAttributes.cs` (line 129) already has `FactoryHintNameLengthAttribute` with `[AttributeUsage(AttributeTargets.Assembly)]`. The proposed `NeatooFactoryRegistrarAttribute` follows this established pattern. No contradiction.

5. **Auth type auto-registration for trimming.** `src/Design/CLAUDE-DESIGN.md` ("Auth Type Auto-Registration for Trimming") documents that the generator emits explicit `services.TryAddTransient<IFooAuth, FooAuth>()` in `FactoryServiceRegistrar`. This pattern is inside the `FactoryServiceRegistrar` method body, so it is preserved as long as the `FactoryServiceRegistrar` method itself survives trimming — which is exactly what the proposed fix ensures.

6. **`RegisterMatchingName` uses `assembly.GetTypes()` too.** `src/RemoteFactory/AddRemoteFactoryServices.cs` (lines 150-154) has a separate `RegisterMatchingName` method that also uses `assembly.GetTypes()`. The todo's scope correctly limits the fix to `RegisterFactories()` only. `RegisterMatchingName` is user-called for convention registration and is a separate concern documented in `docs/trimming.md` (lines 208-215).

7. **Server setup via `AddNeatooAspNetCore`.** `src/RemoteFactory.AspNetCore/ServiceCollectionExtensions.cs` (line 30) delegates to `services.AddNeatooRemoteFactory(NeatooFactory.Server, ...)`, which calls `RegisterFactories()`. The proposed change to `RegisterFactories()` will automatically apply to both client (`AddNeatooRemoteFactory`) and server (`AddNeatooAspNetCore`) registration paths. No additional changes needed.

8. **Design project test infrastructure.** `src/Design/Design.Tests/TestInfrastructure/DesignClientServerContainers.cs` (line 230) calls `services.AddNeatooRemoteFactory(mode, serializationOptions, typeof(ExampleClassFactory).Assembly)`, which internally calls `RegisterFactories()`. The proposed change will be transparent to this call chain.

9. **TrimmingTests project.** `src/Tests/RemoteFactory.TrimmingTests/Program.cs` calls `TrimTestEntityFactory.FactoryServiceRegistrar(services, NeatooFactory.Remote)` directly (line 14), bypassing `RegisterFactories()`. This direct call pattern will continue to work since the `FactoryServiceRegistrar` method signature is unchanged. However, this test should be updated or extended to verify that the assembly-attribute-based discovery also works.

10. **Multi-targeting requirement.** `src/Directory.Build.props` targets net9.0 and net10.0. The proposed `NeatooFactoryRegistrarAttribute` and `[DynamicallyAccessedMembers]` are available in both target frameworks. `[FeatureSwitchDefinition]` (used by the existing trimming infrastructure) was introduced in .NET 9, confirming both TFMs are compatible.

11. **No Design Debt conflict.** The Design Debt table in `src/Design/CLAUDE-DESIGN.md` lists five deferred topics (private setter support, OR logic for AspAuthorize, automatic Remote detection, collection factory injection, IEnumerable serialization). None relate to factory registration or trimming discovery. No conflict.

### Gaps

1. **No existing unit tests for `RegisterFactories()`.** There are no tests in `src/Tests/RemoteFactory.UnitTests/` that verify the `RegisterFactories()` method's behavior. The architect should include tests that verify assembly-attribute-based discovery works for all three factory types (class, static, interface).

2. **No integration test for static factory trimming.** The `src/Tests/RemoteFactory.TrimmingTests/` project only tests a class factory (`TrimTestEntity`). The architect should consider adding a static factory to this project to verify the fix addresses the original bug (static factories being trimmed).

3. **Generator pipeline dual-path clarification.** The todo's Q1 states "Both paths are active" (FactoryGenerator.cs inline and Renderer classes). However, inspection of `FactoryGenerator.cs` shows that `GenerateExecute` (line 84) and the class/interface generation (line 822) are defined but **not called from `Initialize()`**. The `Initialize()` method (lines 19-81) only wires up the Renderer path via `FactoryModelBuilder.Build` + `FactoryRenderer.Render`. The old inline methods appear to be dead code. The architect should verify this before attempting to "remove or migrate" the old path — it may already be dead.

4. **Published docs update needed.** `docs/trimming.md` (line 215) recommends `[DynamicDependency]` as a user-facing option for preserving types. If the library moves to assembly-level attributes internally, the docs should still accurately describe the available user-facing options. The internal mechanism change should not require docs updates, but if the `[DynamicDependency]` on factory interface methods is removed, verify this doesn't break any documented guidance.

### Contradictions

None found. The proposed solution is consistent with all documented patterns, rules, and design decisions.

### Recommendations for Architect

1. **Verify the old FactoryGenerator.cs inline path is dead code.** The `GenerateExecute` method (line 84) and class/interface generation (line 822) in `FactoryGenerator.cs` are not called from `Initialize()`. If they are indeed dead, the Q1 clarification to "remove the old inline path" is simply dead code cleanup, not a migration. Confirm before spending effort on migration.

2. **Place `NeatooFactoryRegistrarAttribute` in `src/RemoteFactory/FactoryAttributes.cs`.** This file already contains all factory-related attributes including the existing assembly-level `FactoryHintNameLengthAttribute`. Follow the established pattern.

3. **Ensure `[DynamicallyAccessedMembers]` is on both constructor parameter AND property.** The Q2 clarification specifies this dual annotation requirement. This is critical for end-to-end trimmer tracking.

4. **Extend TrimmingTests with a static factory.** The original bug specifically affects static factories. The trimming test project should include a static factory to prevent regression.

5. **The assembly attribute emission should be in the Renderer classes, not in `FactoryModelBuilder`.** Keep the Renderers as the sole code-generation path, consistent with the current architecture where `FactoryModelBuilder` builds models and Renderers produce output.

6. **Consider the `TrimTestEntity` direct call pattern.** `src/Tests/RemoteFactory.TrimmingTests/Program.cs` line 14 calls `FactoryServiceRegistrar` directly. This bypasses `RegisterFactories()` entirely. If the goal is to test the full registration pipeline, the test should be updated to call `AddNeatooRemoteFactory()` instead.

---

## Plans

- [Trimming-Safe Factory Registration Plan](../plans/completed/trimming-safe-factory-registration.md)

---

## Tasks

- [x] Architect comprehension check (Step 2)
- [x] Business requirements review (Step 3)
- [x] Architect plan creation & design (Step 4)
- [x] Developer review (Step 5)
- [x] Implementation (Step 7)
- [x] Verification (Step 8)
- [x] Documentation (Step 9)
- [x] Completion (Step 10)

---

## Progress Log

### 2026-03-08
- Created todo from bug report: static factories (e.g., TherapyCommands) trimmed in Blazor WASM apps
- v0.21.1 fix addressed class factories but not static factories
- dotnet-runtime-debugger agent confirmed `[DynamicDependency]` is not valid on delegates (CS0592)
- Agent recommended assembly-level attribute with `[DynamicallyAccessedMembers]` as the proper fix

---

## Completion Verification

Before marking this todo as Complete, verify:

- [x] All builds pass
- [x] All tests pass

**Verification results:**
- Build: Pass (Release mode, 0 errors)
- Tests: Pass (2,024 tests: 490×2 unit + 481×2 integration + 41×2 design, 0 failures)

---

## Results / Conclusions

Replaced the trim-unsafe `assembly.GetTypes()` factory discovery with a `NeatooFactoryRegistrarAttribute` assembly-level attribute pattern. All three factory types (class, static, interface) now emit `[assembly: NeatooFactoryRegistrar(typeof(X))]`, and `RegisterFactories()` enumerates these attributes instead of scanning types. The `[DynamicDependency]` workaround from v0.21.1 was removed. This fixes static factories being trimmed in Blazor WASM apps and makes the entire factory registration pipeline trim-safe.
8 changes: 8 additions & 0 deletions docs/trimming.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,14 @@ If server-only type names still appear in the output, check that:
2. The `RuntimeHostConfigurationOption` has `Trim="true"`
3. You're inspecting the `publish/` output, not the `build/` output

## Factory Type Preservation

All factory types — class, static, and interface — are automatically preserved from trimming. The source generator emits `[assembly: NeatooFactoryRegistrar(typeof(X))]` for every factory, creating a static reference that the IL trimmer follows. The `NeatooFactoryRegistrarAttribute` carries `[DynamicallyAccessedMembers]` annotations that instruct the trimmer to preserve all methods on the referenced type, including the internal `FactoryServiceRegistrar` method used for DI registration.

At startup, `AddNeatooRemoteFactory()` and `AddNeatooAspNetCore()` discover factory types by enumerating these assembly attributes rather than scanning all types via reflection. This means factory registration is fully trimming-safe — no factory types are lost during IL trimming, regardless of whether they are class factories, static factories, or interface factories.

You do not need to take any action to preserve your factory types. This is handled automatically by the generator.

## Authorization Types and Trimming

RemoteFactory's generator automatically emits explicit DI registrations for `[AuthorizeFactory<T>]` types in the generated `FactoryServiceRegistrar`. This creates static references that the IL trimmer preserves — your auth classes survive trimming without any additional configuration.
Expand Down
Loading