This reference documents all factory-related attributes and the generated factory interface. Factories are generated by Roslyn Source Generators based on your entity definitions, providing type-safe lifecycle management for your domain objects.

Factory Attributes

[Factory]

Marks a class for factory generation. Required on any class that needs a generated factory.

[Factory]
internal partial class Person : EntityBase<Person>, IPerson
{
    // ...
}

The source generator creates:

  • IPersonFactory interface
  • PersonFactory implementation
  • Custom serializers for remote operations
  • DI registrations

Naming Convention:

Entity Class Interface Generated Factory
Person IPerson IPersonFactory
Order IOrder IOrderFactory
PersonPhoneList IPersonPhoneList IPersonPhoneListFactory

[Create]

Marks a method to be called when creating a new entity instance.

[Create]
public void Create([Service] IChildListFactory childFactory)
{
    // Initialize new entity
    ChildList = childFactory.Create();
    Status = EntityStatus.Draft;
    CreatedDate = DateTime.UtcNow;
}

Characteristics:

  • Called when factory.Create() is invoked
  • Entity has IsNew == true after creation
  • Typically not marked [Remote] (runs locally)
  • Used to initialize child collections and default values

Return Type:

  • void - Standard creation
  • Task - Async creation (if async initialization needed)

Generated Factory Method:

// From [Create] public void Create([Service] IChildFactory f)
IPerson? Create();

// From [Create] public void Create(string name, [Service] IChildFactory f)
IPerson? Create(string name);

[Create] with Records

C# records (10.1.0+) support a type-level [Create] attribute, enabling concise Value Object declarations:

[Factory]
[Create]
public record Money(decimal Amount, string Currency = "USD");

Generated Factory:

public interface IMoneyFactory
{
    Money Create(decimal amount, string currency = "USD");
}

Records with Service Injection:

[Factory]
[Create]
public record Address(
    string Street,
    string City,
    string State,
    string ZipCode,
    [Service] IAddressValidator validator);

// Generated (services hidden from interface):
public interface IAddressFactory
{
    Address Create(string street, string city, string state, string zipCode);
}

Records with Validation:

For validation logic, use a static [Create] method:

[Factory]
public record Money(decimal Amount, string Currency)
{
    [Create]
    public static Money Create(decimal amount, string currency = "USD")
    {
        if (amount < 0)
            throw new ArgumentException("Amount cannot be negative");
        return new Money(amount, currency.ToUpperInvariant());
    }
}

Record Type Constraints:

Type Supported
record ✅ Yes
record class ✅ Yes
record struct ❌ No (diagnostic NF0206)

See C# Records for Value Objects for comprehensive coverage.

[Fetch]

Marks a method to retrieve an existing entity from persistence.

[Remote]
[Fetch]
public async Task<bool> Fetch(
    [Service] IPersonDbContext dbContext,
    [Service] IPersonPhoneListFactory phoneListFactory)
{
    var entity = await dbContext.Persons
        .Include(p => p.Phones)
        .FirstOrDefaultAsync(p => p.Id == Id);

    if (entity == null) return false;

    MapFrom(entity);
    PersonPhoneList = await phoneListFactory.Fetch(entity.Phones);
    return true;
}

Characteristics:

  • Called when factory.Fetch() is invoked
  • Entity has IsNew == false after fetch
  • Usually marked [Remote] (needs database access)
  • Return false or null if entity not found

Return Type:

  • bool - Return false for “not found”
  • Task<bool> - Async fetch with not-found handling
  • Entity type - Return null if not found
  • void / Task - Throws if not found

Generated Factory Method:

// From [Fetch] public async Task<bool> Fetch([Service] IDbContext db)
Task<IPerson?> Fetch();

// From [Fetch] public async Task<bool> Fetch(Guid id, [Service] IDbContext db)
Task<IPerson?> Fetch(Guid id);

[Insert]

Marks a method to persist a new entity.

[Remote]
[Insert]
public async Task Insert(
    [Service] IPersonDbContext dbContext,
    [Service] IPersonPhoneListFactory phoneListFactory)
{
    Id = Guid.NewGuid();

    var entity = new PersonEntity();
    MapTo(entity);              // Copy ALL properties
    dbContext.Persons.Add(entity);

    await phoneListFactory.Save(PersonPhoneList, Id.Value);
    await dbContext.SaveChangesAsync();
}

Characteristics:

  • Called by factory.Save() when IsNew == true
  • Entity has IsNew == false after successful insert
  • Almost always marked [Remote] (needs database)
  • Generate identity (GUID) here or let database generate

Return Type:

  • void / Task - Standard insert
  • Entity type - Return updated persistence entity for reference

[Update]

Marks a method to persist changes to an existing entity.

[Remote]
[Update]
public async Task Update(
    [Service] IPersonDbContext dbContext,
    [Service] IPersonPhoneListFactory phoneListFactory)
{
    var entity = await dbContext.Persons.FindAsync(Id);

    MapModifiedTo(entity);      // Copy only MODIFIED properties

    await phoneListFactory.Save(PersonPhoneList, Id.Value);
    await dbContext.SaveChangesAsync();
}

Characteristics:

  • Called by factory.Save() when IsNew == false and IsDeleted == false
  • Entity has IsModified == false after successful update
  • Almost always marked [Remote] (needs database)
  • Use MapModifiedTo() for efficient updates

Return Type:

  • void / Task - Standard update
  • Entity type - Return updated persistence entity for reference

[Delete]

Marks a method to remove an entity from persistence.

[Remote]
[Delete]
public async Task Delete([Service] IPersonDbContext dbContext)
{
    var entity = await dbContext.Persons.FindAsync(Id);
    if (entity != null)
    {
        dbContext.Persons.Remove(entity);
        await dbContext.SaveChangesAsync();
    }
}

Characteristics:

  • Called by factory.Save() when IsDeleted == true
  • Trigger by calling entity.Delete() before save
  • Almost always marked [Remote] (needs database)
  • Handle cascading deletes to children

To Delete an Entity:

person.Delete();                // Sets IsDeleted = true
await factory.Save(person);     // Calls [Delete] method

[Remote]

Indicates that the operation must execute on the server.

[Remote]
[Fetch]
public async Task<bool> Fetch([Service] IDbContext db)
{
    // This code runs on the server, even when called from Blazor WASM
}

Behavior by Client Type:

Client Without [Remote] With [Remote]
Blazor WASM Runs in browser Serializes to server
Blazor Server Runs on server Runs on server
Console/WPF Runs locally Serializes to server

When to Use:

  • Database access required
  • Server-only services needed
  • Authoritative operations (validation must be trusted)
  • Resources not available on client

When NOT to Use:

  • [Create] operations that only initialize state
  • Operations using only client-available data
  • Performance-critical operations that can run locally

Method Parameters

Factory methods support two types of parameters: regular parameters passed by the caller and service parameters injected from DI.

Regular Parameters

Regular parameters are passed through to the generated factory interface:

[Fetch]
public async Task<bool> Fetch(
    Guid personId,              // Regular - appears in IPersonFactory.Fetch()
    bool includePhones,         // Regular - appears in IPersonFactory.Fetch()
    [Service] IDbContext db)    // Service - hidden from factory interface
{
    // personId and includePhones come from caller
    // db is injected at runtime
}

Generated Interface:

Task<IPerson?> Fetch(Guid personId, bool includePhones);

[Service] Parameters

Parameters marked with [Service] are resolved from the dependency injection container:

[Remote]
[Insert]
public async Task Insert(
    [Service] IPersonDbContext dbContext,
    [Service] IPersonPhoneListFactory phoneListFactory,
    [Service] IEmailService emailService)
{
    // All services injected at runtime
}

Service Resolution:

  • Resolved from IServiceProvider when the method is called
  • Scoped to the request (disposed after method completes)
  • Must be registered in your DI container

Parameter Order Rule:

Regular parameters must come before [Service] parameters:

// Correct
[Fetch]
public Task<bool> Fetch(
    Guid id,                    // Regular first
    string filter,              // Regular second
    [Service] IDbContext db)    // Services last
{ }

// Incorrect - will cause compilation error
[Fetch]
public Task<bool> Fetch(
    [Service] IDbContext db,    // Services cannot come first
    Guid id)
{ }

Passing Parameters to Factory Methods

The generated factory exposes regular parameters:

// Entity method
[Fetch]
public async Task<bool> Fetch(
    Guid id,
    bool includeChildren,
    [Service] IDbContext db)
{ }

// Generated factory interface
public interface IPersonFactory
{
    Task<IPerson?> Fetch(Guid id, bool includeChildren);
}

// Usage
var person = await personFactory.Fetch(personId, includeChildren: true);

Generated Factory Interface

For each entity with [Factory], the source generator creates a factory interface.

Standard Factory Interface

public interface IPersonFactory
{
    // Lifecycle operations
    IPerson? Create();
    Task<IPerson?> Fetch(Guid id);
    Task<IPerson?> Save(IPerson target);
    Task<Authorized<IPerson>> TrySave(IPerson target);

    // Authorization checks
    Authorized CanCreate();
    Authorized CanFetch();
    Authorized CanInsert();
    Authorized CanUpdate();
    Authorized CanDelete();
    Authorized CanSave();
}

Create()

Instantiates a new entity by calling the [Create] method:

var person = personFactory.Create();
// person.IsNew == true
// person.IsModified == true

Parameters: Match regular parameters from your [Create] method.

Returns: The entity interface type (IPerson?).

Fetch()

Retrieves an entity by calling the [Fetch] method:

var person = await personFactory.Fetch(personId);
if (person == null)
{
    // Entity not found
}
// person.IsNew == false
// person.IsModified == false

Parameters: Match regular parameters from your [Fetch] method.

Returns: The entity interface type, or null if not found.

Save()

Persists changes by routing to [Insert], [Update], or [Delete]:

// New entity -> Insert
var newPerson = personFactory.Create();
newPerson.FirstName = "John";
var savedPerson = await personFactory.Save(newPerson);
// savedPerson.IsNew == false

// Existing entity -> Update
var existingPerson = await personFactory.Fetch(id);
existingPerson.LastName = "Updated";
await personFactory.Save(existingPerson);

// Deleted entity -> Delete
existingPerson.Delete();
await personFactory.Save(existingPerson);

Routing Logic:

Entity State Operation Called
IsDeleted == true [Delete] method
IsNew == true [Insert] method
IsNew == false [Update] method

Parameters: Always takes the entity as the first parameter. Additional parameters come from your [Insert]/[Update]/[Delete] methods.

Returns: The updated entity.

Throws: SaveOperationException if entity is not savable (see IsSavable).

Critical: Always Reassign After Save()

When you call Save(), the aggregate is serialized to the server, persisted, and a new instance is returned via deserialization. You MUST capture this return value:

// CORRECT - captures the new deserialized instance
person = await personFactory.Save(person);
// WRONG - original object is now stale!
await personFactory.Save(person);
// person still has old state, no database-generated values

Why This Happens

The Remote Factory pattern transfers your object across the client-server boundary:

  1. Client: Your aggregate is serialized to JSON/binary
  2. Server: A new instance is created from that data, persistence runs
  3. Server: The updated aggregate is serialized back
  4. Client: A NEW instance is deserialized and returned

The object you started with is not the same object that comes back. They are two different instances in memory.

Consequences of Forgetting

What You Lose Example
Database-generated IDs person.Id remains Guid.Empty or 0
Server-computed values Timestamps, calculated fields
Updated validation state IsValid, IsSavable reflect old state
Property modification flags IsModified doesn’t reflect saved state
Concurrency tokens RowVersion/ETag for optimistic concurrency

See also: Blazor Integration - Reassign After Save for Blazor-specific guidance.

TrySave()

Like Save(), but returns authorization result instead of throwing:

var result = await personFactory.TrySave(person);

if (result.IsAuthorized)
{
    var savedPerson = result.Value;
    // Success
}
else
{
    var message = result.Message;
    // Show authorization failure to user
}

Returns: Authorized<IPerson> containing either the saved entity or an authorization failure message.

CanXYZ() Methods

Check authorization before attempting operations:

// Check before showing UI elements
if (personFactory.CanCreate().IsAuthorized)
{
    // Show "Create New" button
}

if (personFactory.CanDelete().IsAuthorized)
{
    // Show "Delete" button
}

// Check with message
var canUpdate = personFactory.CanUpdate();
if (!canUpdate.IsAuthorized)
{
    ShowMessage(canUpdate.Message);  // "You don't have permission..."
}

Available Methods:

Method Checks Authorization For
CanCreate() Creating new entities
CanFetch() Retrieving entities
CanInsert() Inserting new entities
CanUpdate() Updating existing entities
CanDelete() Deleting entities
CanSave() Current save operation (routes based on state)

Save() Validation Requirements

The Save() method verifies the entity is savable before proceeding:

public virtual bool IsSavable => IsModified && IsValid && !IsBusy && !IsChild;

Requirements:

Property Required Value Reason
IsModified true Nothing to save if unchanged
IsValid true Cannot persist invalid data
IsBusy false Async rules must complete first
IsChild false Children save through their parent

Checking Before Save:

if (person.IsSavable)
{
    await personFactory.Save(person);
}
else
{
    if (!person.IsValid)
        ShowValidationErrors(person.PropertyMessages);
    else if (person.IsBusy)
        ShowMessage("Please wait for validation to complete");
    else if (!person.IsModified)
        ShowMessage("No changes to save");
    else if (person.IsChild)
        ShowMessage("Save the parent entity instead");
}

Collection Factory Methods

Entity list factories have slightly different patterns.

Collection Create

Creates an empty collection:

[Factory]
internal partial class PersonPhoneList : EntityListBase<IPersonPhone>, IPersonPhoneList
{
    [Create]
    public void Create()
    {
        // Usually empty - collection is initialized by base class
    }
}

Usage:

var phoneList = phoneListFactory.Create();
// Returns empty collection ready for items

Collection Fetch

Populates the collection from persistence data:

[Fetch]
public async Task Fetch(
    IEnumerable<PersonPhoneEntity> entities,
    [Service] IPersonPhoneFactory phoneFactory)
{
    foreach (var entity in entities)
    {
        var phone = await phoneFactory.Fetch(entity);
        Add(phone);
    }
}

The parent entity passes the data:

// In Person's Fetch method
PersonPhoneList = await phoneListFactory.Fetch(entity.Phones);

Collection Save (Update)

Handles inserts, updates, and deletes within the collection:

[Remote]
[Update]
public async Task Update(
    Guid parentId,
    [Service] IPersonDbContext dbContext)
{
    // Process deletions from DeletedList
    foreach (var deleted in DeletedList.Cast<IPersonPhone>())
    {
        var entity = await dbContext.PersonPhones.FindAsync(deleted.Id);
        if (entity != null)
        {
            dbContext.PersonPhones.Remove(entity);
        }
    }

    // Process remaining items
    foreach (var phone in this)
    {
        if (phone.IsNew)
        {
            // Insert
            phone.Id = Guid.NewGuid();
            var entity = new PersonPhoneEntity { PersonId = parentId };
            phone.MapTo(entity);
            dbContext.PersonPhones.Add(entity);
        }
        else if (phone.IsModified)
        {
            // Update
            var entity = await dbContext.PersonPhones.FindAsync(phone.Id);
            phone.MapModifiedTo(entity);
        }
    }

    await dbContext.SaveChangesAsync();
}

The parent calls the collection’s save:

// In Person's Update method
await phoneListFactory.Save(PersonPhoneList, Id.Value);

Complete Examples

Basic Entity Factory

[Factory]
internal partial class Person : EntityBase<Person>, IPerson
{
    public Person(IEntityBaseServices<Person> services) : base(services) { }

    public partial Guid? Id { get; set; }
    public partial string? Name { get; set; }

    // MapFrom and MapTo are manually implemented
    // Only MapModifiedTo is source-generated
    public void MapFrom(PersonEntity entity)
    {
        LoadProperty(nameof(Id), entity.Id);
        LoadProperty(nameof(Name), entity.Name);
    }

    public void MapTo(PersonEntity entity)
    {
        entity.Id = Id ?? Guid.Empty;
        entity.Name = Name;
    }

    public partial void MapModifiedTo(PersonEntity entity);

    [Create]
    public void Create()
    {
        // Initialize defaults
    }

    [Remote]
    [Fetch]
    public async Task<bool> Fetch([Service] IDbContext db)
    {
        var entity = await db.Persons.FindAsync(Id);
        if (entity == null) return false;
        MapFrom(entity);
        return true;
    }

    [Remote]
    [Insert]
    public async Task Insert([Service] IDbContext db)
    {
        Id = Guid.NewGuid();
        var entity = new PersonEntity();
        MapTo(entity);
        db.Persons.Add(entity);
        await db.SaveChangesAsync();
    }

    [Remote]
    [Update]
    public async Task Update([Service] IDbContext db)
    {
        var entity = await db.Persons.FindAsync(Id);
        MapModifiedTo(entity);
        await db.SaveChangesAsync();
    }

    [Remote]
    [Delete]
    public async Task Delete([Service] IDbContext db)
    {
        var entity = await db.Persons.FindAsync(Id);
        if (entity != null)
        {
            db.Persons.Remove(entity);
            await db.SaveChangesAsync();
        }
    }
}

Entity with Multiple Fetch Overloads

[Factory]
internal partial class Order : EntityBase<Order>, IOrder
{
    // Fetch by ID
    [Remote]
    [Fetch]
    public async Task<bool> Fetch([Service] IDbContext db)
    {
        var entity = await db.Orders.FindAsync(Id);
        if (entity == null) return false;
        MapFrom(entity);
        return true;
    }

    // Fetch by order number
    [Remote]
    [Fetch]
    public async Task<bool> FetchByOrderNumber(
        string orderNumber,
        [Service] IDbContext db)
    {
        var entity = await db.Orders
            .FirstOrDefaultAsync(o => o.OrderNumber == orderNumber);
        if (entity == null) return false;
        MapFrom(entity);
        return true;
    }
}

// Generated factory includes both:
public interface IOrderFactory
{
    Task<IOrder?> Fetch();                              // Uses Id property
    Task<IOrder?> FetchByOrderNumber(string orderNumber); // Uses parameter
}

Entity with Parent-Child Relationship

[Factory]
internal partial class Order : EntityBase<Order>, IOrder
{
    public partial IOrderLineList? Lines { get; set; }

    [Create]
    public void Create([Service] IOrderLineListFactory lineFactory)
    {
        Lines = lineFactory.Create();
    }

    [Remote]
    [Fetch]
    public async Task<bool> Fetch(
        [Service] IDbContext db,
        [Service] IOrderLineListFactory lineFactory)
    {
        var entity = await db.Orders
            .Include(o => o.Lines)
            .FirstOrDefaultAsync(o => o.Id == Id);

        if (entity == null) return false;

        MapFrom(entity);
        Lines = await lineFactory.Fetch(entity.Lines);
        return true;
    }

    [Remote]
    [Insert]
    public async Task Insert(
        [Service] IDbContext db,
        [Service] IOrderLineListFactory lineFactory)
    {
        Id = Guid.NewGuid();
        var entity = new OrderEntity();
        MapTo(entity);
        db.Orders.Add(entity);

        // Save children
        await lineFactory.Save(Lines, Id.Value);
        await db.SaveChangesAsync();
    }

    [Remote]
    [Update]
    public async Task Update(
        [Service] IDbContext db,
        [Service] IOrderLineListFactory lineFactory)
    {
        var entity = await db.Orders.FindAsync(Id);
        MapModifiedTo(entity);

        // Save children (handles inserts, updates, deletes)
        await lineFactory.Save(Lines, Id.Value);
        await db.SaveChangesAsync();
    }

    [Remote]
    [Delete]
    public async Task Delete([Service] IDbContext db)
    {
        // Cascade delete is handled by database or explicitly here
        var entity = await db.Orders.FindAsync(Id);
        if (entity != null)
        {
            db.Orders.Remove(entity);
            await db.SaveChangesAsync();
        }
    }
}