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
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 |
|---------|------|------------|
| [v1.7.0](v1.7.0.md) | 2026-08-14 | **Fix**: `[Remote]` method bodies no longer ship to trimmed Blazor WASM clients. Two mechanisms: the registrar attribute's `[DynamicallyAccessedMembers]` named the consumer's own class (`[Execute]` static factories and `[FactoryEventHandler<T>]` classes, since v0.21.2) — now a single-method forwarding holder; and the `IsServerRuntime` guard did not fold inside an `async` state machine (**every class factory with an async operation**) — now a non-async wrapper forwarding to a private core. **Feat**: positional-record DTOs, DTOs carried as `[Factory]` entity properties, and `FactoryEventBase` descendants reachable only from a subscription lambda are now preserved (DAM does **not** flow to derived types under ILLink). **Perf**: incremental caching restored on the relay-handler branch. **Behaviour change**: the server-only guard throws synchronously rather than as a faulted `Task`. Interface factories are explicitly carved out — body removal there is *not established*. |
| [v1.6.1](v1.6.1.md) | 2026-05-24 | **Fix**: `[Factory]` classes whose only operation is `[Execute]` and whose ctor requires DI services were not registered as transients by the generator. Client-side deserialization could not resolve such types via `GetRequiredService`, so the ctor never ran and ctor-injected services stayed null. `requiresEntityRegistration` now also fires when `typeInfo.RequiresServiceInstantiation` is true. |
| [v1.6.0](v1.6.0.md) | 2026-04-25 | **Feat**: new generator diagnostic **NF0106** — factory-operation attribute (`[Create]`/`[Fetch]`/`[Insert]`/`[Update]`/`[Delete]`/`[Execute]`) on a `[Factory]` interface method. Replaces cryptic CS0111/CS0738 duplicate-codegen errors with one clear diagnostic. Design project + skill gain interface-factory `[AuthorizeFactory<T>]` pedagogy (`AuthorizedRepository.cs`, 10 tests, snippet-backed skill section) demonstrating the shipped Execute/Read + parameter-matching model. No breaking changes. |
| [v1.5.0](v1.5.0.md) | 2026-04-14 | **Breaking**: entire `[Event]` method attribute API deleted — `EventAttribute`, `FactoryOperation.Event`, `AuthorizeFactoryOperation.Event`, `IEventTracker`, `IEventScopeInitializer`, `EventTrackerHostedService`, `AddRemoteFactoryEventScopeInitializer`, diagnostics NF0401-NF0404, log events 9001-9009. Consumers using `[Event]` for fire-and-forget work migrate to manual `Task.Run` + `IServiceScopeFactory.CreateScope()`. `[FactoryEventHandler<T>]`, `IFactoryEvents`, `IFactoryEventRelay`, and `ICorrelationContext` are **unchanged**. |
Expand Down Expand Up @@ -65,6 +66,7 @@ Releases with new features, breaking changes, or bug fixes.

## All Releases

- [v1.7.0](v1.7.0.md) - 2026-08-14 - **Fix**: `[Remote]` bodies stripped from trimmed clients for `[Execute]` static factories, `[FactoryEventHandler<T>]` classes, and class factories with `async` operations (generated single-method registrar holders + non-async guard wrapper). **Feat**: positional-record DTOs, entity-property DTOs, and subscribe-only event records now preserved under IL trimming. **Perf**: relay-handler incremental caching. **Behaviour change**: server-only guard throws synchronously instead of returning a faulted `Task`. Interface-factory body removal remains **unestablished** and is not claimed.
- [v1.6.1](v1.6.1.md) - 2026-05-24 - **Fix**: `[Execute]`-only `[Factory]` classes with DI-requiring ctors are now auto-registered as transients so client-side deserialization can invoke the ctor via DI (was missing in `FactoryServiceRegistrar`).
- [v1.6.0](v1.6.0.md) - 2026-04-25 - **Feat**: NF0106 generator diagnostic for factory-operation attributes on interface factory methods (replaces cryptic CS0111/CS0738 duplicate-codegen errors). Interface-factory `[AuthorizeFactory<T>]` pedagogy added to Design project (`AuthorizedRepository.cs`, 10 tests) and skill (snippet-backed section).
- [v1.5.0](v1.5.0.md) - 2026-04-14 - **Breaking**: `[Event]` method attribute API fully deleted (attribute, `FactoryOperation.Event`, `AuthorizeFactoryOperation.Event`, `IEventTracker`, `IEventScopeInitializer`, `EventTrackerHostedService`, `AddRemoteFactoryEventScopeInitializer`, NF0401-NF0404, log events 9001-9009). Migrate fire-and-forget to manual `Task.Run` + `IServiceScopeFactory.CreateScope()`. `[FactoryEventHandler<T>]` / `IFactoryEvents` / `IFactoryEventRelay` / `ICorrelationContext` unchanged.
Expand Down
2 changes: 1 addition & 1 deletion docs/release-notes/v1.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ layout: default
title: "v1.0.0"
description: "Release notes for Neatoo RemoteFactory v1.0.0"
parent: Release Notes
nav_order: 5
nav_order: 9
---

# v1.0.0 — Production Release
Expand Down
2 changes: 1 addition & 1 deletion docs/release-notes/v1.1.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ layout: default
title: "v1.1.0"
description: "Release notes for Neatoo RemoteFactory v1.1.0"
parent: Release Notes
nav_order: 4
nav_order: 8
---

# v1.1.0 — Transactional Factory Events
Expand Down
2 changes: 1 addition & 1 deletion docs/release-notes/v1.2.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ layout: default
title: "v1.2.0"
description: "Release notes for Neatoo RemoteFactory v1.2.0"
parent: Release Notes
nav_order: 3
nav_order: 7
---

# v1.2.0 — Trimming-Safe Factory Events
Expand Down
2 changes: 1 addition & 1 deletion docs/release-notes/v1.3.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ layout: default
title: "v1.3.0"
description: "Release notes for Neatoo RemoteFactory v1.3.0"
parent: Release Notes
nav_order: 2
nav_order: 6
---

# v1.3.0 — `CanSave(target)` Overload + Auth Method Triplication Fix
Expand Down
2 changes: 1 addition & 1 deletion docs/release-notes/v1.4.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ layout: default
title: "v1.4.0"
description: "Release notes for Neatoo RemoteFactory v1.4.0"
parent: Release Notes
nav_order: 2
nav_order: 5
---

# v1.4.0 — Factory Event Relay Redesign + Post-Return Ordering Fix
Expand Down
2 changes: 1 addition & 1 deletion docs/release-notes/v1.5.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ layout: default
title: "v1.5.0"
description: "Release notes for Neatoo RemoteFactory v1.5.0"
parent: Release Notes
nav_order: 2
nav_order: 4
---

# v1.5.0 — Delete `[Event]` Method Attribute API
Expand Down
2 changes: 1 addition & 1 deletion docs/release-notes/v1.6.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ layout: default
title: "v1.6.0"
description: "Release notes for Neatoo RemoteFactory v1.6.0"
parent: Release Notes
nav_order: 2
nav_order: 3
---

# v1.6.0 — NF0106 Diagnostic + Interface-Factory `[AuthorizeFactory<T>]` Pedagogy
Expand Down
2 changes: 1 addition & 1 deletion docs/release-notes/v1.6.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ layout: default
title: "v1.6.1"
description: "Release notes for Neatoo RemoteFactory v1.6.1"
parent: Release Notes
nav_order: 1
nav_order: 2
---

# v1.6.1 — `[Execute]`-only Factory Registration Fix
Expand Down
163 changes: 163 additions & 0 deletions docs/release-notes/v1.7.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
---
layout: default
title: "v1.7.0"
description: "Release notes for Neatoo RemoteFactory v1.7.0"
parent: Release Notes
nav_order: 1
---

# v1.7.0 — Trimming Preservation and Over-Preservation

**Release Date:** 2026-08-14
**NuGet:** [Neatoo.RemoteFactory 1.7.0](https://nuget.org/packages/Neatoo.RemoteFactory/1.7.0)
**Breaking changes:** None to the public API. One **behaviour change** in an error path — see below.

---

## Overview

This release closes both halves of RemoteFactory's IL-trimming story.

**Under-preservation** — DTO shapes that the generator failed to preserve, so they were trimmed away and failed to deserialize on a Blazor WASM client. Positional records in factory signatures, plain DTOs reachable only as `[Factory]` entity properties, and `FactoryEventBase` descendants known only through a subscription lambda.

**Over-preservation** — the inverse defect, and the more serious one. `[Remote]` method bodies were shipping to trimmed clients, decompilable, for **most** factory shapes. Every `[Execute]` static factory and `[FactoryEventHandler<T>]` class since v0.21.2; and every class factory with an `async` operation — which is most aggregate roots — since the beginning. The headline promise that server-side SQL, business rules, and IP do not reach the browser was true for synchronous class-factory operations and false almost everywhere else.

Both are fixed and both are now **measured** in a publish-trimmed artifact under a CI gate, rather than inferred from the shape of the generated code. The gate carries positive controls, so a missing or unreadable artifact fails loudly instead of passing silently.

One shape is **not** covered — see [What this release does not claim](#what-this-release-does-not-claim).

---

## What's New

### Positional-record DTOs are preserved in factory signatures

A record with only a parameterized constructor — the ordinary positional-record shape — appearing as a factory method's return type, as a parameter, or nested as a property of another discovered DTO, now survives publish-trimming with no consumer `LinkerConfig.xml` entry.

The walker required a parameterless constructor before descending, so positional records were skipped with no preservation at all. `DtoConstructorRegistry.PreserveType<T>()` already existed for exactly this shape but was emitted nowhere; the generator now emits it.

### DTOs carried as `[Factory]` entity properties are discovered

The walker stopped at `[Factory]`-annotated roots without descending into their properties, so a DTO reachable only as an entity property — for example a record carried by an `[Execute]`-opened aggregate — was trimmed on the client. The generator now walks the entity's property graph.

### Event records are preserved without a handler on the client

`FactoryEventBase` carries `[DynamicallyAccessedMembers]` with `Inherited = true`, and that was assumed to preserve descendants. It does not: **DAM does not flow to derived types under ILLink.** A `FactoryEventBase`-derived record whose only client-side reference was a subscription lambda was trimmed and failed to deserialize.

The generator now emits a per-assembly **event-preservation registrar** that roots every concrete, accessible descendant and its nested property graph. This was verified with a red trimmed repro before the fix, not assumed.

### Incremental-generator caching restored on the relay-handler branch (`perf`)

`RelayHandlerModel` carried non-equatable collection fields, so its transform never compared equal and the relay-handler branch re-ran on every keystroke. Fixed, with a regression guard that pins the cached pipeline steps.

---

## Bug Fixes

### `[Remote]` bodies no longer ship to trimmed clients

Two distinct mechanisms were leaking method bodies. Both needed fixing; neither fix alone is sufficient.

**1. The registrar attribute named the consumer's own class.**

`[assembly: NeatooFactoryRegistrar(typeof(T))]` carries `[DynamicallyAccessedMembers(PublicMethods | NonPublicMethods)]`, which preserves **every method on `T`, bodies included**. Static factories and `[FactoryEventHandler<T>]` classes have no separate generated type — the generator re-opens the user's own partial class to host `FactoryServiceRegistrar` — so the attribute named the user's class and DAM retained all of it.

The generator now emits a single-method forwarding holder and points the attribute at that:

```csharp
internal static class NeatooFactoryRegistrar_MyCommands
{
internal static void FactoryServiceRegistrar(IServiceCollection services, NeatooFactory remoteLocal)
=> MyCommands.FactoryServiceRegistrar(services, remoteLocal);
}
```

Naming a *generated* type is necessary but **not sufficient** — what makes a holder safe is that it has exactly one method. Class factories therefore emit one too (`NeatooClassFactoryRegistrar_{ClassName}`) rather than naming `{X}Factory`, which hosts every `Local*` method for that factory.

**2. The `IsServerRuntime` guard did not fold inside an `async` method.**

In an `async` method the compiler lowers the whole body — guard included — into the state machine's `MoveNext`, inside the builder's protected region. ILLink folds the feature switch there but does not eliminate the unreachable remainder, so the body survived. A synchronous method puts the guard ahead of any protected region, which is why sync operations always trimmed correctly and `async` ones never did.

The generator now emits the guard in a **non-async wrapper** that forwards to a private `async` core:

```csharp
public Task<Person> LocalFetch(int id, CancellationToken cancellationToken = default)
{
if (!NeatooRuntime.IsServerRuntime)
throw new InvalidOperationException("Server-only method called in non-server runtime.");
return LocalFetchCore(id, cancellationToken);
}

private async Task<Person> LocalFetchCore(int id, CancellationToken cancellationToken = default) { /* ... */ }
```

Applied at all five guarded emission sites: reads, writes, `LocalSave`, `Can*`, and class-level `[Execute]`.

**Affected shapes, now measured absent on a trimmed client:** `[Execute]` static factories, `[FactoryEventHandler<T>]` classes, class factories with any `async` read or write operation, `LocalSave` routing, and class-level `[Execute]`.

---

## Behaviour Changes

**The server-only guard now throws synchronously.**

Previously the guard lived inside the `async` body, so calling a server-only method on a non-server runtime produced a **faulted `Task`**. It now throws from the non-async wrapper, so it can reach the call site **synchronously**.

- **Awaiting callers are unaffected.** `await factory.FetchAsync(id)` behaves identically, and `try { await … } catch (InvalidOperationException) { }` still catches it.
- Whether it reaches *your* call site synchronously depends on the entry point: non-async entry points (most reads) propagate it; `async` ones (`Save` on an authorized factory) capture it back into a faulted `Task`.
- **Only the server-only guard moved.** Authorization failures, target casts, and DI resolution failures still surface as faulted tasks in every case.

The exception type and message are unchanged: `InvalidOperationException("Server-only method called in non-server runtime.")`.

This fires only when a server-only method is invoked on a client runtime, which is already a misconfiguration — the boundary error you would want surfaced either way.

---

## What this release does not claim

**Interface factories are carved out.** `InterfaceFactoryRenderer` still emits the guard inline in the `async` body and still points its assembly attribute at `{ImplName}Factory` — it has received **neither** fix above. No leak is observed, but no proof is available either: the leg reaches its implementation through interfaces, so a client-side trimmed harness reads "absent" whether or not the body survives.

Body removal on this leg is therefore **not established**. It is not asserted in the docs, the skill, the Design projects, or the generator's own comments. If you carry sensitive server-only logic in a `[Factory]` **interface** implementation, do not rely on it being trimmed from a WASM client in this release.

---

## Migration Guide

**No action required for the vast majority of consumers.** No public API changed; no attribute, type, or method signature was added, removed, or altered. Upgrading is a package-version bump.

**If you consume RemoteFactory from a trimmed Blazor WASM client**, you may now be able to delete `LinkerConfig.xml` entries for types that flow through RemoteFactory — positional-record DTOs, DTOs carried as `[Factory]` entity properties, and `FactoryEventBase`-derived event records. Delete them, publish `Release`, and smoke-test; DTOs that reach the client through your own HTTP/JSON paths still need their entries.

**If you split a factory call from its `await`**, review those sites. This pattern now surfaces the guard earlier:

```csharp
// The exception now escapes at the call, not at the await.
var task = factory.FetchAsync(id);
try { await task; } catch (InvalidOperationException) { /* no longer reached */ }

// Same for un-awaited tasks handed to a combinator:
await Task.WhenAll(factory.FetchAsync(a), factory.FetchAsync(b)); // throws before WhenAll
```

Wrap the call site rather than the await:

```csharp
try { await factory.FetchAsync(id); } catch (InvalidOperationException) { /* still correct */ }
```

Again, this only applies when the method is server-only and the runtime is not the server.

---

## Commits

- `bed0651` — feat(generator): preserve positional-record DTOs in factory signatures (TRIM-001)
- `925ab0b` — feat(generator): discover DTOs carried as `[Factory]` entity properties (TRIM-002)
- `80762ed` — feat(generator): emit per-assembly event-preservation registrar (TRIM-007)
- `710498c` — perf(generator): restore incremental caching on the relay-handler branch (TRIM-006)
- `bd133e2` — fix(generator): point static-factory registrar attribute at a forwarding holder (TRIM-008)
- `c383530` — fix(generator): point relay-handler registrar attribute at a forwarding holder (TRIM-008)
- `c7cc680` — fix(generator): stop shipping async `[Remote]` bodies to trimmed clients (TRIM-009)

Full engineering record, including the falsified hypotheses and the two abandoned diagnoses, is in the [TRIM todo container](https://github.com/NeatooDotNet/RemoteFactory/tree/main/docs/todos/TRIM-dto-trimming-preservation-gaps).

See the [GitHub release](https://github.com/NeatooDotNet/RemoteFactory/releases/tag/v1.7.0) for the full commit list (auto-generated at release time).
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Every checkable finding was independently re-derived at the keyboard before bein

**Queued as new rows:** C2 → row **23** (two `FactoryEventRelayTests` tests pass vacuously under exactly the condition that reddens their siblings — *a check that cannot go red, inside the class this arc has called a flake for five weeks*, which makes item 10's frequency an undercount by construction). C3 → item **11** widened (TRIM-009 routed its Step 7B to a row whose rationale does not cover it; `ClassFactoryWithExecute.cs` is the one Design source-of-truth file silent on the shape AC6 was held open for).

**Accepted with reason:** C7 (`attributes-reference.md` attributes `[Execute]` body removal to the guard alone — incomplete, not false, and it links to `trimming.md`), C8 (`TrimmingTests/README.md` "How It Works" predates the wrapper/holder — omission, to fix at the release step), C10 (AC6 measured on one TFM and one RID; same SDK therefore same ILLink, so the risk is small, but the wording is "every shape that can be measured" and the measurement is single-TFM).
**Accepted with reason:** C7 (`attributes-reference.md` attributes `[Execute]` body removal to the guard alone — incomplete, not false, and it links to `trimming.md`), C8 (`TrimmingTests/README.md` "How It Works" predates the wrapper/holder — omission, to fix at the release step; **done 2026-08-14**: rewritten as two independently-broken conditions A and B, with the holder-prefix table taken from the renderers by grep — the first draft named the relay prefix from memory and got it wrong — and an explicit note that the interface leg's checks are not evidence of body removal), C10 (AC6 measured on one TFM and one RID; same SDK therefore same ILLink, so the risk is small, but the wording is "every shape that can be measured" and the measurement is single-TFM).

**Queued separately, pre-existing and outside arc scope:** C9 — `skills/RemoteFactory/references/polymorphic-hierarchy.md` cites a file in the private zTreatment repo, violating `CLAUDE.md`'s self-containment rule for the distributable skill.

Expand Down
Loading
Loading