Skip to content

Latest commit

 

History

History
695 lines (547 loc) · 28 KB

File metadata and controls

695 lines (547 loc) · 28 KB
title Interfaces Reference
nav_order 11

Interfaces Reference

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.

Lifecycle Hooks

IFactoryOnStart

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");
}

snippet source | anchor

IFactoryOnStartAsync

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");
}

snippet source | anchor

IFactoryOnComplete

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.
}

snippet source | anchor

IFactoryOnCompleteAsync

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}");
}

snippet source | anchor

IFactoryOnCancelled

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
}

snippet source | anchor

IFactoryOnCancelledAsync

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
}

snippet source | anchor

Lifecycle Hook Execution Order

When a factory operation executes:

  1. IFactoryOnStart / IFactoryOnStartAsync - Before operation
  2. Factory operation method ([Create], [Fetch], [Update], etc.)
  3. IFactoryOnComplete / IFactoryOnCompleteAsync - After successful operation

If cancelled:

  • IFactoryOnCancelled / IFactoryOnCancelledAsync - After OperationCanceledException

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;
    }
}

snippet source | anchor

Save Operation

IFactorySaveMeta

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 → Insert
  • IsNew = false, IsDeleted = false → Update
  • IsNew = false, IsDeleted = true → Delete
  • IsNew = 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);
    }
}

snippet source | anchor

See Save Operation for complete usage details.

IFactorySave<T>

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). Returns Authorized(true) when no authorization is configured.
  • CanSave(target) -- runs ALL Write auth methods, including target-parameterized auth that inspects entity state. Returns Authorized(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
    }
}

snippet source | anchor

Authorization

IAspAuthorize

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 AspForbidException if forbid = true and 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;
    }
}

snippet source | anchor

See Authorization for standard authorization patterns.

Serialization

IOrdinalSerializable

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"}
}

snippet source | anchor

See Serialization for details on ordinal format.

IOrdinalConverterProvider<TSelf>

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();
    }
}

snippet source | anchor

IOrdinalSerializationMetadata

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.

Deferred Loading

ILazyLoadFactory

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, call LoadAsync() to trigger the loader
  • Create<T>(value) — Pre-loaded: IsLoaded = true, Value is 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.

Factory Events

IFactoryEvents

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.

IFactoryEventRelay

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 = one Relay invocation. The empty-batch case (events.Count == 0) still produces one invocation — useful as a "a factory call just returned" signal. The only exception is UnknownFactoryEventTypeException during deserialization, which aborts the batch and is logged (EventId 3009).
  • Post-return ordering is a hard guarantee (Task.Run + Task.Yield dispatch).
  • 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.

Factory Core

IFactoryCore<T>

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.

Summary

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)

Next Steps