Page Persistence Plugin
Table of Contents
- Overview
- Key Concepts
- Getting Started
- Plugin Context Reference
- Return Values
- Working with Components
- Common Scenarios
- Complete Example
- Dependency Injection
- Best Practices
- Troubleshooting
- API Reference
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
- Build project in Release mode
- Copy compiled DLL to CMS bin folder
- 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()
Related List Component (Multiple Entities)
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:
- Check configuration field names (case-sensitive)
- Verify DLL in bin folder
- Restart application
- Check class is public
- 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