Skip to content

Latest commit

 

History

History
215 lines (155 loc) · 10.7 KB

File metadata and controls

215 lines (155 loc) · 10.7 KB
layout default
title v1.0.0
description Release notes for Neatoo RemoteFactory v1.0.0
parent Release Notes
nav_order 5

v1.0.0 — Production Release

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. AwaitRemote and ContinueOnFail have been removed. RaiseOptions.ServerOnly is unchanged. Read these notes for the 1.0 milestone story but refer to v1.1.0 and Factory Events for the current behavior.

Overview

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.

What's New

IFactoryEvents Mediator

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.

[FactoryEventHandler<T>] Class Attribute

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.

Server-to-Client Event Relay

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, RelayedEvents is null (not an empty list), preserving backward-compatible JSON payloads.
  • Registered handlers are held by WeakReference, so a handler garbage-collected without calling Unregister is silently removed — no memory leak.

RaiseOptions.ServerOnly

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.

IFactoryEventRelay

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.

New Diagnostics

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.

New Documentation

1.0 Milestone: The Complete Feature Set

At 1.0, RemoteFactory provides:

Three factory patterns

  • Class factory — aggregate roots with [Create], [Fetch], [Insert], [Update], [Delete], [Save] lifecycle and IFactorySave<T> / IFactorySaveMeta support.
  • 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=true on the client to drop 60–90% of assembly size.

Authorization

  • [AuthorizeFactory<T>] for domain-specific rules with generated Can* 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 and CancellationToken.
  • [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) and net10.0 (LTS).
  • CancellationToken support across all factory operations, events, and save lifecycle hooks.

Breaking Changes

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.

Migration Guide

Upgrading from v0.29.0 requires no code changes.

To adopt the new factory events feature:

  1. Define an event type as a record inheriting FactoryEventBase:
    public record OrderCheckoutCompleted(int OrderId, decimal Total) : FactoryEventBase;
  2. 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));
    }
  3. Add a handler class with [FactoryEventHandler<T>]. Use a static method for server-side or an instance method for client-side relay. On the client, register the instance with IFactoryEventRelay from the constructor and unregister in Dispose.
  4. 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.

API Stability Commitment

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.

Commits

  • d832010 docs: complete factory event relay — Design, published docs, skill, release notes
  • 3850fd7 feat: add event relay to Person example app
  • 1bbedb4 feat: add factory event relay with [FactoryEventHandler] class attribute
  • 1750f52 feat: add IFactoryEvents mediator pattern with source-generated dispatch

Related: Factory Event Relay Plan