This page documents framework-level source-generator behavior (not storage-provider behavior).
[GenerateAggregate] supports three inheritance paths:
- No declared base class: generated partial type automatically inherits
AggregateBase. - Direct inheritance from
AggregateBase. - Transitive inheritance through one or more intermediate base classes.
Other eligibility rules:
- Aggregate type must be
partial. - Nested and generic aggregate types are not supported.
RegisterEvents()is generated and cannot be manually declared.
// 1) No declared base class (generator adds AggregateBase on generated partial)
[GenerateAggregate]
public partial class ProductAggregate
{
[GenerateAggregateEvent]
public partial void Create(string name);
}
// 2) Direct inheritance
[GenerateAggregate]
public partial class OrderAggregate : AggregateBase
{
[GenerateAggregateEvent]
public partial void CreateOrder(string customerId);
}
// 3) Transitive inheritance
public abstract class DomainAggregateBase : AggregateBase { }
public abstract class BillingAggregateBase : DomainAggregateBase { }
[GenerateAggregate]
public partial class InvoiceAggregate : BillingAggregateBase
{
[GenerateAggregateEvent]
public partial void CreateInvoice(string invoiceNumber);
}Default event namespace:
<AggregateNamespace>.<AggregateNameWithoutSuffix>Events- Example:
Testing.OrderAggregate->Testing.OrderEvents
Default event type naming:
- Event names are inferred from method names (or overridden with
EventName = ...). - Event type suffix defaults to
Event(configurable withEventSuffixdefaults/overrides). - Typical generated type:
Testing.OrderEvents.OrderCreatedEvent.
Namespace can be overridden per method (EventNamespace) or by aggregate defaults.
namespace Testing;
[GenerateAggregate]
public partial class OrderAggregate : AggregateBase
{
[GenerateAggregateEvent]
public partial void CreateOrder(string customerId);
[GenerateAggregateEvent(EventName = "OrderRegistered", EventNamespace = "Testing.Custom.Events")]
public partial void RegisterOrder(string customerId);
}Typical generated types:
Testing.OrderEvents.OrderCreatedEvent(default namespace/name)Testing.Custom.Events.OrderRegisteredEvent(explicit namespace/name)
Property hooks are property-scoped:
On<Property>Changing(ref value)runs on generated command methods before event creation.On<Property>Changed(previous, current)runs in generatedApply(...)after assignment.- If different events update the same property, the same property hooks run for each.
- Hooks run only when the event method maps that property.
Replay behavior:
- Replay executes generated
Apply(...). On<Property>Changedruns on replay.On<Property>Changingdoes not run on replay.
Event hooks are event-scoped:
OnRaising<EventName>Event(ref ...)OnRaised<EventName>Event(@event)OnApplied<EventName>Event(@event)OnShouldApply<EventName>Event(@event, ref bool shouldApply)
Manual behavior:
Manual = truedoes not auto-wire property hooks unless manual code invokes them.
[GenerateAggregate]
public partial class CustomerAggregate : AggregateBase
{
public string Email { get; private set; } = string.Empty;
[GenerateAggregateEvent(EventName = "CustomerRegistered")]
public partial void Register(string email);
[GenerateAggregateEvent(EventName = "CustomerEmailChanged")]
public partial void ChangeEmail(string email);
partial void OnEmailChanging(ref string email) => email = email.Trim().ToLowerInvariant();
partial void OnEmailChanged(string previous, string current) { /* audit */ }
}OnEmailChanging/Changed run for both Register and ChangeEmail because both map to Email.
[GenerateAggregateEvent]methods must bepartialdeclarations without bodies.- Return types must be
void,bool, or the containing aggregate type. - Parameters must map to writable aggregate properties unless explicitly handled as metadata/manual payload.
- Collection event methods (
[GenerateAggregateCollectionEvent]) requireEventStoreList<T>/EventStoreSet<T>target properties.
[GenerateAggregate]
public partial class ReportAggregate : AggregateBase
{
public EventStoreSet<string> Tags { get; private set; } = [];
[GenerateAggregateCollectionEvent(nameof(Tags))]
public partial void AddTag(string tag);
}- Generated mapping paths use
Create(...)semantics for strict command-time conversion/validation. - Contextual
Create(TValue, in ValueObjectContext<TAggregate>)is used when available. - Replay/hydration paths apply event payloads through generated
Apply(...)logic. - Snapshot-query translation depends on how the provider maps the resulting property graph, not only on the value-object generator behavior.
- A
[Scalar]value object that wraps a complex CLR type may serialize correctly while still requiring a separate directly mapped complex mirror property for deep SQL predicates.
// Scalar conversion
[Scalar]
public readonly partial record struct EmailAddress
{
public string Value { get; }
static partial void OnNormalize(ref string value) => value = value.Trim().ToLowerInvariant();
static partial void OnValidate(string value) { /* format checks */ }
}// Contextual conversion
[Scalar]
public readonly partial record struct OrderStatus
: IContextualValueObject<OrderStatus, OrderStatusCode, OrderAggregate>
{
public OrderStatusCode Value { get; }
public static OrderStatus Create(OrderStatusCode value, in ValueObjectContext<OrderAggregate> context)
=> IsValidTransition(context.Aggregate.Status.Value, value)
? new(value)
: throw new InvalidOperationException();
}Common diagnostic IDs:
EVENTSTORE001aggregate must be partialEVENTSTORE002aggregate must inheritAggregateBase(or have no base so generator can add it)EVENTSTORE003nested aggregates unsupportedEVENTSTORE004generic aggregates unsupportedEVENTSTORE005manualRegisterEventsunsupportedEVENTSTORE007generated event method must be partialEVENTSTORE010parameter must map to writable propertyEVENTSTORE018unsupported aggregate collection property type