```json //[doc-seo] { "Description": "Add custom business logic to dynamic entity CRUD operations using Interceptors in the ABP Low-Code System. Validate, transform, and react to data changes with JavaScript." } ``` # Interceptors > **Preview:** Interceptors and their JavaScript context are preview extension points. Script context members, validation behavior, and lifecycle hooks may change before general availability. Use designer actions and descriptor metadata for standard low-code behavior first. Interceptors are an advanced extension point for adding JavaScript lifecycle logic when the generated CRUD flow needs validation, transformation, or replacement behavior. Interceptors allow you to run custom JavaScript code before, after, or instead of Create, Update, and Delete operations on dynamic entities. ## Interceptor Types | Command | Type | When Executed | |---------|------|---------------| | `Create` | `Pre` | Before entity creation — validation, default values | | `Create` | `Post` | After entity creation — notifications, related data | | `Create` | `Replace` | Instead of entity creation — **must return the new entity's Id** (see below) | | `Update` | `Pre` | Before entity update — validation, authorization | | `Update` | `Post` | After entity update — sync, notifications | | `Update` | `Replace` | Instead of entity update — no return value needed | | `Delete` | `Pre` | Before entity deletion — dependency checks | | `Delete` | `Post` | After entity deletion — cleanup | | `Delete` | `Replace` | Instead of entity deletion — no return value needed | ## Defining Interceptors in JSON Descriptors The designer stores entity interceptors in the entity `interceptors` array: ```json { "name": "LowCodeDemo.Customers.Customer", "interceptors": [ { "commandName": "Create", "type": "Pre", "javascript": "if (!args.getValue('Name')) {\n globalError = 'Name is required.';\n}" } ] } ``` ### Interceptor Descriptor | Field | Type | Description | |-------|------|-------------| | `commandName` | string | `"Create"`, `"Update"`, or `"Delete"` | | `type` | string | `"Pre"`, `"Post"`, or `"Replace"` | | `javascript` | string | JavaScript code to execute | ## Defining Interceptors with Attributes Use the `[DynamicEntityCommandInterceptor]` attribute on a C# class: ````csharp [DynamicEntity] [DynamicEntityCommandInterceptor( "Create", InterceptorType.Pre, "if(!context.commandArgs.data['Name']) { globalError = 'Name is required!'; }" )] [DynamicEntityCommandInterceptor( "Create", InterceptorType.Post, "context.log('Entity created: ' + context.commandArgs.entityId);" )] public class Organization { public string Name { get; set; } } ```` The `Name` parameter must be one of: `"Create"`, `"Update"`, or `"Delete"`. The `InterceptorType` can be `Pre`, `Post`, or `Replace`. When `Replace` is used, the default database operation is completely skipped and only your JavaScript handler executes. Multiple interceptors can be added to the same class (`AllowMultiple = true`). ## Defining Interceptors with Fluent API Use the `Interceptors` list on an `EntityDescriptor` to add interceptors programmatically in startup configuration: ````csharp AbpDynamicEntityConfig.EntityConfigurations.Configure( "MyApp.Organizations.Organization", entity => { entity.Interceptors.Add(new CommandInterceptorDescriptor("Create") { Type = InterceptorType.Pre, Javascript = "if(!context.commandArgs.data['Name']) { globalError = 'Name is required!'; }" }); entity.Interceptors.Add(new CommandInterceptorDescriptor("Delete") { Type = InterceptorType.Post, Javascript = "context.log('Deleted: ' + context.commandArgs.entityId);" }); } ); ```` See [Attributes & Fluent API](fluent-api.md) for more details on Fluent API configuration. ## JavaScript Context Inside interceptor scripts, you have access to: ### `context.commandArgs` | Property / Method | Type | Description | |----------|------|-------------| | `data` | object | Entity data dictionary (for Create/Update) | | `entityId` | string | Entity ID (for Update/Delete) | | `commandName` | string | Command name (`"Create"`, `"Update"`, or `"Delete"`) | | `entityName` | string | Full entity name | | `getValue(name)` | function | Get a property value | | `setValue(name, value)` | function | Set a property value (Pre-interceptors only) | | `hasValue(name)` | function | Check if a property exists in the data | | `removeValue(name)` | function | Remove a property from the data | ### `context.currentUser` | Property / Method | Type | Description | |----------|------|-------------| | `isAuthenticated` | bool | Whether user is logged in | | `id` | string | User ID | | `userName` | string | Username | | `email` | string | Email address | | `name` | string | First name | | `surName` | string | Last name | | `phoneNumber` | string | Phone number | | `phoneNumberVerified` | bool | Whether phone is verified | | `emailVerified` | bool | Whether email is verified | | `tenantId` | string | Tenant ID (for multi-tenant apps) | | `roles` | string[] | User's role names | | `isInRole(roleName)` | function | Check if user has a specific role | ### `context.emailSender` | Property / Method | Description | |--------|-------------| | `isAvailable` | Whether the email sender is configured and available | | `sendAsync(to, subject, body)` | Send a plain-text email | | `sendAsync(from, to, subject, body)` | Send a plain-text email with an explicit sender | | `sendHtmlAsync(to, subject, htmlBody)` | Send an HTML email | | `sendHtmlAsync(from, to, subject, htmlBody)` | Send an HTML email with an explicit sender | | `queueAsync(to, subject, body)` | Queue a plain-text email | | `queueAsync(from, to, subject, body)` | Queue a plain-text email with an explicit sender | | `queueHtmlAsync(to, subject, htmlBody)` | Queue an HTML email | | `queueHtmlAsync(from, to, subject, htmlBody)` | Queue an HTML email with an explicit sender | ### Logging | Method | Description | |--------|-------------| | `context.log(message)` | Log an informational message | | `context.logWarning(message)` | Log a warning message | | `context.logError(message)` | Log an error message | > Use these methods instead of `console.log` (which is blocked in the sandbox). ### `db` (Database API) Full access to the [Scripting API](scripting-api.md) for querying and mutating data. Interceptors can also use common scripting services such as `user`, `tenant`, `auth`, `settings`, `features`, `config`, `http`, `events`, `jobs`, `blob`, `encryption`, `textTemplating`, `files`, `images`, and `attachments` when they are enabled by the scripting capability profile. ### `globalError` Set this variable to a string to **abort** the operation and return an error: ```javascript globalError = 'Cannot delete this entity!'; ``` ![Interceptor validation error displayed in the UI](images/interceptor-error.png) ## Examples ### Pre-Create: Validation ```json { "commandName": "Create", "type": "Pre", "javascript": "if(!context.commandArgs.data['Name']) {\n globalError = 'Organization name is required!';\n}" } ``` ### Post-Create: Email Notification ```json { "commandName": "Create", "type": "Post", "javascript": "if(context.currentUser.isAuthenticated && context.emailSender) {\n await context.emailSender.sendAsync(\n context.currentUser.email,\n 'New Order Created',\n 'Order total: $' + context.commandArgs.data['TotalAmount']\n );\n}" } ``` ### Pre-Update: Role-Based Authorization ```json { "commandName": "Update", "type": "Pre", "javascript": "if(context.commandArgs.data['IsDelivered']) {\n if(!context.currentUser.roles.includes('admin')) {\n globalError = 'Only administrators can mark orders as delivered!';\n }\n}" } ``` ### Pre-Delete: Business Rule Check ```json { "commandName": "Delete", "type": "Pre", "javascript": "var project = await db.get('LowCodeDemo.Projects.Project', context.commandArgs.entityId);\nif(project.Budget > 100000) {\n globalError = 'Cannot delete high-budget projects!';\n}" } ``` ### Pre-Update: Negative Value Check ```json { "commandName": "Update", "type": "Pre", "javascript": "if(context.commandArgs.data['Quantity'] < 0) {\n globalError = 'Quantity cannot be negative!';\n}" } ``` ### Replace-Create: Custom Insert Logic When you need to completely replace the default create operation with custom logic: ```json { "commandName": "Create", "type": "Replace", "javascript": "var data = context.commandArgs.data;\ndata['Code'] = 'PRD-' + Date.now();\nvar result = await db.insert('LowCodeDemo.Products.Product', data);\ncontext.log('Product created with custom code: ' + data['Code']);\nreturn result.Id;" } ``` > **Important:** `Replace-Create` interceptors **must** return the new entity's `Id` (Guid). The system uses this value to fetch and return the created entity. Use `return result.Id;` after `db.insert(...)`. > > `Replace-Update` and `Replace-Delete` interceptors do not need to return a value. ### Pre-Update: Self-Reference Check ```json { "commandName": "Update", "type": "Pre", "javascript": "if(context.commandArgs.data.ParentCategoryId === context.commandArgs.entityId) {\n globalError = 'A category cannot be its own parent!';\n}" } ``` ## See Also * [Scripting API](scripting-api.md) * [Model Descriptor Files](model-json.md) * [Custom Endpoints](custom-endpoints.md)