| title | Interfaces Reference |
|---|---|
| nav_order | 11 |
RemoteFactory provides interfaces for lifecycle hooks, authorization, state management, and save routing. Implement these interfaces on your domain models to integrate with factory-generated code.
Called before a factory operation executes.
public interface IFactoryOnStart
{
void FactoryStart(FactoryOperation factoryOperation);
}When to use: Pre-operation validation, logging, or setup that doesn't require async work.
Implement this interface on your domain model to receive a callback before any factory operation:
// IFactoryOnStart: Called before factory operation executes
public void FactoryStart(FactoryOperation factoryOperation)
{
// Pre-operation validation
if (factoryOperation == FactoryOperation.Delete && EmployeeId == Guid.Empty)
throw new InvalidOperationException("Cannot delete unsaved employee");
}Async version of IFactoryOnStart.
public interface IFactoryOnStartAsync
{
Task FactoryStartAsync(FactoryOperation factoryOperation);
}When to use: Pre-operation work that requires async calls (database queries, external services).
Async pre-operation hook with database access:
// IFactoryOnStartAsync: Async pre-operation hook for database/service calls
public async Task FactoryStartAsync(FactoryOperation factoryOperation)
{
if (_repository == null) return;
// Async validation: check department limit before insert
var existing = await _repository.GetAllAsync();
if (factoryOperation == FactoryOperation.Insert && existing.Count >= 100)
throw new InvalidOperationException("Maximum department limit reached");
}Called after a factory operation completes successfully.
public interface IFactoryOnComplete
{
void FactoryComplete(FactoryOperation factoryOperation);
}When to use: Post-operation cleanup, audit logging, or state updates that don't require async work.
Implement this interface to track successful operations:
// IFactoryOnComplete: Called after factory operation succeeds
public void FactoryComplete(FactoryOperation factoryOperation)
{
CompletedOperation = factoryOperation;
CompleteTime = DateTime.UtcNow;
// Post-operation: audit logging, cache invalidation, etc.
}Async version of IFactoryOnComplete.
public interface IFactoryOnCompleteAsync
{
Task FactoryCompleteAsync(FactoryOperation factoryOperation);
}When to use: Post-operation work that requires async calls (notifications, external logging).
Async post-operation hook for notifications:
// IFactoryOnCompleteAsync: Async post-operation hook
public async Task FactoryCompleteAsync(FactoryOperation factoryOperation)
{
if (_notificationService != null)
await _notificationService.SendAsync("admin@company.com",
$"Operation {factoryOperation} completed for {Name}");
}Called when a factory operation is cancelled via CancellationToken.
public interface IFactoryOnCancelled
{
void FactoryCancelled(FactoryOperation factoryOperation);
}When to use: Cleanup after operation cancellation, rollback logic, or cancellation logging.
Handle operation cancellation:
// IFactoryOnCancelled: Called when operation cancelled via CancellationToken
public void FactoryCancelled(FactoryOperation factoryOperation)
{
CancelledOperation = factoryOperation;
CleanupPerformed = true;
// Cleanup logic for cancelled operation
}Async version of IFactoryOnCancelled.
public interface IFactoryOnCancelledAsync
{
Task FactoryCancelledAsync(FactoryOperation factoryOperation);
}When to use: Async cleanup after cancellation (database rollback, external API calls).
Async cancellation with database rollback:
// IFactoryOnCancelledAsync: Async cancellation cleanup
public async Task FactoryCancelledAsync(FactoryOperation factoryOperation)
{
if (_unitOfWork != null)
await _unitOfWork.RollbackAsync(); // Rollback partial changes
}When a factory operation executes:
IFactoryOnStart/IFactoryOnStartAsync- Before operation- Factory operation method (
[Create],[Fetch],[Update], etc.) IFactoryOnComplete/IFactoryOnCompleteAsync- After successful operation
If cancelled:
IFactoryOnCancelled/IFactoryOnCancelledAsync- AfterOperationCanceledException
Combining sync and async hooks:
// Lifecycle execution order: Start -> Operation -> Complete (or Cancelled)
[Factory]
public partial class EmployeeWithLifecycleOrder : IFactoryOnStart, IFactoryOnComplete, IFactoryOnCancelled
{
public List<string> LifecycleEvents { get; } = new();
public void FactoryStart(FactoryOperation factoryOperation)
=> LifecycleEvents.Add($"Start: {factoryOperation}");
public void FactoryComplete(FactoryOperation factoryOperation)
=> LifecycleEvents.Add($"Complete: {factoryOperation}");
public void FactoryCancelled(FactoryOperation factoryOperation)
=> LifecycleEvents.Add($"Cancelled: {factoryOperation}");
// After Fetch: ["Start: Fetch", "Complete: Fetch"]
// If cancelled: ["Start: Fetch", "Cancelled: Fetch"]
public Guid EmployeeId { get; private set; }
public string Name { get; set; } = "";
[Create]
public EmployeeWithLifecycleOrder() => EmployeeId = Guid.NewGuid();
[Remote, Fetch]
internal async Task<bool> Fetch(Guid id, [Service] IEmployeeRepository repository, CancellationToken ct)
{
var entity = await repository.GetByIdAsync(id, ct);
if (entity == null) return false;
EmployeeId = entity.Id;
Name = $"{entity.FirstName} {entity.LastName}";
return true;
}
}Provides state properties that the generated factory's Save method uses to route to Insert, Update, or Delete.
public interface IFactorySaveMeta
{
bool IsDeleted { get; }
bool IsNew { get; }
}Routing logic:
IsNew = true, IsDeleted = false→ InsertIsNew = false, IsDeleted = false→ UpdateIsNew = false, IsDeleted = true→ DeleteIsNew = true, IsDeleted = true→ No operation (new item deleted before save)
Implement this interface on domain models that use the Save pattern:
// IFactorySaveMeta: Provides IsNew/IsDeleted for Save routing
[Factory]
public partial class EmployeeSaveDemo : IFactorySaveMeta
{
public bool IsNew { get; private set; } = true; // true = Insert, false = Update
public bool IsDeleted { get; set; } // true = Delete
// Routing: IsNew=true -> Insert, IsNew=false -> Update, IsDeleted=true -> Delete
public Guid Id { get; private set; }
public string FirstName { get; set; } = "";
public string LastName { get; set; } = "";
[Create]
public EmployeeSaveDemo()
{
Id = Guid.NewGuid();
IsNew = true;
}
[Remote, Fetch]
internal async Task<bool> Fetch(Guid id, [Service] IEmployeeRepository repo, CancellationToken ct)
{
var entity = await repo.GetByIdAsync(id, ct);
if (entity == null) return false;
Id = entity.Id;
FirstName = entity.FirstName;
LastName = entity.LastName;
IsNew = false; // Fetched = existing
return true;
}
[Remote, Insert]
internal async Task Insert([Service] IEmployeeRepository repo, CancellationToken ct)
{
var entity = new EmployeeEntity
{
Id = Id, FirstName = FirstName, LastName = LastName,
Email = $"{FirstName.ToLowerInvariant()}@example.com",
DepartmentId = Guid.Empty, Position = "New",
SalaryAmount = 0, SalaryCurrency = "USD", HireDate = DateTime.UtcNow
};
await repo.AddAsync(entity, ct);
await repo.SaveChangesAsync(ct);
IsNew = false;
}
[Remote, Update]
internal async Task Update([Service] IEmployeeRepository repo, CancellationToken ct)
{
var entity = new EmployeeEntity
{
Id = Id, FirstName = FirstName, LastName = LastName,
Email = $"{FirstName.ToLowerInvariant()}@example.com",
DepartmentId = Guid.Empty, Position = "Updated",
SalaryAmount = 0, SalaryCurrency = "USD", HireDate = DateTime.UtcNow
};
await repo.UpdateAsync(entity, ct);
await repo.SaveChangesAsync(ct);
}
[Remote, Delete]
internal async Task Delete([Service] IEmployeeRepository repo, CancellationToken ct)
{
await repo.DeleteAsync(Id, ct);
await repo.SaveChangesAsync(ct);
}
}See Save Operation for complete usage details.
Generated factories implement this interface when the domain model implements IFactorySaveMeta.
public interface IFactorySave<T> where T : IFactorySaveMeta
{
Task<IFactorySaveMeta?> Save(T entity, CancellationToken cancellationToken = default);
Task<Authorized> CanSave(CancellationToken cancellationToken = default);
Task<Authorized> CanSave(T target, CancellationToken cancellationToken = default);
}You do not implement this interface. The generator creates it automatically.
CanSave overloads:
CanSave()-- runs non-target Write auth methods (role checks, permissions). ReturnsAuthorized(true)when no authorization is configured.CanSave(target)-- runs ALL Write auth methods, including target-parameterized auth that inspects entity state. ReturnsAuthorized(true)when no authorization is configured.
Using the generated Save method:
// IFactorySave<T>: Generated Save() routes to Insert/Update/Delete based on state
public class SaveLifecycleDemo
{
public async Task Demo(IEmployeeWithSaveMetaFactory factory)
{
var employee = factory.Create(); // IsNew=true
employee.Name = "John Smith";
await factory.Save(employee); // IsNew=true -> Insert
employee.Name = "Jane Smith";
await factory.Save(employee); // IsNew=false -> Update
employee.IsDeleted = true;
await factory.Save(employee); // IsDeleted=true -> Delete
}
}Performs ASP.NET Core authorization checks on the server. Injected into factory operations decorated with [AspAuthorize].
public interface IAspAuthorize
{
Task<string?> Authorize(
IEnumerable<AspAuthorizeData> authorizeData,
bool forbid = false);
}When to use: Custom ASP.NET Core authorization implementations that need different policy evaluation logic.
You rarely implement this interface. The default implementation (AspAuthorize) is registered automatically by AddNeatooAspNetCore() and integrates with ASP.NET Core's IAuthorizationPolicyProvider and IPolicyEvaluator.
Return value:
- Empty string if authorized
- Error message string if not authorized
- Throws
AspForbidExceptionifforbid = trueand authorization fails
Custom authorization implementation:
// IAspAuthorize: Custom authorization with audit logging
public class AuditingAspAuthorize : IAspAuthorize
{
private readonly IAspAuthorize _inner;
private readonly IAuditLogService _auditLog;
public AuditingAspAuthorize(IAspAuthorize inner, IAuditLogService auditLog)
{
_inner = inner;
_auditLog = auditLog;
}
public async Task<string?> Authorize(IEnumerable<AspAuthorizeData> authorizeData, bool forbid = false)
{
var policies = string.Join(", ", authorizeData.Select(a => a.Policy ?? a.Roles ?? "Default"));
await _auditLog.LogAsync("AuthCheck", Guid.Empty, "Auth", $"Policies: {policies}", default);
var result = await _inner.Authorize(authorizeData, forbid);
await _auditLog.LogAsync(string.IsNullOrEmpty(result) ? "AuthSuccess" : "AuthFailed",
Guid.Empty, "Auth", result ?? "OK", default);
return result;
}
}See Authorization for standard authorization patterns.
Interface for types that support ordinal (positional) JSON serialization. Types implementing this interface are serialized as JSON arrays instead of objects, reducing payload size.
public interface IOrdinalSerializable
{
object?[] ToOrdinalArray();
}When to use: Types that need compact array-based serialization instead of the default object-based format. The source generator automatically implements this for types with [Factory] attribute.
Ordinal order: Properties are serialized alphabetically by name. For inherited types, base class properties come first (alphabetically), followed by derived class properties (alphabetically).
Example of implementing IOrdinalSerializable:
// IOrdinalSerializable: Compact array-based JSON serialization
public class MoneyValueObject : IOrdinalSerializable
{
public decimal Amount { get; }
public string Currency { get; }
public MoneyValueObject(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
// Properties in alphabetical order: Amount, Currency
public object?[] ToOrdinalArray() => [Amount, Currency];
// JSON: [100.50, "USD"] instead of {"Amount":100.50,"Currency":"USD"}
}See Serialization for details on ordinal format.
Provides a custom JsonConverter for ordinal serialization. Uses static abstract interface members for IL trimming compatibility.
public interface IOrdinalConverterProvider<TSelf> where TSelf : class
{
static abstract JsonConverter<TSelf> CreateOrdinalConverter();
}When to use: Types implementing IOrdinalSerializable that need a custom converter for compact serialization. The source generator automatically implements this for [Factory] types.
This is a static abstract interface (C# 11+). The implementing type provides a static factory method for its converter.
Custom ordinal converter for a value object:
// IOrdinalConverterProvider<TSelf>: Custom converter for ordinal serialization
public class MoneyWithConverter : IOrdinalSerializable, IOrdinalConverterProvider<MoneyWithConverter>
{
public decimal Amount { get; }
public string Currency { get; }
public MoneyWithConverter(decimal amount, string currency)
=> (Amount, Currency) = (amount, currency);
public object?[] ToOrdinalArray() => [Amount, Currency];
// Static factory provides the converter
public static JsonConverter<MoneyWithConverter> CreateOrdinalConverter()
=> new MoneyOrdinalConverter();
}
// Converter implementation (outside snippet for brevity)
file sealed class MoneyOrdinalConverter : JsonConverter<MoneyWithConverter>
{
public override MoneyWithConverter Read(
ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray) throw new JsonException();
reader.Read(); var amount = reader.GetDecimal();
reader.Read(); var currency = reader.GetString() ?? "USD";
reader.Read();
return new MoneyWithConverter(amount, currency);
}
public override void Write(
Utf8JsonWriter writer, MoneyWithConverter value, JsonSerializerOptions options)
{
writer.WriteStartArray();
writer.WriteNumberValue(value.Amount);
writer.WriteStringValue(value.Currency);
writer.WriteEndArray();
}
}Provides metadata about ordinal serialization for a specific type. Used by the serializer to reconstruct objects from ordinal arrays.
public interface IOrdinalSerializationMetadata
{
static abstract string[] PropertyNames { get; }
static abstract Type[] PropertyTypes { get; }
static abstract object FromOrdinalArray(object?[] values);
}You do not implement this interface. The source generator automatically implements it for types with [Factory] attribute to enable ordinal deserialization.
PropertyNames and PropertyTypes: Arrays in ordinal order (alphabetical by property name, base class properties first).
FromOrdinalArray: Creates an instance from an array of property values in ordinal order.
Creates LazyLoad<T> instances for deferred async loading. Registered as a singleton by AddNeatooRemoteFactory().
public interface ILazyLoadFactory
{
LazyLoad<TChild> Create<TChild>(Func<Task<TChild?>> loader) where TChild : class?;
LazyLoad<TChild> Create<TChild>(TChild? value) where TChild : class?;
}When to use: Set up LazyLoad<T> properties in factory methods with a loader delegate for on-demand loading, or with a pre-loaded value for eager scenarios.
Two creation patterns:
Create<T>(loader)— Deferred:IsLoaded = false, callLoadAsync()to trigger the loaderCreate<T>(value)— Pre-loaded:IsLoaded = true,Valueis set immediately
[Remote, Fetch]
internal void Fetch(int id,
[Service] ILazyLoadFactory lazyLoadFactory,
[Service] IReviewService reviewService)
{
Id = id;
// Deferred: loader set up but not invoked
Reviews = lazyLoadFactory.Create<string>(async () =>
{
return await reviewService.GetReviewsAsync(Id);
});
}LazyLoad<T> properties serialize their Value and IsLoaded state across the wire. The loader delegate is not serialized — it is reconstructed via the constructor-initialization pattern on deserialization. See Serialization — LazyLoad Properties for format details.
See LazyLoad for the full usage guide.
Request-scoped mediator for publishing factory events. Injected as [Service] IFactoryEvents into factory methods.
public interface IFactoryEvents
{
Task Raise<T>(
T factoryEvent,
RaiseOptions options = RaiseOptions.None,
CancellationToken cancellationToken = default)
where T : FactoryEventBase;
}When to use: Publishing domain events from within a factory method. Raise<T> dispatches to every matching [FactoryEventHandler<T>] static-method handler in the caller's DI scope, sequentially, awaited — handlers share the caller's DbContext and transaction, and an exception in any handler aborts the chain and propagates to the caller. Unless RaiseOptions.ServerOnly is set, the event is also captured for relay back to the client. For fire-and-forget work that should not participate in the caller's transaction, compose a manual Task.Run + IServiceScopeFactory.CreateScope() pattern inside the factory method (see the v1.5.0 release notes). See Factory Events.
Single-method integration hook. Consumers implement this interface to receive events relayed from the server after a [Remote] factory call returns. RemoteFactory invokes it fire-and-forget, exactly once per [Remote] call (including the empty-batch case), strictly after the caller's continuation resumes.
public interface IFactoryEventRelay
{
Task Relay(IReadOnlyList<FactoryEventBase> events);
}When to use: Implement on a service that bridges relayed events to your own event aggregator (MediatR, a UI message bus, a reactive subject, etc.). Register it in DI either before or after AddNeatooRemoteFactory — Remote mode registers NoOpFactoryEventRelay via TryAddSingleton, so a consumer registration before wins, and a consumer registration after overrides.
Contract:
- One
[Remote]call = oneRelayinvocation. The empty-batch case (events.Count == 0) still produces one invocation — useful as a "a factory call just returned" signal. The only exception isUnknownFactoryEventTypeExceptionduring deserialization, which aborts the batch and is logged (EventId 3009). - Post-return ordering is a hard guarantee (
Task.Run + Task.Yielddispatch). - Relay exceptions are caught and logged (EventId 3008). They never propagate to the factory caller.
- The consumer owns SyncContext marshaling for UI work.
Migration note. The former surface (
Register(object handler)/Unregister(object handler), instance-method[FactoryEventHandler<T>]on client classes,WeakReference-based handler tracking) has been removed. Classes that still declare instance-method handlers inside[FactoryEventHandler<T>]emit NF0503 (Warning) and are silently skipped at runtime. See Factory Events — Client-Side Relay.
Low-level factory execution abstraction. Generated factories use this internally for lifecycle hook invocation and operation tracking.
public interface IFactoryCore<T>
{
T DoFactoryMethodCall(FactoryOperation operation, Func<T> factoryMethodCall);
Task<T> DoFactoryMethodCallAsync(FactoryOperation operation, Func<Task<T>> factoryMethodCall);
Task<T?> DoFactoryMethodCallAsyncNullable(FactoryOperation operation, Func<Task<T?>> factoryMethodCall);
T DoFactoryMethodCall(T target, FactoryOperation operation, Action factoryMethodCall);
T? DoFactoryMethodCallBool(T target, FactoryOperation operation, Func<bool> factoryMethodCall);
Task<T> DoFactoryMethodCallAsync(T target, FactoryOperation operation, Func<Task> factoryMethodCall);
Task<T?> DoFactoryMethodCallBoolAsync(T target, FactoryOperation operation, Func<Task<bool>> factoryMethodCall);
}You rarely implement this interface. The default implementation (FactoryCore<T>) handles lifecycle hooks (IFactoryOnStart, IFactoryOnComplete, IFactoryOnCancelled) and logging. You can register a custom implementation for a specific type to add custom factory behavior without inheritance.
| Interface | Purpose | Who Implements |
|---|---|---|
IFactoryOnStart |
Pre-operation sync hook | Domain models |
IFactoryOnStartAsync |
Pre-operation async hook | Domain models |
IFactoryOnComplete |
Post-operation sync hook | Domain models |
IFactoryOnCompleteAsync |
Post-operation async hook | Domain models |
IFactoryOnCancelled |
Cancellation sync hook | Domain models |
IFactoryOnCancelledAsync |
Cancellation async hook | Domain models |
IFactorySaveMeta |
Save routing state | Domain models |
IFactorySave<T> |
Save method signature | Generated factories |
IAspAuthorize |
ASP.NET Core authorization | Custom auth implementations |
IOrdinalSerializable |
Ordinal serialization marker | Domain models, value objects |
IOrdinalConverterProvider<TSelf> |
Ordinal converter provider | Source generator (automatic) |
IOrdinalSerializationMetadata |
Ordinal deserialization metadata | Source generator (automatic) |
ILazyLoadFactory |
Deferred loading factory | Framework (inject via [Service]) |
IFactoryEvents |
Mediator for publishing factory events | Factory methods (inject) |
IFactoryEventRelay |
Client-side single-method integration hook for relayed events | Application (implement + register in DI) |
IFactoryCore<T> |
Factory execution pipeline | Framework (rarely customized) |
- Attributes Reference - All available attributes
- Factory Operations - CRUD operation details
- Service Injection - DI integration
- Factory Events -
[FactoryEventHandler<T>]mediator and client relay - Authorization - Authorization patterns