| layout | default |
|---|---|
| title | v1.0.0 |
| description | Release notes for Neatoo RemoteFactory v1.0.0 |
| parent | Release Notes |
| nav_order | 5 |
Release Date: 2026-04-10 NuGet: Neatoo.RemoteFactory 1.0.0
Partially superseded by v1.1.0. The
[FactoryEventHandler<T>]execution model described below — detached fire-and-forget dispatch,RaiseOptions.AwaitRemote,RaiseOptions.ContinueOnFail— was reversed in v1.1.0, which shipped the same day. In v1.1.0 and later, handlers run in the caller's DI scope, sequentially, awaited, with exceptions propagating to the caller.AwaitRemoteandContinueOnFailhave been removed.RaiseOptions.ServerOnlyis unchanged. Read these notes for the 1.0 milestone story but refer to v1.1.0 and Factory Events for the current behavior.
Neatoo RemoteFactory reaches 1.0. The API surface is stable, the three factory patterns (class, interface, static) are complete, IL trimming is supported end-to-end, and this release adds the last major feature needed for rich client/server domain modeling: [FactoryEventHandler<T>] — a unified mediator + server-to-client event relay.
If you've been tracking the 0.x pre-release series, this is the release you've been waiting on. API stability commitments begin here.
A request-scoped IFactoryEvents service publishes strongly-typed events that inherit FactoryEventBase. Inject it as a [Service] parameter inside a factory method and call Raise:
public record OrderCheckoutCompleted(int OrderId, decimal Total) : FactoryEventBase;
[Factory]
internal partial class Order
{
[Remote, Create]
internal async Task Create(int id, decimal total, [Service] IFactoryEvents events)
{
Id = id; Total = total;
await events.Raise(new OrderCheckoutCompleted(id, total));
}
}Handlers are discovered at compile time by the source generator — no reflection at runtime, trimming-safe by default.
A class-level attribute that unifies mediator and relay handlers. The source generator finds one matching method by signature (Task Name(T evt [, services…] [, CancellationToken ct])) and registers either a server-side handler or a client-side relay handler based on whether the method is static or an instance method.
// Static method → server-side handler (runs in isolated scope)
[FactoryEventHandler<OrderCheckoutCompleted>]
public static partial class OrderAudit
{
internal static Task Log(
OrderCheckoutCompleted evt,
[Service] IAuditLogService audit,
CancellationToken ct) =>
audit.LogAsync("Checkout", evt.OrderId, "Order", $"Total: {evt.Total:C}", ct);
}
// Instance method → client-side relay handler (called on registered instance)
[FactoryEventHandler<OrderCheckoutCompleted>]
public sealed partial class CheckoutViewModel : IDisposable
{
private readonly IFactoryEventRelay _relay;
public CheckoutViewModel(IFactoryEventRelay relay)
{
_relay = relay;
_relay.Register(this);
}
public Task Handle(OrderCheckoutCompleted evt) => Task.CompletedTask;
public void Dispose() => _relay.Unregister(this);
}A single class can stack multiple [FactoryEventHandler<T>] attributes to handle multiple event types; the generator matches one method per attribute.
Events raised on the server during a factory operation are captured by a request-scoped IFactoryEventCollector and travel back to the client on the existing RemoteResponseDto — a new RelayedEvents property carries a list of RelayedFactoryEvent DTOs (type full name + JSON payload). On the client, IFactoryEventRelay deserializes and dispatches each event to any registered handler instance using source-generated, trimming-safe dispatch delegates.
No SignalR or separate push infrastructure required. The relay piggybacks on the existing HTTP response channel.
Ordering guarantees:
- The factory operation result is returned to the caller first.
- Relayed events are dispatched after (fire-and-forget) — handler exceptions never propagate to the factory caller.
- When zero events are captured,
RelayedEventsisnull(not an empty list), preserving backward-compatible JSON payloads. - Registered handlers are held by
WeakReference, so a handler garbage-collected without callingUnregisteris silently removed — no memory leak.
A new flag on RaiseOptions that runs server-side handlers but excludes the event from the client relay. Use it for server-internal concerns the UI doesn't need to know about. Composes with existing flags:
await events.Raise(
new OrderCheckoutCompleted(id, total),
RaiseOptions.ServerOnly | RaiseOptions.ContinueOnFail);| Flag | Meaning |
|---|---|
None |
Default. Server handlers run; event is relayed to the client. |
AwaitRemote |
Wait for server handlers before Raise returns. |
ContinueOnFail |
Continue dispatching remaining handlers if one throws. |
ServerOnly |
Server handlers run; event is NOT relayed to the client. |
A new client-side singleton service for registering instance handlers:
public interface IFactoryEventRelay
{
void Register(object handler);
void Unregister(object handler);
}Multiple handlers for the same event type all receive the dispatch. If no handler is registered for a relayed event type, the event is silently dropped.
| ID | Severity | Description |
|---|---|---|
| NF0501 | Error | No matching handler method found for [FactoryEventHandler<T>]. The class must declare exactly one method returning Task whose first non-[Service]/non-CancellationToken parameter is of type T. |
| NF0502 | Error | Multiple matching handler methods found for [FactoryEventHandler<T>]. Remove the extras or split into separate handler classes. |
docs/factory-events.md— comprehensive guide to the mediator, relay,RaiseOptions, and diagnostics.docs/events.mdnow has a prominent cross-reference distinguishing the[Event]method attribute (isolated-scope fire-and-forget) from the[FactoryEventHandler<T>]class attribute (mediator + relay).docs/attributes-reference.mdanddocs/interfaces-reference.mdnow include[FactoryEventHandler<T>],IFactoryEvents, andIFactoryEventRelay.
At 1.0, RemoteFactory provides:
Three factory patterns
- Class factory — aggregate roots with
[Create],[Fetch],[Insert],[Update],[Delete],[Save]lifecycle andIFactorySave<T>/IFactorySaveMetasupport. - Interface factory — remote services (repositories, command handlers) with automatic client proxy generation.
- Static factory — stateless commands via
[Execute]and fire-and-forget delegates via[Event].
Client/server fundamentals
[Remote]routes client calls to server factory methods via HTTP, while non-remote methods stay server-side and are trimmed from the client.[Service]parameter injection for server-only dependencies (EF contexts, repositories, etc.).- Ordinal serialization (40–50% smaller payloads) with trim-safe generation via
[JsonSerializable]contexts. - Shared reference handling for mutable types; record bypass converter for immutable value types.
- Full IL trimming support — enable
PublishTrimmed=trueon the client to drop 60–90% of assembly size.
Authorization
[AuthorizeFactory<T>]for domain-specific rules with generatedCan*methods the client calls to drive UI.[AspAuthorize]for ASP.NET Core policy integration on server endpoints.
Events
[Event]— per-method fire-and-forget delegates with isolated scope andCancellationToken.[FactoryEventHandler<T>](new in 1.0) — mediator pattern with server-side handlers and client-side relay over the existing HTTP response channel.
Infrastructure
- Correlation ID propagation across client → server → event scopes.
- Structured logging via
ILogger<T>throughout the pipeline. - Multi-targeting:
net9.0(STS) andnet10.0(LTS). CancellationTokensupport across all factory operations, events, and save lifecycle hooks.
None from v0.29.0. All 1.0 additions are new APIs. The RelayedEvents property on RemoteResponseDto is nullable and defaults to null, so existing wire payloads and older clients are unaffected.
Upgrading from v0.29.0 requires no code changes.
To adopt the new factory events feature:
- Define an event type as a record inheriting
FactoryEventBase:public record OrderCheckoutCompleted(int OrderId, decimal Total) : FactoryEventBase;
- Raise the event from inside a factory method:
[Remote, Create] internal async Task Create(int id, decimal total, [Service] IFactoryEvents events) { // ... do work ... await events.Raise(new OrderCheckoutCompleted(id, total)); }
- Add a handler class with
[FactoryEventHandler<T>]. Use astaticmethod for server-side or an instance method for client-side relay. On the client, register the instance withIFactoryEventRelayfrom the constructor and unregister inDispose. - For server-internal events you don't want relayed, pass
RaiseOptions.ServerOnly.
Nothing in existing code paths changes — [Event] methods, factory operations, authorization, and serialization behave exactly as before.
With 1.0, RemoteFactory commits to semantic versioning:
- Patch releases (1.0.x) — bug fixes only, no API changes.
- Minor releases (1.x.0) — additive features only. Existing code keeps working.
- Major releases (2.0.0+) — breaking changes, accompanied by a migration guide and deprecation period where possible.
Diagnostics IDs, generated code shapes, and runtime contracts (serialization format, RemoteResponseDto, registrar methods) are all considered part of the public surface.
d832010docs: complete factory event relay — Design, published docs, skill, release notes3850fd7feat: add event relay to Person example app1bbedb4feat: add factory event relay with [FactoryEventHandler] class attribute1750f52feat: add IFactoryEvents mediator pattern with source-generated dispatch
Related: Factory Event Relay Plan