Skip to content

CometServer SideEffects

samatstarion edited this page May 4, 2026 · 1 revision

CometServer SideEffects

Sub-page of CometServer. Read that and CometServer Services first.

A SideEffect is the per-Thing lifecycle hook the IOperationProcessor runs around every Create/Update/Delete during a POST. SideEffects are where ECSS-E-TM-10-25 Annex A invariants that cannot be expressed as foreign keys live: acyclic super-categories, non-self-referential terms, ordering rules between ParameterValueSets and their state list, and so on. They run inside the same NpgsqlTransaction as the data write, so any throw rolls the whole operation back.

The implementations live under CometServer/Services/Operations/SideEffects/Implementation/.

The full list of cycle-prevention SideEffects is enumerated in Cyclic Cycles.

The contract

Defined across two files:

Services/Operations/SideEffects/IOperationSideEffectFunctions.cs declares the six lifecycle methods. Verbatim:

public interface IOperationSideEffectFunctions
{
    bool ValidateProperty(Thing thing, string propertyName);

    Task<bool> BeforeCreateAsync(Thing thing, Thing container,
        NpgsqlTransaction transaction, string partition, ISecurityContext securityContext);

    Task AfterCreateAsync(Thing thing, Thing container, Thing originalThing,
        NpgsqlTransaction transaction, string partition, ISecurityContext securityContext);

    Task BeforeUpdateAsync(Thing thing, Thing container,
        NpgsqlTransaction transaction, string partition, ISecurityContext securityContext,
        ClasslessDTO rawUpdateInfo);

    Task AfterUpdateAsync(Thing thing, Thing container, Thing originalThing,
        NpgsqlTransaction transaction, string partition, ISecurityContext securityContext);

    Task BeforeDeleteAsync(Thing thing, Thing container,
        NpgsqlTransaction transaction, string partition, ISecurityContext securityContext);

    Task AfterDeleteAsync(Thing thing, Thing container, Thing originalThing,
        NpgsqlTransaction transaction, string partition, ISecurityContext securityContext);
}

Services/Operations/SideEffects/IOperationSideEffect.cs adds the registry key:

public interface IOperationSideEffect : IOperationSideEffectFunctions
{
    string RegistryKey { get; }
}

What the parameters mean:

Parameter Used for
thing The Thing being created / updated / deleted.
container Its parent in the containment tree (e.g. the RequirementsSpecification that contains a RequirementsGroup).
originalThing The pre-operation state. Used by After… hooks that want to diff old vs. new.
transaction The NpgsqlTransaction for the entire POST. SideEffects must use this — opening a new transaction would race the operation.
partition The schema name (SiteDirectory, EngineeringModel_<iid>, Iteration_<iid>).
securityContext The cached read/write decisions for the parent (see [[CometServer Services
rawUpdateInfo (Update only.) ClasslessDTO containing only the properties the client actually sent. SideEffects test for the presence of a property here, not against the resolved thing.
Return: BeforeCreateAsync : Task<bool> false cancels the create; the operation processor skips it without rolling back the transaction. The other hooks have no return value — to refuse, throw an exception.

OperationSideEffect<T>

Services/Operations/SideEffects/OperationSideEffect.cs is the abstract base every concrete SideEffect derives from. It:

  • Implements IOperationSideEffect.RegistryKey as typeof(T).Name (OperationSideEffect.cs:76-82), which is how the processor finds the right SideEffect for an incoming Thing.
  • Provides default no-op implementations of every hook so subclasses only override what they need.
  • Wires up generic-typed overloads (BeforeCreateAsync(T thing, …)) to the polymorphic interface (BeforeCreateAsync(Thing thing, …)) via boilerplate boxing in the same file.
  • Exposes a DeferPropertyValidation virtual property (OperationSideEffect.cs:87) that lets a SideEffect skip standard property validation for properties it intends to validate itself.
  • Property-injects IRequestUtils, IPermissionService, ICdp4TransactionManager, IOrganizationalParticipationResolverService.

The standard pattern for a concrete SideEffect, taken from Implementation/CategorySideEffect.cs:

public sealed class CategorySideEffect : OperationSideEffect<Category>
{
    public ISiteReferenceDataLibraryService SiteReferenceDataLibraryService { get; set; }
    public ICategoryService CategoryService { get; set; }

    public override async Task BeforeUpdateAsync(
        Category thing, Thing container, NpgsqlTransaction transaction,
        string partition, ISecurityContext securityContext, ClasslessDTO rawUpdateInfo)
    {
        if (rawUpdateInfo.TryGetValue("SuperCategory", out var value))
        {
            var superCategoriesId = value as IEnumerable<Guid>;

            if (superCategoriesId.Contains(thing.Iid))
            {
                throw new AcyclicValidationException(
                    $"Category {thing.Name} {thing.Iid} cannot have itself as a SuperCategory");
            }

            // … walks the chain of RDLs to verify the proposed super categories
            // are reachable and that the new edge does not introduce a cycle …
        }
    }
}

Three things are characteristic of every SideEffect:

  1. Read from rawUpdateInfo for updates. That dictionary contains only the fields the client posted. If you read from thing.SuperCategory you can't tell whether the property was actually changed in this request or merely echoed back.
  2. Throw a typed exception. AcyclicValidationException, Cdp4ModelValidationException, Cdp4InvalidContainerException (all in CometServer.Exceptions) are translated by the Carter handler into the Annex C error envelope.
  3. Property-inject the services and DAOs you need. SideEffects are wired via .PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies), so adding a new dependency is a public IXxxService XxxService { get; set; } away.

Registration and dispatch

In Startup.cs the assembly is scanned for every IOperationSideEffect:

builder.RegisterAssemblyTypes(typeof(IOperationSideEffect).Assembly)
    .Where(x => typeof(IOperationSideEffect).IsAssignableFrom(x))
    .AsImplementedInterfaces()
    .PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies)
    .InstancePerLifetimeScope();

Autofac then injects the full IEnumerable<IOperationSideEffect> into OperationSideEffectProcessor, whose constructor builds a name-keyed map (Services/Operations/SideEffects/OperationSideEffectProcessor.cs):

public OperationSideEffectProcessor(IEnumerable<IOperationSideEffect> operationSideEffects)
{
    foreach (var operationSideEffect in operationSideEffects)
    {
        this.operationSideEffectMap.Add(operationSideEffect.RegistryKey, operationSideEffect);
    }
}

When the operation processor is about to create / update / delete a Thing, it calls into the IOperationSideEffectProcessor via the matching method:

public async Task<bool> BeforeCreateAsync(Thing thing, Thing container, NpgsqlTransaction transaction,
    string partition, ISecurityContext securityContext)
{
    var sideEffects = this.SideEffects(thing).ToArray();

    foreach (var sideEffect in sideEffects)
    {
        if (!await sideEffect.BeforeCreateAsync(thing, container, transaction, partition, securityContext))
        {
            return false;
        }
    }

    return true;
}

The dispatch helper SideEffects(thing) (OperationSideEffectProcessor.cs) walks the class hierarchy of the Thing from the concrete type up to Thing itself, yielding each registered SideEffect along the way:

private IEnumerable<IOperationSideEffect> SideEffects(Thing thing)
{
    var type = GetTypeName(thing);

    do
    {
        if (this.IsSideEffectRegistered(type))
        {
            yield return this.GetOperationSideEffect(type);
        }

        var metaInfo = this.MetaInfoProvider.GetMetaInfo(type);
        type = metaInfo.BaseType;
    } while (!string.IsNullOrEmpty(type));
}

The consequences:

  • Inheritance is implicit. Updating an AndExpression runs AndExpressionSideEffect and then BooleanExpressionSideEffect because BooleanExpression is its base type. There is no explicit registration step for the polymorphism — it follows from IMetaInfoProvider.
  • Order is hierarchy-derived. Concrete-type hooks fire before base-type hooks. If you write a SideEffect for ElementUsage you cannot rely on the ElementBaseSideEffect having already run; the reverse is true.
  • At most one SideEffect per type. Recall RegistryKey = typeof(T).Name and that the registration loop calls Add (not []=) — registering two SideEffects for the same type fails fast at startup.

A worked example: BooleanExpressionSideEffect

Implementation/BooleanExpressionSideEffect.cs is a good example because it is itself an abstract subclass of OperationSideEffect<T> from which AndExpressionSideEffect, OrExpressionSideEffect, ExclusiveOrExpressionSideEffect and NotExpressionSideEffect derive. Its BeforeUpdateAsync (lines 80-156) does three things when the client touches the Term property:

  1. Decode the proposed new terms from rawUpdateInfo into a List<Guid> — handling the four shapes Term can take (AndExpression.Term : List<Guid>, NotExpression.Term : Guid, etc.).
  2. Reject self-reference. if (termsId.Contains(thing.Iid)) throw new AcyclicValidationException(...);
  3. Reject cycles. Pull every BooleanExpression from the same ParametricConstraint via IParametricConstraintService.GetDeepAsync, then for each proposed term walk its existing transitive Term graph; throw if thing.Iid appears anywhere in that graph.

This is the canonical shape for a graph-invariant SideEffect: read from rawUpdateInfo, fetch the relevant slice from the database via a Service that wraps the DAOs, do the graph walk in memory, throw on violation.

Pre-existing cycle protections

The full inventory of Thing types whose cycles are checked in SideEffects, and which SideEffect to look at for each, is in Cyclic Cycles. Use that page when you need to understand or extend the cycle protection — it links to every relevant Implementation/ file.

Adding a new SideEffect

  1. Create Implementation/<ThingName>SideEffect.cs as a sealed class deriving from OperationSideEffect<TThing>.
  2. Override only the hooks you need. Default no-ops handle the rest.
  3. Property-inject collaborators via public auto-properties — they will be filled by Autofac.
  4. For updates, branch on rawUpdateInfo.TryGetValue("PropertyName", …). Do not validate against thing.X for properties the client did not post.
  5. Throw AcyclicValidationException, Cdp4ModelValidationException, or another CometServer.Exceptions exception for violations. The Carter response pipeline maps these to the right HTTP error.
  6. Use the supplied transaction and partition. Never open a new connection.
  7. If you need to defer or skip a property's standard validation (for instance because you intend to validate it yourself in a way the operation processor cannot), override DeferPropertyValidation to return the property names.
  8. Tests live in the integration test project. A SideEffect is verified by issuing a PostOperation over HTTP that exercises the rule; see Integration Test Suite.

No registration step is needed — the assembly scan in Startup.cs picks the new class up automatically and RegistryKey (typeof(T).Name) determines which Things it serves.

Anti-patterns to avoid

  • Mutating rawUpdateInfo from a Before… hook is supported (PersonSideEffect does it, see the worked example in ORM Dao) but only when you know exactly what the operation processor will do with the modified dictionary downstream. Treat it as an explicit, advanced extension point.
  • Querying outside the supplied transaction can deadlock. The operation processor holds row locks on every container in the chain.
  • Doing work inside After… hooks that should have been a Before… validation. After… hooks fire after the row has been written; throwing rolls the transaction back, but the cost has already been paid.
  • Reading thing.Property to decide whether to act on an update. Every property arrives populated; the change signal is rawUpdateInfo, not thing.

See also

Clone this wiki locally