-
Notifications
You must be signed in to change notification settings - Fork 5
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.
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. |
Services/Operations/SideEffects/OperationSideEffect.cs is the abstract base every concrete SideEffect derives from. It:
- Implements
IOperationSideEffect.RegistryKeyastypeof(T).Name(OperationSideEffect.cs:76-82), which is how the processor finds the right SideEffect for an incomingThing. - 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
DeferPropertyValidationvirtual 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:
-
Read from
rawUpdateInfofor updates. That dictionary contains only the fields the client posted. If you read fromthing.SuperCategoryyou can't tell whether the property was actually changed in this request or merely echoed back. -
Throw a typed exception.
AcyclicValidationException,Cdp4ModelValidationException,Cdp4InvalidContainerException(all inCometServer.Exceptions) are translated by the Carter handler into the Annex C error envelope. -
Property-inject the services and DAOs you need. SideEffects are wired via
.PropertiesAutowired(PropertyWiringOptions.AllowCircularDependencies), so adding a new dependency is apublic IXxxService XxxService { get; set; }away.
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
AndExpressionrunsAndExpressionSideEffectand thenBooleanExpressionSideEffectbecauseBooleanExpressionis its base type. There is no explicit registration step for the polymorphism — it follows fromIMetaInfoProvider. -
Order is hierarchy-derived. Concrete-type hooks fire before base-type hooks. If you write a SideEffect for
ElementUsageyou cannot rely on theElementBaseSideEffecthaving already run; the reverse is true. -
At most one SideEffect per type. Recall
RegistryKey = typeof(T).Nameand that the registration loop callsAdd(not[]=) — registering two SideEffects for the same type fails fast at startup.
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:
-
Decode the proposed new terms from
rawUpdateInfointo aList<Guid>— handling the four shapesTermcan take (AndExpression.Term : List<Guid>,NotExpression.Term : Guid, etc.). -
Reject self-reference.
if (termsId.Contains(thing.Iid)) throw new AcyclicValidationException(...); -
Reject cycles. Pull every
BooleanExpressionfrom the sameParametricConstraintviaIParametricConstraintService.GetDeepAsync, then for each proposed term walk its existing transitiveTermgraph; throw ifthing.Iidappears 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.
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.
-
Create
Implementation/<ThingName>SideEffect.csas asealedclass deriving fromOperationSideEffect<TThing>. - Override only the hooks you need. Default no-ops handle the rest.
- Property-inject collaborators via public auto-properties — they will be filled by Autofac.
-
For updates, branch on
rawUpdateInfo.TryGetValue("PropertyName", …). Do not validate againstthing.Xfor properties the client did not post. -
Throw
AcyclicValidationException,Cdp4ModelValidationException, or anotherCometServer.Exceptionsexception for violations. The Carter response pipeline maps these to the right HTTP error. -
Use the supplied
transactionandpartition. Never open a new connection. -
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
DeferPropertyValidationto return the property names. -
Tests live in the integration test project. A SideEffect is verified by issuing a
PostOperationover 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.
-
Mutating
rawUpdateInfofrom aBefore…hook is supported (PersonSideEffectdoes 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 aBefore…validation.After…hooks fire after the row has been written; throwing rolls the transaction back, but the cost has already been paid. -
Reading
thing.Propertyto decide whether to act on an update. Every property arrives populated; the change signal israwUpdateInfo, notthing.
- The full list of cycle-prevention SideEffects: Cyclic Cycles.
- The DAO pipeline these hooks run alongside: ORM Dao.
- The operation processor that drives the dispatch: CometServer Services.
- The HTTP entry points that initiate a CUD operation: CometServer Modules.
copyright @ Starion Group S.A.