Table of Contents

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 which using to 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 (OnFetchData is 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>

  • ContentCriteriaV2GroupViewDataOperation : string · Conditions : IEnumerable<ContentCriteriaV2GroupFilterViewData>
  • ContentCriteriaV2GroupFilterViewDataFilterId : string · Value : object · Id : int
  • ContentCriteriaV2OrderViewDataPrimary : ContentCriteriaV2OrderOptionViewData · Secondary : … · MaxItemCount : int
  • ContentCriteriaV2OrderOptionViewData (a struct)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

ListSchema has no ParentField (that's on RelatedListSchema) and its title is Name, not Title.

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/Info have internal setters — 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

ContentCriteriaSchema has no Configuration and no ParentField.

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, Props are C# required — any new FormFieldSchema { … } must set all three. There is no Name/DataIndex on a form field (its identity is buried in Config/Props), which is why FormSchemaBuilder selects 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.