Component Validation
Component validation enforces constraints between components at runtime, catching invalid entity configurations early. The system supports both attribute-based declarative constraints and custom validation logic.
Attribute-Based Validation
RequiresComponent
Declare that a component requires another component to be present:
[Component]
[RequiresComponent(typeof(Transform))]
public partial struct RigidBody
{
public float Mass;
public float Drag;
}
When validation is enabled, adding RigidBody to an entity without Transform throws ComponentValidationException:
// This throws - Transform is required
var entity = world.Spawn()
.With(new RigidBody { Mass = 1.0f })
.Build();
// This works
var entity = world.Spawn()
.With(new Transform())
.With(new RigidBody { Mass = 1.0f })
.Build();
Multiple Requirements
Apply multiple RequiresComponent attributes for multiple dependencies:
[Component]
[RequiresComponent(typeof(Transform))]
[RequiresComponent(typeof(Renderable))]
public partial struct Sprite
{
public string TextureId;
public int Layer;
}
ConflictsWith
Declare that two components cannot coexist on the same entity:
[Component]
[ConflictsWith(typeof(DynamicBody))]
public partial struct StaticBody
{
public bool IsKinematic;
}
[Component]
[ConflictsWith(typeof(StaticBody))]
public partial struct DynamicBody
{
public float Mass;
public float Drag;
}
Adding both components to the same entity throws:
// This throws - StaticBody conflicts with DynamicBody
var entity = world.Spawn()
.With(new StaticBody())
.With(new DynamicBody())
.Build();
Conflicts are bidirectional - if A conflicts with B, then B implicitly conflicts with A.
Validation Modes
Control when validation runs via World.ValidationMode:
// Always validate (default)
world.ValidationMode = ValidationMode.Enabled;
// Never validate (maximum performance)
world.ValidationMode = ValidationMode.Disabled;
// Only validate in DEBUG builds
world.ValidationMode = ValidationMode.DebugOnly;
Choosing a Mode
| Mode | When to Use |
|---|---|
Enabled |
Development, testing, or when correctness is critical |
Disabled |
Production builds where performance is critical and code is trusted |
DebugOnly |
Balance between safety during development and performance in production |
// Common pattern: debug-only validation
#if DEBUG
world.ValidationMode = ValidationMode.Enabled;
#else
world.ValidationMode = ValidationMode.Disabled;
#endif
Custom Validators
Register custom validation logic for complex constraints:
// Validate Health component values
world.RegisterValidator<Health>((world, entity, health) =>
health.Current >= 0 &&
health.Current <= health.Max &&
health.Max > 0);
// This throws ComponentValidationException
world.Add(entity, new Health { Current = 150, Max = 100 });
Validator Parameters
Custom validators receive:
world- The world instanceentity- The entity receiving the componentcomponent- The component data being added
Return true if validation passes, false to throw an exception.
Cross-Component Validation
Custom validators can check other components on the entity:
// Weapon requires enough strength to wield
world.RegisterValidator<Weapon>((world, entity, weapon) =>
{
if (!world.Has<Stats>(entity))
{
return true; // Let RequiresComponent handle this
}
ref readonly var stats = ref world.Get<Stats>(entity);
return stats.Strength >= weapon.RequiredStrength;
});
Removing Validators
bool removed = world.UnregisterValidator<Health>();
When Validation Runs
Validation occurs:
- During
EntityBuilder.Build()- All components are validated together - During
World.Add<T>()- When adding to an existing entity
// Validated at Build()
var entity = world.Spawn()
.With(new Transform())
.With(new RigidBody())
.Build();
// Validated immediately
world.Add(entity, new Sprite { TextureId = "player.png" });
Handling Validation Errors
Catch ComponentValidationException to handle validation failures:
try
{
world.Add(entity, new RigidBody { Mass = 1.0f });
}
catch (ComponentValidationException ex)
{
Console.WriteLine($"Validation failed for {ex.ComponentType.Name}");
Console.WriteLine($"Entity: {ex.Entity}");
Console.WriteLine($"Reason: {ex.Message}");
}
Use Cases
Physics System Constraints
// Colliders require Transform
[Component]
[RequiresComponent(typeof(Transform))]
public partial struct BoxCollider
{
public float Width;
public float Height;
}
// RigidBody requires Collider
[Component]
[RequiresComponent(typeof(BoxCollider))]
public partial struct RigidBody
{
public float Mass;
}
// Static and Dynamic bodies are mutually exclusive
[Component]
[ConflictsWith(typeof(DynamicBody))]
public partial struct StaticBody { }
[Component]
[ConflictsWith(typeof(StaticBody))]
public partial struct DynamicBody
{
public float Mass;
}
Rendering Pipeline
[Component]
[RequiresComponent(typeof(Transform))]
public partial struct Renderable
{
public int Layer;
}
[Component]
[RequiresComponent(typeof(Renderable))]
public partial struct Sprite
{
public string TextureId;
}
[Component]
[RequiresComponent(typeof(Renderable))]
[ConflictsWith(typeof(Sprite))]
public partial struct Model3D
{
public string ModelPath;
}
Game Logic Validation
// Custom validator for inventory slots
world.RegisterValidator<Inventory>((world, entity, inventory) =>
inventory.Slots > 0 && inventory.Slots <= 100);
// Custom validator for experience/level consistency
world.RegisterValidator<Experience>((world, entity, exp) =>
{
if (!world.Has<Level>(entity))
{
return true;
}
ref readonly var level = ref world.Get<Level>(entity);
return exp.Total >= GetExpForLevel(level.Current);
});
Performance Considerations
- Attribute scanning: Component constraints are cached after first access
- Validation overhead: Adds checking during component addition
- Production builds: Use
ValidationMode.Disabledfor maximum performance
The source generator creates metadata for attribute-based constraints, avoiding reflection in hot paths when generated code is available.
Best Practices
- Use attributes for structural constraints - Dependencies between component types
- Use custom validators for data constraints - Value range checks, cross-component logic
- Keep validators fast - They run on every component add
- Document constraints - Make dependencies clear in code comments
- Test validation paths - Ensure constraints catch invalid configurations
/// <summary>
/// Sprite rendering component.
/// Requires <see cref="Transform"/> and <see cref="Renderable"/>.
/// </summary>
[Component]
[RequiresComponent(typeof(Transform))]
[RequiresComponent(typeof(Renderable))]
public partial struct Sprite
{
public string TextureId;
}