Extension Seam Reference
This is the source-verified, exhaustive reference for the Core-component extension seams — exact signatures, the types each seam hands you, the enums, and the schema shapes. It is the companion to the narrative Extending Core Components guide; read that first for when and why. Read this when you need the precise shape — and when an LLM or a code generator needs an authoritative contract it can rely on.
Every signature, property, and enum value on this page was copied verbatim from the 7.0 BFF source (
MediaiBox.Cms.FrontEnd.*). Where a name differs from what you might guess, that is called out — those are the traps. Namespaces are given so you know whichusingto add.
1. Seam signature reference
Every seam is a protected virtual method (or, for ShouldHideComponent, a
public virtual interface method) with a no-op / identity default. "Stage"
governs what a thrown exception does — see the
Error & resilience contract.
| Seam | Archetypes | Exact signature (verbatim) | Stage | Fires at |
|---|---|---|---|---|
TransformData |
List · Related · Ordered | protected virtual void TransformData(ListV2ViewData data, ListViewData viewData) |
Render | end of MapData, before return (and on the live FetchExternalData self-fetch path) |
TransformData |
Form | protected virtual void TransformData(FormV2ViewData data, FormViewData viewData) |
Render | end of MapData |
TransformData |
ContentCriteria | protected virtual void TransformData(ContentCriteriaV2ViewData data, ContentCriteriaViewData viewData) |
Render | end of MapData |
ContributeSchema |
List | protected virtual void ContributeSchema(ListSchema schema) |
Render | end of MapSchema, before return |
ContributeSchema |
Related · Ordered | protected virtual void ContributeSchema(RelatedListSchema schema) |
Render | end of MapSchema (Ordered uses RelatedListSchema, not a dedicated type) |
ContributeSchema |
Form | protected virtual void ContributeSchema(FormSchema schema) |
Render | end of MapSchema |
ContributeSchema |
ContentCriteria | protected virtual void ContributeSchema(ContentCriteriaSchema schema) |
Render | end of MapSchema |
ShouldHideComponent |
all 5 | public virtual Task<bool> ShouldHideComponent(ComponentContext context, CancellationToken cancellationToken) |
Render | ComponentEvaluator, before render, once per component |
OnPermissionResolved |
all 5 | protected virtual void OnPermissionResolved(ComponentPermission permission, List<ResponseItemContent> responses) |
Render | end of GetPermissions (Form: not on the BulkEditPermissionConflictException throw path) |
OnSearchCriteriaBuilt |
List only | protected virtual void OnSearchCriteriaBuilt(SearchCriteria criteria) |
Render | in GetSecondaryInformationRequests and GetRefreshRenderData, on the resolved criteria |
OnRelatedSaveReconciled |
Related · Ordered | protected virtual void OnRelatedSaveReconciled(PersistenceComponentSaveRelatedEntitiesData saveData, PostbackComponentRequest request) |
Save (before persist) | in Save, after reconcile; not on the HideBulkEdit no-op path |
OnFormSaveBuilt |
Form | protected virtual void OnFormSaveBuilt(PersistenceComponentSaveSingleEntityData saveData, PostbackComponentRequest request) |
Save (before persist) | in Save, after the save model is built; not on the HideBulkEdit no-op path |
OnContentCriteriaSaveBuilt |
ContentCriteria | protected virtual void OnContentCriteriaSaveBuilt(IPersistenceSaveData saveData, PostbackComponentRequest request) |
Save (before persist) | in Save, on both the recreate and new-from-item paths (cast saveData to the concrete type) |
OnAfterSaveResponseBuilt |
Related · Form · Ordered · ContentCriteria | protected virtual void OnAfterSaveResponseBuilt(PostbackComponentResponse response, List<PersistenceComponentResult> results) |
Save (after persist) | end of OnAfterSave |
OnDeleteBuilt |
Related · Form · Ordered · ContentCriteria | protected virtual void OnDeleteBuilt(IPersistenceDeleteData deleteData, PostbackComponentRequest request) |
Delete (before persist) | in Delete, on every delete-model path (the MissingId throw and HideBulkEdit no-op are excluded) |
OnAfterDeleteResponseBuilt |
Related · Form · Ordered · ContentCriteria | protected virtual void OnAfterDeleteResponseBuilt(PostbackComponentResponse response, List<PersistenceComponentResult> results) |
Delete (after persist) | end of OnAfterDelete |
ProvidesExternalData |
List · Related · Ordered | protected virtual bool ProvidesExternalData => false; |
— | opt-in gate read by the render/refresh paths |
OnFetchData |
List · Related · Ordered | protected virtual Task<ListV2ViewData> OnFetchData(FetchContext context, CancellationToken cancellationToken) |
Render | replaces the MibApi fetch when ProvidesExternalData is true |
List has no entity save/delete of its own (it is a read surface), so the save/delete seams do not apply to it. Form and ContentCriteria do not provide external data (
OnFetchDatais List/Related/Ordered only).
Collaborators exposed on the base classes
An override can reach these without re-resolving anything (types verbatim):
| Member | Type | Notes |
|---|---|---|
ApiClient |
IMibApiClientLibrary |
the MIB API client (protected) |
WorkflowFactory |
IWorkflowFactory |
list/form/meta/preference workflows (protected; on List the field is _workflowFactory) |
Configuration |
ListComponentConfiguration / RelatedListComponentConfiguration / OrderedRelatedListComponentConfiguration / FormComponentConfiguration / ContentCriteriaComponentConfiguration |
the typed config row (public) |
Context |
ContextViewData |
IDs, User, CopiedFrom, RestoreState, … (public) |
HttpContextAccessor |
IHttpContextAccessor |
on List / Related (protected) |
2. FetchContext (the OnFetchData request)
MediaiBox.Cms.FrontEnd.Model.UI.List.FetchContext — a sealed class, all
properties { get; init; }.
| Property | Type | Meaning |
|---|---|---|
ParentIds |
IReadOnlyList<int> |
parent entity id(s) for a related/ordered list; empty for a top-level list |
Page |
int |
zero-based page index |
Limit |
int |
rows per page |
Order |
string |
sort, e.g. "DATEINS,desc"; null when unsorted |
Filter |
string |
free-text / global filter — or the raw value of a structured filter |
FilterCondition |
Condition |
structured field + operator + value filter; read this for id < 2 vs name == 2 |
MediaType |
string |
the component's configured media type |
Population by archetype (what is actually filled when OnFetchData runs):
| Field | List | SimpleRelated / Ordered |
|---|---|---|
ParentIds |
empty | parent id(s) — always |
MediaType |
✅ always | ✅ always |
Page / Limit |
✅ from SearchCriteria |
✅ from the request |
Order |
✅ from SearchCriteria.Order |
❌ never (the related criteria has no sort field) |
Filter (raw) |
✅ from SearchCriteria.GlobalFilter |
the FilterCondition.Value, when present |
FilterCondition |
— (List uses free-text Filter) |
✅ on the async-data endpoint; ❌ on async-refresh (no filter sent) |
3. Type glossary — the models a seam hands you
Full public shapes, verbatim. Namespace precedes each type.
Render data
MediaiBox.Cms.FrontEnd.Model.UI.List.ListV2ViewData
Items : List<ItemV2ViewData> · Total : int · Charts : List<DashboardWidgetData> · AddedIds : int[] · SaveOptions : SaveOptions
MediaiBox.Cms.FrontEnd.Model.UI.List.ItemV2ViewData : ModelObject
Id : int · Fields : Dictionary<string, object> · Permissions : Permissions · Etag : string (JSON checksum) · Title : string
MediaiBox.Cms.FrontEnd.Model.UI.Form.FormV2ViewData : ItemV2ViewData
adds CreatedAt : string. Form field values live in the inherited
Fields dictionary (values are FormFieldV2ViewData subclasses:
SelectableFieldViewData, KeyValueViewData, ImageFieldViewData,
UserFieldViewData, LinkFieldViewData). There is no per-field wrapper on
the view data itself.
MediaiBox.Cms.FrontEnd.Model.UI.ContentCriteria.ContentCriteriaV2ViewData
Id : int · Operation : string · Groups : IEnumerable<ContentCriteriaV2GroupViewData> · Order : ContentCriteriaV2OrderViewData · Highlights : IEnumerable<ItemV2ViewData>
ContentCriteriaV2GroupViewData→Operation : string·Conditions : IEnumerable<ContentCriteriaV2GroupFilterViewData>ContentCriteriaV2GroupFilterViewData→FilterId : string·Value : object·Id : intContentCriteriaV2OrderViewData→Primary : ContentCriteriaV2OrderOptionViewData·Secondary : …·MaxItemCount : intContentCriteriaV2OrderOptionViewData(astruct) →Value : string·HasPreview : bool
MediaiBox.Cms.FrontEnd.Model.Dao.Permissions (the Permissions on an item)
Create : bool · Write : bool (JSON edit) · Delete : bool (JSON remove) · Read : bool · Preview : bool? · Download : bool? · QuickEdit : bool
Permissions
MediaiBox.Cms.FrontEnd.Model.UI.Component.ComponentPermission
Read : PermissionType · Create : PermissionType · Write : PermissionType · Delete : PermissionType (ctor defaults all to Deny)
Save / delete models
MediaiBox.Cms.FrontEnd.Model.Dao.Persistence.PersistenceComponentSaveSingleEntityData : IPersistenceSaveData (Form)
IDs : IEnumerable<int> · Fields : Dictionary<string, object> · Relateds : Dictionary<string, List<int>> · Etags : Dictionary<int, string> · Files : EntityFileLinkCollection · MediaType/TemplateComponentKey/ParentTemplateComponentKey/PageKey : string · ApiClient : IMibApiClientLibrary · Context : BaseContext · BatchReference : string (computed; throws if blank)
MediaiBox.Cms.FrontEnd.Model.Dao.Persistence.PersistenceComponentSaveRelatedEntitiesData (Related / Ordered)
ParentIDs : List<int> · ParentBatchReference : string · AddedRelateds : List<int> · RemovedRelateds : List<int> · OrderedRelateds : List<string> · CurrentItems : List<dynamic> · BulkMode : PersistenceBulkMode · ParentRelateds : Dictionary<int, List<int>> (+ inherited Etags, MediaType, TemplateComponentKey, …)
MediaiBox.Cms.FrontEnd.Model.Dao.Persistence.IPersistenceDeleteData is an empty marker (: IPersistenceData). The common concrete type is PersistenceComponentDeleteData:
IDs : IEnumerable<int> · Etags : Dictionary<int, string> · MediaType/TemplateComponentKey/ParentTemplateComponentKey/PageKey : string · ApiClient : IMibApiClientLibrary
MediaiBox.Cms.FrontEnd.Model.UI.Postback.PostbackComponentResponse : ModelObject
TemplateComponentKey : string · Status : PostbackStatus · ErrorMessage : string · JsonData : string · EtagData : string
MediaiBox.Cms.FrontEnd.Model.Dao.Persistence.PersistenceComponentResult : ModelObject
ResponseIdentifier : string · Status : PostbackStatus · Response : IPersistenceResponse · IsThereAnErrorInSomeOtherComponent : bool
Context, criteria, identity
MediaiBox.Cms.FrontEnd.Model.Mvc.UI.Component.ComponentContext — the type
ShouldHideComponent receives. Single member: ViewData : object (the
component's built view data). It does not carry IDs/User — for those read the
component's own Context. (Note: a different, unrelated ComponentContext
lives under …Model.UI.Context.Components.Context — not this one.)
MediaiBox.Cms.FrontEnd.Model.UI.Context.ContextViewData : BaseContext — the
component's Context property.
IDs : IEnumerable<int> · User : UserViewData · CopiedFrom : int · IgnoreRelateds : bool · ReturnUrl : string · RestoreState : bool · PreviousPageKey : string · IsCopyFlow : bool (=> CopiedFrom > 0)
MediaiBox.Cms.FrontEnd.Model.Dao.Item.SearchCriteria (the OnSearchCriteriaBuilt argument) — selected members:
Page : int · Limit : int · MediaType : string · Order : string · GlobalFilter : string · GlobalFilters : Condition[] · Conditions : Condition[] · DisplayFields : string[] · IsAdvancedSearch : bool (+ more)
MediaiBox.Cms.FrontEnd.Model.Dao.Item.Condition (the FetchContext.FilterCondition and SearchCriteria.Conditions element)
MediaType : string · Field : string · Operator : string (declared as @Operator) · Value : string · Clause : string (default "Or") · LeftSearchClause : Condition[] · RightSearchClause : Condition[]
ResponseItemContent (the OnPermissionResolved responses element) lives in
the sibling API-client assembly: MediaiBox.Cms.Api.Client.Model.ResponseItemContent : ResponseContent
(Identifier : string, StatusCode : HttpStatusCode, RequestId : string, Container : PayloadResponse).
4. Enum reference (exact members + values)
// MediaiBox.Cms.FrontEnd.Model.UI.Component.PermissionType
enum PermissionType { Deny = -1, DontCare = 1, Allow = 2 } // NOTE: no 0; Deny always wins, DontCare defers, Allow grants
// MediaiBox.Cms.FrontEnd.Model.UI.Postback.PostbackStatus
enum PostbackStatus {
Success = 0, SuccessNoContent = 1, Error = 2, NoOperation = 3,
ValidationException = 4, ErrorHandled = 5, InvalidEtag = 6,
SuccessEtag = 7, SuccessWithWarning = 8
}
Building-block enums (namespace MediaiBox.Cms.FrontEnd.Server.Component.Extensions):
enum BulkMode { None, Add, Remove, Clear, Replace } // BulkReconciler
enum SaveAtomicityMode { BestEffort, FailFastStop, Compensating } // SaveTransactionContext
enum SaveOutcomeStatus { Rejected, Committed, PartialCommittedSideEffectFailed }
enum PostSaveStatus { Success, SuccessWithWarning, Error } // PostSaveResult
enum RelatedCopyType { DoNotCopy, ShallowCopy, DeepCopy } // LifecycleContext
enum ComponentRenderOutcome { Rendered, Hidden, Degraded, Errored } // RenderResult
enum HttpVerb { /* ActionPipeline */ }
enum ActionResponseKind { /* ActionPipeline */ }
5. Schema type reference
What ContributeSchema hands you. Override-relevant properties are bold.
MediaiBox.Cms.FrontEnd.Model.UI.List.ListSchema : IComponentSchema
Columns : List<ColumnV2ViewData> · Filters : List<MediaTypeFilter> · FormPageKey : string · Configuration : object · MediaType : string · Name : string (this is the list's title) · Charts : DashboardSchema
ListSchemahas noParentField(that's onRelatedListSchema) and its title isName, notTitle.
MediaiBox.Cms.FrontEnd.Model.UI.List.RelatedListSchema : IComponentSchema
Columns : List<ColumnV2ViewData> · Filters : List<MediaTypeFilter> · FormPageKey : string · Configuration : object · ParentField : string · MediaType : string · Title : string (internal setter — see trap) · Info : string (internal setter)
Trap:
RelatedListSchema.Title/Infohaveinternalsetters — code in a customer assembly cannot assign them. Set the title through configuration, not the schema. (Used by both SimpleRelatedList and OrderedRelatedList.)
MediaiBox.Cms.FrontEnd.Model.UI.Form.FormSchema : IComponentSchema
Fields : List<FormFieldSchema> · Title : string · Configuration : FormSchemaConfiguration (strongly typed, not object) · MediaType : string
MediaiBox.Cms.FrontEnd.Model.UI.ContentCriteria.ContentCriteriaSchema : IComponentSchema
Columns : IEnumerable<ContentCriteriaColumnV2ViewData> (element type is ContentCriteriaColumnV2ViewData, not ColumnV2ViewData) · Filters : IEnumerable<MediaTypeFilter> · Fields : IEnumerable<ContentCriteriaField> · Order : IEnumerable<ContentCriteriaOrderOption> · Title/Info : string · Threshold/MaxFilters/MaxGroups : int · FormPageKey : string
ContentCriteriaSchemahas noConfigurationand noParentField.
MediaiBox.Cms.FrontEnd.Model.UI.List.ColumnV2ViewData (a list column)
DataIndex : string (the match key) · Title : string · Hidden : bool · DefaultHidden : bool · DefaultOrder : int · DisabledHide : bool · Sorter : bool · Width : int · Field : EditableListFieldSchema (JSON editionField) · RenderConfig : RenderConfig
MediaiBox.Cms.FrontEnd.Model.UI.Form.FormFieldSchema (a form field)
Type : string (required; serializes as JSON variant) · Config : Dictionary<string, dynamic> (required) · Props : object (required) · Behaviors : object
Trap:
Type,Config,Propsare C#required— anynew FormFieldSchema { … }must set all three. There is noName/DataIndexon a form field (its identity is buried inConfig/Props), which is whyFormSchemaBuilderselects fields by predicate, not by name.
6. Availability matrix (at a glance)
| Capability | Seam | List | Related | Form | Ordered | ContentCriteria |
|---|---|---|---|---|---|---|
| Render data | TransformData |
✅ | ✅ | ✅ | ✅ | ✅ |
| Schema | ContributeSchema |
✅ | ✅ | ✅ | ✅ | ✅ |
| Visibility | ShouldHideComponent |
✅ | ✅ | ✅ | ✅ | ✅ |
| Permission | OnPermissionResolved |
✅ | ✅ | ✅ | ✅ | ✅ |
| Save · before persist | OnRelatedSaveReconciled / OnFormSaveBuilt / OnContentCriteriaSaveBuilt |
— | ✅ | ✅ | ✅ | ✅ |
| Save · after persist | OnAfterSaveResponseBuilt |
— | ✅ | ✅ | ✅ | ✅ |
| Delete · before persist | OnDeleteBuilt |
— | ✅ | ✅ | ✅ | ✅ |
| Delete · after persist | OnAfterDeleteResponseBuilt |
— | ✅ | ✅ | ✅ | ✅ |
| Fetch shaping | OnSearchCriteriaBuilt |
✅ | — | — | — | — |
| Data source (swap fetch) | ProvidesExternalData + OnFetchData |
✅ | ✅ | — | ✅ | — |
7. Copy-paste recipes
Complete, compiling overrides for the most common tasks. Each is the entire custom class — everything else is inherited. Pick one, rename, adjust. For larger, realistic components that combine several seams (with wiring, what the operator sees, and tests), see Samples & Use Cases.
Add a computed/derived column (value + the column that shows it):
public sealed class MovieListComponent : ListComponent
{
protected override void TransformData(ListV2ViewData data, ListViewData viewData)
{
foreach (var item in data.Items)
item.Fields["AVAILABILITY"] = Availability(item); // computed, not stored
}
protected override void ContributeSchema(ListSchema schema) =>
ListSchemaBuilder.For(schema).AddColumn("AVAILABILITY", "Availability");
}
Hide / reorder / relabel columns:
protected override void ContributeSchema(ListSchema schema) =>
ListSchemaBuilder.For(schema)
.Hide("INTERNAL_NOTES")
.Override("TITLE", c => c.Title = "Name")
.SetOrder("STATUS", 0);
Make a form field read-only (or change its variant):
protected override void ContributeSchema(FormSchema schema) =>
FormSchemaBuilder.For(schema)
.OverrideWhere(f => (f.Config.GetValueOrDefault("name") as string) == "SLUG",
f => f.Behaviors = new { readOnly = true });
Server-side filter + sort a list (List only):
protected override void OnSearchCriteriaBuilt(SearchCriteria criteria)
{
criteria.GlobalFilter = string.IsNullOrEmpty(criteria.GlobalFilter)
? "STATUS=ACTIVE" : $"({criteria.GlobalFilter}) AND STATUS=ACTIVE";
criteria.Order = "DATEINS,desc";
}
Hide a whole panel by entity state or a flag:
public override Task<bool> ShouldHideComponent(ComponentContext context, CancellationToken ct)
=> Task.FromResult(Configuration.GetFlag("PPV_ENABLED") == false);
Deny (or force) a permission for this component:
protected override void OnPermissionResolved(ComponentPermission permission,
List<ResponseItemContent> responses)
{
if (!Context.User.HasCapability("subscription.create"))
permission.Create = PermissionType.Deny; // Deny always wins; leave the rest as resolved
}
Block a save with a clean, localized message (before-persist; throwing is intended here):
protected override void OnFormSaveBuilt(PersistenceComponentSaveSingleEntityData saveData,
PostbackComponentRequest request)
{
if (string.IsNullOrWhiteSpace(saveData.Fields.GetValueOrDefault("TITLE") as string))
throw new PersistenceValidationException(TemplateComponentKey, "{DICT:ERROR/TITLE_REQUIRED}");
}
Dedupe / filter the reconciled relation set:
protected override void OnRelatedSaveReconciled(PersistenceComponentSaveRelatedEntitiesData saveData,
PostbackComponentRequest request)
{
var existing = GetExistingIds();
saveData.AddedRelateds.RemoveAll(existing.Contains); // drop already-related ids
}
Guarded post-save side effect (never fail a committed save for a hiccup):
protected override void OnAfterSaveResponseBuilt(PostbackComponentResponse response,
List<PersistenceComponentResult> results)
{
if (response.Status == PostbackStatus.Error) return; // don't act on a failed save
try { _kafka.PublishChanged(Context.IDs.First()); }
catch (Exception ex) { _log.Exception(ex); response.Status = PostbackStatus.SuccessWithWarning; }
}
Swap the data source to an external API (List — paging + structured filter):
public sealed class JobsListComponent : ListComponent
{
private readonly IJobsApi _jobs;
protected override bool ProvidesExternalData => true;
protected override void ContributeSchema(ListSchema schema) =>
ListSchemaBuilder.For(schema).AddColumn("ID", "Job").AddColumn("STATUS", "Status");
protected override async Task<ListV2ViewData> OnFetchData(FetchContext ctx, CancellationToken ct)
{
// ctx.FilterCondition carries Field+Operator+Value; ctx.Filter is the raw value.
var page = await _jobs.ListAsync(ctx.Page, ctx.Limit, ctx.Order, ctx.FilterCondition, ct);
return new ListV2ViewData(new ListViewData())
{
Total = page.Total,
Items = page.Items.Select(j => new ItemV2ViewData
{ Id = j.Id, Fields = new() { ["STATUS"] = j.Status } }).ToList()
};
}
}
External cleanup after a delete:
protected override void OnAfterDeleteResponseBuilt(PostbackComponentResponse response,
List<PersistenceComponentResult> results)
{
if (response.Status != PostbackStatus.Error) _blobStore.DeleteOrphans(Context.IDs);
}
Mask / redact a value before it reaches the client:
protected override void TransformData(ListV2ViewData data, ListViewData viewData)
{
foreach (var item in data.Items)
if (!Context.User.HasCapability("pii.view"))
item.Fields["EMAIL"] = "•••";
}
See the Building Blocks reference for the reusable helpers a seam body calls, and the Troubleshooting guide when an override doesn't behave as expected.