Table of Contents

Aspecting Abstraction

Namespace: RisingV.Shared.Aspects
Assembly: RisingV.Shared

The Aspect pattern (also called Aspecting) provides a way to attach typed views or behaviors onto an existing domain object (like an ECS Entity, a data DTO, or any class) without modifying that object’s source. An aspect wraps a target instance and exposes a tailored API for cross‑cutting concerns (validation, convenience methods, helper logic, etc.) while preserving the original model’s purity.


1. Core Interfaces

IAspect

A marker interface with no members:

public interface IAspect { }

This non‑generic base allows you to treat all aspects uniformly when you don’t care about the concrete target type.

IAspect<T>

public interface IAspect<T> : IAspect
{
    /// <summary>
    /// The wrapped object this aspect operates on.
    /// </summary>
    T Target { get; }

    /// <summary>
    /// Returns true if the provided instance satisfies the structural and semantic
    /// requirements of this aspect (e.g., required components are present, tags set,
    /// or valid state).
    /// </summary>
    bool Qualifies(T entity);
}
  • Target
    The underlying object (e.g., an Entity or some DTO) that this aspect augments.
  • Qualifies(T entity)
    A guard method that checks whether the passed‐in entity has the correct shape, components, tags, or state needed to create this aspect. For example, a UnitAspect might require that entity.Has<ComponentHealth>() && entity.Has<TagUnit>().

2. Base Support: BaseAspect<T>

Most aspects will inherit from BaseAspect<T>, which implements common logic:

public abstract class BaseAspect<T> : IAspect<T>
{
    protected abstract Logger Log { get; }

    /// <summary>
    /// The wrapped target instance.
    /// </summary>
    public T Target { get; }

    protected BaseAspect(T target)
    {
        // Verify that target is not null
        if (target == null)
            throw new UnqualifiedTargetException(target);

        if (!Qualifies(target))
            throw new UnqualifiedTargetException(target);

        this.Target = target;
    }

    /// <summary>
    /// Derived types override this to define qualification logic.
    /// </summary>
    public abstract bool Qualifies(T entity);

    /// <summary>
    /// Friendly display of the target for logging (can be overridden).
    /// </summary>
    protected virtual string DisplayName(T obj) 
        => obj?.ToString() ?? string.Empty;

    /// <summary>
    /// Additional details about the target for error messages (optional).
    /// </summary>
    protected virtual string ExtraDetails(T obj) 
        => string.Empty;

    // … additional utility methods to throw with context, etc. …
}

Key points:

  • The constructor in BaseAspect<T> calls Qualifies(target) and throws UnqualifiedTargetException if it returns false.
  • Derived aspects implement Qualifies to check for required components, tags, or properties.
  • Use DisplayName and ExtraDetails to improve error/log messages.
  • A Logger field is provided for consistent logging inside aspect methods.

3. Handling Unqualified Targets: UnqualifiedTargetException

public class UnqualifiedTargetException : BaseException
{
    /// <summary>
    /// The actual object that failed qualification.
    /// </summary>
    public object? Target { get; }

    public UnqualifiedTargetException(object? target)
        : base(target == null 
            ? "Target is null" 
            : $"Target {target} is not qualified") 
    {
        Target = target;
    }

    public UnqualifiedTargetException(string message, object? target)
        : base(message)
    {
        Target = target;
    }

    public UnqualifiedTargetException(string message, Exception innerException, object? target)
        : base(message, innerException)
    {
        Target = target;
    }
}
  • Thrown when you attempt to create an aspect around a target that fails Qualifies.
  • Contains the offending Target object for diagnostics.

4. Metadata Support: MetaSerializer

public static class MetaSerializer
{
    // Provides JSON serialization helpers for aspect‐related data.
    // (e.g., generating “aspect metadata” files for documentation or runtime reflection).
    public static string Serialize<TAspect>(TAspect aspect) 
        where TAspect : IAspect 
    {
        // …implementation using reflection attributes and JSON …
    }

    public static TAspect Deserialize<TAspect>(string json)
        where TAspect : IAspect 
    {
        // …reconstruct aspect metadata from JSON, then wrap a target…
    }
}

Although not strictly required for simple aspects, MetaSerializer can be used to:

  • Export aspect specifications (e.g., field names, default values) to JSON.
  • Rehydrate aspects at runtime or for code generation.
  • Combine with source‐generation to produce high‐performance lookup tables.

5. Typical Usage

Below is a step‐by‐step example of creating and using a custom aspect:

Step 1: Define a Domain Type

Assume you have a DOTS Entity that represents an in‑game “Unit”:

public struct ComponentHealth : IComponentData 
{ 
    public int Current; 
}
public struct TagUnit : IComponentData { }

Step 2: Create a UnitAspect

public sealed class UnitAspect : BaseAspect<Entity>
{
    private readonly EntityManager _mgr;
    protected override Logger Log { get; } = Logger.Create<UnitAspect>();

    public UnitAspect(Entity entity, EntityManager manager) : base(entity)
    {
        _mgr = manager;
    }

    public override bool Qualifies(Entity entity)
    {
        // Must have both a Health component and be tagged as a Unit
        return _mgr.HasComponent<ComponentHealth>(entity) 
            && _mgr.HasComponent<TagUnit>(entity);
    }

    /// <summary>
    /// Reduce health by the specified damage amount.
    /// </summary>
    public void TakeDamage(int damage)
    {
        var health = _mgr.GetComponentData<ComponentHealth>(Target);
        health.Current = Math.Max(0, health.Current - damage);
        _mgr.SetComponentData(Target, health);
        Log.Info($"Unit {Target} took {damage} damage, now at {health.Current} HP.");
    }

    /// <summary>
    /// Heal the unit by the specified amount.
    /// </summary>
    public void Heal(int amount)
    {
        var health = _mgr.GetComponentData<ComponentHealth>(Target);
        health.Current += amount;
        _mgr.SetComponentData(Target, health);
        Log.Info($"Unit {Target} healed by {amount}, now at {health.Current} HP.");
    }
}

Step 3: Attach & Use the Aspect

Somewhere in your gameplay code, given an Entity e:

// Attempt to wrap e as a UnitAspect
UnitAspect? unit;
try
{
    unit = new UnitAspect(e, entityManager);
}
catch (UnqualifiedTargetException)
{
    // e is not a valid “Unit”
    unit = null;
}

if (unit != null)
{
    // Safely call aspect methods
    unit.TakeDamage(10);
    if (unit.TargetExists()) // example helper
        unit.Heal(5);
}

Alternatively, you can write an extension method for concise syntax:

public static class EntityExtensions
{
    public static UnitAspect AsUnit(this Entity e, EntityManager mgr, bool strict = true)
    {
        if (!new UnitAspect(e, mgr).Qualifies(e))
        {
            if (strict)
                throw new UnqualifiedTargetException(e);
            else
                return null!;
        }
        return new UnitAspect(e, mgr);
    }
}

// Usage:
var unit = e.AsUnit(entityManager, strict: false);
unit?.TakeDamage(10);

6. Design Principles & Best Practices

  1. Separation of Concerns
    Keep domain data (components, DTOs) free from UI/validation/logging logic. Aspects live outside the core model.

  2. Stateless Aspects
    Aspects should not store mutable state beyond references to the target and required services (EntityManager, other managers, etc.).

  3. Qualifies Method

    • Efficient checks: avoid expensive reflection in Qualifies.
    • Return false quickly if missing a required component or tag.
  4. Throw Early
    The base constructor in BaseAspect<T> immediately throws UnqualifiedTargetException if the target fails qualification. This prevents misuse later.

  5. Logging & Diagnostics

    • Use the built‑in Logger to record aspect‐specific events.
    • Override DisplayName/ExtraDetails in your aspect to provide helpful context in errors.
  6. Composable
    You can wrap multiple aspects around the same target if needed. For example, a single Entity could have both a UnitAspect and a MovementAspect.


7. Troubleshooting

Symptom:
UnqualifiedTargetException: Target Entity(0x1234) is not qualified

Cause:
Your aspect’s Qualifies check returned false (missing component/tag).

Fix:

  • Ensure the Entity has all required components before creating the aspect.
  • If dynamic, add debug logs in Qualifies to see which condition failed.

TL;DR

  • IAspect<T> defines a typed wrapper around a target object, with a Qualifies guard.
  • BaseAspect<T> provides a constructor that enforces qualification and supplies logging helpers.
  • UnqualifiedTargetException is thrown if the target doesn’t meet the aspect’s requirements.
  • Use aspects to attach cross‑cutting behavior (validation, helper methods, etc.) without modifying the original data model.
  • Implement each aspect by inheriting BaseAspect<T> and overriding Qualifies(T) plus any additional methods you need.