Table of Contents

Page Persistence Plugin

Table of Contents


Overview

The Page Persistence Plugin allows you to intercept and modify page persistence operations (save and delete) in the MediaiBox CMS. This powerful extensibility point enables you to:

  • Validate data before saving or deleting
  • Add metadata or audit fields automatically
  • Cancel operations based on business rules
  • Execute post-processing after successful operations
  • Implement cross-component validations
  • Trigger external integrations or notifications

Key Concepts

Plugin Lifecycle

The plugin provides 4 execution hooks:

Hook When Can Cancel Data Modifiable
BeforeSave Before data is persisted Yes Yes
AfterSave After successful save No No (read-only)
BeforeDelete Before data is deleted Yes Limited
AfterDelete After successful delete No No (read-only)

Execution Flow:

  • User submits form
  • BeforeSave Plugin (if validation fails, return Error)
  • Persist to Database
  • AfterSave Plugin
  • Return response to user

Plugin Scope

  • One plugin per page - Each page can have its own plugin configured
  • Access to all components - Plugin receives data from all components on the page
  • Shared data - Communication between Before and After hooks via SharedData dictionary
  • Request-scoped - Plugin state is not preserved between different user requests

Getting Started

Step 1: Create the Plugin Class

Install NuGet package: MediaiBox.Cms.FrontEnd.Model

Create implementation of IPagePersistencePlugin with constructor accepting IMibLog and IMibApiClientReadOnlyWrapper.

Implement all 4 methods: ExecuteBeforeSave, ExecuteAfterSave, ExecuteBeforeDelete, ExecuteAfterDelete.

Step 2: Build and Deploy

  1. Build project in Release mode
  2. Copy compiled DLL to CMS bin folder
  3. Restart application

Step 3: Configure the Plugin

In CMS Admin > Pages, add configuration:

  • PagePersistencePlugin_AssemblyName: Your assembly name
  • PagePersistencePlugin_ClassName: Fully qualified class name (case-sensitive)

Plugin Context Reference

BeforeSavePagePersistenceContext

Properties:

  • PageKey (string, read-only) - Page identifier
  • Context (ContextViewData, read-only) - User info, permissions
  • ComponentsData (IDictionary, modifiable) - Data to be saved by TemplateComponentKey
  • SharedData (IDictionary, modifiable) - Communication with AfterSave

Use for: Validation, data modification, adding audit fields

AfterSavePagePersistenceContext

Properties:

  • PageKey (string, read-only)
  • Context (ContextViewData, read-only)
  • ComponentsData (IReadOnlyDictionary) - Data that was saved
  • ComponentsResults (IReadOnlyDictionary) - Save operation results with ResponseIdentifier and Status
  • SharedData (IDictionary) - Data from BeforeSave

Use for: Notifications, logging, external integrations

BeforeDeletePagePersistenceContext

Properties:

  • PageKey (string, read-only)
  • Context (ContextViewData, read-only)
  • ComponentsData (IDictionary) - Data to be deleted with MediaType and IDs
  • SharedData (IDictionary) - Communication with AfterDelete

Use for: Validation, dependency checks

AfterDeletePagePersistenceContext

Properties:

  • PageKey (string, read-only)
  • Context (ContextViewData, read-only)
  • ComponentsData (IReadOnlyDictionary) - Data that was deleted
  • ComponentsResults (IReadOnlyDictionary) - Delete operation results
  • SharedData (IDictionary) - Data from BeforeDelete

Use for: Audit logging, cleanup


Return Values

PagePersistencePluginResult.Ok() - Operation succeeded, continue

PagePersistencePluginResult.Error(message) - Cancel operation, show error to user (only effective in BeforeSave/BeforeDelete)

PagePersistencePluginResult.CannotHandle() - Plugin cannot handle

Important: Before hooks can cancel operations. After hooks errors are logged only and do NOT rollback.


Working with Components

Form Component (Single Entity)

Cast to PersistenceComponentSaveSingleEntityData.

Access fields: data.Fields["field_name"] Modify fields: data.Fields["new_field"] = "value" Check exists: data.Fields.ContainsKey("field_name") Get entity ID: data.IDs?.FirstOrDefault()

Cast to PersistenceComponentSaveRelatedEntitiesData.

Access: CurrentItems, NewEntities, UpdatedEntities, RemovedIDs Iterate items and access item.Fields dictionary


Common Scenarios

Scenario 1: Field Validation

In ExecuteBeforeSave, check field values and return Error if invalid.

Example: Validate email contains @ symbol, age is >= 18

Scenario 2: Adding Audit Fields

In ExecuteBeforeSave, add created_by, created_at, modified_by, modified_at fields automatically.

Detect new records by checking if IDs is empty.

Scenario 3: Cross-Component Validation

Access multiple components from ComponentsData dictionary.

Example: Validate invoice total matches sum of line items

Scenario 4: Post-Save Notification

In ExecuteAfterSave, check ComponentsResults for successful operations.

Detect new vs update by checking original IDs in ComponentsData.

Send emails, webhooks, or trigger external systems.

Scenario 5: Prevent Deletion with Dependencies

In ExecuteBeforeDelete, use IMibApiClientReadOnlyWrapper to check dependencies.

Example: Prevent deleting category if it has products, prevent deleting customer with active orders

Scenario 6: Using SharedData

Store data in BeforeSave: context.SharedData["key"] = value

Retrieve in AfterSave: context.SharedData.TryGetValue("key", out var value)

Use for: Calculated values, flags, timestamps, processing duration


Complete Example

See full OrderManagementPlugin implementation that:

  • Validates status transitions (shipped requires tracking_number)
  • Calculates order total from line items
  • Adds audit fields (last_modified_by, last_modified_at)
  • Sends notifications for new orders
  • Prevents deleting non-draft orders
  • Checks for shipments before deletion

Dependency Injection

Available services:

  • IMibLog - Logging with Info, Error, Verbose methods
  • IMibApiClientReadOnlyWrapper - Query database with GetItemByIdAsync, GetItemsAsync

Constructor injection example: public MyPlugin(IMibLog log, IMibApiClientReadOnlyWrapper apiClient)

API Client usage:

  • Get item: await apiClient.GetItemByIdAsync(mediaType, id, cancellationToken)
  • Query items: await apiClient.GetItemsAsync(mediaType, filter, null, cancellationToken)
  • Check exists: result.Data?.Any()

Best Practices

Do's:

  • Use BeforeSave for validation
  • Use AfterSave for side effects
  • Return descriptive errors
  • Log operations
  • Use SharedData for communication
  • Check null values
  • Handle CancellationToken
  • Keep plugins focused
  • Test thoroughly

Don'ts:

  • Long operations in Before hooks (keep under 1 second)
  • Modify data in After hooks
  • Throw exceptions (use Error result)
  • Store state between requests
  • Ignore CancellationToken
  • Assume components exist
  • Hardcode values

Performance:

  • Cache lookups in SharedData
  • Minimize queries
  • Use indexed fields
  • Avoid N+1 queries
  • Target under 500ms for Before hooks

Troubleshooting

Plugin Not Executing:

  1. Check configuration field names (case-sensitive)
  2. Verify DLL in bin folder
  3. Restart application
  4. Check class is public
  5. Review logs for "No PagePlugin configured"

Dependency Injection Error:

  • Only use IMibLog and IMibApiClientReadOnlyWrapper

Data Not Modified:

  • Verify TemplateComponentKey
  • Check cast type (SingleEntity vs RelatedEntities)
  • Ensure modifying in Before hook not After

Plugin Works in Dev Not Prod:

  • Deploy DLL to production
  • Update configuration in prod database
  • Restart production application
  • Check .NET version match

API Reference

IPagePersistencePlugin interface with 4 methods.

Context types: PagePersistenceContextBase, BeforeSavePagePersistenceContext, AfterSavePagePersistenceContext, BeforeDeletePagePersistenceContext, AfterDeletePagePersistenceContext

PagePersistencePluginResult with Ok(), Error(message), CannotHandle() methods.

Data types: PersistenceComponentSaveSingleEntityData, PersistenceComponentSaveRelatedEntitiesData, IPersistenceDeleteData, PersistenceComponentResult