$35
Neatoo is a .NET framework for building domain models with automatic change tracking, validation, and rules through Roslyn source generators. It provides base classes that map to DDD concepts.
Neatoo focuses on the domain model: properties, change tracking, validation, rules, and collections. RemoteFactory is a separate, independent tool that generates client-server factories for any .NET class — it works with Neatoo entities, plain ViewModels, or POCOs. For factory attributes, authorization, and client-server patterns, see the RemoteFactory skill.
<a id='snippet-skill-quickstart'></a>
[Factory]
public partial class Product : EntityBase<Product>
{
public Product(IEntityBaseServices<Product> services) : base(services) { }
[Required]
public partial string Name { get; set; }
public partial decimal Price { get; set; }
[Create] public void Create() { }
}
<sup><a href='/src/samples/QuickStartSamples.cs#L11-L23' title='Snippet source file'>snippet source</a> | <a href='#snippet-skill-quickstart' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->This generates a factory (IProductFactory) with a Create() method. Properties auto-track changes, trigger validation, and fire PropertyChanged.
Business logic belongs in the domain model, not the UI. Neatoo domain models are not DTOs that shuttle data to a smart UI. They are rich domain objects that encapsulate business rules, computed state, validation, and reactive behavior. The UI is a thin binding layer.
When implementing a feature: design domain properties and rules first. Write the UI as a binding layer over those properties. If you find yourself writing business logic in a .razor file, stop and move it to the domain model.
These patterns use RuleManager.AddAction and AddActionAsync, covered in Core Patterns below and in references/validation.md.
| Logic Type | Neatoo Mechanism | NOT in |
|---|---|---|
| Computed/derived values | AddAction with trigger properties | .razor arithmetic/ternary |
| Conditional visibility | Domain bool property via AddAction | .razor @if chains |
| Parent reacts to child changes | AddAction with child trigger t => t.Items![0].Prop | UI event handlers |
| Cross-property validation (single trigger) | AddValidation / AddValidationAsync | UI event handlers |
| Cross-property validation (multiple triggers) | RuleBase<T> / AsyncRuleBase<T> | UI event handlers |
| Reactive data fetch | AddActionAsync | UI OnChanged handlers |
| Cascading state changes | Chained rules (rule sets property -> triggers next rule) | UI code-behind |
| Workflow transitions | Domain methods + AddAction for CanX properties | UI button click handlers |
| LINQ over children | AddAction with child trigger, computed property | .razor inline LINQ |
| Parent orchestrates between children | AddAction with child trigger, action updates other child | UI bridging code |
| Cross-sibling rules in a list | Override HandleNeatooPropertyChanged | UI bridging code |
When writing or reviewing .razor files: if there are more than 3 conditional/computed expressions, business logic is leaking into the UI. Move it to the domain model as rules or computed properties.
// WRONG: UI computes
<MudText>@(order.Quantity * order.UnitPrice)</MudText>
@if (order.Quantity > 0 && order.UnitPrice > 0 && order.Total > 500)
{ <MudAlert>Discount!</MudAlert> }
// RIGHT: Domain computes, UI binds
// In constructor: RuleManager.AddAction(t => t.Total = t.Quantity * t.UnitPrice, t => t.Quantity, t => t.UnitPrice);
// In constructor: RuleManager.AddAction(t => t.QualifiesForDiscount = t.Total > 500, t => t.Total);
<MudText>@order.Total</MudText>
@if (order.QualifiesForDiscount) { <MudAlert>Discount!</MudAlert> }
See references/domain-logic-placement.md for detailed patterns: computed properties, conditional visibility, cascading state, async side-effects, workflow state machines, child property triggers for parent-child reactivity, class-based rules with DI, and the refactoring smell test table.
| DDD Concept | Neatoo Base Class | Use When |
|---|---|---|
| Aggregate Root | EntityBase<T> | Root entity with full CRUD lifecycle |
| Entity | EntityBase<T> | Child entity within an aggregate |
| Value Object | ValidateBase<T> | Data with validation, no persistence lifecycle |
| Entity Collection | EntityListBase<I> | List of child entities (tracks deletions) |
| Validate Collection | ValidateListBase<I> | List of value objects (no deletion tracking) |
| Command | Static class with [Execute] | Server-side operation returning result |
| Read Model | ValidateBase<T> with [Fetch] only | Query result (no Insert/Update/Delete) |
There is no IsDirty in Neatoo. Use IsModified / IsSelfModified.
| Property | Type | Meaning |
|---|---|---|
IsModified | bool | Needs persistence: PropertyManager.IsModified || IsDeleted || IsNew || IsSelfModified. True after Create (because IsNew), false after Fetch. |
IsSelfModified | bool | This object's own properties changed (excludes children, excludes IsNew) |
IsValid | bool | This object and all children pass validation |
IsSelfValid | bool | This object (only) passes validation |
IsSavable | bool | IsValid && IsModified && !IsBusy && !IsChild |
IsNew | bool | Not yet persisted. Set true by Create, set false by Fetch/Insert. Implies IsModified. |
IsDeleted | bool | Marked for deletion |
RuleManager | IRuleManager | Access to validation rules |
All Neatoo properties use partial properties. The source generator implements backing fields with automatic change tracking and validation triggering:
<a id='snippet-skill-properties-basic'></a>
public partial string Name { get; set; }
public partial decimal Price { get; set; }
<sup><a href='/src/samples/QuickStartSamples.cs#L35-L38' title='Snippet source file'>snippet source</a> | <a href='#snippet-skill-properties-basic' title='Start of snippet'>anchor</a></sup>
The generator creates property implementations that call Getter<T>() and Setter() internally.
Neatoo entities use RemoteFactory for factory generation. See the /RemoteFactory skill for factory attributes ([Factory], [Create], [Fetch], [Insert], [Update], [Delete]), service injection ([Service]), remote execution ([Remote]), and authorization ([AuthorizeFactory]).
When Save() is called, the factory routes based on Neatoo entity state:
IsNew == true → [Insert] methodIsNew == false && IsDeleted == false → [Update] methodIsDeleted == true → [Delete] methodThis routing is automatic based on entity state properties.
State cascades UP automatically; saves cascade DOWN manually — each parent's [Insert]/[Update] must call childFactory.SaveAsync() on its children. See references/entities.md → "Aggregate Save Cascading" for the full pattern, rules, and anti-patterns.
Add validation rules in the constructor using RuleManager or validation attributes:
<a id='snippet-skill-validation'></a>
public SkillValidationExample(IEntityBaseServices<SkillValidationExample> services) : base(services)
{
// Inline validation with lambda
RuleManager.AddValidation(
emp => string.IsNullOrEmpty(emp.Name) ? "Name is required" : "",
e => e.Name);
// Or use validation attributes on properties
// [Required(ErrorMessage = "Name is required")]
// public partial string Name { get; set; }
}
<sup><a href='/src/samples/SkillValidationSamples.cs#L52-L64' title='Snippet source file'>snippet source</a> | <a href='#snippet-skill-validation' title='Start of snippet'>anchor</a></sup>
RuleManager also provides AddAction, AddActionAsync, AddValidationAsync, and class-based rules. AddValidation/AddValidationAsync accept exactly one trigger property — for multiple triggers, use a class-based rule. See references/validation.md for details.
Check validation state with IsValid, IsSelfValid, and PropertyMessages.
Rules (including AddAction computed properties) do NOT fire during [Create], [Fetch], [Insert], [Update], [Delete], or LoadValue. Factory operations are wrapped in PauseAllActions(). ResumeAllActions() does NOT run rules — it only recalculates cached validity. PropertyChanged does NOT fire for changes made while paused.
RunRules works while paused — it has no IsPaused guard. Call await RunRules(RunRulesFlag.All) at the end of any factory method that sets properties with dependent AddAction rules:
[Create]
public async Task Create()
{
Quantity = 10;
UnitPrice = 5.00m;
await RunRules(RunRulesFlag.All); // Forces computed properties to populate
// Total is now 50.00
}
Without this call, computed properties remain at their default values when the entity reaches the client. See references/rules-lifecycle.md for the complete execution lifecycle, RunRulesFlag enum reference, and the factory method timeline.
To react to child property changes in an aggregate, use a child property trigger expression with AddAction. The [0] indexer is a syntactic placeholder — any child whose named property changes triggers the rule:
// Parent recalculates when any child's LineTotal changes
RuleManager.AddAction(
t => t.OrderTotal = t.Items?.Sum(i => i.LineTotal) ?? 0,
t => t.Items![0].LineTotal);
// Multiple child property triggers
RuleManager.AddAction(
t => t.HasInvalidQuantities = t.Items?.Any(i => i.Quantity <= 0) ?? false,
t => t.Items![0].Quantity);
The action body can also push changes to other children — the parent acts as orchestrator:
// When ShippingAddress.State changes, update tax on all items
RuleManager.AddAction(
t => { foreach (var item in t.Items!) item.TaxRate = TaxRates.Get(t.ShippingAddress!.State); },
t => t.ShippingAddress!.State);
Do NOT use t => t.Items as the trigger — that only fires when the Items property reference itself is reassigned, not when child items change. TriggerProperty.IsMatch uses exact string equality: "Items" != "Items.LineTotal".
See references/domain-logic-placement.md → "Pattern 6: Child Property Triggers" for child triggers, orchestrator patterns, NeatooPropertyChanged, and HandleNeatooPropertyChanged overrides.
Critical: Never mock Neatoo interfaces or classes. Use real factories and mock only external dependencies. Use [SuppressFactory] on test-only classes that inherit from Neatoo base classes. See references/testing.md for patterns and references/pitfalls.md for common mistakes.
Detailed documentation for each topic area:
references/domain-logic-placement.md - Where business logic belongs: computed properties, conditional visibility, cascading state, async side-effects, child property triggers, workflow state machines, refactoring smell testreferences/base-classes.md - Neatoo-to-DDD mapping, when to use each basereferences/properties.md - Partial properties, change tracking, calculated propertiesreferences/validation.md - RuleManager, attributes, async validationreferences/rules-lifecycle.md - When rules fire and when they don't, RunRulesFlag enum, factory method gap, RunRules works while pausedreferences/shared-rules.md - Shared rules across entities via interface-typed AsyncRuleBase and DI injectionreferences/entities.md - EntityBase lifecycle, persistence, Save routingreferences/collections.md - EntityListBase, parent-child relationships, deletion trackingreferences/lazy-loading.md - EntityLazyLoad<T>, IEntityLazyLoadFactory, explicit LoadAsync(), passive Value read, WaitForTasks integrationreferences/source-generation.md - What gets generated, Generated/ folder, [SuppressFactory]references/trimming.md - IL trimming annotations, suppression strategy, consumer project setupreferences/blazor.md - Blazor-specific binding and component patterns (see also the MudNeatoo skill for component binding and anti-patterns)references/testing.md - No mocking Neatoo, integration test patternsreferences/pitfalls.md - Common mistakes and gotchasRemoteFactory topics (see /RemoteFactory skill):
See references/pitfalls.md for common issues. Key quick checks: class and properties must be partial, class needs [Factory] attribute, and IsSavable requires both IsValid and IsModified.