From fb109f4b5895a4eaaa4410dab678b7b4f45b46a7 Mon Sep 17 00:00:00 2001 From: Keith Voels Date: Thu, 19 Mar 2026 14:16:09 -0500 Subject: [PATCH] docs: add Interface Factory pattern documentation to published docs The Interface Factory pattern was thoroughly documented in the Design source of truth and the skill, but barely mentioned in user-facing docs. Adds a dedicated page, updates the decision guide with a factory pattern comparison, and adds an interface example to the attributes reference. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/attributes-reference.md | 14 +++ docs/decision-guide.md | 28 ++++++ docs/interface-factory.md | 165 +++++++++++++++++++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 docs/interface-factory.md diff --git a/docs/attributes-reference.md b/docs/attributes-reference.md index 9296cc7..f954156 100644 --- a/docs/attributes-reference.md +++ b/docs/attributes-reference.md @@ -45,6 +45,20 @@ public partial class MinimalEmployee snippet source | anchor +On an interface, `[Factory]` generates a remote proxy. All interface methods become remote entry points — no operation attributes needed. The server provides the implementation class (without `[Factory]`). See [Interface Factory](interface-factory.md) for the full pattern. + +```csharp +[Factory] // Generates proxy — all methods are remote +public interface IOrderQueryService +{ + Task> GetAllAsync(); + Task GetByIdAsync(int id); +} + +// Server implementation — no [Factory] here +public class OrderQueryService : IOrderQueryService { ... } +``` + ### [SuppressFactory] Prevents factory generation for a class or interface. Use when a base class has `[Factory]` but a derived class shouldn't have its own factory. diff --git a/docs/decision-guide.md b/docs/decision-guide.md index 079f415..94c4663 100644 --- a/docs/decision-guide.md +++ b/docs/decision-guide.md @@ -22,6 +22,33 @@ Is this method called directly from client code (UI, Blazor component)? --- +## Which Factory Pattern? + +RemoteFactory has three factory patterns. The choice depends on what you're building. + +| Pattern | `[Factory]` On | Use When | Example | +|---------|---------------|----------|---------| +| **Class Factory** | `internal partial class` | Aggregate roots and entities with lifecycle (Create, Fetch, Save) | `Order`, `Employee`, `Invoice` | +| **Interface Factory** | `public interface` | Remote services without entity identity | `IOrderQueryService`, `IReportGenerator` | +| **Static Factory** | `public static partial class` | Stateless commands and fire-and-forget events | `EmailCommands`, `AuditEvents` | + +``` +Does this type manage entity state (properties, IsNew, IsDeleted)? +├── YES → Class Factory +│ (Create/Fetch/Save lifecycle, serialization across boundary) +└── NO + ├── Service with multiple methods the client calls → Interface Factory + │ (server implementation, client proxy, no operation attributes) + └── One-shot operation or fire-and-forget event → Static Factory + ([Execute] for commands, [Event] for side effects) +``` + +**Key difference — Interface Factory has no operation attributes.** Class and Static factories use `[Create]`, `[Fetch]`, `[Remote]`, `[Execute]`, etc. to tell the generator what each method does. Interface Factory methods need none of these — the `[Factory]` on the interface is sufficient. Every method is automatically a remote entry point. + +See [Interface Factory](interface-factory.md) for the full pattern, [Factory Operations](factory-operations.md) for Class Factory operations, and [Events](events.md) for Static Factory events. + +--- + ## Constructor vs Method Injection? Constructor injection puts services on both client and server. Method injection puts them only where the method executes — typically the server. This is how you control which services are available on each side. @@ -123,6 +150,7 @@ See [Authorization](authorization.md) for details. ## Next Steps +- [Interface Factory](interface-factory.md) — Remote service proxy pattern - [Attributes Reference](attributes-reference.md) — Complete attribute documentation - [Client-Server Architecture](client-server-architecture.md) — Understanding `[Remote]` - [Service Injection](service-injection.md) — DI patterns diff --git a/docs/interface-factory.md b/docs/interface-factory.md new file mode 100644 index 0000000..291780f --- /dev/null +++ b/docs/interface-factory.md @@ -0,0 +1,165 @@ +# Interface Factory + +The Interface Factory pattern generates a client-side proxy for a server-only service. You define the contract as a C# interface with `[Factory]`, provide the implementation on the server, and RemoteFactory generates a proxy that serializes calls across the client/server boundary. Clients inject the interface from DI and call it normally — the proxy handles serialization and HTTP transport. + +This pattern is for services without entity identity: query services, report generators, third-party API wrappers. If you need entity lifecycle management (Create, Fetch, Save), use a [Class Factory](factory-operations.md) instead. If you need stateless command delegates, use a [Static Factory](factory-operations.md#execute-operation). + +See the [Decision Guide](decision-guide.md#which-factory-pattern) for a comparison of all three patterns. + +## Complete Example + +### 1. Define the interface + +Place `[Factory]` on the interface. Methods need no operation attributes — every method on the interface is automatically a remote entry point. + +```csharp +[Factory] +public interface IOrderQueryService +{ + Task> GetAllAsync(); + Task GetByIdAsync(int id); + Task CountAsync(); +} +``` + +### 2. Implement on the server + +The implementation is a plain class. It does **not** get `[Factory]` — only the interface has it. + +```csharp +public class OrderQueryService : IOrderQueryService +{ + private readonly AppDbContext _db; + + public OrderQueryService(AppDbContext db) => _db = db; + + public async Task> GetAllAsync() + { + return await _db.Orders + .Select(o => new OrderSummary { Id = o.Id, CustomerName = o.CustomerName }) + .ToListAsync(); + } + + public async Task GetByIdAsync(int id) + { + var order = await _db.Orders.FindAsync(id); + return order == null ? null : new OrderSummary { Id = order.Id, CustomerName = order.CustomerName }; + } + + public async Task CountAsync() + { + return await _db.Orders.CountAsync(); + } +} +``` + +### 3. Register in DI (server only) + +```csharp +// Program.cs (server) +builder.Services.AddScoped(); +``` + +Or use convention-based registration: +```csharp +builder.Services.RegisterMatchingName(); // Auto-finds OrderQueryService +``` + +### 4. Use from client code + +Inject the interface and call it. The generated proxy handles the rest. + +```csharp +@inject IOrderQueryService OrderQuery + +@code { + private IReadOnlyList? orders; + + protected override async Task OnInitializedAsync() + { + orders = await OrderQuery.GetAllAsync(); + } +} +``` + +## What the Generator Produces + +For an interface with `[Factory]`, the generator creates: + +1. **A proxy class** that implements the interface. Each method serializes its arguments, sends an HTTP POST to `/api/neatoo`, and deserializes the response. +2. **Delegate types** for each method on the interface. +3. **DI registrations** that wire the proxy in Remote mode and the real implementation in Server/Logical mode. + +The client never sees the proxy directly — it injects `IOrderQueryService` from DI and gets the proxy automatically. + +## Critical Rule: No Attributes on Interface Methods + +Interface methods do **not** need `[Fetch]`, `[Create]`, `[Remote]`, or any other operation attribute. The `[Factory]` attribute on the interface itself is sufficient. Every method on the interface is automatically remote. + +```csharp +// WRONG - causes duplicate generation +[Factory] +public interface IOrderQueryService +{ + [Fetch] // Don't do this + Task GetByIdAsync(int id); +} + +// RIGHT - no attributes on methods +[Factory] +public interface IOrderQueryService +{ + Task GetByIdAsync(int id); +} +``` + +**Why it matters:** The generator treats every interface method as a remote entry point. Adding operation attributes creates duplicate registrations and generation conflicts. + +## Anti-Pattern: [Factory] on the Implementation Class + +Only the interface gets `[Factory]`. The implementation is a plain service class registered in DI. + +```csharp +// WRONG - duplicate registration +[Factory] +public interface IOrderQueryService { ... } + +[Factory] // Don't do this +public class OrderQueryService : IOrderQueryService { ... } + +// RIGHT - only interface has [Factory] +[Factory] +public interface IOrderQueryService { ... } + +public class OrderQueryService : IOrderQueryService { ... } // No [Factory] +``` + +**Why it matters:** The interface defines the factory contract. Adding `[Factory]` to the implementation creates a second, conflicting factory registration. + +## Differences from Class Factory + +| Aspect | Class Factory | Interface Factory | +|--------|---------------|-------------------| +| `[Factory]` goes on | The class (`internal partial class Order`) | The interface (`public interface IOrderQueryService`) | +| Operation attributes | Required (`[Create]`, `[Fetch]`, `[Remote]`, etc.) | Not used — all methods are implicitly remote | +| Entity state | Yes — properties serialized across boundary | No — request/response only | +| `IFactorySaveMeta` | Supported (Insert/Update/Delete routing) | Not applicable | +| `partial` keyword | Required on the class | Not required on the interface | +| Implementation | The `[Factory]` class IS the implementation | Separate implementation class, no `[Factory]` | +| Generated output | Factory that creates/manages instances | Proxy that forwards calls to server | + +## When to Use Interface Factory + +- **Query services** — Read-only data access that returns DTOs or projections +- **Third-party API wrappers** — Wrap external services behind a clean interface +- **Report generators** — Server-side computation exposed to the client +- **Any remote service without entity lifecycle** — If you don't need Create/Fetch/Save semantics, interface factory is the simpler choice + +When you need entity identity, state management, and persistence routing, use a [Class Factory](factory-operations.md) instead. + +## Next Steps + +- [Decision Guide](decision-guide.md#which-factory-pattern) — Choosing between factory patterns +- [Factory Operations](factory-operations.md) — Class Factory operations reference +- [Client-Server Architecture](client-server-architecture.md) — How the proxy fits in the architecture +- [ASP.NET Core Integration](aspnetcore-integration.md) — Server setup for the `/api/neatoo` endpoint