Exception Handling Reference
Neatoo provides a structured exception hierarchy for handling errors that occur during entity operations. Understanding these exceptions helps you build robust error handling and provide meaningful feedback to users.
Exception Hierarchy
All Neatoo exceptions inherit from NeatooException:
NeatooException
├── SaveOperationException
├── ChildObjectBusyException
├── TypeNotRegisteredException
├── RuleNotAddedException
├── PropertyReadOnlyException
└── PropertyMissingException
NeatooException
The base exception class for all Neatoo-specific errors.
public class NeatooException : Exception
{
public NeatooException(string message) : base(message) { }
public NeatooException(string message, Exception innerException)
: base(message, innerException) { }
}
When It’s Thrown
NeatooException is the base class; you’ll typically catch its subclasses for specific error handling. Catching NeatooException handles all framework-specific errors.
try
{
await factory.Save(entity);
}
catch (NeatooException ex)
{
// Handle any Neatoo framework error
logger.LogError(ex, "Neatoo operation failed");
}
SaveOperationException
The most common exception you’ll encounter. Thrown when Save() cannot proceed due to entity state issues.
public class SaveOperationException : NeatooException
{
public SaveOperationReason Reason { get; }
public SaveOperationException(SaveOperationReason reason, string message)
: base(message)
{
Reason = reason;
}
}
SaveOperationReason Enum
The Reason property indicates why the save failed:
| Reason | Description | Common Cause |
|---|---|---|
IsChildObject |
Entity is a child in an aggregate | Trying to save a child directly instead of through the root |
IsInvalid |
Entity failed validation | Validation rules returned errors |
NotModified |
Entity has no changes | Calling save on an unmodified entity |
IsBusy |
Async operations pending | Saving while validation rules are still running |
NoFactoryMethod |
No appropriate factory method found | Missing [Insert], [Update], or [Delete] attribute |
Handling SaveOperationException
try
{
await factory.Save(person);
}
catch (SaveOperationException ex) when (ex.Reason == SaveOperationReason.IsInvalid)
{
// Show validation errors to user
foreach (var msg in person.PropertyMessages)
{
Console.WriteLine($"{msg.PropertyName}: {msg.Message}");
}
}
catch (SaveOperationException ex) when (ex.Reason == SaveOperationReason.IsBusy)
{
// Wait for pending operations and retry
await person.WaitForTasks();
if (person.IsSavable)
{
await factory.Save(person);
}
}
catch (SaveOperationException ex) when (ex.Reason == SaveOperationReason.IsChildObject)
{
// Save through parent instead
var parent = person.Parent as IOrder;
if (parent != null)
{
await orderFactory.Save(parent);
}
}
catch (SaveOperationException ex)
{
// Handle other save failures
logger.LogError("Save failed: {Reason} - {Message}", ex.Reason, ex.Message);
}
Proactive Prevention
Rather than catching SaveOperationException, check IsSavable before attempting to save:
// Preferred approach - check before save
await person.WaitForTasks();
if (!person.IsSavable)
{
if (!person.IsModified)
ShowMessage("No changes to save.");
else if (!person.IsValid)
ShowValidationErrors(person.PropertyMessages);
else if (person.IsBusy)
ShowMessage("Please wait for validation to complete.");
else if (person.IsChild)
ShowMessage("Save through the parent entity.");
return;
}
await factory.Save(person);
ChildObjectBusyException
Thrown when attempting to modify a child collection while async operations are in progress on child entities.
public class ChildObjectBusyException : NeatooException
{
public bool IsAddOperation { get; }
public ChildObjectBusyException(bool isAddOperation)
: base(isAddOperation
? "Cannot add child while async operations are in progress"
: "Cannot remove child while async operations are in progress")
{
IsAddOperation = isAddOperation;
}
}
When It’s Thrown
- Adding to an
EntityListBasewhile a child hasIsBusy == true - Removing from an
EntityListBasewhile a child hasIsBusy == true
Handling ChildObjectBusyException
try
{
order.Lines.Add(newLine);
}
catch (ChildObjectBusyException ex)
{
if (ex.IsAddOperation)
{
// Wait for existing items to complete validation
foreach (var line in order.Lines)
{
await line.WaitForTasks();
}
// Retry add
order.Lines.Add(newLine);
}
}
Prevention
Wait for tasks before collection modifications:
// Wait for all children to finish async operations
foreach (var line in order.Lines)
{
await line.WaitForTasks();
}
// Now safe to modify
order.Lines.Add(newLine);
order.Lines.Remove(existingLine);
TypeNotRegisteredException
Thrown when a required type is not registered in the dependency injection container.
public class TypeNotRegisteredException : NeatooException
{
public Type MissingType { get; }
public TypeNotRegisteredException(Type type)
: base($"Type '{type.FullName}' is not registered in the service container")
{
MissingType = type;
}
}
Common Causes
- Missing
AddNeatooServices()call:// Forgot this in Program.cs builder.Services.AddNeatooServices(NeatooFactory.Server, typeof(IPerson).Assembly); - Assembly not included:
// Entity is in a different assembly that wasn't registered builder.Services.AddNeatooServices( NeatooFactory.Server, typeof(IPerson).Assembly, typeof(IOrder).Assembly // Don't forget additional assemblies ); - Missing service registration:
// Custom service not registered builder.Services.AddScoped<IMyCustomService, MyCustomService>();
Handling
This is typically a configuration error. Fix the registration rather than catching the exception:
// In Program.cs - ensure all assemblies are registered
builder.Services.AddNeatooServices(
NeatooFactory.Server,
typeof(IPerson).Assembly,
typeof(IOrder).Assembly,
typeof(IInventory).Assembly
);
// Register custom services
builder.Services.AddScoped<IEmailService, EmailService>();
builder.Services.AddScoped<IUniqueNameRule, UniqueNameRule>();
RuleNotAddedException
Thrown when attempting to execute a rule that was never added to the RuleManager.
public class RuleNotAddedException : NeatooException
{
public Type RuleType { get; }
public RuleNotAddedException(Type ruleType)
: base($"Rule '{ruleType.Name}' was not added to RuleManager")
{
RuleType = ruleType;
}
}
Common Cause
Forgetting to add an injected rule to the RuleManager:
public Person(
IEntityBaseServices<Person> services,
IUniqueNameRule uniqueNameRule) : base(services)
{
// Forgot this line!
// RuleManager.AddRule(uniqueNameRule);
}
Prevention
Always add injected rules in the constructor:
public Person(
IEntityBaseServices<Person> services,
IUniqueNameRule uniqueNameRule,
IEmailValidationRule emailRule) : base(services)
{
RuleManager.AddRule(uniqueNameRule);
RuleManager.AddRule(emailRule);
}
PropertyReadOnlyException
Thrown when attempting to set a property that is marked as read-only.
public class PropertyReadOnlyException : NeatooException
{
public string PropertyName { get; }
public PropertyReadOnlyException(string propertyName)
: base($"Property '{propertyName}' is read-only")
{
PropertyName = propertyName;
}
}
When It’s Thrown
When setting a property that has been marked read-only through the property system.
PropertyMissingException
Thrown when attempting to access a property that doesn’t exist on the entity.
public class PropertyMissingException : NeatooException
{
public string PropertyName { get; }
public Type EntityType { get; }
public PropertyMissingException(string propertyName, Type entityType)
: base($"Property '{propertyName}' not found on type '{entityType.Name}'")
{
PropertyName = propertyName;
EntityType = entityType;
}
}
Common Cause
Typo in property name when using string-based property access:
// Wrong - typo in property name
var prop = person["FistName"]; // Should be "FirstName"
// Correct
var prop = person[nameof(person.FirstName)];
Exception Handling Best Practices
1. Prefer Proactive Validation
Check entity state before operations rather than relying on exceptions:
// Good - proactive checking
public async Task SavePerson(IPerson person)
{
await person.WaitForTasks();
if (!person.IsSavable)
{
HandleNotSavable(person);
return;
}
await factory.Save(person);
}
// Less ideal - reactive exception handling
public async Task SavePerson(IPerson person)
{
try
{
await factory.Save(person);
}
catch (SaveOperationException ex)
{
HandleSaveFailure(ex);
}
}
2. Use When Filters for Specific Handling
Use C# exception filters for targeted handling:
try
{
await factory.Save(person);
}
catch (SaveOperationException ex) when (ex.Reason == SaveOperationReason.IsInvalid)
{
// Handle validation errors specifically
}
catch (SaveOperationException ex) when (ex.Reason == SaveOperationReason.IsBusy)
{
// Handle busy state specifically
}
3. Log with Context
Include relevant entity state when logging exceptions:
catch (SaveOperationException ex)
{
logger.LogError(ex,
"Save failed for {EntityType}. Reason: {Reason}, IsValid: {IsValid}, IsBusy: {IsBusy}, IsModified: {IsModified}",
entity.GetType().Name,
ex.Reason,
entity.IsValid,
entity.IsBusy,
entity.IsModified);
}
4. Blazor Component Pattern
Standard pattern for save operations in Blazor:
private async Task Save()
{
try
{
IsSaving = true;
// Wait for async validation
await _person.WaitForTasks();
if (!_person.IsSavable)
{
if (!_person.IsValid)
{
Snackbar.Add("Please fix validation errors", Severity.Warning);
}
return;
}
await _factory.Save(_person);
Snackbar.Add("Saved successfully", Severity.Success);
NavigationManager.NavigateTo("/persons");
}
catch (SaveOperationException ex)
{
Snackbar.Add($"Save failed: {ex.Message}", Severity.Error);
}
catch (HttpRequestException ex)
{
Snackbar.Add("Connection error. Please try again.", Severity.Error);
}
finally
{
IsSaving = false;
}
}
5. TrySave for Authorization-Aware Operations
Use TrySave() when authorization might prevent the operation:
var result = await factory.TrySave(person);
if (result.IsAuthorized)
{
var savedPerson = result.Value;
ShowSuccess("Person saved");
}
else
{
ShowError(result.Message ?? "You don't have permission to save");
}
Diagnostic Helper
Use this helper method to diagnose entity state issues:
public static void DiagnoseEntityState(IEntityBase entity)
{
Console.WriteLine($"Entity: {entity.GetType().Name}");
Console.WriteLine($" IsNew: {entity.IsNew}");
Console.WriteLine($" IsModified: {entity.IsModified}");
Console.WriteLine($" IsSelfModified: {entity.IsSelfModified}");
Console.WriteLine($" IsValid: {entity.IsValid}");
Console.WriteLine($" IsSelfValid: {entity.IsSelfValid}");
Console.WriteLine($" IsBusy: {entity.IsBusy}");
Console.WriteLine($" IsSelfBusy: {entity.IsSelfBusy}");
Console.WriteLine($" IsChild: {entity.IsChild}");
Console.WriteLine($" IsDeleted: {entity.IsDeleted}");
Console.WriteLine($" IsSavable: {entity.IsSavable}");
if (!entity.IsValid)
{
Console.WriteLine(" Validation Errors:");
foreach (var msg in entity.PropertyMessages)
{
Console.WriteLine($" {msg.PropertyName}: {msg.Message}");
}
}
}
Related Topics
- Troubleshooting Guide - Common issues and solutions
- EntityBase Reference - Entity state properties
- Rules Engine Reference - Validation rule errors
- Factory Operations Reference - Save operation details